双指针+贪心思想
本文最后更新于51 天前,其中的信息可能已经过时,如有错误请发送邮件到184874483@qq.com

题目考点

这道题主要考的是:

  1. 双指针
  2. 贪心思想
  3. 列表两端取值
  4. 审题后把“文字规则”翻译成“序列单调性”

这题表面上像“堆方块”,本质上其实是在问:

能不能从数组两端依次取数,取出的序列始终保持“不变大”。

也就是:后拿出来放上去的方块,边长不能比下面那个更大。


审题

输入是什么

有 T 组测试数据。

每组数据里:

  • 第一行是方块数量 n
  • 第二行是 n 个整数,表示一排方块的边长

输出是什么

对于每组数据:

  • 如果能按题目要求堆起来,输出 Yes
  • 否则输出 No

题目真正要求我们判断什么

每次只能拿:

  • 最左边的方块
  • 或最右边的方块

而且新堆出来的竖直柱子要满足:

  • 上面的方块不能比下面的大

换句话说,如果我们把“拿方块的顺序”记下来,这个顺序必须是一个**从大到小(允许相等)**的序列。

容易忽略的点

这题最容易卡住的地方有两个:

第一,题目不是让你排序

你不能随便重排,只能从两端拿。

第二,不是去模拟“整个柱子长什么样”

你只需要保证:

当前拿出来的这个方块,不能比前一个已经放好的方块更大。

所以我们只需要记住“上一个放上去的方块大小”。


思路提示

先不要急着看代码,先抓住这题最核心的一句话:

每一步都应该优先拿两端中更大的那个。

为什么?

因为你要让整个取出的序列保持“不变大”。

假设两端分别是 4 和 7:

  • 如果你先拿 4
  • 那么 7 以后就只能放在 4 上面
  • 但 7 > 4,肯定不行

所以更安全的做法是:

  • 先拿大的
  • 小的留到后面

这就是这道题的贪心核心。

你可以先自己记住这个判断模板:

  1. 看左端和右端
  2. 取较大的那个
  3. 判断它是否还能放在当前堆顶上
  4. 如果能,继续
  5. 如果不能,直接 No

完整设计思路

我们把“已经堆好的最上面那个方块”记成 top

一开始还没堆,所以 top 可以看成无穷大。
也就是第一个方块无论多大都能放。

然后不断做下面这件事:

第一步:看两端哪个更大

设:

  • 左端是 blocks[left]
  • 右端是 blocks[right]

因为我们最后想形成一个不递增序列,所以优先选较大的那个:

  • 如果左边 >= 右边,就选左边
  • 否则选右边

第二步:判断能不能放

如果选出来的这个值 candidate 满足:

candidate <= top

说明它可以放到当前堆顶上面。

那么:

  • 更新 top = candidate
  • 对应那一侧指针向中间移动

第三步:一旦出现 candidate > top

说明你现在无论怎么选,已经破坏规则了,直接输出 No

第四步:如果所有方块都处理完了

说明整个过程一直合法,输出 Yes


为什么这个贪心是对的

这里一定要把道理想透。

我们每一步都在两端里选一个。
假设当前两端是 ab,并且 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 里效率很低,因为前面的元素删掉后,后面都要整体前移。

这题数据很大,最好用双指针:

  • left
  • right

3. 不是选较小的那个

有些同学会想:“上面要小,那我每次拿小的不就好了?”

这是错的。

因为小的放上去以后,大的可能就再也放不上去了。
所以应该优先拿两端中较大的那个。


本节小结

这题本质不是“堆方块模拟”,而是一个很经典的贪心判断题。

核心只有三步:

  1. 维护当前堆顶 top
  2. 每次从两端选较大的那个
  3. 判断它是否 <= top

能一直放下去就是 Yes,中途有一步放不下就是 No


练习

你先自己试试这道同类型小练习,不要急着看答案。

练习题

判断下面这组方块能否成功堆起来:

blocks = [5, 4, 3, 2, 1, 6]

输出应该是 Yes 还是 No

提示

按这题的方法来:

  1. 两端比较
  2. 每次取较大的那个
  3. 记录当前堆顶
  4. 看会不会出现“新拿出来的方块比堆顶还大”

你先自己手动模拟一遍。

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 = 0
  • right = 5

表示当前还没处理的范围是整个列表。


left <= right 表示“还有东西没处理完”

这个条件要分三种情况看:

1. left < right

说明左右之间还有至少两个方块没处理。

例如:

  • left = 1
  • right = 4

表示下标 1 到 4 这段还没处理完。

2. left == right

说明只剩下一个方块还没处理。

例如:

  • left = 2
  • right = 2

这时候中间只剩一个方块,也还是要处理的。

所以这里必须允许 =

3. left > right

说明已经没有方块了,全部处理结束。

例如:

  • left = 3
  • right = 2

这时候循环就该停了。


为什么不能只有一句 if blocks[left] >= blocks[right]:

因为这句 if 只会判断 一次

而这道题不是只选一次方块,而是要:

  • 选第 1 个
  • 选第 2 个
  • 选第 3 个
  • 一直选到所有方块都处理完

所以这里必须有一个“重复执行”的结构。

if 不是循环,它只是一次分支判断。


你可以把它理解成下面这个流程

这道题其实是在不断重复做同一件事:

每一轮都要做两步

第一步:看两端谁更大
第二步:拿走那个,并更新指针

然后再进入下一轮,继续比较新的两端。

所以逻辑其实是这样:

只要还有方块没处理:
    比较左端和右端
    选较大的那个
    判断能不能放
    更新 left 或 right

这就是为什么需要:

while left <= right:

然后在循环内部再写:

if blocks[left] >= blocks[right]:

whileif 在这题里的分工

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 = 2
  • right = 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 是多少
  • 比较的是哪两个数
  • 最后什么时候循环结束

提示

重点观察这三个阶段:

  1. left < right
  2. left == right
  3. left > right

你把这三个阶段想明白,while left <= right 就彻底懂了。

文末附加内容
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