【动态规划】(五)动态规划——子序列问题

news2024/11/15 20:10:36

动态规划——子序列问题

  • 子序列问题
    • ☆ 最长递增子序列(离散)
    • 最长连续递增序列(连续)
    • 最大子序和(连续)
    • 最长重复子数组(连续)
    • ☆ 最长公共子序列(离散-编辑距离过渡)
    • 不相交的线(等价最大公共子序列)
  • 编辑距离问题
    • 判断子序列
    • 不同的子序列
    • 两个字符串的删除操作
      • 直接法
      • 间接法
    • 编辑距离
    • 编辑距离总结
  • 回文问题
    • 回文子串
    • 最长回文子序列

子序列问题

☆ 最长递增子序列(离散)

力扣链接

  • 给定一个整数数组 nums ,找到其中最长严格递增子序列的长度。
  • 子序列是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。

示例 1:
输入:nums = [10,9,2,5,3,7,101,18]
输出:4
解释:最长递增子序列是 [2,3,7,101],因此长度为 4 。

示例 2:
输入:nums = [0,1,0,2,2,3]
输出:4

题解:

  • 子序列问题是动态规划解决的经典问题,当前下标 i 的递增子序列长度,其实和 i 之前的下标 j 的子序列长度有关系

  • 学习参考视频:Leetcode 300 最长递增子序列

动规五部曲
1. 确实dp数组含义: dp[i]表示 i 之前包括 i 的以 nums[i] 结尾的最长递增子序列的长度

2. 确定递推公式:

  • 位置 i 的最长升序子序列等于 j 从 0 到 i-1 各个位置的最长升序子序列 + 1 的最大值。

  • 相当于由 dp[j] + 1 的最大值形成的子序列( i 之前的最长子序列)后加上 dp[i] 形成新的最长子序列

  • 所以,递推公式为:

    if (nums[i] > nums[j]) 
    	dp[i] = max(dp[i], dp[j] + 1);
    
  • 注意这里不是要dp[i] 与 dp[j] + 1进行比较,而是我们要取dp[j] + 1的最大值。

3. dp数组初始化: dp[i] = 1,根据定义,单独一个元素形成的最长递增子序列长度为1

4. 遍历顺序: dp[i] 是有 0 到 i - 1 各个位置的最长递增子序列 推导而来,那么遍历 i 一定是从前向后遍历。

5. 举例推导dp数组: [0,1,0,3,2],dp数组的变化如下:


程序实现:

int lengthOfLIS(vector<int>& nums) 
{
	// 10,9,2,5,3,7,101,18
	// 1  1 1 2 2 3  4   4
	//dp[i]: 表示考虑下标 i 可形成的最长递增子序列长度
	vector<int> dp(nums.size(),1);
	dp[0] = 1;
	for(int i = 1; i < nums.size();i++)
	{
		for(int j = 0; j < i; j++)
		{
			// 遍历0-i,在nums[j]小的的dp中取dp的max + 1
			if(nums[i] > nums[j])
				dp[i] = max(dp[i], dp[j] + 1);
		}
	}
	// 求dp数组中的最大值
	return *max_element(dp.begin(),dp.end());
}

最长连续递增序列(连续)

力扣链接

  • 给定一个未经排序的整数数组,找到最长且 连续递增的子序列,并返回该序列的长度。

  • 连续递增的子序列 可以由两个下标 l 和 r(l < r)确定,如果对于每个 l <= i < r,都有 nums[i] < nums[i + 1] ,那么子序列 [nums[l], nums[l + 1], …, nums[r - 1], nums[r]] 就是连续递增子序列。

示例 1:

输入:nums = [1,3,5,4,7]
输出:3
解释:最长连续递增序列是 [1,3,5], 长度为3。尽管 [1,3,5,7] 也是升序的子序列, 但它不是连续的,因为 5 和 7 在原数组里被 4 隔开。

示例 2:
输入:nums = [2,2,2,2,2]
输出:1
解释:最长连续递增序列是 [2], 长度为1。

思路

  • 本题与上题及其相似,唯一区别在于是否连续
  • 延续上题思想,对于非延续,j 需要遍历 [0,j)的区间内找最大的dp,而对于延续而言,j 的取值只有 i-1
  • 因此递推公式中的 j 改为 i - 1即可,递推公式如下:
    if(nums[i] > nums[i-1])
    	dp[i] = dp[i-1] + 1
    

程序实现:

// 动规
int findLengthOfLCIS(vector<int>& nums) 
{
	//1,3,5,4,7
	//1 2 3 3 2
	//dp[i]: 表示考虑下标 i 可形成的最长连续递增子序列长度
	vector<int> dp(nums.size(),1);
	dp[0] = 1;
	for(int i = 1; i < nums.size();i++)
	{
		// 连续递增
		if(nums[i] > nums[i-1])   
			dp[i] = dp[i-1] + 1;
	}
	return *max_element(dp.begin(),dp.end());
}

最大子序和(连续)

力扣链接

  • 给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。

示例:
输入: [-2,1,-3,4,-1,2,1,-5,4]
输出: 6

注:本题也可以用贪心的思想,在贪心章节有涉及: 核心思想是不断累加,加到负数则舍弃之前的累加值(贪心),重新累加,同时在累加过程中记录累加的最大值。

动规五部曲
1. 确实dp数组含义: 包括下标i(以nums[i]为结尾)的最大连续子序列和为dp[i]。

