树木园探险之旅

来源:互联网 发布:康奈尔商学院 知乎 编辑:程序博客网 时间:2024/04/28 01:58

 

今天天气晴朗,你打算去树木园探险。土地爷爷跟你开了个玩笑:

“咱们玩个游戏吧!我心中想一个1~100之间的数字,你的任务是猜出它。

如果你猜小了,我会告诉你猜小了。

如果你猜大了,我会告诉你猜大了。但是,一旦你猜大了一次后,那么下一次你再猜的时候,我只会告诉你猜对了或者猜错了,而不会告诉你大小。

你猜对了,我便会告诉你。”

你在想:这老头,必定是太久没人过来看他,闷得慌。那我就大发慈悲地陪你玩玩。

于是你说:“请问我有多少次机会呢?”

“无限次。”

“好!”

你心想:为了谨慎起见,一开始不能猜太大。既然不能二分,那就四分。说不定四分就是最优解。

于是你说:“25。” “猜小了。”

“37。” “猜小了。”

“42。” “猜小了。”

“66。” “猜大了。”

你心想:这下糟了。剩下的数字只能随机猜了。早知道不猜那么大了。

“49。” “猜错了。”

“63。” “猜错了。”

你心想:我的幸运数字到底是多少呢?来个质数。

“43。” “恭喜你,猜对了。”

最终你获得了树木园探险之旅的入场券。

离开后,你越想越不甘心:我要找出最优解!回去写个brute force(暴力破解)的代码。

 

思考过程:

假设你第一次猜25。如果猜大了,剩下的1~24只能随机猜了。那么,在运气最差的情况下,你需要猜24次。如果猜小了,剩下的26~100又该如何去猜呢?

灵机一动。若能知道猜26~100的最少次数,就能知道,在第一次猜25的前提下,猜1~100的最少次数了。

定义  “猜a~b的最少次数”:对于土地爷爷心中想的a~b之间的那个数字(包括a和b),你猜出它所需的最少次数。

换句话说,无论土地爷爷心中所想的是哪一个数,都能保证在“最少次数”内猜对。

 

咦——这难道是dynamicprogramming(动态规划)?

 

因此,在第一次猜25的前提下,猜1~100的最少次数=max(24,猜26~100的最少次数)+1,其中max是取两者中的大的那个。+1是因为要加上第一次猜的那个25。

为什么是max呢?如果猜对了,当然最好啦,下一次就不用再猜了嘛。而猜大了和猜小了是两种不一样的情况,需要分开讨论。

 

于是乎,你便有了以下的思路:

猜1~100的最少次数=

在第一次猜1的前提下,=猜2~100的最少次数+1(因为不可能猜大了);

在第一次猜2的前提下,=max(1,猜3~100的最少次数)+1;

在第一次猜3的前提下,=max(2,猜4~100的最少次数)+1;

。。。。。。

在第一次猜24的前提下,=max(23,猜25~100的最少次数)+1;

在第一次猜25的前提下,=max(24,猜26~100的最少次数)+1;

在第一次猜26的前提下,=max(25,猜27~100的最少次数)+1;

。。。。。。

在第一次猜99的前提下,=max(98,猜100~100的最少次数)+1;

在第一次猜100的前提下,=99+1=100(因为不可能猜小了);

综上可得,取上述100个值的最小者,因为不知道第一次要猜哪个数好。

假设第一次猜25是最优策略,那么猜26~100的最少次数可以同理递归地算下去。

用一个二维数组Guess来保存中间状态,也就是说Guess[a][b]保存猜a~b的最少次数(1<=a<=b<=100)。这么一来,时间复杂度和空间复杂度都是O(n^2)。并没有什么优惠。

 

转念一想,猜1~20、15~34、71~90的最少次数必定是一样的。最优策略的第一次猜的数的相对位置是一样的。比如,猜1~20,假设第一次应该猜7:那么猜15~34时,第一次应该猜21;猜71~90时,第一次应该猜77。

这样一来,Guess数组只需保存不同区间长度对应的最少次数。也就是说,Guess[n]保存猜1~n的最少次数。空间复杂度降到了O(n)。

为了知道完整的猜数过程,记录猜的最少次数的同时,还需记录猜的该范围的第几个数。所以Guess[n]要保存猜1~n的最少次数和第一次要猜的数的相对位置(下标从1开始)。

边界情况:猜1~n(n=1)的最少次数和第一次要猜的数的相对位置都是1啦,因为只有一个数可以猜,就是它自己。

此刻,小精灵在你耳边细语:“动态规划:先解决更小的子问题,再解决更大的子问题。(Introduction to Algorithms, 3rd ed. P424)尽管思想是递归的,但是实现可以是迭代的。”

 

(代码和程序完整输出在文章末尾。)

程序关于猜数过程的输出:

猜1~100的最少次数是14
猜数过程:
9-->22-->34-->45-->55-->64-->72-->79-->85-->90-->94-->97-->99

 

所以,正确的猜数过程就是上面打印的顺序。若在某一次猜大了,剩下的范围随机猜就行了。

 

其实,上述序列不唯一。换一种思路,既然有14次机会,那么

第1次,1~100,猜14。剩下13次机会:若猜大了,用于随机猜1~13;若猜小了,用于猜15~100。

第2次,15~100,猜15+13-1=27。剩下12次机会:若猜大了,用于随机猜15~26;若猜小了,用于猜28~100。

第3次,28~100,猜28+12-1=39。剩下11次机会:若猜大了,用于随机猜28~38;若猜小了,用于猜40~100。

第4次,40~100,猜40+11-1=50。剩下10次机会:若猜大了,用于随机猜40~49;若猜小了,用于猜51~100。

第5次,51~100,猜51+10-1=60。剩下9次机会:若猜大了,用于随机猜51~59;若猜小了,用于猜61~100。

