【算法】卡特兰数问题(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 generateT(int i,int j){
        vector 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 left = generateT(i,k-1);
            vector 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 generateTrees(int n) {
        if(n<1)
            return vector();
        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;kreturn 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;mfor(int i = m;ifor(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的数目,这不就是卡特兰数的标准形式了嘛

你可能感兴趣的:(算法)