2. 确定递推公式:

  • 取nums[i]:dp[i] = dp[i-1] + nums[i];
  • 不取nums[i]:dp[i] = nums[i];
  • 递推公式:dp[i] = max(dp[i-1] + nums[i], nums[i]);

3. dp数组初始化: dp[0] = nums[0]; 递推的根基

4. 遍历顺序: 递推公式中dp[i]依赖于dp[i - 1]的状态,需要从前向后遍历。

5. 举例推导dp数组: 以示例一为例,输入:nums = [-2,1,-3,4,-1,2,1,-5,4],对应的dp状态如下:

程序实现:

int maxSubArray(vector<int>& nums) 
{
	// dp[i]:考虑到下标 i 形成的最大子序列数组和
	// 延续前面的子序列:dp[i-1] + nums[i]
	// 不延续前面子序列:nums[i]
	vector<int> dp(nums.size());
	dp[0] = nums[0];
	int result = dp[0];
	for(int i = 1; i < nums.size(); i++)
	{
		dp[i] = max(dp[i-1] + nums[i], nums[i]);
		if(dp[i] > result)
			result = dp[i];	
	}
	for(int num : dp)
		cout << num << " ";
	return result;
}

最长重复子数组(连续)

力扣链接

  • 给两个整数数组 A 和 B ,返回两个数组中公共的、长度最长的子数组(连续子序列)的长度。

示例:
输入: A: [1,2,3,2,1]    B: [3,2,1,4,7]
输出:3
解释:长度最长的公共子数组是 [3, 2, 1] 。

注: 本题是上述 【最长连续递增序列】 的二维化(求两个数组的最长公共连续序列),同时又为求最长【 连续公共子序列】(非连续)做了铺垫,属于一道过渡题。

动规五部曲
1. 确实dp数组含义:

  • 长度为[0, i - 1]的nums(以下标 i - 1 结束)与长度为[0, j - 1]的nums2(以下标 j - 1 结束)的最长公共子数组长度为dp[i][j]
  • 为什么这样定义:简化dp数组第一行和第一列的初始化逻辑,并且接触的程序大多都这样定义

2. 确定递推公式: 比较数组元素是否相同,决定了公共子数组的长度,比较元素就两种情况 :

  • 元素相同: 两个数组单个元素相同,即最长公共子数组长度 = 前面一个(因为连续)最长公共子数组长度 + 1,即:

    if(nums1[i-1] == nums2[j-1])
    	dp[i][j] = dp[i-1][j-1] + 1;
    
  • 元素不相等: 无重复子数组,dp[i][j] = 0;

3. dp数组初始化:

  • dp[i][0]:第一列,无意义,但又是递推公式累加的根基,初始化为 0
  • dp[0][j]:第一行,无意义,但又是递推公式累加的根基,初始化为 0
  • 其他非0下标初始化可任意值,因为递推时会被覆盖,不妨初始化0
// dp[i][j]: 以 i-1 为结尾的nums1, 以 j -1 为结尾的nums2结尾的最长重复子数组的长度
vector<vector<int>> dp(nums1.size() + 1, vector<int>(nums2.size() + 1));
// 第一行 第一列无意义
for(int i = 0; i < nums1.size(); i++)
	dp[i][0] = 0;
for(int j = 0; j < nums2.size(); j++)
	dp[0][j] = 0;
int result = 0;

4. 遍历顺序: 根据递推公式,i,j 依靠 i-1,j-1,因此从前向后,从上至下遍历。

for (int i = 1; i <= nums1.size(); i++) {
    for (int j = 1; j <= nums2.size(); j++) {
        if (nums1[i - 1] == nums2[j - 1]) {
            dp[i][j] = dp[i - 1][j - 1] + 1;
        }
        if (dp[i][j] > result) result = dp[i][j];
    }
}

根据dp数组的定义,最终结果为dp[i][j]中最大的元素,因此代码 if (dp[i][j] > result) result = dp[i][j];,即为了实时更新最大值,保存结果。

5. 举例推导dp数组: 拿示例1中,A: [1,2,3,2,1],B: [3,2,1,4,7]为例,画一个dp数组的状态变化,如下:

程序实现:

//本题是动规解决的经典题目
//用二维数组记录两个字符串的所有比较情况
int findLength(vector<int>& nums1, vector<int>& nums2) 
{
	// dp[i][j]: 以 i-1 为结尾的nums1, 以 j -1 为结尾的nums2结尾的最长重复子数组的长度
	vector<vector<int>> dp(nums1.size() + 1, vector<int>(nums2.size() + 1));
	// 第一行 第一列无意义
	for(int i = 0; i < nums1.size(); i++)
		dp[i][0] = 0;
	for(int j = 0; j < nums2.size(); j++)
		dp[0][j] = 0;
	int result = 0;
	for(int i = 1; i <= nums1.size(); i++)
	{
		for (int j = 1; j <= nums2.size(); j++)
		{
			// 更新最长公共子数组长度
			if(nums1[i-1] == nums2[j-1])
				dp[i][j] = dp[i-1][j-1] + 1;
			// 记录dp[i][j]最大值
			if(dp[i][j] > result)
				result = dp[i][j];
		}
	}
	// 打印dp数组
	for(int i = 0; i <= nums1.size(); i++)
	{
		for(int j = 0; j <= nums2.size(); j++)
			cout << dp[i][j] << " ";
		cout << endl;
	}
	return result;
}

☆ 最长公共子序列(离散-编辑距离过渡)

学习参考视频:[轻松掌握动态规划] 5.最长公共子序列 LCS

