约瑟夫环(JosephProblem)

来源:互联网 发布:粤贵银手机行情软件 编辑:程序博客网 时间:2024/06/10 17:19

约瑟夫环(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%nif pos == 0 then  pos = n - 1else  pos = pos - 1end

其实上面的这个逻辑等价于:

数到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个位置后,得到的序列为:

kk+1k+2...n-2n-101...k-2

其中k对应0编号(右移):

k       0k+1     1k+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       0k+1     1k+2     2...n-2     n-k-2n-1     n-k-10       n-k1       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]=0f[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
……

如果有错误请大伙指出来,哈!

1 0
原创粉丝点击