約瑟夫環問題求解:約瑟夫告訴我,當年,他就是這麼在決賽圈躺贏吃雞的....

1. 約瑟夫環問題

傳說有這樣一個故事,在羅馬人佔領喬塔帕特後,39 個猶太人與約瑟夫及他的朋友躲到一個洞中,39個猶太人決定寧願死也不要被敵人抓到,因而決定了一個自殺方式,41我的排成一個圓圈,第一我的從1開始報數,依次日後,若是有人報數到3,那麼這我的就必須自殺,而後再由他的下一我的從新從1開始報數,直到全部人都自殺身亡爲止。然而約瑟夫和他的朋友並不想聽從。因而,約瑟夫要他的朋友先僞裝聽從,他將朋友與本身安排在第16個與第31個位置,從而逃過了這場死亡遊戲 。html

問題轉換:java

n我的坐一圈,第一我的編號爲1,第二我的編號爲2,第n我的編號爲n。node

  1. 編號爲1的人開始從1報數,依次向後,報數爲ans的那我的退出圈;數組

  2. 自退出那我的開始的下一我的再次從1開始報數,以此類推;數據結構

  3. 求出最後退出的那我的的編號學習

看來,約瑟夫是玩過雙排,知道決賽圈怎麼打,才能帶基友吃雞。一跳飛機別人都在惟惟諾諾,約瑟夫同志直接規劃好在決賽圈躺贏。ui

如今,我來告訴你當年約瑟夫是怎麼想的,怎麼用如下方法直接在決賽圈躺贏吃雞!

說來慚愧,做爲一個大三的老菜雞,數據結構我在大一學過了,可是當時以爲這玩意好複雜,感受沒啥用,就上課划水。可是,比較有印象的一節課就是老師講約瑟夫環問題,當時也沒怎麼搞懂,如今重頭來。若是有剛接觸開始學習數據結構的同窗,必定要好好學習呀。this

2. 分析一波

約瑟夫問題是個環,咱們把每一個人編號後放到這個環中。首先,這個環怎麼來實現呢?數據結構裏也沒有環呀?

咱們首先想到的應該就是循環鏈表,這是個環呀。那,鏈表能實現的,數據應該也能夠呀。spa

其實,約瑟夫環問題共有三種解法,分別是循環鏈表,數組,還有數學方法來解決。.net

循環鏈表

【循環鏈表解題思路】:

  1. 構建含有n個結點的單向循環鏈表,分別存儲1~n的值,分別表明這n我的;

  2. 使用計數器count,記錄當前報數的值;

  3. 遍歷鏈表,每循環一次,count++;

  4. 判斷count的值,若是是ans,則從鏈表中刪除這個結點並打印結點的值,把count重置爲0;


來具體分析一下鏈表解題思路是怎麼作的?

首先是來構建節點數爲n的循環鏈表

  • 建立節點類

  • 構建循環鏈表

其次是開始循環報數,刪除節點

刪除節點的細節性操做:

【完整的代碼】

package com.topic.joseph;

/** * @Author: Mr.Q * @Date: 2020-05-06 21:10 * @Description:循環鏈表 * @Solution: * 1.構建含有41個結點的單向循環鏈表,分別存儲1~41的值,分別表明這41我的; * 2.使用計數器count,記錄當前報數的值; * 3.遍歷鏈表,每循環一次,count++; * 4.判斷count的值,若是是3,則從鏈表中刪除這個結點並打印結點的值,把count重置爲0 */
public class CircularList {
    /** * 約瑟夫環 * @param n 圍成環人的編號(從1開始到n) * @param ans 數到ans的那我的出列 * @return 倖存人的編號 */
    public static int joseph(int n, int ans) {
        if ( ans < 2) {
            return n;
        }
        //建立循環鏈表
        Node first = buildCircularList(n);

        //count計數器,模擬報數
        int count = 0;

        //遍歷刪除節點,模擬自殺
        //記錄每次遍歷(報數)拿到的節點
        Node<Integer> temp = first;
        //記錄當前節點的上一個節點befo,爲的是在刪除(自殺)時,befo直接指向自殺節點的下一個節點,完成當前節點的刪除
        Node<Integer> befo = null; //默認的首節點無上一個節點

        //若是當前環只剩最後一個節點時,結束循環(防止自環)
        while (temp != temp.next) {
            //模擬報數
            count++;
            //判斷當前報數是否是ans
            if (count == ans) {
                //若是是ans,則把當前結點刪除調用,打印當前結點;
                //重置count=0,讓當前結點temp後移
                befo.next = temp.next; //befo直接指向自殺節點的下一個節點,完成當前節點的刪除
                System.out.print(temp.data + " ");
                count = 0;
                temp = temp.next;
            }else {
                //若是不是ans,讓befo變爲當前結點,讓當前結點後移
                befo = temp;
                temp = temp.next;
            }
        }
        return temp.data;
    }

    //節點類
    private static class Node<T> {
        //存儲數據
        T data;
        //指向下一個節點
        Node next;
        public Node(T data, Node next) {
            this.data = data;
            this.next = next;
        }
    }

