约瑟夫环(JosephProblem)
题目:
有41个人围坐成一圈玩游戏,编号分别是0,1,2,…,39,40.
从1开始,每次数到3的人就退出游戏,下个人再次从1开始。
请问最后剩下的人的编号?
约瑟夫环有很多解题思路:
1.用一个标识数组来模拟;
2.用数据结构来实现;
3.用递归实现;
4.用递推实现。
用一个标识数组来模拟:
#include <stdio.h>
#include <stdlib.h>
/******************************************************
* 函数名称:JosephProblem
* 参数列表:
* 1.n:从0到n-1共有n个人
* 2.m:每次数到m就退出游戏
* 返回值 :最后的人的编号
* Author :test1280
* History :2017/05/02
* 备注 :
* ***************************************************/
int JosephProblem(int n, int m)
{
int i = 0;
int *flagArr = malloc(sizeof(int)*n);
// 初始化标识数组
for (i = 0; i<n; i++)
{
flagArr[i] = 1;
}
// 计数变量
int cnt = 0;
// 剩余人数,为1时退出
int last = n;
int index = 0;
while (last != 1)
{
if (flagArr[index] == 1)
{
cnt++;
if (cnt == m)
{
flagArr[index] = 0;
last--;
cnt = 0;
}
}
index++;
index %= n;
}
int result = -1;
for (i = 0; i<n && flagArr[i]==0; i++);
result = i;
free(flagArr);
flagArr = NULL;
return result;
}
int main()
{
int n = 41;
int m = 3;
int result = JosephProblem(n, m);
printf("the result is %d.\n", result);
return 0;
}
基本上就是模拟人工来进行,使用一个数组来标识当前的位置的人是否已经退出游戏。
这个仔细看看应该比较好理解。
用数据结构来实现:
#include <iostream>
#include <queue>
using namespace std;
/*****************************************************
* 函数名称:JosephProblem
* 参数列表:
* 1.n:从0到n-1共有n个人
* 2.m:数到m的人退出游戏
* 返回值 :最后的人的编号
* Author :test1280
* History:2017/05/02
* 备注 :
* **************************************************/
int JosephProblem(int n, int m)
{
queue<int> q;
int i = 0, j;
for (; i<n; i++)
{
q.push(i);
}
while (q.size() > 1)
{
j = m-1;
while (j--)
{
q.push(q.front());
q.pop();
}
q.pop();
}
return q.front();
}
int main()
{
int result = JosephProblem(41, 3);
cout<<"the result is "<<result<<endl;
return 0;
}
这个算是很简洁的了。
现将所有的编号(从0到n-1)存储到一个queue中,然后也还是模拟。
虽然简单易懂,但是效率低。
用递归实现:
使用递归来实现就很有意思。
首先,m%n得到的值一定是0到n-1范围内的某一值(不论m和n的大小关系如何)。
其次,如果我想要求n个人第一次数到m的那个人的编号,可以使用:
pos = m%n
if pos == 0 then
pos = n - 1
else
pos = pos - 1
end
其实上面的这个逻辑等价于:
数到m的那个人的编号=(m%n-1+n)%n。
-1+n是防止变负数,再%n可以防止本身就是正值超出n范围。
再其次,我现在有个位置是pos,我想将其右移x个位置,求得右移后的位置?
右移后的位置=(pos + x) % n
例如:
我有序列:0 1 2 3 4 此时n=5
我想要:
将2右移1位:2+1=3
将2右移2位:2+2=4
将2右移3为:2+3=5=0
将2右移4位:2+4=6=1
将2右移5位:2+5=7=2
将2右移6位:2+6=8=3
……
将pos右移x位就是(pos+x)%n啦。
最后想想左移,我现在的位置是pos,想要将其左移x位,求左移后的位置?
还是那个序列:0 1 2 3 4 此时n=5
我想要:
将2左移1位:2-1=1
将2左移2位:2-2=0
将2左移3位:2-3=-1=4
将2左移4位:2-4=-2=3
将2左移5位:2-5=-3=2
将2左移5位:2-6=-4=1
……
将pos左移x位就是(pos-x+n)%n啦。
想明白这几点后,我们可以思考:
我想要求n个人,实际是先求第一个退出的人的位置(记为delPos,意为delete-position),此时知道谁退出游戏了;
从退出游戏的那个人(delPos)的下一个人(记为k)开始,让他(k)成为0编号,相当于将所有的元素右移了n-1-k+1个(n-1是最后的编号,k是当前的编号,n-1-k是将k移动到n-1处的右移量,而0编号是n-1的下一个位置,故移动量为n-1-k+1);
然后将n-1个人(相对一开始的n个人少了一个,那个人已经数到m退出游戏了)按照谁数到m就退出游戏的规则再进行,得到n-1个人数m的最后的编号;
将此编号再左移n-1-k+1个位置回到原位置,即我们求得的n个人数m退出的解。
代码如下:
#include <stdio.h>
#include <stdlib.h>
/*******************************************
* 函数名称:JosephProblem
* 参数列表:
* 1.n:从0到n-1共有n个人
* 2.m:数到m的人退出游戏
* 返回值 :最后的人的编号
* 备注 :
* Autohr :test1280
* History :2017/05/08
*******************************************/
int JosephProblem(int n, int m)
{
// 递归终止条件
// 如果只有一个人,那么必然是返回0位置;
if (n == 1)
return 0;
// 本次剔除的位置
// 假设m==x*n:delPos = n-1(x为[0,...]);
// 假设m=6, n=3, delPos=2=(6%3-1+3)%3;
// 假设m=5, n=3, delPos=1=(5%3-1+3)%3;
// 假设m=4, n=3, delPos=0=(4%3-1+3)%3;
// 假设m=3, n=3, delPos=2=(3%3-1+3)%3;
// 假设m=2, n=3, delPos=1=(2%3-1+3)%3;
// 假设m=1, n=3, delPos=0=(1%3-1+3)%3;
int delPos = (m % n -1 + n) % n;
// k是delPos的下一位
// 假设m=6, n=3, k=0=(2+1)%3=0;
// 假设m=5, n=3, k=2=(1+1)%3=2;
// 假设m=4, n=3, k=1=(0+1)%3=1;
// 假设m=3, n=3, k=0=(2+1)%3=0;
// 假设m=2, n=3, k=2=(1+1)%3=2;
// 假设m=1, n=3, k=1=(0+1)%3=1;
int k = (delPos + 1) % n;
// 0 1 .. delPos k .. n-1;
// 使上一行全部的数字向右移动,使得k移动到左首第0个;
// 移动的位数为:((n-1)+1-k)%n;
// 假设有序列:0 1 2 3 4 此时n=5;
// 当delPos=1时(k=2),将k右移至0处,共移动3=((5-1)+1-2)%5;
// 当delPos=4时(k=0),将k右移至0处,共移动0=((5-1)+1-0)%5;
int move = ((n-1)+1-k)%n;
// 由subResult回推到原位置
int subResult = JosephProblem(n-1, m);
return (subResult - move + n) % n;
}
int main()
{
int result = JosephProblem(10, 3);
printf("result is %d\n", result);
return 0;
}
其实核心步骤就是:先剔除一个,然后将剩余的人右移,求n-1个人的解,再左移回来即可。
就像一开始的拨号电话,先旋转数字键盘,然后放开手回退…
代码本来没有几行,注释比较多。。。
另,递归有个缺点,数字不能很大,否则栈溢出。
(如果有好的尾调用倒是可以避免,但是也有很多限制)
用递推实现:
有了上面递归的铺垫,我们能不能写的更简单一点?
我的递归是从大到小(解决n规模是要解决n-1规模,解决n-1规模是解决n-2规模…)进行的,我能否从小到大进行?
我们的0-(n-1)的序列,删除第一次数到m的人之后的序列为:
0 1 2 … k … n-2 n-1 共计n-2个。
将k右移n-1+1-k个位置后,得到的序列为:
k
k+1
k+2
...
n-2
n-1
0
1
...
k-2
其中k对应0编号(右移):
k 0
k+1 1
k+2 2
...
n-2 ?(p)
n-1 ?
0 ?
1 ?
...
k-2 n-2
其中有很多?不知道值是多少。
假定第一个?为p,很容易有等式:p-2=(n-2)-(k+2),得出p=n-k-2,于是:
k 0
k+1 1
k+2 2
...
n-2 n-k-2
n-1 n-k-1
0 n-k
1 n-k+1
...
k-2 n-2
结束......
x1<-----x2
我们的目的是从右侧向左侧进行推导:
假定右侧的最后编号是x2,x2对应的左侧编号是x1,即从左侧右移思考改变为:右侧左移。
左侧的右移量是k,那么右侧的左移量也是k,此时还记得我们之前的左移右移吗?
x1=(x2-k+n)%n
而k的值是多少?看看上面的递归:
k = (delPos + 1) % n = ((m % n -1 + n) % n + 1)%n
x5=(x4-k4+4)%4;
x4=(x3-k3+3)%3;
x3=(x2-k2+2)%2;
x2=(x1-k1+1)%1;
x1仔细一想,当然就是0啦。
f[i]表示i个人玩游戏报m退出最后胜利者的编号,最后的结果是f[n]。
f[1]=0
f[i]=(f[i-1] -k* + i) % i
代码如下:
#include <stdio.h>
#include <stdlib.h>
/*******************************************
* 函数名称:JosephProblem
* 参数列表:
* 1.n:从0到n-1共有n个人
* 2.m:数到m的人退出游戏
* 返回值 :最后的人的编号
* 备注 :
* Autohr :test1280
* History :2017/05/08
*******************************************/
int JosephProblem(int n, int m)
{
int s = 0;
for (int i=2; i<=n; i++)
{
int k = ((m % i - 1 + i) % i + 1) % i;
s = (s -k + i) % i;
}
return s;
}
int main()
{
int result = JosephProblem(41, 3);
printf("the result is %d\n", result);
return 0;
}
那么一长串其实可以化简:
int JosephProblem(int n, int m)
{
int s = 0;
for (int i=2; i<=n; i++)
{
s = (s + m) % i;
}
return s;
}
至于是怎么化简的,大家可以多找找规律。
其实我更提倡用数学方法化简。。回头我再看看再补上来,嘿嘿。
额,至此,几种解题方法都实现过啦,恩,大家可以多看看,多想想,其实还有很多可以优化来处理。
参考资料:
1.http://blog.163.com/soonhuisky@126/blog/static/157591739201321341221179/
2.http://blog.csdn.net/kangroger/article/details/39254619
3.https://www.zhihu.com/question/20065611
……
如果有错误请大伙指出来,哈!