【演算法】卡特蘭數問題(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],問怎麼個扎破順序使得最後得到的總和最大。
這些題背後的思想或者用法都跟卡特蘭數有關。對於卡特蘭數的問題,總結如下:
- 輸出所有情況的問題:95,241兩道題。一定需要用到遞迴,是一個卡特蘭數級別的複雜度演算法。
- 輸出一個最值結果,如最大最小:312題。因為卡特蘭數,用遞迴的時候會有大量的重複情況,這種情況下一定需要用到動態規劃。而卡特蘭數相關問題的動態規劃,一定是一個
O(n3) 的演算法。 - 還有最簡單的一種情況,就是輸出情況個數: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);
}
最值類問題
最值類問題需要用到備忘錄方法,即遞迴的時候記錄一下最值,大部分時候可以化簡為動規問題。而且一定是一個
對於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];
}
以及我發現了個神奇的現象就是,用遞迴的速度並不如用動規的速度,即使是在遞迴的時候有些點可能不需要搜尋。所以這說明遞迴壓棧,函式呼叫的開銷還是非常的大啊。
簡單計數類問題
先說卡特蘭數的的公式:
具體的推導方法可以用折線反射法來做,可以參考:折現法——卡特蘭數證明
這種題目常在小題中出現,總結一下常出現的小題:
- 一個棧(無窮大)的進棧序列為1,2,3,..n,有多少個不同的出棧序列
- 給定四個1和四個0,進行排列組合,使得從左往右讀0的個數不超過1的個數
- 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的數目,這不就是卡特蘭數的標準形式了嘛