搜索专项训练

登录以参加训练计划

为什么要学搜索思维?

以【P1002. [NOIP 2002 普及组] 过河卒】为例分析,为什么学了动态规划DP思维还需要学搜索思维?应该按什么题单步骤循序渐进的学习?

回答

问得非常好!这是一个困扰着无数初学者的核心问题:“既然 DP 看起来那么‘高级’和‘高效’,为什么我还要回头去学看起来很‘笨’的搜索呢?”

作为你的教练,我可以告诉你一个核心观点:DP 和搜索,就像一个顶尖剑客的左手与右手,相辅相成,缺一不可。DP 的思维根基,正是源于对搜索空间的深刻理解。

我们先用一个比喻来理解,然后再以【过河卒】为例进行剖析。

一、为什么学了DP,还需要学搜索?

想象一下,你要解决一个问题,就像要从山脚爬到山顶寻找最优路径。

  • 搜索 (DFS/BFS):就像一个探险家。他从一个点出发,尝试所有可能的分岔路,一条路走到黑(DFS),或者一层一层地毯式探索(BFS)。这是最符合人类直觉的解决问题方式——暴力枚举所有可能性
  • DP (动态规划):就像一个地图绘制员。他并不急于出发,而是先研究地图的结构。他发现,无论从哪条路走到某个中间点(状态),从这个中间点再往上爬的最优路径是固定的,和他之前怎么走过来的无关(无后效性)。于是他从山脚开始,一层一层地计算并记录下到达每个中间点的最优解(状态转移),最终得到山顶的最优解。

现在,回答你的问题:

  1. 搜索是DP思维的“母体”:你只有先当过“探险家”,亲身体验过整个山脉的岔路(状态空间),你才能发现其中的规律,升级成为“地图绘制员”。搜索让你理解问题的本质和规模,而 DP 是在你理解了这一切之后,找到的一种“聪明”的、避免重复探索的计算方法。
  2. “记忆化搜索”是连接两者的桥梁:一个纯粹的 DFS 探险家很笨,他会反复走同一段路。但如果他很聪明,每次到达一个岔路口,就把从这里到山顶的最优路径记在小本本上(Memoization),下次再到这里就直接查本子。这个“聪明的探险家”就是记忆化搜索。而记忆化搜索,本质上就是自顶向下的 DP (Top-Down DP)
  3. 有些问题更适合用搜索来表达:对于某些状态转移不规则、或者状态本身很复杂的问题(如树形DP、区间DP、状态压缩DP),用循环来写自底向上的 DP 会非常烧脑。而用一个递归的搜索函数来描述状态转移,会自然得多,也更容易调试。
  4. 剪枝是搜索的独门绝技:当状态空间巨大,无法开下 DP 数组时,DP 就失效了。此时,一个优秀的搜索算法,配合强大的剪枝 (Pruning) 技巧,可以砍掉大量无效的搜索分支,奇迹般地在时限内找到答案。这是纯 DP 无法做到的。

二、以【P1002. 过河卒】为例

这道题是 DP 的经典入门题,但它完美地诠释了 DP 与搜索的关系。

1. DP 的视角 (地图绘制员)

  • 状态定义dp[i][j] 表示从起点 (0,0) 走到 (i,j) 的方案数。
  • 状态转移:卒只能向下或向右走。所以要想到达 (i,j),只能从 (i-1,j) 或者 (i,j-1) 走过来。
    • 因此,dp[i][j] = dp[i-1][j] + dp[i][j-1]
  • 实现:我们用两层循环,从 (0,0) 开始,一步步计算并填满整个 dp 表格,直到终点。
    • (当然要处理马的控制点,将它们的 dp 值设为 0)。

这是自底向上 (Bottom-Up) 的 DP,非常清晰高效。

2. 搜索的视角 (探险家)

现在我们换个思路,写一个函数 long long search(x, y),表示“从 (x,y) 走到终点 (B_x, B_y) 有多少种方案”。(为了和 DP 对应,我们也可以定义成“从起点走到 (x,y) 的方案数”,逻辑是完全一样的)。

