约瑟夫环的两种解法

n个人想玩残酷的死亡游戏,游戏规则如下:

n个人进行编号,分别从1到n,排成一个圈,顺时针从1开始数到m,数到m的人被杀,剩下的人继续游戏,活到最后的一个人是胜利者。

请输出最后一个人的编号。

Input
输入n和m值。m>1。

Output
输出胜利者的编号。

Sample Input
5 3

Sample Output
4
Hint
第一轮:3被杀第二轮:1被杀第三轮:5被杀第四轮:2被杀

  • 比较普遍的解决方案是利用循环链表进行模拟
  • (1)直接建立循环链表
#include <iostream>
#include <cstring>

using namespace std;   //使用名称空间std

//链表法
struct Lnode
{
	int no;
	Lnode* pre;
	Lnode* next;
};

typedef struct
{
	Lnode* befo;
	Lnode* rear;
}DLinklist;


int main()
{
	DLinklist kill;        //自杀环
	int n, m;              //n人循环m
	int i;
	while (cin >> n >> m)
	{
		kill.befo = (Lnode*)new Lnode;
		kill.rear = kill.befo;
		kill.rear->no = 1;
		i = 2;
		while (i <= n)
		{
			kill.rear->next = (Lnode*) new Lnode;
			Lnode* p = kill.rear->next;
			p->no = i++;
			p->pre = kill.rear;
			p->next = kill.befo;
			kill.rear = p;
		}
		kill.befo->pre = kill.rear;
		//约瑟夫环初始化完成

		int flag = 1;        //计数器
		Lnode* t, *j;    //工作指针
		t = kill.befo;
		while (flag < n)
		{
			for (i = 1;i < m;i++)
				t = t->next;     //t为被杀者

			j = t;
			t = t->next;         //此时逻辑关系:j->pre  j  t
			t->pre = j->pre;
			j->pre->next = t;    //删除被杀者
			delete j;
			flag++;

		}
		cout << t->no << endl;
		delete t;
	}
	return 0;
}

(2)用数组模拟循环链表

#include <iostream>
#include <cstring>

using namespace std;   //使用名称空间std

int main()
{
	int pre[100], next[100];
	int i, t;
	int n, m;
	int flag;     //计数器

	while (cin >> n >> m)
	{
		flag = 1;
		for (i = 0;i < n;i++)
			next[i] = (i + 1) % n;
		for (i = 1;i < n;i++)
			pre[i] = (i - 1) % n;
		pre[0] = n - 1;
		//循环链表初始化完毕

		i = 0;
		while (flag < n)
		{
			for (t=1;t <=m - 1;t++)
				i = next[i];
			t = i;
			i = next[i];
			pre[next[t]] = pre[t];
			next[pre[t]] = next[t];   //删除结点
			flag++;
		}
		cout << i+1 << endl;     //逻辑次序从0开始,实际编号从1开始

	}

	return 0;
}
  • 利用数学递归法
    初始情况: 0, 1, 2 …n-2, n-1 (共n个人)

第一个人(编号一定是(m-1)%n,设之为(k-1) ,读者可以分m<n和m>=n的情况分别试下,就可以得出结论) 出列之后,
剩下的n-1个人组成了一个新的约瑟夫环(以编号为k==m%n的人开始):
k k+1 k+2 … n-2, n-1, 0, 1, 2, …,k-3, k-2

现在我们把他们的编号做一下转换:
old -> new
k --> 0
k+1 --> 1
k+2 --> 2


k-2 --> n-2
k-1 --> n-1

变换后就完完全全成为了(n-1)个人报数的子问题,假如我们知道这个子问题的解:例如new是最终的胜利者,那么根据上面这个表把这个new变回去不刚好就是n个人情况的解吗!

new ->old?(这正是从n-1时的结果反过来推n个人时的编号!)
0 -> k
1 -> k+1
2 -> k+2


n-2 -> k-2
变回去的公式 old=(new+k)%n

那么,如何知道(n-1)个人报数的问题的解?只要知道(n-2)个人的解就行了。(n-2)个人的解呢?只要知道(n-3)的情况就可以了 ---- 这显然就是一个递归问题:

令f[i]表示i个人玩游戏报m退出最后胜利者的编号,最后的结果就是f[n]
递推公式
f[1]=0;
f[i] = (f[i-1] + m) % i ; (i>1)


本文来自 d4shman 的CSDN 博客 ,全文地址请点击:https://blog.csdn.net/wusuopubupt/article/details/18214999?utm_source=copy
(1)递归法(反向推)

#include <iostream>
#include <cstring>

using namespace std;   //使用名称空间std

//递推法
int main()
{
	int n, m;
	int f(int n, int m);
	while (cin >> n >> m)
	{
		cout << f(n, m)+1 << endl;    //逻辑上由0-(n-1),实际是1-n
	}


	return 0;
}

int f(int n,int m)
{//每个环对应一个新环,每个新环使用一次后又对应下一个新环
	if (n == 1)                                      //最后一个新环对应的幸存者序号
		return 0;
	else
		return (f(n - 1, m) + m ) % n;     //old=(new+m)%n

(2)递推法(正向推)

#include <iostream>
#include <cstring>

using namespace std;   //使用名称空间std

int main()
{//递推
	int _new;
	int n, m;
	while (cin >> n >> m)
	{
		_new = 0;     //最后一个新环(只有一人)的序号为0
		for (int i = 2;i <= n;i++)   //倒数第二个新环——倒数第n-1个新环
			_new = (_new + m) % i;   //old=(new+m)%n;
		cout << _new + 1 << endl;   //实际编号=逻辑编号加一
	}
	return 0;
}

猜你喜欢

转载自blog.csdn.net/qq_40432881/article/details/82938132