问题描述
有N个人,编号为 1~n,围成一个圈,编号1的人手上有一个热狗开始向下一个人传递,在热狗传递 m 次后,此时拿着热狗的人退出,剩余的人围成一个圈,从退出的人的下一位再开始上述循环,知道最后一个留下来的人胜利。
如果n为5,m为1,则退出的人编号依次为2,4,1,5,最后胜利的是3。
算法
1. 非递归法
这种方法是目前找到的最优算法,时间复杂度为 ,因为没有用递归,不需要额外的空间。
算法介绍参考:约瑟夫环非递归算法分析
算法理解
- 实际玩家编号是1~n,但为了计算操作方便使用编号0~n-1,原因如下:
m可能大于n,将编号改为0~n-1,则第一次退出的编号可以记为:m%n,因为m%n范围是0~n-1,如果n为5,m为5,则退出的是0,最后计算出最终的胜利者后只需将编号加1就是实际编号了。
- 第一次退出的玩家编号是 m%n,则下一位的编号k为 m%n+1,因此新的环为:
新一轮循环开始后,重新编号,上述编号映射为:
共有 n-1 个人。
可得出上述映射关系,新编号 x 对应旧编号 (k+x)%n.
- 运用递归思想求解
假设玩家人数是 n-1 时赢家编号是 x,则由上述可得玩家编号是 n 时,赢家编号是 (k+x)%n,将 k = m%n+1 带入得到:
(x+m%n+1)%n = (x%n + (m%n)%n + 1%n)%n
因为x小于n,m%n 小于n,当 n>1 时,1%n 为1,因此 n > 1 时得到:
(x+m%n+1)%n,进一步得到 (x+m+1)%n。
当 n=1 时,只有编号0,因此赢家为编号0,对应实际为编号1。
想要求x,相同的办法要知道 n-2 时的赢家,则要知道 n-3 时的赢家,最终可根据 n 为1时求解,因为对应的映射公式相同。
假设人数为 i-1 时赢家编号为 f(i-1),则人数为 i 时赢家编号为 f(i) = ((f(i-1) + m + 1) % i。
程序
/*n = 1 和其他情况分开讨论,但在下面式子中,n为1时结果为1,
符合,因此合并到一起
*/
int josephus(int n, int m)
{
int i, temp = 0;
for (i = 1; i <= n; i++)
temp = (temp + m + 1) % i;
return temp+1;
}
2. 递归法
递归思想与上述一样,程序改为递归形式:
程序
int josephus(int n, int m)
{
if (n = 1)
return 0;
else
return (josephus(n-1,m) + m + 1) % n;
}
最后将求出的结果加1
3. 循环双链表
思路
实现方法参考:
约瑟夫环双向链表
进一步优化方法:
- 令 m’ = m%n,这样如果 m 大于或等于n时可以避免多走一个循环。
- 当 m’ > n/2 时,可以选择从另一个方向遍历,节约时间,因此需要用双链表。
思路:
-
输入参与游戏人数n和传递次数m,记录数据为 0~n,则从当前节点开始,删除之后的第 m 个节点。
-
建立循环双链表,带头节点,头节点指向首节点,尾节点指向首节点。
为了让插入首节点和中间节点操作一致,加入首节点,创建链表时让尾节点指向首节点,链表创建完成后,将尾节点和首节点相连,头节点指向首节点。 -
链表建立后查找往后的第m个节点,每次循环后人数减1,然后令 m = m%n。判断 m 是否大于 n/2,大于则逆向查找,否则顺序查找。
另外,删除节点时判断是否是首节点,删除首节点需要改变头节点指向地址。
程序
#include<stdio.h>
#include<stdlib.h>
struct node;
typedef struct node *ptrtonode;
typedef ptrtonode list;
typedef ptrtonode position;
struct node {
ptrtonode prior;
int data;
ptrtonode next;
};
int input(void);
list InitList(void);
void CreatList(list L, int n);
int Josephus(list L, int n, int m);
void DeleteList(list L);
int main(void)
{
int n, m, s;
list L;
printf("Please enter the number of people: ");
n = input();
printf("Please enter the number of m: ");
m = input();
m = m % n;//避免一次游戏时循环几次
L = InitList();
CreatList(L,n);
s = Josephus(L,n,m);
printf("\nThe winner is %d\n",s);
DeleteList(L);
return 0;
}
int input(void)
{
int n, status, ok = 1;
while (ok) {
status = scanf("%d",&n);
if (status != 1 || n < 1 || getchar() != '\n') {
while (getchar() != '\n')
continue;
printf("Enter again (n > 0): ");
continue;
}
return n;
}
}
//链表初始化,创建头节点,头指针
list InitList(void)
{
list L = (list)malloc(sizeof(struct node));
if (L == NULL) {
fprintf(stderr,"Out of space\n");
exit(EXIT_FAILURE);
}
L->prior = NULL;
L->next = NULL;
return L;
}
void InsertTail(position p, position h, int n)
{
list tem = InitList();
tem->data = n;
p->next = tem;
tem->prior = p;
tem->next = h;//每次让尾节点指向头节点
h->prior = tem;
}
/*建立循环链表,带有头节点,为了让插入第一个节点和其他节点
操作一致。
先让尾节点指向头节点,链表建立完成后,再让尾节点指向首节点。
*/
void CreatList(list L, int n)
{
int num;
position p = L, head = L;//head指向头节点
for (int i = 0; i < n; i++) {
InsertTail(p,head,i);
p = p->next;
}
//链表创建完成让首节点和尾节点相连
position pfirst = head->next, pend = head->prior;
pfirst->prior = pend;
pend->next = pfirst;
}
int IsFirst(position p, position h)
{
return h->next == p;
}
position DeleteNode(position p)
{
position pbefore = p->prior, pafter = p->next;
pbefore->next = pafter;
pafter->prior = pbefore;
free(p);
return pafter;
}
position AntiDelete(position p, position h, int m)
{
for (int i = 0; i < m; i++)
p = p->prior;
//判断要删除的节点是不是首节点,
//删除首节点要改变头节点指向地址
if (IsFirst(p,h))
h->next = p->next;
return DeleteNode(p);
}
position Delete(position p, position h, int m)
{
for (int i = 0; i < m; i++)
p = p->next;
if (IsFirst(p,h))
h->next = p->next;
return DeleteNode(p);
}
int Josephus(list L, int n, int m)
{
position h = L, p = h->next;
int num = n;
while (p->next != p) {
//如果 m 大于人数的一半,逆时针删节点
//顺时针移动m,则逆时针移动num-m
if (m > (int)num/2)
p = AntiDelete(p,h,num-m);
else
p = Delete(p,h,m);
num = n-1;
m = m % num;
}
//退出时说明只剩一个节点(不包括头节点)
//将最后的节点数据加1为实际编号
return p->data + 1;
}
void DeleteList(list L)
{
position p = L->next, tem;
L->next = NULL;
L->prior = NULL;
while (p->next != p) {
tem = p->next;
tem->prior = p->prior;
p->prior->next = tem;
free(p);
p = tem;
}
free(p);
}
测试结果:
Please enter the number of people: 5
Please enter the number of m: 1
The winner is 3