频率很高的笔试题--动态规划类型(下)

题目1:不同的二叉搜索树

给定一个整数 n,求以 1 … n 为节点组成的二叉搜索树有多少种?

示例:
输入: 3
输出: 5
解释:
给定 n = 3, 一共有 5 种不同结构的二叉搜索树:

   1         3     3      2      1
    \       /     /      / \      \
     3     2     1      1   3      2
    /     /       \                 \
   2     1         2                 3

解题思路:动态规划,当 n = 1的时候有 1 中,等于 2 的时候有两种,分别是以 1 为根节点和以 2 为根节点;

dp[i] 表示节点数为 i 的搜索二叉树有 dp[i] 种

dp[i] = dp[j-1] * dp[i-j] (j为当前的根节点)

dp[j-1] 为左子树的种类,dp[i-j] 为右子树的种类

C++ 参考代码

class Solution {
public:
    int numTrees(int n) {
        vector<int>dp(n+1,0);
        dp[0] = 1,dp[1] = 1;
        // 当前节点总数
        for(int i = 2;i<n+1;++i){
            // 以 j 为根节点
            for(int j =1;j<i+1;++j){
                // 左节点 * 右节点数
                dp[i] += dp[j-1]*dp[i-j];
            }
        }
        return dp[n];
    }
};

题目2:不同的子序列

给定一个字符串 S 和一个字符串 T,计算在 S 的子序列中 T 出现的个数。

一个字符串的一个子序列是指,通过删除一些(也可以不删除)字符且不干扰剩余字符相对位置所组成的新字符串。(例如,“ACE” 是 “ABCDE” 的一个子序列,而 “AEC” 不是)

示例 1:
输入: S = “rabbbit”, T = “rabbit”
输出: 3
解释:

如下图所示, 有 3 种可以从 S 中得到 “rabbit” 的方案
(上箭头符号 ^ 表示选取的字母)

rabbbit
^^^^ ^^
rabbbit
^^ ^^^^
rabbbit
^^^ ^^^

解题思路:动态规划

步骤1:初始化,当字符串 T 字符串为空的时候,因为空串是任何字符串的一个子集,所以在 S 字符串出现的次数是 1

dp[i][j] 代表 S [0-i] 字符串可以由 T[0-j] 字符串组成最多个数

步骤2:动态转义方程

在这里插入图片描述
如果 T[j-1] == S[i-1] 的时候,需要检测 T 的前一个匹配数量 + T 当前的匹配数量

在这里插入图片描述
如果 j > i 代表 T 的长度大于 S ,则也没有必要寻找了,因为肯定找不到,给 dp[i][j] 赋值为 0 或者直接跳过
在这里插入图片描述
C++ 参考代码

class Solution {
public:
    int numDistinct(string s, string t) {
        int m = s.size();
        int n = t.size();
        vector<vector<long>>dp(m+1,vector<long>(n+1));
        for(int i = 0;i<=m;++i){
            dp[i][0] = 1;
        }
        // 如果 S 为空字符串的话, 让 T 字符串在 S 中寻找,
        // 肯定是找不到的,所以应该初始化为0,我们在定义的时候
        // 已经初始化为0了,所以这一步我们不操作

        // dp[i][j] 代表 S [0-i] 字符串可以由 T[0-j] 字符串
        for(int i = 1;i<=m;++i){
            for(int j = 1;j<=n;++j){
                // 没必要在找了,因为 T 的长度大于 S ,肯定找不到
                if(j>i) continue;
                
                // 下面是最难找的动态方程,用图来说明一下
                if(s[i-1] == t[j-1]){
                    dp[i][j] = dp[i-1][j-1] + dp[i-1][j];
                }else{
                    dp[i][j] = dp[i-1][j]; 
                }
            }
        }
        return dp[m][n];
    }
};

题目3:交错字符串

给定三个字符串 s1, s2, s3, 验证 s3 是否是由 s1 和 s2 交错组成的

示例 1:
输入: s1 = “aabcc”, s2 = “dbbca”, s3 = “aadbbcbcac”
输出: true

示例 2:
输入: s1 = “aabcc”, s2 = “dbbca”, s3 = “aadbbbaccc”
输出: false

解题思路:动态规划,s1 和 s2 的某个前缀是否组成 s3 的前缀

dp[i][j] 代表 s1 的前 i 个字符和 s2 的前 j 个字符是否组成 s3 的前 k 个字符(k = i + j + 1)

初始化,如果 s2 为空串,则要满足题必须 s1[i] == s3[i],同理若 s1 为空串,则 s2[j] == s3[j]