力扣链接

  • 给定两个字符串 text1 和 text2,返回这两个字符串的最长公共子序列的长度。
  • 一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。
  • 若这两个字符串没有公共子序列,则返回 0。

示例 1:
输入:text1 = “abcde”, text2 = “ace”
输出:3
解释:最长公共子序列是 “ace”,它的长度为 3。

  • 1 <= text1.length <= 1000
  • 1 <= text2.length <= 1000
  • 输入的字符串只含有小写英文字符。

动规五部曲
1. 确实dp数组含义:

  • 长度为[0, i - 1]的字符串text1(以下标 i - 1 结束)与长度为[0, j - 1]的字符串text2(以下标 j - 1 结束)的最长公共子序列长度为dp[i][j]

  • 为什么这样定义:简化dp数组第一行和第一列的初始化逻辑,并且接触的程序大多都这样定义

2. 确定递推公式: 比较数组元素是否相同,决定了公共子序列的长度,比较元素就两种情况 :

  • 元素相同: 两个数组单个元素相同,即最长公共子序列长度 + 1,即:

    if(text1[i-1] == text2[j-1])
    	dp[i][j] = dp[i-1][j-1] + 1;
    
  • 不相同

    • 舍弃text1[i-1],考虑由text1[i-2]text2[j-1] 结束形成的最大公共子序列
      dp[i][j] = dp[i-1][j];
      
    • 舍弃text2[j-1],考虑由text1[i-1]text2[j-2] 结束形成的最大公共子序列
      dp[i][j] = dp[i][j-1];
      
    • 因此当 text1[i-1] == text2[j-1] 不相等时,递归公式为:
      dp[i][j] = max(dp[i-1][j], dp[i][j-1]);
      

3. dp数组初始化:

  • dp[i][0]:text1[0, i-1]和空串的最长公共子序列自然是0,所以 dp[i][0] = 0;

  • 同理,dp[0][j] = 0;

  • 其他下标都是随着递推公式逐步覆盖,初始为多少都可以,那么就统一初始为0

    vector<vector<int>> dp(text1.size() + 1, vector<int>(text2.size() + 1, 0));
    

4. 遍历顺序: 从递推公式,可以看出,有三个方向可以推出dp[i][j],如图:

那么为了在递推的过程中,这三个方向都是经过计算的数值,所以要从前向后,从上到下来遍历这个矩阵。


5. 举例推导dp数组: text1 = “ABCBDAB”, text2 = “BDCABC” 为例,dp状态如图:

程序实现

//用二维数组记录两个字符串的所有比较情况
int longestCommonSubsequence(string text1, string text2) 
{
	// dp[i][j]: 以 i-1 为结尾的 text1, 以 j -1 为结尾的 text2 结尾的最长重复子数组的长度
	// 长度为[0, i - 1]的字符串text1与长度为[0, j - 1]的字符串text2的最长公共子序列为dp[i][j]
	vector<vector<int>> dp(text1.size() + 1, vector<int>(text2.size() + 1, 0));
	// 第一行 第一列无意义 初始化全为 0
	int result = 0;
	for(int i = 1; i <= text1.size(); i++)
	{
		for (int j = 1; j <= text2.size(); j++)
		{
			// 元素相同
			if(text1[i-1] == text2[j-1])
				dp[i][j] = dp[i-1][j-1] + 1;
			// 元素不相同
			// dp[i][j-1]:abc 和 考虑ac不考虑e
			// dp[i-1][j]:考虑ab不考虑c 和 ace
			else
				dp[i][j] = max(dp[i][j-1], dp[i-1][j]);
			
			if(dp[i][j] > result)
				result = dp[i][j];
		}
	}
//		for(int i = 0; i <= text1.size(); i++)
//		{
//			for(int j = 0; j <= text2.size(); j++)
//			{
//				cout << dp[i][j] << " ";
//			}
//			cout << endl;
//		}
	return result;
}

不相交的线(等价最大公共子序列)

力扣链接

  • 我们在两条独立的水平线上按给定的顺序写下 A 和 B 中的整数。
  • 现在,我们可以绘制一些连接两个数字 A[i] 和 B[j] 的直线,只要 A[i] == B[j],且我们绘制的直线不与任何其他连线(非水平线)相交。
  • 以这种方法绘制线条,并返回我们可以绘制的最大连线数。

示例:
输入:nums1 = [1,4,2], nums2 = [1,2,4]
输出:2

示例 2:
输入:nums1 = [2,5,1,2,5], nums2 = [10,5,2,1,5,2]
输出:3

示例 3:
输入:nums1 = [1,3,7,1,7,5], nums2 = [1,9,2,5,1]
输出:2

题解:

  • 拿示例一A = [1,4,2], B = [1,2,4]为例,相交情况如图:
  • 其实也就是说A和B的最长公共子序列是[1,4],长度为2。 这个公共子序列指的是相对顺序不变(即数字4在字符串A中数字1的后面,那么数字4也应该在字符串B数字1的后面)

  • 通过分析发现:本题说是求绘制的最大连线数,其实就是求两个字符串的最长公共子序列的长度!

  • 这与上一题最最长公共子序列的程序完全一致,这里不再赘述。

编辑距离问题

判断子序列

力扣链接

  • 给定字符串 s 和 t ,判断 s 是否为 t 的子序列。

  • 字符串的一个子序列是原始字符串删除一些(也可以不删除)字符而不改变剩余字符相对位置形成的新字符串。(例如,"ace"是"abcde"的一个子序列,而"aec"不是)。