    /** * 構建循環鏈表,分別存儲1~n的編號 * @param n n人編號 * @return */
    private static Node buildCircularList(int n) {
        //首節點構建
        Node<Integer> first = null;
        //記錄新建立的節點的前一個節點prev
        Node<Integer> prev = null;
        for (int i = 1; i <= n; i++) {
            //若是是第一個節點
            if (i == 1) {
                //首節點放入編號1,指向爲空(由於此時後面尚未節點,鏈表只有一個節點)
                first = new Node<> (i, null);
                prev = first;
                continue; //本次循環執行結束
            }

            //若是不是第一個節點,將產生的新節點鏈在prev後
            Node<Integer> node = new Node<> (i, null);
            prev.next = node;
            //連接以後,讓prev指向當前鏈表的最新節點,繼續建立下一個新節點
            prev = node;

            //若是是最後一個節點,則須要指向first,造成循環鏈表
            if (i == n) {
                prev.next = first;
            }
        }
        return first;
    }
}
複製代碼

數組

數組的思想就是經過一個數組,數組中每一個元素都是一我的,而後對數組進行循環處理,反覆遍從來達到」環「的效果,每當數組中的人數到ans時,將其標記爲淘汰。直到最後數組中只有一我的未被淘汰。

爲了更直觀的體現淘汰與存貨兩種狀態,咱們建立一個boolean數組。

固然,int數組也能夠,把數組按1-n編號,只須要把淘汰的元素致爲-1便可。boolean類型的巧妙之處就是利用數組下標來編號。

【第一步】咱們須要一個長度爲n的布爾值數組,數組的index就表示了第幾我的,元素的truefalse表示了這我的是否被淘汰。一開始咱們須要將全部人都設置爲未被淘汰。

【第二步】 咱們須要三個變量:

  1. stay記錄剩下未被淘汰的人數,初始值爲總人數;

  2. count計數器,每過一我的加一,加到ans時歸零,初始化爲0

  3. index標記從哪裏開始,index記錄了此時數到了第幾我的,當index等於總人數n時歸零 ,初始化爲0 由於是一個圈,因此最後一我的數完後又輪到第一我的數

【第三步】開始循環計算了

  • 首先判斷剩餘的人數是否大於一,若是大於一進入循環;

  • 判斷第index人,若是這我的未被淘汰,則計數器加一,若是等於ans則淘汰這我的,不然跳過計數繼續

  • 當index等於總人數n時,第二輪循環開始

【最後】計算結束後,數組中只有一個元素爲true,而這個就是約瑟夫那位靚仔了!

【完整代碼】

package com.topic.joseph;

/** * @Author: Mr.Q * @Date: 2020-05-07 09:12 * @Description:數組求解 * @Solution: */
public class ArraySolution {
    /** * @param n 圍成環人的編號(從1開始到n) * @param ans 數到ans的那我的出列 * @return 倖存人的編號 */
    public static int joseph(int n, int ans) {
        //開始時設置一個長度爲n的數組,並將元素都設爲true
        //數組的下標表明人到編號,數組元素的值(T,F)表明是否淘汰
        Boolean[] peopleFlags = new Boolean[n];
        for (int i = 0; i < n; i++) {
            peopleFlags[i] = true;
        }

        //剩下未被淘汰的人數
        int stay = n;
        //計數器,每過一我的加一,加到ans時歸零
        int count = 0;
        //標記從哪裏開始,index記錄了此時數到了第幾我的,當index等於總人數n時歸零
        //由於是一個圈,因此最後一我的數完後又輪到第一我的數
        int index = 0;
        while (stay > 1) {
            if (peopleFlags[index]) {
                //說明尚未被淘汰 計數器加1
                count++;
                if (count == ans) {
                    count = 0; //計數器歸0
                    peopleFlags[index] = false; //此人被淘汰
                    stay--; //未被淘汰的人數-1
                }
            }
            index++;

            //數到本輪最後一人時,則又從第一人開始計數
            if (index == n) {
                index = 0;
            }
        }

        //通過上面的循環,如今數組中被淘汰的人都標記爲false,最後沒被淘汰都人標記爲true
        for (int j = 0; j < n; j++) {
            if (peopleFlags[j]) {
                return j + 1;
            }
        }
        return -1;
    }
}
複製代碼

數學解法

這就涉及到咱的知識盲區了,做爲一個數學渣渣,就算把頭髮拔光也想不出來。不過,咱學學大佬們怎麼操做。

首先咱們把這n我的的序號編號從0~n-1(理由很簡單,因爲m是可能大於n的,而當m大於等於n時,那麼第一個出列的人編號是m%n,而m%n是可能等於0的,這樣編號的話可以簡化後續出列的過程).

當數到m-1的那我的出列,所以咱們編號完成以後,開始分析出列的過程:

第一次出列:

一開始的時候,全部人的編號排成序列的模式即爲:

0, 1, 2, 3, 4, 5 ... n-2,n-1

那麼第一次出列的人的編號則是(m-1)%n1,那麼在第一我的出列以後,從他的下一我的又開始從0開始報數,爲了方便咱們設

