2021年数据结构课程设计 -- 问题 C-E: Josephus问题(Ⅰ)(Ⅱ)(Ⅲ)

问题 C-E: Josephus问题(Ⅰ)(Ⅱ)(Ⅲ)

题目描述 - Josephus问题(Ⅰ)

n个人排成一圈,按顺时针方向依次编号1,2,3…n。从编号为1的人开始顺时针"一二"报数,报到2的人退出圈子。这样不断循环下去,圈子里的人将不断减少。最终一定会剩下一个人。试问最后剩下的人的编号。

要求程序模拟题意来实现

题目描述 - Josephus问题(Ⅱ)

n个人排成一圈,按顺时针方向依次编号1,2,3…n。从编号为1的人开始顺时针"一二"报数,报到2的人退出圈子。这样不断循环下去,圈子里的人将不断减少。最终一定会剩下一个人。试问最后剩下的人的编号。

n很大,直接模拟题意不行,请找出规律

题目描述 - Josephus问题(Ⅲ)

n个人排成一圈,按顺时针方向依次编号1,2,3…n。从编号为1的人开始顺时针"一二三…"报数,报到m的人退出圈子。这样不断循环下去,圈子里的人将不断减少。最终一定会剩下一个人。试问最后剩下的人的编号。

本题的数据规模更具有挑战性,尝试更通用且高效的算法。

输入(Ⅰ、Ⅱ)

不超过1000组数据。
每组数据一行,每行一个正整数,代表人数n。 (1 <= n <= 1000)

输出(Ⅰ、Ⅱ)

每组输入数据输出一行, 仅包含一个整数,代表最后剩下的人的编号。

样例输入(Ⅰ、Ⅱ)

7
2

样例输出(Ⅰ、Ⅱ)

7
1

题目提示

第一组数据出队顺序: 2 4 6 1 5 3

输入(Ⅲ)

超过1000组数据。
每组数据一行,每行两个正整数,代表人数n (1 <= n < 231)和m(1<=m<=100)。

输出(Ⅲ)

每组输入数据输出一行, 仅包含一个整数,代表最后剩下的人的编号。

样例输入(Ⅲ)

7 2
2 2

样例输出(Ⅲ)

7
1

解题过程

思路 一

首先我们就题论题的来看,Josephus(1)题目的要求就是设计一个程序来简单模拟题意,循环出队,最后一个出队的成员被记录输出。
因为这个思路过于简单,我们就直接分享一下代码,不多做解释了。

#include <stdio.h>
#include <stdlib.h>
#define max 1024
void findout(int n, int m)
{
	int buf[max];
	int dex = 0,in = 0, i;
	for ( i = 1; i <= n; i++)
	{
		buf[i] = i;
	}
	while (n > in + 1)
	{
		for (i = 1; i <= n; i++)
		{
			if (buf[i] != 0)
			{
				dex++;
				if (dex == m)
				{
					buf[i] = 0;
					in++;
					dex = 0;
				}
			}
		}
	}
	for (i = 1; i <= n; i++)
		if (buf[i] != 0)
			printf("%d\n", buf[i]);
}
int main()
{
	int n, m;
	/*
	m=2
	while (scanf("%d", &n) != EOF)
	*/
	while (scanf("%d %d", &n, &m) != EOF)
	{
		if (m == 1)
		{
			printf("%d\n", n);//当 m = 1 时,也就是每一个人都排队出列,此时最后一个出列的就是第 n 个人
			continue;
		}
		findout(n, m);
	}
	return 0;
}
代码分析

这样解决的话时间复杂度很高,O(n) = n^3,所以当n很大的时候不适用(其实一般大可能就会时间超限,所以不推荐使用),采用主函数注释中的内容就可以解决小规模的问题(m = 2)。

思路二

利用构造双向循环链表解决Josephus问题

代码分析

首先创建结构体数组,每个结构体包括元素的编号、前驱元的索引(previous)及后继元的索引(next)。每次循环删除一个元素,使该元素的前驱元的next指向后继元的previous,后继元的previous指向前驱元的next,并将此元素标号置为-1。
从被删除元素的下一个元素开始继续循环,直到数组中只剩下一个元素(此元素的previous与next相等)
显然该算法时间复杂度为O(m*n)

