约瑟夫环问题递归解法的一点理解
大家好,又见面了,我是你们的朋友全栈君。
先说明一点,如果有什么不对的地方,欢迎大家批评指正。
先来看这个类型的某个题目描述:
约瑟夫生者死者游戏
约瑟夫游戏的大意:30个游客同乘一条船,因为严重超载, 加上风浪大作,危险万分。因此船长告诉乘客,只有将全船 一半的旅客投入海中,其余人才能幸免于难。无奈,大家只 得同意这种办法,并议定30 个人围成一圈,由第一个人数起,依次报数,数到第9人,便把他投入大海中,然后再从 他的下一个人数起,数到第9人,再将他投入大海中,如此 循环地进行,直到剩下 15 个游客为止。问:哪些位置是将 被扔下大海的位置?
不失一般性,将 30 改为一个任意输入的正整数 n,而报数 上限(原为9)也为一个任选的正整数k
第一次看到这个题目,我首先想到的是用 链表 或者是 数组 来模拟,但是当我写完之后,与大神对答案,发现他的c语言代码是这么写的:
int ysfdg ( int sum, intvalue, intn) { if ( n == 1 ) return ( sum + value – 1 ) %sum; else return ( ysfdg ( sum-1, value,n-1 ) +value ) %sum; }
7行。。。。。。
(已经不能做朋友了吗。。。)
sum指的是总人数,value指的是每次最大报到的数值,n是第n次,该函数每次可以求出第n次扔海里的人的编号,( ysfdg指的是约瑟夫递归 ) 。
大神飘然而去,而我懵逼了一天,才明白了这段代码的意思。
举个栗子:
总人数sum为10人,从0开始,每报到4就把一人扔下去(value=4)。
初始情况为:
0 1 2 3 4 5 6 7 8 9
扔下去一个之后:
0 1 2 4 5 6 7 8 9
此时,这些编号已经不能组成一个环,但是可以看出4至2之间还是连着的(4 5 6 7 8 9 0 1 2),且下一次报数将从4开始。但是,之后的报数将总要考虑原编号3处的空位问题。
如何才能避免已经产生的空位对报数所造成的影响呢?
可以将剩下的9个连续的数组成一个新的环(将2、4连接),这样报数的时候就不用在意3的空位了。但是新产生的环的数字并非连续的,报数时不像之前那样好处理了(之前没人被扔海里时下一个报数的人的编号可以递推,即(当前编号+1)%sum ),无法不借助存储结构得知下一个应该报数的现存人员编号。
如何使新环上的编号能够递推来简化我们之后的处理呢?
可以建立一种有确定规则的映射,要求映射之后的数字可以递推,且可以将在新环中继续按原规则报数得到的结果逆推出在旧环中的对应数字。
方法:将它与 sum-1 个人组成的(0 ~ sum-1)环一 一映射。
比如之前的栗子,将剩余的 9 人与 9 人环(0~8)一 一映射。既然 3 被扔到海里之后,报数要从4开始 (4 其实在数值上等于最大报数值),那么就将4映射到0~8的新环中0的位置,也就是说在新环中从0开始报数即可,且新环中没有与3对应的数字,因此不必担心有空位的问题。从旧环的 4 开始报数等效于从新环中的 0 开始报数。
原始 0 1 2 3 4 5 6 7 8 9
旧环 0 1 2 4 5 6 7 8 9
新环 6 7 8 0 1 2 3 4 5
新环有这么一个优势: 相比于旧环中2与4之间在数学运算上的不连续性,新环8和0之间在对9取余的运算中是连续的,也就是说根本不需要单独费心设计用以记录并避开已产生的空位(如 编号3)的机制 ,新环的运算不受之前遗留结果的掣肘。同时只要能将新环与旧环的映射关系逆推出来,就能利用在新环中报数的结果退出之前旧环中的报数结果。
以下是新环与旧环中下一个要人扔海里的人位置:
旧环 0 1 2 4 5 6 7 8 9
^
新环 6 7 8 0 1 2 3 4 5
^
如何由新环中的 3 得到旧环中的 7 呢。其实可以简单地逆推回去 : 新环是由 (旧环中编号-最大报数值)%旧总人数 得到的,所以逆推时可以由 ( 新环中的数字 + 最大报数值 )% 旧总人数 取得。即 old_number = ( new_number + value ) % old_sum.
如 : ( 3 + 4 ) % 10 =7 .
也就是说在,原序列( sum ) 中第二次被扔入海中编号可以由新序列( sum – 1) 第一次扔海里的编号通过特定的逆推运算得出。
而新序列 (sum -1)也是(从0开始)连续的,它的第二次被扔入海中的编号由可以由(sum – 2)的第一次扔入海里的编号通过特定的逆推运算得出,并且它的第二次被扔入海中的编号又与原序列中的第三次被扔入海里的编号是有对应关系的。
也求是说有以下推出关系:
(sum-2)环的第1次出环编号 >>>(sum-1)环的第2次出环编号 >>>(sum)环的第3次出环编号
即 在以 k 为出环报数值的约瑟夫环中, m人环中的第n次出环编号可以由 (m-1) 人环中的第 (n-1) 次出环编号通过特定运算推出。
幸运的是,第一次出环的编号是可以直接求出的,也就是说,对于任意次出环的编号,我们可以将之一直降到第一次出环的编号问题,再一 一 递推而出。
注意 以下图示中的环数字排列都是顺序的,且从编号0开始。
由图知,10人环中最后入海的是4号,现由其在1人环中的对应编号0来求解。
通过以上运算,其实我们已经求出分别位于9个环中九个特定次数的结果,只不过我们需要的是10人环的结果罢了。
这种方法既可以写成递归也可以写成循环,它对于求特定次数的出环编号效率较高。
递归就比较好写了,出口即是当次数为1时。
实际编号是从1开始,而不是0,输出时要注意转换。
借此就可以看懂那个大神的代码了。
以下是三种约瑟夫环解法(数组,链表,递归)的c语言代码,作者水平不高,将就看看吧 ╮(╯_╰)╭:
#include<stdio.h>
#include<stdlib.h>
#define FAIL 0
#define SUCCESS 1
typedef struct gamenode
{
int number;
struct gamenode* next;
} node;
//读入初值
int getvalue(int* sum,int* count,int* alive)
{
printf("请输入要参与约瑟夫生存游戏的人数:(人数>0)\n");
while(1)
{
scanf("%d",sum);
if(*sum>0)
break;
printf("输入无效,请重新输入。\n");
};
printf("请输入能报到的最大数字:(1<=数字)\n");
while(1)
{
scanf("%d",count);
if(*count>=1)
break;
printf("输入无效,请重新输入。\n");
};
printf("请输入要求最后存活的人数:(0<=人数<=%d)\n",*sum);
while(1)
{
scanf("%d",alive);
if(*alive>=0&&*alive<=*sum)
break;
printf("输入无效,请重新输入。\n");
};
return SUCCESS;
}
//数组解法
int ysfsz(int n,int k,int s)
{
int i=0,*p=NULL,sum=n-k,j=1,o=0,pr=0;
if ((p=(int*)malloc(sizeof(int)*n))==NULL)
{
printf("FAIL!\n");
return FAIL;
}
for(i=0;i<n;i++)
{
p[i]=1;
}
i=0;
while(1)
{
if (sum==0)
break;
if (j==s&&p[i]==1)
{
p[i]=0;
j=1;
--sum;
}
else if(p[i]==1)
{
j++;
}
i++;
i=i%n;
}
printf("\n生存下来的人的位置是:");
for(i=0;i<n;i++)
{
if(p[i]==1)
printf("%d ",i+1);
}
printf("\n");
printf("\n扔海里的位置是:");
for(i=0;i<n;i++)
{
if(p[i]==0)
printf("%d ",i+1);
}
printf("\n");
free(p);
return SUCCESS;
}
//链表解法
int ysflb(int n,int k,int s)
{
node *h=NULL,*p=NULL,*q=NULL;
int i=0;
if ((h=(node*)malloc(sizeof(node)*n))==NULL)
{
printf("FAIL!\n");
return FAIL;
}
h->number=1;
h->next=h;
q=h;
for(i=1;i<n;i++)
{
if ((p=(node*)malloc(sizeof(node)*n))==NULL)
{
printf("FAIL!\n");
exit (-1);
}
else
{
p->next=NULL;
p->number=i+1;
q->next=p;
q=q->next;
}
}
q->next=h;
p=h;
i=1;
k=n-k;
while(k>0)
{
if(i==s)
{
if(p!=p->next)
{
q->next=p->next;
printf("%d号已被扔进海里。\n",p->number);
free(p);
p=q->next;
}
else
{
printf("%d号已被扔进海里。\n",p->number);
p=q=h=NULL;
}
--k;
}
else
{
p=p->next;
q=q->next;
}
++i;
if(i>=s+1)
i=1;
}
if(p!=NULL)
{
printf("幸存下来的人有:");
h=p;
p=p->next;
while(p!=h)
{
q=p->next;
printf("%d ",p->number);
free(p);
p=q;
}
printf("%d ",h->number);
free(h);
}
else
printf("全部扔海里去了。\n");
return SUCCESS;
}
//递归解法
int ysfdg(int sum,int value,int n)
{
if(n==1)
return (sum+value-1)%sum;
else
return (ysfdg(sum-1,value,n-1)+value)%sum;
}
//主函数
int main(void)
{
int sum=0,count=0,alive=0,i=0;
//读入总人数,报数值,存活人数
getvalue(&sum,&count,&alive);
/*------------------------------------------------------------*/
//1.约瑟夫环的数组解法
//ysfsz(sum,alive,count);
//2.约瑟夫环的链表解法
//ysflb(sum,alive,count);
//3. 约瑟夫环递归解法
for(i=1;i<=sum-alive;i++)
printf("第%2d个被扔海里人的编号:%2d\n",i,ysfdg(sum,count,i)+1);
return 0;
}
发布者:全栈程序员栈长,转载请注明出处:https://javaforall.cn/136034.html原文链接:https://javaforall.cn
相关文章
- 递归算法 数据结构_数据结构中递归的定义
- java递归结束条件_方法递归必须有结束条件
- C——递归方式实现打印一个整数的每一位
- 汉罗塔问题的递归实现「建议收藏」
- java 递归方法卡住_递归算法怎么理解
- c语言递归求组合数_c语言求一维数组元素之和
- N皇后问题_Java递归解决N皇后问题
- 「递归」第三季Ep4:深耕源于热爱
- infer 递归实现反转数组
- 关于递归算法的优化Ⅰ(以经典的斐波那契数列为例)
- 【数据挖掘】决策树算法简介 ( 决策树模型 | 模型示例 | 决策树算法性能要求 | 递归创建决策树 | 树根属性选择 )
- 使用SqlServer CTE递归查询处理树、图和层次结构
- sqlserver中存储过程的递归调用示例
- JAVA语言实现二叉树的层次遍历的非递归算法及递归算法详解编程语言
- C++汉诺塔递归算法完全攻略
- 研究Oracle触发器递归的应用(oracle触发器递归)
- 查询SQL Server中的递归查询技术——极致性能体验(sqlserver递归)
- Redis实现的可重入递归锁(redis递归锁)
- php递归列出所有文件和目录的代码
- 如何使用递归和非递归方式反转单向链表
- SQL实现递归及存储过程中In()参数传递解决方案详解
- c++递归实现n皇后问题代码(八皇后问题)
- C#递归题目实例代码