示例 1:
输入:s = “abc”, t = “ahbgdc”
输出:true

示例 2:
输入:s = “axc”, t = “ahbgdc”
输出:false

这道题应该算是编辑距离的入门题目,因为从题意中我们也可以发现,只需要计算删除的情况,不用考虑增加和替换的情况。

所以掌握本题的动态规划解法是对后面要讲解的编辑距离的题目打下基础。

动规五部曲

1. 确实dp数组含义: 由下标 i-1 结束的s, j-1结束的 t 相同子序列的长度为dp[i][j],其中 s <= t

2. 确定递推公式:

  • s[i-1] == t[j-1]: t 中找到了一个字符在 s 中也出现了,此时最长子序列长度 + 1,递推公式即:

    dp[i][j] = dp[i-1][j-1] + 1;
    
  • s[i-1] != t[j-1]: 相当于 t 要删除元素,继续匹配,t 如果把当前元素t[j - 1]删除,那么dp[i][j] 的数值就是 看s[i - 1]与 t[j - 2]的比较结果了,递推公式即:

    dp[i][j] = dp[i][j-1];
    

3. dp数组初始化: dp[i][0] 表示以下标 i-1为结尾的字符串,与空字符串的相同子序列长度,所以为0. dp[0][j]同理。

vector<vector<int>> dp(s.size() + 1, vector<int>(t.size() + 1, 0));

4. 遍历顺序: 同理从递推公式可以看出dp[i][j]都是依赖于dp[i - 1][j - 1] 和 dp[i][j - 1],那么遍历顺序也应该是从上到下,从左到右,如图所示:

5. 举例推导dp数组: 以示例一为例,输入:s = “abc”, t = “ahbgdc”,dp状态转移图如下:

程序实现:

bool isSubsequence(string s, string t) 
{
	// s <= t
	// dp[i][j]:由下标 i-1 结束的s, j-1结束的t 相同子序列的长度为dp[i][j]
	// 类似于最长公共子序列
	vector<vector<int>> dp(s.size() + 1,vector<int>(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;
			else
				dp[i][j] = dp[i][j-1];
		}
	}
	
	for(int i = 0; i <= s.size(); i++)
	{
		for(int j = 0; j <= t.size(); j++)
		{
			cout << dp[i][j] << " ";
		}
		cout << endl;
	}
	if(dp[s.size()][t.size()] == s.size())
		return true;
	else
		return false;
}

注: 此外本题可以求 s 和 t 的公共子序列长度,若公共子序列长度 == s.size(),那么 s 就是 t 的子序列,否则不是。

不同的子序列

力扣链接

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

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

示例 1:

输入:s = “rabbbit”, t = “rabbit”
输出:3
解释:如下所示, 有 3 种可以从 s 中得到 “rabbit” 的方案:rabbbitrabbbitrabbbit

示例 2:
输入:s = “babgbag”, t = “bag”
输出:5
解释:如下所示, 有 5 种可以从 s 中得到 “bag” 的方案:babgbag  babgbagbabgbag  babgbag  babgbag

动规五部曲
1. 确实dp数组含义: 以下标 i-1结尾的s,以下标 j-1结尾的t,s的子序列中,t 出现的个数为 dp[i][j]
2. 确定递推公式:

  • s[i - 1] == t[j - 1]

    • 用s[i - 1]来匹配,那么个数为 dp[i - 1][j - 1]。即不需要考虑当前 s 子串和 t 子串的最后一位字母,所以只需要 dp[i-1][j-1]

    • 一部分是不用s[i - 1]来匹配,个数为 dp[i - 1][j]

    • Q1:为什么还要考虑 不用s[i - 1]来匹配

    • A1:例如 s:bagg 和 t:bag ,s[3] 和 t[2]是相同的,但是字符串s也可以不用s[3]来匹配,即用s[0]s[1]s[2]组成的bag。

    • Q2:为什么只考虑 “不用s[i - 1]来匹配” 这种情况, 不考虑 “不用t[j - 1]来匹配” 的情况呢

    • A2:我们求的是 s 中有多少个 t,而不是 求 t 中有多少个 s,所以只考虑 s 中删除元素的情况,即 不用s[i - 1]来匹配 的情况。

    • 所以当s[i-1] == t[j-1]时候,递推公式为:

      dp[i][j] = dp[i-1][j-1] + dp[i-1][j];
      
  • s[i - 1] != t[j - 1]

    • dp[i][j]只有一部分组成,不用s[i - 1]来匹配(就是模拟在s中删除这个元素),即:dp[i - 1][j]

3. dp数组初始化:

  • dp[i][0] 表示:以i-1为结尾的s可以随便删除元素,出现空字符串的个数,dp[i][0] = 1;,因为也就是把以i-1为结尾的s,删除所有元素,出现空字符串的个数就是1
  • dp[0][j]:空字符串s可以随便删除元素,出现以j-1为结尾的字符串t的个数,那么dp[0][j]一定都是0,s如论如何也变成不了t。
    vector<vector<long long>> dp(s.size() + 1, vector<long long>(t.size() + 1));
    for (int i = 0; i <= s.size(); i++) 
    	dp[i][0] = 1;
    for (int j = 1; j <= t.size(); j++) 
    	dp[0][j] = 0; 
    

4. 遍历顺序: 从递推公式dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j]; 和 dp[i][j] = dp[i - 1][j]; 中可以看出dp[i][j]都是根据左上方和正上方推出来的,遍历的时候一定是是从上到下,从左到右

