当前位置: 首页 > article >正文

约瑟夫环四种解法(数组,链表,递归,数学归纳)C/C++

一.题目概述

约瑟夫环问题是一个很经典的问题:一个圈共有N个人(N为不确定的数字),第一个人的编号为1第二个人的编号为2号,第三个人的编号为3号,第N个人的编号为N号,现在提供一个数字M,第一个人开始从1报数,第二个人报的数就是2,依次类推,报到M这个数字的人出局,紧接着从出局的这个人的下一个人重新开始从1报数,和上面过程类似,报到M的人出局,直到N个人全部出局,请问,这个出局的顺序是什么?

二.解题流程

1.问题重述

通过阅读题干,我们可总结出本题的关键信息:

N个人围成一个圈,编号由一到N ,报到M 的人出局直到剩最后一个人,求出局的顺序。

2.分析问题

        首先,要求出局的顺序,说明要按出局顺序输出这N个人的编号,所以我们先要存储N个人的编号,存储已知个数的数据,首先想到的是数组,但数组开辟的固定空间无法释放,若N的值非常大,会造成空间占用过大,此时可以想到用链表存储这N个数据,可通过释放节点空间进行空间节约,同时在出局操作中链表也更便捷。

        解决了存储问题后,第二步我们要考虑的便是如何淘汰报数到M的人,这里的解决方法根据存储方式的不同而不同,我们下面对数组和链表分别进行分析。

3.解决问题

1.普通数组法

        最直接的思路便是把N个人的编号存储到大小为N的数组中。

        因为题目要求从1开始报数,所以我们可以从数组的下标1开始存储,不使用下标0。

        对于出局的操作,我们将出局的人的编号改为一个标记值,例如本题可以选择将淘汰的人的标号值赋值为零,通过判断0的个数得出淘汰了多少人。

        最后解决什么时候数到M的问题,本题必然涉及遍历数组,但遍历过程中的 i 值是固定1-N的,永远都在 M 的倍数处赋0,无法实现目标。这时转换思路,重新定义一个变量例如 j ,j 从 1 开始自增,遇到 M 时把对应的编号置零,这是可能有疑问,报数到M后从新从1开始报数,如果 j 一直自增如何遇到第二个M , 其实很好解决,只要 j 是 M 的整数倍,便是下一次报到M的时候,即 ( j % M == 0 ) 。至此几个关键问题我们均已解决,接下来便是代码实现了。

#include <iostream>
using namespace std;
int main()
{
    int n,m;
    cin >> n >> m;
    int cnt = n;//存储人数
    int p[n];//编号数组
    int j=1;
    //构建编号数组
    for(int i=1;i<=n;i++)
    {
        p[i] = i;
    }
    //人数大于1时进入循环
    while(cnt > 1)
    {
        //循环遍历数组
        for(int i=1;i<=n;i++)
        {
            if(p[i]==0)continue;//标记为0 跳过本次循环
            if( j % m == 0 )//报数到M
            {
                cnt--;//人数自减
                cout << p[i] << " ";//输出出局人编号
                p[i]=0;//标记为0
                j++;//报数自增
            }
            else 
            {
                j++;//报数自增
            }
        }
    }
    for(int i=0;i<n;i++)
    {
        if(p[i]!=0)cout << p[i] << endl;//输出最后的幸存者的编号
    }
    return 0;
}

2.1.循环链表法

使用循环链表在空间利用上更加灵活,同时思路更加清晰,出局操作只需要将前一节点的指针指向出局结点的后一个结点即可,同时将出局的节点空间释放。

#include <stdio.h>

int main()
{
    int n, m;
    scanf("%d %d", &n, &m);
    struct List
    {
        int num;
        struct List *next;
    };
    List *head = new List;//虚拟头结点
    List *p = new List;//链表结点
    p = head;//初始结点在虚拟头结点位置
    // 创建链表
    for (int i = 1; i <= n; i++)
    {
        List *node = new List;//新建结点
        node->num = i;//赋初值
        node->next = NULL;//新结点的后结点初始化为空
        //p随node移动形成链式结构
        p->next = node;
        p = p->next;
    }
    p->next = head->next;//尾结点指向头结点形成循环链表
    // 遇到m-1 指针跳两个 指向自己时停
    p = head->next;//p从第一个结点开始
    int i = 1;//报数的值
    while (p->next->num != p->num)//只剩一个结点(指针指向自己)时结束
    {
        if (i == m - 1)//出局结点的前一个结点
        {
            printf("%d ", p->next->num);//输出出局者编号
            List *temp = p->next;//临时变量存出局者结点
            p->next = p->next->next;//淘汰出局者
            i = 0;//报数归零
            delete temp;//释放出局者空间
        }
        i++;//从一报数
        p = p->next;//移动链表
    }
    printf("%d", p->num);//输出幸存者编号
    return 0;
}

