
题目考点
这道题主要考的是:
- 双指针
- 贪心思想
- 列表两端取值
- 审题后把“文字规则”翻译成“序列单调性”
这题表面上像“堆方块”,本质上其实是在问:
能不能从数组两端依次取数,取出的序列始终保持“不变大”。
也就是:后拿出来放上去的方块,边长不能比下面那个更大。
审题
输入是什么
有 T 组测试数据。
每组数据里:
- 第一行是方块数量 n
- 第二行是 n 个整数,表示一排方块的边长
输出是什么
对于每组数据:
- 如果能按题目要求堆起来,输出
Yes - 否则输出
No
题目真正要求我们判断什么
每次只能拿:
- 最左边的方块
- 或最右边的方块
而且新堆出来的竖直柱子要满足:
- 上面的方块不能比下面的大
换句话说,如果我们把“拿方块的顺序”记下来,这个顺序必须是一个**从大到小(允许相等)**的序列。
容易忽略的点
这题最容易卡住的地方有两个:
第一,题目不是让你排序
你不能随便重排,只能从两端拿。
第二,不是去模拟“整个柱子长什么样”
你只需要保证:
当前拿出来的这个方块,不能比前一个已经放好的方块更大。
所以我们只需要记住“上一个放上去的方块大小”。
思路提示
先不要急着看代码,先抓住这题最核心的一句话:
每一步都应该优先拿两端中更大的那个。
为什么?
因为你要让整个取出的序列保持“不变大”。
假设两端分别是 4 和 7:
- 如果你先拿 4
- 那么 7 以后就只能放在 4 上面
- 但 7 > 4,肯定不行
所以更安全的做法是:
- 先拿大的
- 小的留到后面
这就是这道题的贪心核心。
你可以先自己记住这个判断模板:
- 看左端和右端
- 取较大的那个
- 判断它是否还能放在当前堆顶上
- 如果能,继续
- 如果不能,直接
No
完整设计思路
我们把“已经堆好的最上面那个方块”记成 top。
一开始还没堆,所以 top 可以看成无穷大。
也就是第一个方块无论多大都能放。
然后不断做下面这件事:
第一步:看两端哪个更大
设:
- 左端是
blocks[left] - 右端是
blocks[right]
因为我们最后想形成一个不递增序列,所以优先选较大的那个:
- 如果左边 >= 右边,就选左边
- 否则选右边
第二步:判断能不能放
如果选出来的这个值 candidate 满足:
candidate <= top
说明它可以放到当前堆顶上面。
那么:
- 更新
top = candidate - 对应那一侧指针向中间移动
第三步:一旦出现 candidate > top
说明你现在无论怎么选,已经破坏规则了,直接输出 No。
第四步:如果所有方块都处理完了
说明整个过程一直合法,输出 Yes。
为什么这个贪心是对的
这里一定要把道理想透。
我们每一步都在两端里选一个。
假设当前两端是 a 和 b,并且 a < b。
如果你现在选了较小的 a,那么以后 b 还在那里。
可因为 b > a,它以后要放到 a 上面,就不合法了。
所以:
- 小的可以放在大的上面
- 大的不能放在小的上面
因此两端都可选时,优先选更大的那个,才是更稳妥的。
这就是这题的贪心依据。
代码实现
下面给你一个适合初学者理解的写法,用双指针,不用真的从列表头部删除元素,因为那样效率低。
t = int(input())
for _ in range(t):
n = int(input())
blocks = list(map(int, input().split()))
left = 0
right = n - 1
top = float('inf') # 当前堆顶,开始时设为无穷大
possible = True
while left <= right:
if blocks[left] >= blocks[right]:
candidate = blocks[left]
left += 1
else:
candidate = blocks[right]
right -= 1
if candidate <= top:
top = candidate
else:
possible = False
break
if possible:
print("Yes")
else:
print("No")
运行演示
样例 1
输入:
blocks = [4, 3, 2, 1, 3, 4]
初始:
- left 指向 4
- right 指向 4
- top = 无穷大
第一次
两端是 4 和 4,任选较大,取左边 4
- 4 <= inf,可以放
- top = 4
第二次
两端是 3 和 4,取 4
- 4 <= 4,可以放
- top = 4
第三次
两端是 3 和 3,取 3
- 3 <= 4,可以放
- top = 3
第四次
两端是 2 和 3,取 3
- 3 <= 3,可以放
- top = 3
第五次
两端是 2 和 1,取 2
- 2 <= 3,可以放
- top = 2
第六次
只剩 1
- 1 <= 2,可以放
全部成功,所以输出:
Yes
样例 2
输入:
blocks = [1, 3, 2]
初始:
- 左端 1
- 右端 2
- top = inf
第一次
取较大的一端 2
- 2 <= inf,可以放
- top = 2
第二次
现在两端是 1 和 3,取较大的一端 3
但:
- 3 > 2
说明 3 不能放到 2 上面,失败。
所以输出:
No
方法总结
这类题以后你可以这样识别:
一看到这些特征,就要想到“双指针 + 贪心”
题目如果同时出现下面几个关键词:
- 只能从两端取
- 每次取一个
- 要满足某种大小顺序
- 问能不能做到
那么大概率就是这类题。
这类题的通用思考套路
先把题目翻译成“序列规则”:
- 这里的规则就是:取出的顺序必须不递增
然后再想:
- 每次只能从两端选
- 为了不让后面更难放,应该优先拿较大的那个
最后用双指针模拟整个过程。
这题最关键的不是代码,而是这句转换
“堆起来合法” = “取出来的序列不能变大”。
一旦你把这句话想明白,代码其实很直。
易错点提醒
1. 想去排序
不能排序。
因为题目限制了只能从两端拿。
2. 用 pop(0)
很多初学者会写:
blocks.pop(0)
这个在 Python 里效率很低,因为前面的元素删掉后,后面都要整体前移。
这题数据很大,最好用双指针:
leftright
3. 不是选较小的那个
有些同学会想:“上面要小,那我每次拿小的不就好了?”
这是错的。
因为小的放上去以后,大的可能就再也放不上去了。
所以应该优先拿两端中较大的那个。
本节小结
这题本质不是“堆方块模拟”,而是一个很经典的贪心判断题。
核心只有三步:
- 维护当前堆顶
top - 每次从两端选较大的那个
- 判断它是否
<= top
能一直放下去就是 Yes,中途有一步放不下就是 No。
练习
你先自己试试这道同类型小练习,不要急着看答案。
练习题
判断下面这组方块能否成功堆起来:
blocks = [5, 4, 3, 2, 1, 6]
输出应该是 Yes 还是 No?
提示
按这题的方法来:
- 两端比较
- 每次取较大的那个
- 记录当前堆顶
- 看会不会出现“新拿出来的方块比堆顶还大”
你先自己手动模拟一遍。
while left <= right:是什么意思呢?为什么不能直接if blocks[left] >= blocks[right]:?
这两个东西根本不是同一个层面的作用
你问得非常关键。很多初学者看到这里,都会把这两句混在一起:
while left <= right:
和
if blocks[left] >= blocks[right]:
它们看起来都在“判断”,但其实职责完全不同。
可以先直接记住一句话:
while left <= right:是在判断:还有没有方块没处理完if blocks[left] >= blocks[right]:是在判断:这一轮该拿左边还是右边
也就是说:
while 管整个过程要不要继续,if 管这一轮具体怎么选。
先讲 while left <= right 到底是什么意思
left 和 right 表示什么
我们用两个指针:
left指向最左边还没拿的方块right指向最右边还没拿的方块
比如:
blocks = [4, 3, 2, 1, 3, 4]
一开始:
left = 0right = 5
表示当前还没处理的范围是整个列表。
left <= right 表示“还有东西没处理完”
这个条件要分三种情况看:
1. left < right
说明左右之间还有至少两个方块没处理。
例如:
left = 1right = 4
表示下标 1 到 4 这段还没处理完。
2. left == right
说明只剩下一个方块还没处理。
例如:
left = 2right = 2
这时候中间只剩一个方块,也还是要处理的。
所以这里必须允许 =。
3. left > right
说明已经没有方块了,全部处理结束。
例如:
left = 3right = 2
这时候循环就该停了。
为什么不能只有一句 if blocks[left] >= blocks[right]:
因为这句 if 只会判断 一次。
而这道题不是只选一次方块,而是要:
- 选第 1 个
- 选第 2 个
- 选第 3 个
- …
- 一直选到所有方块都处理完
所以这里必须有一个“重复执行”的结构。
而 if 不是循环,它只是一次分支判断。
你可以把它理解成下面这个流程
这道题其实是在不断重复做同一件事:
每一轮都要做两步
第一步:看两端谁更大
第二步:拿走那个,并更新指针
然后再进入下一轮,继续比较新的两端。
所以逻辑其实是这样:
只要还有方块没处理:
比较左端和右端
选较大的那个
判断能不能放
更新 left 或 right
这就是为什么需要:
while left <= right:
然后在循环内部再写:
if blocks[left] >= blocks[right]:
while 和 if 在这题里的分工
while 的作用:控制“重复”
它解决的是:
这件事要做多少轮?
答案是:
只要还有没处理的方块,就继续做。
if 的作用:控制“选择”
它解决的是:
这一轮,到底选左边还是右边?
答案是:
比较左右两端,选更大的那个。
一个很形象的类比
你可以把这题想成“每轮都要做一道选择题”。
while 像老师说:
“题还没做完,就继续做下一题。”
if 像每一题里的选项判断:
“这一题该选 A 还是选 B?”
所以:
while是“做几题”if是“这一题选什么”
两者不是互相替代的关系。
用一个具体例子看就更清楚了
假设:
blocks = [4, 3, 2, 1, 3, 4]
如果你只写一次 if
比如你这样想:
if blocks[left] >= blocks[right]:
...
else:
...
那么程序只会比较最开始的:
- 左端 4
- 右端 4
选完一次之后,就结束了。
可这题明明还有后面的 3、2、1、3、4 没处理。
所以只写 if 根本不够。
正确做法是每轮都比较一次
第 1 轮
两端:4 和 4
选一个 4
第 2 轮
新的两端:3 和 4
再比较一次
选 4
第 3 轮
新的两端:3 和 3
再比较一次
选 3
第 4 轮
新的两端:2 和 3
再比较一次
选 3
……
你会发现:
“比较两端”这件事不是做一次,而是要反复做很多次。
所以必须用 while 包起来。
为什么条件是 <=,而不是 <
这也是一个特别值得问的问题。
如果写成:
while left < right:
那就表示只有左右两边还剩至少两个元素时才继续。
可如果最后只剩一个元素,比如:
left = 2right = 2
这个元素也应该处理。
所以必须写成:
while left <= right:
这样最后那个单独剩下的方块也会被处理到。
你可以把这句翻译成人话
while left <= right:
最适合初学者的翻译就是:
“只要左指针还没有越过右指针,说明还有方块没拿完,就继续处理。”
这是最本质的意思。
再把完整关系压缩成一句最核心的话
这题里:
while left <= right:
表示:
只要还有未处理区间,就继续一轮一轮地选方块。
而循环里面的:
if blocks[left] >= blocks[right]:
表示:
在这一轮里,决定选左边还是右边。
一个很简短的伪代码版
你可以把它记成这个模板:
left = 0
right = n - 1
while left <= right: # 还有没处理的方块
if blocks[left] >= blocks[right]:
选左边
left += 1
else:
选右边
right -= 1
看这个模板时,一定要分清:
while:还要不要继续if:这一次怎么选
本节小结
你这个问题,本质上是在问:
为什么这里既要 while,又要 if?
答案就是:
while负责“重复处理所有方块”if负责“每一轮选择左边还是右边”
不能只写 if,因为 if 只能比较一次;
而这道题需要不断比较新的左右端点,直到所有方块都处理完。
同时,left <= right 里的等号也非常重要,因为它保证了最后只剩一个方块时,也能被处理到。
练习
你可以自己手动模拟一下这组数据:
blocks = [1, 2, 3]
请你试着写出每一轮的:
left是多少right是多少- 比较的是哪两个数
- 最后什么时候循环结束
提示
重点观察这三个阶段:
left < rightleft == rightleft > right
你把这三个阶段想明白,while left <= right 就彻底懂了。



