一个简单的约瑟夫问题,却有许多种写法。
n 个人围成一个圆圈,第1个人从1开始顺时针报数, 报到m的人,令其出列。然后再从下一个人开始,从1顺时针报数,报到第m个人,再令其出列,…,如此下去, 直到圆圈中只剩一个人为止。此人即为优胜者。
为了求出胜利者,我们需要排除掉报数后出去的人,有人用数组来模拟人们报数的过程,除掉n-1个人之后,便得出了胜利者。
也可以选择使用循环链表,每次循环到第m个人,就把他从链表中删去。 这里结束循环的条件可以是两种,一个是循环链表的结点元素p->next == p, 也就是只剩下一个人时结束,但我写起来却有点尴尬,因为当m为1时,我没办法找到要删的第一个结点的前驱结点,所以还是用除掉n-1个人做判断条件吧。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
View Code 1 #include<iostream> 2 using namespace std; 3 #include<stdlib.h> 4 typedef struct l 5 { 6 int n; 7 struct l * next; 8 }L; 9 void InitList( L* &head, int n) //建立没有头结点的链表 注意& 10 { 11 L * tail; 12 L * temp; 13 head = NULL; 14 tail = NULL; 15 int i; 16 for( i = 0; i < n; i++) 17 { 18 temp = (L *) malloc( sizeof( L )); 19 temp->n = i+1; 20 if( head == NULL ) 21 { 22 head = temp; 23 } 24 if( tail != NULL) 25 tail->next = temp; 26 tail= temp; 27 if( tail->next != NULL) 28 tail->next = NULL; 29 } 30 tail->next = head; 31 } 32 void List( L * head, int m, int n) 33 { 34 int i = 1; //因为是从第一个结点开始 所以i初始为1 35 L * p; 36 p = head; 37 n--; 38 while(n-- ) //在只剩下一个结点前 39 { 40 while( i!=m) //每次前进m-1个结点 到达从起始位置起第m个结点 41 { 42 43 p = head; 44 head = head->next; 45 i++; 46 } 47 i = 1; 48 p->next = head->next; //该元素删掉 i从下个元素开始 49 head = p->next; 50 //free(l); 51 52 } 53 //cout << r << endl; 54 cout << head->n << endl; 55 56 57 } 58 59 void List2( L * head) 60 { 61 while( head) 62 { 63 cout << head->n; 64 head = head->next; 65 } 66 cout << endl; 67 } 68 69 70 int main() 71 { 72 L * p; 73 p = (L*)malloc(sizeof(L)); 74 int n, m; 75 while(1) 76 { 77 cin >> n >> m; 78 if( n == 0 && m==0) 79 break; 80 InitList( p, n); //建立一个n个节点的链表 81 List( p , m, n); //找出胜利者 82 //List2(p); 83 free(p); 84 } 85 return 0; 86 }
更为方便的一种写法是用一个带头结点的单链表,每次数一个数就拆下第一个数据接到尾部,数到m时拆下的那个结点不再接到尾部,然后继续重新开始数,丢掉n-1个人之后,胜利者求出。这种写法虽然略微多了点操作,却比较简单好写。
但是 当人们用数学方法来求约瑟夫问题时,上面的全部方法都被超越了,当我们求出第一个被排除的人之后,剩下的n-1个人继续开始报数,这是一个新的约瑟夫环,而这个约瑟夫环的胜利者一定也是原来那个约瑟夫环的胜利者,由于这个约瑟夫环的第一个人是上个环的第k+1号,k = m % n - 1(编号从0开始,因为编号从1开始要考虑(x+k)%n = 0的情况)。所以新环的胜利者编号x与原环胜利者编号y的关系为 y = ( x + k)%n.
同样的n-1个人组成的环的胜利者编号可以由n-2个人组成的胜利者编号求出,n-2由n-3求出。。。
f[i]表示i个人玩游戏的胜利者编号 最后的结果自然是f[n]递推公式 f[1]=0; f[i]=(f[i-1]+m)%i; (i>1)
程序时间复杂度跟代码长度都得到了降低
#include<iostream>
using namespace std;
int main()
{
int n,m, r ;
cin >> n >> m;
r = 0;
for(int i = 2 ; i <= n; i++) //递归n-1次
r = (r+m)%i;
cout << r<< endl; //如果编号为从1开始 则输出r+1
return 0;
}
//编号从1开始也可以这么写
r = 1;
for( int i = 2 ; i <= n; i++)
{
r = (r+m)%i;
if( r == 0 ) //因为编号从1开始时 如果胜利者在原来环的位置是最后一位,这里求出的结果是为0的
r = i;
}
但是有另外一种情况,在n极大,而m较小时,可以用另一种方法
如果做一个表,我们从1开始报数 每次报到m的倍数时出局,最终报数为 m * n的人为胜利者
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 |
由上表可知,第四人为胜利者,设胜利者在i行的报数为 X i , 只有X i不是 m 的倍数时才有胜利的可能,所以可以设 Xi = k * m + j , k 为对 Xi / m 的取整。( j < m , j≠0)
可知,当前出局的人数为 k , 剩下的人数为 n – k , 胜利者在 i 行的报数为 Xi ,那么他在
i + 1 行的报数一定为 Xi 加上剩余人数 ,即 Xi+1 = Xi + n – k
可推出 Xi+1 = k * m + j + n – k = k*(m -1 ) + j + n
反过来 若已知胜利者在第 i+ 1 行的报数 Xi+1 ,可以通过Xi+1 – n =k * (m – 1) + j 推出
j = (Xi+1 – n ) % ( m – 1), 如果j=0, 则j = m-1,
可以通过 j 来求出 k = ( Xi+1 – n – j )/ ( m – 1)
有了j 和 k 便可以通过Xi+1 求Xi, 由最后一行一定是 m * n 反向推导,推导至第一行
#include<iostream> using namespace std; int main() { int n, m; int j, k; cin >> n >> m; int Xn = n * m; //最后一行一定为 n * m while( Xn > n) { j = ( Xn - n) % ( m -1 ); if( j == 0) j = m -1; k = ( Xn - n - j)/ (m -1); Xn = k * m + j; } cout << Xn << endl; return 0; }
由于一次可以消去多个数据,时间复杂度取决于 m与n的关系,m与n相差越大,此算法越优于上一种算法