[刷題]劍指offer之圓圈中最后剩下的數(shù)字

題目

題目:有n個人,圍成一個環(huán),編號為 0、1、2、3、、、n-1,從第一個人開始循環(huán)報數(shù)(從1開始),假設(shè)數(shù)到m的那個人出列,然后從下一個人繼續(xù)數(shù)數(shù),數(shù)到m出列,以此循環(huán),最后那個人為勝利者,求勝利者的編號。

這其實就是有名的約瑟夫問題。

輸入示例

  • n=4, m=3 result=0
  • n=50, m=20 result=33
  • n=100, m=37 result=44

思路

解法一--模擬法

可以使用數(shù)組或者鏈表來模擬這n個人,每次刪除第m個人,直到只剩下一個人為止。

使用數(shù)組刪除元素的復(fù)雜度較高,鏈表是最優(yōu)選擇,C++ stl中的鏈表用的不太熟練,所以自己寫一個。為了刪除方便,選擇雙向鏈表,單向鏈表刪除的時候還需要兩個指針,很麻煩。

class Solution {
public:
    struct Node{
        int val;
        Node* next;
        Node* parent;
    }; // 鏈表節(jié)點
    int LastRemaining_Solution(int n, int m)
    {
        if(m == 0 || n == 0)
            return -1; // 無效情況返回-1
        // 為所有節(jié)點分配空間
        vector<Node> node(n);
        //頭節(jié)點單獨(dú)賦值
        node[0].val = 0;

        //構(gòu)造鏈表
        for(int i=1;i<n;i++){
            node[i].val = i; //編號
            node[i].next = NULL; 
            node[i-1].next = &node[i]; 
            node[i].parent = &node[i-1];
        }
        node[n-1].next = &node[0];
        node[0].parent = &node[n-1];
        
        //模擬
        int num = n; //還剩下的人數(shù)
        Node *head = &node[0]; //頭節(jié)點
        //在只剩下一個人的時候停止模擬
        while(num > 1){
            // 找到第m個
            int count = 0;
            while(count < m-1){
                head = head->next;
                count ++;
            }
            //找到了第m個 開始刪除
            head->parent->next = head->next; 
            head->next->parent = head->parent;
            head = head->next;
            num --; // 剩余人數(shù)減1
        }
        return head->val;
    }
};

這種算法時間復(fù)雜度為O(mn),空間復(fù)雜度為O(n)。

解法二--數(shù)學(xué)推導(dǎo)

感覺應(yīng)該存在某種規(guī)律,但我等數(shù)學(xué)渣渣并不能夠推導(dǎo)出來。

看完書上的解答,表示有點繞,現(xiàn)在用自己的話解釋一遍:

f(n,m):表示 每次在n個數(shù)字0,1,...,n-1中刪除第m個數(shù)字最后剩下的數(shù)字(也就是要求的結(jié)果)。(注意,要求數(shù)字標(biāo)號需要是連續(xù)的,所以后面刪除一個元素后標(biāo)號不連續(xù)了,需要重新標(biāo)號)。

在這n個數(shù)字中,第一個被刪除的數(shù)字是 (m-1)%n (取余的原因是m可能比n大),記作k,則k=(m-1)%n。刪除后的序列為 0,1,...,k-1,k+1,...,n-1。由于下一次刪除是從k+1開始計數(shù)的,所以相當(dāng)于從標(biāo)號為k+1,k+2,...,n-1,0,1,2,...,k-1的序列中繼續(xù)刪除第m個數(shù)字,最終剩下的數(shù)字就是結(jié)果。

剩下的n-1個數(shù)字如果重新按順序標(biāo)號得到序列0,1,...,n-2,則每次刪除第m個數(shù),最后剩下的數(shù)字就是f(n-1,m)。由于重新標(biāo)號了,所以并不是f(n,m)=f(n-1,m)! 那么f(n-1,m)對應(yīng)的數(shù)字在修改標(biāo)號之前是什么數(shù)呢?

事實上,原先的不連續(xù)的序列A(k+1,k+2,...,n-1,0,1,2,...,k-1)變成了序列B(0,1,...,n-2),而我們主要是想知道如何從序列B中的某個數(shù)找到序列A中對應(yīng)的關(guān)系,先建立個映射表格:

B序列 序列A
0 k+1
1 k+2
n-k-2 n-1
n-k-1 0
n-k 1
n-2 k-1
f(n-1,m) (f(n-1,m)+k+1)%n

根據(jù)表格,可以很直觀的看出,在B序列中數(shù)字x,對應(yīng)于A序列中的(x+k+1)%n(注意:必須取余數(shù),因為B序列中為n-k,而n-k+k+1n+1,必須取余數(shù)才能得到1)。所以在B序列中標(biāo)號為f(n-1,m),對于在A序列中就為(f(n-1,m)+k+1)%n

還記得k嗎,k就是在第一次刪除的時候刪掉的數(shù)(與n有關(guān)的變量),k=(m-1)%n。

將其帶入上面的式子,就得到:

(f(n-1,m)+k+1)%n = (f(n-1,m)+(m-1)+1)%n = (f(n-1,m)+m)%n

因此,我們就得到了這個遞歸公式,而當(dāng)n=1的時候,也就是序列中只有標(biāo)號為0的數(shù)字,顯然最后剩下的數(shù)字就是0,所以整個公式就是:

遞歸公式.png

根據(jù)這個公式,寫代碼就很簡單了。

class Solution {
public:
    int LastRemaining_Solution(int n, int m)
    {
        if(m == 0 || n == 0)
            return -1; // 無效情況返回-1
        int last = 0;
        for(int i=2;i<=n;i++)
            last = (last+m) % i;
        return last;
    }
};

這種算法的時間復(fù)雜度是O(n),空間復(fù)雜度是O(1),遠(yuǎn)遠(yuǎn)優(yōu)于第一種算法,但是推導(dǎo)復(fù)雜,數(shù)學(xué)渣渣表示如果沒見過這個題,絕對推導(dǎo)不出來。。。

總結(jié)

使用鏈表模擬的常規(guī)解法必須掌握,數(shù)學(xué)推導(dǎo),那就看狀態(tài)吧。。

我的SegmentFault鏈接

參考

劍指offer第二版--面試題62

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容