5. 举例推导dp数组: 以s:“baegg”,t:"bag"为例,推导dp数组状态如下:

程序实现:

int numDistinct(string s, string t)
{
	// s >= t
	// dp[i][j]:由下标 i-1 结束的s, j-1结束的t 子序列出现的个数
	vector<vector<int>> dp(s.size() + 1,vector<int>(t.size() + 1));
	for(int i = 0; i <= s.size();i++)
		dp[i][0] = 1;
	for(int j = 1; j <= t.size();j++)
		dp[0][j] = 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] + dp[i-1][j];
			else
				dp[i][j] = dp[i-1][j];
		}
	}
	
	for(int i = 0; i <= s.size(); i++)
	{    
		for(int j = 0; j <= t.size(); j++)
			cout << dp[i][j] << " ";
		cout << endl;
	}
	
	return dp[s.size()][t.size()];
}

两个字符串的删除操作

力扣链接

  • 给定两个单词 word1 和 word2,找到使得 word1 和 word2 相同所需的最小步数,每步可以删除任意一个字符串中的一个字符。

示例:
输入: “sea”, “eat”
输出: 2
解释: 第一步将"sea"变为"ea",第二步将"eat"变为"ea"

本题和上题不同的子序列相比,其实就是两个字符串都可以删除了,情况虽说复杂一些,但整体思路是不变的。

直接法

动规五部曲
1. 确实dp数组含义: 以 i-1为结尾的字符串word1,和以 j-1位结尾的字符串word2,想要达到相等,所需要删除元素的最少次数

2. 确定递推公式:

word1[i - 1] == word2[j - 1],不操作,dp[i][j] = dp[i - 1][j - 1];

word1[i - 1] != word2[j - 1]

  • 删除word1[i-1],最少操作步骤为:

    dp[i][j] = dp[i - 1][j ] + 1
    
  • 删除word2[j-1],最少操作步骤为:

    dp[i][j] = dp[i ][j-1 ] + 1;
    
  • 同时删除word1[i-1]和word2[j-1],最少操作步骤为:

    dp[i][j] = dp[i -1][j-1 ] + 2;
    
  • 所以递推公式为:

    dp[i][j] = min(dp[i - 1][j ] + 1, dp[i][j - 1] + 1, dp[i -1][j-1 ] + 2);
    
  • 这里不再画图解释,删除操作参考上述两题的图解

3. dp数组初始化:

  • dp[i][0]:word2为空,以i-1为结尾的字符串word1要删除 i 个元素,才和word2相同,因此 dp[i][0] = i
  • 同理,dp[0][j] = j;
  • 其余位置无需初始化,会通过递推覆盖

4. 遍历顺序: 通过递推公式可得,从上往下,从左往右遍历。

5. 举例推导dp数组: 以word1:“sea”,word2:"eat"为例,推导dp数组状态图如下:

程序实现:

// 动规 直接法
int minDistance(string word1, string word2)
{
	// dp[i][j]:由下标 i-1 结束的s, j-1结束的t 最少删除dp[i][j]步才能使字符串相等
	vector<vector<int>> dp(word1.size() + 1,vector<int>(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];
			else	// 不相等
				dp[i][j] = min(dp[i-1][j] + 1,min(dp[i][j-1] + 1, dp[i-1][j-1] + 2));
		}
	}
	
	for(int i = 0; i <= word1.size(); i++)
	{    
		for(int j = 0; j <= word2.size(); j++)
		{
			cout << dp[i][j] << " ";
		}
		cout << endl;
	}
	
	return dp[word1.size()][word2.size()];
}

间接法

  • 通过问题分析,需要对两个字符串删除多余的元素,使其变成 word1 = word2,即变成公共子序列。
  • 从结果出发,计算两个字符串的最长公共子序列长度n,那么最少需要 word1.size() + word2.size() - 2 * n 就可以使得word1 == word2。

程序实现:

// 动规 求最长公共子序列 算出结果剩下的数组
int minDistance2(string word1, string word2)
{
	// dp[i][j]:由下标 i-1 结束的s, j-1结束的t组成的最长公共子序列的长度
	vector<vector<int>> dp(word1.size() + 1,vector<int>(word2.size() + 1, 0));
	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] + 1;
			else
				dp[i][j] = max(dp[i-1][j], dp[i][j-1]);
		}
	}
	
	return word1.size() + word2.size() - 2 * dp[word1.size()][word2.size()];
}

编辑距离

力扣链接

  • 给你两个单词 word1 和 word2,请你计算出将 word1 转换成 word2 所使用的最少操作数 ,你可以对一个单词进行如下三种操作:
    • 插入一个字符
    • 删除一个字符
    • 替换一个字符

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

示例 2:
输入:word1 = “intention”, word2 = “execution”
输出:5

动规五部曲
1. 确实dp数组含义: dp[i][j] 表示以下标i-1为结尾的字符串word1,和以下标j-1为结尾的字符串word2,最近编辑距离为dp[i][j]。

2. 确定递推公式:
在确定递推公式的时候,首先要考虑清楚编辑的几种操作,整理如下:

if (word1[i - 1] == word2[j - 1])
    不操作
if (word1[i - 1] != word2[j - 1])//

if (word1[i - 1] == word2[j - 1]) :那么说明不用任何编辑,dp[i][j] 就应该是 dp[i - 1][j - 1],即dp[i][j] = dp[i - 1][j - 1];

