hiho一下 第九十四周 数论三·约瑟夫问题

 数论三·约瑟夫问题

时间限制: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 }

 

    原文作者:SeeKHit
    原文地址: https://www.cnblogs.com/SeekHit/p/5457509.html
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