1.给定一个字符串 s,求最长回文子序列的长度。

思路:

  • 子序列说明可以不连续。

  • 对于任意字符串,如果其头尾相同,则其回文子序列的长度是其去头去尾字符串回文子序列长度+2,如果头尾不同,则是去头或去尾字符串回文子序列中长的那个。

状态转移方程:
使用数组dpi表示子串i-j的最长回文子序列,则

 if(i==j) dp[i][j] = 1;
 if(i>j) dp[i][j] = 0;  
 if(i<j && s[i]==s[j]) dp[i][j] = dp[i+1][j-1]+2;
 if(i<J && s[i]!=s[j]) dp[i][j] = max(dp[i+1][j],dp[i][j-1]);

代码:

 int longestPalindromeSubseq(string s) {
    int ss = s.size();
    int dp[ss][ss];
    memset(dp,0,sizeof(dp));
    for(int i=ss-1;i>=0;--i){
        dp[i][i] = 1;
        for(int j=i+1;j<ss;++j){
            if(s[i]==s[j])
                dp[i][j] = dp[i+1][j-1]+2;
            else
                dp[i][j] = max(dp[i+1][j],dp[i][j-1]);
        }
    }
    return dp[0][ss-1];
}

2.有一种选数游戏,给定一个最大可选数m,和目标数d,两名玩家从1-m共m个数中轮流选数,当选数后,两人所有选择的数的和大于等于d时,该玩家获胜,对于给定的m和d,判断先选的玩家是否能胜利。

思路:
对于这种题,我们需要遍历所有可能的游戏状态,并使用动态规划避免重复遍历。游戏状态可以表示成当前剩下的可选数列表。对于某个状态,有两种转移,一种是我们从剩下数中可以选择一个数直接获胜,另一个是我们选择一个数后,在经过若干回合后对方会输。也就是两个递归的状态。

代码:

unordered_map<int,bool> mp;
bool canIWin(int m, int d) {
    if(m>=d) return true;
    if(m*(m+1)/2 < d) return false;
    return dp(m,d,0);
}
bool dp(int m,int d,int state){
    if(mp.count(state)) return mp[state];
    for(int i=0;i<m;++i){
        if(((1<<i) & state)==0){
            if(i+1>=d || !dp(m,d-i-1,state | (1<<i))){
                return mp[state] = true;
            }
        }
    }
    return mp[state] = false;
    
}

3.输入一个n,求出所有由1,...,n组成的异构二叉查找树的数目。

思路:

对于某个n,我们可以分别把1-n作为根,求出其结构数,然后加起来就是总数。

代码:

int numTrees(int n) {
    if(n<2) return 1;
    int res[n+1] = {1,1};
    for(int i=2;i<=n;++i){
        for(int j=0;j<i;++j){
            res[i] += res[j]*res[i-j-1];
        }
    }
    return res[n];
}

4.输入一个n,列出所有由1,...,n组成的异构二叉查找树。

二叉树的结构:

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode(int x) : val(x), left(NULL), right(NULL) {}
 * };
 */

思路:
对于一个1-n中的i,我们可以根据其i-1时的二叉树构建出新的二叉树,分为两种情况:

  1. 新节点作为根节点,那么i-1的所有二叉树分别作为其左子树就好

  2. 新节点不是根节点,那么对于i-1的二叉树,只要对于找寻最大节点路径上的每个节点,把新节点插入进去就得到所有的情形

语言描述可能不太清楚,可以看代码

代码:

TreeNode * clone(TreeNode *root){
    if(root==nullptr) return nullptr;
    TreeNode *node = new TreeNode(root->val);
    node->left = clone(root->left);
    node->right = clone(root->right);
    return node;
}

vector<TreeNode*> generateTrees(int n) {
    vector<TreeNode*> res;
    if(n==0) return res;
    res.push_back(nullptr);
    for(int i=1;i<=n;++i){//ith new node
        vector<TreeNode *> temp;
        for(int j=0;j<res.size();++j){//jth old trees of i-1
            TreeNode * newtree = new TreeNode(i);
            newtree->left = res[j];
            temp.push_back(clone(newtree));
            if(res[j]!=nullptr){
                TreeNode *tnode = res[j];
                while(tnode->right!=nullptr){
                    newtree->left = tnode->right;
                    tnode->right = newtree;
                    temp.push_back(clone(res[j]));
                    tnode->right = newtree->left;
                    
                    tnode = tnode->right;
                }
                tnode->right = newtree;
                newtree->left = nullptr;
                temp.push_back(clone(res[j]));
            }
        }
        res = std::move(temp);
    }
    return res;
}

