劍指offer學(xué)習(xí)筆記:6.4 抽象建模能力

建模的第一步是選擇合理的數(shù)據(jù)結(jié)構(gòu)來(lái)表達(dá)問(wèn)題。實(shí)際生活中的問(wèn)題千變?nèi)f化,但是數(shù)據(jù)結(jié)構(gòu)就只有固定的幾種。選擇合理的數(shù)據(jù)結(jié)構(gòu)來(lái)表達(dá)問(wèn)題,也就是建立模型。
建模的第二步是分析模型的內(nèi)在規(guī)律,并用變成語(yǔ)言表達(dá)其規(guī)律。

面試題43:n個(gè)骰(tou二聲)子的點(diǎn)數(shù)

把n個(gè)骰子扔在地上,所有的骰子朝上一面的點(diǎn)數(shù)和為s。輸入n,打印出s的所有的可能值出現(xiàn)的概率
leetcode鏈接 https://leetcode-cn.com/problems/nge-tou-zi-de-dian-shu-lcof/

class Solution {
public:
   vector<double> twoSum(int n) {
       vector<double> result;
       if (n < 1)
       {
           return result;
       }
       // 為了好交替調(diào)用,將兩個(gè)矩陣定義為二維矩陣
       int* sum[2];
       sum[0] = new int[n*6 + 1];  // 所有可能和的總數(shù),每位存放的就是該位對(duì)應(yīng)和出現(xiàn)次數(shù)
       sum[1] = new int[n*6 + 1];
       int flag = 0;
       for(int i = 0; i < n*6 + 1; i++)
       {
           sum[0][i] = 0;
           sum[1][i] = 0;
       }
       for(int i = 1; i <= 6; i++)
       {
           sum[0][i] = 1;    // 只有一個(gè)骰子的情況
       }
       for(int k = 2; k <= n; k++)
       {
           for(int i = 0; i < k; i++) // k個(gè)骰子最小和是k,因此小于k是不可能了
           {
               sum[1-flag][i] = 0;
           }
           for(int i = k ; i < n*6 + 1; i++)  // 當(dāng)點(diǎn)數(shù)大于k,考慮當(dāng)前骰子點(diǎn)數(shù)和=上一次骰子點(diǎn)數(shù)和-1....前一次骰子點(diǎn)數(shù)和-6
           {
               sum[1-flag][i] = 0;
               for(int j = 1; j <= 6 && j <= i; j++)
               {
                   sum[1-flag][i] += sum[flag][i-j];
               }
           }
           flag = 1 -flag;
       }
       double total = pow(6, n);
       for(int i = n; i <= 6*n; i++)
       {
           result.push_back(sum[flag][i] / total);
       }
       return result;
   }
};

解題思路:
思路1:全排列。建立數(shù)組,每次排列后sum=n,則數(shù)組第n位+1,最后循環(huán)數(shù)組,計(jì)算每一個(gè)非零位概率。全排列用遞歸,會(huì)超時(shí)。這里遞歸思路用的是每次固定數(shù)組中一位,更正規(guī)一點(diǎn)的思想是用回溯。定義一個(gè)棧,每次循環(huán)push進(jìn)當(dāng)前位固定好的元素,遞歸調(diào)用n-1函數(shù),函數(shù)退出后將push進(jìn)元素pop出來(lái)。

class Solution {
public:
    int all = 0;
    void fullPer(int* arr, map<int, int>& sum, int index, int n)
    {
        if (index >= n - 1)
        {
            all++;
            int tmp = 0;
            for(int i = 0; i < n; i++)
            {
                // cout << arr[i] << endl;
                tmp = tmp + arr[i];
            }
            sum[tmp]++;
            return;
        }
        for(int i = 1; i < 7; i++)
        {
            arr[index+1] = i;
            fullPer(arr, sum, index+1, n);
        }
        return;
    }
    vector<double> twoSum(int n) {
        vector<double> result;
        map<int, int> sum;
        int *arr = new int[n];
        for(int i = 1; i < 7; i++)
        {
            arr[0] = i;
            fullPer(arr, sum, 0, n);
        }
        map<int, int>::iterator iter = sum.begin();
        while(iter != sum.end())
        {
            result.push_back(iter->second / double(all));
            iter++;
        }
        return result;
    }
};

