Java之约瑟夫环

  • 前言
  • 一、约瑟夫环
  • 1、故事
  • 2、例题
  • 3、题解
  • A、List模拟删除
  • B、动态规划+递归
  • C、动态规划+循环
  • 总结
  • 参考文献


前言

通过学习Java约瑟夫环问题,理解大问题拆解成小问题,然后小问题之间递推求解,这也是动态规划的核心思想。

一、约瑟夫环

1、故事

约瑟夫斯是1世纪的一名犹太历史学家。他在自己的日记中写道,他和他的40个战友被罗马军队包围在洞中。他们讨论是自杀还是被俘,最终决定自杀,并以抽签的方式决定谁杀掉谁。约瑟夫斯和另外一个人是最后两个留下的人。约瑟夫斯说服了那个人,他们将向罗马军队投降,不再自杀。约瑟夫斯把他的存活归因于运气或天意,他不知道是哪一个。 —— 【约瑟夫问题】维基百科

2、例题

0,1,···,n-1这n个数字排成一个圆圈,从数字0开始,每次从这个圆圈里删除第m个数字(删除后从下一个数字开始计数)。求出这个圆圈里剩下的最后一个数字。
例如,0、1、2、3、4这5个数字组成一个圆圈,从数字0开始每次删除第3个数字,则删除的前4个数字依次是2、0、4、1,因此最后剩下的数字是3。

示例 1:
输入: n = 5, m = 3
输出: 3
示例 2:
输入: n = 10, m = 17
输出: 2

3、题解

A、List模拟删除

List模拟删除,快速解题,热身解法。
思想)模拟该环的删除方式,先将所有数字存入list,然后再一个个的删除,共删n-1个,最后剩下结果。

public int lastRemaining(int n, int m) {
        //用List将所有元素存上,然后一个一个的删除
        List<Integer> list = new ArrayList<>();
        for (int i = 0; i < n; i++)
            list.add(i);
        int cur_point = -1;
        for (int i = 0; i < n - 1; i++) {
            cur_point += m;
            cur_point %= n - i;
            list.remove(cur_point);
            cur_point--;
        }
        return list.get(0);
    }

B、动态规划+递归

思想)将大问题拆解成小问题,将 n 序列删除 跟 n - 1 序列删除关联起来,最终关联到 2 序列、1 序列。
1)当为 1 序列时,只能返回第0个值。确定了 1 序列结果,就去推 2 序列结果。
2)如何关联?确定了 n - 1 序列的结果,如何确定 n 序列的结果?设 n - 1序列结果为第 x 位数,而 n - 1 序列是从 0 为起点,它的结果离起点走 x 步,即(0 + x) % n。n - 1序列又是由 n 序列删出一个得来,而此时的起点应该是 m % n的地方,即删除的地方。所以 n 序列的结果应该是 离起点 m % n 走 x 步,即(m % n + x) % n == (m + x) % n。(这也是所谓的状态转移方程,递归关系)

public int lastRemaining2(int n, int m) {
        //递归解决,即将大问题分解为一个个的小问题,小问题之间有递归递推的性质,就像动态规划一样。
        return recursion(n, m);
    }

    public int recursion(int n, int m) {
        //如果只有一个数,那么剩下的是这个唯一的数,即第一个数。
        if (n == 1)
            return 0;
        //这里展现前一个和后一个如何递推
        //n-1的序列从第0个位置开始删,最终剩下的数所在位置设为第x位数。
        //n个数删除m%n之后,就是n-1个数,而再次开删的起点在m%n这里,通过递归得到n-1的结果是从0开始删的。
        //所以对于n个数,它的结果应该是以m%n这个起点走x步(m%n+x),就像n-1序列中,是从0开始走x步找到结果。
        int x = recursion(n - 1, m);
        //返回( m % n + x ) % m == ( m + x ) % n
        return (m + x) % n;
    }

C、动态规划+循环

递归代码简洁,但是要用到递归栈,开销额外的空间,而递归的本质是循环,可以用循环来完成。

public int lastRemaining3(int n, int m) {
        //上面是递归解决,递归的本质是循环,可以用循环来改写,从而减少空间开销。
        //最开始,只有一个数,所以只能返回第一个数,即下标第0个数。就是递归的出口,这里的初始化。
        int res = 0;
        //i表示有多少数,从有2个数到有n个数
        for (int i = 2; i <= n; i++) {
            res = (res + m) % i;
        }
        return res;
    }

总结

1)ArrayList模拟删除过程,快速题解,算法热身,Time:O(n),Space:O(n)
2)动态规划+递归,Time:O(n),Space:O(n)
3)动态规划+循环,Time:O(n),Sapce:O(1)