Joseph(约瑟夫环)问题

故事背景

据说著名犹太历史学家 Josephus有过以下的故事:在罗马人占领乔塔帕特后,39 个犹太人与Josephus及他的朋友躲到一个洞中,39个犹太人决定宁愿死也不要被敌人抓到,于是决定了一个自杀方式,41个人排成一个圆圈,由第1个人开始报数,每报数到第3人该人就必须自杀,然后再由下一个重新报数,直到所有人都自杀身亡为止。然而Josephus 和他的朋友并不想遵从。首先从一个人开始,越过k-2个人(因为第一个人已经被越过),并杀掉第k个人。接着,再越过k-1个人,并杀掉第k个人。这个过程沿着圆圈一直进行,直到最终只剩下一个人留下,这个人就可以继续活着。问题是,一开始要站在什么地方才能避免被处决?Josephus要他的朋友先假装遵从,他将朋友与自己安排在第16个与第31个位置,于是逃过了这场死亡游戏。

这里我们就算出最后一个存活的人即可。


方案1(一维数组)

    /**
     * 一维数组解法
     */
    public static void ysf3() {
        Scanner sc = new Scanner(System.in);
        System.out.println("请输入总人数");
        int personNum = sc.nextInt();           // 总人数
        System.out.println("请输入相隔几个人报数");
        int skip = sc.nextInt();                // 相隔几个人报数
        int[] josephArr = new int[personNum];
        int aliveNum = personNum;
        int index = -1;                         // 因为第一个报数的下标是0,因此这个index初始值是-1
        while (aliveNum > 1) {
            int tempSkip = skip;
            // 开始找下个要自杀的人
            while (true) {
                index = (index+1)%personNum;       // 下标+1
                if (josephArr[index] == 1) continue;    // 如果该下标的值为1,那么就说明这个位置的人已经死了,跳出这个小循环
                tempSkip --;
                if (tempSkip == 0) break;
            }
            // 找到这个下标之后 将他的值赋1
            josephArr[index] = 1;
            System.out.println("杀死的人是:"+index);
            aliveNum--;                     // 存活人数-1
            for (int i=0; i<personNum; i++) {
                System.out.print(josephArr[i]+" ");
            }
            System.out.println();
        }

        for (int i=0; i<personNum; i++)
            if (josephArr[i] == 0)
                System.out.println("活下来的人是:"+i);
    }

这个解法比较简单,我就不做多详细解释了。


方案2(递归)

单纯求出存活到最后的人的初始下标

这个相对比较简单。
先上代码:

    public static void main(String[] args) {
        int aliveIndex = ysf1(6, 3);
        System.out.println("下标为"+aliveIndex+"的人能活着!");
    }
    /**
     * 返回值含义: 人数为n的圈,下个(也就是第一个)要自杀的人在当前圈中的下标
     * @param n : 当前圈中一共多少人
     * @param k : 报数的次数
     * @return
     */
    public static int ysf1(int n, int k) {
        if (n == 1)
            return 0;
        else
            return (ysf1(n-1,k)+k) % n;
    }

理解:
ysf1(n, k) 的含义是,当前圈有n个人,以k为步长,最终最后一个留下的人在当前数组中的坐标
显然,当还有最后一个人的时候,那么最终这个人在当前圈的下标为0,即ysf1(1,k)=0

有n人,删掉第k人(下标为k-1)之后变成了n-1人,下次起始的地方是从这个出局的人后面开始算的,那么我们可以将这个新的起始位置的下标设为“0”组成一个新圈(认真思考,每个圈都是独立的
以 n为6, k为3,举例说明:

0	1	2	3	4	5				// 删之前 即“先状态”
3	4 	    0	1	2				// 删之后 即“后状态”

那么若要从n-1个人的“后状态”恢复到“先状态”,其实所有人的编号相当于加k,亦即f[n] = f[n-1] + k,再由于是环的问题,所以加完再用“先状态”时的人数n取模即可。若递归到了1个人的情况,赢家就是编号为0的人。

求出自杀的人的出圈顺序

这个相对于上面来说稍难一点,但本质其实是一样的!

    public static void main(String[] args) {
        for (int i=1; i<=5; i++) {
            System.out.println("第个"+i+"自杀的人,初始下标为"+ysf2(6,3,i));
        }
    }

    /**
     * 返回值含义: 人数为n的圈,第i个自杀的人在当前圈中的下标
     * @param n : 当前圈中共有n个人
     * @param k : 报数的次数
     * @param i : 想求出第几个人自杀
     * @return
     */
    public static int ysf2(int n, int k,int i) {
        if (i == 1)
            return (k-1+n)%n;
        else
            return (ysf2(n-1,k,i-1)+k)%n;
    }

理解:
ysf2(n, k, i)的含义是,人数为n的圈,以k为步长第i个出圈的人在当前圈的下标
以 n为10,k为3 为例

0	1	2	3	4	5	6	7	8	9
7	8		0	1	2	3	4	5	6	第一次出圈
4	5		6	7		0	1	2	3	第二次出圈
1	2		3	4		5	6		0	第三次出圈

由上面数据可得

ysf2(10, 3, 1) = 2
ysf2(10, 3, 2) = 5			=>  ysf2(9, 3, 1) = 2
ysf2(10, 3, 3) = 8			=>  ysf2(9, 3, 2) = 5		=>  ysf2(8, 3, 1) = 2

可以得出规律

ysf2(n, k ,i) = (ysf2(n-1, k, i-1)+k)%n,		i != 1
ysf2(n, k, i) = (k-1+n)%n,						i = 1

总结

约瑟夫递归解法最主要的就是找到新圈与旧圈下标之间的关系,然后构造出一个有意义的方法,然后找寻其中的规律。 ysf2(n,k,i)相对于ysf1(n,k)来说难理解一点,多思考几遍。
如果这片文章有帮助到你,点个赞呗!

参考文章:「约瑟夫问题 递归解法」

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

coderzpw

你的鼓励将是我创作的最大动力

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

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

打赏作者

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

抵扣说明:

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

余额充值