问题描述
已知N个人围成一个圆,从编号为k的人开始报数,数到m的人出列;从m+1个人开始,下一轮又从1开始报数,数到m的人又出列,以此类推。求:最后一个出列者的序号。
思路分析
1、明确变量:
该问题的变量理论上有4个:人数N、报数周期C、起始编号i,以及方向d(顺时针或逆时针);
2、明确数量关系:
2.1、i的取值范围只能是1~N之间;
2.2、d为开关变量,后期可用逻辑值true/false替代,表示是否按顺时针执行;
2.3、N和C之间无必然大小关系,但有一点很明确:得出最终结果时,从1到C的报数周期一共执行了(N-1)次。
3、问题分解:
原问题为:Y=F(N,C,i,d),为简化问题,可以分三步走:
3.1、确定y1=G(N,C, 1, true);
3.2、确定y2=G(N, C,i, true);(i∈[1, N])
3.3、确定y3=G(N,C, i, d);(i∈[1, N],d∈{true, false})
4、基本思路:
4.1、确定方案:
对于每轮报数后出列的人,无非两种处理方案:
(1)要么做好标记,下轮报数到C时直接跳过此人;
(2)要么不做标记,直接从原数组中清退,下轮报数从该位置开始。
本例先采用前者,按不清退分析,后面会给出清退方案的代码。
4.2、具体思路:
(1)生成boolean型数组arr[N],赋初值为true,表示全部可以参与报数。
(2)用while循环嵌套for循环。外层的while用于统计报数周期C的执行次数kill,递增超过(N-1)时跳出;内层for循环用于生成1~C的有效报数。每当arr中顺次找到一个值为真的元素,报数变量i递增+1,递增到C时对应的arr元素下标index,即为本轮报数出列者,此时出列的总人数,亦即kill,递增+1,然后开始下一次报数。最后,arr[N]中仅剩一个值为真的元素arr[k],(k+1)即为问题的解。
实现代码(两个参数):
1、y1=G(N, C, 1, true):
public static void main(String[] args) {
int members = 8;
int cycle = 5;
int initial = 3;
boolean forward = false;
int survivor1 = showJoseph(members, cycle);
System.out.printf("幸存者为: %d (人数=%d, 周期=%d, (默认起点=1、正向))\n",
survivor1, members, cycle);
}
private static int showJoseph(int total, int cycle) {
boolean[] arr = new boolean[total];
Arrays.fill(arr, true);
int kill = 0;
int index = 0;
int result = 0;
while (kill < total) {
for (int i = 0; i < cycle; i++) {
while (!arr[index]) {
index = (index + 1) % total;
}
if (i == cycle - 1) {
System.out.print((index + 1) + (kill < total - 1 ? " " : "\n"));
arr[index] = false;
kill++;
}
if(kill==total-1) result = (index+1);
index = (index+1) % total;
}
}
return result;
}
/*
运行结果:
5 2 8 7 1 4 6 3
幸存者为: 3 (人数=8, 周期=5, (默认起点=1、正向))
*/
实现代码(三个参数):
2、y2=G(N, C, i, true);(i∈[1, N]):
public static void main(String[] args) {
int members = 8;
int cycle = 5;
int initial = 3;
boolean forward = false;
int survivor2 = showJoseph(members, cycle, initial);
System.out.printf("幸存者为: %d (人数=%d, 周期=%d, 起点=%d, (默认正向))\n",
survivor2, members, cycle, initial);
}
private static int showJoseph(int total, int cycle, int start) {
boolean[] arr = new boolean[total];
Arrays.fill(arr, true);
int kill = 0;
int index = start - 1;
int result = 0;
while (kill < total) {
for (int i = 0; i < cycle; i++) {
while (!arr[index]) {
index = (index + 1) % total;
}
if (i == cycle - 1) {
System.out.print((index + 1) + (kill < total - 1 ? " " : "\n"));
arr[index] = false;
kill++;
}
if(kill==total-1) result = index+1;
index = (index+1) % total;
}
}
return result;
}
/*
运行结果:
7 4 2 1 3 6 8 5
幸存者为: 5 (人数=8, 周期=5, 起点=3, (默认正向))
*/
这里有两点值得注意:
1、起始位置满足一定的周期性:
H(k+mN)=H(k)(其中m为任意整数,N为周期,即总人数)
2、起点为1与起点为3的结果间有很强的相关性:
H(1)=3=H(1)+(1-1)
H(2)=4=H(1)+(2-1)
H(3)=5=H(1)+(3-1)
H(4)=6=H(1)+(4-1)
H(5)=7=H(1)+(5-1)
H(6)=8=H(1)+(6-1)
问题出在H(7),此时H(1)+(7-1)=9,而实际为1,因此需要改进为:
H(7)=[ H(1)+(7-1)]% 8 = 1
为保证与前面统一,再改进为:
H(7)=[ H(1)+(7-1)-1 ] % 8 +1 = 1
H(8)=[ H(1)+(8-1)-1 ] % 8 +1 = 2
H(1)=[ H(1)+(1-1)-1 ] % 8+1 = 3
H(2)=[ H(1)+(2-1)-1 ] % 8+1 = 4
H(3)=[ H(1)+(3-1)-1 ] % 8+1 = 5
H(4)=[ H(1)+(4-1)-1 ] % 8+1 = 6
H(5)=[ H(1)+(5-1)-1 ] % 8+1 = 7
H(6)=[ H(1)+(6-1)-1 ] % 8+1 = 8
由此可知,
H(x+k) = ( H(x) + k - 1 ) % N + 1(其中x∈[1, N],且k∈[1-x, N-x])
这样,三个变量时的代码可以进一步简化如下:
实现代码(三参数法另解):
y2*=G(N, C, i, true);(i∈[1, N]):
private static int showJoseph(int total, int cycle, int start) {
int result = (showJoseph(total, cycle) + (start-1) -1) % total + 1;
return result;
}
接下来处理包含4个参数的情况: y3= G(N, C, i, d);(i∈[1, N],d∈{true, false})
注意到,每次顺次考察arr中元素时,下标index都用index=(index+1)%N得到。现在变为反向,则只需令index=(index-1)%N即可。但是Java的模运算不会自动令余数为正,为避免(index-1)<0,这里需要附加一个周期N,变为:
index = (index-1+N) % N
或
index = (--index+N) % N
因此得到具备4个参数(自由度)的约瑟夫环处理方法,代码如下。
实现代码(四个参数):
y3=G(N, C, i, d);(i∈[1, N],d∈{true, false}):
public static void main(String[] args) {
int members = 8;
int cycle = 5;
int initial = 3;
boolean forward = false;
int survivor3 = showJoseph(members, cycle, initial, forward);
System.out.printf("幸存者为: %d (人数=%d, 周期=%d, 起点=%d, %s向)\n",
survivor3, members, cycle, initial, forward?"正":"反");
int survivor4 = showJoseph(members, cycle, initial, !forward);
System.out.printf("幸存者为: %d (人数=%d, 周期=%d, 起点=%d, %s向)\n",
survivor4, members, cycle, initial, !forward?"正":"反");
}
private static int showJoseph(int total, int cycle, int start, boolean forward) {
boolean[] arr = new boolean[total];
Arrays.fill(arr, true);
int kill = 0;
int index = start - 1;
int result = 0;
while (kill < total) {
for (int i = 0; i < cycle; i++) {
while (!arr[index]) {
if (forward) {
index = (++index + total) % total;
} else {
index = (--index + total) % total;
}
}
if (i == cycle - 1) {
System.out.print(index + 1 + (kill < total - 1 ? " " : "\n"));
arr[index] = false;
kill++;
}
if(kill==total-1) result = index+1;
if (forward) {
index = (++index + total) % total;
} else {
index = (--index + total) % total;
}
}
}
return result;
}
/*
运行结果:
7 2 4 5 3 8 6 1
幸存者为: 1 (人数=8, 周期=5, 起点=3, 反向)
7 4 2 1 3 6 8 5
幸存者为: 5 (人数=8, 周期=5, 起点=3, 正向)
*/
方案二:不标记,只清退
利用LinkedList链表可以不作标记,直接删除每轮报数为C的元素,代码如下:
package javastudy;
import java.util.LinkedList;
import java.util.List;
public class Demo2 {
public static void main(String[] args) {
senario2(100,14);
}
private static void senario2(int total, int cycle) {
List<Integer> all = new LinkedList<Integer>();
for(int i = 1;i <= total;i++){
all.add(i);
}
int i = 0;
for(int n = 1;n < total;n++){
i = (i + cycle -1) % all.size();
System.out.print(all.get(i)+(n==total-1?"\n":" "));
all.remove(i);
}
System.out.printf("幸存者为:%d号 (人数=%d, 周期=%d, (默认从第1个人开始、正向报数))\n",
all.get(0), total, cycle);
}
}
/*
运行结果:
14 28 42 56 70 84 98 12 27 43 58 73 88 3 19 35 51 67 83 100 17 34 52 69 87 5 23 41 61 79 97 18 38 59 78 99 21 44 64 86 8 31 54 77 2 26 50 76 4 30 57 85 11 40 71 96 32 63 93 25 62 94 33 68 7 46 82 22 66 10 53 1 48 95 49 9 65 20 81 45 15 89 60 37 24 13 6 16 36 55 80 39 91 90 29 75 47 74 72
幸存者为:92号 (人数=100, 周期=14, (默认从第1个人开始、正向报数))
*/
P.S.:感谢 东风化宇 在 flyne上 提供的思路与分享。(查看 原始代码 )
至于链表法的参数拓展,这里就不赘述了。
要点梳理
1、从1开始生成1~N的连续下标:
正向:index = ( ++index - 1 ) % N + 1 = index % N + 1
反向:index = ( --index +N - 1 ) % N + 1
2、初始化数组时,用Arrays.fill()赋相同的值:
Arrays.fill(arr, true);
3、方案一中,报数前必须先判定arr[index]是否可以参与报数,若不能参与,则必须顺次找到第一个可以报数的arr[index]。然后,报数周期的循环变量i才自动递增;
4、利用链表也可以实现该问题的求解,原数组长度顺次递减