详解约瑟夫问题

本文详细介绍了约瑟夫问题,包括问题的起源、数学模型和多种算法实现,如递推公式法。文章通过举例和代码展示了解决约瑟夫问题的思路,强调了数学策略在优化算法效率上的重要作用。
部署运行你感兴趣的模型镜像
                    算法集锦第一话:详解约瑟夫问题

约瑟夫问题(Josephus problem)是一个在是一个出现在计算机科学和数学中的问题。有时也称为约瑟夫斯置换。在计算机编程的算法中,类似问题又称为约瑟夫环。又称“丢手绢问题”。
如果有参加NOI/NOIP(现称CSP)的同学们,就会很清楚一个事实:约瑟夫问题绝对是你童年的阴影/之一/它以复杂,变幻莫测,算法简单/复杂/,容易/艰巨/记忆而著称。不过今天我们就来了解并学习几个简单的约瑟夫问题示例,扫清障碍!

模块一:约瑟夫问题简介

约瑟夫问题的来历也挺魔幻的(现今来讲理由也很魔幻😥😥),据说著名犹太历史学家 Josephus有过以下的故事:在罗马人占领乔塔帕特后,39 个犹太人与Josephus及他的朋友躲到一个洞中,39个犹太人决定宁愿死也不要被敌人抓到,于是决定了一个自杀方式,41个人排成一个圆圈,由第1个人开始报数,每报数到第3人该人就必须自杀,然后再由下一个重新报数,直到所有人都自杀身亡为止。然而Josephus 和他的朋友并不想遵从。首先从一个人开始,越过k-2个人(因为第一个人已经被越过),并杀掉第k个人。接着,再越过k-1个人,并杀掉第k个人。这个过程沿着圆圈一直进行,直到最终只剩下一个人留下,这个人就可以继续活着。问题是,给定了和,一开始要站在什么地方才能避免被处决?Josephus要他的朋友先假装遵从,他将朋友与自己安排在第16个与第31个位置,于是逃过了这场死亡游戏。 [1]
17世纪的法国数学家加斯帕在《数目的游戏问题》中讲了这样一个故事:15个教徒和15 个非教徒在深海上遇险,必须将一半的人投入海中,其余的人才能幸免于难,于是想了一个办法:30个人围成一圆圈,从第一个人开始依次报数,每数到第九个人就将他扔入大海,如此循环进行直到仅余15个人为止。问怎样排法,才能使每次投入大海的都是非教徒。
问题分析与算法设计
要说实话,约瑟夫问题并不难,但求解的方法很多;题目的变化形式也很多。这里给出一种实现方法。
题目中30个人围成一圈,因而启发我们用一个循环的链来表示,可以使用结构数组来构成一个循环链。结构中有两个成员,其一为指向下一个人的指针,以构成环形的链;其二为该人是否被扔下海的标记,为1表示还在船上。从第一个人开始对还未扔下海的人进行计数,每数到9时,将结构中的标记改为0,表示该人已被扔下海了。这样循环计数直到有15个人被扔下海为止。
由此看来,约瑟夫问题,可以转化为一个类似于余数或猫吃老鼠问题之类的。如果比较能理解这种做法的,我们来讲解一下法1:
以杀人问题(原来约瑟夫问题一直那么残暴🎃🎃)为例,N个人围成一圈,从第一个开始报数,第M个将被杀掉,最后剩下一个,其余人都将被杀掉。例如N=6,M=5,被杀掉的顺序是:5,4,6,2,3,1。
我们简单分析一下:
(1)由于对于每个人只有死和活两种状态,因此可以用布尔型数组标记每个人的状态,可用true表示死,false表示活。
(2)开始时每个人都是活的,所以数组初值全部赋为false。
(3)模拟杀人过程,直到所有人都被杀死为止。
如果你看懂了,就可以看下面的代码了:
学Pascal的同学看这个代码:
好吧没有代码块·····

