约瑟夫环问题的Java版解法

问题描述

  已知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(NC,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(NC1, 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、正向))
*/


  显然,处理 y2=G( N C i true );(i∈[1, N])时,只需要将上述index的初始值扩展为指定值(start-1)即可,因为数组下标是从0开始的。


实现代码(三个参数):

2、y2=G(NCitrue);(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+15

 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(NCid);(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(NCid);(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、利用链表也可以实现该问题的求解,原数组长度顺次递减

  • 6
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

安冬的码畜日常

您的鼓励是我持续优质内容的动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值