约瑟夫环问题的两种解决方法

  约瑟夫环问题是一个非常出名而且经典的问题,也叫枪毙问题,问题是这样的:有N个人围成一圈,从第一个人开始报数,数到M的人就被枪毙,再有下一个人开始重新报数,数到M的那个人被枪毙,求最后活着的人是几号?

方法一:模拟过程

可以使用模拟过程的方式输出每个被枪毙的人的编号

#include <iostream>
#include<cstdio>
#include <cstring>
#include <algorithm>
using namespace std;

bool a[101];
int main(){
	int n,m;
	cin >> n >> m;
	memset(a,0,sizeof a);//初始化数组,一开始全都是活着的人
	int f = 0,s = 0,t = 0;//f表示已经被枪毙的人的数量,s表示当前报数的是什么,t表示当前报数的人的编号。
	while(f != n - 1) {//被枪毙的人的数目达到了n - 1个的时候
		if(t == n) t = 0;//因为是一个环,所以最后一个与第一个相连。
		if(!a[t]) s ++;//如果当前这个编号的人活着 就会报数 就会s ++
		if(s == m) {//当有人报数到M的时候,这个人被枪毙
			s = 0;//又重新开始报数,计数器归0
			cout << t << " ";//输出这个被枪毙的 人的编号
			a[t] = true;//标记为被枪毙
			f ++;//被枪毙的人数+ 1
		}
		t ++;//下一个人继续报数
 	}
	return 0;
}

最后a数组中没有被枪毙的人就是最后活着的人

方法二:自底向上递归

但是其实约瑟夫问题可以使用递归的方式进行求解。

假设f(n,m)表示的是有n个人报数到m就被枪毙时最后活着的人的编号,那么f(n - 1,m)就是有n - 1个人报数到m被枪毙时最后活着的人的编号,其实f(n ,m )与f(n ,m )是有一定关系的。

f(n ,m )是有n个人围成一圈,编号为0 ,1,2, 3, 4, 5, 6 ,..... n - 1,0 , 1, 2,3,4... 假设报数到m就被枪毙.那么第一个就是枪毙编号为m - 1的人,然后下一个人(m)从1开始报数,这个时候 我们重新编号,将m编号为0,m + 1编号为 1.... 重新编号,重新组成环,这个时候就剩下了n - 1个人,也是数到m就被枪毙,那么现在就变为了有n - 1个人围成一圈,数到m就枪毙,那么最后活着的那个人就是f(n - 1,m )(我们可以看作新开了一局游戏),通过下表我们可以看到,从m重新编号,与原来的编号对比,其实现在只有n- 1个人的状态下的所有人的编号和原来那局游戏的人的编号是有一个映射关系的。就是有n -1个人的状态下的任何一个人的编号 + m % n (因为是环 所有要取模)就能得到他在原来有n个人的状态下的编号,所以我们只要得到只有n - 1个人的状态下最后那个活着的人的编号f(n - 1,m )就能得到他在原来有n个人的状态下的编号 f( n,m ) = (f ( n - 1, m ) + m ) % n,就能得出一个递推关系。

... m - 1(被枪毙) m m + 1 ...
0 1

​ 有点类似于把f(n,m )这个原问题分解为了一个子问题(只有一个子问题的分治法),然后得到结果之后再求出原来的问题,这其实是一个自底向上的递归(获得有n - 1个人的状态下存活的人的编号 (就是上一层的结果)然后经过处理得到这一层的结果,也就是n个人的状态下(当前层)存活的人的编号,同理f(n - 1,m )可以根据上一层f ( n -2 ,m )得到 .... 类推)base case 也就是最底的一层自然是f (1,m )意思就是只存在一个人 的时候 存活的编号是多少,那肯定是0.所以得到base case 是if (n == 1) return 0;

所以可以写出一个非常简洁的自底向上的递归算法:

int lastRemain(int n,int m){
    if(n == 1) return 0;//base case 
    return (lastRemain(n - 1,m) + m ) % n;//当前问题需要根据子问题f (n - 1,m)的结果经过处理得到
}
posted @ 2020-08-30 23:37  驿站Eventually  阅读(682)  评论(0编辑  收藏  举报