重温数据结构与算法之约瑟夫问题

这篇具有很好参考价值的文章主要介绍了重温数据结构与算法之约瑟夫问题。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。


数据结构约瑟夫问题总结,java,leetcode,链表,数据结构,约瑟夫环,约瑟夫问题,动态规划

前言

约瑟夫问题,是一个计算机科学和数学中的问题,在计算机编程的算法中,类似问题又称为约瑟夫环,又称“丢手绢问题”。

据说著名犹太历史学家 Josephus 有过以下的故事:

在罗马人占领乔塔帕特后,39个犹太人与Josephus及他的朋友躲到一个洞中,39个犹太人决定宁愿死也不要被敌人抓到,于是决定了一个自杀方式,41个人排成一个圆圈,由第1个人开始报数,每报数到第3人该人就必须自杀,然后再由下一个重新报数,直到所有人都自杀身亡为止。

然而 Josephus 和他的朋友并不想遵从这个规则,Josephus 要他的朋友先假装遵从,他将朋友与自己安排在第16个与第31个位置,于是逃过了这场死亡游戏。

一、暴力法

可以使用暴力法模拟整个过程:

  • 首先自定义一个循环链表,需要将末尾节点的 next 指针指向首节点
  • n(= 41) 个人编号从 0-40,添加到链表中
  • 删除节点需要一个前置节点 pre ,和一个要删除的当前节点 cur,这样删除时只需要将 pre 的 next 指向 cur 的 next 就能实现删除cur 节点
  • 定义1个计数器变量 count 当达到 m(= 3) 时,删除节点并将计数器归0
  • 由于最终存活2人,循环条件可以为 cur.next != pre 。如果存活1人,条件可以为 cur.next != cur。
  • 所以最终存活编号 15 和 30,由于这个编号从 0 开始,所以 Josephus 将朋友与自己安排在第16个与第31个位置,逃过了这场死亡游戏。
class Node {
    int data;
    Node next;
}

class CycleLinkedList  {
    Node first;
    Node last;

    public void add(int o) {
        Node l = last;
        Node n = new Node();
        n.data = o;
        last = n;
        if (l == null) {
            first = n;
        } else {
            l.next = n;
        }
        last.next = first;
    }
}

public void josephus() {

    int n = 41, m = 3;
    CycleLinkedList list = new CycleLinkedList();
    for (int i = 0; i < n; i++) {
        list.add(i);
    }

    int count = 0;
    Node pre = list.first;
    Node cur = pre;
    while (cur.next != pre) {
        count++;

        if (count == m) {
            pre.next = cur.next;
            System.out.println(" killer 编号:" + cur.data);
            count = 0;
        } else {
            pre = cur;
        }
        cur = cur.next;
    }
    System.out.println("最终存活2人编号:" + cur.data + "," + pre.data);
    // 最终存活2人编号:15,30
}

二、动态规划

上述暴力法易于理解,实现简单。但是重复遍历降低效率,每删除一个元素需要移动 m 步,对于 n 个元素,时间复杂度为 O ( m n ) O(mn) O(mn)

其实可以发现上述问题其实满足动态规划的解决范畴。每一步求编号就是子问题,上一步求得的编号对下一步有帮助。那么 dp 重要的三部分如下:

  • 状态定义:dp[i] 表示 约瑟夫问题的解,即 i 个元素每 m 个删去,最终留下元素的编号

  • 转移方程推导:

    • 0 , 1 , ⋯   , i − 2 ⏟ i-1个 \underbrace{0,1,\cdots, i-2}_{\text{i-1个}} i-1 0,1,,i2 得到最终元素编号为 d p [ i − 1 ] dp[i-1] dp[i1]

    • 那么 0 , 1 , ⋯   , k − 1 , k , k + 1 , ⋯   , i − 1 ⏟ i个 \underbrace{0,1,\cdots,k-1,k,k+1,\cdots, i-1}_{\text{i个}} i 0,1,,k1,k,k+1,,i1 去掉第一个 ( m − 1 ) % i = k (m - 1)\%i = k (m1)%i=k后(存在m大于i的情况,需要取余),剩下 k + 1 , k + 2 , ⋯   , i − 2 , i − 1 , 0 , 1 , ⋯   , k − 1 ⏟ i-1个 \underbrace{k+1,k+2,\cdots,i-2,i-1,0,1,\cdots,k-1}_{\text{i-1个}} i-1 k+1,k+2,,i2,i1,0,1,,k1,元素数量也变为 i − 1 i-1 i1个,由于元素顺序是递增而且是环状的,到达最大又会从最小开始,可以等价到上述从 0 0 0开始到 i − 2 i-2 i2的数字序列。

    • 得到 d p [ i − 1 ] dp[i-1] dp[i1] 的数字序列是从0开始,那么可以推导 d p [ i ] dp[i] dp[i]了,为 d p [ i ] = ( k + 1 + d p [ i − 1 ] ) % i dp[i] = (k + 1 + dp[i - 1] ) \% i dp[i]=(k+1+dp[i1])%i d p [ i ] = ( ( m − 1 ) % i + 1 + d p [ i − 1 ] ) % i dp[i] = ((m - 1)\%i + 1 + dp[i - 1] ) \% i dp[i]=((m1)%i+1+dp[i1])%i

    • 所以最终推导方程为: d p [ i ] = ( d p [ i − 1 ] + m ) % i dp[i] = (dp[i - 1] + m) \% i dp[i]=(dp[i1]+m)%i

  • 初始状态:dp[1]=0 表示 1个元素最终留下元素的编号为0

