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

題目

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

這其實(shí)就是有名的約瑟夫問題

輸入示例

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

思路

解法一--模擬法

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

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

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

        //構(gòu)造鏈表
        for(int i=1;i<n;i++){
            node[i].val = i; //編號(hào)
            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é)點(diǎn)
        //在只剩下一個(gè)人的時(shí)候停止模擬
        while(num > 1){
            // 找到第m個(gè)
            int count = 0;
            while(count < m-1){
                head = head->next;
                count ++;
            }
            //找到了第m個(gè) 開始刪除
            head->parent->next = head->next; 
            head->next->parent = head->parent;
            head = head->next;
            num --; // 剩余人數(shù)減1
        }
        return head->val;
    }
};

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

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

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

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

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

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

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

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

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,對(duì)應(yīng)于A序列中的(x+k+1)%n(注意:必須取余數(shù),因?yàn)锽序列中為n-k,而n-k+k+1n+1,必須取余數(shù)才能得到1)。所以在B序列中標(biāo)號(hào)為f(n-1,m),對(duì)于在A序列中就為(f(n-1,m)+k+1)%n。

還記得k嗎,k就是在第一次刪除的時(shí)候刪掉的數(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

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

遞歸公式.png

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

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;
    }
};

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

總結(jié)

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

我的SegmentFault鏈接

參考

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

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

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

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