这次的总结比以前明了多了
115. 不同的子序列力扣
给你两个字符串 s 和 t ,统计并返回在 s 的 子序列 中 t 出现的个数。
题目数据保证答案符合 32 位带符号整数范围。
示例 1:
输入:s = "rabbbit", t = "rabbit"
输出:3
解释:
如下所示, 有 3 种可以从 s 中得到 "rabbit" 的方案。
rabbbit
rabbbit
rabbbit
示例 2:输入:s = "babgbag", t = "bag"
输出:5
解释:
如下所示, 有 5 种可以从 s 中得到 "bag" 的方案。
babgbag
babgbag
babgbag
babgbag
babgbag
这题的思路其实也是选和不选的模型,我们这样看,我现在有一个下标i和一个下标j,这两个下标一开始都是0,并且分别指向s和t,进行匹配字符。
假如s[i] == t[j] 那么我们就两个选择,一个是两个都选那么就转移到了上一个状态也就是i - 1 和 j - 1,二是不选i,为什么是不选i而不是不选j,因为你是从s中凑出t,所以对于s的字符你是有选择性的,所以转移的状态就是i - 1 和 j。
假如s[i] != t[j] 那么直接返回i - 1和 j状态,因为i这个字符不能选。
我们先创建一个二维数组,dp[i][j]表示s中前i个和t中前j个有多少能匹配的。
我们再用二维数组去解释以下上面为什么是转移i,而不是转移j
如果图解不理解,可以自己尝试填充数据就懂了
因为你的i用不上,那么你的值从哪里来呢,只能从上一行继承过来。
前面都搞定了,现在就要解决边界问题,我们可以很清楚得到dp[i][0] = 1, dp[0][j] = 0
class Solution
{
public:
int numDistinct(string s, string t)
{
int n = s.size(), m = t.size();
if(n < m) return 0;
vector<vector<unsigned long long>> dp(n + 1, vector<unsigned long long>(m + 1, 0));
for(int i = 0; i <= n; ++i) dp[i][0] = 1;
for(int i = 1; i <= n; ++i)
for(int j = 1; j <= m; ++j)
{
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[n][m];
}
};
这个题还可以优化空间,我们观察得到在这个状态转移方程里面,我们需要左上角数据,上方数据,那么上面的数据很好解决,直接转移就行,而左上的数据需要我们用一个临时变量去记录。
class Solution
{
public:
int numDistinct(string s, string t)
{
int n = s.size(), m = t.size();
if(n < m) return 0;
vector<unsigned long long> dp(m + 1, 0);
for(int i = 1; i <= n; ++i)
{
dp[0] = 1;
int pre = dp[0];
for(int j = 1; j <= m; ++j)
{
int temp = dp[j];
if(s[i - 1] == t[j - 1]) dp[j] = pre + dp[j];
pre = temp;
}
}
return dp[m];
}
};
516. 最长回文子序列力扣
给你一个字符串 s ,找出其中最长的回文子序列,并返回该序列的长度。
子序列定义为:不改变剩余字符顺序的情况下,删除某些字符或者不删除任何字符形成的一个序列。
示例 1:
输入:s = "bbbab"
输出:4
解释:一个可能的最长回文子序列为 "bbbb" 。
示例 2:输入:s = "cbbd"
输出:2
解释:一个可能的最长回文子序列为 "bb" 。
这个题首先用一个中心扩散也可以完成,就是枚举每一下标为中心点或者两个中心点的其中一个,然后用dfs去暴力搜索就行了。但是这里用动态规划更好解决,但是其实动态规划都可以用dfs实现,动态规划就是只归不递,这里就用这个思想,因为递归递进去的时候是举例中心点,所以我们归的时候就是从两边回来。
class Solution
{
public:
int longestPalindromeSubseq(string s)
{
int n = s.size();
vector<vector<int>> dp(n, vector<int>(n, 0));
for(int i = n - 1; i >= 0; --i)
{
dp[i][i] = 1;
for(int j = i + 1; j < n; ++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][n - 1];
}
};
// 两个不同的方向
class Solution
{
public:
int longestPalindromeSubseq(string s)
{
int n = s.size();
vector<vector<int>> dp(n, vector<int>(n, 0));
for(int i = 0; i < n; ++i)
{
dp[i][i] = 1;
for(int j = i - 1; j >= 0; --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[n - 1][0];
}
};
一维空间,和上一个不同的是,这里需要的是右上角或者左下角,下面这种就是需要右上角
class Solution
{
public:
int longestPalindromeSubseq(string s)
{
int n = s.size();
vector<int> dp(n, 0);
for(int i = 0; i < n; ++i)
{
dp[i] = 1;
int pre = 0;
for(int j = i - 1; j >= 0; --j)
{
int temp = dp[j];
if(s[i] == s[j]) dp[j] = pre + 2;
else dp[j] = max(dp[j], dp[j + 1]);
pre = temp;
}
}
return dp[0];
}
};
131. 分割回文串力扣
给你一个字符串 s,请你将 s 分割成一些子串,使每个子串都是 回文串 。返回 s 所有可能的分割方案。
回文串 是正着读和反着读都一样的字符串。
示例 1:
输入:s = "aab"
输出:[["a","a","b"],["aa","b"]]
示例 2:输入:s = "a"
输出:[["a"]]
这题明显的组合形式的划分,所以这里用dfs,其实就是选和不选的问题,只不过我们选之前得知道我们选的是不是回文字符串,所以我们还需要一个二维数组去记录是不是回文字符串,为什么是二维,因为dp[i][j] 这个代表i -- j是不是字符串。那么这里思路清楚了,首先用一个二维数组去记录是不是回文数字符串,然后再dfs搜索。
class Solution
{
public:
vector<vector<string>> ret;
vector<string> ans;
vector<vector<bool>> dp;
int n;
vector<vector<string>> partition(string s)
{
n = s.size();
dp = vector<vector<bool>> (n, vector<bool>(n, true));
for(int i = n - 1; i >= 0; --i)
for(int j = i + 1; j < n; ++j)
dp[i][j] = (s[i] == s[j]) && dp[i + 1][j - 1];
dfs(s, 0);
return ret;
}
void dfs(const string& s, int i)
{
if(i == n)
{
ret.emplace_back(ans);
return;
}
for(int j = i; j < n; ++j)
{
if(dp[i][j])
{
ans.emplace_back(s.substr(i, j - i + 1));
dfs(s, j + 1);
ans.pop_back();
}
}
}
};
132. 分割回文串 II力扣
给你一个字符串 s,请你将 s 分割成一些子串,使每个子串都是回文。
返回符合要求的 最少分割次数 。
示例 1:
输入:s = "aab"
输出:1
解释:只需一次分割就可将 s 分割成 ["aa","b"] 这样两个回文子串。
示例 2:输入:s = "a"
输出:0
示例 3:输入:s = "ab"
输出:1
这个题档次一下子就上来了, 如果用上面那个代码,然后遍历每一行的长度取最小值的话会超时,所以不能使用上述代码,其实这里不需要考虑具体是怎么分,我只需要知道分了多少次就行,这个感觉有点像背包问题,都是从前面的所有状态中找到一种最适合的情况转移过来。看代码就理解了。
class Solution
{
public:
int minCut(string s)
{
int n = s.size();
vector<vector<bool>> dp(n, vector<bool>(n, true));
for(int i = n - 1; i >= 0; --i)
for(int j = i + 1; j < n; ++j)
dp[i][j] = (s[i] == s[j]) && dp[i + 1][j - 1];
vector<int> f(n, INT_MAX);
for(int i = 0; i < n; ++i)
if(dp[0][i])
{
f[i] = 0;
}
else
// 归根究底还是状态转移
// 想了想为什么dp[j + 1][i]可以确保dp[0][j]中可以分割字符串,因为这个里是循环上来的
// 所以f[j]肯定也早就受到了前面的影响
// 所以还是一个状态转移,只能说太妙了
{
for(int j = 0; j < i; ++j)
if(dp[j + 1][i]) f[i] = min(f[i], f[j] + 1);
}
return f[n - 1];
}
};
总结:我只能说太妙了,这些题是几天前写的,当时还是有点懵懵懂懂的,直到今天我看到了灵神的一句话,动态规划就是只归不递,恍然大悟,其实归根究底动态规划就是dfs,每一个状态都对应dfs的每一个状态,所以把握住dfs就离动态规划不远了。其实我现在感觉就是先确定dfs然后可以反推dp,然后再确定边界,如果熟练可以跳过dfs阶段,直接写状态方程,然后再确定边界。