约瑟夫环问题起源
约瑟夫问题,或称“约瑟夫环”,又名“丢手绢问题”。
百度百科对于其起源有这样的介绍:
据说著名犹太历史学家Josephus有过以下的故事:在罗马人占领乔塔帕特后,39 个犹太人与Josephus及他的朋友躲到一个洞中,39个犹太人决定宁愿死也不要被敌人抓到,于是决定了一个自杀方式,41个人排成一个圆圈,由第1个人开始报数,每报数到第3人该人就必须自杀,然后再由下一个重新报数,直到所有人都自杀身亡为止。然而Josephus 和他的朋友并不想遵从。首先从一个人开始,越过k-2个人(因为第一个人已经被越过),并杀掉第k个人。接着,再越过k-1个人,并杀掉第k个人。这个过程沿着圆圈一直进行,直到最终只剩下一个人留下,这个人就可以继续活着。问题是,给定了和,一开始要站在什么地方才能避免被处决。Josephus要他的朋友先假装遵从,他将朋友与自己安排在第16个与第31个位置,于是逃过了这场死亡游戏。
而它的规则其实是这样的,一共有M个人围成一圈报数,当报到N或者N的倍数的人会被剔除掉,余下的人继续围成一圈报数,直到只剩最后一个人,这个人最开始的时候所在的位置即为幸运数字。
好了,历史来源就不聊了,规则相信大家也已经清楚了。接下来的代码演示从复杂到简单我一共写了3.5种思路,而每种又分别以python和java做了详细的注释。
代码演示
1、 普通解法
这个没什么可说的,思路逻辑就是从1开始报数,报一人就加1,遇到n及n的倍数就删掉当前的人。报完一轮以后索引达到列表(集合)最大长度则归0,即代表从头开始继续遍历,直到列表(集合)剩余人数不大于1,然后他的编号就是那个不死的幸运编号
python代码
# 确定总人数及退出编号
m = int(input("请输入总人数:\n"))
n = int(input("请输入退出编号:\n"))
print("总人数为:", m, ",退出编号为:", n)
# 列表推导式构建list列表存所有编号
numList = [i for i in range(1, m + 1)]
print("所有人编号为:", numList)
# 开始设置报数的起始值
count = 1
# 索引起始值
i = 0
# 开始报数,当集合长度不大于1的时候退出循环
while len(numList) > 1:
# 如果索引值等于集合长度,则接着从0开始
if i == len(numList):
i = 0
# 如果报到n的倍数,则去掉此人,并且索引需减1以防漏掉刚刚前移位置的人
if count % n == 0:
numList.pop(i)
i -= 1
# 索引加1
i += 1
# 报数加1
count += 1
print("剩余最后一个人的编号:", numList)
java代码
public class Demo0 {
public static void main(String[] args) {
//确定总人数及退出编号
Scanner scanner = new Scanner(System.in);
System.out.println("请输入总人数:");
int m = scanner.nextInt();
System.out.println("请输入退出编号:");
int n = scanner.nextInt();
System.out.println("总人数为:" + m + ",退出编号为:" + n);
//构建list集合存所有编号
List<Integer> numList = new ArrayList<>();
for (int i = 0; i < m; i++) {
numList.add(i + 1);
}
System.out.println("所有人编号为:" + numList);
//开始设置报数的起始值
int count = 1;
//索引起始值
int i = 0;
//开始报数,当集合长度不大于1的时候退出循环
while (numList.size() > 1) {
//如果索引值等于集合长度,则接着从0开始
if (i == numList.size()) {
i = 0;
}
//如果报到n的倍数,则去掉此人,并且索引需减1以防漏掉刚刚前移位置的人
if (count % n == 0) {
numList.remove(i);
i -= 1;
}
//索引加1
i += 1;
//报数加1
count += 1;
}
System.out.println("剩余最后一个人的编号:" + numList);
}
}
2、 队列解法
队列解法的思路就是模拟队列,先进先出,一次一次的把队首的第一个人拿出来放到队尾变成最后一个,这样拿n-1次之后第n次直接删除掉队首元素,循环反复,直到队列只剩唯一一个元素就停止
python代码
# 确定总人数及退出编号
m = int(input("请输入总人数:\n"))
n = int(input("请输入退出编号:\n"))
print("总人数为:", m, ",退出编号为:", n)
# 列表推导式构建list列表存所有编号
numList = [i for i in range(1, m + 1)]
print("所有人编号为:", numList)
# 获取集合长度
length = len(numList)
# 开始报数,当集合长度不大于1的时候退出循环
while length > 1:
for i in range(n - 1):
# 将前n-1个循环删除并依次添加到集合末尾
pop = numList.pop(0)
numList.append(pop)
# 将第n个直接删除
numList.pop(0)
# 重新获取集合长度
length = len(numList)
print("剩余最后一个人的编号:", numList)
java代码
public class Demo1 {
public static void main(String[] args) {
//确定总人数及退出编号
Scanner scanner = new Scanner(System.in);
System.out.println("请输入总人数:");
int m = scanner.nextInt();
System.out.println("请输入退出编号:");
int n = scanner.nextInt();
System.out.println("总人数为:" + m + ",退出编号为:" + n);
//构建list集合存所有编号
List<Integer> numList = new ArrayList<>();
for (int i = 0; i < m; i++) {
numList.add(i + 1);
}
System.out.println("所有人编号为:" + numList);
//获取集合长度
int length = numList.size();
//开始报数,当集合长度不大于1的时候退出循环
while (length > 1) {
//将前n-1个循环删除并依次添加到集合末尾
for (int i = 0; i < n - 1; i++) {
Integer remove = numList.remove(0);
numList.add(remove);
}
//将第n个直接删除
numList.remove(0);
//重新获取集合长度
length = numList.size();
}
System.out.println("剩余最后一个人的编号:" + numList);
}
}
3、 规律解法
这种解法的关键在于我们要找到这个问题内在的规律,我们假设有m个人,从1开始报数,逢n就去掉一个,那么当幸存者想要在m人中存活下来的时候,首先需要在m-1人的队列中幸存下来,而m人的队列如何成为m-1人的队列呢?
那么就是在去掉第一个人的时候,即遇到第一个n并去掉,此时队列转换为m-1人的队列。
由于此队列也是一个相对独立的队列,所以此时索引0需重新计算,变为上一轮次(即m人)时的n+1,即索引为n的那个人。
(这里为什么不能还用原来(即m人)的索引0呢,因为相对于新队列(m-1)来说,旧队列的索引由于删人的原因已经断掉了,所以不能再沿用上一轮的索引。)
索引重新计算以后我们的幸存者索引有什么变化呢(先假设他不在前n位)?对,就是减了n,因为他前面少了n个人。
此时我们继续向下,其想在m-1人中存活就必须在m-2人中存活,而m-1变为m-2就意味着又去掉了一个人,即队列前方又没了n个人(其中有n-1个人去了队列末尾),所以m-2人的队列中的索引0变为了m-1人的队列时的索引n。此时索引重新计算后我们的幸存者索引又少了n(假设他又不在前n位)。
依次列推,当所剩人数为1时,幸存者想要存活所在的索引依然需要是曾经人数为2时的索引减去n,但是很惊喜的是,我们到1个人的时候就不会去掉人了,那唯一的一个人就是独一的幸存者,这一路上他所在的每一个索引就是那唯一一个希望的索引。
所以我们可以从队列只有一个人时存活的索引开始,将每一轮次时这个幸运儿的当时索引推出来,即从0开始加n。
不过从一个人升到两个人的时候大家能发现,只要n大于1那索引就越界了。因为我们的n只要超过2那么在队列只有两个人的时候那他们就要报不止一圈的数,但他们内部究竟报了几圈我们根本不关心,我们只需要最后一圈的结果就可以,所以我们要模上个2,这样就取到了最后一圈的结果。
上面的这种情况在每一轮的时候都有可能发生,所以我们在计算每一轮索引的时候以防索引出现这种越界的情况都要模上当前人数。
综上所述,我们假定n为3,由一个人时幸存者索引为0可以知道两个人时幸存者索引为(0+n)%2=(0+3)%2=1,三个人时为(1+n)%3=(1+3)%3=1,四个人时为(1+n)%4=(1+3)%4=0,五个人时为(0+n)%5=(0+3)%5=3……
由此我们可以得出结论,m人时的幸存者索引即为m-1人时幸存者的索引加上n再模上m,可以表示为f(n)=( f(n-1)+n )%m
python代码
# 确定总人数及退出编号
m = int(input("请输入总人数:\n"))
n = int(input("请输入退出编号:\n"))
print("总人数为:", m, ",退出编号为:", n)
# 从索引0开始计算
j = 0
# 如果只有一个人那么就不用计算了,所以索引i从2开始,依次循环计算当前i人数时的存活索引
for i in range(2, m + 1):
# 当前i人数时的存活索引即为i-1时的存活索引加上退出编号,
# 为防索引越界需要再模上当前人数i
j = (j + n) % i
# 直到计算到m人的存活索引时停止循环,由于当前j为索引所以编号需要加1
print("剩余最后一个人的编号:", j + 1)
java代码
public class Demo2 {
public static void main(String[] args) {
//确定总人数及退出编号
Scanner scanner = new Scanner(System.in);
System.out.println("请输入总人数:");
int m = scanner.nextInt();
System.out.println("请输入退出编号:");
int n = scanner.nextInt();
System.out.println("总人数为:" + m + ",退出编号为:" + n);
//从索引0开始计算
int j = 0;
//如果只有一个人那么就不用计算了,所以索引i从2开始,依次循环计算当前i人数时的存活索引
for (int i = 2; i < m + 1; i++) {
//当前i人数时的存活索引即为i-1时的存活索引加上退出编号,
// 为防索引越界需要再模上当前人数i
j = (j + n) % i;
}
//直到计算到m人的存活索引时停止循环,由于当前j为索引所以编号需要加1
System.out.println("剩余最后一个人的编号:" + (j + 1));
}
}
3.5、 递归解法
此种解法思路参考第三种规律解法中的详细说明,其实就是上面所写规律的递归版本,所以姑且称之为第3.5种思路
python代码
# 定义递归函数,传入总人数m,及退出编号n
def josephRing(m, n):
# 如果只剩一人时直接返回索引0
if m == 1:
return 0
# 否则要获取当前m人时的存活索引则需先获取m-1人时的存活索引,
# 并且将其结果加上退出编号n,以防索引越界还需模上当前人数m
return (josephRing(m - 1, n) + n) % m
if __name__ == '__main__':
# 确定总人数及退出编号
m = int(input("请输入总人数:\n"))
n = int(input("请输入退出编号:\n"))
print("总人数为:", m, ",退出编号为:", n)
# 调用递归函数
index = josephRing(m, n)
# 获取存活索引加1即得最后一人编号
print("剩余最后一个人的编号:", index + 1)
java代码
public class Demo3 {
public static void main(String[] args) {
//确定总人数及退出编号
Scanner scanner = new Scanner(System.in);
System.out.println("请输入总人数:");
int m = scanner.nextInt();
System.out.println("请输入退出编号:");
int n = scanner.nextInt();
System.out.println("总人数为:" + m + ",退出编号为:" + n);
//调用递归函数
int index = josephRing(m, n);
//获取存活索引加1即得最后一人编号
System.out.println("剩余最后一个人的编号:" + (index + 1));
}
//定义递归函数,传入总人数m,及退出编号n
private static int josephRing(int m, int n) {
//如果只剩一人时直接返回索引0
if (m == 1) {
return 0;
}
//否则要获取当前m人时的存活索引则需先获取m-1人时的存活索引,
// 并且将其结果加上退出编号n,以防索引越界还需模上当前人数m
return (josephRing(m - 1, n) + n) % m;
}
}