约瑟夫环 / 转圈中最后剩下的数字(第1个+第k个)

剑指offer:面试题62

参考:https://blog.csdn.net/weixin_42659809/article/details/82596676

一、题目概述

30个游客同乘一条船,因为严重超载, 加上风浪大作,危险万分。因此船长告诉乘客,只有将全船 一半的旅客投入海中,其余人才能幸免于难。无奈,大家只 得同意这种办法,并议定30 个人围成一圈,由第一个人数起,依次报数,数到第9人,便把他投入大海中,然后再从 他的下一个人数起,数到第9人,再将他投入大海中,如此 循环地进行,直到剩下 15 个游客为止。问:哪些位置是将 被扔下大海的位置?

不失一般性,将 30 改为一个任意输入的正整数 n,而报数 上限(原为9)也为一个任选的正整数k

二、理解思路

假设总人数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,输出时要注意转换。

int ysfdg ( int sum, intvalue, intn)
{
    if ( n == 1 )
        return ( sum + value - 1 ) %sum;
    else
        return ( ysfdg ( sum-1, value,n-1 ) +value ) %sum;
}
//sum指的是总人数,value指的是每次最大报到的数值,n是第n次,
//该函数每次可以求出第n次扔海里的人的编号,( ysfdg指的是约瑟夫递归 ) 。
三、从第一个开始数,约瑟夫环代码(数组,链表,递归)
#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;  
}  
四、进阶:从第k个开始数

参考:https://www.jianshu.com/p/0a54197c04b6
在这里插入图片描述

#include <iostream>
#include <cmath>

using namespace std;

int main(){
    int n,k,m,count=0,i,j=0;
    cin>>n>>k>>m;
    int *p= new int[n]();
    i=k;
    while(true){
        j=(j+1)%m;
        do{
            i=(i+1)%n;
        }while(p[i]!=0);
        if(j==m-1){
            p[i]=++count;
            if(count==n)break;
        }
    }
    cout<<i<<endl;
    return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值