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

news2024/12/28 21:05:10

文章目录

  • 前言
  • 一、暴力法
  • 二、动态规划
  • 三、实战
    • 3.1 力扣 1823. 找出游戏的获胜者
    • 3.2 洛谷 P1996 约瑟夫问题
  • 参考

约瑟夫问题

前言

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

据说著名犹太历史学家 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 ) dp[i] = (dp[i - 1] + m) % i dp[i]=(dp[i1]+m)
  • 初始状态: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 的人再出圈,依次类推,直到所有的人都出圈,请输出依次出圈人的编号。

这里要依次求出圈人的编号, 可以使用暴力法。

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. 约瑟夫问题求解

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/186629.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

K8s关键性概念图解

Kubernetes可以看做云原生时代的操作系统&#xff0c;统一管理下层的基础设施&#xff0c;如计算资源、网络资源、存储资源等等。将集群中存在的各种复杂关系抽象成各种API资源&#xff0c;以统一的方式暴露出各种接口&#xff0c;也便于未来的扩展以及开发团队根据自己的需要定…

JS创建ZIP文件,JSZip的使用

Hi I’m Shendi 最近编写压缩工具&#xff0c;需要使用js创建zip文件&#xff0c;使用 JSZip 插件 官网&#xff1a; https://stuk.github.io/jszip/ Github&#xff1a; https://github.com/Stuk/jszip https://sdpro.top/blog/html/article/1012.html 下载 NPM : npm inst…

论文笔记:NeRF: Representing Scenes as Neural Radiance Fields for View Synthesis

目录 文章摘要 1 Neural Radiance Field Scene Representation (基于神经辐射场的场景表示) 2 Volume Rendering with Radiance Fields (基于辐射场的体素渲染) 2.1 经典渲染方程 2.2 经典的体素渲染方法 2.3 基于分段采样近似的体素渲染方法 3 Optimizing a Neural Rad…

高通开发系列 - linux kernel启动阶段串口无打印采用LED点灯

By: fulinux E-mail: fulinux@sina.com Blog: https://blog.csdn.net/fulinus 喜欢的盆友欢迎点赞和订阅! 你的喜欢就是我写作的动力! 目录 问题背景kernel启动汇编探测kernel启动C函数阶段探测这篇文章之前请参考下:高通开发系列 - MSM8909指示灯操作 问题背景 最近在基于…

38. 外观数列

打卡!!!每日一题 今天给大家带来一道比较有意思的题目&#xff0c;先看看题目描述 题目描述&#xff1a; 题目示例&#xff1a; 大家题目读完可能还没太理解什么意思&#xff0c;我简单给大家翻译翻译&#xff1a; n&#xff1a;表示我们要计算多少次 每一次都是对前面一个…

详细介绍Sentence-BERT:使用连体BERT网络的句子嵌入

Sentence-BERT:Sentence Embeddings using Siamese BERT-Networks 使用连体BERT网络的句子嵌入 BERT和RoBERTa在诸如语义文本相似性&#xff08;STS&#xff09;的句子对回归任务上创造了新的最优的性能。然而&#xff0c;它要求将两个句子都输入网络&#xff0c;这导致了巨大的…

代码随想录--二叉树章节总结Part IV 完结篇

代码随想录–二叉树章节总结Part IV 完结篇&#x1f389; 1.Leetcode501 二叉树中的众数 给你一个含重复值的二叉搜索树&#xff08;BST&#xff09;的根节点 root &#xff0c;找出并返回 BST 中的所有 众数&#xff08;即&#xff0c;出现频率最高的元素&#xff09;。 如…

大数据行业如何获取高薪岗位offer?

在互联网行业需要保持不断的学习。学习大数据先思考自身未来想往哪个方向发展&#xff0c;想要入门快、基础深厚&#xff0c;并且需求多应用广建议从JAVA开始学起&#xff0c;找到适合自己的学习方法。 大数据行业人才稀缺&#xff0c;据第三方统计2020年全国招收程序员394699…

mybatis-plus2

目录 一、乐观锁 二、乐观锁与悲观锁的区别 1.乐观锁和悲观锁的应用场景 三、条件查询构造器 四、分页查询 五、逻辑删除 六、在Mybatis-plus中使用xml配置 一、乐观锁 乐观锁插件 | MyBatis-PlusMyBatis-Plus 官方文档https://baomidou.com/pages/0d93c0/ 当要更新一条…