2.2模拟链表法

对于链表运用不熟的伙伴,我们可以用数组模拟链表的效果,用数组下标表示每个人的编号,数组的值为下一个结点的地址(下一个下标)。同时最后一个结点指回头部。

#include <stdio.h>
int main()
{
    int n, m;
    scanf("%d %d", &n, &m);
    int peo[n];
    for (int i = 1; i <= n; i++)//初始化模拟链表
    {
        peo[i] = i + 1;//数组的值为下一个结点的下标
    }
    peo[n] = 1;//最后一个值指向头结点形成循环
    int k = 1;
    while (peo[k] != k)
    {
        for (int i = 1; i < m - 1; i++)
        {
            k = peo[k];//移动链表
            printf("%d ", peo[k]);
        }
        k = peo[k] = peo[peo[k]];//淘汰出局者
    }
    printf("%d", k);//打印幸存者
    return 0;
}

3.递归法

本题的结束条件是人数变为1,我们可将此条件作为递归结束条件,另一个输入变量是M 可作为函数的另一个参数,由此构建一个函数 f (int n , int m)

其中 n 表示当前环内人数,m 表示报数淘汰者报数

最终要求的是淘汰人的编号,所以下一步要找出每一轮报数与最初编号的关系:

第一次报数顺序(即编号)为  1 2  ...    M      M+1    M+2                     ...                        一直到 n

淘汰一次后报数顺序变为         1 2  ...   淘汰     1          2        M           M+1       M + 2

淘汰两次后报数顺序变为         1 2  ...              1           2       淘汰          1             2

上下对应可发现 每个人的编号等于当前报的数加M取余n,n为当前轮次的人数。 因为从1开始编号,所以需要将(报数+M-1)后再对n取余(从1开始会使加M的结果多1,顾减去一个1),取余后再加1 ,确保从1开始编号的结果。

#include <iostream>

using namespace std;

int f(int n,int m)
{
    return n == 1 ? n : (f(n - 1 , m) + m - 1) % n + 1;
}

int main()
{
    int n,m;
    cin >> n >> m;
    int ans = f(n,m);
    cout << ans << endl;
    return 0;
}

需要注意的是递归法只能求得最后幸存者的编号,无法输出前几次淘汰者的编号。 

4.数学归纳法

由于递归对空间占用较大,我们可以通过数学归纳法总结刚才的规律,得到公式。

将刚才递归的推到过程逆转,最后幸存者的编号等于他报的数加M取余当前的人数(n==1),倒数第二轮幸存者必然安全,他的编号同样等于他报的数加M取余n(两个人n==2)以此类推,直到n个人时,幸存者的编号为报的数加M取余n(n==n)。取余的数由1到n ,用一个循环即可实现。

#include <iostream>
using namespace std;
int main()
{
    int n,m;
    cin >> n >> m;
    int ans = 0;
    for(int i=1;i<=n;i++)
    {
        ans = (ans+m)%i;
    }
    cout << ans + 1 << endl;//从1开始报数
    return 0;
}

同样这个方法无法得到每一次出局人编号,只能得到最后幸存者的编号。 


http://www.kler.cn/a/441389.html

相关文章:

  • 996引擎 - NPC-动态创建NPC
  • 在Rust应用中访问.ini格式的配置文件
  • K8S中的数据存储之基本存储
  • Pyecharts之地图图表的强大功能
  • Linux C openssl aes-128-cbc demo
  • INCOSE需求编写指南-第1部分:介绍
  • 【学习笔记】桌面浏览器的视口
  • 【mysql】大型互联网项目为什么考虑禁止使用外键
  • 中阳科技:量化模型驱动的智能交易革命
  • DATA-HUB 安装与启动:
  • 静态路由、RIP、OSPF、BGP的区别
  • Qt 实现 UDP 广播的详细教程
  • 部署WordPress6.7.1版本(官网最新版本)
  • C# 机器视觉-RANSAC算法拟合圆
  • 基于RRT(Rapidly-exploring Random Tree)的无人机三维路径规划,MATLAB代码
  • 【Redis】一人一单秒杀活动
  • Spring Boot 启动时间优化全攻略
  • macos big sur 软件icons图标大全(新增至2719枚大苏尔风格图标)
  • Nodejs架构
  • 【MySQL中多表查询和函数】
  • Linux 入门指南(详细版:基于 CentOS,使用 WSL 环境)
  • 【Linux】软件包管理与vim工具使用详解
  • 微服务系统架构设计参考
  • 题目 3010: 奇偶数之和
  • 【算法day14】二叉树:搜索树的递归问题
  • 如何利用Python爬虫京东获得JD商品详情