var
a:array [1…20] of integer;
n,m,i,j,k,n1,m1:integer;
begin
readln(m,n);
for i:=1 to m do
a[i]:=i;
m1:=m;
n1:=1;
while m1>0 do
begin
j:=(n+n1-1-1) mod m1 +1;
n1:=j;
m1:=m1-1;
writeln(a[j]);
for k:=j to m1 do
a[k]:=a[k+1];
end;
end.
如果学C++的同学就看下面这个代码:
#include
using namespace std;
main()
{
bool a[101]={0};
int n,m,i,f=0,t=0,s=0;
cin>>n>>m;
do
{
++t;//逐个枚举圈中的所有位置
if(t>n)
t=1;//数组模拟环状,最后一个与第一个相连
if(!a[t])
s++;//第t个位置上有人则报数
if(s==m)//当前报的数是m
{
s=0;//计数器清零
cout<<t<<’ ‘;//输出被杀人编号
a[t]=1;//此处人已死,设置为空
f++;//死亡人数+1
}
}while(f!=n);//直到所有人都被杀死为止
}
看懂了吧?
可是如果你能坚持这么写,那么恭喜你,小酱敬你是条汉子(A)
这种做法好理解是好理解,可是无论是用链表实现还是用数组实现都有一个共同点:要模拟整个游戏过程,不仅程序写起来比较烦,而且时间复杂度高达
O(nm),当n,m非常大(例如上百万,上千万)的时候,几乎是没有办法在短时间内出结果的。我们注意到原问题仅仅是要求出最后的胜利者的序号,而不是要读者模拟整个过程。因此如果要追求效率,就要打破常规,实施一点数学策略(毕竟大家都入了OI坑)
因此,为了讨论方便,我们把问题转化一下,这样做并不影响题目原意。
我们知道第一个人(编号一定是(m-1)) 出列之后,剩下的n-1个人组成了一个 新的约瑟夫环(以编号为k=m mod n的人开始):
k ,k+1,k+2 … n-2,n-1,0,1,2,… k-2
并且从k开始报0。
我们把他们的编号做一下转换:
k --> 0
k+1 --> 1
k+2 --> 2