思路2:用兩個(gè)數(shù)組來(lái)存儲(chǔ)骰子點(diǎn)數(shù)的每一個(gè)總數(shù)出現(xiàn)次數(shù)。在一次循環(huán)中,第一個(gè)數(shù)組中的第n個(gè)數(shù)字表示骰子和為n出現(xiàn)的次數(shù)。在下一次循環(huán)中,我們加上一個(gè)新的骰子,此時(shí)和為n的骰子出現(xiàn)的次數(shù)應(yīng)該等于上一次循環(huán)中骰子點(diǎn)數(shù)和為n-1,n-2...n-6的和,所以我們把另一個(gè)數(shù)組的n位設(shè)置為n-1,n-2...n-6的和。最后被循環(huán)的數(shù)組就代表全排列后的結(jié)果。

class Solution {
public:
    vector<double> twoSum(int n) {
        vector<double> result;
        if (n < 1)
        {
            return result;
        }
        // 為了好交替調(diào)用,將兩個(gè)矩陣定義為二維矩陣
        int* sum[2];
        sum[0] = new int[n*6 + 1];  // 所有可能和的總數(shù),每位存放的就是該位對(duì)應(yīng)和出現(xiàn)次數(shù)
        sum[1] = new int[n*6 + 1];
        int flag = 0;
        for(int i = 0; i < n*6 + 1; i++)
        {
            sum[0][i] = 0;
            sum[1][i] = 0;
        }
        for(int i = 1; i <= 6; i++)
        {
            sum[0][i] = 1;    // 只有一個(gè)骰子的情況
        }
        for(int k = 2; k <= n; k++)
        {
            for(int i = 0; i < k; i++) // k個(gè)骰子最小和是k,因此小于k是不可能了
            {
                sum[1-flag][i] = 0;
            }
            for(int i = k ; i < n*6 + 1; i++)  // 當(dāng)點(diǎn)數(shù)大于k,考慮當(dāng)前骰子點(diǎn)數(shù)和=上一次骰子點(diǎn)數(shù)和-1....前一次骰子點(diǎn)數(shù)和-6
            {
                sum[1-flag][i] = 0;
                for(int j = 1; j <= 6 && j <= i; j++)
                {
                    sum[1-flag][i] += sum[flag][i-j];
                }
            }
            flag = 1 -flag;
        }
        double total = pow(6, n);
        for(int i = n; i <= 6*n; i++)
        {
            result.push_back(sum[flag][i] / total);
        }
        return result;
    }
};

面試題44:撲克牌的順子

從撲克牌中隨機(jī)抽5張牌,判斷是不是順子。撲克牌中2-10為數(shù)字本身,A為1,J為11,Q為12,K為13。而大王小王可以看做任意數(shù)字。
leetcode鏈接 https://leetcode-cn.com/problems/bu-ke-pai-zhong-de-shun-zi-lcof/

class Solution {
public:
   bool isStraight(vector<int>& nums) {
       if (nums.size() == 0)
       {
           return false;
       }
       int max = nums[0];
       int min = nums[0];
       int zeroNum = 0;
       vector<int> tmpVec;
       for(int i = 0; i < nums.size(); i++)
       {
           int tmp;
           if (nums[i] == 0)
           {
               zeroNum++;
               continue;
           }
           if (nums[i] == 'A')
           {
               tmp = 1;
           } else if (nums[i] == 'J')
           {
               tmp = 11;
           } else if (nums[i] == 'Q')
           {
               tmp = 12;
           } else if (nums[i] == 'K')
           {
               tmp = 13;
           }
           else {
               tmp = nums[i];
           }
           if (find(tmpVec.begin(), tmpVec.end(), tmp) == tmpVec.end())
           {
               tmpVec.push_back(tmp);
           }
           else{
               return false;
           }
           if (tmp < min || min == 0)
           {
               // cout << "min: " << min << endl;
               min = tmp;
           }
           if (tmp > max || max == 0)
           {
               max = tmp;
           }
       }
       // cout << "max: " << max << " min: " << min << " zeroNum: " << zeroNum << " size: " << nums.size() << endl;
       return (max - min + 1) <= nums.size();
   }
};

解題思路:由于大小王可以是任意數(shù),將其設(shè)為0,方便操作 1)數(shù)組排序 2)記錄數(shù)組中0的個(gè)數(shù),記錄數(shù)組最大值-非零最小值的差 + 1=數(shù)組長(zhǎng)度+n,若n=0的個(gè)數(shù),則為順子,否則,不是。
轉(zhuǎn)變下思路,感覺(jué)不排序也可以,遍歷,最大,最小值,無(wú)相等元素,若最大值-最小值+1 <= 數(shù)組長(zhǎng)度,則為順子。

