约瑟夫环问题

来源:互联网 发布:好看的饰品店知乎 编辑:程序博客网 时间:2024/06/15 15:25

问题描述:有n个人坐成一圈,编号为1到n,从编号为1的人开始传递热马铃薯。m次传递之后,持有热马铃薯的人退出游戏,圈缩小,然后游戏从退出人下面的人开始,继续进行。最终留下来的人获胜。这样,如果m=0,n=5,那么参加游戏的人依次推出,5号获胜。如果m=1,n=5,那么退出的顺序是2、4、1、5。


方法1:使用循环链表的数据结构来解决。

代码如下:

                                           CircularList.h

//---------【利用循环链表解决约瑟夫环问题】------------#ifndef __CIRCULARLIST__#define __CIRCULARLIST__#include <iostream>// 循环链表的实现class CircularList{private:struct ListNode{int val;ListNode *next;ListNode(int ival) : val(ival), next(NULL) {}};public:CircularList(int n){first = new ListNode(1);ListNode *prev = first;ListNode *curr = NULL;for(int i = 1; i < n; ++i){curr = new ListNode(i+1);prev->next = curr;prev = curr;}curr->next = first;}~CircularList(){ListNode *curr = first->next;ListNode *prev = NULL;while(curr != first){prev = curr;curr = curr->next;delete prev;}delete first;first = NULL;}void printList() const{std::cout << first->val;ListNode *curr = first->next;while(curr != first){std::cout << " " << curr->val;curr = curr->next;}std::cout << std::endl;}// 时间复杂度:O(mn)    void erase(int m){        ListNode *prev = first->next;while(prev->next != first){prev = prev->next;}        ListNode *curr = first;while(curr->next != curr){for(int i = 0; i < m; ++i){prev = curr;curr = curr->next;}std::cout << "The person out: " << curr->val << "\n";prev->next = curr->next;delete curr;curr = prev->next;first = curr;printList();}first = curr;std::cout << "The winner is " << curr->val;}private:ListNode *first;};#endif
                                                main.cpp

//-----------------【约瑟夫环问题】--------------------#include "CircleList.h"using namespace std;int main(int argc, char const *argv[]){// 利用循环链表:O(mn)CircleList l(5);l.printList();l.erase(1);return 0;}


方法2:使用数学推导。

原问题可以解释为编号为1到n的人,从编号为1的人开始喊数字0,下一个喊数字1,依次类推,直到有人喊数字m,则这个人退出游戏。下一个人又从0开始喊,直到只剩一个人。

原序列1:1,.......,k-2,k-1,k,......,n(其中序号k-1出列)。

由推导可得,出列的序号为(m+1)%n(因为m可能比n大),所以有k-1=(m+1)%n,即k=(m+2)%n。

出列后得到序列2:1,.......,k-2,k,......,n。

将前面的k-2个元素放到序号n的后面,则得到序列3:k,......,n,1,.......,k-2。

将序号1及后面的序号全部加上n,则得到序列4:k,......,n,n+1,......,n+k-2。

再将全部序号减去k-1,得到序列5:1,......,n-1。

由于最后的胜利者在序列1中,也肯定在序列5中,设其在序列5中的序号为x,在序列1中的序号为x’,现在问题变为推导x和x'的关系。

从序列5到序列4,全部加上k-1即可,即x'=x+k-1。

其中比较难的的推导是从序列4到序列3,也就是将序列n+1,......,n+k-2这一部分变为1,......,k-2,其实就是对n取余数,即x'=(x+k-1)%n。由于k=(m+2)%n,故x'=(x+m+1)%n。需要注意的是此时n会变为0,需要将其修改回来。

从序列3到序列2和序列1,序号没有改变。

所以我们可以总结如下:

设i为剩余参加比赛的人数,f[i]为最终胜利者的序号,有

f[1]=1;f[i]=(f[i-1]+m+1)%i(i>1);如果f[i]==0,f[i]=i。

代码如下:

#include <stdio.h>int main(int argc, char const *argv[]){    // 数学方法:O(n)int m = 1;int n = 5;int winner = 1; for(int i = 2; i <= n; ++i){winner = (winner + m + 1) % i;if(winner == 0)winner = i;}printf("%d\n", winner);return 0;}
这种类型的问题还有其它的类似递推公式:

f[1]=0;f[i]=(f[i-1]+m)%i(i>1):人的序号为0到n-1,数字从1开始喊到m的情况。

f[1]=1;f[i]=(f[i-1]+m)%i(i>1);如果f[i]==0,f[i]=i:人的序号为1到n,数字从1开始喊到m的情况。

将上面第一种情况的结果加上1也可得到第二种情况的结果,或者写成f[1]=1;f[i]=(f[i-1]+m-1)%i + 1(i>1)。

0 0
原创粉丝点击