if (word1[i - 1] != word2[j - 1]),此时就需要编辑了,

  • 操作一:word1删除一个元素,那么就是以下标i - 2为结尾的word1 与 j-1为结尾的word2的最近编辑距离 再加上一个操作,即:
    dp[i][j] = dp[i - 1][j] + 1;
    
  • 操作二:word2删除一个元素,那么就是以下标i - 1为结尾的word1 与 j-2为结尾的word2的最近编辑距离 再加上一个操作
    dp[i][j] = dp[i][j - 1] + 1;
    
  • 操作三:替换元素: word1替换word1[i - 1],使其与word2[j - 1]相同,只需要一次替换的操作,就可以让 word1[i - 1] 和 word2[j - 1] 相同,即:
    dp[i][j] = dp[i - 1][j - 1] + 1
    
  • 所以当 word1[i - 1] != word2[j - 1]时,递推公式为:
    dp[i][j] = min(dp[i-1][j] + 1, dp[i][j-1] + 1, dp[i-1][j-1] + 1 );
    

3. dp数组初始化:

  • dp[i][0] :以下标i-1为结尾的字符串word1,和空字符串word2,需要最小编辑的距离才是其相等,因此 dp[i][0] = i;
  • 同理,dp[0][j] = j;

4. 遍历顺序: 根据递推公式从左到右从上到下去遍历


5. 举例推导dp数组: 以示例1为例,输入:word1 = “horse”, word2 = "ros"为例,dp矩阵状态图如下:

程序实现

// 动规
int minDistance(string word1, string word2)
{
	// dp[i][j]:由下标 i-1 结束的word1, j-1结束的word2 最少的操作次数为dp[i][j]
	vector<vector<int>> dp(word1.size() + 1,vector<int>(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];
			else
			{
			//删除word1[i-1]:	dp[i-1][j] + 1    
			//删除word2[j-1]:	dp[i][j-1] + 1    
			// 替换 dp[i-1][j-1] + 1 	
				dp[i][j] = min(dp[i-1][j] + 1,min(dp[i][j-1] + 1, dp[i-1][j-1] + 1 ));
			}
		}
	}
	
	for(int i = 0; i <= word1.size(); i++)
	{    
		for(int j = 0; j <= word2.size(); j++)
		{
			cout << dp[i][j] << " ";
		}
		cout << endl;
	}
	
	return dp[word1.size()][word2.size()];
}

编辑距离总结

编辑距离总结篇

回文问题

回文子串

力扣链接

  • 给定一个字符串,你的任务是计算这个字符串中有多少个回文子串。
  • 具有不同开始位置或结束位置的子串,即使是由相同的字符组成,也会被视作不同的子串。

示例 1:
输入:“abc”
输出:3
解释:三个回文子串: “a”, “b”, “c”

示例 2:
输入:“aaa”
输出:6
解释:6个回文子串: “a”, “a”, “a”, “aa”, “aa”, “aaa”

动规五部曲
1. 确实dp数组含义:
  本题如果定义dp[i] 为下标i结尾的字符串有 dp[i]个回文串的话,我们会发现很难找到递归关系,因为dp[i] 和 dp[i-1],dp[i + 1] 看上去都没啥关系。

所以我们要看回文串的性质。 如图:

判断一个子字符串(字符串下标范围[i,j])是否回文,依赖于,子字符串(下标范围[i + 1, j - 1])) 是否是回文串

定义dp[i][j]数组:dp[i][j]表示以i开头,j结尾的字符串(区间[i,j])是否为回文串,是则 true,否则 false

2. 确定递推公式: 根据上图可知,dp[i][j]依赖于dp[i+1][j-1],以及比较 s[i]和s[j]是否相等

当s[i]与s[j]不相等dp[i][j] = false

当s[i]与s[j]相等:

  • 情况1:i = j: dp[i][j] = true;
  • 情况2:j - i = 1: dp[i][j] = true;,例如 aa
  • 情况3:j - i > 1: 取决于dp[i+1][j-1]是否为字符串
    if(dp[i+1][j-1])
    	dp[i][j] = true;
    

3. dp数组初始化: 默认全部非回文串,在遍历中判断哪些是回文串,因此 dp[i][j] = false

4. 遍历顺序: 遍历顺序可有有点讲究了

  • 首先从递推公式中可以看出,情况三是根据dp[i + 1][j - 1]是否为true,在对dp[i][j]进行赋值true的。

  • dp[i + 1][j - 1] 在 dp[i][j]的左下角,如图:

  • 因此遍历顺序为:从下往上,从左往右,这样保证dp[i + 1][j - 1]都是经过计算的。

5. 举例推导dp数组: 举例,输入:“aaa”,dp[i][j]状态如下,图中有6个true,所以就是有6个回文子串。

程序实现

//动规
int countSubstrings(string s) 
{
	// dp[i][j]:表示区间范围:[i,j]内的字符串是否为子串
	vector<vector<bool>> dp(s.size(), vector<bool>(s.size(),false));
	int result = 0;
	// s[i] == s[j]		
		//  i == j 		
		//相邻:i + 1 = j	dp[i][j] = true     result++
		// j - i > 1		if(dp[i+1][j-1]) 	dp[i][j] = true    result++
	// s[i] != s[j]			dp[i][j] = false;		
	for(int i = s.size() - 1; i >= 0; i--)
	{
		for(int j = i; j < s.size(); j++)
		{
			//相等
			if(s[i] == s[j])
			{
				//情况1 + 情况2
				if(j - i <= 1){
					dp[i][j] = true;
					result++;
				}
				//情况3
				else{
					if(dp[i+1][j-1]){
						dp[i][j] = true;
						result++;
					}
				}	
			}
		}
	}
	return result;
}

