1 题目描述
先来看这个类型的某个题目描述:
约瑟夫生者死者游戏
约瑟夫游戏的大意:30个游客同乘一条船,因为严重超载, 加上风浪大作,危险万分。因此船长告诉乘客,只有将全船 一半的旅客投入海中,其余人才能幸免于难。无奈,大家只 得同意这种办法,并议定30 个人围成一圈,由第一个人数起,依次报数,数到第9人,便把他投入大海中,然后再从 他的下一个人数起,数到第9人,再将他投入大海中,如此 循环地进行,直到剩下 15 个游客为止。问:哪些位置是将 被扔下大海的位置?
1.1 例题
剑指offer JZ46
每年六一儿童节,牛客都会准备一些小礼物去看望孤儿院的小朋友,今年亦是如此。HF作为牛客的资深元老,自然也准备了一些小游戏。其中,有个游戏是这样的:首先,让小朋友们围成一个大圈。然后,他随机指定一个数m,让编号为0的小朋友开始报数。每次喊到m-1的那个小朋友要出列唱首歌,然后可以在礼品箱中任意的挑选礼物,并且不再回到圈中,从他的下一个小朋友开始,继续0…m-1报数…这样下去…直到剩下最后一个小朋友,可以不用表演,并且拿到牛客名贵的“名侦探柯南”典藏版(名额有限哦!!_)。请你试着想下,哪个小朋友会得到这份礼品呢?(注:小朋友的编号是从0到n-1)
如果没有小朋友,请返回-1
2 题解
2.1 链表法
创建0~n-1的环形列表,每数到第m个数将其删除,知道node->next=node停止循环。在牛客网提交时要注意数据越界问题,需判断node是否为空,在这为判断m和n是否为0。
class Solution {
public:
typedef struct linklist{
int data;
struct linklist *next;
}node;
int LastRemaining_Solution(int n, int m)
{
if(m==0 || n==0)//判断数据是否越界,是否为空,否则代码不能通过
return -1;
node *L,*r,*s;
L=new node;
r=L;
// 注意链表是0~n-1 还是 0~n
for(int i=0;i<n;i++){ //尾插入法创建链表
s=new node;
s->data=i;
r->next=s;
r=s;
}
r->next=L->next; // 将最后一个数字的下一个指向链表头,构成环形链表
node *p;
p=L->next;
delete L;
while(p->next!=p){
for(int i=1;i<m-1;i++){
p=p->next;
}
// cout<<p->next->data<<" "; 输出每次删除的数字
p->next=p->next->next;
p=p->next;
}
return p->data;
}
};
2.2 推理法
举个例子:
总人数sum为10人 从0开始,每报到4就把一人扔下去(n=10,m=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处的空位需要如何处理?
掉下去一人后将剩余的人重新编号进行映射。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
如何利用新环的数字推出旧环的呢? 逆推:新环是由(旧环中编号-最大报数值)% 旧总人数 得到的,所以逆推时可以由(新环中的数字+最大报数值)%旧总人数 得到。即:old_number=(new_number+value)%old_sum.
eg (3+4)%10=7;
也就是说,原序列(sum)中第二次被扔掉的编号可以由新序列(sum-1)第一次扔掉的编号通过特定的逆推运算得出。
而新序列(sum-1)也是(从0开始)连续的,它第二次被扔掉的编号可以由(sum-2)的第一次扔掉的编号逆推得出,并且它的第二次扔掉的编号又与原序列中第三次扔掉的编号有对应关系。
所以:
(sum-2)环的第一次出环编号>>>(sum-1)环的第2次出环编号>>>(sum)环的第3次出环编号
即在以k为出环报数值的约瑟夫环中,m人环中第n次出环编号可以由(m-1)人环中的第(n-1)次出环编号通过特定运算推出
注意 以下图示中的环数字排列都是顺序的,且从编号0开始。
由图知,10人环中最后入海的是4号,现由其在1人环中的对应编号0来求解。
通过以上运算,其实我们已经求出分别位于9个环中九个特定次数的结果,只不过我们需要的是10人环的结果罢了。
这种方法既可以写成递归也可以写成循环,它对于求特定次数的出环编号效率较高。
递归就比较好写了,出口即是当次数为1时。
实际编号是从1开始,而不是0,输出时要注意转换。
递推公式
old_number=(new_number+value)%old_sum.
value为报数个数 old_sum为游戏开始前总人数
2.2.1 递归法
class Solution {
public:
int f(int n,int m){
if(n==1)
return 0;
int x=f(n-1, m); // 新报数编号
return (x+m)%n; // old_number=(new_number+value)%old_sum.
}
int LastRemaining_Solution(int n, int m)
{
if(n<=0)
return -1;
return f(n,m);
}
};
2.2.2 循环迭代法
根据递归法可知,
f[1] = 0
f[2] = (f{1] + m) % 2
f[3] = (f[2] + m) % 3
…
f[n] = (f[n-1] + m) % n
所以代码如下:
class Solution {
public:
int LastRemaining_Solution(int n, int m)
{
if(n<=0)
return -1;
int res=0; //相当于f[0];
for(int i=2;i<=n;i++){
res=(res+m)%i; // f[i]=(f[i-1]+m)%i
}
return res;
}
};