我们以前者为例,自顶向下思考:

  • 函数定义long long solve(int x, int y),计算从 (0,0) 走到 (x,y) 的方案数。
  • 递归出口
    • 如果 (x,y) 是马的控制点,或者 x<0, y<0(走出边界),说明此路不通,return 0
    • 如果 (x,y) 是起点 (0,0),那么只有 1 种方案(就是原地不动),return 1
  • 递归分解:要走到 (x,y),只能从 (x-1,y) 或者 (x,y-1) 过来。所以方案数是这两者之和。
    • return solve(x-1, y) + solve(x, y-1);

问题来了:这个纯搜索会超时!因为它会重复计算大量的 solve(i,j)。例如,solve(3,3) 会调用 solve(2,3)solve(3,2),而这两者都会去调用 solve(2,2)

如何升级?——记忆化!

我们开一个 memo[x][y] 数组,初始化为 -1,表示没算过。

long long solve(int x, int y) {
    // 边界和障碍判断
    if (x < 0 || y < 0 || is_controlled(x, y)) return 0;
    
    // 起点
    if (x == 0 && y == 0) return 1;
    
    // 查备忘录
    if (memo[x][y] != -1) return memo[x][y];
    
    // 计算并记录
    long long ans = solve(x-1, y) + solve(x, y-1);
    memo[x][y] = ans;
    return ans;
}

现在请你对比一下:

  • 记忆化搜索中的 memo 数组,是不是就是 DP 中的 dp 数组?
  • 递归公式 solve(x-1, y) + solve(x, y-1),是不是就是 DP 的状态转移方程 dp[i-1][j] + dp[i][j-1]

是的,它们是完全等价的! 记忆化搜索就是“自顶向下”的 DP 实现。

结论:学习搜索思维,能让你从问题的本质出发,自然地推导出状态转移方程。当你发现朴素搜索中存在大量重复计算时,加上一个“备忘录”,就自动进化成了 DP。


三、循序渐进的学习题单

你应该按照“纯搜索 -> 记忆化搜索 -> 简单DP”的路径来训练,彻底打通任督二脉。

第一阶段:掌握纯粹的搜索(当好探险家)

目标是熟练掌握递归、回溯、状态表示和剪枝。这些问题通常没有 DP 特性。

  1. P1706 全排列:DFS 最基础的应用,理解递归树。
  2. P1036 [NOIP2002 普及组] 选数:组合型枚举,学习如何传递状态和剪枝。
  3. P1135 奇怪的电梯:BFS 的入门,理解广度优先搜索的层次性。
  4. P1219 [USACO1.5] 八皇后 Checker Challenge:经典回溯问题,剪枝的绝佳练习。

第二阶段:从搜索到记忆化搜索(给探险家配上小本本)

目标是识别“重叠子问题”,并加上记忆化数组,完成从暴力到高效的蜕变。

  1. P1464 Function:最简单的记忆化搜索,题目就是为你量身定做的。
  2. P1002 [NOIP2002 普及组] 过河卒:用记忆化搜索再做一遍,体会和 DP 的等价性。
  3. P1216 [USACO1.5] [IOI1994]数字三角形:既可以纯 DP,也可以用记忆化搜索,是绝佳的对比材料。
  4. P1048 [NOIP2005 普及组] 采药:经典的 0/1 背包问题,尝试用记忆化搜索 solve(i, w)(考虑前i个物品,背包容量w)来求解。

第三阶段:拥抱 DP(成为地图绘制员)

在第二阶段的基础上,尝试将记忆化搜索的逻辑翻译成自底向上的循环 DP。

  1. P1048 [NOIP2005 普及组] 采药:写出自底向上的 0/1 背包 DP 代码。
  2. B3637 最长上升子序列:学习经典的 O(N2)O(N^2) LIS DP。
  3. P1091 [NOIP2004 提高组] 合唱队形:LIS 的经典应用,需要正反两次 DP。

