约瑟夫问题
据说著名犹太历史学家Josephus有过以下的故事:在罗马人占领乔塔帕特后,39 个犹太人与Josephus及他的朋友躲到一个洞中,39个犹太人决定宁愿死也不要被敌人抓到,于是决定了一个自杀方式,41个人排成一个圆圈,由第1个人开始报数,每报数到第3人该人就必须自杀,然后再由下一个重新报数,直到所有人都自杀身亡为止。然而Josephus 和他的朋友并不想遵从。首先从一个人开始,越过k-2个人(因为第一个人已经被越过),并杀掉第k个人。接着,再越过k-1个人,并杀掉第k个人。这个过程沿着圆圈一直进行,直到最终只剩下一个人留下,这个人就可以继续活着。问题是,给定了和,一开始要站在什么地方才能避免被处决。Josephus要他的朋友先假装遵从,他将朋友与自己安排在第16个与第31个位置,于是逃过了这场死亡游戏。
另一个场景,17世纪的法国数学家加斯帕在《数目的游戏问题》中讲了这样一个故事:15个教徒和15 个非教徒在深海上遇险,必须将一半的人投入海中,其余的人才能幸免于难,于是想了一个办法:30个人围成一圆圈,从第一个人开始依次报数,每数到第九个人就将他扔入大海,如此循环进行直到仅余15个人为止。问怎样排法,才能使每次投入大海的都是非教徒。
约瑟夫问题是个有名的问题:N个人围成一圈,从第一个开始报数,第M个将被杀掉,最后剩下一个,其余人都将被杀掉。例如N=6,M=5,被杀掉的顺序是:5,4,6,2,3。1
解题思路
1. 使用单向环形链表模拟游戏过程
1.1 单向环形链表
单向链表总是有一个指针指向下一个元素.
单向环形链表最后一个元素循环指向第一个元素,形成闭环。
1.2 定义一个单向环形链表模拟约瑟夫问题
/**
* 约瑟夫问题与单向环形列表
*/
public class JosepfuQuestion {
private Node first;
private Node last;
public JosepfuQuestion() {
}
public JosepfuQuestion(Integer amount) {
addAll(amount);
}
/**
* 初始化一个环形链表
* @param amount 链表长度
* @return
*/
public boolean addAll(Integer amount) {
if (first != null) {
System.err.println("链表已经初始化过了!");
}
if (amount == null || amount < 1) {
System.err.println("错误的初始化参数");
return false;
}
for (int i = 0; i < amount; i++) {
Node node = new Node(i + 1);
if (first == null) {
last = first = node;
first.setNext(last);
}else{
last.setNext(node);
last = node;
last.setNext(first);
}
}
return true;
}
/**
* 展示链表中所有数据
*/
public void display() {
if (first == null) {
System.err.println("链表为空");
return;
}
Node tmp = first;
while (tmp.hasNext()) {
System.out.println("当前对象编号:" + tmp.getNo());
tmp = tmp.getNext();
if (first == tmp) {
break;
}
}
}
/**
* 游戏开始 默认从第一个人开始报数
* @param outIndex 第几人出局
* @param amount 一共有几人
* @return
*/
public String doPlay(int outIndex, int amount) {
return doPlay(1, outIndex, amount);
}
public String doPlay(int startIndex, int outIndex, int amount) {
// 初始化链表
this.addAll(amount);
// 1. 找到开始的元素
Node startNode = first;
for (int i = 1; i < startIndex; i++) {
startNode = startNode.getNext();
}
// 2. 找到当前元素的前一个元素, 初始时即循环至末尾
Node preNode = startNode;
while (true) {
if (preNode.getNext() == startNode) {
// 如果前一个节点的下一个节点就是开始节点, 则已经找到开始元素的前一个元素
break;
}
preNode = preNode.getNext();
}
int times = outIndex - 1;
while (true) {
// 3. 将开始元素和其前一个元素同时向后移动 times-1 次
for (int i = 0; i < times; i++) {
startNode = startNode.getNext();
preNode = preNode.getNext();
}
if (startNode == preNode) {
// 向后移动后发现前一个元素和删除元素重合则表示幸存者出现
System.out.println("幸存者产生, 编号为: " + startNode.getNo());
break;
}
// 此时的startNode就是需要移除的元素
System.out.println("即将移除元素: " + startNode.getNo());
// 4. 移除,将前一个元素指向删除元素的下一个元素即可
preNode.setNext(startNode.getNext());
// 5. 令下一个元素为新的开始元素 继续循环
startNode = startNode.getNext();
}
return "幸存者是:" + startNode.getNo();
}
private static class Node {
private Integer no;
private Node next;
public Node() {
}
public Node(Integer no) {
this.no = no;
}
public Node(Integer no, Node next) {
this.no = no;
this.next = next;
}
public boolean hasNext() {
return this.next != null;
}
public Integer getNo() {
return no;
}
public void setNo(Integer no) {
this.no = no;
}
public Node getNext() {
return next;
}
public void setNext(Node next) {
this.next = next;
}
}
}
1.3 验证
// ....
@Test
public void test1() {
JosepfuQuestion josepfuQuestion = new JosepfuQuestion();
josepfuQuestion.doPlay(1, 3,41);
}
// 输出
/**
* 即将移除元素: 3
* 即将移除元素: 6
* 即将移除元素: 9
* 即将移除元素: 12
* 即将移除元素: 15
* 即将移除元素: 18
* 即将移除元素: 21
* 即将移除元素: 24
* 即将移除元素: 27
* 即将移除元素: 30
* 即将移除元素: 33
* 即将移除元素: 36
* 即将移除元素: 39
* 即将移除元素: 1
* 即将移除元素: 5
* 即将移除元素: 10
* 即将移除元素: 14
* 即将移除元素: 19
* 即将移除元素: 23
* 即将移除元素: 28
* 即将移除元素: 32
* 即将移除元素: 37
* 即将移除元素: 41
* 即将移除元素: 7
* 即将移除元素: 13
* 即将移除元素: 20
* 即将移除元素: 26
* 即将移除元素: 34
* 即将移除元素: 40
* 即将移除元素: 8
* 即将移除元素: 17
* 即将移除元素: 29
* 即将移除元素: 38
* 即将移除元素: 11
* 即将移除元素: 25
* 即将移除元素: 2
* 即将移除元素: 22
* 即将移除元素: 4
* 即将移除元素: 35
* 即将移除元素: 16
* 幸存者产生, 编号为: 31
*
* Process finished with exit code 0
*/