k1 = m%n1(n1爲當前序列的總人數)那麼在第一我的出列以後,k1則是下一次新的編號序列的首位元素,

那麼咱們獲得的新的編號序列爲:

k1,k1+1,k1+2,k1+3...n-2,n-1,0,1,2...k1-3,k1-2 (k1-1第一次已出列)

那麼在這個新的序列中,第一我的依舊是從0開始報數,那麼在這個新的序列中,每一個人報的相應數字爲:

0, 1, 2, 3 .... n-2

那麼第二次每一個人報的相應數字與第一次時本身相應的編號對應起來的關係則爲:

0 --> k1

1 --> k1+1

2 --> k1+2

...

n-2 ---> (k1+n-2)%n1(n1爲當前序列的總人數,由於是循環的序列,k1+n-1可能大於總人數)

那麼這時咱們要解決的問題就是n-1我的的報數問題(即n-1階約瑟夫環的問題)

可能以上過程你仍是以爲不太清晰,那麼咱們重複以上過程,繼續推導剩餘的n-1我的的約瑟夫環的問題:

那麼在這剩下的n-1我的中,咱們也能夠爲了方便,將這n-1我的編號爲:

0,1,2,3,4...n-2

那麼此時出列的人的編號則是(m-1) % n2(n2爲當前序列的總人數),一樣的咱們設k2 = m % n2,那麼在這我的出列了之後,序列重排,重排後新的編號序列爲:

k2,k2+1,k2+2,k2+3...n-2,n-1,0,1,2...k2-3,k2-2 (k2-1第一次已出列)

那麼在這個新的序列中,第一我的依舊是從1開始報數,那麼在這個新的序列中,每一個人報的相應數字爲:

1,2,3,4....n-2

那麼這樣的話是否是又把問題轉化成了n-2階約瑟夫環的問題呢?

後面的過程與前兩次的過程如出一轍,那麼遞歸處理下去,直到最後只剩下一我的的時候,即可以直接得出結果 當咱們獲得一我的的時候(即一階約瑟夫環問題)的結果,那麼咱們是否能經過一階約瑟夫環問題的結果,推導出二階約瑟夫環的結果呢?

藉助上面的分析過程,咱們知道,當在解決n階約瑟夫環問題時,序號爲k1的人出列後,剩下的n-1我的又從新組成了一個n-1階的約瑟夫環,那麼:

假如獲得了這個n-1階約瑟夫環問題的結果爲ans(即最後一個出列的人編號爲ans),那麼咱們經過上述分析過程,能夠知道,n階約瑟夫環的結果: (ans + k)%n(n爲當前序列的總人數),而k = m%n

則有:n階約瑟夫環的結果

(ans + m % n)%n,那麼咱們還能夠將該式進行一下簡單的化簡:

  • 當 m < n 時,易得上式可化簡爲:(ans + m)% n

  • 而當m>=n時,那麼上式則化簡爲:(ans % n + m%n%n)% n 即爲:(ans % n + m%n)% n

  • 而 (ans + m)% n = (ans % n + m%n)% n

所以得證:

(ans + m % n)%n = (ans + m)% n

這樣的話,咱們就獲得了遞推公式,因爲編號是從0開始的,那麼咱們能夠令

f[1] = 0;//當一我的的時候,出隊人員編號爲0

f[n] = (f[n-1] + m)%n; //m表示每次數到該數的人出列,n表示當前序列的總人數

而咱們只須要獲得第n次出列的結果便可,那麼不須要另外聲明數組保存數據,只須要直接一個for循環求得n階約瑟夫環問題的結果便可

因爲每每現實生活中編號是從1-n,那麼咱們把最後的結果加1便可

果真啊,數學纔是大哥。

使用Java提供的LinkedList

相比較於本身實現的循環鏈表,用API的LinkedList簡化了不少,關鍵是在於remove()方法。

  • 設置index指針,模擬報數。到達ans或者一輪判斷走完時重置

  • remove刪除自殺的節點,來不斷縮短鏈表長度

  • 最終鏈表只剩一個元素,即爲存活的約瑟夫的編號

【代碼】

/** * @Author: Mr.Q * @Date: 2020-05-08 18:26 * @Description:Java自帶鏈表實現 */
public class LinkedListSolution {
    public static int joseph(int n, int ans) {
        LinkedList<Integer> list = new LinkedList<> ();
        for (int i = 1; i <= n; i++) {
           list.add(i);
        }
        int index = 0;
        while (list.size() > 1) {
            for (int i = 0; i < list.size(); i++) {
                index++;
                int away = 0;
                if (index == ans) {
                    away = list.get(i);
                    list.remove(i);
                    index = 1;  //指針重置
                    if(i == list.size() || index == ans){
                        index = 0;
                    }
                    System.out.print(away + " ");
                }
            }
        }
        return list.get(0);
    }
}

複製代碼

這不就簡簡單單奧利幹了麼,當年的約瑟夫,就是這麼躺贏的!

【參考文章】

  1. 約瑟夫環問題

  2. 約瑟夫環的幾種實現方式

  3. 使用JAVALinkedList解決約瑟夫圓環問題

相關文章
相關標籤/搜索