5.有一个行数为m,列数为n的二维数组,求出从左上到右下的这样的路径:路径上的值的和最小。每一步只能向右或向上走。

思路:
很显然,状态转移方程是

dp[i][j] = grid[i][j] + min(dp[i+1][j],dp[i][j+1]);

根据这个方程,我们可以直接写出代码如下:

int minPathSum(vector<vector<int>>& grid) {
    if(grid.size()==0) return 0;
    int m = grid.size()-1;
    int n = grid[0].size()-1;
    int dp[m+1][n+1];
    memset(dp,0,sizeof dp);
    dp[m][n] = grid[m][n];
    for(int j=n-1;j>=0;--j){
        dp[m][j] = grid[m][j] + dp[m][j+1];
    }
    for(int i=m-1;i>=0;--i){
        for(int j=n;j>=0;--j){
            dp[i][j] = grid[i][j] + min(dp[i+1][j],dp[i][j+1]);
        }
    }
    return dp[0][0];
}

但是这样做的空间复杂度有O(N^2),我们还可以做的更好,我们注意到,在每一次状态转移时,我们只用到了两行,一行是当前要修改的行,另一行是修改行的下面一行,所以我们可以只用到O(N)的空间来解决:

int minPathSum(vector<vector<int>>& grid) {
    if(grid.size()==0) return 0;
    int m = grid.size()-1;
    int n = grid[0].size()-1;
    int dp[2][n+1];
    memset(dp,0,sizeof dp);
    dp[0][n] = grid[m][n];
    for(int j=n-1;j>=0;--j){
        dp[0][j] = grid[m][j] + dp[0][j+1];
    }
    int cur = 1,old = 0;
    for(int i=m-1;i>=0;--i){
        for(int j=n;j>=0;--j){
            dp[cur][j] = grid[i][j] + min(dp[old][j],dp[cur][j+1]);
        }
        swap(old,cur);
    }
    return dp[old][0];
}

6.实现bool isMatch(string s, string p)函数,其s是一个字符串,p是一个仅有'.'和'*'语法的正则表达式,如果p能匹配s,返回true,否则返回false。

思路

首先设一个bool型dp数组,dpi表示s[0:i-1]和p[0:j-1]是否匹配。
显而易见的是dpi = i==0?true:false;
而dp0取决于pattern中是否都是a的形式,dp0肯定是false,i>1时,dp0 = p[j-1]==''&&dp0==true;

对于一般情况,我们要注意的是p的最后一个字符是不是'*',因为'.'没有讨论的必要,它可以匹配任何字符。
如果p[j-1]不是'*',那么dpi仅仅取决于p[j-1]是否能和s[i-1]匹配,并且dpi-1需要是能匹配空字符串的格式;
如果p[j-1]是'',又可以分成两种情况,一种是p最后的x不匹配字符,另一种是x*匹配一个字符,并且dpi-1是true的。

状态转移方程

dp[i][0] = i==0?true:false;
dp[0][i] = i==1?false:(p[i-1]=='*' && dp[0][i-2]);
dp[i][j] = p[j-1]=='*'?(dp[i][j-2] || (s[i-1]==p[j-2] || p[j-2]=='.') && dp[i-1][j]):(dp[i][j] = (s[i-1]==p[j-1] || p[j-1]=='.') && dp[i-1][j-1]);

代码

bool isMatch(string s, string p) {
    int m = s.size(),n = p.size();
    bool dp[m+1][n+1];
    memset(dp,0,sizeof dp);
    dp[0][0] = true;
    for(int i=2;i<=n;++i){
        dp[0][i] = p[i-1]=='*' && dp[0][i-2];
    }
    for(int i=1;i<=m;++i){
        for(int j=1;j<=n;++j){
            if(p[j-1]=='*'){
                dp[i][j] = dp[i][j-2] || (s[i-1]==p[j-2] || p[j-2]=='.') && dp[i-1][j];
            }else{
                dp[i][j] = (s[i-1]==p[j-1] || p[j-1]=='.') && dp[i-1][j-1];
            }
        }
    }
    return dp[m][n];   
}

chnmagnus
71 声望15 粉丝

于代码中打滚卖萌ing


引用和评论

0 条评论