1. 程式人生 > >【演算法】卡特蘭數問題(BST排列個數,矩陣乘法,算數加括號,排隊等)

【演算法】卡特蘭數問題(BST排列個數,矩陣乘法,算數加括號,排隊等)

卡特蘭數當年大二時候就知道了其在行走路線問題上面的應用,後來發現其還有更多的應用場景,而且最近做LeetCode也碰見了不少這樣的問題,特此總結一番。

LeetCode上跟卡特蘭數相關的問題有如下四道:
96. Unique Binary Search Trees
95. Unique Binary Search Trees II
這兩道題題幹差不多,就是1~n總共n個數,將其構建成一個BST數,問總共有多少種不同的構建方法(96題),輸出所有情況(95題)

241. Different Ways to Add Parentheses
給一個運算公式如:2-1-1,將括號加到其中,並輸出所有可能加括號後的結果,如上面這個就輸出:[2,0] ( 解釋:(2-(1-1)) = 2,((2-1)-1) = 0)

312.Burst Balloons
一排氣球,每個氣球上面都有一個數字nums,扎破一個氣球i之後,得到nums[i-1]*nums[i]*nums[i+1],問怎麼個扎破順序使得最後得到的總和最大。

這些題背後的思想或者用法都跟卡特蘭數有關。對於卡特蘭數的問題,總結如下:

  1. 輸出所有情況的問題:95,241兩道題。一定需要用到遞迴,是一個卡特蘭數級別的複雜度演算法。
  2. 輸出一個最值結果,如最大最小:312題。因為卡特蘭數,用遞迴的時候會有大量的重複情況,這種情況下一定需要用到動態規劃。而卡特蘭數相關問題的動態規劃,一定是一個O(n3)的演算法。
  3. 還有最簡單的一種情況,就是輸出情況個數:95題。這種情況下,就可以直接套用卡特蘭數的通項公式。

找出所有情況的問題

如上所示,要找出所有情況,一定是用到了遞迴。

對於95題

    vector<TreeNode*> generateT(int i,int j){
        vector<TreeNode*> res;
        if(i>j){
            res.push_back(NULL);
            return res;
        }
        if(i == j){
            TreeNode* tmp = new TreeNode(i);
            res.push_back(tmp);
            return
res; } for(int k = i;k <= j;k++){ vector<TreeNode*> left = generateT(i,k-1); vector<TreeNode*> right = generateT(k+1,j); for(auto m:left) for(auto n:right){ //注意這個地方一定要在這個地方new tmp,如果在之前new tmp,並且複用的話,會導致結果出錯,因為等於每次push進最終結果的都是同一個tmp TreeNode* tmp = new TreeNode(k); tmp->left = m; tmp->right = n; res.push_back(tmp); } } return res; } vector<TreeNode*> generateTrees(int n) { if(n<1) return vector<TreeNode*>(); return generateT(1,n); }

最值類問題

最值類問題需要用到備忘錄方法,即遞迴的時候記錄一下最值,大部分時候可以化簡為動規問題。而且一定是一個O(n3)的演算法。
對於312題,這道題最開始我看到的時候還是比較無從下手的狀態,後來仔細一想,這不就相當於一個矩陣連乘加括號的問題嘛,一模一樣。遞迴+記錄中間狀態的寫法:

vector<vector<int>> mark;
int maxrec(int i,int j,const vector<int> &nums){
//實際上,以下兩個if判斷:if(i == j-2) 和 if(i == j-1) 都是可以省略的,因為加不加它都不會影響什麼。
    if(i == j-2){
        mark[i][j] = nums[i]*nums[i+1]*nums[i+2];
        return mark[i][j];
    }
    if(i == j-1)
        return mark[i][j];
    if(mark[i][j] == 0)
        for(int k = i+1;k<j;k++)
            mark[i][j] = max(mark[i][j],maxrec(i,k,nums)+maxrec(k,j,nums)+nums[i]*nums[k]*nums[j]);
    return mark[i][j];

}
int maxCoins(vector<int>& nums) {
    vector<int> new_nums(nums.size()+2,1);
    for(int i = 1;i<=nums.size();i++)
        new_nums[i] = nums[i-1];

    mark = vector<vector<int>>(nums.size()+2,vector<int>(nums.size()+2,0));
    return maxrec(0,nums.size()+1,new_nums);
}

它的動規解法:

int maxCoins(vector<int>& nums) {
    vector<int> new_nums(nums.size()+2,1);
    for(int i = 1;i<=nums.size();i++)
        new_nums[i] = nums[i-1];

    vector<vector<int>> mark = vector<vector<int>>(new_nums.size(),vector<int>(new_nums.size(),0));
    for(int m = 2;m<new_nums.size();m++)
        for(int i = m;i<new_nums.size();i++)
            for(int k = i-m+1;k < i;k++)
                mark[i-m][i] = max(mark[i-m][i],mark[i-m][k]+mark[k][i]+new_nums[i-m]*new_nums[k]*new_nums[i]);
    return mark[0][new_nums.size()-1];
}

以及我發現了個神奇的現象就是,用遞迴的速度並不如用動規的速度,即使是在遞迴的時候有些點可能不需要搜尋。所以這說明遞迴壓棧,函式呼叫的開銷還是非常的大啊。

簡單計數類問題

先說卡特蘭數的的公式:

h(n)=Cn2nCn12n
具體的推導方法可以用折線反射法來做,可以參考:折現法——卡特蘭數證明
這種題目常在小題中出現,總結一下常出現的小題:
  1. 一個棧(無窮大)的進棧序列為1,2,3,..n,有多少個不同的出棧序列
  2. 給定四個1和四個0,進行排列組合,使得從左往右讀0的個數不超過1的個數
  3. 12個高矮不同的人,排成兩排,每排必須是從矮到高排列,而且第二排比對應的第一排的人高,問排列方式有多少種?

上面的第三題還是非常精髓的,12個人排成兩排,則我們可以用0,1分別代表排在第一排還是排在第二排。則12個人身高從低到高先排個序1~12,然後:
1 2 3 4 5 6
7 8 9 10 11 12
這樣的排列方式可以用00000111111的方式來表示
1 3 5 7 9 11
2 4 6 8 10 12
這樣的排列可以用010101010101的方式來表示
如果要滿足題意,則必須從做到右,1的數目不大於0的數目,這不就是卡特蘭數的標準形式了嘛