思路三
#include<cstdio>
#include<algorithm>
using namespace std;
long long solve_Josephus(long long n, long long m)
{
    long long p = 0, i = 2;
    long long buf;
    if (m == 1) 
        return 0;
    while (i <= n)
    {
        if (p + m < i)
        {
            buf = (i - p - 1) % (m - 1) ? (i - p - 1) / (m - 1) : (i - p - 1) / (m - 1) - 1;
            if (i + buf > n)
            {
                p += (n + 1 - i) * m;
                break;
            }
            i = buf + i;
            p = p + buf * m;
        }
        else  
        {
            p = (p + m) % i;
            i++;
        }
    }
    return (p + 1) % n;
}
int main()
{
    long long n, m, p;
    while (scanf("%lld %lld", &n, &m) != EOF)
    {
        p = solve_Josephus(n, m);
        printf("%lld\n", p ? p : n);
    }
    return 0;
}
代码分析

利用数学规律探求Josephus问题的数学解法,只进行数学运算,不利用递归和循环解法直接求解,这样的话就可以极大程度的减少时间复杂度,此方法可解决问题2&3,解决问题2时要选择性的改变m。
显然该算法时间复杂度为O(1)

综合分析

假设当前游戏玩家总数为t,报数最大值为m,第一个出席的玩家编号为f(t)。那么很显然,当t=1时,f(1)=1。
当t>1时,很显然,f(t)=m。如下图:
编号为m的离席
剩下的(t-1)个成员,重新制订编号,假设原编号为i,新编号为j,
则i和j满足关系:i = (j + m - 1) % t + 1
所以得到:f(n) = (f(n-1) + m - 1)%n + 1
此时我们得到了一个递归关系
离席者编号等差
其中x为[t/m](或写作“t mod m”)。之前的递归是每次离席1人就递归1次,但上图这种情况,可以从m离席“快进”到xm离席,节省了中间的迭代过程,即f(n) = (f(n-x) + xm - 1)%n + 1。
这种算法最终的时间复杂度是O(1)

public int GetResultByFastRecursion()
        {
            int result = 1;
            int delta;
            for (int t = 2; t <= Total; t++)
            {
                if (result + MaxFlag < t)
                {
                    delta = (t - result) / MaxFlag;
                    if (delta > Total - t)
                    {
                        delta = Total - t;
                    }
                    t = t + delta;
                    result = result + MaxFlag * delta;
                }
                result = (result + MaxFlag - 1) % t + 1;
            }
            return result;
        }

【为了避免大家复制粘贴我换了一种语言】

附录

著名的Josephus问题
据说著名犹太历史学家 Josephus有过以下的故事:在罗马人占领乔塔帕特后,39 个犹太人与Josephus及他的朋友躲到一个洞中,39个犹太人决定宁愿死也不要被人抓到,于是决定了一个自杀方式,41个人排成一个圆圈,由第1个人开始报数,每报数到第3人该人就必须自杀,然后再由下一个重新报数,直到所有人都自杀身亡为止。
然而Josephus 和他的朋友并不想遵从,Josephus要他的朋友先假装遵从,他将朋友与自己安排在第16个与第31个位置,于是逃过了这场死亡游戏。

百度提供的解决代码(搬运,侵权请联系我)

百度源网页

