【劍指offer-Java版】45圓圈中最後剩下的數字

圓圈中最後剩餘的數字:約瑟夫環問題
0-n這n個數字排成一圈,從數字零開始每次從這個圓圈中刪除第m個數字,求出剩餘的最後一個數字


    public class _Q45<T> {

    // 低效的模擬而已
    public int LastRemaining(int n, int m){
        if(n < 0 || m < 0) return -1;

        int circle[] = new int[n];
        for(int i=0; i<n; i++){
            circle[i] = i;
        }

        int count = 0;
        int time = 0;
        for (int i = 0; i < n; i++) {

            if (circle[i] != -1) {
                count++;
                if (count == m) {
                    circle[i] = -1;
                    count = 0;
                    time++;
                }
            }

            if ((i == n - 1)  && (time != (n-1)))
                i = -1;
        }
        int result = 0;
        for(int i=0; i<n; i++){
            if(circle[i] != -1){
                result = circle[i];
                break;
            }
        }

        return result;
    }

    // 採用映射的方法:
    /*
     * 對於原始 0 - n數據,刪除第m個數,被刪除的數字爲(m-1)%n,將該數記爲k ,那麼剩餘數字爲
     * 0 1 2 .. m-2 m m+1 ... n-1
     * 新的計數起點爲 m.
     * 
     * 將每次被刪除的數字記爲m 和 n的一個函數,那麼 k = f(n,m)
     * 刪除一個數字之後,原序列如果不變的話,是不能繼續使用f的,因爲f中關於n的序列是從0-n
     * 因此剩餘序列中刪除的下一個數字可以用g(n,m)來表示 -- 顯然已經破壞了遞歸的條件
     * 
     * 爲了使刪除下一個數字時仍然可以使用f函數,可以將刪除一個數字之後的序列做一個映射,以刪除第一個數字之後的序列爲例
     * m   -> 0
     * m+1 -> 1
     * ...
     * n-1 -> n-m-1
     * 0   -> n-m
     * 1   -> n-m+1
     * ...
     * m-2 -> n-2
     * 映射函數記爲p,是關於x的一個函數p(x) = (x-m)%n 其中x取[0, m-2]並[m, n-1]
     * 該函數最終的值是最後剩餘數字的映射後的編號,因此該函數的逆即爲最後剩餘數字實際編號
     * 其逆爲: p`(x) = (x+m)%n
     * 
     * g(n-1, m) = p`(f(n-1, m)) = (f(n-1, m) + m)%n = f(n, m)
     * 注意該公式第一個等號成立的原因:
     * 該等號左側函數代表在不是從0開始的序列中刪除第m個元素
     * 右側函數代表從0開始的n-1個元素中刪除第m個元素,由於刪除前都對這些元素做了映射,因此對於最後結果求逆,得出最後剩餘元素的實際編號
     * 根據實際經驗,無論是每次重新從0開始刪除還是每次從新的被刪除元素的下一元素開始刪除,最終結果是相等的 -- 其實還是存在嚴格的數學證明
     * 
     * 第二個等號只是把參數帶入而已 x = f(n-1, m)
     * 
     * 第三個等號成立的原因是 g(n-1, m) = f(n, m)
     * 
     * 最終得到遞歸等式:
     * f(n, m) = (f(n-1, m) + m) % n 其中變量爲n;而m其實可以看做是常量
     * 當n 取爲1的時候就代表只剩下最後一個數字,即爲所求 -- 當然n == 1也就是遞歸的出口;不過不使用遞歸直接用循環也可以
     * 
     */
    // 非遞歸版本
    public int LastRemainingV2(int n, int m){
        if(m < 1 || n < 1) return -1;

        int last = 0;
        for(int i=2; i<=n; i++){
            last = (last + m) % i;
        }

        return last;
    }

    // 遞歸版本
    public int LastRemainingV3(int n, int m){
        if(m < 1 || n < 1) return -1;

        if(n == 1) return 0; // 出口

        return (LastRemainingV3(n-1, m) + m)%n;
    } 
    }

測試代碼:


    public class _Q45Test extends TestCase {

    _Q45<?> remain = new _Q45();

    public void test(){
        int n1 = 5;
        int m1 = 3;

        int n2 = 6;
        int m2 = 5;

        System.out.println(remain.LastRemaining(n1, m1));
        System.out.println(remain.LastRemainingV2(n1, m1));
        System.out.println(remain.LastRemainingV3(n1, m1));

        System.out.println(remain.LastRemaining(n2, m2));
        System.out.println(remain.LastRemainingV2(n2, m2));
        System.out.println(remain.LastRemainingV3(n2, m2));

    }
    }
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章