当你完成了这个流程,你就会发现,搜索和 DP 已经不再是两个孤立的知识点,而是你解决问题时可以随时切换的一体两面。祝你成功!


搜索的双指针技巧有哪些循序渐进的洛谷题单?

你好!很高兴看到你对双指针 (Two Pointers) 这个技巧产生了浓厚的兴趣。

双指针是一种非常强大且优雅的算法思想,它能将某些问题的时间复杂度从 O(N2)O(N^2) 暴力优化到 O(N)O(N)。它的核心在于利用问题的单调性,通过同向或反向移动两个指针来“扫描”序列,从而避免重复计算。

我为你精心设计了一份循序渐进的洛谷题单,从最经典的入门模型到复杂的综合应用,帮助你彻底掌握双指针。


第一阶段:入门与核心模型(反向指针/相向指针)

这个阶段的目标是掌握最基础的“两端夹逼”模型。指针 l 从左到右,r 从右到左,通常用于有序序列

1. P1102 A-B 数对

  • 难度:🟠 普及-
  • 一句话题意:给定序列和差值 CC,求有多少对 (A,B)(A, B) 满足 AB=CA - B = C
  • 教练点评绝对的入门第一题。先将数组排序,然后用双指针寻找差值为 CC 的数对。这题也可以用二分查找做,是练习两种思维的好材料。

2. P1638 逛画展

  • 难度:🟡 普及/提高-
  • 一句话题意:在一个序列中找到一个最短的连续子段,使其包含所有 MM 种不同的元素。
  • 教练点评:这是滑动窗口的经典模型,是双指针的变体(同向指针)。一个指针 r 负责扩张窗口,另一个 l 负责收缩窗口。

3. P3143 [USACO16OPEN] Diamond Collector S

  • 难度:🟢 普及+/提高
  • 一句话题意:在一个序列中找出两个不重叠的区间,使得每个区间内的最大值与最小值之差不超过 KK,求这两个区间包含的元素总数的最大值。
  • 教练点评:这题需要双指针和预处理/DP思想的结合。先用一次双指针扫一遍,预处理出以 i 结尾的最长满足条件的区间长度。然后再扫一遍找最优组合。

第二阶段:同向指针与滑动窗口

这个阶段的目标是熟练运用“尺取法 (Inchworm Method)”,即两个指针 l, r 都从左往右移动,共同维护一个动态变化的区间(窗口)。

4. P1147 连续自然数和

  • 难度:🟡 普及/提高-
  • 一句话题意:求所有和为 MM 的连续正整数序列。
  • 教练点评:滑动窗口的入门题。r 指针向右扩张,使窗口内的和增加;当和超过 MM 时,l 指针向右收缩,使和减小。

5. P8772 [蓝桥杯 2022 省 A] 选数异或

  • 难度:🟢 普及+/提高
  • 一句话题意:在序列中找一个区间,使得区间内所有数的异或和等于某个值。
  • 教练点评:这题将双指针与位运算前缀和思想结合。需要用到前缀异或和来快速计算区间异或值。

6. P3811 【模板】乘法逆元 (没错,是这道数论题)

  • 题目:P3811 【模板】乘法逆元
  • 难度:🟢 普及+/提高
  • 教练点评:你可能会惊讶为什么这里会出现一道数论题。实际上,求解 1N1 \dots N 所有数的逆元有一个非常巧妙的 O(N)O(N) 线性递推算法。这个递推算法的推导过程,就蕴含了双指针的思想(虽然代码里看不出指针)。理解它能加深你对“线性扫描”这种思想的理解。
    • inv[i] = (p - p / i) * inv[p % i] % p

第三阶段:综合应用与思维拓展

这个阶段的题目将双指针与其他算法(如DP、数据结构、贪心)结合,需要更灵活的思维。

