数论三·约瑟夫问题
时间限制:10000ms 单点时限:1000ms 内存限制:256MB
描述
小Hi和小Ho的班级正在进行班长的选举,他们决定通过一种特殊的方式来选择班长。
首先N个候选人围成一个圈,依次编号为0..N-1。然后随机抽选一个数K,并0号候选人开始按从1到K的顺序依次报数,N-1号候选人报数之后,又再次从0开始。当有人报到K时,这个人被淘汰,从圈里出去。下一个人从1开始重新报数。
也就是说每报K个数字,都会淘汰一人。这样经过N-1轮报数之后,圈内就只剩下1个人了,这个人就作为新的班长。
举个例子,假如有5个候选人,K=3:
初始 0: 0 1 2 3 4 从0号开始报数,第1次是2号报到3 1: 0 1 - 3 4 // 0 1 2, 2号候选人淘汰 从3号开始报数,第2次是0号报到3 2: - 1 3 4 // 3 4 0, 0号候选人淘汰 从1号开始报数,第3次是4号报到3 3: 1 3 - // 1 3 4, 4号候选人淘汰 从1号开始报数,第4次是1号报到3 4: - 3 // 1 3 1, 1号候选人淘汰
对于N=5,K=3的情况,最后当选班长的人是编号为3的候选人。
小Ho:小Hi,我觉得当人数和K都确定的时候已经可以确定结果了。
小Hi:嗯,没错。
小Ho:我也想当班长,小Hi你能提前告诉我应该站在哪个位置么?
小Hi:我可以告诉你怎么去求最后一个被淘汰的位置,不过具体的值你得自己去求解。
小Ho:嗯,没问题,那么你快告诉我方法吧!
输入
第1行:1个正整数t,表示多组输入数据,1≤t≤100
第2..t+1行:每行2个正整数n,k,第i+1行表示第i组测试数据,2≤n≤1,000,000,000。2≤k≤1,000
输出
第1..t行:每行1个整数,第i行表示第i组数据的解
- 样例输入
2 5 3 8 3
- 样例输出
3 6
解答:
提示:约瑟夫问题
小Hi:这个问题其实还蛮有名的,它被称为约瑟夫的问题。
最直观的解法是用循环链表模拟报数、淘汰的过程,复杂度是O(NM)。
今天我们来学习两种更高效的算法,一种是递推,另一种也是递推。第一种递推的公式为:
令f[n]表示当有n个候选人时,最后当选者的编号。 f[1] = 0 f[n] = (f[n - 1] + K) mod n
接下来我们用数学归纳法来证明这个递推公式的正确性:
(1) f[1] = 0
显然当只有1个候选人时,该候选人就是当选者,并且他的编号为0。
(2) f[n] = (f[n – 1] + K) mod n
假设我们已经求解出了f[n – 1],并且保证f[n – 1]的值是正确的。
现在先将n个人按照编号进行排序:
0 1 2 3 ... n-1
那么第一次被淘汰的人编号一定是K-1(假设K < n,若K > n则为(K-1) mod n)。将被选中的人标记为”#”:
0 1 2 3 ... K-2 # K K+1 K+2 ... n-1
第二轮报数时,起点为K这个候选人。并且只剩下n-1个选手。假如此时把k+1看作0’,k+2看作1’…
则对应有:
0 1 2 3 ... K-2 # K K+1 K+2 ... n-1 n-K' n-2' 0' 1' 2' ... n-K-1'
此时在0′,1′,…,n-2’上再进行一次K报数的选择。而f[n-1]的值已经求得,因此我们可以直接求得当选者的编号s’。
但是,该编号s’是在n-1个候选人报数时的编号,并不等于n个人时的编号,所以我们还需要将s’转换为对应的s。
通过观察,s和s’编号相对偏移了K,又因为是在环中,因此得到s = (s’+K) mod n。
即f[n] = (f[n-1] + k) mod n。
至此递推公式的两个式子我们均证明了其正确性,则对于任意给定的n,我们可以使用该递推式求得f[n],写成伪代码为:
Josephus(N, K): f[1] = 0 For i = 2 .. N f[i] = (f[i - 1] + K) mod i End For Return f[N]
同时由于计算f[i]时,只会用到f[i-1],因此我们还可以将f[]的空间节约,改进后的代码为:
Josephus(N, K): ret = 0 For i = 2 .. N ret = (ret + K) mod i End For Return ret
该算法的时间复杂度为O(N),空间复杂度为O(1)。对于N不是很大的数据来说,可以解决。
小Ho:要是N特别大呢?
小Hi:那么我们就可以用第二种递推,解决的思路仍然和上面相同,而区别在于我们每次减少的N的规模不再是1。
同样用一个例子来说明,初始N=10,K=4:
初始序列:
0 1 2 3 4 5 6 7 8 9
当7号进行过报数之后:
0 1 2 - 4 5 6 - 8 9
在这里一轮报数当中,有两名候选人退出了。而对于任意一个N,K来说,退出的候选人数量为N/K(“/”运算表示整除,即带余除法取商)
由于此时起点为8,则等价于:
2 3 4 - 5 6 7 - 0 1
因此我们仍然可以从f[8]的结果来推导出f[10]的结果。
但需要注意的是,此时f[10]的结果并不一定直接等于(f[8] + 8) mod 10。
若f[8]=2,对于原来的序列来说对应了0,(2+8) mod 10 = 0,是对应的;若f[8]=6,则有(6+8) mod 10 = 4,然而实际上应该对应的编号为5。
这是因为在序列(2 3 4 – 5 6 7 – 0 1)中,数字并不是连续的。
因此我们需要根据f[8]的值进行分类讨论。假设f[8]=s,则根据s和N mod K的大小关系有两种情况:
1) s < N mod K : s' = s - N mod K + N 2) s ≥ N mod K : s' = s - N mod K + (s - N mod K) / (K - 1)
此外还有一个问题,由于我们不断的在减小N的规模,最后一定会将N减少到小于K,此时N/K=0。
因此当N小于K时,就只能采用第一种递推的算法来计算了。
最后优化方法的伪代码为:
Josephus(N, K): If (N == 1) Then Return 0 End If If (N < K) Then ret = 0 For i = 2 .. N ret = (ret + K) mod i End For Return ret End If ret = Josephus(N - N / K, K); If (ret < N mod K) Then ret = ret - N mod K + N Else ret = ret - N mod K + (ret - N mod K) / (K - 1) End If Return ret
改进后的算法可以很快将N的规模减小到K,对于K不是很大的问题能够快速求解。
代码: 1,开始的时候用的循环链表法,提交之后一直TLE超时,仔细排查了下,因为当k特别大的时候,n比较小的时候,需要循环很多次链表,导致超时。
1 #include<iostream> 2 #include<list> 3 #include<cstdlib> 4 using namespace std; 5 6 int solve(int total,int key){ 7 8 list<int>* table = new list<int>(); 9 10 for (int i = 0; i < total; i++) 11 { 12 table->push_back(i); 13 } 14 15 int shout = 1; 16 for (list<int>::iterator it = table->begin(); table->size() != 1;) 17 { 18 if (shout++ == key) 19 { 20 it = table->erase(it); 21 shout = 1; 22 } 23 else 24 { 25 ++it; 26 } 27 28 if (it == table->end()) 29 { 30 it = table->begin(); 31 } 32 } 33 return *table->begin(); 34 35 } 36 int main(int argc, char* argv[]) 37 { 38 int n, total, key; 39 cin>>n; 40 41 while (n--){ 42 cin >> total >> key; 43 44 cout << solve(total,key) << endl; 45 } 46 return 0; 47 }
后来改的递推,效率提高了许多,AC了,这也体现了算法设计中,好的算法,不单单要解决实际问题,还要考虑效率问题,高效合理。
1 #include<iostream> 2 #include<list> 3 #include<cstdlib> 4 using namespace std; 5 6 int solve(int total, int key) 7 { 8 if (total == 1) 9 return 0; 10 if (total < key){ 11 int anw = 0; 12 for (int i = 2; i <= total; i++) 13 anw = (anw + key) % i; 14 return anw; 15 } 16 17 int anw = solve(total - total / key,key); 18 int temp = total % key; 19 if (anw < temp) 20 anw = anw - temp + total; 21 else 22 anw = anw - temp + (anw - temp) / (key - 1); 23 return anw; 24 } 25 26 int main(int argc, char* argv[]) 27 { 28 int n, total, key; 29 cin >> n; 30 31 while (n--){ 32 cin >> total >> key; 33 cout << solve(total, key) << endl; 34 } 35 return 0; 36 }
最后在补充下左老师提供的代码,很简短!!!对左老师说“我不管,反正我一定要让你会”
1 #include "iostream" 2 3 using namespace std; 4 5 int n, l; 6 7 int getlive(int i, int m)//i长 m数 8 { 9 if (i == 1) 10 return 1; 11 return (getlive(i - 1, m) + m - 1) % i + 1; 12 } 13 14 int main() 15 { 16 while (true) 17 { 18 cin >> n >> l; 19 cout<<getlive(n, l); 20 } 21 }