最长回文子序列

力扣链接

  • 给定一个字符串 s ,找到其中最长的回文子序列,并返回该序列的长度。可以假设 s 的最大长度为 1000 。

示例 1:
输入: “bbbab”
输出: 4
一个可能的最长回文子序列为 “bbbb”。

示例 2:
输入:“cbbd”
输出: 2
一个可能的最长回文子序列为 “bb”。

动态规划五部曲
1. dp数组含义: 由区间[i,j]组成的字符串的最长回文子序列长度为dp[i][j]

2. 确定递推公式:

  • s[i] == s[j]dp[i][j] = dp[i+1][j-1] + 2;

  • s[i] != s[j]:说明s[i]和s[j]的同时加入 并不能增加[i,j]区间回文子序列的长度,那么分别加入s[i]、s[j]看看哪一个可以组成最长的回文子序列。

    • 加入s[j]的回文子序列长度为dp[i + 1][j]。

    • 加入s[i]的回文子序列长度为dp[i][j - 1]

  • 因此递推公式即:

    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]);
    

3. 初始化数组:

  • 根据递推公式,i,j两个指针一左一右移动,最终 i == j,则是递推的根基,根据定义,dp[i][i] = 1;

    for (int i = 0; i < s.size(); i++) 
    	dp[i][i] = 1;
    
  • 其他情况初始化为0,这样在 max时候,dp[i][j]才不会被覆盖

4. 确定遍历顺序: 从递归公式中,可以看出,dp[i][j] 依赖于 dp[i + 1][j - 1] ,dp[i + 1][j] 和 dp[i][j - 1],如图:

遍历顺序:从下到上,从左到右,这样才能保证下一行的数据是经过计算的。

5. 举例推导dp数组: 输入s:“cbbd” 为例,dp数组状态如图:

程序实现

//动规
int longestPalindromeSubseq(string s) 
{
	// dp[i][j]:表示区间范围:[i,j]内的最长回文子序列长度为 dp[i][j]
	vector<vector<int>> dp(s.size(), vector<int>(s.size(),0));
	// s[i] == s[j]		dp[i][j] = dp[i+1][j-1] + 2 
	// s[i] != s[j]			
		// 加入s[i]		dp[i][j-1]	
		// 加入s[j]		dp[i+1][j]	
	// i 和 j 相等的情况下: dp[i][j] = 1
	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;
			else
				dp[i][j] = max(dp[i+1][j],dp[i][j-1]);
		}
	}
	
	for(int i = 0; i < s.size(); i++)
	{
		for(int j = 0; j < s.size(); j++){
			cout << dp[i][j] << " ";
		}
		cout <<  endl;
	}
	return dp[0][s.size()-1];
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2161349.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【驱动】修改USB转串口设备的属性,如:Serial

1、查看串口信息 在Windows上,设备管理窗口中查看设备号 2、修改串口号工具 例如使用:CH34xSerCfg.exe 使用步骤:恢复默认值 - -> 修改 Serial String(或者Product String等属性)–> 写入配置 3、查看设备节点 在linux上使用lsub查看新增的设备信息,如下这个…

python多线程开发的具体示例

用一个具体的示例&#xff0c;展示如何使用 ThreadPoolExecutor 和 asyncio 来并行运行多个任务&#xff0c;并输出结果。 代码&#xff1a; import asyncio import time from concurrent.futures import ThreadPoolExecutorclass WorkJob:def __init__(self, job_id):self.j…

报表做着太费劲?为你介绍四款好用的免费报表工具

1. 山海鲸可视化 介绍&#xff1a; 山海鲸可视化是一款免费的国产可视化报表软件&#xff0c;与许多其他宣传免费的软件不同&#xff0c;山海鲸的报表功能完全免费并且没有任何限制&#xff0c;就连网站管理后台这个功能也是免费的。同时山海鲸可视化还提供了种类丰富的可视化…

11.安卓逆向-安卓开发基础-api服务接口设计2

免责声明&#xff1a;内容仅供学习参考&#xff0c;请合法利用知识&#xff0c;禁止进行违法犯罪活动&#xff01; 内容参考于&#xff1a;图灵Python学院 本人写的内容纯属胡编乱造&#xff0c;全都是合成造假&#xff0c;仅仅只是为了娱乐&#xff0c;请不要盲目相信。 工…

云手机推荐:五款热门云手机测评!

在云手机市场中&#xff0c;各个品牌层出不穷&#xff0c;让人难以选择。为了帮助你更好地找到适合的云手机应用&#xff0c;我们整理了五款最受欢迎的云手机进行测评。2024年&#xff0c;哪款云手机是你的不二之选&#xff1f;且慢下结论&#xff0c;看看这五款云手机的真实表…

【深度学习】深度卷积神经网络(AlexNet)

在 LeNet 提出后&#xff0c;卷积神经网络在计算机视觉和机器学习领域中很有名气&#xff0c;但并未起到主导作用。 这是因为 LeNet 在更大、更真实的数据集上训练的性能和可行性还有待研究。 事实上&#xff0c;在 20 世纪 90 年代到 2012 年之间的大部分时间里&#xff0c;…

Windows系统的Tomcat日志路径配置