public void josephus1() {

    int n = 41, m = 3;
    int [] dp = new int[n + 1];
    dp[1] = 0;
    for (int i = 2; i < n + 1; i++) {
        dp[i] = (dp[i - 1] + m) % i;
    }
    System.out.println(dp[n]);
}

// 上述dp数组可用变量替代
public void josephus2() {

    int n = 41, m = 3;
    int start = 0;
    for (int i = 2; i < n + 1; i++) {
        start = (start + m) % i;
    }
    System.out.println(start);
}

上述使用动态规划可以求出最后存活人的编号,而且时间复杂度为 O ( n ) O(n) O(n),使用上述代码有个问题就是不能得到倒数第2个人的编号,dp[n-1]是40个人最后存活人的编号。其实也可以利用 dp 的思想求倒数第二个人的编号。

  • 状态定义:dp[i] 表示 i 个元素每 m 个删去,倒数第 2 个留下元素的编号
  • 状态转移方程:和上面一样, d p [ i ] = ( d p [ i − 1 ] + m ) % i dp[i] = (dp[i - 1] + m) \% i dp[i]=(dp[i1]+m)%i
  • 初始状态:dp[2] = (m + 1) % 2
int n = 41, m = 3;
int start = (m + 1) % 2;
for (int i = 3; i < n + 1; i++) {
    start = (start + m) % i;
}
System.out.println(start);

三、实战

3.1 力扣 1823. 找出游戏的获胜者

https://leetcode.cn/problems/find-the-winner-of-the-circular-game/

共有 n 名小伙伴一起做游戏。小伙伴们围成一圈,按 顺时针顺序 从 1 到 n 编号。确切地说,从第 i 名小伙伴顺时针移动一位会到达第 (i+1) 名小伙伴的位置,其中 1 <= i < n ,从第 n 名小伙伴顺时针移动一位会回到第 1 名小伙伴的位置。

游戏遵循如下规则:

从第 1 名小伙伴所在位置 开始 。
沿着顺时针方向数 k 名小伙伴,计数时需要 包含 起始时的那位小伙伴。逐个绕圈进行计数,一些小伙伴可能会被数过不止一次。
你数到的最后一名小伙伴需要离开圈子,并视作输掉游戏。
如果圈子中仍然有不止一名小伙伴,从刚刚输掉的小伙伴的 顺时针下一位 小伙伴 开始,回到步骤 2 继续执行。
否则,圈子中最后一名小伙伴赢得游戏。

给你参与游戏的小伙伴总数 n ,和一个整数 k ,返回游戏的获胜者。

经典的约瑟夫环问题,不过换了个说法

public int findTheWinner(int n, int k) {
	int start = 0;
    for (int i = 2; i < n + 1; i++) {
        start = (start + k) % i;
    }
    return start + 1;
}

3.2 洛谷 P1996 约瑟夫问题

https://www.luogu.com.cn/problem/P1996

n 个人围成一圈,从第一个人开始报数,数到 m 的人出列,再由下一个人重新从 1 开始报数,数到 m 的人再出圈,依次类推,直到所有的人都出圈,请输出依次出圈人的编号。

这里要依次求出圈人的编号, 可以使用暴力法。文章来源地址https://www.toymoban.com/news/detail-720501.html

import java.io.*;
import java.util.*;
public class Main {
    static class Node {
        int data;
        Node next;
    }

    static class CycleLinkedList  {
        Node first;
        Node last;

        public void add(int o) {
            Node l = last;
            Node n = new Node();
            n.data = o;
            last = n;
            if (l == null) {
                first = n;
            } else {
                l.next = n;
            }
            last.next = first;
        }
    }
    public static void main(String args[]) throws Exception {
        Scanner cin=new Scanner(System.in);
        int n = cin.nextInt(), m = cin.nextInt();
        int [] ans = new int[n];
        int c = 0;

        CycleLinkedList list = new CycleLinkedList();
        for (int i = 0; i < n; i++) {
            list.add(i + 1);
        }

        int count = 0;
        Node pre = list.first;
        Node cur = pre;
        while (cur.next != cur) {
            count++;

            if (count == m) {
                pre.next = cur.next;
                ans[c++] = cur.data;
                count = 0;
            } else {
                pre = cur;
            }
            cur = cur.next;
        }
        ans[c] = cur.data;
        for (int an : ans) {
            System.out.print(an + " ");
        }
    }
}

