本节书摘来自异步社区《趣题学算法》一书中的第1章1.1节累积计数法,作者徐子珊,更多章节内容可以访问云栖社区“异步社区”公众号查看。
第1章 计数问题
趣题学算法
1.1 累积计数法
1.2 简单的数学计算
1.3 加法原理和乘法原理
1.4 图的性质
1.5 置换与轮换
人类的智力启蒙发端于计数。原始人在狩猎过程中为计数猎获物,手指、结绳等都是曾经使用过的计数工具。今天,我们所面对、思考的问题更加复杂、庞大,计数的任务需要强大的计算机来帮助我们完成。事实上,很多计算问题本身就是计数问题。
1.1 累积计数法
这样的问题在实际中往往要通过几个步骤来解决,每个步骤都会产生部分数据,问题的目标是计算出所有步骤产生数据的总和。对这样的问题通常设置一个计数器(变量),然后依步骤(往往可以通过循环实现各步骤的操作)将部分数据累加到计数器,最终得到数据总和。
问题描述
国王用金币赏赐忠于他的骑士。骑士在就职的第一天得到一枚金币。接下来的两天(第二天和第三天)每天得到两枚金币。接下来的三天(第四、五、六天)每天得到三枚金币。接下来的四天(第七、八、九、十天)每天得到四枚金币。这样的赏赐形式一直延续:即连续N天骑士每天都得到N枚金币后,连续N+1天每天都将得到N+1枚金币,其中N为任一正整数。
编写一个程序,对给定的天数计算出骑士得到的金币总数(从任职的第一天开始)。
输入
输入文件至少包含一行,至多包含21行。输入中的每一行(除最后一行)表示一个测试案例,其中仅含一个表示天数的正整数。天数的取值范围为1~10000。输入的最后一行仅含整数0,表示输入的结束。
输出
对输入中的每一个测试案例,恰好输出一行数据。其中包含两个用空格隔开的正整数,前者表示案例中的天数,后者表示骑士从第一天到指定的天数所得到的金币总数。
输入样例
10
6
7
11
15
16
100
10000
1000
21
22
0
输出样例
10 30
6 14
7 18
11 35
15 55
16 61
100 945
10000 942820
1000 29820
21 91
22 98
解题思路
(1)数据的输入与输出
根据题面对输入数据格式的描述,我们知道输入文件中包含多个测试案例,每个测试案例的数据仅占一行,且仅含一个表示骑士任职天数的正整数N。N=0是输入结束标志。对于每个案例,计算所得结果为国王赐予骑士的金币数,作为一行输出到文件。按此描述,我们可以用下列过程来读取数据,处理后输出数据。
1 打开输入文件inputdata
2 创建输出文件outputdata
3 从inputdata中读取案例数据N
4 while N>0
5 do result←GOLDEN-COINS(N)
6 将"N result"作为一行写入outputdata
7 从inputdata中读取案例数据N
8 关闭inputdata
9 关闭outpudata
其中,第5行调用计算骑士执勤N天能得到金币数的过程GOLDEN-COINS(N)是解决一个案例的关键。
(2)处理一个案例的算法过程
问题中的一个案例,是典型的累积计数问题。如果测试案例给出的天数N 存在k,使得和数sumnolimits^k_{i=1}i恰为N,则骑士第N天总计所得金币数为nolimitssum^k_{i=1}i^2。例如题面中的第一个测试案例N=10(=1+2+3+4)就是这样的情形,所得金币数为12+22+32+42=30。一般地,有N=sumnolimits^k_{i=1}i+j,其中0leqslantjleqslantk 。此时,只要计算出j,骑士所得金币就应是nolimitssum^k_{i=1}i^2+(k+1)×j。例如,题面的第三个测试案例中的7=(1+2+3)+1,所得金币数为12+22+32+4×1=18。金币数中的nolimitssum^k_{i=1}i^2部分显然可以用循环累加而得(同时跟踪天数nolimitssum^k_{i=1}i)。由于计算金币数中nolimitssum^k_{i=1}i^2部分时所跟踪的天数nolimitssum^k_{i=1}ileqslantN,所以,N-nolimitssum^k_{i=1}i就是N=nolimitssum^k_{i=1}i+j中的j。这样,我们就可以将问题分成k个阶段,每个阶段的部分金币数为i2(1leqslantileqslantk),必要时(N>nolimitssum^k_{i=1}i)还有一个步骤。此时,设j=N−nolimitssum^k_{i=1}i,这一步骤中所得金币数应为j(k+1)。将每一步骤中所得的部分金币数累加即为所求,可将其描述成如下伪代码过程。
GOLDEN-COINS(N)
1 coins←0, k←1, days←0
2 while days+k\leqslantN
3 do coins←coins+k*k ▷coins=
4 days←days+k ▷days=
5 k←k+1
6 j←N-days ▷计算N=+j中的j
7 coins←coins+k*j
8 return coins
算法1-1 对已知的天数N,计算从第1天到第N天总共所得金币数的过程
算法1-1中设置了两个计数器days和coins,分别表示骑士工作的天数和所得的金币数(在第1行初始化为0)。k是循环控制变量,第2~5行的while循环即实现coins的k步累加。第6~7行完成可能发生的第k+1(N>nolimitssum^k_{i=1}i时)步计算。
算法中第1、6、7、8行所需都是常数时间,分别为3、2、3和1。第2~5行的while循环至多重复sqrt{N}次(这是因为1+2+…+kleqslantN当且仅当k2+kleqslant2N,当且仅当k
解决本问题的算法的C++实现代码存储于文件夹laboratory/Golden Coins中,读者可打开文件GoldenCoins.cpp研读,并试运行之。
问题描述
你能将一摞扑克牌在桌边悬挂多远?若有一张牌,你最多能将它的一半悬挂于桌边。若有两张牌,最上面的那张最多有一半伸出下面的那张牌,而底下的那块牌最多伸出桌面三分之一。因此两张牌悬挂于桌边的总长度为1/2 + 1/3 = 5/6。一般地,对n张牌伸出桌面的长度为1/2 + 1/3 + 1/4 + … + 1/(n + 1),其中最上面的那块牌伸出其下的牌1/2,第二块牌伸出其下的那块牌1/3,第三块牌伸出其下的那块牌1/4,以此类推,最后那块牌伸出桌面1/(n+1)。如图1-1所示。
输入
输入包含若干个测试案例,每个案例占一行。每行数据包含一个有两位小数的浮点数c,取值于[0.01, 5.20]。最后一行中c为0.00,表示输入文件的结束。
输出
对每个测试案例,输出能达到悬挂长度为c的最少的牌的张数。需按输出样例的格式输出。
输入样例
1.00
3.71
0.04
5.19
0.00
输出样例
3 card(s)
61 card(s)
1 card(s)
273 card(s)
解题思路
(1)数据的输入与输出
根据题面描述,输入文件的格式与问题1-1的相似,含有多个测试案例,每个案例占一行数据,其中包含表示扑克牌悬挂于桌边的总长度的数据c。c=0.0是输入数据结束的标志。对每个案例数据c进行处理,计算所得的结果为能悬挂于桌边的总长度为c的扑克牌的张数,按格式“张数card(s)”作为一行输出文件。
1 打开输入文件inputdata
2 创建输出文件outputdata
3 从inputdata中读取案例数据c
4 while c≠0.0
5 do result← HANGOVER(c)
6 将"result card(s) "作为一行写入outputdata
7 从inputdata中读取案例数据c
8 关闭inputdata
9 关闭outpudata
其中,第5行调用计算能悬挂在桌边的长度为c的扑克牌张数的过程HANGOVER(c)是解决一个案例的关键。
(2)处理一个案例的算法过程
对每一个测试案例的输入数据c,根据题意就是要求出max {n|nin Nu ,sumnolimits_{i=1}^{n}{1/(i+1)} underline{Omega } c}。写出伪代码过程如下。
HANGOVER(c)
1 n←1, length←0
2 while length<c
3 do length←length+1/(n+1)
4 n←n+1
5 if length>c
6 then n←n-1
7 return n
算法1-2 对已知的纸牌悬挂长度c,计算纸牌张数的过程
算法中,第1行设置了两个计数器:n(初始化为1)和length(初始化为0)分别表示扑克牌张数和悬挂在桌边的长度。第2~4行的while循环的重复执行条件是lengthc,则意味着n应当减少1(这就是第5~6行的功能)。
算法的运行时间依赖于第2~4行的while循环重复次数n。由于
`
javascript
1/2+1/3+…+1/n
leqslant1+1/2+1/3+…+1/(2⌈n/2⌉-1)
=1+(1/2+1/3)+(1/22+1/(22+2)+1/(22+3))+…
+(1/2i+1/(2i+1)+…+1/(2i+2i-1))+…
+(1/2lg⌈n/2⌉+1/(2lg⌈n/2⌉+1)+…+1/(2lg⌈n/2⌉+2lg⌈n/2⌉-1))
<1+(1/2+1/2)+(1/22+1/22+1/22+1/22)+…
+([underbrace{1/{{2}^{i}}+1/{{2}^{i}}+ldots +1/{{2}^{i}}}_{{{2}^{i}}}])+…
+([underbrace{1/{{2}^{lg leftlceil n/2 rightrceil }}+1/{{2}^{lg leftlceil n/2 rightrceil }}+ldots +1/{{2}^{lg leftlceil n/2 rightrceil }}}_{{{2}^{lg leftlceil n/2 rightrceil }}}])
=[underbrace{1+1+ldots +1}_{leftlceil n/2 rightrceil }]=Θ(lgn)
即c=Θ(lgn),亦即n=Θ(2c)。于是该算法的运行时间T(c)=n=Θ(2c)。幸好c介于0.01~5.20之间,否则当c很大时,算法是极费时的。
解决本问题的算法的C++实现代码存储于文件夹laboratory/Hangover中,读者可打开文件Hangover.cpp研读,并试运行之。C++代码的解析请阅读9.1节中程序9-1的说明。
<div style="text-align: center">
<img src="https://yqfile.alicdn.com/a21a771de97223d42c770663e0d2cf90c8bbdd8e.png" >
</div>
问题描述
魔法师百小度也有遇到难题的时候——现在,百小度正在一个古老的石门面前,石门上有一段古老的魔法文字,读懂这种魔法文字需要耗费大量的能量和脑力。
过了许久,百小度终于读懂了魔法文字的含义:石门里面有一个石盘,魔法师需要通过魔法将这个石盘旋转X度,以使上面的刻纹与天相对应,才能打开石门。
但是,旋转石盘需要N点能量值,而为了解读密文,百小度的能量值只剩M点了!破坏石门是不可能的,因为那将需要更多的能量。不过,幸运的是,作为魔法师的百小度可以耗费V点能量,使得自己的能量变为现在剩余能量的K倍(魔法师的世界你永远不懂,谁也不知道他是怎么做到的)。例如,现在百小度有A点能量,那么他可以使自己的能量变为(A-V)×K点(能量在任何时候都不可以为负,即:如果A小于V的话,就不能够执行转换)。
然而,在解读密文的过程中,百小度预支了他的智商,所以他现在不知道自己是否能够旋转石盘并打开石门,你能帮帮他吗?
输入
输入数据第一行是一个整数T,表示包含T组测试案例。
接下来是T行数据,每行有4个自然数N, M, V, K(字符含义见题目描述)。
数据范围如下:
T\leqslant100
N, M, V, K \leqslant 108
输出
对于每个测试案例,请输出最少做几次能量转换才能够有足够的能量点开门;如果无法做到,请直接输出“−1”。
输入样例
4
10 3 1 2
10 2 1 2
10 9 7 3
10 10 10000 0
输出样例
3
-1
-1
0
解题思路
(1)数据的输入与输出
题面告诉我们,输入文件的第一行给出了测试案例的个数T,其后的T行数据,每行表示一个案例,读取每个案例的输入数据N, M, V, K,处理后得到的结果是能量转换次数(若经过若干次能量转换能够打开石门)或−1(不可能打开石门),并将所得结果作为一行写入输出文件。表示成伪代码过程如下。
1 打开输入文件inputdata
2 创建输出文件outputdata
3 从inputdata中读取案例数T
4 for t←1 to T
5 do 从inputdata中读取案例数据N, M, V, K
6 result← ENERGY-CONVERSION(N, M, V, K)
7 将result作为一行写入outputdata中
8 关闭inputdata
9 关闭outpudata
其中,第6行调用计算百小度最少能量转换次数的过程ENERGY-CONVERSION(N, M, V, K)是解决一个案例的关键。
(2)处理一个案例的算法过程
对于问题输入中的一个案例数据N, M, V, K,需考虑两个特殊情况:
① M \geqslantN,即百小度一开始就具有足够的能量打开石门。此时,百小度立刻打开石门。
② M<N且M<V ,百小度必须增加能量才可能打开石门,但按题面,一开始就不可能进行能量转换。所以百小度不可能打开石门。
一般情况下,(即M<N且M\geqslantV),从A =M开始,模拟百小度反复转换能量A←(A-V)×K,设置跟踪转换能量的次数的计数器count,直至能量足以打开石门为止(即A\geqslantN),count 即为所求。在这一过程中,需要监测能量转换A←(A−V)×K是否增大了能量A,如果检测到某次转换后A\geqslant (A−V)×K,那意味着从此不可能增大能量,所以在这种情况下百小度也不能打开石门。
将上述思考写成伪代码如下。
ENERGY-CONVERSION(N, M, V, K)
1 A←M, count←0
2 if AgeqslantN ▷情形①
3 then return 0
4 if A5 then return -1;
6 repeat
7 if Ageqslant(A-V)*K ▷转换不能增大能量
8 then return -1;
9 A←(A-V)*K;
10 count ← count +1;
11 until AgeqslantN
12 return count
算法1-3 对一个案例数据N, M, V, K,计算最少能量转换次数的过程
算法1-3中,第1、12行耗时为常数。第2~3行和第4~5行的if结构也都是常数时间的操作。第6~11行的repeat-until结构,A从M开始,循环条件是A\geqslantN,每次重复第9行将使A至少增加1,所以至多重复N-M次。因此,过程ENERGY-CONVERSION的运行时间为O(N-M)。
解决本问题的算法的C++实现代码存储于文件夹laboratory/Energy Conversion中,读者可打开文件Energy Conversion.cpp研读,并试运行之。C++代码的解析请阅读第9章9.1.2节中程序9-3的说明。
<div style="text-align: center">
<img src="https://yqfile.alicdn.com/06f831f01d1a031f53f9d4c092f6dd0f51dc5014.png" >
</div>
描述
牛妞Betsy绕着谷仓闲逛时,发现农夫John建了一个秘密的暖房,里面培育了各种奇花异草,五彩缤纷。Betsy惊喜万分,她的小牛脑瓜里顿时与暖房一样充满了各色的奇思妙想。
“我要沿着农场篱笆挖上一排共F(7\leqslantF\leqslant10000)个种花的坑。”Betsy心里想着。“我要每3个坑(每隔2个坑)种一株玫瑰,每7个坑(每隔6个坑)种一株秋海棠,每4个坑(每隔3个坑)种一株雏菊……并且让这些花儿永远开放。”Betsy不知道如此栽种后还会留下多少个坑,但她知道这个数目取决于每种花从哪一个坑开始,每N个坑栽种一株。
我们来帮Betsy计算出会留下多少个坑可以栽种其他的花。共有K (1\leqslantK\leqslant100)种花,每种花从第L (1\leqslantL\leqslantF)个坑开始,每隔I-1个坑占据一个坑。计算全部栽种完成后剩下的未曾占用的坑。
按Betsy的想法,她可以将种植计划描述如下:
30 3 [30个坑;3种不同的花]
1 3 [从第1个坑开始,每3个坑种一株玫瑰]
3 7 [从第3个坑开始,每7个坑种一株秋海棠]
1 4 [从第1个坑开始,每4个坑种一株雏菊]
于是,花园中篱笆前开始时那一排空的坑形状如下:
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
种上玫瑰后形状如下:
R . . R . . R . . R . . R . . R . . R . . R . . R . . R . .
种上秋海棠后形状如下:
R . B R . . R . . R . . R . . R B . R . . R . B R . . R . .
种上雏菊后形状如下:
R . B R D . R . D R . . R . . R B . R . D R . B R . . R D .
留下13个尚未栽种任何花的坑。
输入
*第1行:两个用空格隔开的整数F和K。
第2~K+1行:每行包含两个用空格隔开的整数Lj和Ij,表示一种花开始栽种的位置和间隔。
输出
*仅含一行,只有一个表示栽种完毕后剩下的空坑数目的整数。
输入样例
30 3
1 3
3 7
1 4
输出样例
13
解题思路
(1)数据的输入与输出
本问题的输入仅含一个测试案例。输入的开头是表示栽种花的坑数目和栽种花的种数的两个数F和K。案例中还包含两个序列:每种花的栽种起始位置L[1..K]和栽种间隔I[1..K]。读取这些数据,处理计算出栽种完所有K种花后,F个坑中还剩多少个是空的,并把结果作为一行数据写入输出文件中。
1 打开输入文件inputdata
2 创建输出文件outputdata
3 从inputdata中读取案例数F,K
4 创建数组L[1..K],I[1..K]
5 for i←1 to K
6 do 从inputdata中读取案例数据L[i], I[i]
7 result← THE-FLOWER-GARDEN(F, K, L, I)
8 将result作为一行写入outputdata中
9 关闭inputdata
10 关闭outpudata
其中,第7行调用过程THE-FLOWER-GARDEN(F, K, L, I)计算Betsy在篱笆前将K种花按计划裁种完毕还剩下的空坑数目是解决这个案例的关键。
(2)处理这个案例的算法过程
对于一个测试案例,设K种花中开始栽种位置最小的坑的编号为i,设置一个空坑计数器count,初始化为i-1,因为在i之前的坑必不会载上任何花。从当前位置开始依次考察每一个坑是否会栽上一株花。如果K种花按计划都不会占据这个坑,则count自增1。所有的位置考察完毕,累加在count中的数据即为所求。
THE-FLOWER-GARDEN(F, K, L, I)
1 i←MIN-ELEMENT(L) ▷最先开始栽种花的坑
2 count←i-1 ▷之前的坑当然是空的
3 while ileqslantF ▷逐一考察以后的每个坑
4 do for j←1 to K ▷逐一考察每一种花
5 do if i-1 Mod I[j]≡L[j] ▷查看第i个坑是否栽上第j种花
6 then break this loop
7 if j>K ▷若i号坑没有种上任何花
8 then count←count+1 ▷空坑计数器增加1
9 i ←i+1
10 return count
算法1-4 对一个案例数据F, K, L, I,计算剩下空坑数目的过程
算法1-4的运行时间取决于第3~9行的两重循环的最里层循环体(第5~6行的操作)的重复次数。由于外层的while循环最多重复F次,里层的for循环最多重复K次,所以时间复杂度为O(FK)。