データ構造とアルゴリズムのジョセフ問題を再考する


ジョセフの質問

序文

ジョセフ問題はコンピューター科学と数学の問題であり、コンピューター プログラミングのアルゴリズムでは、同様の問題はジョセフ リングとも呼ばれ、「ハンカチ投げ問題」としても知られています。

有名なユダヤ人歴史家ヨセフスには次のような話があると言われています。

ローマ人がチョタパットを占領した後、39 人のユダヤ人がヨセフスとその友人たちと一緒に洞窟に隠れました。39 人のユダヤ人は、敵に捕まるくらいなら死んだほうがましだと決心し、自殺を決意しました。41 人が輪の中に並び、数えました。一人目から始まり、三人目が数えるたびにその人は自殺しなければならず、全員が自殺するまで次の人がまた数えます。

しかし、ヨセフスたちはこのルールに従うことを望まず、友人に先に従うふりをしてもらい、友人と自分を16位と31位に配置し、デスゲームを脱出した。

1. 暴力の法則

プロセス全体は、ブルート フォース手法を使用してシミュレートできます。

  • まず循環リンク リストをカスタマイズします。終了ノードの次のポインタが最初のノードを指すようにする必要があります。
  • n(= 41) 個の 0 ~ 40 の個人番号がリンク リストに追加されました
  • ノードを削除するには、プレ ノード pre と現在のノード cur を削除する必要があるため、削除する場合は、pre の次を cur の次にポイントして cur ノードを削除するだけで済みます。
  • カウンタ変数 count を定義 m(=3)に到達したらノードを削除しカウンタを0にリセット
  • 最終的に生き残るのは2人なので、ループ条件は cur.next != pre となります。1 人が生き残った場合、条件は cur.next != cur になります。
  • したがって、最終的な生存番号は 15 と 30 になります。この番号は 0 から始まるため、ヨセフスは自分と友人を 16 番目と 31 番目の位置に配置し、このデスゲームを脱出しました。
class Node {
    
    
    int data;
    Node next;
}

class CycleLinkedList  {
    
    
    Node first;
    Node last;

    public void add(int o) {
    
    
        Node l = last;
        Node n = new Node();
        n.data = o;
        last = n;
        if (l == null) {
    
    
            first = n;
        } else {
    
    
            l.next = n;
        }
        last.next = first;
    }
}

public void josephus() {
    
    

    int n = 41, m = 3;
    CycleLinkedList list = new CycleLinkedList();
    for (int i = 0; i < n; i++) {
    
    
        list.add(i);
    }

    int count = 0;
    Node pre = list.first;
    Node cur = pre;
    while (cur.next != pre) {
    
    
        count++;

        if (count == m) {
    
    
            pre.next = cur.next;
            System.out.println(" killer 编号:" + cur.data);
            count = 0;
        } else {
    
    
            pre = cur;
        }
        cur = cur.next;
    }
    System.out.println("最终存活2人编号:" + cur.data + "," + pre.data);
    // 最终存活2人编号:15,30
}

2. 動的プログラミング

上記の強引な方法は理解しやすく、実装も簡単です。ただし、走査を繰り返すと効率が低下します。要素が削除されるたびに、m ステップ移動する必要があります。n 要素の場合、時間計算量はO ( mn ) O(mn)です。()

実際、上記の問題は実際に動的計画法の解決カテゴリーを満たすことがわかります。各ステップで得られた数字はサブ質問であり、前のステップで得られた数字は次のステップで役立ちます。dp の 3 つの重要な部分は次のとおりです。

  • 状態定義: dp[i] はジョセフ問題の解を表します。つまり、m ごとに i 個の要素が削除され、最終的に要素の番号が残ります。

  • 伝達方程式の導出:

    • 0 , 1 , ⋯   , i − 2 ⏟ i-1个 \underbrace{0,1,\cdots, i-2}_{\text{i-1个}} i- 1 0 1 2最終要素番号を取得dp [ i − 1 ] dp[i-1]d p [ i1 ]

    • 次に、 0 , 1 , ⋯ , k − 1 , k , k + 1 , ⋯ , i − 1 ⏟ i \underbrace{0,1,\cdots,k-1,k,k+1,\cdots, i- 1 }_{\text{i 個}}私は_ 0 1 k1 k k+1 1最初の( m − 1 ) を削除します % i = k (m - 1)\%i = k( m1 ) % i=kの後には(m が i より大きい場合、剰余を取る必要があります)、k + 1 、 k + 2 、 ⋯ 、 i − 2 、 i − 1 、 0 、 1 、 ⋯ 、 k − 1 ⏟ i- があります。 1 \ アンダーブレース{k+1,k+2,\cdots,i-2,i-1,0,1,\cdots,k-1}_{\text{i-1}}i- 1 k+1 k+2 2 1 0 1 k1、要素の数もi − 1 i-1になります。1、要素の順序は増加し循環しているため、最大値への到達は最小値から始まり、上記の0 00からi − 2 i-2まで2桁のシーケンス。

    • dp [ i − 1 ] dp[i-1]を取得します。d p [ i1 ]数列は 0 から始まり、dp [ i ] dp[i]d p [ i ]了,だからdp [ i ] = ( k + 1 + dp [ i − 1 ] ) % i dp[i] = ( k + 1 + dp[i - 1] ) \% id p [ i ]=( k+1+d p [ i1 ]) % idp [ i ] = ( ( m − 1 ) % i + 1 + dp [ i − 1 ] ) % i dp[i] = ((m - 1)\%i + 1 + dp[ i - 1] ) \%id p [ i ]=(( m1 ) % i+1+d p [ i1 ]) % i

    • したがって、最終的な導出方程式は次のようになります。dp [ i ] = ( dp [ i − 1 ] + m ) % i dp[i] = (dp[i - 1] + m) \% id p [ i ]=( d p [ i1 ]+m ) % i

  • 初期状態: dp[1]=0 は、1 つの要素が最終的に要素番号 0 のままになることを意味します。

