1 约瑟夫环
约瑟夫环问题起源于一个犹太故事。约瑟夫环问题的大意如下:
罗马人攻占了乔塔帕特,41个人藏在一个山洞中躲过了这场浩劫。这41个人中,包括历史学家Joseph(约瑟夫)和他的一个朋友。剩余的39个人为了表示不向罗马人臣服,决定集体自杀。大家决定了一个自杀方案,所有这41个人围成一个圆圈,由第1个人开始顺时针报数,每报数为3的人就立刻自杀,然由再由下一个人重新开始报数,仍然是每报数为3的人就立刻自杀……,直到所有人都自杀身亡为止。
约瑟夫和他的朋友些不想自杀,于是约瑟夫想到丁一个计策,他们两个同样参与到自术方案中,但是最后却躲过了自杀。请问,他们是怎么做到的?
一个明显的求解方法便是将41个人排成一个环,内圈为按照顺时针的最初编号,外圈为每个人数到数字3的顺序。可以使用数组来保存约瑟夫环中该自杀的数据,而数组的下标作为参与人员的编号,并将数组看作环形来处理。
package math_problem;
import java.util.Scanner;
public class JosephRingDemo {
/**
* @description : 约瑟夫环问题求解
* @params : [number:总人数, key:关键字]
* @return :
* @time : 2020/2/7 20:29
*/
private static void josephRing(int number, int key) {
// 若第 i 个人存活,则 men[i] == 0,否则 men[i] == 死亡序号
int[] men = new int[number];
int count = 0; // 人的死亡序号
int temp = 0, pos = -1; // pos:每个人对应的数组元素的下标
while (number - count >= key) {
do {
pos = (pos + 1) % number;
if (men[pos] == 0)
temp++;
// 若对应的人该死,则跳出循环,显示数据
if (temp == key) {
temp = 0;
break;
}
} while (true);
// 将该自杀的人序号所对应的元素值由 0 变为 自杀序号
men[pos] = ++count;
System.out.printf("第 %-3d 个人自杀!初始位置编号为 %-3d ,\n", count, pos + 1);
}
System.out.print("\n可存活的人的初始位置编号为:");
for (int j = 0; j < number; j++) {
if (men[j] == 0) {
System.out.printf("%-3d,", j + 1);
}
}
}
public static void main(String[] args) {
josephRing(41, 3);
}
}
第 1 个人自杀!初始位置编号为 3 ,
第 2 个人自杀!初始位置编号为 6 ,
第 3 个人自杀!初始位置编号为 9 ,
第 4 个人自杀!初始位置编号为 12 ,
第 5 个人自杀!初始位置编号为 15 ,
第 6 个人自杀!初始位置编号为 18 ,
第 7 个人自杀!初始位置编号为 21 ,
第 8 个人自杀!初始位置编号为 24 ,
第 9 个人自杀!初始位置编号为 27 ,
第 10 个人自杀!初始位置编号为 30 ,
第 11 个人自杀!初始位置编号为 33 ,
第 12 个人自杀!初始位置编号为 36 ,
第 13 个人自杀!初始位置编号为 39 ,
第 14 个人自杀!初始位置编号为 1 ,
第 15 个人自杀!初始位置编号为 5 ,
第 16 个人自杀!初始位置编号为 10 ,
第 17 个人自杀!初始位置编号为 14 ,
第 18 个人自杀!初始位置编号为 19 ,
第 19 个人自杀!初始位置编号为 23 ,
第 20 个人自杀!初始位置编号为 28 ,
第 21 个人自杀!初始位置编号为 32 ,
第 22 个人自杀!初始位置编号为 37 ,
第 23 个人自杀!初始位置编号为 41 ,
第 24 个人自杀!初始位置编号为 7 ,
第 25 个人自杀!初始位置编号为 13 ,
第 26 个人自杀!初始位置编号为 20 ,
第 27 个人自杀!初始位置编号为 26 ,
第 28 个人自杀!初始位置编号为 34 ,
第 29 个人自杀!初始位置编号为 40 ,
第 30 个人自杀!初始位置编号为 8 ,
第 31 个人自杀!初始位置编号为 17 ,
第 32 个人自杀!初始位置编号为 29 ,
第 33 个人自杀!初始位置编号为 38 ,
第 34 个人自杀!初始位置编号为 11 ,
第 35 个人自杀!初始位置编号为 25 ,
第 36 个人自杀!初始位置编号为 2 ,
第 37 个人自杀!初始位置编号为 22 ,
第 38 个人自杀!初始位置编号为 4 ,
第 39 个人自杀!初始位置编号为 35 ,
可存活的人的初始位置编号为:16 ,31 ,
Process finished with exit code 0
2 复杂的约瑟夫环
在历史上,约瑟夫环被广泛研究。一个推广的约瑟夫环问题如下:
有n个人环坐一圈,按照顺时针方向依次编号为1、2、3、…、m。有一个黑盒子中放置着许多纸条,其上随机写有数字。每个人随机取一个纸条,纸条上的数字为出列数字。游戏开始时,任选一个出列数字。从第一个人开始,按编号顺序自1开始顺序报数,报到m的人出列,同时将其手中的数字作为新的出列数字。然后,从下一个人开始重新从1报数,如此循环报数下去。问最后剩下哪个人?
先来分析一下这个问题,同前面的约瑟夫环相比,这里要明显复杂,主要体现在:
“参与人的个数为n是一个可变量,因此,不能使用数组来表示,应该使用链表结构来表示。另外,这n个人首尾相接构成一个环,因此应该使用循环链表来处理该问题。
package math_problem;
import java.util.Scanner;
class LNode {
int no; // 游戏者编号
int key; // 保存游戏者出列数字
LNode next; // 指向下一个节点
public LNode(int no, int key, LNode next) {
this.no = no;
this.key = key;
this.next = next;
}
public LNode(int no, int key) {
this.no = no;
this.key = key;
}
}
// 循环链表:尾指针 next 指向头结点
public class CircularLinkedList {
static LNode head = null;
static LNode tail = null;
int listLen = 0;
private void addHead(int no, int key) {
// 新建头结点,此时 head.next 为空
head = new LNode(no, key, null);
// 只有一个结点,头结点也是尾结点
if (tail == null)
tail = head;
listLen++;
}
private void addTail(int no, int key) {
// 新建结点作为尾结点的后继结点
tail.next = new LNode(no, key);
// 将新建结点作为新的尾结点
tail = tail.next;
// 使新尾结点的 next 指向 head,使链表成环
tail.next = head;
listLen++;
}
/**
* @description : 求解复杂的约瑟夫环
* @params : [cListHead:第一个开始游戏的人, key:第一个出列数字]
* @return :
* @time : 2020/2/7 22:43
*/
private static void josephRing(LNode cListHead, int key) {
// 先使 tempHead 指向序号为 1 的游戏者
LNode tempHead = cListHead, tempTail = cListHead;
// 使 tempTail 指向序号最大的游戏者
while (tempTail.next != tempHead)
tempTail = tempTail.next;
System.out.println("游戏者出列顺序为:");
// 若剩余人数 > 1,则一直循环
while (tempHead.next != tempHead) {
// 搜寻该出列的游戏者,并用 tempHead 指向该游戏者
for (int i = 0; i < key - 1; i++) {
// 将 tempTail 作为 tempHead 的前驱节点
tempTail = tempHead;
tempHead = tempHead.next;
}
tempTail.next = tempHead.next;
System.out.println("出列序号:" + tempHead.no + " ,下一个出列数字:" + tempHead.key);
key = tempHead.key; // 更新出列数字
tempHead = null; // 从链表中删除出列的游戏者
tempHead = tempTail.next; // 从出列者下一位重新开始游戏
}
System.out.println("最后一个人的序号:" + tempHead.no + " ,出列数字:" + tempHead.key);
}
public static void main(String[] args) {
System.out.println("约瑟夫环问题求解:");
System.out.print("请输入游戏人数:");
Scanner scanner = new Scanner(System.in);
int number = scanner.nextInt();
System.out.print("请按顺序输入每个人手中的出列数字:");
// 初始化第一个游戏者的信息
CircularLinkedList circularLinkedList = new CircularLinkedList();
int key = scanner.nextInt();
circularLinkedList.addHead(1, key);
// 初始化剩余游戏者的信息
for (int i = 2; i <= number; i++) {
key = scanner.nextInt();
circularLinkedList.addTail(i, key);
}
System.out.print("请输入第一次出列的数字:");
key = scanner.nextInt();
josephRing(head, key);
}
}
约瑟夫环问题求解:
请输入游戏人数:8
请按顺序输入每个人手中的出列数字:3 5 6 2 1 5 7 2
请输入第一次出列的数字:4
游戏者出列顺序为:
出列序号:4 ,下一个出列数字:2
出列序号:6 ,下一个出列数字:5
出列序号:3 ,下一个出列数字:6
出列序号:5 ,下一个出列数字:1
出列序号:7 ,下一个出列数字:7
出列序号:8 ,下一个出列数字:2
出列序号:2 ,下一个出列数字:5
最后一个人的序号:1 ,出列数字:3
Process finished with exit code 0