算法-27环形链表的约瑟夫问题

描述

据说著名犹太历史学家 Josephus 有过以下故事:在罗马人占领乔塔帕特后,39 个犹太人与 Josephus 及他的朋友躲到一个洞中,39 个犹太人决定宁愿死也不要被敌人抓到,于是决定了一种自杀方式,41 个人排成一个圆圈,由第 1 个人开始报数,报数到 3 的人就自杀,然后再由下一个人重新报 1,报数到 3 的人再自杀,这样依次下去,直到剩下最后一个人时,那个人可以自由选择自己的命运。这就是著名的约瑟夫问题。现在请用单向环形链表得出最终存活的人的编号。
 

输入描述:

一行两个整数 n 和 m, n 表示环形链表的长度, m 表示每次报数到 m 就自杀。

输出描述:

输出最后存活下来的人编号(编号从1开始到n)

示例1

输入:
5 2

输出:
3

备注:
1≤n,m≤10000

思路

普通解法
普通的解法就像题目描述的过程一样,具体实现请参看如下代码中的 josephusKill1 方法。

  1.如果链表为空或者链表节点数为 1,或者 m 的值小于 1,则不用调整就直接返回。
  2.在环形链表中遍历每个节点,不断转圈,不断让每个节点报数。
  3.当报数到达 m 时,就删除当前报数的节点。
  4.删除节点后,别忘了还要把剩下的节点继续连成环状,继续转圈报数,继续删除。
  5.不停地删除,直到环形链表中只剩一个节点,过程结束。

import java.util.Scanner;

// 注意类名必须为 Main, 不要有任何 package xxx 信息
public class Main {

public static class Node{
    public int value;
    public Node next;

    public Node(int value) {
        this.value = value;
    }
}

public static Node  josephusKill(Node head,int m) {
    if(head ==null||head.next == head || m<1) {
        return head;
    }
    Node last = head;
    while(last.next != head) {
        last = last.next;
    }
    int count = 0;
    while(head != last) {
        count++;
        if(count == m) {
            last.next = head.next;
            count = 0;
        }else{
            last = last.next;
        }
        head = last.next;
    }
    return head;
}

    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        // 注意 hasNext 和 hasNextLine 的区别
        int n = scanner.nextInt();
        int m = scanner.nextInt();
        Node head = new Node(1);
        Node cur = head;
        for(int i=2;i<=n;i++) {
            cur.next = new Node(i);
            cur = cur.next;
        }
        cur.next = head;
        head = josephusKill(head,m);
        System.out.println(head.value);
    }
}

进阶解法

原问题之所以花费的时间多,是因为我们一开始不知道到底哪一个节点最后会活下来。所以依靠不断地删除来淘汰节点,当只剩下一个节点的时候,才知道是这个节点。如果不通过一直删除方式,有没有办法直接确定最后活下来的节点是哪一个呢?这就是进阶解法的实质。

这道题的一个复杂之处在于,每次删除了当前数环中的一个元素以后,剩下元素的序号和它的值是不对应的。要想知道最后留下了哪个元素,就需要根据它在数环的相对序号来倒推原始的位置。

我们已知删至只剩一个元素时,该元素的序号为1;假设当数环长度为i时,待删元素的序号为Num(i),Num(1) = 1。我们现在需要明确的就是Num(i)和Num(i-1)的关系。

首先考虑一下,当数环中有i个元素时,报数A的节点编号为B,A与B的关系:B = (A - 1)%i + 1

接下来考虑从长为i的环变为长为i - 1的环时,环中元素的编号变化。假设长为i的环中,被删除元素的编号为d,其余元素在删除元素之前的编号为old,在删除元素以后的编号为new,则new 与 old的关系是:old = (new + s - 1) % i + 1。

由于每次删除的元素报数为m,即A = m, 故B = (m - 1)%i+1,也即 s = (m-1)%i+1;带入后一个等式可知, old = (new + (m-1)%i + 1 - 1)%i+1 = (new + m - 1) % i + 1。

考虑最后剩下的那个节点,它的序号递推公式为:Num(i) = (Num(i - 1) + m - 1) % i + 1,Num(1) = 1。考虑将该公式递推N次,可得原始数组中该元素的序号。

import java.util.Scanner;

// 注意类名必须为 Main, 不要有任何 package xxx 信息
public class Main {

public static class Node{
    public int value;
    public Node next;

    public Node(int value) {
        this.value = value;
    }
}

public static Node  josephusKill2(Node head,int m) {
    if(head == null || head.next == head || m<1) {
        return head;
    }
    Node cur = head.next;
    int tmp = 1;
    while(cur != head) {
        tmp++;
        cur = cur.next;
    }
    tmp = getLive(tmp,m);
    while(--tmp != 0) {
        head = head.next;
    }
    head.next = head;
    return head;
}

public static int getLive(int i,int m) {
    if(i==1) {
        return 1;
    }
    return (getLive(i-1,m)+m-1)%i+1;
}

    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        // 注意 hasNext 和 hasNextLine 的区别
        int n = scanner.nextInt();
        int m = scanner.nextInt();
        Node head = new Node(1);
        Node cur = head;
        for(int i=2;i<=n;i++) {
            cur.next = new Node(i);
            cur = cur.next;
        }
        cur.next = head;
        head = josephusKill2(head,m);
        System.out.println(head.value);
    }
}

 

posted @ 2022-11-08 17:13  思凡念真  阅读(32)  评论(0编辑  收藏  举报