k-2 --> n-2
变换后就完完全全成为了(n-1)个人报数的子问题,假如我们知道这个子问题的解:例如x是最终的胜利者,那么根据上面这个表把这个x变回去不刚好就是n个人情况的解吗?!!变回去的公式很简单,相信大家都可以推出来:x’=(x+k) mod n
如何知道(n-1)个人报数的问题的解?对,只要知道(n-2)个人的解就行了。(n-2)个人的解呢?当然是先求(n-3)的情况 ---- 这显然就是一个倒推问题!好了,思路出来了,下面写递推公式:
令f表示i个人玩游戏报m退出最后胜利者的编号,最后的结果自然是f[n]
递推公式
f[1]=0;
f[i]=(f[i-1]+m) mod i; (i>1)
有了这个公式,我们要做的就是从1-n顺序算出f的数值,最后结果是f[n]。因为实际生活中编号总是从1开始,我们输出f[n]+1。
如果你这个公式看懂了,那么恭喜你,你可以应付X-J的CSP考试了(小酱没参加过提高组),接下来就看代码吧:
/PS:有同学可能有疑问:我是不是要保存每个f呢?其实不用,由于我们的程序是逐级递推的,不需要保存每个f,因此程序也是异常简单/
Pascal有4种写法:
1号选手:
var
n,m,i,s,p:integer;
a:array[1…10000] of integer;
begin
read(n,m);//这步不用说了吧?
for i:=1 to n do
a[i]:=1;//先全部赋值1
p:=0;s:=0;//统计人数和报数字用的
repeat
for i:=1 to n do
begin
if a[i]=0
then continue; //用于等会排除出圈者
s:=s+a[i];//不断累加(报数字)
if s=m then//出圈者
begin
write(i,’ ');打印出圈者;
a[i]:=0;//明白刚才continue的意思了吧
p:=p+1;//人数减少一个;
s:=0;//重头报起.
end;
end;
until p=n;//直到人数到了
end.

2号选手:
var
a:array[1…100] of integer;
n,m,i,j,p:integer;
Begin
write(‘Input n,m:’);
readln(n,m);
for i:=1 to n do
a[i]:=i;
p:=1; {p用于记录报数的位置}
for i:=1 to n do
begin
j:=0; {j用于记录报到的人数}
while j<m do
begin
if a[p]<>0 then j:=j+1;
if p=n then p:=1 else p:=p+1; {处理边界情况}
end;
if p<>1
then begin write(a[p-1],’ ‘);a[p-1]:=0;end {处理边界情况}
else begin write(a[n],’ ');a[n]:=0;end;
end;
end.

3号选手:
var
a:array[1…100] of integer;
n,m,p,i,j:integer;
begin
readln(n,m);
for i:=1 to n-1 do
a[i]:=i+1;
a[n]:=1;
p:=n;
for i:=1 to n do
begin
for j:=1 to m-1 do
p:=a[p];
write(a[p],’ ');
a[p]:=a[a[p]];
end;
end.

4号选手:
var n,m,i,s:integer;
begin
write(‘N M =’);
read(n,m);
for i:=2 to n do
s:=(s+m) mod i;
writeln(s+1, );
end.

学Pascal的同学可以选择性记忆哦~
C++/C的写法有一种,如下:
#include
using namespace std;
const int m = 3;
int main()
{
int n, f = 0;
cin >> n;
for (int i = 1; i <= n; i++) f = (f + m) % i;
cout << f + 1 << endl;
}
这次为python争取到了一个名额,不过是3.x版的,学python的同学可以看:
#控制参数:

nums = 41
call = 3

#参数定义:
peoples = []
for _ in range(nums):
peoples.append(True)

result = []
num =1
#主逻辑
while(any(peoples)):
for index,people in enumerate(peoples):
if people:
if num == call:
peoples[index] = False
result.append(index+1)
print(index+1)#每轮的出局者
print(peoples)#每次的队列状态
num = 1
else:
num += 1
print(’-’* 25)
print(’\n总数为%d,报数为%d’ % (nums,call))
print(‘约瑟夫序列为:\n%s\n’ % result)
print(’-’* 25)
可以看到(推导出),这个算法的时间复杂度为O(n),相对于模拟算法已经有了很大的提高。算n,m等于一百万,一千万的情况不是问题了。可见,适当地运用数学策略,不仅可以让编程变得简单,而且往往会成倍地提高算法执行效率。
拓展一道例题:

约瑟夫问题10e100版(Pascal版)
描述 Description
n个人排成一圈。从某个人开始,按顺时针方向依次编号。从编号为1的人开始顺时针“一二一”报数,报到2的人退出圈子。这样不断循环下去,圈子里的人将不断减少。由于人的个数是有限的,因此最终会剩下一个人。试问最后剩下的人最开始的编号。
输入格式 Input Format
一个正整数n,表示人的个数。输入数据保证数字n不超过100位。
输出格式 Output Format
一个正整数。它表示经过“一二一”报数后最后剩下的人的编号。
样例输入 Sample Input
9
样例输出 Sample Output
3
时间限制 Time Limitation
各个测试点1s
注释 Hint
样例说明
当n=9时,退出圈子的人的编号依次为:
2 4 6 8 1 5 9 7
最后剩下的人编号为3
初见这道题,可能会想到模拟。可是数据实在太大啦!!
我们先拿手来算,可知n分别为12345678...时的结果是11313571...
有如下规律:从1到下一个1为一组,每一组中都是从1开始递增的奇数,且每组元素的个数分别为124...
这样就好弄了!!
大体思路如下:
①read(a)
②b:=1,c:=1{b为某一组的元素个数,c为累计所加到的数}while c<a do (b:=b*2,c:=b+c){超过目标时停止加数}
⑥c:=c-b{退到前一组}
⑦x:=a-c{算出目标为所在组的第几个元素}
⑧ans:=x*2-1{求出该元素}write(ans)
有了思路,再加上高精度就可以了。我写的代码比较烦琐,因为是先把上面的思路敲进去,再写过程,又把一些简单的过程合到主程序中了,所以有点乱,也有点烦琐。起提供思路的作用还是完全可以的吧~~~

直接上代码:
var
a,b,c:array[1…105]of longint;
la,lb,lc,i:longint;
s:string;
procedure incc;
var
i:integer;
begin
for i:=1 to 105 do
c:=c+b;
for i:=1 to 104 do
if c>9 then
begin
c:=c+cdiv10;
c:=c mod 10;
end;
end;
functioncxiaoa:boolean;
vari:integer;
begin
cxiaoa:=false;
fori:=105downto1do
ifc<athenbegincxiaoa:=true;break;end
elseifc>athenbreak;
end;
proceduredoubleb;
vari:integer;
begin
fori:=1to105dob:=b2;
fori:=1to104doifb>9then
begin
b:=b+bdiv10;
b:=bmod10;
end;
end;
proceduredecc;
vari,j:integer;
begin
fori:=1to104do
ifc>=bthenc:=c-belse
begin
j:=i+1;
whilec[j]=0doinc(j);
whilej>ido
begin
c[j]:=c[j]-1;
c[j-1]:=c[j-1]+10;
dec(j);
end;
c:=c-b;
end;
end;
procedurefua;
vari:integer;
begin
fori:=1to104do
ifa>cthena:=a-celse
begin
a:=a-1;
a:=a+10;
a:=a-c;
end;
end;
procedureoutit;
vari,j:integer;
begin
fori:=1to105doa:=a
2;
fori:=1to104doifa>9then
begin
a:=a+adiv10;
a:=amod10;
end;
ifa[1]>0thena[1]:=a[1]-1else
begin
j:=2;
whilea[j]=0doinc(j);
whilej>1do
begin
a[j]:=a[j]-1;
a[j-1]:=a[j-1]+10;
dec(j);
end;
a[1]:=a[1]-1;
end;
fori:=105downto1doifa>0thenbeginj:=i;break;end;
fori:=jdownto1dowrite(a);
end;
begin
readln(s);
la:=length(s);
fori:=ladownto1doa:=ord(s[la+1-i])-ord(‘0’);
b[1]:=1;
c[1]:=1;
whilecxiaoado
begin
doubleb;
incc;
end;
decc;
fua;
outit;
end.

能看懂吧?
### 猴子选大王问题
这个问题非常经典,学队列的同学都知道他的存在,不过我们既然要“扫清全部障碍”,就一定要击破他!不说了,先看第一种理论问题版:
题目描述
一群猴子,编号是1,2,3 …m,这群猴子(m个)按照1-m的顺序围坐一圈。

从第1只开始数,每数到第n个,该猴子就要离开此圈,这样依次下来,直到圈中只剩下最后一只猴子,则该猴子为大王。

输入m和n,输出为大王的猴子是几号。

输入
共一行,包含两个整数,分别表示m和n,中间以空格隔开

输出
共一行,包含一个整数,表示被选为大王的猴子的编号

样例输入
4 12
样例输出
1
提示:NULL
这道题不难吧??相信在解决了上面的问题后应该就十分不难了,话不多说,直接上代码(C++):

```cpp
#include <iostream>
using namespace std;
int main()
{
    int n,k,T=1;
    while(T--)
    {
        cin>>n>>k;
        int ans = 0;
        for(int i=2;i<=n;i++)
        {
           ans=(ans+k)%i;
           cout<<ans+1<<endl;
        }
    return 0;
}

这是数学做法,应该看的出来,喜欢理论法的同学稍加改动即可。
接下来的是应用版:
问题描述:
一堆猴子都有编号,编号是1,2,3 …m,这群猴子(m个)按照1-m的顺序围坐一圈,从第1开始数,每数到第N个,该猴子就要离开此圈,这样依次下来,直到圈中只剩下最后一只猴子,则该猴子为大王。
其中N,M从键盘输入,1<=N,M<=10^21。
二. 基本要求:
(1) 输入数据:输入m,n m,n 为整数,n<m
(2)中文提示按照m个猴子,数n 个数的方法,输出为大王的猴子是几号 ,建立一个函数来实现此功能。
这道题有编程和数学两种解法,大神可以两种都看(自愧不如中,请稍后)
先说编程法:
C/C++共有6位选手:
一号选手:

#include <stdio.h>
#include <malloc.h>
#defineLENsizeof(structmonkey)//定义structmonkey这个类型的长度
struct monkey
{
int num;
struct monkey *next;
};
struct monkey *create(int m)
{
struct monkey *head,*p1,*p2;
inti;
p1=p2=(struct monkey*)malloc(LEN);
head=p1;
head->num=1;
for(i=1,p1->num=1;i<m;i++)
{
p1=(struct monkey*)malloc(LEN);
p1->num=i+1;
p2->next=p1;
p2=p1;
}
p2->next=head;
return head;
}
struct monkey *findout(struct monkey *start,int n)
{
int i;
struct monkey *p;
i=n;
p=start;
for(i=1;i<n-1;i++)
p=p->next;
return p;
}
struct monkey *letout(struct monkey *last)
{
struct monkey *out,*next;
out=last->next;
last->next=out->next;
next=out->next;
free(out);
return next;
}
int main()
{
int m,n,i,king;
struct monkey *p1,*p2;
printf("请输入猴子的个数m:\n");
scanf("%d",&m);
printf("每次数猴子的个数n:\n");
scanf("%d",&n);
if(n==1{
king=m;
}
else
{
p1=p2=create(m);
for(i=1;i<m;i++)
{
p2=findout(p1,n);
p1=p2;
p2=letout(p1);
p1=p2;
}
king=p2->num;
free(p2);
}
printf("猴王的编号是:%d\n",king);
return 0;
}

二号选手:

//猴子选大王问题(约瑟夫环问题)
 #include<stdio.h>
 #include<string.h>
 #include<stdlib.h>
 intfre(charmok[],intk)
 {
 inti;
 printf("\n猴子编号:\n");
 for(i=0;mok[i]!='\0';i++)
 printf("%d\t",mok[i]);//输出为踢出之前的编号,测试用
 for(i=k;mok[i]!='\0';i++)
 {
 mok[i]=mok[i+1];
 }//一个循环,将k以后的元素前移
 putchar('\n');
 for(i=0;mok[i]!='\0';i++)
 printf("%d\t",mok[i]);//输出踢出之后的编号,测试用
 printf("\n按回车继续下一轮:\n");
 getch();//暂停,测试用
 return0;
 }
 int main()
 {
 char mok[50];
 int i;
 int n,s,b;//n表示猴子总数;s表示步进;b表示元素个数及大王编号
 int j,k;//j,k都是计数器
 mok[0]=1;//初始化mok[0],让后面编号更简单的进行
 printf("请输入猴子的总数:\n");
 scanf("%d",&n);//输入猴子的总数
 for(i=1;i<n;i++)
 {
 mok[i]=i+1;
 }//对猴子进行编号
 mok[n]='\0';//用0来表示数组的结尾
 printf("请输入循环单位:\n");
 scanf("%d",&s);//单位长度
 b=n;//统计猴子的个数
 for(j=1,k=0;;j++,k++)
 {
 if(b==1)
 {
 b=mok[0];
 break;
 }//如果元素只剩下一个,那么退出循环
 if(j==s)
 {
 printf("\n它出列了:%d\n",mok[k]);
 fre(mok,k);//用于元素前移的函数
 b--;
 j=1;
 }//将猴子从数组中踢出,并重置计数器J。
 if(mok[k+1]=='\0')
 k=-1;//重置计数器k,因为后面有k++所以k要在重置基础上-1.
 }//判断是否为数组最后元素,重置计数器k。
 printf("\n最终大王是他:%d\n",b);
 return0;
 }

三号选手:

#include<stdio.h>
#include<malloc.h>
int main()
{
int *person,i,node,n,m;
scanf("%d%d",&n,&m);
person=(int*)malloc(sizeof(int)*(n+1));
for(i=1;i<n;i++)//初始化圈
{
person[i]=i+1;//i表示编号为i的人,person[i]的值表示编号为i的猴的下一个人的编号
}
person[n]=1;//编号为n的下一个猴的编号是1
node=1;
while(node!=person[node])//如果某个猴的下一个猴不是自己,意味着人数超过1人
{
for(i=1;i<m-1;i++)//这个循环终止于被踢出的猴的前一个猴
{
node=person[node];//下一个猴的编号为node,node的值来自于前一个猴的person[node]
}
printf("%d",person[node]);//输出被踢出的猴编号
person[node]=person[person[node]];//修改被踢出的猴的前一个猴的person[node]为被踢出的猴的后一个猴的编号
node=person[node];//这句话中的node是被踢出的猴的后一个猴
}
printf("%d",node);//输出最后大王的编号
return 0;
}
#include<stdio.h>
#include<conio.h>
int main(void)
{
int n,i=0,m,p;
scanf("%d%d",&n,&m);//n总人数,m步长
while(++i<=n)
{
p=i*m;
while(p>n)
p=p-n+(p-n-1/(m-1;
printf("%d\n",p);
}
getch();
return0;
}
#include<iostream>
usingnamespacestd;
int king(intM,intN)
{
int k=0;
for(int i=2;i<=M;i++)
k=(k+N)%i;
return++k;
}
int main()
{
int n,m;
while(scanf("%d%d",&n,&m)&&n&&m)
{
cout<<king(n,m)<<endl;
}
return0;
}
#include<iostream>
#include<conio.h>
using namespace std;
main()
{
    int n,m,i,s=0;
    cout<<"N:";cin>>n;   
    cout<<"M:";cin>>m;
    for(i=2;i<=n;i++)
    s=(s+m)%i;
    cout<<"新的大王是:"<<s+1;
    getch();  
}

Pascal有一位选手:

var
a:array[1..10000]ofinteger;
n,s,i,j:integer;
begin
read(m,n);
fori:=1tomdoa[i]:=1;
j:=0;
fori:=1tomdo
begin
s:=0;
whiles<ndo
begin
ifj<mtheninc(j)
elsej:=1;
s:=s+a[j];
end;
write(j);
a[j]:=0;
end;
end.

python&PHP各有一位选手,我们再次对PHP严重表扬一下,PHP有非常完善的数据结构模拟方案,可以非常简洁的解决这样的问题!

functionking($n,$m){
$monkey=range(1,$n);//模拟建立一个连续数组
$i=0;
while(count($monkey)>1){
$i+=1;//开始查数
$head=array_shift($monkey);//直接一个一个出列最前面的猴子
if($i%$m!=0){
array_push($monkey,$head);//如果没数到m或m的倍数,则把该猴放回尾部去.
}//否则就抛弃掉了
}
return$monkey[0];
}
echo'剩余',king(3,4),'号猴子';
def Monkey(n, M):
    if n == 0 or M == 1: #过滤两种特殊情况
        return 0
    L = list(map(list,list(enumerate([True] * n,start=1)))) #两种状态以布尔值表示
    counter = 0
    while [L[i][1] for i in range(len(L))].count(True) > 1:
        for i in range(len(L)):
            if L[i][1] == True:
                counter += 1
            if counter % M == 0 and L[i][1] == True:
                L[i][1] = False
    return list(filter(lambda x:x[1] == True, L))[0][0]
     
Monkey(2009, 3)

OK,那这节课的普通场就到这里了,如果有数学大神在此,那么接下来的高级场请拭目以待~~
约瑟夫问题有两种数学解法:笔算解法和规律解法。
笔算解法:
在M比较小的时候 ,可以用笔算的方法求解,
M=2
即N个人围成一圈,1,2,1,2的报数,报到2就去死,直到只剩下一个人为止。
当N=2^k的时候,第一个报数的人就是最后一个死的,
对于任意的自然数N 都可以表示为N=2^k+t,其中t<n/2
于是当有t个人去死的时候,就只剩下2^k个人 ,这2k个人中第一个报数的就是最后去死的。这2k个人中第一个报数的人就是2t+1
于是就求出了当M=2时约瑟夫问题的解:
求出不大于N的最大的2的整数次幂,记为2k,最后一个去死的人是2(N-2k)+1
M=3
即N个人围成一圈,1,2,3,1,2,3的报数,报到3就去死,直到只剩下一个人为止。
此时要比M=2时要复杂的多
我们以N=2009为例计算
N=2009,M=3时最后被杀死的人记为F(2009,3),或者可以简单的记为F(2009)
假设这种情况下还剩下n个人,则下一轮将杀死[n/3]个人,[]表示小于等于这个数的最大整数,还剩下n-[n/3]个人
设这n个人为a1,a2,…,a(n-1),an
从a1开始报数,一圈之后,剩下的人为a1,a2,a4,a5,…a(n-n mod 3-1),a(n-n mod 3+1),…,an
于是可得:
1、这一轮中最后一个死的是a(n-n mod 3),下一轮第一个报数的是a(n-n mod 3+1)
2、若3|n,则最后死的人为新一轮的第F(n-[n/3])个人
若n mod 3≠0 且f(n-[n/3])<=n mod 3则最后死的人为新一轮的第n-[n/3]+F(n-[n/3])-(n mod 3)人
若n mod 3≠0 且f(n-[n/3])>n mod 3则最后死的人为新一轮的第F(n-[n/3])-(n mod 3)人
3、新一轮第k个人对应原来的第 3*[(k-1)/2]+(k-1)mod 2+1个人
综合1,2,3可得:
F(1)=1,F(2)=2,F(3)=2,F(4)=1,F(5)=4,F(6)=1,
当f(n-[n/3])<=n mod 3时 k=n-[n/3]+F(n-[n/3])-(n mod 3),F(n)=3*[(k-1)/2]+(k-1)mod 2+1
当f(n-[n/3])>n mod 3时 k=F(n-[n/3])-(n mod 3) ,F(n)=3*[(k-1)/2]+(k-1)mod 2+1
这种算法需要计算 [log(3/2)2009]次 这个数不大于22,可以用笔算了
于是:
第一圈,将杀死669个人,这一圈最后一个被杀死的人是2007,还剩下1340个人,
第二圈,杀死446人,还剩下894人
第三圈,杀死298人,还剩下596人
第四圈,杀死198人,还剩下398人
第五圈,杀死132人,还剩下266人
第六圈,杀死88人,还剩下178人
第七圈,杀死59人,还剩下119人
第八圈,杀死39人,还剩下80人
第九圈,杀死26人,还剩下54人
第十圈,杀死18人,还剩36人
十一圈,杀死12人,还剩24人
十二圈,杀死8人,还剩16人
十三圈,杀死5人,还剩11人
十四圈,杀死3人,还剩8人
十五圈,杀死2人,还剩6人
F(1)=1,F(2)=2,F(3)=2,F(4)=1,F(5)=4,F(6)=1,
然后逆推回去
F(8)=7 F(11)=7 F(16)=8 f(24)=11 f(36)=16 f(54)=23 f(80)=31 f(119)=43 f(178)=62 f(266)=89 f(398)=130
F(596)=191 F(894)=286 F(1340)=425 F(2009)=634。
规律解法:
对于经典约瑟父问题,视频中的解法是:
1)找出令等式 成立的最大的 ,记为
2)求解出
3)所以,最后留下来的人的序号为
视频中给出的解释是:
当 时,序号为1的人总是是最后留下来的人。对于 ,当去掉 个人后,剩下的人正好组成 个人围成的圈,此圈中的序号1的人将是最后留下来的人。而对应到原来的圈,这个人的序号就是 ,因为去掉 个人时正好就跳过了 个人,而下一个人的序号就是 。
推广到猴子选王问题,从以上解法不难看出,解法就是把2换成N,即:
1)找出令等式 成立的最大的 ,记为
2)求解出
3)所以,最后留下来的猴子的序号为 ,mod是取余数,例如:3 mod 2 = 1
m=8, N=3,8=3^1+5, 按照他的算法,此时N=3,l=5, 按照他的算法最后剩下来的是8,事实上很容易直接验算最后留下来的是7,上面的公式是错误的。
本次的算法集锦小食堂就到这里结束啦,如果看官喜欢,请留个言或点个赞为小酱加个🍗哦~
拜拜~
PS:下期想学什么,可以在下面留言哦~

您可能感兴趣的与本文相关的镜像

Stable-Diffusion-3.5

Stable-Diffusion-3.5

图片生成
Stable-Diffusion

Stable Diffusion 3.5 (SD 3.5) 是由 Stability AI 推出的新一代文本到图像生成模型,相比 3.0 版本,它提升了图像质量、运行速度和硬件效率

评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值