在这里插入图片描述

class Solution {
public:
    bool isInterleave(string s1, string s2, string s3) {
        if(s1.size() + s2.size() != s3.size()){
            return false;
        }
        int n1 = s1.size();
        int n2 = s2.size();
        vector<vector<bool>>dp(n1+1,vector<bool>(n2+1));
        // dp[i][j] 代表 s1[0~i-1] 和 s2[0~j-1]可以交错为s3[0~i+j-1]
        dp[0][0] = true;
        // 如果 s2 为空串,则要想可以交错成功
        // 下标对应的字符必须相等,也就是 s1[i] == s2[i]
        // 只要有一个字符不相等,就无法成功
        for(int i = 1;i<=n1;++i){
            dp[i][0] = dp[i-1][0] && (s1[i-1] == s3[i-1]);
        }
        // s1 同上
        for(int i = 1;i<=n2;++i){
            dp[0][i] = dp[0][i-1] && (s2[i-1] == s3[i-1]);
        }
        for(int i = 1;i<=n1;++i){
            for(int j = 1;j<=n2;++j){
                dp[i][j] = (dp[i-1][j] && s1[i-1] ==s3[i-1+j])                  || (dp[i][j-1] && s3[i-1+j] ==s2[j-1]);
            }
        }  
        return dp[n1][n2];
    }
};

题目4:分割回文串

给定一个字符串 s,将 s 分割成一些子串,使每个子串都是回文串。

返回符合要求的最少分割次数。

示例:
输入: “aab”
输出: 1
解释: 进行一次分割就可将 s 分割成 [“aa”,“b”] 这样两个回文子串

解题思路:动态规划

dp[i] 代表到第 i 个字符为止,最少切割的次数
如果我们可以保证第 j 到 i-1 的字符是回文串,那么只要再切割一次就可以保证 j 到 i 个字符是回文串

初始化最多的切割次数 dp[i] = i - 1 ,例如有 5 个字符,最多需要 4 次切割

方程为:dp[i] = min(dp[i],d[j]+1)

判断第 j 到 i-1 是否是回文串,也可以使用动态规划的方式去判断,从后往前遍历

情况1:如果 j == i ,则代表的是一个字符,那么它一定是回文串

情况2:如果 j == i+1 ,表示 j 到 i +1 的字符,如果字符 s[i+1] == s[j] 相等,那么就是一个回文串,否则不是

情况3:如果 s[i] == s[j],如果去掉首位字符它依然是回文串,那么第 j 到 i 的字符串就是回文串

C++ 参考代码

class Solution {
public:
    int minCut(string s) {
        if(s.empty()) return 0;
		int n = s.size();
        vector<int>dp(n+1,0);
        // dp[i] 表示到第 i 个字符需要的最小分割次数
        for(int i = 0;i<=n;++i){
            dp[i] = i-1;
        } 
        // 这里已经判断好了第 j 到 i-1 个字符是否是回文串
        vector<vector<bool>>mat = ispalindrome(s);
        for(int i = 1;i<=n;++i){
            for(int j = 0;j<i;++j){
                // dp(i) = min(dp(i),1+dp(j)) 
                if(mat[j][i-1]){
                    dp[i] = min(dp[i],1+dp[j]);
                }
            }
        }
		return dp[n];
    }
    // 判断标记第 j 到 i-1 是否是回文串
    vector<vector<bool>>ispalindrome(string s){
        int len = s.size();
        vector<vector<bool>>mat(len,vector<bool>(len,false));
        for(int i = len-1;i>=0;--i){
            for(int j = i;j<len;++j){
                if(j == i){
                    mat[i][j] = true;
                }
                else if(j == i+1){
                    mat[i][j] = (s[i]==s[j]);
                }else{
                    mat[i][j] = ((s[i]==s[j]) && mat[i+1][j-1]);
                }
            }
        }
        return mat;
    }
};

写法二:

class Solution {
public:
    int minCut(string s) {
		int n = s.size();
		vector<vector<bool>>boolean(n,vector<bool>(n));
        vector<int>dp(n); 
        // dp[i]表示s中第i个字符到第(n-1)个字符
        // 所构成的子串的最小分割次数
		for (int i = n - 1; i >= 0; i--) {
			dp[i] = INT_MAX;
			for (int j = i; j < n; j++) {
				/// 满足这个条件,说明 i 到 j 为回文串
				if (s[i] == s[j] && (j - i <= 1 || boolean[i + 1][j - 1])) {
					boolean[i][j] = true;
					// 寻找最小的切割点 j 对于的次数
					if (j + 1 < n) {
						dp[i] = min(dp[i], 1 + dp[j + 1]);
					}else{
						// j == n-1 的时候,说明整个字符串是一个回文串
						dp[i] = 0;
					}
				}
			}
		}
		return dp[0];
    }
};