文章目录 引言I Windows系统的Tomcat日志路径配置配置常规日志路径访问日志路径配置,修改server.xmlII 日志文件切割:以分隔割tomcat 的 catalina.out 文件为例子通过Linux系统自带的切割工具logrotate来进行切割引言 需求:C盘空间不足,处理日志文件,tomcat日志迁移到D盘…

中国科学院云南天文台博士招生目录

中国科学院云南天文台是专业基础研究与应用研究结合的综合性天文研究机构&#xff08;其前身是1938年中央研究院天文研究所在昆明东郊凤凰山创建的凤凰山天文台&#xff09;&#xff0c;总部在云南省昆明市&#xff0c;设有两个观测站&#xff08;丽江高美古天文观测站和澄江抚…

Boruta 的库的初识

我在一个kaggle比赛时间预测中发现Boruta我并不熟悉与是我学习了一下 Boruta 的工作原理&#xff1a; 影子特征&#xff08;Shadow Features&#xff09;: Boruta 首先创建一组影子特征&#xff0c;这些影子特征是通过随机打乱原始特征的值生成的。影子特征的目的是作为对照组…

【完结】【PCL实现点云分割】ROS深度相机实践指南(下):pcl::BoundaryEstimation实现3D点云轮廓检测的原理(论文解读)和代码实现

前言 本教程使用PCL对ROS深度相机捕获到的画面进行操场上锥桶的分割 上:[csdn 博客] 【PCL实现点云分割】ROS深度相机实践指南&#xff08;上&#xff09;:PCL库初识和ROS-PCL数据类型转换中:[csdn 博客] 【PCL实现点云分割】ROS深度相机实践指南&#xff08;中&#xff09;:Pl…

电梯节能 引领趋势

电梯&#xff0c;之前对我们来说&#xff0c;就是让我们省时省力的工具&#xff0c;谁知电梯也可加装【节能设备】。 电梯节能评估&#xff0c;节电率达20%-50%。 电梯节能&#xff08;电梯回馈装置&#xff09;通常电梯在轻载上行&#xff0c;重载下行和平层停梯状态下&#…

监控和维护 Linux 系统的健康状态:从服务启动故障到操作系统查询

个人名片 &#x1f393;作者简介&#xff1a;java领域优质创作者 &#x1f310;个人主页&#xff1a;码农阿豪 &#x1f4de;工作室&#xff1a;新空间代码工作室&#xff08;提供各种软件服务&#xff09; &#x1f48c;个人邮箱&#xff1a;[2435024119qq.com] &#x1f4f1…

rar文件怎么打开?这几款软件压缩和查看很方便!

在这个数字化信息爆炸的时代&#xff0c;我们每天都会接触到各种各样的文件&#xff0c;其中RAR格式文件以其高压缩率和良好的文件保护特性&#xff0c;成为了许多人分享和存储大文件的首选。然而&#xff0c;面对这样一个看似“神秘”的文件格式&#xff0c;不少朋友可能会感到…

Stable Diffusion绘画 | 来训练属于自己的模型:配置完成,炼丹启动

前言 效率设置-优化器 优化器可以分为4类&#xff1a; 第一类 AdamW &#xff1a;梯度下降算法&#xff0c;结合自适应学习率&#xff0c;既可以快速收敛&#xff0c;又可以避免 Loss值 震荡 AdamW8bit&#xff1a;能降低显存占用&#xff0c;并略微加快训练速度&#xff0…

Mysql—主从复制的slave添加及延迟回放

MySQL 主从复制是什么&#xff1f; ​ MySQL 主从复制是指数据可以从一个 MySQL 数据库服务器主节点复制到一个或多个从节点。MySQL 默认采用异步复制方式&#xff0c;这样从节点不用一直访问主服务器来更新自己的数据&#xff0c;数据的更新可以在远程连接上进行&#xff0c;…

国产分布式数据库-tidb单机部署文档

tidb单机部署文档 1、创建用户 #创建用户 useradd tidb #设置密码 passwd tidb2、配置免密码登录 编辑/etc/sudoers文件,文末加入&#xff1a; tidb ALL(ALL) NOPASSWD:ALL如果想要控制某个用户(或某个组用户)只能执行root权限中的一部分命令, 或者允许某些用户使用sudo时…

充电桩设备升级扩展多段计费

一 项目背景 某省某市的一个充电桩项目近日收到业主需求&#xff0c;需在国庆节增加一个时间段&#xff08;深谷计费段&#xff09;&#xff0c;但充电桩设备仅支持4段&#xff08;尖时段&#xff0c;峰时段&#xff0c;平时段&#xff0c;谷时段&#xff09;&#xff0c;今…

【CoppeliaSim V4.7】The Python interpreter could not handle the wrapper script

[sandboxScript:error] The Python interpreter could not handle the wrapper script (or communication between the launched subprocess and CoppeliaSim could not be established via sockets). Make sure that the Python modules ‘cbor2’ and ‘zmq’ are properly i…

Spring MVC 基本配置步骤 总结

1.简介 本文记录Spring MVC基本项目拉起配置步骤。 2.步骤 在pom.xml中导入依赖&#xff1a; <dependency><groupId>org.springframework</groupId><artifactId>spring-webmvc</artifactId><version>6.0.6</version><scope>…

关于javascript中防抖和节流的使用详解

防抖&#xff08;Debounce&#xff09;和节流&#xff08;Throttle&#xff09;是两种常见的优化技巧&#xff0c;通常用于控制函数在短时间内频繁触发的场景&#xff0c;尤其是在处理用户输入、滚动、窗口大小调整等事件时。它们的主要目的是减少不必要的函数调用&#xff0c;…