第6次,61~100,猜61+9-1=69。剩下8次机会:若猜大了,用于随机猜61~68;若猜小了,用于猜70~100。

第7次,70~100,猜70+8-1=77。剩下7次机会:若猜大了,用于随机猜70~76;若猜小了,用于猜78~100。

第8次,78~100,猜78+7-1=84。剩下6次机会:若猜大了,用于随机猜78~83;若猜小了,用于猜85~100。

第9次,85~100,猜85+6-1=90。剩下5次机会:若猜大了,用于随机猜85~89;若猜小了,用于猜91~100。

第10次,91~100,猜91+5-1=95。剩下4次机会:若猜大了,用于随机猜91~94;若猜小了,用于猜96~100。

第11次,96~100,猜96+4-1=99。剩下3次机会:若猜大了,用于随机猜96~98;若猜小了,用于猜100~100。

第12次,100~100,猜100。

 

你发现了规律。设x为猜1~100的最少次数,那么x满足

x+(x-1)+(x-2)+…+2+1>=100

解得x>=14。

 

最优策略就是,无论猜大了或是猜小了,接下来仍需猜的次数应尽可能接近,最好相等。

此刻,你接收到了小精灵的心灵感应:“贪心算法:每一步都是当前最优(局部最优),走到最后便是全局最优。(Introduction to Algorithms, 3rd ed. P424)”

你发现了秘密:第一次猜9~14的任意一个数都无所谓。因为14次机会,不仅可以猜1~100,而且可以猜1~105。

下次去树木园就好玩了!每次都猜同一串数字,那该多无趣啊!


#include<cstdio>#include<algorithm>using namespace std;const int ultimate_interval_length=100;struct {int number;int minimal_times;}Guess[ultimate_interval_length+1];void print_Guess_array(void){printf("Guess数组:\n");printf("i:猜的数字 猜的次数\n");for(int interval_length=0;interval_length<=ultimate_interval_length;interval_length++){printf("%-3d: %3d %3d\n",interval_length,Guess[interval_length].number,Guess[interval_length].minimal_times);}}void print_guessing_process(int a,int b){printf("猜%d~%d的最少次数是%d\n",a,b,Guess[b-a+1].minimal_times);printf("猜数过程:\n");int isfirst=1;while(a<b){if(isfirst) isfirst=0;else printf("-->");printf("%d",Guess[b-a+1].number+a-1);a+=Guess[b-a+1].number;}putchar('\n');}int main(void){Guess[1].number=1;Guess[1].minimal_times=1;for(int interval_length=2;interval_length<=ultimate_interval_length;interval_length++){//猜第1个数,猜小了Guess[interval_length].number=1;int minimal_times=Guess[interval_length-1].minimal_times+1;//猜中间(除去首尾2个)的数for(int guessing_number=2;guessing_number<=interval_length-1;guessing_number++){int minimal_times_for_guessing_number=max(guessing_number-1,Guess[interval_length-guessing_number].minimal_times)+1;//max(猜大了,猜小了)if(minimal_times_for_guessing_number<minimal_times){minimal_times=minimal_times_for_guessing_number;Guess[interval_length].number=guessing_number;}}//猜最后一个数,猜大了if(interval_length<minimal_times){minimal_times=interval_length;Guess[interval_length].number=interval_length;}Guess[interval_length].minimal_times=minimal_times;}print_Guess_array();print_guessing_process(1,ultimate_interval_length);return 0;}



程序完整输出:

Guess数组:
i:猜的数字 猜的次数
0  :   0   0
1  :   1   1
2  :   1   2
3  :   2   2
4  :   1   3
5  :   2   3
6  :   3   3
7  :   1   4
8  :   2   4
9  :   3   4
10 :   4   4
11 :   1   5
12 :   2   5
13 :   3   5
14 :   4   5
15 :   5   5
16 :   1   6
17 :   2   6
18 :   3   6
19 :   4   6
20 :   5   6
21 :   6   6
22 :   1   7
23 :   2   7
24 :   3   7
25 :   4   7
26 :   5   7
27 :   6   7
28 :   7   7
29 :   1   8
30 :   2   8
31 :   3   8
32 :   4   8
33 :   5   8
34 :   6   8
35 :   7   8
36 :   8   8
37 :   1   9
38 :   2   9
39 :   3   9
40 :   4   9
41 :   5   9
42 :   6   9
43 :   7   9
44 :   8   9
45 :   9   9
46 :   1  10
47 :   2  10
48 :   3  10
49 :   4  10
50 :   5  10
51 :   6  10
52 :   7  10
53 :   8  10
54 :   9  10
55 :  10  10
56 :   1  11
57 :   2  11
58 :   3  11
59 :   4  11
60 :   5  11
61 :   6  11
62 :   7  11
63 :   8  11
64 :   9  11
65 :  10  11
66 :  11  11
67 :   1  12
68 :   2  12
69 :   3  12
70 :   4  12
71 :   5  12
72 :   6  12
73 :   7  12
74 :   8  12
75 :   9  12
76 :  10  12
77 :  11  12
78 :  12  12
79 :   1  13
80 :   2  13
81 :   3  13
82 :   4  13
83 :   5  13
84 :   6  13
85 :   7  13
86 :   8  13
87 :   9  13
88 :  10  13
89 :  11  13
90 :  12  13
91 :  13  13
92 :   1  14
93 :   2  14
94 :   3  14
95 :   4  14
96 :   5  14
97 :   6  14
98 :   7  14
99 :   8  14
100:   9  14
猜1~100的最少次数是14
猜数过程:
9-->22-->34-->45-->55-->64-->72-->79-->85-->90-->94-->97-->99

0 0
原创粉丝点击