7. P1115 最大子段和

  • 难度:🟡 普及/提高-
  • 一句话题意:求一个序列中连续子段的最大和。
  • 教练点评:虽然这题的标准解法是 O(N)O(N) 的简单 DP 或贪心,但它体现了双指针“维护最优性”的思想。你可以思考一下,能否用一个指针 r 扫描,另一个指针 l 动态地指向“最佳起点”?这能帮你理解 Kadane 算法的本质。

8. P2678 [NOIP2015 提高组] 跳石头

  • 难度:🔵 提高+/省选-
  • 一句话题意:通过移除 MM 块石头,使得最短的跳跃距离最大化。
  • 教练点评:这是一道非常经典的“二分答案 + 判定”问题。而它的 check 函数,就是一个贪心判定,通常用**双指针(或者说一个指针扫描)**来实现。这题是“二分”与“双指针”这对黄金搭档的完美体现。

9. CF22A Second Order Statistics (Codeforces)

  • 难度:🔴 入门
  • 教练点评:作为放松,这道 CF 入门题虽然简单,但它提醒我们双指针的最基本前提——排序。先排序,再去重,然后用指针找到第二小的数。

刷题建议

  1. 先排序,再思考:拿到一个序列问题,如果感觉可以用双指针,第一步通常是排序
  2. 明确指针移动的条件:仔细分析,什么情况下移动 l 指针?什么情况下移动 r 指针?指针移动的依据必须是单调的
  3. 画图模拟:在纸上画出数组和两个指针,手动模拟几步,能帮你理清逻辑,发现边界问题。
  4. 一题多解:像 P1102 这样的题,尝试用双指针二分查找两种方法都写一遍,对比它们的优劣和思维差异。

祝你刷题愉快,早日成为双指针大师!

labuladong的环形数组双指针

https://labuladong.online/algo/data-structure-basic/cycle-array/#代码实现


单调栈

你好!我是你的 OI 教练。这个问题问得非常本质!

很多同学只会背模板代码,却不知道“它为什么叫这个名字”以及“它到底和数组有什么纠葛”。理解了这两点,你就不需要死记硬背了。

我们把它拆解开来把玩一下。


一、 “单调栈”名字的来历 (The Etymology)

这个名字是由两个词组成的:单调 (Monotonic) + 栈 (Stack)

1. 什么是“栈”?

这个你肯定知道。它是一种 LIFO (Last In First Out) 的数据结构,像一个杯子,只能从上面放东西(入栈),也只能从上面拿东西(出栈)。

2. 什么是“单调”?

这是一个数学概念。函数的单调性(Monotonicity)指的是函数值随着自变量的增加,一直增加或者一直减少,不会忽高忽低。

  • 单调递增1,3,5,9,121, 3, 5, 9, 12 (一直变大)
  • 单调递减10,8,5,2,110, 8, 5, 2, 1 (一直变小)

3. 为什么合起来叫“单调栈”?

普通栈对里面的元素大小没有要求,可以是乱序的。 而单调栈在普通栈的基础上加了一条铁律

从栈底到栈顶,元素必须严格保持单调递增或单调递减。

这个名字的由来就是因为它时刻维护着栈内元素的有序性。

为了维护这个规矩,它表现出了一种“排他性”:

  • 如果新来的元素破坏了规矩,栈顶元素就必须滚蛋(Pop),直到规矩恢复,新元素才能进门(Push)

形象的比喻: 想象一个“按个头排队”的单调递减栈(栈底高 \to 栈顶矮)。

  • 现在栈里是 [10, 5, 3]
  • 来了一个 4
  • 4 说:“我要进来,但我不能压在 3 头上,因为 4 > 3,破坏了递减规矩。”
  • 于是 3 被踢出栈。
  • 现在栈顶是 54 < 5,规矩成立。
  • 4 进栈,变成 [10, 5, 4]

二、 单调栈和数组的关系 (Relationship with Arrays)

单调栈不是孤立存在的,它就像是数组的“过滤器”数组的“辅助索引”

1. 关系一:数组是“食材”,单调栈是“锅”