#include<stdio.h>
#include<math.h>
#define FALSE 0
#define TRUE 1
typedef int DataType; /* 定义元素类型为整型,也可定义为其他类型 */
struct Node; /* 单链表结点类型 */
typedef struct Node* PNode; /* 结点指针类型 */
struct Node /* 单链表结点结构 */
{
	DataType info;
	PNode link;
};
struct LinkList /* 单链表类型定义 */
{
	PNode head; /* 指向单链表中的第一个结点 */
};
typedef struct LinkList* PLinkList; /* 单链表类型的指针类型 */
int insert_clink(PLinkList pclist, DataType x, PNode p)
/* 在pclist所指的循环单链表中最后一个结点p的后面插入元素x */
{
	PNode q;
	q = (PNode)malloc(sizeof(struct Node));
	if (q == NULL)
	{
		printf("Out of space!!! \n");
		return (FALSE);
	}
	else
	{
		q->info = x;
		q->link = pclist->head->link;
		p->link = q;
		return (TRUE);
	}
}
PLinkList createNullList_clink(void)
/* 创建带头结点的空循环链表*/
{
	PLinkList pclist;
	PNode p;
	pclist = (PLinkList)malloc(sizeof(struct LinkList));
	if (pclist != NULL)
	{
		p = (PNode)malloc(sizeof(struct Node)); /* 申请头结点 */
		if (p != NULL)
		{
			pclist->head = p;
			p->link = NULL;
		}
		else
			pclist->head = NULL;
	}
	else
		printf("Out of space!!\n");
	return pclist;
}
PNode next_clink(PNode p)
{
	return p->link;
}
PNode find_clink(PLinkList pclist, int i)
/* 在带有头结点的循环单链表clist中求第i(i>0)个结点的位置 */
/* 当表为空循环单链表时,返回值为NULL */
{
	PNode p;
	int j;
	p = pclist->head->link;
	if (i < 1)
	{
		printf("The value of i=%d is not reasonable.\n", i);
		return NULL;
	}
	if (p == NULL)
		return NULL;
	for (j = 1; j
		p = p->link;
		return p;
}
void josephus_clink(PLinkList pclist, int s, int m)
{
	PNode p, pre, tp;
		int i;
		p = find_clink(pclist, s); /* 找第s个结点 */
		if (p == NULL) /* 无第s个结点 */
		{
			printf(" s = %d not exit.\n ", s);
			exit(1);
		}
	while (pclist->head->link != NULL)
	{
		for (i = 1; i
			{
			pre = p;
			p = p->link;
			}
		printf(" out element: %i \n", p->info); /* 输出该结点 */
			if (pre != p) /* 当表中元素个数大于1时,删除该结点 */
			{
				pre->link = p->link;
					tp = p;
					p = p->link;
					free(tp);
			}
			else /* 当表中元素个数等于1时,将头结点指针置空 */
			{
				free(pre);
				pclist->head->link = NULL;
			}
	}
	free(pclist->head);
	free(pclist);
}
int main()
{
	PLinkList jos_clist;
	PNode p;
	int i, n, s, m, k;
	printf("\n please input the values of n = ");
	scanf("%d", &n);
	printf(" please input the values of s = ");
	scanf("%d", &s);
	printf(" please input the values of m = ");
	scanf("%d", &m);
	jos_clist = createNullList_clink(); /* 创建空循环链表 */
	if (jos_clist == NULL || jos_clist->head == NULL) return (FALSE);
	p = jos_clist->head;
	for (i = 1; i <= n; i++) /* 创建循环链表 */
	{
		k = insert_clink(jos_clist, i, p);
		if (k == FALSE) return(FALSE);
		p = next_clink(p);
	}
	josephus_clink(jos_clist, s, m);
	return (TRUE);
}

声明:本文由博主原创,请同学们自己借鉴思考后,独立完成课程设计的有关题目,请勿直接复制粘贴,祝同学们课程设计顺利(课程设计后的期末考试也要加油哦!)

如果发现本文有那些错误和问题请及时私信博主,感谢各位大佬批评指正!

看都看了,点个赞再走吧!

  • 44
    点赞
  • 96
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 5
    评论
好的,我可以为您提供一种实现方法。 约瑟夫环问题是一个经典的问题,可以用链表来实现。具体实现可以参考以下代码: ```c #include <stdio.h> #include <stdlib.h> // 定义链表结点 typedef struct node { int data; struct node *next; } Node; // 创建链表 Node* CreateList(int n) { Node *head, *p, *tail; head = (Node*)malloc(sizeof(Node)); // 头结点 tail = head; for (int i = 1; i <= n; i++) { p = (Node*)malloc(sizeof(Node)); p->data = i; tail->next = p; tail = p; } tail->next = head->next; // 将链表首尾相连形成环 free(head); // 头结点释放 return tail->next; // 返回环的起始结点 } // 约瑟夫环问题处理函数 void Josephus(int n, int m) { Node *head, *p, *q; head = CreateList(n); // 创建有 n 个结点的环 p = head; // p 指向当前结点的前一个结点 while (n > 1) { for (int i = 1; i < m; i++) { p = p->next; } q = p->next; // q 指向当前结点 p->next = q->next; // 将当前结点删除 printf("%d ", q->data); // 输出删除的结点的编号 free(q); // 释放删除的结点 n--; } printf("%d\n", head->data); // 输出最后剩余的结点的编号 free(head); // 释放链表 } int main() { int n, m; printf("请输入链表结点数 n 和报数 m:"); scanf("%d%d", &n, &m); Josephus(n, m); return 0; } ``` 这个程序的输入为链表结点数 n 和报数 m,输出为依次删除结点之后最后剩余的结点的编号。您可以根据自己的需求修改输入输出方式,或者将该函数封装为一个库函数供其他程序调用。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Dr.Water

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值