【算法】约瑟夫环问题

server/2024/10/18 5:05:37/

据说著名的犹太历史学家Josephus有过以下故事, 罗马人占领乔塔帕特, 39个犹太人与Josephus和他的朋友躲在洞中,其中39个犹太人决定自杀,
,他们的自杀方式是41个人绕成一圈,第一个人报数1,报数到3的人自杀。然后新一个人重新报数为1。最终活下来的人可以自由选择自己的命运。当剩下约瑟夫斯和他朋友时,说服了对方,选择向罗马军队投降,不再自杀。
约瑟夫斯把他的存活归因于运气或天意,他不知道是哪一个。其实,这是一个数学问题。

前置准备

  • 数据结构-链表
  • 本题至少需要了解链表删除节点的逻辑,和循环链表的概念

初阶部分:链表组织结构, 模拟上述的过程求解。(本解法在洛谷中只能过部分用例。考察基本的coding)😏
进阶部分:数学和递归,迭代解法。 给定洛谷题中不需要链表组织了, 但建议读者写一份链表版本的结构代码

题目:约瑟夫环

在这里插入图片描述

直观解法:模拟

一种直观的解法。
[1,n]的数据以循环单链表的形式存储。然后模拟计数过程自杀过程。
定义一个count变量, 当count报到k的时候就执行链表的删除, 然后重置count

  • 定义一个Main类, 里面定义一个内部类Node
java">static class Node {int val;Node next;public Node(int val) {this.val = val;}}

构建[1,n]的循环单链表

java">public static Node createList(int n) {if(n < 1) {return null;}//构建[1...n]的序列Node head = new Node(1);Node tail = head;for(int i=2;i<=n;i++) {tail.next = new Node(i);tail = tail.next;}tail.next = head;//成环。return head;}

模拟约瑟夫环逻辑, 返回最后存在的节点

java">public static Node josephusKilll(Node head, int k) {//链表为空,单节点的循环链表,输入k不合理的逻辑。if(head==null||head.next==head || k < 1) {return head;}Node last = head;//last是head的前驱节点while(last.next!=head) {last = last.next;}int count = 0;//计数变量while(head != last) {if(++count == k) {last.next = head.next;//删除head引用节点count = 0;//重新计数}else {last = last.next;}head = last.next;}return head;}

compute函数:返回最后存活节点的编号

java">public static int compute(int n, int k) {Node head = createList(n);//创建单向循环链表head = josephusKilll(head, k);//返回约瑟夫环最后的节点。return head==null?-1:head.val;}

main函数处理输入输出逻辑

java">public static void main(String[] args) throws IOException {BufferedReader br = new BufferedReader(new InputStreamReader(System.in));StreamTokenizer in = new StreamTokenizer(br);PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out));in.nextToken();int n = (int) in.nval;in.nextToken();int k = (int) in.nval;out.println(compute(n,k));out.close();br.close();}

Main类

java">import java.io.*;public class Main {static class Node {int val;Node next;public Node(int val) {this.val = val;}}public static Node createList(int n) {if(n < 1) {return null;}//构建[1...n]的序列Node head = new Node(1);Node tail = head;for(int i=2;i<=n;i++) {tail.next = new Node(i);tail = tail.next;}tail.next = head;//成环。return head;}public static Node josephusKilll(Node head, int k) {//链表为空,单节点的循环链表,输入k不合理的逻辑。if(head==null||head.next==head || k < 1) {return head;}Node last = head;//last是head的前驱节点while(last.next!=head) {last = last.next;}int count = 0;//计数变量while(head != last) {if(++count == k) {last.next = head.next;//删除head引用节点count = 0;//重新计数}else {last = last.next;}head = last.next;}return head;}public static int compute(int n, int k) {Node head = createList(n);//创建单向循环链表head = josephusKilll(head, k);//返回约瑟夫环最后的节点。return head==null?-1:head.val;}public static void main(String[] args) throws IOException {BufferedReader br = new BufferedReader(new InputStreamReader(System.in));StreamTokenizer in = new StreamTokenizer(br);PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out));in.nextToken();int n = (int) in.nval;in.nextToken();int k = (int) in.nval;out.println(compute(n,k));out.close();br.close();}
}

提交代码到洛谷上
在这里插入图片描述

呃, 超时了。

模拟时间复杂度分析