单调栈是为了解决数组中的特定问题而生的(特别是 Next Greater Element 类问题)。

  • 我们遍历数组,将数组中的元素一个个扔进单调栈里处理。
  • 数组提供了原始数据的时间序列。
  • 单调栈在遍历过程中,帮你筛选出“当前还可能有用的元素”,扔掉“永远没用的元素”。

2. 关系二:栈里存的是数组的“下标” (The Index Link)

这是新手最容易忽略的关键点! 在 OI 竞赛中,单调栈里存的通常不是数值,而是数组的下标(Index)!

  • 为什么存下标?
    • 如果只存数值:我知道右边有个比我大的数是 10,但我不知道它在哪里,距离我多远。
    • 如果存下标 i:我既可以通过 arr[i] 拿到数值,又能通过 i 知道位置,还能通过 current_index - i 算出距离(求矩形宽度时必须用这个)。

示意图:

数组 Arr:    [ 2,  1,  5,  6,  2,  3 ]
索引 Index:    0   1   2   3   4   5

单调栈 (存下标):
[ 0 ]      -> 代表栈里有数值 Arr[0]=2
[ 0, 1 ]   -> 代表栈里有数值 Arr[0]=2, Arr[1]=1 (单调递减)

3. 关系三:时间复杂度的魔法 (O(N2)O(N)O(N^2) \to O(N))

如果你直接在数组上找“下一个更大元素”,需要两层循环暴力扫描,复杂度 O(N2)O(N^2)。 单调栈利用了数组的线性特性:

  • 每个数组元素最多进栈一次,出栈一次。
  • 所以单调栈把对数组的操作从“反复扫描”变成了“流式处理”,复杂度降为 O(N)O(N)

三、 教练总结

单调栈这个名字,描述的是它内部的“状态”(有序)。 而它和数组的关系,描述的是它的“功能”(处理数组的工具)。

你可以这样理解:

数组是一群乱哄哄排队的人。 单调栈是一个严格的管理员,他从头到尾扫视这群人,手里拿着一个小本本(栈)。 为了快速找到每个人旁边比他高的人,管理员强制要求小本本上记的人必须是按身高排序的。凡是不符合规矩的,就被管理员从小本本上划掉(出栈),并记录下此时的答案。

理解了这一点,下一回做题时,记得在栈里存下标哦!

你好!作为OI教练,我为你整理了一份洛谷(Luogu)单调栈专题循序渐进题单

这份题单的设计逻辑是:模板肌肉记忆 \to 基础物理模型(找最近的大值) \to 经典贡献问题(矩形面积) \to 二维/综合应用


第一阶段:模板与基础 (Hello, Monotonic Stack)

目标:掌握单调栈的两种形态(单调递增/递减),学会 O(N)O(N) 寻找“左/右边第一个比我大/小的元素”。

  1. P5788 【模板】单调栈

    • 难度:普及/提高-
    • 知识点:寻找右边第一个大于该元素的下标。
    • 教练点评:这是一切的起点。请务必背下来,并且要能熟练地写出“存下标”而不是“存数值”的版本。
  2. P2947 [USACO09MAR] Look Up S

    • 难度:普及/提高-
    • 知识点:寻找右边第一个比自己高的奶牛。
    • 教练点评:跟模板题几乎一模一样,用来巩固手感。注意,这题如果 O(N2)O(N^2) 暴力是过不了的,必须用单调栈。

第二阶段:经典模型——视野与接雨水 (Visibility & Trapping)