题目5:地下城游戏

一些恶魔抓住了公主(P)并将她关在了地下城的右下角。地下城是由 M x N 个房间组成的二维网格。我们英勇的骑士(K)最初被安置在左上角的房间里,他必须穿过地下城并通过对抗恶魔来拯救公主

骑士的初始健康点数为一个正整数。如果他的健康点数在某一时刻降至 0 或以下,他会立即死亡

在这里插入图片描述有些房间由恶魔守卫,因此骑士在进入这些房间时会失去健康点数(若房间里的值为负整数,则表示骑士将损失健康点数);其他房间要么是空的(房间里的值为 0),要么包含增加骑士健康点数的魔法球(若房间里的值为正整数,则表示骑士将增加健康点数)

为了尽快到达公主,骑士决定每次只向右或向下移动一步

解题思路:动态规划,从右到左,从下当上,定义 dp[i][j] 为勇士到达点(i,j) 的最小生命值

class Solution {
public:
    int calculateMinimumHP(vector<vector<int>>& dungeon) {
        int n = dungeon.size();
        int m = dungeon[0].size();
        // dp[i][j] 表示到达(i,j) 位置最小生命值
        vector<vector<int>> dp(n, vector<int>(m));         
        for (int i = n-1; i >= 0; --i) {
            for (int j = m-1; j >= 0; --j) {
                if(i == n-1 && j==m-1){
                    dp[i][j] = max(1,1-dungeon[i][j]);
                }
                else if(i== n-1){
                    dp[i][j] = max(1,dp[i][j+1]-dungeon[i][j]);
                }
                else if(j == m-1){
                    dp[i][j] = max(1,dp[i+1][j]-dungeon[i][j]);
                }else{
                    dp[i][j] = max(1,min(dp[i+1][j],dp[i][j+1])-dungeon[i][j]);
                }
            }
        }
        return dp[0][0];
    }
};

题目6:最长回文字符串

给定一个字符串 s,找到 s 中最长的回文子串。你可以假设 s 的最大长度为 1000

示例1
输入: “babad”
输出: “bab”
注意: “aba” 也是一个有效答案。

示例2
输入: “cbbd”
输出: “bb”

解题思路:遍历字符串,以每个字符和以每相邻两字符作为中心,搜索以其为中心的回文串长度,保存当前最长回文子串,中心扩展法判断是否符合回文要求

python 参考代码

def longestPalindrome(self, s):
    def expand(s, left, right):
        while left>=0 and right<len(s) and s[left] == s[right]:
            left -= 1
            right += 1
        return right-left-1
    start = 0
    end = 0
    for i in range(len(s)):
        len1 = expand(s, i, i)
        len2 = expand(s, i, i+1)
        max_len = max(len1, len2)
        if max_len > end - start:
            start = i - (max_len-1)/2
            end = i + max_len/2
    return s[start:end+1]

C++ 参考代码

class Solution {
public:
    int expend(string& s,int start,int end){
        while(start >=0 && end < s.size()
            && s[start]==s[end]){
            --start;
            ++end;
        }
        return end-start-1;
    }
    string longestPalindrome(string s) {
        int max_len = 0,start = 0;
        for(int i = 0;i<s.size();++i){
            int len1 = expend(s,i,i);
            // 为什么需要有下面的判断,主要针对的是这种案列
            // a b b a,那么需要这种 i , i + i 作为中心扩展
            int len2 = expend(s,i,i+1);
            int temp = max(len1,len2);
            
            // ... a b b b b a ... 
            // ... a b b b a ... 
            // 如果 i 指向第 2 个 b 那么对于的 start 和 end 
            // 计算的方式是一样的
            if(temp > max_len){
                max_len = temp;
                start = i - (max_len-1)/2;
            }
        }
        // 第一个参数是起始下标,第二个参数是返回元素的个数
        return s.substr(start,max_len);
    }
};

方法二:Manacher 算法

题目7:最长有效括号

给定一个只包含 ‘(’ 和 ‘)’ 的字符串,找出最长的包含有效括号的子串的长度

示例 1
输入: “(()”
输出: 2
解释: 最长有效括号子串为 “()”
示例 2:
输入: “)()())”
输出: 4
解释: 最长有效括号子串为 “()()”

