跟着Carl学算法--动态规划【7】
判断子序列
力扣链接:判断子序列
题目:给定字符串 s 和 t ,判断 s 是否为 t 的子序列。
字符串的一个子序列是原始字符串删除一些(也可以不删除)字符而不改变剩余字符相对位置形成的新字符串。(例如,"ace"
是"abcde"
的一个子序列,而"aec"
不是)。
进阶:
如果有大量输入的 S,称作 S1, S2, … , Sk 其中 k >= 10亿,你需要依次检查它们是否为 T 的子序列。在这种情况下,你会怎样改变代码?
思路:
S中是否含有T这样的子序列
可以使用公共最长子序列的模板,最后判断s的长度是否等于s和t的最长公共子序列长度,
也可以进行优化,因为是判断s是否是t的子序列,即要判断的是s的元素是否在t种按照相对顺序出现过,所以当出现两者不同时,肯定是回退t,木桶效应,回退s肯定小于等于回退t的,因为s长度小于t长度。
所以递推式:
if (s[i - 1] == t[j - 1])dp[i][j] = dp[i - 1][j - 1] + 1;
elsedp[i][j] = dp[i][j - 1];
代码:
class Solution {
public:bool isSubsequence(string s, string t) {vector<vector<int>> dp(s.size() + 1, vector(t.size() + 1, 0));for (int i = 1; i <= s.size(); i++)for (int j = 1; j <= t.size(); j++)if (s[i - 1] == t[j - 1])dp[i][j] = dp[i - 1][j - 1] + 1;elsedp[i][j] = dp[i][j - 1];return dp[s.size()][t.size()] == s.size();}
};
不同的子序列
力扣链接:不同的子序列
题目:给你两个字符串 s
和 t
,统计并返回在 s
的 子序列 中 t
出现的个数。
思路:
S中含有多少个T这样的子序列
-
dp[i][j]
:代表s[0] ~ s[i - 1] 中有几个 t[0] ~ t[j - 1] -
递推式:
if (s[i - 1] == t[j - 1])dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j]; elsedp[i][j] = dp[i - 1][j];
始终是从当前t的位置开始匹配
- 如果t当前位置与s当前位置的字符相同,
- 那如果使用s当前位置的字符,那不同的子序列数就取决于双方前一个状态了,即s[0] ~ s[i - 1] 中有几个 t[0] ~ t[j - 1]
- 如果不使用s当前位置的字符,那就需要对s进行回退了,就继续在s[0] ~ s[i - 1] 中查找有几个 t[0] ~ t[j]这样的字符串
- 如果t当前位置与s当前位置的字符不相同,那肯定就直接在s[0] ~ s[i - 1] 中查找有几个 t[0] ~ t[j]这样的字符串
- 如果t当前位置与s当前位置的字符相同,
-
初始化:
dp[i][0]
代表在s[0]到s[i]之间的字符串有多少个空字符串(t[-1]到t[0]),1个,即将s所有字符删完,dp[0][j]
代表在空字符串(t[-1]到t[0])之间有多少个t[0]到t[i]这样的字符串,0个dp[0][0]
空字符串中有多少个空字符串?1个
class Solution {
public:int numDistinct(string s, string t) {vector<vector<unsigned>> dp(s.size() + 1, vector<unsigned>(t.size() + 1, 0));for (int i = 0; i <= s.size(); i++)dp[i][0] = 1;for (int i = 1; i <= s.size(); i++)for (int j = 1; j <= t.size(); j++)if (s[i - 1] == t[j - 1])dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j];elsedp[i][j] = dp[i - 1][j];return dp[s.size()][t.size()];}
};
两个字符串的删除操作
力扣链接:两个字符串的删除操作
题目:给定两个单词 word1
和 word2
,返回使得 word1
和 word2
相同所需的最小步数。每步 可以删除任意一个字符串中的一个字符。
思路:
-
dp[i][j]
:代表word1[0]到word1[i-1]部分和word2[0]到word2[j-1]部分,两部分字符串相同需要删除的最少字符 -
递推式:
if (word1[i - 1] == word2[j - 1]) {dp[i][j] = dp[i - 1][j - 1]; } elsedp[i][j] = min(dp[i - 1][j] + 1, dp[i][j - 1] + 1);
-
如果两个字符串的当前位置字符相同,那就不需要删除此处了,最少删除次数就是,
word1[0]到word1[i-1]和word2[0]到word2[j-1],这两部分相同锁删除的最少字符
-
如果两个字符串当前位置的字符不同,那就可以删除word1当前位置,或者word2当前位置的字符,或者两者都删除(这种其实已经被前两种包含了,比如删除word1当前位置后,如果还是和word2位置的字符不相等,那就会再删除word2或者word1当前位置)
-
-
初始化:
dp[i][0]
代表word1[0]到word1[i-1]部分与空字符串(word2[0]到word2[-1])相等需要删除的字符数,那就是i(索引i-1到0之间的字符数)了,同理dp[0][j]
也是如此,其他部分是由左和上方推导出来的,每一步都是直接赋值,所以赋任意值均可。
class Solution {
public:int minDistance(string word1, string word2) {vector<vector<int>> dp(word1.size() + 1, vector(word2.size() + 1, 0));for (int i = 0; i <= word1.size(); i++)dp[i][0] = i;for (int j = 0; j <= word2.size(); j++)dp[0][j] = j;for (int i = 1; i <= word1.size(); i++)for (int j = 1; j <= word2.size(); j++) {if (word1[i - 1] == word2[j - 1]) {dp[i][j] = dp[i - 1][j - 1];} elsedp[i][j] = min(dp[i - 1][j] + 1, dp[i][j - 1] + 1);}return dp[word1.size()][word2.size()];}
};
也可以使用公共最长子序列的模板,求两个字符串的最长公共子序列,然后两个字符串分别减去公共最长子序列的长度就是各自要删除的长度,加起来即可。
编辑距离
力扣链接:编辑距离
题目:给你两个单词 word1
和 word2
, 请返回将 word1
转换成 word2
所使用的最少操作数 。
你可以对一个单词进行如下三种操作:插入一个字符、删除一个字符、替换一个字符
思路:
比较字符串时始终都是从当前位置往前比较的
删除、替换、插入这三个操作,其实使用插入和删除的操作次数和效果是一样的,比如删除word1当前位置,其实就是相当于在word2当前位置之后插入与word1相同的字符,使得能比较word1的前一个位置。
-
dp[i][j]
代表代表word1[0]到word1[i-1]部分和word2[0]到word2[j-1]部分,两部分字符串相同需要进行的操作 -
递推式:
if (word1[i - 1] == word2[j - 1])dp[i][j] = dp[i - 1][j - 1]; elsedp[i][j] = min(dp[i][j - 1] + 1,min(dp[i - 1][j] + 1, dp[i - 1][j - 1] + 1));
-
如果两个字符串的当前位置字符相同,那就不需要操作此处了,最少操作次数就是,
word1[0]到word1[i-1]和word2[0]到word2[j-1],这两部分相同所操作的最少次数。
-
如果两个字符串当前位置的字符不同,那就可以
- 删除word1当前位置(或者word2当前位置的字符),就可以比较word1前一个位置的字符了,然后最少的操作次数就是从word1的0到当前位置的前一个位置和word2的0到当前位置的字符串两者相同所需的操作次数+1,即
dp[i-1][j] + 1
(或者删除word2对应的dp[i][j-1] + 1
) - 在word1当前位置之后插入word2当前位置的字符(或者word2当前位置的字符之后插入word1当前位置的字符),就可以比较word2前一个位置的字符了,然后最少的操作次数就是从word1的0到当前位置和word2的0到当前位置的的前一个位置字符串两者相同所需的操作次数+1,即
dp[i][j - 1] + 1
(或者删除word2对应的dp[i - 1][j] + 1
) - 替换就是强行让两者此处相等,所以可以直接跳过此处,比较两者前一个位置,然后操作次数+1,即
dp[i - 1][j - 1] + 1
- 删除word1当前位置(或者word2当前位置的字符),就可以比较word1前一个位置的字符了,然后最少的操作次数就是从word1的0到当前位置的前一个位置和word2的0到当前位置的字符串两者相同所需的操作次数+1,即
-
-
初始化:上一题一样,
dp[i][0]
代表word1[0]到word1[i-1]部分与空字符串(word2[0]到word2[-1])相等需要删除的字符数,那就是i(索引i-1到0之间的字符数)了,同理dp[0][j]
也是如此,其他部分是由左和上方推导出来的,每一步都是直接赋值,所以赋任意值均可。
class Solution {
public:int minDistance(string word1, string word2) {vector<vector<int>> dp(word1.size() + 1, vector(word2.size() + 1, 0));for (int i = 0; i <= word1.size(); i++)dp[i][0] = i;for (int j = 0; j <= word2.size(); j++)dp[0][j] = j;for (int i = 1; i <= word1.size(); i++)for (int j = 1; j <= word2.size(); j++)if (word1[i - 1] == word2[j - 1])dp[i][j] = dp[i - 1][j - 1];elsedp[i][j] = min(dp[i][j - 1] + 1,min(dp[i - 1][j] + 1, dp[i - 1][j - 1] + 1));return dp[word1.size()][word2.size()];}
};
回文子串
力扣链接:回文子串
题目:给你一个字符串 s
,请你统计并返回这个字符串中 回文子串 的数目。
回文字符串 是正着读和倒过来读一样的字符串。
子字符串 是字符串中的由连续字符组成的一个序列。
思路:
回文子串根据定义,需要从两边来往中间遍历,当一直相等时,就是回文串,
-
dp[i][j]
代表,以i为开头,j为结尾的子串是否是回文串 -
递推式:
if (j - i <= 1) {dp[i][j] = true;count = count + 1;} else if (dp[i + 1][j - 1]) {dp[i][j] = true;count = count + 1; }
当i和j位置处的字符串相等时,进一步判断,字符串长度为1或者2时显然就是回文串,当字符串长度大于2时,是否是回文串需要进一步判断,即以i+1开头j-1结尾的子串是否是回文串
-
初始化:全部初始化为false,如果不为回文串就不需要赋值了,只考虑是回文串的情况
-
遍历顺序:因为当前位置依赖于左下角位置,所以需要从左下角一直遍历到左上角,这样就能确保所依赖的值是已经计算过的,即i从大到小,j从小到大遍历。
class Solution {
public:int countSubstrings(string s) {vector<vector<bool>> dp(s.size(), vector(s.size(), false));int count = 0;for (int i = s.size()-1; i >=0 ; i--)for (int j = i; j < s.size(); j++) {if (s[i] == s[j]) {if (j - i <= 1) {dp[i][j] = true;count = count + 1;} else if (dp[i + 1][j - 1]) {dp[i][j] = true;count = count + 1;}}}return count;}
};
最长回文子序列
力扣链接:最长回文子序列
题目:给你一个字符串 s
,找出其中最长的回文子序列,并返回该序列的长度。
子序列定义为:不改变剩余字符顺序的情况下,删除某些字符或者不删除任何字符形成的一个序列。
思路:
回文子串根据定义,需要从两边来往中间遍历,当一直相等时,就是回文串,
-
dp[i][j]
代表,以i为开头,j为结尾的字符串的最长回文串子序列长度 -
递推式:
if (s[i] == s[j]) {dp[i][j] = dp[i + 1][j - 1] + 2; } elsedp[i][j] = max(dp[i + 1][j], dp[i][j - 1]);
-
当首尾字符相等时,就是i+1到j-1为字符串的最大回文子序列长度+2
-
当两者不相等时,就是删除其中一个,然后再继续寻找剩下的字符串的最长回文子序列长度中的最大值
-
-
初始化:
-
当i=j时,即只有一个字符时,最大回文子序列长度肯定为1
-
当i>j时,即结尾已经跑到了开头之前,空字符串的最大回文子序列设为0,
-
i<j时,是需要推导填入的值,不需要初始化,方便起见也可以初始化为0
-
-
遍历顺序:同样
所以也是从下往上,从左往右。
class Solution {
public:int longestPalindromeSubseq(string s) {vector<vector<int>> dp(s.size() , vector(s.size() , 0));for (int i = 0; i < s.size(); i++) dp[i][i] = 1;for (int i = s.size() - 1; i >= 0; i--)for (int j = i+1; j < s.size(); j++) {if (s[i] == s[j]) {dp[i][j] = dp[i + 1][j - 1] + 2;} elsedp[i][j] = max(dp[i + 1][j], dp[i][j - 1]);}return dp[0][s.size()-1];}
};