目标:理解单调栈在实际物理场景中的意义,通常涉及“能看到多远”或“被谁挡住”。

  1. P2866 [USACO06NOV] Bad Hair Day S

    • 难度:普及/提高-
    • 知识点:计算每头牛能看到右边多少头牛(直到被更高的挡住)。
    • 教练点评:这题有两种思路:
      1. 找右边第一个比我高的(正向思维)。
      2. 维护一个单调递减栈,看新来的牛能挡住多少之前的牛(逆向/维护栈内元素个数)。推荐尝试思路2,理解“维护栈内性质”的含义。
  2. P1901 发射站

    • 难度:普及/提高-
    • 知识点:向两边发射能量,被第一个更高的接收。
    • 教练点评:这是双向单调栈的入门题。你需要分别求出“左边第一个比我高”和“右边第一个比我高”的塔,然后累加能量。
  3. P1823 [COI2007] Patrik 音乐会的等待

    • 难度:提高+/省选-
    • 知识点:计算相互能看到的人的对数。
    • 教练点评易错题! 难点在于处理高度相同的人。在单调栈中,当 height[top] == current 时,需要特殊处理计数的逻辑。这是单调栈细节处理的试金石。

第三阶段:核心考点——直方图与最大矩形 (Histogram & Rectangle)

目标:掌握单调栈最高频的考点——“以当前元素为高,向左右能延伸多宽”

  1. SP1805 HISTOGRA - Largest Rectangle in a Histogram

    • (注:洛谷 Remote Judge 题目,或者搜 P4147 的弱化版)
    • 难度:提高+/省选-
    • 知识点:直方图最大矩形面积。
    • 教练点评必做经典。对于每一根柱子,找到左边第一个比它矮的 l,右边第一个比它矮的 r,那么以这根柱子为高的最大面积就是 height * (r - l - 1)
  2. P4147 玉蟾宫

    • 难度:提高+/省选-
    • 知识点:二维网格中的最大全 'F' 矩形。
    • 教练点评:这是 SP1805 的二维升级版。技巧是“悬线法”或者“逐行单调栈”。把每一行及其上方看作一个直方图,问题就转化为了 NN 次“直方图最大矩形”问题。

第四阶段:思维与综合 (Advanced Thinking)

目标:单调栈不再是唯一的解法,而是作为优化思维的一部分,或者结合其他技巧。

  1. P3467 [POI2008] PLA-Postering

    • 难度:提高+/省选-
    • 知识点:贪心 + 单调栈。
    • 教练点评:这题非常有意思。问最少用多少张海报覆盖轮廓。本质是看有多少个矩形可以“共用”高度。如果后面的楼和前面的楼一样高,且中间没有比它们矮的,就可以省一张海报。
  2. P1950 长春

    • 难度:省选/NOI-
    • 知识点:统计所有子矩形的数量(涉及单调栈 + 计数DP/贡献法)。
    • 教练点评:不仅仅是求最大面积,而是求由于某种限制下的方案数。需要利用单调栈找到边界后,通过数学公式计算贡献。
  3. P1169 [ZJOI2007] 棋盘制作

    • 难度:省选/NOI-
    • 知识点:最大黑白相间子矩形(悬线法/单调栈 + 预处理)。
    • 教练点评:属于 P4147 的变种,难点在于如何把“黑白相间”转化为“01矩阵”来处理(通常用 (i+j)%2 的异或技巧)。

💡 教练的刷题建议

  1. 手动画图:做前三道题时,一定要在草稿纸上画出栈的 pushpop 过程,搞清楚到底是在“入栈时”统计答案,还是在“出栈时”统计答案。
  2. 关注边界:做“最大矩形”类问题时,为了防止遍历完数组后栈还不为空,通常会在数组最后加一个高度为 0 的哨兵元素,强制清空栈,计算剩余元素的贡献。
  3. 调试技巧:如果 WA 了,检查是否混淆了“存下标”和“存数值”。通常存下标更灵活,因为通过下标既能找到值,也能计算宽度。

加油!把这 10 道题吃透,单调栈这个知识点你就通关了。

章节 1. 纯搜索-1维

开放

题目 尝试 AC 难度
373   算法9-1:静态表的顺序查找 4 1 10
374   算法9-2:有序表的折半查找 0 0 (无)
1124   查找数组中的某个数 2 1 10
1125   查找数组中的最大值、最小值 1 1 10
2316   查找最小的k个元素(栈和队列) 0 0 (无)
2291   查找元素(线性表) 0 0 (无)