由于0<n,k<10^6 。
删除一个节点要循环k次,一共要删除n-1个节点。
总的时间复杂度 O ( n k ) O(nk) O(nk), 当k,n都取最大时,k与n是线性关系,时间复杂度可认为 O ( n 2 ) O(n^2) O(n2)
因此, 对于简单小的数据测试量还可以, 但规模一大逃不开超时的命运。

解决方案2

方案1的方法, 在于不知道最后哪个节点能活下来, 只能靠着计数模拟的方式, 一步一步淘汰节点, 通过存在留下的节点确定最终节点。
那么, 有没有更高效的方式。比如,给我一个通项公式直接求出来, 或者递推公式递推一下求出最后的节点。

假设环形链表1->2->3->4->5->1, 节点数是5,报数m是3。
那么最终节点是4活下来。

节点编号
11
22
33
44
55

干掉报数为3的节点, 这里标记为-表示被干掉了。

节点编号
13
24
3-
41
52

继续

节点编号
1-
21
3-
42
53

重复

节点编号
1-
21 & 3
3-
42
5-

最后,节点数小于m了,要重复报数了。
发现节点2报了3,干掉节点2。
最终节点4存活。
且节点4最后编号是1。

节点编号
41

好的, 我们记录一下最终存活节点的编号。
记作函数Num

编号函数Num(i), i是当前存活的节点数量
Num(1) = 1
Num(2)
Num(i-1)
Num(i)

可以得出上述节点4的编号规律
4->1
4->2
4->2
4->1
4->4

🆗,我们用自然智慧归纳出这个公式。
最后存活节点的编号是1, 请记住我是从编号1开始推导的。

老编号删除s后的新编号
1i-s+1
2i-s+2
s-2i-2
s-1i-1
s-
s+11
s+22
ii-s
  • 接下来有些神奇操作。
  1. 所有小于S的老编号对于的新编号满足
    o l d = n e w + s − i old = new + s - i old=new+si这个表达式。
  2. 所有大于S的老编号对于新编号满足
    o l d = n e w + s old = new + s old=new+s这个表达式。
  • 可以统一两个公式吗?
    答案是可以的。
    对于2的关系式,
    只需要 o l d = ( n e w + s − i − 1 ) % i + 1 old = (new + s - i - 1) \%i + 1 old=(new+si1)%i+1
    对于1的关系式, o l d = ( n e w + s − i ) % i + 1 old = (new + s - i)\%i + 1 old=new+si)%i+1这个表达式也是成立的。因为$0<=(new + s - i - 1)<S<=i。
    因为取模x的范围是[0,x],所以需要整体右移(-1)和上移(+1)进行调整, 不能单纯%i,因为会改变范围造成错误。
编号和报数的关系

s = ( m − 1 ) % i + 1 s = (m-1) \%i +1 s=m1%i+1

m:报数s:编号
11
ii
i+11
2ii

跟上面老编号和新编号的推导一样,通过取模和拼凑出来[1...i]的范围。

n与m的关系

老编号 = (新编号 + s − 1 ) % i + 1 老编号 = (新编号 + s - 1) \%i + 1 老编号=(新编号+s1)%i+1
s = ( m − 1 ) % i + 1 s = (m-1) \%i +1 s=m1%i+1
推导:
老编号 = ( 新编号 + ( m − 1 ) % i ) % i + 1 老编号 = (新编号 + (m-1)\%i) \%i +1 老编号=(新编号+(m1)%i)%i+1
化简:
老编号 = ( 新编号 + m − 1 ) % i + 1 老编号 = (新编号 + m-1) \%i +1 老编号=(新编号+m1)%i+1

f ( n , k ) , n 为当前节点总数, k 为报数 f(n,k), n为当前节点总数,k为报数 f(n,k),n为当前节点总数,k为报数
f ( n , k ) = ( f ( n − 1 , k ) + m − 1 ) % i + 1 f(n,k) = (f(n-1,k) + m - 1)\%i + 1 f(n,k)=(f(n1,k)+m1)%i+1

代码

迭代法重写compute函数即可, 不需要改上面的main函数

java">public static int compute(int n, int k){int ans = 1;//初始编号为1for(int i=2;i<=n;i++) {ans = (ans + k-1)%i + 1;//自下而上递推公式往上推。}return ans;}

perfect
在这里插入图片描述

递归法
本题递归过不了, 栈溢出报RE了。

java">public static int compute(int n, int k) {if(n==1) {return 1;//编号从1开始}else {return (compute(n-1,k)+k-1)%n+1;}}
参考和小小吐槽

