TEXT Complete Search
思路(The Idea)
使用穷举搜索解决问题基于“KISS” 原则(Keep It Simple, Stupid)。在竞赛中解决问题的目标是在规定时间内写出能运行的程序——是否存在更快的算法并不重要。
穷举搜索是一种利用暴力、直接、尝试所有可能的方法来寻找答案的策略。这种方法几乎应该是你优先考虑的第一个算法或解法。如果它能在时间和空间限制内运行,那么就使用它:这种方法通常易于编码,也易于调试。这样你就有更多时间去解决那些暴力法无法快速解决的难题。
如果一个问题的可能情况不超过几百万个,就可以对所有情况进行遍历,验证哪个符合要求。
如何评估“可能性数量”(How to evaluate ‘number of possibilities’)
要判断是否可以使用暴力法,需要估算满足某个判定条件所需进行的总操作次数。这要求你对在最大输入规模下需要多少操作有一个大致的判断。
这正是“大 O 记法(Big-O Notation)”的用途。你会看到算法复杂度被描述为 O ( N ) O(N) O(N)、 O ( N l o g N ) O(NlogN) O(NlogN)、 O ( N 2 ) O(N²) O(N2) 等。这意味着在一般情况下,输入规模增加会使运行时间按 O ( ) O() O() 中的函数增长。
例如:
- 对于 O ( N ) O(N) O(N),如果 N N N 加倍,操作数也加倍。
- 对于 O ( N 2 ) O(N²) O(N2),当 N N N 为 1000 1000 1000 时,总操作量是 1 , 000 , 000 1,000,000 1,000,000,通常远超循环体中的基本操作速度,导致运行时间迅速增长。
一般来说, O ( N 2 ) O(N²) O(N2) 或更高阶的复杂度在 N N N 较大时会变得很慢,但有些编程题目的 N N N 较小, O ( N 2 ) O(N²) O(N2) 的复杂度仍可以接受。
判断程序复杂度最基本的方法是找出嵌套最深的循环,统计其中循环变量的数量。如果循环是无条件执行的,可以将各层循环次数相乘:
for i = 1 to N for j = 1 to Nfor k = 1 to N
上述是 3 3 3 层嵌套,每层大小为 N N N,时间复杂度为 O ( N 3 ) O(N³) O(N3)。
在典型的竞赛题中,内部计算通常非常快,大约需要几十或几百纳秒(十亿分之一秒),有时是几微秒(百万分之一秒)。但如果 N = 1000 N=1000 N=1000, O ( N 3 ) O(N³) O(N3) 的复杂度将使总运行时间达到秒级,往往过慢。
关于递归的说明
递归解法的复杂度评估与循环类似,但要额外考虑栈空间开销。每一层递归都会为局部变量分配栈空间。当递归层数达到大约 10 , 000 10,000 10,000 时,很可能超出内存限制。
建议:
- 避免无用的局部变量
- 计算最大递归深度,确保不会耗尽栈空间
注意,顺序执行的代码段最终运行时间由最慢的一部分决定。比如有三个 O ( N 2 ) O(N²) O(N2) 的循环和一个 O ( N 3 ) O(N³) O(N3) 的循环,整体复杂度就是 O ( N 3 ) O(N³) O(N3)。
小心陷阱(Careful, Careful)
有时候,并不容易看出是否可以使用穷举搜索。
-
例题:Party Lamps [IOI 1998]
问题描述:你有 N N N 个灯和 4 4 4 个开关:
- 第一个开关:切换所有灯的状态
- 第二个开关:切换编号为偶数的灯
- 第三个开关:切换编号为奇数的灯
- 第四个开关:切换编号为 1 , 4 , 7 , 10 , . . . 1, 4, 7, 10,... 1,4,7,10,... 的灯
给定 N N N、最多 10000 10000 10000 次按钮按压、以及某些灯的状态(如灯 7 7 7 是关的),要求输出所有可能的灯状态。
初始思路:
每次按按钮有 4 4 4 种选择,总共 4 10000 4¹⁰⁰⁰⁰ 410000 种组合(约为 1 0 6020 10⁶⁰²⁰ 106020),完全无法穷举。
简化观察:
按钮顺序无关 → 降为 1 0 4 10⁴ 104 次选择(约为 1 0 16 10¹⁶ 1016),仍然太大。
同一按钮按两次=没按 → 每个按钮只有按或不按两种状态,变为 2 4 = 16 2⁴=16 24=16 种组合。
最终只需遍历 16 16 16 种按钮组合,完全可行,暴力法即可解决。
-
例题:The Clocks [IOI 1994]
问题描述:一个 3 × 3 3×3 3×3 网格中的 9 9 9 个时钟,每个时间为 12 : 00 12:00 12:00、 3 : 00 3:00 3:00、 6 : 00 6:00 6:00 或 9 : 00 9:00 9:00。目标是将所有时钟都调成 12 : 00 12:00 12:00。你可以使用 9 9 9 种不同的“移动”,每种会使特定一组时钟顺时针旋转 90 90 90 度。
初始思路:
递归查找是否存在 1 1 1 步、 2 2 2 步… 的解,时间复杂度为 9 k 9^k 9k( k k k 为步数),效率低下。
优化观察:
移动顺序无关 → 降为 k 9 k⁹ k9,仍然不够快。
同一个移动做 4 4 4 次 = 没做 → 每个移动最多做 3 3 3 次
所以总组合为 4 9 = 262 , 144 4⁹ = 262,144 49=262,144,完全可以暴力搜索。
结论:有了这个观察后,暴力法是足够的。
-
示例题目(Sample Problems)Milking Cows [USACO 1996]
给出奶牛的挤奶时间段(例如 Farmer A: 3001000,Farmer B: 7001200),求:至少有一头牛在被挤奶的最长连续时间段;没有任何牛在被挤奶的最长时间段
-
Perfect Cows & Perfect Cow Cousins [USACO 1995]
定义:
- 完美数:所有真因子的和等于这个数(如 28 = 1 + 2 + 4 + 7 + 14 28 = 1+2+4+7+14 28=1+2+4+7+14)
- 完美对:两个数互为其因子和
- 完美集合:多个数的因子和依次对应下一个,最后一个因子和回到第一个
任务:
- 每头牛有编号( 1 ∼ 32000 1 \sim 32000 1∼32000)
- 找出所有编号为完美数的牛(完美牛)
- 找出所有能组成完美集合的牛群(完美牛亲族)