原创 | codeforces 1419D2,有趣的思维题
点击上方蓝字,关注并星标,和我一起学技术。
大家好,欢迎来到周末算法专题。
今天选择的题目是codeforces contest 1419的D2题,难度也不算很大,也是典型的思维题。如果大家跟过几篇文章,相信应该对codeforces的风格也比较熟悉了,以思维题为主,算法为辅。我个人比较喜欢这样的风格,做不出来题目一定不是算法不会,而是思维能力不够没想到解法,我个人觉得比较锻炼人,非常适合用来面试之前的思维训练。
题目链接:https://codeforces.com/contest/1419/problem/D2
多说一句一般来说一场比赛当中题目都是一个字母,怎么今天这道题是D2呢?这是因为这道题出题人给了两个版本,一个Easy版和一个Hard版。
废话不多说,我们来看题目。
题意
有一个叫Sage的妹子过生日,她准备买一点冰激凌吃,但是她有一个奇怪的要求就是只买相对便宜的冰激凌。如果某一个冰激凌比它相邻的冰激凌都要便宜,那么这个冰激凌就是相对便宜的。
冰激凌店有一排n个冰激凌等待售卖,这n个冰激凌当中最左和最右两个冰激凌只有一个相邻的冰激凌,所以都不是相对便宜的。现在你想要给Sage一个惊喜,准备在她去冰激凌店之前先给这些冰激凌排个序,好让她可以买到尽量多的冰激凌。请问最多可以买到多少个冰激凌,并且给出冰激凌的排列方式。
题目的输入数据有两行,第一行是一个整数n表示冰激凌的数量(
)。
第二行一共有n个数,
,表示这n个冰激凌的价格,其中
。要求输出也是两行,第一行是Sage最多购买的冰激凌的数量,第二行是冰激凌的排列情况。
样例
在这样的排序下,Sage可以买到价格为1,2,2的三个冰激凌。
题解
在分析问题之前,我们先来说说这道题Easy版本和Hard版本的区别,区别只有一个地方。就是在Easy版本当中,所有冰激凌的价格均不相等,而在Hard版当中去掉了这个限制。
所以我们可以先来思考一下简单版本的题目,再来进阶到Hard版。
简单版本
我们很容易发现一个结论:在这n个冰激凌当中,我们最多购买其中n/2个。原因很简单,我们就只看其中的a1,a2,a3这三个冰激凌。我们先假设a2是低价冰激凌,那么需要满足a2 < a1, a2 < a3。那么对于a1和a3来说都一定不可能是低价的,因为它们已经比相邻的大了。根据这个结论出发,我们可以得到最好的情况应该是在这n个冰激凌当中,每隔一个出现一个低价冰激凌。
也就是样例的[3 1 4 2 4 2 5]的状态,想要达成这样的效果,也不难,我们只需要把所有的冰激凌的价格从小到大排序。然后把数组分为两半,分为价格高的一半和价格低的一半,最后把它们交替摆放,就可以收获n/2个低价冰激凌。
实际上这也的确可以AC,我们直接附上代码:
n = int(input())
spheres = list(map(int, input().split(' ')))
spheres = sorted(spheres)
ret = [-1 for _ in range(n)]
# 两个指针,一个获取低价一个获取高价
l, r = 0, n-1
cnt = 0
for i in range(n):
if i % 2 == 0:
ret[i] = spheres[r]
r -= 1
else:
ret[i] = spheres[l]
l += 1
# 如果a[i-1]的价格低于相邻的两个,那么答案++
if i > 1 and ret[i-1] < ret[i-2] and ret[i-1] < ret[i]:
cnt += 1
print(cnt)
print(' '.join(list(map(str, ret))))
困难版本
那么简单版本的解法能不能AC困难版本呢?当然是不行的,我找来了错误样例。
也就是对于1 2 2 4 5这样的样例就无法满足,无法满足的原因也很简单,因为出现了重复的2。按照我们刚才的做法,导致4 2 2无法构成低价。
我们分析一下出现这种状况的原因,按照我们刚才的做法,想要把一半的冰激凌做成低价,我们把这些冰激凌称为“夹心”冰激凌,还有一半的高价冰激凌想要用来分开“夹心”,我们把它们称为挡板冰激凌。我们刚才是一个夹心、一个挡板这样交错放置的。由于在Easy版本当中所有冰激凌的价格都不相同,所以不会出现挡板和夹心相等的情况。但是在Hard版本当中这会出现,这会导致很大的问题。
会出现挡板和夹心相等的原因也很简单,因为我们夹心是从小到大放的,而挡板是从大到小放的,如果我们倒过来,把夹心也从大到小放,是不是就可以尽量让夹心都碰到比它大的挡板,这样尽可能地构成答案呢?这也是贪心法的思路,从思路推导上来说,这个想法应该是正确的。
但是依然不能AC,不能AC的原因也很简单,我们来看这么一个case:
4
1 4 4 4
我们按照刚才的做法很容易发现挡板应该是[4, 4],夹心应该是[4, 1],我们按照刚才的策略得到的结果是[4 4 4 1]。因为我们把大的夹心4放在了前面,而把小的夹心1放在了最后,导致了1不能构成低价冰激凌,浪费了一个机会。
同样的例子可以举出来很多,比如[4 2 1 1]。
我们很难办,如果我们把大夹心放在后面可能会导致相等答案不对, 如果我们把大夹心放在前面又会导致最后一个最小的夹心被浪费。我们到底应该怎么办呢?
很多人想到这里就被困住了,左右为难,然后可能会质疑自己的思路是不是一开始就错了,这个解法是不是就不可行?这种情况是很多人都会遇到的,这种情况除了需要经验之外,最重要的就是冷静,不能慌,先冷静下来仔细想想,会导致这样错误的根本原因究竟是什么,是我们的算法有问题呢,还是有特殊情况?
冷静下来一想就发现了,之所以会出现这样的情况,其实很简单,只有一个原因就是我们的n是偶数,导致了原本最后应该构成答案的夹心被浪费了。所以我们需要调整的不是算法,而是特殊处理n是偶数的情况。我们特殊处理n的奇偶性不是拍脑袋来的,而是有这么一个思考链的。如果大家直接看题解要判断n的奇偶性,有时候是很难get到这当中的思考过程的,这样下次遇到了类似的问题也未必能想起来要判断奇偶性。
n是奇数的时候很好办,刚好挡板可以比夹心多一个,可以保证所有的夹心都有两块挡板。但当n是偶数的时候,就会导致有一个冰激凌是浪费的,它起不到任何用处。我们去掉了这一个浪费的,剩下来的就是n是奇数的局面。也就是说我们要首先选一个冰激凌出来丢到一旁,让剩下的局面变成奇数,再来用上面的方法。
所以关键的点就锁定在了我们怎么选择这个牺牲的冰激凌呢?
我们一点一点来思考,首先我们应该选夹心呢还是选挡板呢?这个不难回答,应该选夹心。当n是偶数的时候,答案最大是
,因为只有n/2个挡板,所以最多只能给出
个夹心。也就是说至少有一个夹心一定是用不到的,那么显然我们应该选一个夹心来牺牲。下一个问题是我们应该选哪一个夹心呢?
这个问题也不难回答,应该选最大的那个夹心,因为它最有可能不是低价。到这里,我们整个思路已经很清楚了,首先对所有冰激凌的价格进行排序,然后分成两个部分,一个部分作为夹心,一个部分作为挡板。接着根据n的奇偶性选择放置的方法。如果n是奇数则大小交错放置,否则先放置最大的夹心之后,再交错放置。
我们来看代码,获取一下所有的细节:
n = int(input())
spheres = list(map(int, input().split(' ')))
spheres = sorted(spheres)
ret = [-1 for _ in range(n)]
cnt = 0
block_n = n // 2 + (n % 2)
inner_n = n - block_n
# inners即夹心,blocks即挡板
inners = spheres[: inner_n]
blocks = spheres[inner_n: n]
st = 0
# 如果n是偶数,先把最大的夹心放在第一位
if n % 2 == 0:
ret[0] = inners.pop()
st = 1
# 接着夹心和挡板轮流放置
for i in range(st, n):
if i % 2 == st:
ret[i] = blocks.pop()
else:
ret[i] = inners.pop()
if i > 1 and ret[i-1] < ret[i-2] and ret[i-1] < ret[i]:
cnt += 1
print(cnt)
print(' '.join(list(map(str, ret))))
到这里我们这道题就算是解开了,这道题不算很难,也没有太多的弯弯绕,就是需要我们冷静下来仔细把这些变量之间的关系以及出现的问题思考清楚。这也是我们算法能力当中很大的一部分,比如面试的时候当我们遇到问题卡住的时候,就非常需要这样固定的思维模式。希望大家好好体会一下这道题的思考过程,领会这种分析问题解决问题的思考过程。
今天的文章就到这里,衷心祝愿大家每天都有所收获。如果还喜欢今天的内容的话,请来一个三连支持吧~(点赞、在看、转发)
- END -
- 1589: [Usaco2008 Dec]Trick or Treat on the Farm 采集糖果
- 1647: [Usaco2007 Open]Fliptile 翻格子游戏
- 1295: [SCOI2009]最长距离
- 1644: [Usaco2007 Oct]Obstacle Course 障碍训练课
- 数据结构之哈夫曼树和编码器的构造
- 1578: [Usaco2009 Feb]Stock Market 股票市场
- webp图片实践之路
- 3522: [Poi2014]Hotel
- 3299: [USACO2011 Open]Corn Maze玉米迷宫
- 2272: [Usaco2011 Feb]Cowlphabet 奶牛文字
- 1632: [Usaco2007 Feb]Lilypad Pond
- 1630/2023: [Usaco2005 Nov]Ant Counting 数蚂蚁
- Java设计模式(七)Decorate装饰器模式
- 1623: [Usaco2008 Open]Cow Cars 奶牛飞车
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- Thanos 与 VictoriaMetrics,谁才是打造大型 Prometheus 监控系统的王者?
- 阅读大型开源软件的四个技巧
- Tomcat NIO(7)-Poller
- python 求解线性规划问题
- Netty高性能之道
- 关于im-live-sells自定义群组字段的使用
- 浅谈CAP与Kafka
- windows下查看进程(进阶)
- Salesforce LWC学习(二十二) 简单知识总结篇二
- 初识Netty
- SpringCloud微服务项目实战 - 缓存详解及高效缓存接入
- Vue + Flask 实战开发系列(十)
- 【CS学习笔记】17、登录验证的难点
- CS学习笔记 | 18、密码哈希散列设置信任
- CS学习笔记 | 19、代码执行的方式