约瑟夫环问题

约瑟夫环问题

问题描述:

n个人围成一个环,然后给从某个人开始顺时针从1开始报数,每报到m时,将此人出环杀死(当然不杀死也可以啊),然后从下一个人继续从1报数,直到最后只剩下一个人,求这个唯一剩下的存活的人是谁?

 

分析:

首先,我们要标示这n个人,别小看这一步,其实蛮重要的。第一种标示方法是从0开始,将n个人标示为0~n-1,第二种方法是从1开始标示,将这n个人标示为1~n。当然会有人说了,那我从x(x>=2)开始,将此n个数标示为x~x+n-1,其实我们可以把这种情况都归到第二种从1开始标示的情况,为什么可以,我们稍后分析。

第一种情况从0开始编号:

编号为k的人拖出去杀死之后,下一个要拖出去受死的人的编号为:(k+m)%n (假设当前有n个人还活在环中)

第二种情况从1开始编号:

编号为k的人拖出去杀死之后,下一个要拖出去受死的人的编号为:(k+m-1)%n+1,于是我们就可以回答上面的问题了,如果从x开始编号的话,下一个拖出去受死的人的编号就应该是:(k+m-x)%n+x了。

其实,上面的这两种情况是完全可以在合并的,编号只是一个识别,就像名字一样,叫什么都没关系,从某个人开始出环,不管他们怎么编号,n个人出环的先后顺序都是一样的,最后该哪个人活下来是确定的,不会因为编号而改变,所以不管从几开始编号,都可以归纳为从0开始编号,其他的编号就是一个从0编号情况的一个偏移而已,从x编号的情况就相当于从0开始编号的情况下每个人的编号都+x,大小先后顺序不变~

于是,下面的讨论都是从0开始编号的~

 

怎么解决这个问题呢?

最简单的方法是模拟,模拟这个出环过程,可以使用链表也可以使用数组,时间复杂度都是O(n*m).当然,这种解法时间复杂度太高,不可取~

 

我们有O(n)的算法~

假设从编号为0的人开始报数,当然从编号为k的人开始报数的情况也是也可以解决的,只要稍微转化就可以,至于怎么解决?我们讲完从编号为0的人开始报数的情况就明白啦~

 

我们从0编号开始报数,第一个出环的人m%n-1,剩下的n-1个人组成一个新的约瑟夫环,接下来从m%n开始报数,令k=m%n,新环表示为:

k, k+1, k+2, ……n-1, 0, 1, 2, …..., k-2

我们对此环重新编号,根据上面的分析,编号并不会影响实际结果。

k    à 0
k+1
à
1
k+2
à
2
...
k-2
à n-2

对应关系为:x’ = (x+k)%n (其中,x’是左侧的,x是右侧重新编号的)

重新编号之后,就成了求n-1约瑟夫环问题了,好似差不过已经解决了,我们递归求解就ok~

但是,虽然怎么编号该哪个人最后剩下了就哪个人最后剩下来,但编号是用来确定人的,如果每次这样重新编号,编号就混乱了,我们无法让计算机知道到底是哪个人最后活下来了。所以我们要对编号进行统一~

统一的方法就是利用上面提到的对应关系:x’ = (x+k)%n (其中,x’是左侧的,x是右侧重新编号的)

我们假设最后活下来的在重新编号后的n-1个人的约瑟夫环中的位置是x,则它在n个人的约瑟夫环中的位置就是x’啦,即:f[n] = (f[n-1]+m%n)%n,化简为:f[n]=(f[n-1]+m)%n

然后,递归的递推关系就有了:

f[1] = 0

f[n]=(f[n-1]+m)%n (n >= 2)

如果只是求得最后一个存活下来的人的编号的话,可以不存储中间结果,编程实现及其简单:

int main(void)
{
int n, m, i, s=0;
scanf("%d%d", &n, &m);
for (i=2; i<=n; i++) s=(s+m)%i;
printf ("The winner is %d/n", s);
}