public void josephus1() {
    
    

    int n = 41, m = 3;
    int [] dp = new int[n + 1];
    dp[1] = 0;
    for (int i = 2; i < n + 1; i++) {
    
    
        dp[i] = (dp[i - 1] + m) % i;
    }
    System.out.println(dp[n]);
}

// 上述dp数组可用变量替代
public void josephus2() {
    
    

    int n = 41, m = 3;
    int start = 0;
    for (int i = 2; i < n + 1; i++) {
    
    
        start = (start + m) % i;
    }
    System.out.println(start);
}

上記の動的計画法を使用して最後の生存者の数を見つけることができ、時間計算量はO ( n ) O(n)です。O ( n )さん、上記のコードを使用する場合の問題は、最後から 2 番目の人の番号を取得できないことです。dp[n-1] は 40 人のうち最後に生き残った人の番号です。実際、dp の考え方を使用して、最後から 2 番目の人の番号を見つけることもできます。

  • 状態定義: dp[i] は、m ごとに i 個の要素が削除され、最後から 2 番目の要素の番号が残されることを意味します。
  • 状態遷移方程式:同上、dp [ i ] = ( dp [ i − 1 ] + m ) % i dp[i] = (dp[i - 1] + m) \% id p [ i ]=( d p [ i1 ]+m ) % i
  • 初期状態: dp[2] = (m + 1) % 2
int n = 41, m = 3;
int start = (m + 1) % 2;
for (int i = 3; i < n + 1; i++) {
    
    
    start = (start + m) % i;
}
System.out.println(start);

3、実戦

3.1 Leek1823. ゲームの勝者を見つける

https://leetcode.cn/problems/find-the-winner-of-the-circular-game/

合計 n 人の友達が一緒にゲームをプレイします。友達は円を形成し、時計回りに 1 から n まで番号が付けられます。正確には、i 番目のパートナーから時計回りに 1 ビット移動すると、(i+1) 番目のパートナーの位置に到達します (1 <= i < n)。n 番目のパートナーから時計回りに 1 ビット移動すると、次の結果が返されます。最初の小さなパートナーの位置に移動します。

ゲームは次のルールに従います。

从第 1 名小伙伴所在位置 开始 。
沿着顺时针方向数 k 名小伙伴,计数时需要 包含 起始时的那位小伙伴。逐个绕圈进行计数,一些小伙伴可能会被数过不止一次。
你数到的最后一名小伙伴需要离开圈子,并视作输掉游戏。
如果圈子中仍然有不止一名小伙伴,从刚刚输掉的小伙伴的 顺时针下一位 小伙伴 开始,回到步骤 2 继续执行。
否则,圈子中最后一名小伙伴赢得游戏。

ゲームに参加している友達の総数 n と整数 k を与えて、ゲームの勝者を返します。

古典的なジョセフ・リングの問題を言い換えたもの

public int findTheWinner(int n, int k) {
    
    
	int start = 0;
    for (int i = 2; i < n + 1; i++) {
    
    
        start = (start + k) % i;
    }
    return start + 1;
}

3.2 羅谷 P1996 ジョセフ問題

https://www.luogu.com.cn/problem/P1996

N人で円を作り、最初の人から数え始め、mまで数えた人が外へ、次の人がまた1から数え、mまで数えた人がまた輪の外へ、というように繰り返します。 、全員まで サークル外の人全員、順番にサークル外の人の番号を出力してください。

ここでは、サークル内の人の数を1人ずつ調べる必要があり、暴力の方法が使用される可能性があります。

import java.io.*;
import java.util.*;
public class Main {
    
    
    static class Node {
    
    
        int data;
        Node next;
    }

    static class CycleLinkedList  {
    
    
        Node first;
        Node last;

        public void add(int o) {
    
    
            Node l = last;
            Node n = new Node();
            n.data = o;
            last = n;
            if (l == null) {
    
    
                first = n;
            } else {
    
    
                l.next = n;
            }
            last.next = first;
        }
    }
    public static void main(String args[]) throws Exception {
    
    
        Scanner cin=new Scanner(System.in);
        int n = cin.nextInt(), m = cin.nextInt();
        int [] ans = new int[n];
        int c = 0;

        CycleLinkedList list = new CycleLinkedList();
        for (int i = 0; i < n; i++) {
    
    
            list.add(i + 1);
        }

        int count = 0;
        Node pre = list.first;
        Node cur = pre;
        while (cur.next != cur) {
    
    
            count++;

            if (count == m) {
    
    
                pre.next = cur.next;
                ans[c++] = cur.data;
                count = 0;
            } else {
    
    
                pre = cur;
            }
            cur = cur.next;
        }
        ans[c] = cur.data;
        for (int an : ans) {
    
    
            System.out.print(an + " ");
        }
    }
}

参考

  1. ジョセフ・リングの 3 つの解決策
  2. これはおそらく、ジョセフの指輪の最も詳細な数学的導出です。
  3. ジョセフの問題解決者

おすすめ

転載: blog.csdn.net/qq_23091073/article/details/128795594