16. JSON解析

1. 什么是 JSON &#xff1f; JSON 指的是 JavaScript 对象表示法&#xff08;JavaScript Object Notation&#xff09;。 JSON 是轻量级的文本数据交换格式。 JSON 独立于语言&#xff1a;JSON 使用 Javascript语法来描述数据对象&#xff0c;但是 JSON 仍然独立于语言和平台…

Kettle基础操作

目录 Kettle基础操作 1 启动Kettle 2 创建本地资源库 3 基础操作 3.1 新建转换 3.2 新建作业 3.3 节点连接 4 导入/导出资源库 5 创建数据库连链接 Kettle基础操作 1 启动Kettle 前置环境&#xff1a;JDK 1.7以上、IE浏览器升级至IE10以上&#xff08;Kettle7.0以下…

Java——打家劫舍

题目链接 leetcode在线oj题——打家劫舍 题目描述 你是一个专业的小偷&#xff0c;计划偷窃沿街的房屋。每间房内都藏有一定的现金&#xff0c;影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统&#xff0c;如果两间相邻的房屋在同一晚上被小偷闯入&#xff…

Hive数仓建设手册

1 数仓的分层及建模理论 1.1 数据仓库的用途 整合公司所有业务数据&#xff0c;建立统一的数据中心产生业务报表&#xff0c;用于作出决策为网站运营提供运营上的数据支持可以作为各个业务的数据源&#xff0c;形成业务数据互相反馈的良性循环分析用户行为数据&#xff0c;通…

MySQL中的正则表达式

目录 一.介绍 二.格式 三.操作 一.介绍 正则表达式(regular expression)描述了一种字符串匹配的规则&#xff0c;正则表达式本身就是一个字符串&#xff0c;使用这个字符串来描述、用来定义匹配规则&#xff0c;匹配一系列符合某个句法规则的字符串。在开发中&#xff0c;正…

5G小区选择重选参数的设置

学习大唐杯的过程中的一些总结。 目录 前言 一、S准则 二、R准则 三、关于频点优先级 总结 前言 5G参数总体的设计思想是在总体成本的控制下&#xff0c;满足覆盖范围和容量的同时&#xff0c;达到5G各个小区之间正确进行通信。 一、S准则 在后续介绍的频点优先级中&#xff0…

【多任务】任务损失/梯度优化策略合集

本文分享如何从loss和gradient方面,优化多任务模型,缓解负迁移或跷跷板的问题。不足之处,还望批评指正。 背景 最近工作中,有使用到多任务模型,但实际使用时,会面临负迁移、跷跷板等现象。 除了从模型角度优化,这里介绍从loss和gradient方面的优化

SpringBoot创建接口

目录 一、创建Spring Boot Project (一)配置Spring Boot (二)配置数据库连接&#xff0c;并启动tomcat 二、新建测试网页——这一步可以忽略&#xff0c;主要是测试配置是否成功 1.新建一个页面 2.创建TestController类 3.启动SpringbootpracticeApplication类——sprin…

【Linux】makemakefile

【Linux】make & makefile 文章目录【Linux】make & makefile1、makefile文件2、make命令3、make原理规则4、.PHONY5、编译与否的判断法1、makefile文件 makefile实际上是一个文件&#xff0c;配置文件 充当Linux上的工程管理工具&#xff0c;可以实现自动化编译 mak…

transformer库的思想

transformer库建立思路 (1) Model类: 如BertModel , 目前收录有超过30个PyTorch模型或Keras模型; (2) Configuration类: 如BertConfig , 用于存储搭建模型的参数; (3) Tokenizer类: 如BertTokenizer , 用于存储分词词汇表以及编码方式; 使用from_pretrained()和save_pretraine…

区块链知识系列 - 系统学习EVM(二)

特点 EVM出于所谓运算速度和效率方面考虑&#xff0c;采用了非主流的256bit整数。不支持浮点数缺乏标准库支持,例如字符串拼接、切割、查找等等都需要开发者自己实现给合约打补丁或是部分升级合约代码在EVM中是完全不可能的 存储 Code code 部署合约时储存 data 字段也就是合…