约瑟夫环问题

问题描述

约瑟夫环(约瑟夫问题)是一个数学的应用问题:已知n个人(以编号1,2,3…n分别表示)围坐在一张圆桌周围。从编号为k的人开始报数,数到m的那个人出列;他的下一个人又从1开始报数,数到m的那个人又出列;依此规律重复下去,直到圆桌周围的人全部出列。通常解决这类问题时我们把编号从0~n-1,最后结果+1即为原问题的解。

参考

解题笔记(10)——约瑟夫环问题

约瑟夫环问题(数学解法)

Josephus problem

小结

可以使用循环链表或者递推公式。m为要报的数。

f[i] = (f[i-1] + m) % i

这里写图片描述

还是捋一捋数学证明吧,不然心里总是不舒服。

引用wiki的一段:

The easiest way to solve this problem in the general case is to use dynamic programming by performing the first step and then using the solution of the remaining problem. When the index starts from one, then the person at s shifts from the first person is in position ((s-1)mod n)+1, where n is the total number of persons. Let f(n,k) denote the position of the survivor. After the k-th person is killed, we’re left with a circle of n-1, and we start the next count with the person whose number in the original problem was (k mod n)+1. The position of the survivor in the remaining circle would be f(n-1,k) if we start counting at 1; shifting this to account for the fact that we’re starting at (k mod n)+1 yields the recurrence
f(n,k)=((f(n-1,k)+k-1) mod n)+1

其实还是动态规划的思想。

设置f(n)为n个人报数m的生还者。f(n-1)是n-1个人报数m的生还者。这两者有什么关系呢?首先,第一轮报数后k出列,然后我们把序列调整映射成以0为开始,映射之后便是n-1个人报数m的初始情形。

1,第一轮报数

n个人编号为0~n-1,报数0~m-1。第一轮报数出列的是(m-1)%n。为了不用在后面的推导写(m-1)%n这么长一串,咱们设一个变量代换k=(m-1)%n

2,第二轮报数

第一轮k出去了,第二轮从k+1开始,到k-1,一共n-1个人。

我们可以做以下的映射,使得序列又变成以0开始,回到我们熟悉的节奏。

k+10
k+21
k+32
k-2n-3
k-1n-2

我们可以看着映射表的特点猜出来,这个映射表的映射关系为: (x+k+1)%n <- x

然后问题就好解决了,因为映射之后就完全变成了子问题:n-1个人报数m。

我们可以知道生还者就是f(n-1)。

而由映射关系,我们可以反推出,映射后的位置是f(n-1),那么映射前的位置是(f(n-1)+k+1)%n,也就是f(n),于是有f(n) = (f(n-1)+k+1)%n

始终不要忘记k=(m-1)%n就是一个纯粹的变量代换,实际上映射表还可以写成下面这样:

(m-1)%n+10
(m-1)%n+21
(m-1)%n+32
(m-1)%n-2n-3
(m-1)%n-1n-2

而这个长一点的映射关系可以写成: (x+((m-1)%n+1))%n <- x


注:

  1. 举个例子(3个人报数5)来验证公式第一个出列的人是(m-1)%n。n=3, m=5。3个人编号为0~2,报数0~4。画图推导可知0 1 2报数5的话第一轮报数出列的是1号,而给出的公式是(m-1)%n=1。公式正确。

  2. 举个例子验证映射(还是3个人报数5)。第一轮出去了k=1,第二轮报数:从k+1=2开始,到k-1=0,一共n-1=2个人。有以下的映射:

    20
    01

    可见,映射关系(x+k+1)%n <- x是对的。


核心思想的部分已经结束。我们完全可以用上面的递推公式来算了。下面是公式的化简。

化简的过程,就是纯粹的变量代换和利用取模的运算法则进行化简了。

变量代换k=(m-1)%n,代入映射关系f(n)=(f(n-1)+k+1)%n可得:
f(n)=(f(n-1)+k+1)%n
<->代入
f(n)=(f(n-1)+1+(m-1)%n)%n
<->利用(a+b)%n=(a%n+b%n)%n
f(n)=((f(n-1)+1)%n+(m-1)%n%n)%n
<->消掉一个%n
f(n)=((f(n-1)+1)%n+(m-1)%n)%n
<->利用(a%n+b%n)%n=(a+b)%n
f(n)=(f(n-1)+1+m-1)%n
<->
f(n)=(f(n-1)+m)%n

这个就是化简完成的递推式。

哦对了,递推的初始条件是f(1)=0。因为只有一个人的话,生还者就是0号咯。

代码

参考牛客网的题目
http://www.nowcoder.com/questionTerminal/3d6043bf4c45410ab65fe4486f7fd7c4

用了循环链表的方法link()和数学递推式的方法mathsolve(),代码如下:

//PLOBLEM
//http://www.nowcoder.com/questionTerminal/3d6043bf4c45410ab65fe4486f7fd7c4
#include <iostream>
#include <string>
#include <list>
using namespace std;

const int CNTNUM = 2; 

int link(const int num)
{
        list<int> l;
        for (int i = 1; i <= num; i++)
                l.push_back(i);
        auto iter = l.begin();
        while (l.size() > 1)
        {
                for (int i = 1; i < CNTNUM; i++)
                {
                        if (++iter == l.end())
                                iter = l.begin();
                }
                cout << *iter << endl;
                auto tmp = iter;
                if (++tmp == l.end())
                        tmp = l.begin();
                l.erase(iter);
                iter = tmp;
        }
        return *l.begin();
}

int mathsolve(const int num)
{
        if (num == 0 || num == 1)
                return 0;
        int res = 0;
        for (int i = 2; i <= num; i++)
        {
                res = (res + CNTNUM) % i;
        }
        return res;
}


int main()
{
        string s;
        while (cin >> s)
        {
                if (s == "00e0")
                        return 0;
                int num = (s[0] - '0');
                num = num * 10 + int(s[1] - '0');
                for (int i = 0; i < int(s[3] - '0'); i++)
                        num *= 10;
                //cout << num << endl;
                //int res = link(num);
                //cout << "res\n";
                //cout << res << endl;
                //cout << "res2\n";
                cout << mathsolve(num) + 1 << endl;
        }
        return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值