1枚举思想
我们总觉得枚举(enumeration)笨笨的,很多人就称之为“暴力算法(brute force enumeration)”;但是枚举却又总是我们面对算法问题时的第一反应。这让我想起很多生活在我们身边关心着我们的人,我们对他们总有点或烦或平淡无奇以致讨厌,但是我们却离不开,比如唠叨的老爸老妈。
首先我们看一看枚举思想的威力吧,我们就是通过这个例子,感悟(perceive)到了枚举只要运用得好,一点都不笨。
Pro.1 George likes arithmetics very much. Especially he likes the natural numbers series. His most favourite thing is the infinite sequence of digits, which results as the concatenation of all natural numbers in ascending order. The beginning of this sequence is 1234567891011121314… Let us call this sequence S. Then S[1] = 1, S[2] = 2, …, S[10] = 1, S[11] = 0, …, and so on.
George takes a sequence of digits A and wants to know when it first appears in S. Help him to solve this difficult problem.
Input
The first line contains A - the given sequence of digits. The number of digits in A does not exceed 200.
Output
Output the only number - the least k such that A[1] = S[k], A[2] = S[k+1], ... A[len(A)] = S[k + len(A) – 1], where len(A) denotes the length of A (i.e. the number of digits in it).
Sample
input |
output |
101 |
10 |
Problem Author: Nikita Shamgunov
Problem Source: ACM ICPC 2001. Northeastern European Region, Northern Subregion
|| 题目的大致意思就是说,给一个很长的数字串S:123456891011121314…,它是由所有的自然数从小到大依次排列起来的。任意给一个数字串S1,求出S1在S中第一次出现的位置。
不出意外的话,一般想到的都是KMP算法之类的串匹配算法吧。另外一种思维就是向数学问题转移,看是否有数学解法。对于串匹配,时间复杂度至少为O(10n),并不是有效算法。对于数学途径,我们就不得而知了,也许有牛人能想到很牛B的数学方法的。但是我们想到的是枚举。怎么枚举呢?
枚举S1可能是从哪些连续的数字段“截取”下来的。例如2132,如果视为2|13|2,则13为完整的一段,前面必为12,后面必为14,可马上出现了数字2,这样分段是不行的。因此如果有一个完整段的情况可以用O(n2)次枚举来判定。剩下只需要解决没有完整段的情况,即被分成两段。相信读者看到这里已经明白了,这里枚举的就是位数。依然是2132,如果是一位数连续,显然不成立;两位数连续,可被分为2|13|2,无解,被分为21|32,还是无解;如果是3位,2|132,无解,21|32,则有可能是321|322,213|2,则可能是213|214;对于四位数,也是依次比较,这里显然还可以运用贪心思想,即位数越少,在长数字串中出现得越早,如果在三位数时找到了合理值,就没必要继续枚举更多位数了。但是在该位数的所有情况一定要枚举完毕,213|214就比321|322更靠前。当然本题还需要注意有进位的情况。
我们一提到“枚举”就觉得很傻很笨,确实,枚举的本质就是一个个生成所有的方案,然后一个一个验证是否符合要求。下面是一个例子。
Pro.2 You must write a program that simulates placing spherical balloons into a rectangular box.
The simulation scenario is as follows. Imagine that you are given a rectangular box and a set of points. Each point represents a position where you might place a balloon. To place a balloon at a point, center it at the point and inflate the balloon until it touches a side of the box or a previously placed balloon. You may not use a point that is outside the box or inside a previously placed balloon. However, you may use the points in any order you like, and you need not use every point. Your objective is to place balloons in the box in an order that maximizes the total volume occupied by the balloons.
You are required to calculate the volume within the box that is not enclosed by the balloons.
The input consists of several test cases. The first line of each test case contains a single integer n that indicates the number of points in the set (1 <= n<= 6). The second line contains three integers that represent the (x, y, z) integer coordinates of a corner of the box, and the third line contains the (x, y, z) integer coordinates of the opposite corner of the box. The next n lines of the test case contain three integers each, representing the (x, y, z) coordinates of the points in the set. The box has non-zero length in each dimension and its sides are parallel to the coordinate axes.
The input is terminated by the number zero on a line by itself.
For each test case print one line of output consisting of the test case number followed by the volume of the box not occupied by balloons. Round the volume to the nearest integer. Follow the format in the sample output given below.
Place a blank line after the output of each test case.
Sample Input |
Sample Output |
2 0 0 0 10 10 10 3 3 3 7 7 7 0 |
Box 1: 774 |
这道题中,“笨拙”的枚举确实胜过了以高效著称的KMP算法。
“草木竹石均可为剑”,关键就在于是否有“御草木竹石而为剑”的功力。但是想问题仅仅到这一层是不够的,怎么才能拥有这种功力呢?努力。持续的努力。可是想问题到了这里还是不够,怎样去努力?于是就有了下面的追寻思维的轨迹的过程。
究竟是什么是的枚举得以凑效?我们认为是选准了最适合的枚举对象。其实我们更是认为,在任何情况下,选准最合适的对象[①],无论是枚举还是其他算法思想,都是最最关键的。
这个道理就不必多说了,有“老生常谈”、“陈词滥调”的嫌疑。我们想说的是另外感悟到的一点:选准(枚举)对象的根本原因还是在于优化,具体表现为减少求解步骤,缩小求解的解空间,或者是使程序更具有可读性和易于编写[②]。有时候选好了枚举对象,确定了枚举思想解决问题,问题就迎刃而解了,就像上面的例子,这往往需要的是发现的眼光;而有时候,当题目逼着你用枚举思想解题时,需要考虑的往往是从众多枚举对象中选择最适合的,这往往需要辨别的智慧。
我们来看一个“从众多枚举对象中选择最适合”的例子:五猴分桃。
Pro.3五只猴子一起摘了一堆桃子,因为太累了,它们商量决定,先睡一觉再分。一会其中的一只猴子来了,它见别的猴子没来,便将这堆桃子平均分成5份 ,结果多了一个,就将多的这个吃了,并拿走其中的一份。一会儿,第2只猴子来了,他不知道已经有一个同伴来过,还以为自己是第一个到的呢,于是将地上的桃子堆起来,再一次平均分成5份,发现也多了一个,同样吃了这1个,并拿走其中一份。接着来的第3、第4、第5只猴子都是这样做的……根据上面的条件,问这5只猴子至少摘了多少个桃子?第5只猴子走后还剩下多少个桃子?
|| 我们设总的桃子数为S0,五子猴子的桃子数分别为S1、S2、S3、S4、S5,则有以下关系式:S0 = 5*S1 + 1;4*S1 = 5*S2 + 1;4*S2 = 5*S3 + 1;4*S3 = 5*S4 + 1;4*S4 = 5*S5 + 1;我们可以枚举桃子总数S0,从5开始直到满足条件,此时S0的值就是最少的总桃子数。程序如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
#include
int main(void) { int s[6] = {0}; int i;
for(s[0]=5; ;s[0]++) { s[1] = s[0] - 1; if (s[1]%5) // (s[0] – 1)要能被5整除 continue; else s[1] /= 5;
s[2] = 4 * s[1] - 1; if (s[2]%5) // (4 * s[1] - 1)要能被5整除 continue; else s[2] /= 5;
s[3] = 4 * s[2] - 1; if (s[3]%5) continue; else s[3] /= 5;
s[4] = 4 * s[3] - 1; if (s[4]%5) continue; else s[4] /= 5;
s[5] = 4 * s[4] - 1; if (s[5]%5) continue; else s[5] /= 5;
break; } printf("摘了%d个桃子, 剩下%d个桃子/n", s[0], s[5]*4);
for (i=0; i<6; i++) printf("%d ", s[i]); getchar(); return 0; } |
程序输出:摘了3121个桃子, 剩下765个桃子。
根据程序结果我们知道循环体执行了3116次,同时我们可以知道第5个猴子分得255个桃子,所以如果枚举S5,则循环体只需执行了255次。对应程序如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
#include int main(void) { int s[6] = {0}; int i;
for(s[5]=1; ;s[5]++) { s[4] = 5 * s[5] + 1; if (s[4]%4) continue; else s[4] /= 4;
s[3] = 5 * s[4] + 1; if (s[3]%4) continue; else s[3] /= 4;
s[2] = 5 * s[3] + 1; if (s[2]%4) continue; else s[2] /= 4;
s[1] = 5 * s[2] + 1; if (s[1]%4) continue; else s[1] /= 4;
s[0] = 5 * s[1] + 1; break; }
printf("摘了%d个桃子, 剩下%d个桃子/n", s[0], s[5]*4); getchar(); return 0; } |
枚举S0和S5的差距是明显的[③]。
百度百科上有这样一段文字(特殊字体部分由我添加):
在进行归纳推理时,如果逐个考察某类事件的所有可能情况,因而得出一般结论,那么这结论是可靠的,这种归纳方法叫做枚举法。特点:将问题的所有可能的答案一一列举,然后根据条件判断此答案是否合适,合适就保留,不合适就丢弃。
枚举算法因为要列举问题的所有可能的答案,所有它具备以下几个特点:
1、得到的结果肯定是正确的;
2、可能做了很多的无用功,浪费了宝贵的时间,效率低下。
3、通常会涉及到求极值(如最大,最小,最重等)。
枚举思想的定义和特点说得很完善,也道出了在运用枚举思想思考时需要面对的问题:
表1:运用枚举思想思考时需要面对的问题
特点及要求 |
可能出现的问题 |
选取考察对象 |
选取的考察对象不恰当; |
逐个考察所有可能的情况 |
没有“逐个”考察,不恰当地遗漏了一些情况; 没有考察“所有”,对解空间集的确定失误; |
选取判断标准 |
判断标准“不正确”,导致结果错误; “不全面”,导致结果错误或得到结果的效率下降; “不高效”,意味着没有足够的剪枝; |
对于表1中第一种情况,前文已有大篇幅论述,这里对其思维仅做一总结:
在分析问题的时候,思维一定要具备开放性,不要总是局限于第一感觉限定的思维区域,比如Pro.1中,存在明显的第一思路KMP,如果仅仅局限于这第一感觉就不好了。把可能的各种思路、各种考察对象,各种条件都“枚举”出来,迅速思考各种对象、条件、思路之间的作用关系,而且,一般情况下,问题的主要矛盾通常就是值得注意的枚举对象,如Pro.1中的S1,亦即后文Pro.3中的“大书位置”,都是题目中明显的“主要矛盾”。我们另一apperception就是:我们选取考察对象不恰当,往往是因为没有把所有可能的考察对象考虑全面。我们之一的fairyprince坚称:思维的广度是思维深度得以凑效的前提之一。而要把所有可能的考察对象考察全面,就必须时刻跳出当前思维所作用的对象,寻找是否还有其他可考察对象。
所以,针对情形1的关键词是:思维广度 à抓主要矛盾+时刻准备迈出当前思考域。
||||||||||||||||||||||||||||||||||||||||||||||||
对于表1中的第二种情况,Pro1也有很鲜明的反映。对给定数字串的拆分有前几位丢失、后几位丢失的情况,有进位的情况,只要遗漏的一个整个算法就功亏一篑了。枚举对思维的细腻程度的要求其实很高。要做到无遗漏,我们想到的第一点依然是与第一感觉、第二感觉作斗争;第二就是要针对给定问题,建立起一套严格的分类学(taxology)标准,只有有了严格的分类标准,才能尽可能“枚举”到各种情况,关于分类学的讨论,可参看我们的这篇文章。
下面是使我们感悟(perceive)到这一点的例子之一(同时本题也反映出了另一重要的思维放学:分步学。分类与分步,本来就是密不可分的两兄弟)。
Pro4[④]. Castaway Robinson Crusoe is living alone on a remote island. One day a ship carrying a royal library has wrecked nearby. Usually Robinson brings any useful stuff from the shipwreck to his island, and this time he has brought a big chest with books.
Robinson has decided to build a bookcase for these books to create his own library. He cut a rectangular niche in the rock for that purpose, hammered in wooden pegs, and placed wooden planks on every pair of pegs that have the same height, so that all planks are situated horizontally and suit to act as shelves.
Unfortunately, Robinson has discovered that one especially old and big tome does not fit in his bookcase. He measured the height and width of this tome and has decided to redesign his bookcase in such a way, as to completely fit the tome on one of the shelves, taking into account locations of other shelves and the dimensions of the niche. With each shelf in the bookcase, one of the following operations should be made:
l Leave the shelf on its original place.
l Move the shelf to the left or to the right.
l Shorten the shelf by cutting off a part of the plank and optionally move it to the left or to the right.
l Move one of the pegs to a different place at the same height and move the shelf to the left or to the right.
l Shorten the shelf by cutting off a part of the plank, move one of the pegs to a different place at the same height, and optionally move the shortened shelf to the left or to the right.
l Remove the shelf from the bookcase along with both supporting pegs.
We say that the spported by their pegs and lengths of ahelf is properly supported by its pegs, if exactly two distinct pegs support the shelf and the center of the shelf is between its pegs or coincides with one of the pegs. The original design of Robinson's library has all the shelves properly sull shelves are integer number of inches. The Robinson may only cut an integer number of inches from the planks, because he has no tools for more precise measurements. All remaining shelves after the redesign must be properly supported by their pegs.
You are to find the way to redesign Robinson's library to fit the special old tome without changing original design too much. You have to minimize the number of pegs that are to be removed from their original places during the redesign (operations 4 and 5 remove one peg, and operation 6 removes two pegs). If there are different ways to solve the problem, then you are to find the one that minimizes the total length of planks that are to be cut off (operations 3 and 5 involve cutting something from the planks, and operation 6 counts as if cutting off the whole plank). Width of planks and diameter of pegs shall be considered zero.
The tome may not be rotated. The tome should completely (to all its width) stand on one of the shelves and may only touch other shelves, their pegs or niche's edge.
Input
The first line of the input file contains four integer numbers XN, YN, XT, and YT, separated by spaces. They are, correspondingly, width and height of the niche, and width and height of the old tome in inches (1 ≤ XN, YN, XT, YT ≤ 1000).
The second line of the input file contains a single integer number N (1 ≤ N ≤ 100) that represents the number of the shelves. Then N lines follow. Each line represents a single shelf along with its two supporting pegs, and contains five integer numbers yi, xi, li, x1i, x2i, separated by spaces, where:
yi (0 < yi < YN) - the height of the ith shelf above the bottom of the niche in inches.
xi (0 ≤ xi < XN) - the distance between the left end of the ith shelf and the left edge of the niche in inches.
li (0 < li ≤ XN - xi) - the length of the ith shelf in inches.
x1i (0 ≤ x1i ≤ li/2) - the distance between the left end of the ith shelf and its leftmost supporting peg in inches.
x2i (li/2 ≤ x2i ≤ li; x1i < x2i) - the distance between the left end of the ith shelf and its rightmost supporting peg in inches.
All shelves are situated on different heights and are properly supported by their pegs. The problem is guaranteed to have a solution for the input data.
Output
The output file shall contain two integer numbers separated by a space. The first one is the minimal number of pegs that are to be removed by Robinson from their original locations to place the tome. The second one is the minimal total length of planks in inches that are to be cut off during the redesign that removes the least number of pegs.
Sample input #1
11 8 3 4
4
1 1 7 1 4
4 3 7 1 6
7 2 6 3 4
2 0 3 0 3
Output for the sample input #1
0 0
Sample input #2
11 8 4 6
4
1 1 7 1 4
4 3 7 1 6
7 2 6 3 4
2 0 3 0 3
Output for the sample input #2
1 3
|| 首先挖掘一个条件:任意两块木板不出在同一水平线上。这样,某块木板移动后不会存在和其他木板重叠的情况,因而我们可以孤立[⑤]地考虑每块木板的移动与锯下,这样,问题简洁明了许多。
可以将问题具化为两个步骤:,确定安放大书的位置;,用最少的开销将其他书也摆上书架。由于题目要求在移动最少木栓的前提下浪费最短的木板,而这两个步骤又是独立的,即可以分别执行(考虑),于是可以确定如下的思维步骤:
最先尝试“不移动木栓也不锯木板”的方案,如果失败,
尝试“不移动木栓且锯最少的木板”的方案,如果失败,
尝试“移动一个木栓同时不锯木板”的方案,如果失败,
尝试“移动一个木栓且锯最少木板”的方案,如果失败,
尝试“撤去整个木板”的方案。
、确定安放大书的位置——选取大书位置(本题中的主要矛盾)为枚举对象,亦可理解为自变量:
由于大书必定安放在某块木板i上,所以只要大书放上去不会顶上天花板就行。针对木板i枚举大书的安放位置。该步骤中可以不考虑其他书架,只要大书安安稳稳放在了木板i上就百事OK!由于锯下木板对于大书的稳定位置并无好处[⑥],而且支撑木板i是不能撤去的,因此只需要检查“不移动木栓也不锯木板”和“移动一个木栓同时不锯木板”两种情况就行了——这就是强化判断标准,后文还会讨论。
、用最少的开销将其他书也摆上书架——因变量:
现在,放置大书的木板已经妥当了,但是却不一定能把所有的书都放上去,因为刚刚并没有考虑其他的书架,而实际上,它们极有可能把大书挡住;另外为了找到最少开销的解——通常会涉及到求极值,也必须考虑其他书籍的情况。好在由于每块木板的移动和木板的锯下也可以独立考虑,可以分别用最小代价消除每块挡住大书的木板,再累加起来。对于没有挡住大书的木板S,先尝试“不移动木栓但锯下最少的木板”(锯下后木板必须能在两木栓之间保持平衡——枚举中必须考虑所有可能),然后考虑“移动一个木栓同时将锯下最少木板”(这包含了“移动一个木栓但不锯下木板”的情况)。如果还是不行,就来最后一招:撤去整个木板。
细心的读者可能已经发现,上面的文字中包含了“分类思想”、“分步思想”、 “强化判断标准”思想等等,这些,都是枚举思想的内容——虽然并不能说从属于枚举思想。但是为本题理清思路出力最大的,无疑是分类思想。
虽然这里不准备详细讨论分类思想,但是分类思想中一个重要指标——无重复——有必要强调一下。众所周知枚举思想解题运算量大,当问题的规模变大,循环的阶数越大,执行的速度越慢,如果再在枚举的过程中出现重复,那简直不可原谅。
还是看一个例子。
Pro.5 翻硬币(Flip Coin)[⑦]
There is an ancient solitaire game named "the flip game". It consists of an array of M rows and 9 columns of two-colored pegs, with a black and a white side. When a peg with its white side showing is flipped, it shows its black side, and the other way around.
In each move of the game the player flips an entire row or an entire column.
The objective of the game is to leave as few pegs on their black side on the board as possible, by doing any number of moves.
Input
Your program should read the input from the file INPUT.TXT. The first line contains one positive integer number M (1 <= M <= 1000), denoting the number of rows in the game board. The next M consecutive lines contain exactly 9 characters, which are "0"s or "1"s, separated by one space character, where "0" means a peg showing its white side and "1" means a peg showing its black side.
Output
Your program should produce its output into the file OUTPUT.TXT as follows:
There will be only one line of text, containing the minimum possible number of pegs showing their black side, which are left on the game board.
Sample Input.txt Sample Output.txt
4 1
1 1 1 1 1 1 1 1 1
1 0 0 0 0 0 0 0 0
1 0 0 0 0 0 0 0 0
1 0 0 0 0 0 0 0 0
Time Limit per test: 3 seconds
看完题的第一感觉就知道要用枚举,但是关键在于,从何处入手呢?在程序中的枚举必然是循环来控制,而看着题目给出的那些数字矩阵,就知道循环必然是以行或者列为对象。但是这个成立吗?有一个关键点,硬币翻两次等于不翻,所以所有行/列最多翻一次。如果跳过了这个能产生重复的陷阱,不但规避了N多制约效率的重复,还打开了整个题目的突破口——行(列)确定以后每列(行)独立(又是独立!)。另外一个问题是,是选择行还是列为枚举对象呢?注意到行有很多但是列不多,枚举每列是否翻有29=512种情况,此时可以用O(n)的时间计算每行是否翻。
上面说了这么多,用一句话来总结:针对情形2的关键词是:思维细腻度 à分类学+分步学。
||||||||||||||||||||||||||||||||||||||||||||||||
对于表1中的第三种情况——选取判断标准,在Pro.3中也已经很鲜明体现出来。我们有这样的感悟,大多数枚举及其家族类的诸如DFS、回溯、分支限界等算法的正确性和优化,几乎都是对判断标准的强化和精化。
首先看一个论证正确性的简单例子。
Pro.6 一元三次方程求解
问题描述:形如ax3+bx2+cx+d=0 的一个一元三次方程。给出该方程中各项的系数(a,b,c,d 均为实数),并约定该方程存在三个不同实根(根的范围在-100至100之间),且根与根之差的绝对值大于或等于1。要求由小到大依次在同一行输出这三个实根(根与根之间留有空格),并精确到小数点后2位。
提示:记方程f(x)=0,若存在2个数x1和x2,且x1
样例
输入:1 -5 -4 20
输出:-2.00 2.00 5.00
|| 题目提示很符合二分法求解的原理,所以此题可以用二分法。用二分法解题相对于枚举法来说很要复杂很多。此题是否能用枚举法求解呢?再分析一下题目,根的范围在-100到100之间,结果只要保留两位小数,我们不妨将根的值域扩大100倍(-10000≤x≤10000),再以根为枚举对象,枚举范围是-10000到10000,用原方程式进行一一验证,找出方程的解。
下面是很容易想到的方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
#include void Function(double xa[], double a, double b, double c, double d);//解方程
int main() { double a = 1, b = -5, c = -4, d = 20; double x[3] = {0};
printf("%f %f %f %f/n", a, b, c, d); Function(x, a, b, c, d);//解方程 printf("%.2f %.2f %.2f/n", x[0], x[1], x[2]); getchar(); return 0; }
void Function(double xa[], double a, double b, double c, double d)//解方程 { int i; double x; int top = 0;
for (i=-10000; i<=10000; i++)//将根的值域扩大100倍 { x = (i * 1.0) / 100;//再变回来 if (((a * x + b) * x + c) * x + d == 0) //有解 { xa[top++] = x; } } } |
可是这种解法是错的。错在哪里呢?前面的分析没错,难道这题不能用枚举法做吗?当然不是。上面的解法中,枚举范围和枚举对象都没有错,但在验证枚举结果时判定条件不正确。因为要保留二位小数,所以求出来的解不一定是方程的精确根,再代入ax3+bx2+cx+d中,所得的结果也就不一定等于0,因此用原方程ax3+bx2+cx+d=0作为判断条件是不准确的。
我们换一个角度来思考问题,设f(x)=ax3+bx2+cx+d,若x为方程的根,则根据提示可知,必有f(x-0.005)*(x+0.005)<0,如果我们以此为枚举判定条件,问题就逆刃而解。另外,如果f(x-0.005)=0,或f(x+0.005)=0,那么就说明(x-0.005)或(x+0.005)是方程的根,这时根据四舍五入,方程的根也为x。所以我们用(f(x-0.005)*f(x+0.005) ≤0)作为判定条件。为了程序设计的方便,我们设计一个函数F(x)计算ax3+bx2+cx+d的值,看程序:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
double F(double a, double b, double c, double d, double x)//函数表达式 { return ((a * x + b) * x + c) * x + d; }
void Function(double xa[], double a, double b, double c, double d) { int i; double x; int top = 0;
for (i=-10000; i<=10000; i++)//将根的值域扩大100倍 { x = (i * 1.0) / 100;//再变回来 if (F(a, b, c, d, x-0.005)*F(a, b, c, d, x+0.005) <= 0) //有解 { xa[top++] = x; } } } |
下面是一个论证效率的例子。
Pro.7整数变换问题。关于整数i的变换f和变换g的定义如下:f(i)=3i; g(i)=└i/2┘。试设计一个算法,对于给定的两个整数n和m,用最少的f和g变换次数将n变换为m。例如,可以将整数15用4次变换为整数4,即4=gfgg(15)。具体变换如下:g(15)=7,g(7)=3,f(3)=9,g(9)=4。
|| 这道题说成是枚举算法似乎并不太合适,对数据结构敏感一点的人,肯定会说成“树的广度遍历”,对搜索敏感一点的人则又会说成广度优先搜索。不过就像我们前面说到的:搜索算法其实都是枚举思想这个家族的。
本题要转换一下主从关系,即想法子得出从 m 到 n,而后再反向输出,m 就是树根,而后 ×2、×2+1、÷3(这个必须在 %3==0 的情况才生成),得出下一级所有分枝,一次循环生成一层,如果在生成一层的过程中遇到 n 就表明已经得到目标,生成整个步骤(逆向输出到 fs)而后退出循环。
但是广度遍历生成的子枝太多,不剪的话内存可能都会成问题。于是,必须对枚举的判断标准加以精化:
如下三种情况需要剪掉(k是当前节点):
1、如果 k%3 不是 0,不存在对应 /3 分枝。
2、如果 k*2、k*2+1 小于 k 表明已经超过了 该题所能容纳的整数上限(规定剪除)。
3、如果已经得到节点集合中已经包括了 k*2、k*2+1、k/3,表明已经生成过这个点,丢弃。这种只可能导致更大长度的生成方案。
核心部分代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
int transform(int n, int m, char *fs) { static short ref[32768], parent[32768]; short lnuml[256],lnum_cur,refcur=0; char getflag; int num=INT_MAX; memset(parent,0,sizeof(parent)); ref[0] = m, refcur = 1; getflag=0, lnum_cur=0; lnuml[lnum_cur++] = 0, lnuml[lnum_cur++]=1; do { for(short i=lnuml[lnum_cur-2] ; !getflag && i<lnuml[lnum_cur-1]; ++i) { int src = ref[i],curv; curv = (src<<1); if(!getflag && curv>src && !parent[curv]) ref[refcur++] = curv, parent[curv] = src, getflag= (curv==n); if(!getflag && (++curv)>src && !parent[curv]) ref[refcur++] = curv, parent[curv] = src, getflag= (curv==n); curv = src / 3; if( !getflag && (curv*3==src) && !parent[curv] ) ref[refcur++] = curv, parent[curv] = src, getflag= (curv==n); } if( getflag ) { short j=refcur-1,cur,prev,k; cur = ref[j]; for(k=0; j>0 && (k+2)<=lnum_cur; ++k) { prev = parent[cur]; fs[lnum_cur-k-2] = 'f'+(cur>prev), cur = parent[cur]; } fs[k] = 0, num = k; } else lnuml[lnum_cur++] = refcur; } while(!getflag && lnum_cur<250); return(num); } |
我们回头看前面两道题,反思总结在运用枚举思想解题的过程中到底应该怎样做,才能保证正确性和效率。
正确性与效率都必须的一个前提:
Ø 一字一句寻找题中所有或明显、或隐含[⑧]、或浓墨重彩地强调、或一笔带过的约束条件(constraints&&bounds),真正做到“一个都不能少”,这需要细腻的分析。
关于效率的apperceptions:
Ø 对这些约束条件进行整合;具体来说就是要弄清楚各个约束条件可以检验什么,这在Pro.6中有很鲜明的体现。检验目标相同的约束条件就可以放在一起使用——其实到了这一步,就可以开始构思程序结构了。另外,这种整合也有对约束条件进行修改的意思,前提是不影响正确性。
关于正确性的apperceptions:
Ø 在找到了题中所有constraints&&bounds之后,不要不自觉地缩小甚至篡改既有约束条件。
上面说了这么多,用一句话来总结:针对情形3的关键词是:(条件)信息的整合 à细腻的分析+客观。
||||||||||||||||||||||||||||||||||||||||||||||||
G.Summary
以上较为详细地记录了我们自身对枚举法的感悟:感受,领悟。在很多时候,无法立刻得出某个问题的可行解或者最优解,但是可以用一种比较“笨”的方法,通过列举所有情况,然后逐一判断所得结果,这就是枚举。
枚举的定义决定了它思想简单,非常容易想到。但是有时候简单的东西也有很不简单的时候,对于“枚举”这一算法思想,可以具化为三种情况:
枚举对象选择不恰当;
对需要考察的情况不是出现遗漏,就是出现重复;
对枚举出来的情况的检验不是检验方法出错,就是检验方式低效。
所以在运用枚举的过程中,我们一定要有“抓主要矛盾”和“时刻准备迈出当前思考域”的意识,运用“分类学”和“分步学”的思想,对题中各种关系进行“细腻”全面的考察,并对考察结果进行全方位地、客观地“整合”。这样就能最大限度地规避以上的错误,并发挥出枚举思想的威力。
枚举法是后面几章马上就要讨论的搜索、回溯等算法思想的“近邻”,理解了枚举法对其他算法思想的感悟是有好处的。往后我们会经常看见,很多问题中,虽然全局不会使用枚举思想,但可以局部使用枚举法,算法设计的难度会减小很多——有些时候局部哪些微乎其微的时间优化是不理智的。
枚举法的价值比我们想象中要强大,在问题毫无头绪之时,枚举[⑨]往往可以为我们打开一个缺口,本书后面的内容会就经常会出现这样的例子。不要鄙视或者害怕枚举,“草木竹石皆可未剑”,重要的是掌握权衡枚举的开销与获得的信息量之间的关系。
||||||||||||||||||||||||||||||||||||||||||||||||
G.Excursus
本章是全书正文第一章,但是却完全没有按照《写在前面》中说述的模式写作,至少感受和理论分析的分界线不明显,以至于写作并不清晰,也没有Amusing Item。这并非是我们的食言,首先对枚举思想的理论分析几乎本身就是一种多余,这太容易理解了,理论分析也无多意义;其次,关于Amusing Item,如果我们没有什么值得记录的东西,何必装B呢?
另外本部分我没有搜索历史上的论文,必要性是主要原因,可行性也是原因之一。
[①] 原来计算机科学中的很多道理和人生中很多是一样一样的。
[②] 不要忘记了Steve McConnell的箴言:编写程序应该以人为本,计算机第二。
[③] 当然, 程序还可以写得更简练更美观一些。
[④] ACM Regional Contest Northeast Europe 2001, Library. 感谢刘汝佳《算法艺术与信息学竞赛》一书的启发。
[⑤] 在概率论中,我们见到“独立变量”的概率问题往往更加兴奋,因为它们更简单和直接。所以虽然我们在前文(和后文)中不断重复“寻找各个事物之间的联系”的观念,但是我们也同时重视“独立各事物”以求解的思维。所有这些关于思维方略的讨论,我们会试图在全书结尾处做一总结。
[⑥] 显然题目保证了木板的稳定性与其书本的重心位置无关,否则会提供相关数据。
[⑦] Balkan olympiad informatics 1999.
[⑧] 从题中推断出来的constraints && bounds都属于隐含条件。
[⑨] 另一利器是对问题进行“特化”,本系列后面的内容中会有讨论。