方法一:动态规划
初始化数组为大小为 0 ,dp[i] 代表的是当前 i 结尾的最长字符串

j = i - dp[i-1] - 1; // 找出前面没有被匹配的 ( 字符
dp[i] = i - j + 1;  // 当前匹配有效括号长度
dp[i] += j-1>=0 ? dp[j-1]:0; // 再加上前面 j - 1为结尾匹配的有效括号长度

总结起来动态规划方程

dp[i] = d[i-1] + 2 + (j-1 >= 0 ? dp[j-1]:0);

C++ 参考代码

class Solution {
public:
    int longestValidParentheses(string s) {
        int ans = 0;
        vector<int>dp(s.size(),0);
        // dp[i] 代表的是当前下标为 i 结尾的最长括号 
        for(int i = 1;i<s.size();++i){
            if(s[i] == ')'){
                // 找到前面没有被匹配的 '('
                int j = i - dp[i-1] - 1;
                if(j >= 0 && s[j] == '('){
                    dp[i] = i-j+1;
                    if(j-1 >= 0){
                        dp[i] += dp[j-1];
                    }
                }
            }
            ans = max(ans,dp[i]);
        }
        return ans;
    }
};

方法二:利用一个栈结构

解题思路:创建一个栈用来存放左括号在字符串中的下标,每当遇到右括号,则栈顶元素出栈,再用右括号的下标减此时栈顶元素的下标作为到现在为止最长有效括号数。当栈顶元素出栈后栈为空时,则直接将右括号下标入栈。一开始将 -1入栈,确保栈中始终至少有一个元素

C++ 参考代码

class Solution {
public:
    int longestValidParentheses(string s) {
        int ans = 0;
        if(s.empty()) return ans;
        stack<int>sta;
        sta.push(-1); 
        for(int i = 0;i<s.size();++i){
            if(s[i] == '('){
                sta.push(i);
            }else if(s[i] == ')'){
                sta.pop();
                if(!sta.empty()){
                    ans = max(ans,i-sta.top());
                }else{
                    sta.push(i);
                }
            }
        }
        return ans;
    }
};

方法三:直接使用两个变量来代替左右括号

题目8:通配符匹配

给定一个字符串 (s) 和一个字符模式 § ,实现一个支持 ‘?’ 和 ‘*’ 的通配符匹配。

‘?’ 可以匹配任何单个字符。
‘*’ 可以匹配任意字符串(包括空字符串)。
两个字符串完全匹配才算匹配成功。

说明:

s 可能为空,且只包含从 a-z 的小写字母。
p 可能为空,且只包含从 a-z 的小写字母,以及字符 ? 和 *

示例 1:
输入:
s = “aa”
p = " * "
输出: true
解释: ’ * ’ 可以匹配任意字符串

示例 2
输入:
s = “cb”
p = “?a”
输出: false
解释: ‘?’ 可以匹配 ‘c’, 但第二个 ‘a’ 无法匹配 ‘b’

解题思路:动态规划

dp [ i ] [ j ] 表示 从 s[ i - 1 ] 到 p [ j - 1 ] 字符已经匹配成功的字符串

(1)当 p[ j-1] 为 * 字符时

在这里插入图片描述

if(p[j-1] == '*'){
	dp[i][j] = dp[i][j-1] || dp[i-1][j-1] || dp[i-1][j] 
}

(2)当 p [ j - 1 ] 为 ’ ?’ 符号的时候
在这里插入图片描述

if(p[j-1] == '?' || s[i-1] == p[j-1]){
	dp[i][j] = dp[i-1][j-1];
}
class Solution {
public:
    bool isMatch(string s, string p) {
        vector<vector<int>>dp(s.size()+1,vector<int>(p.size()+1,0));
        //dp[i][j] 表示 s[i-1] 到 p[j-1]为止的字符串已经匹配ok
        //首先要初始化 dp[0][0] ,当 s 为空且 p 为空的时候匹配成功
        dp[0][0] = 1;
        //初始化 dp[0][i],当 s 为空的时候,看 p 是否全是 *
        for(int i = 1;i<=p.size();++i){
            dp[0][i] = dp[0][i-1] && p[i-1] == '*';
        }
        for(int i = 1;i<=s.size();++i){
            for(int j = 1;j<=p.size();++j){
                // 如果 p[j-1] == '*' 的时候
                // 下列三种情况都可以成功匹配
                if(p[j-1] == '*'){
                    dp[i][j] = dp[i-1][j] || dp[i-1][j-1] || dp[i][j-1];
                }
                // p[j-1] == ? 可以匹配到任意字符,所以相当于
                // p[j-1] == s[i-1]
                else if(p[j-1] == '?' || p[j-1] == s[i-1]){
                    dp[i][j] = dp[i-1][j-1];
                }
            }
        }
        return dp[s.size()][p.size()];
    }
};