参考

  1. 约瑟夫环的三种解法
  2. 这或许是你能找到的最详细约瑟夫环数学推导!
  3. 约瑟夫问题求解

到了这里,关于重温数据结构与算法之约瑟夫问题的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 【数据结构】使用循环链表结构实现约瑟夫环问题

    目录 1.循环链表的定义 2.约瑟夫环问题 3.创建循环链表 4.删除节点操作 5.打印所有节点 6.实现约瑟夫环问题的完整程序代码 🌈嗨!我是Filotimo__🌈。很高兴与大家相识,希望我的博客能对你有所帮助。 💡本文由Filotimo__✍️原创,首发于CSDN📚。 📣如需转载,请事先与我联

    2024年01月18日
    浏览(39)
  • 数据结构—约瑟夫环问题(C语言版)

    目录 首先什么是约瑟夫环 约瑟夫环实现方式 一、创建结构体变量 二、初始化链表 三、构建循环链表 四、删除链表  五、完整代码及注释讲解 约瑟夫环 是 循环链表 中的一个经典问题;题目描述:n 个人围成一圈,从第一个人开始报数,数到 m 的人出列,再由下一个人重新

    2024年02月11日
    浏览(42)
  • 数据结构学习-循环链表:处理约瑟夫环问题

    目录 问题描述 一、基本概念  1.普通链表 2.单向循环链表  二、问题处理 1.创建链表 2.查找 3.删除  4.其他  三.实验环节 四.总结 约瑟夫环问题的一种描述是:编号为1,2,...,n的n个人按顺时针方向围坐一圈,每人持有一个密码(正整数)。一开始任选一个正整数作为报数

    2024年02月07日
    浏览(39)
  • C语言数据结构篇——约瑟夫环的实现

    作者名:Demo不是emo  主页面链接 : 主页传送门 创作初心: 对于计算机的学习者来说,初期的学习无疑是最迷茫和难以坚持的,中后期主要是经验和能力的提高,我也刚接触计算机1年,也在不断的探索,在CSDN写博客主要是为了分享自己的学习历程,学习方法,总结的经验等

    2023年04月08日
    浏览(33)
  • C语言数据结构-用链表解决约瑟夫环问题

    只是普通的大学生一枚,不会很牛的技巧和算法,只是在做数据结构作业中的一点感悟和思考。也不知道自己写得对不对,有什么意见和建议都可以直接指出来哦,我虚心接受(低头鞠躬.jpg)...... 试用线性表的链表存储结构来实现约瑟夫(Josephu)问题。约瑟夫问题如下:设有

    2024年02月06日
    浏览(44)
  • 数据结构实验---顺序表的合并---链表的基本操作---重点解析约瑟夫问题

    实验的写法多种多样,但本文并未采用 #define 定义容量的写法,这样写已经是很老旧过时的写法。所有实验主体采用均为动态开辟,后续如果利用 C++ 来写或许会应用更多语法… 本篇展示数据结构的两个实验 其中,重点分析约瑟夫问题 实验中代码的命名风格等均与下方博客

    2024年02月16日
    浏览(58)
  • 数据结构上机实验——栈和队列的实现、栈和队列的应用、进制转换、约瑟夫环问题

      1.利用栈的基本操作实现将任意一个十进制整数转化为R进制整数。   2.利用循环队列实现.约瑟夫环问题:已知n个人(以编号1,2,3…n分别表示)围坐在一张圆桌周围。从编号为k的人开始报数,数到k的那个人出圈;他的下一个人又从1开始报数,数到k的那个人出圈;依

    2024年02月08日
    浏览(40)
  • C语言---数据结构实验---顺序表的合并---链表的基本操作---重点解析约瑟夫问题

    实验的写法多种多样,但本文并未采用 #define 定义容量的写法,这样写已经是很老旧过时的写法。所有实验主体采用均为动态开辟,后续如果利用 C++ 来写或许会应用更多语法… 本篇展示数据结构的两个实验 其中,重点分析约瑟夫问题 实验中代码的命名风格等均与下方博客

    2024年02月16日
    浏览(65)
  • 【算法】约瑟夫环问题解析与实现

    约瑟夫环(Josephus Problem)是一个经典的数学问题,涉及一个编号为 1 到 n 的人围成一圈,从第一个人开始报数,报到某个数字 m 的人出列,然后再从下一个人开始报数,如此循环,直到所有人都出列。本篇博客将详细解析约瑟夫环问题,并使用 Python 实现算法。 在约瑟夫环问

    2024年02月06日
    浏览(41)
  • 303.【华为OD机试】报数游戏(约瑟夫环算法解题—Java&Python&C++&JS实现)

    本文收录于专栏:算法之翼 本专栏所有题目均包含优质解题思路,高质量解题代码(JavaPythonC++JS分别实现),详细代码讲解,助你深入学习,深度掌握!

    2024年04月15日
    浏览(36)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包