登录以参加训练计划
为什么要学搜索思维?
以【P1002. [NOIP 2002 普及组] 过河卒】为例分析,为什么学了动态规划DP思维还需要学搜索思维?应该按什么题单步骤循序渐进的学习?
回答
问得非常好!这是一个困扰着无数初学者的核心问题:“既然 DP 看起来那么‘高级’和‘高效’,为什么我还要回头去学看起来很‘笨’的搜索呢?”
作为你的教练,我可以告诉你一个核心观点:DP 和搜索,就像一个顶尖剑客的左手与右手,相辅相成,缺一不可。DP 的思维根基,正是源于对搜索空间的深刻理解。
我们先用一个比喻来理解,然后再以【过河卒】为例进行剖析。
一、为什么学了DP,还需要学搜索?
想象一下,你要解决一个问题,就像要从山脚爬到山顶寻找最优路径。
- 搜索 (DFS/BFS):就像一个探险家。他从一个点出发,尝试所有可能的分岔路,一条路走到黑(DFS),或者一层一层地毯式探索(BFS)。这是最符合人类直觉的解决问题方式——暴力枚举所有可能性。
- DP (动态规划):就像一个地图绘制员。他并不急于出发,而是先研究地图的结构。他发现,无论从哪条路走到某个中间点(状态),从这个中间点再往上爬的最优路径是固定的,和他之前怎么走过来的无关(无后效性)。于是他从山脚开始,一层一层地计算并记录下到达每个中间点的最优解(状态转移),最终得到山顶的最优解。
现在,回答你的问题:
- 搜索是DP思维的“母体”:你只有先当过“探险家”,亲身体验过整个山脉的岔路(状态空间),你才能发现其中的规律,升级成为“地图绘制员”。搜索让你理解问题的本质和规模,而 DP 是在你理解了这一切之后,找到的一种“聪明”的、避免重复探索的计算方法。
- “记忆化搜索”是连接两者的桥梁:一个纯粹的 DFS 探险家很笨,他会反复走同一段路。但如果他很聪明,每次到达一个岔路口,就把从这里到山顶的最优路径记在小本本上(Memoization),下次再到这里就直接查本子。这个“聪明的探险家”就是记忆化搜索。而记忆化搜索,本质上就是自顶向下的 DP (Top-Down DP)。
- 有些问题更适合用搜索来表达:对于某些状态转移不规则、或者状态本身很复杂的问题(如树形DP、区间DP、状态压缩DP),用循环来写自底向上的 DP 会非常烧脑。而用一个递归的搜索函数来描述状态转移,会自然得多,也更容易调试。
- 剪枝是搜索的独门绝技:当状态空间巨大,无法开下 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 特性。
- P1706 全排列:DFS 最基础的应用,理解递归树。
- P1036 [NOIP2002 普及组] 选数:组合型枚举,学习如何传递状态和剪枝。
- P1135 奇怪的电梯:BFS 的入门,理解广度优先搜索的层次性。
- P1219 [USACO1.5] 八皇后 Checker Challenge:经典回溯问题,剪枝的绝佳练习。
第二阶段:从搜索到记忆化搜索(给探险家配上小本本)
目标是识别“重叠子问题”,并加上记忆化数组,完成从暴力到高效的蜕变。
- P1464 Function:最简单的记忆化搜索,题目就是为你量身定做的。
- P1002 [NOIP2002 普及组] 过河卒:用记忆化搜索再做一遍,体会和 DP 的等价性。
- P1216 [USACO1.5] [IOI1994]数字三角形:既可以纯 DP,也可以用记忆化搜索,是绝佳的对比材料。
- P1048 [NOIP2005 普及组] 采药:经典的 0/1 背包问题,尝试用记忆化搜索
solve(i, w)(考虑前i个物品,背包容量w)来求解。
第三阶段:拥抱 DP(成为地图绘制员)
在第二阶段的基础上,尝试将记忆化搜索的逻辑翻译成自底向上的循环 DP。
- P1048 [NOIP2005 普及组] 采药:写出自底向上的 0/1 背包 DP 代码。
- B3637 最长上升子序列:学习经典的 LIS DP。
- P1091 [NOIP2004 提高组] 合唱队形:LIS 的经典应用,需要正反两次 DP。
当你完成了这个流程,你就会发现,搜索和 DP 已经不再是两个孤立的知识点,而是你解决问题时可以随时切换的一体两面。祝你成功!
搜索的双指针技巧有哪些循序渐进的洛谷题单?
你好!很高兴看到你对双指针 (Two Pointers) 这个技巧产生了浓厚的兴趣。
双指针是一种非常强大且优雅的算法思想,它能将某些问题的时间复杂度从 暴力优化到 。它的核心在于利用问题的单调性,通过同向或反向移动两个指针来“扫描”序列,从而避免重复计算。
我为你精心设计了一份循序渐进的洛谷题单,从最经典的入门模型到复杂的综合应用,帮助你彻底掌握双指针。
第一阶段:入门与核心模型(反向指针/相向指针)
这个阶段的目标是掌握最基础的“两端夹逼”模型。指针 l 从左到右,r 从右到左,通常用于有序序列。
1. P1102 A-B 数对
- 难度:🟠 普及-
- 一句话题意:给定序列和差值 ,求有多少对 满足 。
- 教练点评:绝对的入门第一题。先将数组排序,然后用双指针寻找差值为 的数对。这题也可以用二分查找做,是练习两种思维的好材料。
2. P1638 逛画展
- 难度:🟡 普及/提高-
- 一句话题意:在一个序列中找到一个最短的连续子段,使其包含所有 种不同的元素。
- 教练点评:这是滑动窗口的经典模型,是双指针的变体(同向指针)。一个指针
r负责扩张窗口,另一个l负责收缩窗口。
3. P3143 [USACO16OPEN] Diamond Collector S
- 难度:🟢 普及+/提高
- 一句话题意:在一个序列中找出两个不重叠的区间,使得每个区间内的最大值与最小值之差不超过 ,求这两个区间包含的元素总数的最大值。
- 教练点评:这题需要双指针和预处理/DP思想的结合。先用一次双指针扫一遍,预处理出以
i结尾的最长满足条件的区间长度。然后再扫一遍找最优组合。
第二阶段:同向指针与滑动窗口
这个阶段的目标是熟练运用“尺取法 (Inchworm Method)”,即两个指针 l, r 都从左往右移动,共同维护一个动态变化的区间(窗口)。
4. P1147 连续自然数和
- 难度:🟡 普及/提高-
- 一句话题意:求所有和为 的连续正整数序列。
- 教练点评:滑动窗口的入门题。
r指针向右扩张,使窗口内的和增加;当和超过 时,l指针向右收缩,使和减小。
5. P8772 [蓝桥杯 2022 省 A] 选数异或
- 难度:🟢 普及+/提高
- 一句话题意:在序列中找一个区间,使得区间内所有数的异或和等于某个值。
- 教练点评:这题将双指针与位运算和前缀和思想结合。需要用到前缀异或和来快速计算区间异或值。
6. P3811 【模板】乘法逆元 (没错,是这道数论题)
- 题目:P3811 【模板】乘法逆元
- 难度:🟢 普及+/提高
- 教练点评:你可能会惊讶为什么这里会出现一道数论题。实际上,求解 所有数的逆元有一个非常巧妙的 线性递推算法。这个递推算法的推导过程,就蕴含了双指针的思想(虽然代码里看不出指针)。理解它能加深你对“线性扫描”这种思想的理解。
inv[i] = (p - p / i) * inv[p % i] % p
第三阶段:综合应用与思维拓展
这个阶段的题目将双指针与其他算法(如DP、数据结构、贪心)结合,需要更灵活的思维。
7. P1115 最大子段和
- 难度:🟡 普及/提高-
- 一句话题意:求一个序列中连续子段的最大和。
- 教练点评:虽然这题的标准解法是 的简单 DP 或贪心,但它体现了双指针“维护最优性”的思想。你可以思考一下,能否用一个指针
r扫描,另一个指针l动态地指向“最佳起点”?这能帮你理解 Kadane 算法的本质。
8. P2678 [NOIP2015 提高组] 跳石头
- 难度:🔵 提高+/省选-
- 一句话题意:通过移除 块石头,使得最短的跳跃距离最大化。
- 教练点评:这是一道非常经典的“二分答案 + 判定”问题。而它的
check函数,就是一个贪心判定,通常用**双指针(或者说一个指针扫描)**来实现。这题是“二分”与“双指针”这对黄金搭档的完美体现。
9. CF22A Second Order Statistics (Codeforces)
- 难度:🔴 入门
- 教练点评:作为放松,这道 CF 入门题虽然简单,但它提醒我们双指针的最基本前提——排序。先排序,再去重,然后用指针找到第二小的数。
刷题建议
- 先排序,再思考:拿到一个序列问题,如果感觉可以用双指针,第一步通常是排序。
- 明确指针移动的条件:仔细分析,什么情况下移动
l指针?什么情况下移动r指针?指针移动的依据必须是单调的。 - 画图模拟:在纸上画出数组和两个指针,手动模拟几步,能帮你理清逻辑,发现边界问题。
- 一题多解:像 P1102 这样的题,尝试用双指针和二分查找两种方法都写一遍,对比它们的优劣和思维差异。
祝你刷题愉快,早日成为双指针大师!
labuladong的环形数组双指针
https://labuladong.online/algo/data-structure-basic/cycle-array/#代码实现
单调栈
你好!我是你的 OI 教练。这个问题问得非常本质!
很多同学只会背模板代码,却不知道“它为什么叫这个名字”以及“它到底和数组有什么纠葛”。理解了这两点,你就不需要死记硬背了。
我们把它拆解开来把玩一下。
一、 “单调栈”名字的来历 (The Etymology)
这个名字是由两个词组成的:单调 (Monotonic) + 栈 (Stack)。
1. 什么是“栈”?
这个你肯定知道。它是一种 LIFO (Last In First Out) 的数据结构,像一个杯子,只能从上面放东西(入栈),也只能从上面拿东西(出栈)。
2. 什么是“单调”?
这是一个数学概念。函数的单调性(Monotonicity)指的是函数值随着自变量的增加,一直增加或者一直减少,不会忽高忽低。
- 单调递增: (一直变大)
- 单调递减: (一直变小)
3. 为什么合起来叫“单调栈”?
普通栈对里面的元素大小没有要求,可以是乱序的。 而单调栈在普通栈的基础上加了一条铁律:
从栈底到栈顶,元素必须严格保持单调递增或单调递减。
这个名字的由来就是因为它时刻维护着栈内元素的有序性。
为了维护这个规矩,它表现出了一种“排他性”:
- 如果新来的元素破坏了规矩,栈顶元素就必须滚蛋(Pop),直到规矩恢复,新元素才能进门(Push)。
形象的比喻: 想象一个“按个头排队”的单调递减栈(栈底高 栈顶矮)。
- 现在栈里是
[10, 5, 3]。 - 来了一个
4。 4说:“我要进来,但我不能压在3头上,因为4 > 3,破坏了递减规矩。”- 于是
3被踢出栈。 - 现在栈顶是
5,4 < 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. 关系三:时间复杂度的魔法 ()
如果你直接在数组上找“下一个更大元素”,需要两层循环暴力扫描,复杂度 。 单调栈利用了数组的线性特性:
- 每个数组元素最多进栈一次,出栈一次。
- 所以单调栈把对数组的操作从“反复扫描”变成了“流式处理”,复杂度降为 。
三、 教练总结
单调栈这个名字,描述的是它内部的“状态”(有序)。 而它和数组的关系,描述的是它的“功能”(处理数组的工具)。
你可以这样理解:
数组是一群乱哄哄排队的人。 单调栈是一个严格的管理员,他从头到尾扫视这群人,手里拿着一个小本本(栈)。 为了快速找到每个人旁边比他高的人,管理员强制要求小本本上记的人必须是按身高排序的。凡是不符合规矩的,就被管理员从小本本上划掉(出栈),并记录下此时的答案。
理解了这一点,下一回做题时,记得在栈里存下标哦!
你好!作为OI教练,我为你整理了一份洛谷(Luogu)单调栈专题循序渐进题单。
这份题单的设计逻辑是:模板肌肉记忆 基础物理模型(找最近的大值) 经典贡献问题(矩形面积) 二维/综合应用。
第一阶段:模板与基础 (Hello, Monotonic Stack)
目标:掌握单调栈的两种形态(单调递增/递减),学会 寻找“左/右边第一个比我大/小的元素”。
-
P5788 【模板】单调栈
- 难度:普及/提高-
- 知识点:寻找右边第一个大于该元素的下标。
- 教练点评:这是一切的起点。请务必背下来,并且要能熟练地写出“存下标”而不是“存数值”的版本。
-
P2947 [USACO09MAR] Look Up S
- 难度:普及/提高-
- 知识点:寻找右边第一个比自己高的奶牛。
- 教练点评:跟模板题几乎一模一样,用来巩固手感。注意,这题如果 暴力是过不了的,必须用单调栈。
第二阶段:经典模型——视野与接雨水 (Visibility & Trapping)
目标:理解单调栈在实际物理场景中的意义,通常涉及“能看到多远”或“被谁挡住”。
-
P2866 [USACO06NOV] Bad Hair Day S
- 难度:普及/提高-
- 知识点:计算每头牛能看到右边多少头牛(直到被更高的挡住)。
- 教练点评:这题有两种思路:
- 找右边第一个比我高的(正向思维)。
- 维护一个单调递减栈,看新来的牛能挡住多少之前的牛(逆向/维护栈内元素个数)。推荐尝试思路2,理解“维护栈内性质”的含义。
-
P1901 发射站
- 难度:普及/提高-
- 知识点:向两边发射能量,被第一个更高的接收。
- 教练点评:这是双向单调栈的入门题。你需要分别求出“左边第一个比我高”和“右边第一个比我高”的塔,然后累加能量。
-
P1823 [COI2007] Patrik 音乐会的等待
- 难度:提高+/省选-
- 知识点:计算相互能看到的人的对数。
- 教练点评:易错题! 难点在于处理高度相同的人。在单调栈中,当
height[top] == current时,需要特殊处理计数的逻辑。这是单调栈细节处理的试金石。
第三阶段:核心考点——直方图与最大矩形 (Histogram & Rectangle)
目标:掌握单调栈最高频的考点——“以当前元素为高,向左右能延伸多宽”。
-
SP1805 HISTOGRA - Largest Rectangle in a Histogram
- (注:洛谷 Remote Judge 题目,或者搜 P4147 的弱化版)
- 难度:提高+/省选-
- 知识点:直方图最大矩形面积。
- 教练点评:必做经典。对于每一根柱子,找到左边第一个比它矮的
l,右边第一个比它矮的r,那么以这根柱子为高的最大面积就是height * (r - l - 1)。
-
P4147 玉蟾宫
- 难度:提高+/省选-
- 知识点:二维网格中的最大全 'F' 矩形。
- 教练点评:这是 SP1805 的二维升级版。技巧是“悬线法”或者“逐行单调栈”。把每一行及其上方看作一个直方图,问题就转化为了 次“直方图最大矩形”问题。
第四阶段:思维与综合 (Advanced Thinking)
目标:单调栈不再是唯一的解法,而是作为优化思维的一部分,或者结合其他技巧。
-
P3467 [POI2008] PLA-Postering
- 难度:提高+/省选-
- 知识点:贪心 + 单调栈。
- 教练点评:这题非常有意思。问最少用多少张海报覆盖轮廓。本质是看有多少个矩形可以“共用”高度。如果后面的楼和前面的楼一样高,且中间没有比它们矮的,就可以省一张海报。
-
P1950 长春
- 难度:省选/NOI-
- 知识点:统计所有子矩形的数量(涉及单调栈 + 计数DP/贡献法)。
- 教练点评:不仅仅是求最大面积,而是求由于某种限制下的方案数。需要利用单调栈找到边界后,通过数学公式计算贡献。
-
P1169 [ZJOI2007] 棋盘制作
- 难度:省选/NOI-
- 知识点:最大黑白相间子矩形(悬线法/单调栈 + 预处理)。
- 教练点评:属于 P4147 的变种,难点在于如何把“黑白相间”转化为“01矩阵”来处理(通常用
(i+j)%2的异或技巧)。
💡 教练的刷题建议
- 手动画图:做前三道题时,一定要在草稿纸上画出栈的
push和pop过程,搞清楚到底是在“入栈时”统计答案,还是在“出栈时”统计答案。 - 关注边界:做“最大矩形”类问题时,为了防止遍历完数组后栈还不为空,通常会在数组最后加一个高度为
0的哨兵元素,强制清空栈,计算剩余元素的贡献。 - 调试技巧:如果 WA 了,检查是否混淆了“存下标”和“存数值”。通常存下标更灵活,因为通过下标既能找到值,也能计算宽度。
加油!把这 10 道题吃透,单调栈这个知识点你就通关了。
章节 2. 纯搜索-双指针技巧
无效
该章节目前不可挑战,请先完成以下章节:
- 章节 1. 纯搜索-1维 (已完成 0%)
| 题目 | 尝试 | AC | 难度 |
|---|---|---|---|
| luoguP1102 A-B 数对 | 2 | 1 | 3 |
| 2427 在数组中查找两个数之和等于输入的另一个数(栈和队列) | 0 | 0 | (无) |
| luoguP3811 【模板】模意义下的乘法逆元 | 0 | 0 | 5 |
章节 3. 纯搜索-单调栈技巧
无效
该章节目前不可挑战,请先完成以下章节:
- 章节 1. 纯搜索-1维 (已完成 0%)
| 题目 | 尝试 | AC | 难度 |
|---|---|---|---|
| luoguP5788 【模板】单调栈 | 0 | 0 | 5 |
| 19264 环形数组和单调栈 | 0 | 0 | (无) |
章节 4. 纯搜索-多维
无效
该章节目前不可挑战,请先完成以下章节:
- 章节 1. 纯搜索-1维 (已完成 0%)
| 题目 | 尝试 | AC | 难度 |
|---|---|---|---|
| luoguP2670 [NOIP 2015 普及组] 扫雷游戏 | 1 | 1 | 1 |
| luoguP1002 [NOIP 2002 普及组] 过河卒 | 0 | 0 | 3 |
章节 5. 记忆化搜索
无效
该章节目前不可挑战,请先完成以下章节:
- 章节 3. 纯搜索-单调栈技巧 (已完成 0%)
| 题目 | 尝试 | AC | 难度 |
|---|---|---|---|
| 2 A+B 输入输出练习II | 0 | 0 | (无) |
| 3 A+B 输入输出练习III | 1 | 1 | 10 |
章节 6. 动态规划DP
无效
该章节目前不可挑战,请先完成以下章节:
- 章节 4. 纯搜索-多维 (已完成 0%)
| 题目 | 尝试 | AC | 难度 |
|---|---|---|---|
| 2 A+B 输入输出练习II | 0 | 0 | (无) |
| 3 A+B 输入输出练习III | 1 | 1 | 10 |
- 参加人数
- 1
- 创建人