面試題45:圓圈中最后剩下的數(shù)字(約瑟夫環(huán))

0,1....n-1這n個(gè)數(shù)字排成一個(gè)圓圈,從數(shù)字0開(kāi)始,每次從圓圈中刪除第m個(gè)數(shù)字,求圓圈中剩余的最后一個(gè)數(shù)字。
leetcode鏈接 https://leetcode-cn.com/problems/yuan-quan-zhong-zui-hou-sheng-xia-de-shu-zi-lcof/

class Solution {
public:
   int lastRemaining(int n, int m) {
       if (n == 0)
       {
           return 0;
       }
       if (m == 1) // 總是刪除當(dāng)前節(jié)點(diǎn),最后獲得的一定是最后一個(gè)節(jié)點(diǎn)
       {
           return n - 1;
       }
       // 遞歸寫(xiě)法,效率低
       // return (lastRemaining(n-1, m) + m) % n;
       int* result = new int[n];
       result[0] = 0;
       for(int i = 1; i < n; i++)
       {
           result[i] = (result[i-1] + m) % (i+1);
           // cout << i << " " << result[i] << endl;
       }
       return result[n-1];
   }
};

解題思路:
解法一:經(jīng)典解法。用環(huán)形鏈表模擬操作過(guò)程。超時(shí),心塞。注意m=1的情況。

class Solution {
public:
    struct ListNode{
        int val;
        ListNode* next;
        ListNode(int val) {this->val = val; next = NULL;}
    };
    int lastRemaining(int n, int m) {
        if (n == 0)
        {
            return 0;
        }
        m = m % n;
        ListNode* head = new ListNode(0);
        ListNode* tmp = head;
        // 構(gòu)建一個(gè)環(huán)形鏈表
        for(int i = 1; i < n; i++)
        {
            ListNode* h = new ListNode(i);
            tmp->next = h;
            tmp = h;
        }
        tmp->next = head;
        /* 驗(yàn)證鏈表構(gòu)建是否正確
        tmp = head;
        for(int i = 0; i < n + 1; i++)
        {
            cout << tmp->val << " ";
            tmp = tmp->next;
        }
        cout << endl;
        */
        if (m == 1)   // 一直在刪除當(dāng)前節(jié)點(diǎn),剩余的一定是尾節(jié)點(diǎn)
        {
            return tmp->val;
        }
        tmp = head;
        while(tmp->next != tmp)
        {
            for(int i = 1; i < m - 1; i++)
            {
                tmp = tmp->next;
            }
            // cout << tmp->val << endl;
            ListNode* del = tmp->next;
            tmp->next = tmp->next->next;
            delete del;
            tmp = tmp->next;  
        }
        return tmp->val;
    }
};

解法二:找規(guī)律,(自己推一推)最終歸納出在n個(gè)數(shù)中刪除第m個(gè)數(shù)的最終剩余元素的關(guān)系是f(n,m)= \begin{cases} \cf 0, &n=1\\ [f(n-1,m) + m] \% n & n > 1 \end{cases}
參考鏈接https://blog.csdn.net/u011500062/article/details/72855826
思路上就是,用f(n,m)代表n個(gè)人殺掉第m個(gè)人之后的幸存者下標(biāo)。那個(gè)f(n-1,m)就是殺掉一個(gè)人之后,當(dāng)前幸存者的下標(biāo)移動(dòng)到的位置。因?yàn)橄乱淮斡?jì)數(shù)是從被殺掉的下一個(gè)開(kāi)始,因此就是從m+1開(kāi)始記。因此幸存者的下標(biāo)往前移動(dòng)了m位。反推,則得到f(n,m) = f(n-1,m) + m。因?yàn)橄聵?biāo)可能存在越界,因此更改為f(n,m)=[f(n-1,m) + m] % n。
過(guò)程

殺人過(guò)程圖解

class Solution {
public:
    int lastRemaining(int n, int m) {
        if (n == 0)
        {
            return 0;
        }
        if (m == 1) // 總是刪除當(dāng)前節(jié)點(diǎn),最后獲得的一定是最后一個(gè)節(jié)點(diǎn)
        {
            return n - 1;
        }
        // 遞歸寫(xiě)法,效率低
        // return (lastRemaining(n-1, m) + m) % n;
        int* result = new int[n];
        result[0] = 0;
        for(int i = 1; i < n; i++)
        {
            result[i] = (result[i-1] + m) % (i+1);
            // cout << i << " " << result[i] << endl;
        }
        return result[n-1];
    }
};
?著作權(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)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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