本题唯一凹点是数学。
程序员代码面试指南这本书给了图解分析出解析式的推导
严格的数学推导

最后, 本题是线性递推,递归子问题严格不会重叠。 不知道为什么有些题标着动态规划的标签。🧐

最后

链表版本的josephusKilll, 感谢您的阅读。

java">public static Node josephusKilll(Node head, int k) {// 如果链表为空,或者链表中只有一个节点,或者k不合理(k < 1),直接返回原始链表if (head == null || head.next == head || k < 1) {return head;}// 初始化指针,指向链表的第二个节点,并开始计算链表中节点的数量Node cur = head.next;int n = 1; // 初始化节点数为1(因为head节点也算在内)// 遍历链表,计算链表的总长度nwhile (cur != head) { n++;  // 每次循环增加1,表示链表的节点数量cur = cur.next;  // 指针移动到下一个节点}// 使用compute(n, k)函数,计算约瑟夫环中最后幸存者的编号// 该编号是从1开始计算的n = compute(n, k);// 根据计算出的编号,移动到链表中对应的节点位置// n表示最终幸存者的位置,需要遍历链表n-1次while (--n != 0) {head = head.next;  // 移动到下一个节点}// 最终head指向的是约瑟夫环中最后剩下的节点,将该节点的next指向自己// 这样就将链表中其他节点删除,形成单节点循环链表head.next = head;// 返回幸存的节点return head;
}

http://www.ppmy.cn/server/132683.html

相关文章

M1 Mac打开Jupyter notebook

当我成功安装了Jupyter之后&#xff0c;发现无法通过 jupyter notebook 开始工作。 最初的问题是 zsh command not found 该问题是个路径问题&#xff0c;通过添加PATH环境变量就行了&#xff0c;设置环境变量时需要注意&#xff0c;zshrc和bash_profile中都可以设置&…

基于Matlab的人脸识别系统设计与仿真(含源文

目录 第一章 绪论 1.1 研究背景 1.2 人脸图像识别的应用前景 1.3 本文研究的问题 1.4 识别系统构成 1.5 论文的内容及组织 第二章 图像处理的Matlab实现 2.1 Matlab简介 2.2 数字图像处理及过程 2.2.1图像处理的基本操作 2.2.2图像类型的转换 2.2.3图像…

串口(UART)的FPGA设计(接收与发送模块)

目录 串口基础知识 一、什么是串口?有哪些特点? 二、常见的串口通信协议有哪些?他们有什么区别?

Failed to connect to github.com port 443

git push无法连接443端口 **问题1****方法一&#xff1a;取消代理设置**git命令 其他解决方案1. **设置 Git 使用 HTTP 而不是 HTTPS**2. **检查证书**3. **配置 Git 忽略 SSL 验证&#xff08;不推荐&#xff09;**4. **检查代理设置** 问题1 Failed to connect to github.com…

Android/鸿蒙应用的资源配置技巧

HarmonyOS NEXT的发布是一道分界线&#xff0c;它将脱离安卓架构&#xff0c;成为真正独立的操作系统&#xff0c;也被称为“纯血鸿蒙”。 目前已有多家头部企业正加速鸿蒙原生应用开发&#xff0c;包括支付宝、美团、京东、钉钉、小红书、新浪、网易等&#xff0c;覆盖便捷生…

GESP CCF python一级编程等级考试认证真题 2024年9月

一、单选题&#xff08;每题 2 分&#xff0c;共 30 分&#xff09; 第 1 题 据有关资料&#xff0c;山东大学于1972年研制成功DJL-1计算机&#xff0c;并于1973年投入运行&#xff0c;其综合性能居当时全国第三位。DJL-1计算机运算控制部分所使用的磁心存储元件由磁心颗粒组成…

性能优化-SQL查询优化技巧全解

SQL查询优化技巧全解 在数据库操作中&#xff0c;SQL查询的性能直接影响到应用程序的响应速度。本文将深入探讨SQL查询优化的关键技术&#xff0c;并结合实例展示如何使用itBuilder这款强大的数据库设计、建模软件&#xff0c;来辅助提升开发效率。 1. SQL查询基础与执行计划…

自由学习记录(7)

文件的判断是否存在&#xff0c;带上文件自己的名字 XmlSerializer (Person)serializer.Deserialize(reader); 如果出错之后&#xff0c;没有try来接&#xff0c;就会直接程序报错暂停&#xff0c; 有了的话无论如何都会继续正常进final using则是正常 为什么要用 using&a…