题目9:编辑距离

给定两个单词 word1 和 word2,计算出将 word1 转换成 word2 所使用的最少操作数

你可以对一个单词进行如下三种操作:

插入一个字符
删除一个字符
替换一个字符

示例 1:
输入: word1 = “horse”, word2 = “ros”
输出: 3
解释:
horse -> rorse (将 ‘h’ 替换为 ‘r’)
rorse -> rose (删除 ‘r’)
rose -> ros (删除 ‘e’)

解题思路:动态规划,初始化一个二维数组

定义:dp[i][j] 代表 以 i 结尾的字符串 word1 到以 j 结尾的字符串 word2 的最少转换次数

初始化:如果 word1 或者 word2 为空,则转换的次数为 word2 的长度或者 word1 的长度

状态方程
dp[i][j] = word1[i-1] == word2[j-1] ? dp[i-1][j-1] : min(dp[i-1][j-1],dp[i-1][j],dp[i][j-1])+1

如果第 i-1 的 word1 和第 j-1 的 word2 字符相等,则次数不变

如果不同则如下图:
在这里插入图片描述

C++ 参考代码

class Solution {
public:
    int minDistance(string word1, string word2) {
        int m = word1.size();
        int n = word2.size();
        vector<vector<int>>dp(m+1,vector<int>(n+1));
        // dp[i][j] 代表以 i 结尾的 word1 字符串到
        // 以 j 结尾的 word2 字符串需要转换的最少次数
        for(int i = 0;i<=m;++i){
            dp[i][0] = i;
        }
        for(int j = 0;j<=n;++j){
            dp[0][j] = j;
        }
        for(int i = 1;i<=m;++i){
            for(int j = 1;j<=n;++j){
                // 如果相等则不需要转换
                if(word1[i-1] == word2[j-1]){
                    dp[i][j] = dp[i-1][j-1]; 
                }else{
                    // 如果不相等则选择一个最小转换的 次数 + 1
                    dp[i][j] = min(dp[i-1][j-1],\
                    min(dp[i-1][j],dp[i][j-1]))+1;
                }
            }
        }
        return dp[m][n];
    }
};

题目10:最大矩形

给定一个仅包含 0 和 1 的二维二进制矩阵,找出只包含 1 的最大矩形,并返回其面积

示例:
输入:
[
[“1”,“0”,“1”,“0”,“0”],
[“1”,“0”,“1”,“1”,“1”],
[“1”,“1”,“1”,“1”,“1”],
[“1”,“0”,“0”,“1”,“0”]
]
输出: 6

解题思路1:动态规划
计算高度,左右边界,则面积 = 高度 * (右边界 - 左边界)
每一行的高度,左右边界都和上一行有关系

解题思路2:单调栈,可以对题目进行转换,求出前 i 行的最大矩形(i = 1,2,3,… n) n 为二进制矩阵的行数

在这里插入图片描述

单调栈的思路

通过一次遍历就可以知道哪个矩阵最大,如果当前矩阵的高度大于栈顶元素,则入栈,如果小于栈顶元素则先弹出顶元素,然后计算出以栈顶元素为高,宽度等于 i - top - 1 的矩阵面积,每次更新最大面积即可

C++ 参考代码

class Solution {
public:
    int largest(vector<int> heights){
        heights.push_back(0);
        stack<int>s;
        int res = 0;
        for(int i = 0;i<heights.size();++i){
            while(!s.empty() && heights[s.top()]>heights[i]){
                int top = s.top();
                s.pop();
                int weith = s.empty()?i:(i-s.top()-1);
                res = max(res,weith*heights[top]);
            }
            s.push(i);
        }
        return res;
    }
    int maximalRectangle(vector<vector<char>>& matrix) {
        if(matrix.empty()) return 0;
        int rows = matrix.size();
        int cols = matrix[0].size();
        vector<int>dp(cols);
        int res = 0;
        for(int i = 0;i<rows;++i){
            for(int j = 0;j<cols;++j){
                if(matrix[i][j] == '1'){
                    dp[j] +=1;
                }else{
                    dp[j] = 0;
                }
            }
            res = max(res,largest(dp));
        }
        return res;
    }
};
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序猿的温柔香

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值