下面我们来回答上面留下来的问题,如果我们从k开始报数,我们只要多加一步转化就ok了,即:

k    à 0
k+1
à
1
k+2
à
2
...
k-2
à n-2

k-1à n-1

所以,如果从0开始编号并且从0编号的人开始报数的n个人的约瑟夫环问题的答案为s的话,则从0开始编号并且从k编号的人开始报数的n个人的约瑟夫环问题的答案就是:(s+k)%n

 

上面的算法的时间复杂度为O(n),基本约瑟夫环问题就解决了~

但是,有时候O(n)的算法也可能接受不了~

对于m=2的情况,我们还有时间复杂度更低的公式算法~

公式是怎样的呢?我们先来看一组数据,其中编号还是从0开始的,我们枚举一些n的值,看m=2时的结果r

n:     1     2 3          4 5 6 7           8 9 10 11 12 13 14 15

r:     0     0 2          0 2 4 6           0 2 4  6  8 10 12 14

我们总结规律:

n表示成n=2^m+k,则r=2*k

编码实现比较绕,仔细看看就明白了:

int main(int argc, char *argv[])

{

    int n;

    scanf("%d", &n);

    int t = 1;

    while (t<=n) t<<=1;

    t>>=1;

    n ^= t;

    n<<=1;

    printf("%d/n", n);

}

有了上面的知识准备就可以轻松搞定下面两个题目了~

http://acm.pku.edu.cn/JudgeOnline/problem?id=2244

http://acm.pku.edu.cn/JudgeOnline/problem?id=1781

 

上面的都只是基本的约瑟夫环问题,还有一类变态的约瑟夫环,它的m值不是固定的,这样我们就无法像上面的那样利用公式做到O(n)的时间复杂度了,只能使用最后一招,没招之招:模拟啦~ 但我们也不想让时间复杂度到O(n*m),所以我们要对模拟方法进行改进~

 

问题背景:

n个人的约瑟夫环,然后每个人都有一个mi值,充当了下一轮报数的m值。假设从编号为k的人开始游戏,游戏是这样玩的:先让k出环杀死,然后取得他的mi值,作为下一轮报数m值,即从编号k+1开始报数,当报数值到达mi时,将此人出环杀死,然后继续取刚出环的人的mi值作为下一轮的m值,继续游戏,直到最后只有一个人剩下。

 

算法讨论:

模拟的方法肯定是可以搞定的~模拟的方法有n-1次找人杀人操作,每次找人操作需要累计访问m个元素,才能确定要找的人。所以如果我们可以快速的找到当前位置开始的剩余在环中的第m个人出环杀掉的话,时间复杂度应该可以降下来~

我们将环中的人的编号记录在线段树中,并且记录线段树中每个区间还剩下的活着的人数cnt域,每次杀一个人,我们就将其从线段树中删除,并且更新更新包含此人的区间的cnt值。这样我们就可以在O(lgn)的时间复杂度内取得还剩下的人中的第x个人的编号。然后剩下的事就是计算每次出环的是剩下在环中的第几个人,也就是x值了。怎么确定呢?

假设环中还有n个活人,某轮杀的是剩下在环中n个中的第k个人(0,1,….k),然后我们取得了它的mi (怎么取得的呢?我们可以在线段树中以O(lgn)的时间复杂度查找此第k个人的编号,然后就可以取得他的mi值了),此时环中还剩下n-1个人,接下来要出环杀死的是谁呢?假设是剩下在环中的第x个人(0,1,2…x),则x=(k+mi-1)%(n-1),然后取得线段树中的第x个元素的mi值作为下一轮的m值,并将其从线段树中删除~继续下一轮杀人游戏~直到还剩下一个人在环中~

我们发现每轮确定要杀的人的编号只需要O(lgn)的时间复杂度,所以总的时间复杂度为O(n*lgn)~

 

 

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值