IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 数据结构与算法 -> [动态规划-练习] 5 最长公共子序列问题详解 -> 正文阅读

[数据结构与算法][动态规划-练习] 5 最长公共子序列问题详解

参考:

  1. 详解最长公共子序列问题,秒杀三道动态规划题目
  2. 两个字符串的最小 ASCII 删除和

原作者总结出来的解决算法的一个技巧:把大的问题细化到一个点,先研究在这个小的点上如何解决问题,然后再通过递归/迭代的方式扩展到整个问题

本文从「最长公共子序列问题」展开,总结三道子序列问题,解这道题仔细讲讲这种子序列问题的套路,你就能感受到这种思维方式了。

1 最长公共子序列

计算1143. 最长公共子序列(Longest Common Subsequence,简称 LCS)是一道经典的动态规划题目:

image-20210713152231518

力扣第 1143 题就是这道题,函数签名如下:

int longestCommonSubsequence(String s1, String s2);

首先,我们先考虑暴力算法:把s1s2的所有子序列都穷举出来,然后看看有没有公共的,然后在所有公共子序列里面再寻找一个长度最大的。

然后,我们在暴力算法的基础上进行改进:我们可以不要考虑整个字符串,而是细化到s1s2的每个字符

在“子序列解题模板”中有一个规律:

对于两个字符串求子序列的问题,都是用两个指针ij分别在两个字符串上移动,大概率是动态规划思路

思路:具体到每一个字符,思考每个字符该做什么

接下来,咱不要看s1s2两个字符串,而是要具体到每一个字符,思考每个字符该做什么

image-20210713160735508

我们只看s1[i]s2[j]

  1. 如果s1[i] == s2[j],说明这个字符一定在lcsimage-20210713160804821

  2. 如果s1[i] != s2[j],应该怎么办呢?s1[i] != s2[j]意味着,s1[i]s2[j]中至少有一个字符不在lcs图片

    如上图,总共可能有三种情况,我怎么知道具体是那种情况呢?

    答:其实我们也不知道,那就把这三种情况的答案都算出来,取其中结果最大的那个呗,因为题目让我们算「最长」公共子序列的长度嘛。

    这里我们可以进行一个优化,我们可以观察情况3:

    情况三在计算s1[i+1..]s2[j+1..]lcs长度,这个长度肯定是小于等于情况二s1[i..]s2[j+1..]中的lcs长度的,因为s1[i+1..]s1[i..]短嘛,那从这里面算出的lcs当然也不可能更长嘛。

    同理,情况三的结果肯定也小于等于情况一。

    也就是说,情况三被情况一和情况二包含了,所以我们可以直接忽略掉情况三。

现在我们大致思路已经有了,就是:注意对比每一个字符,它们有两种可能:

  • s1[i] == s2[j]时,LCS加1;
  • s1[i] != s2[j]时,分三种情况讨论。(实际上,情况3包含在情况1和2之中。)

代码实现

1、不使用「dp数组」,也不使用「备忘录」的动态规划:

 class Solution {
 public:
 	int longestCommonSubsequence(string text1, string text2) {
 		if (text1.size() == 0 || text2.size() == 0)	return 0;

 		return dp(text1, 0, text2, 0);

 	}

 	int dp(string text1, int i, string text2, int j) {
 		// base case
 		if (i == text1.size() || j == text2.size())
 			return 0;

 		// 状态转移
 		if (text1[i] == text2[j])
 			return dp(text1, i + 1, text2, j + 1) + 1;
 		// else if (text1[i] != text2[j]) 
         return max(
             dp(text1, i, text2, j + 1),
             max(dp(text1, i + 1, text2, j), 
                 dp(text1, i + 1, text2, j + 1)) // 这里可以优化
         );

 	}
 };

说明:

  1. 我们的dp函数含义为:

    // 定义:计算 s1[i..] 和 s2[j..] 的最长公共子序列长度
    int dp(String s1, int i, String s2, int j)
    
  2. 情况3我们其实可以不写,但这里为了代码的直观性,还是加上了。

2、使用「dp数组」(迭代实现):

class Solution {
public:
	int longestCommonSubsequence(string text1, string text2) {
		int m = text1.size();
		int n = text2.size();
		// dp数组:dp[i][j]表示s1[i..] 和 s2[j..] 的最长公共子序列长度
		vector<vector<int>> dp(m+1, vector<int>(n+1, -1));

		// 初始情况
		for (int j = 0; j < n; j++) {
			dp[0][j] = 0;
		}
		for (int i = 0; i < m; i++) {
			dp[i][0] = 0;
		}

		// 状态转移
		// 注意i、j的值,因为我们多给dp数组增加了一行、一列,此时text的下标需要注意
		for (int i = 1; i <= m; i++) {
			int ii = i - 1;
			for (int j = 1; j <= n; j++) {
				int jj = j - 1;
				if (text1[ii] == text2[jj]) // 相等时
					dp[i][j] = dp[i - 1][j - 1] + 1;
				else if (text1[ii] != text2[jj]) { // 不相等时,分三种情况讨论
					int a = dp[i-1][j];		// 情况1:text1[i] 不在 lcs 中
					int b = dp[i][j - 1];	// 情况2:text2[j] 不在 lcs 中
					int c = dp[i-1][j - 1]; // 情况3:text1[i]、text2[j] 均不在 lcs 中

					dp[i][j] = max(a, max(b, c)); // 找出这三种情况最大的
				}
			}
		}

		return dp[m][n];

	}
};

说明:

  1. 注意我们这里的text[]dp[][]的下标。

    image-20210713164037703

    可以看到,我们声明的dp数组是dp[m+1][n+1]大小的,这是因为,我们需要考虑空字符串的情况,所以需要多一行、多一列来存储。

    text1[]的下表范围在0~ m-1text2[]的下表范围在0~ n-1。所以,在带入下标值时,需要区分dp和text的下标。

3、使用「备忘录」(递归实现):

 //添加备忘录
 class Solution {
 public:
 	// memo 备忘录:text1[i,...] 、text2[j,...]的最长公共子序列
 	vector<vector<int>> memo;

 	int longestCommonSubsequence(string text1, string text2) {
 		for (int i = 0; i < text1.size(); i++) {			
 			memo.push_back(vector<int>(text2.size(), -1));
 		}

 		if (text1.size() == 0 || text2.size() == 0)	return 0;

 		return dp(text1, 0, text2, 0);

 	}

 	int dp(string text1, int i, string text2, int j) {
 		// base case
 		if (i == text1.size() || j == text2.size())
 			return 0;

 		// 状态转移
 		if (memo[i][j] != -1)// 如果之前计算过,则直接返回备忘录中的答案
 			return memo[i][j];

 		if (text1[i] == text2[j])
 			memo[i][j] = dp(text1, i + 1, text2, j + 1) + 1;
 			// return dp(text1, i + 1, text2, j + 1) + 1;
 		else if (text1[i] != text2[j]) {
 			memo[i][j] =  max(
 				dp(text1, i, text2, j + 1),
 				max(dp(text1, i + 1, text2, j),
 					dp(text1, i + 1, text2, j + 1))
 			);
 		}

 		return memo[i][j];
 	}
 };

进阶:空间优化

这里我们可以来练习空间优化,详细的可见前文的 状态压缩 一文。

首先,看上一小节中的使用「dp数组」实现的代码。发现在求dp[i][j]时需要[i-1][j]、[i][j-1]、[i-1][j-1]位置的值

class Solution {
public:
	int longestCommonSubsequence(string text1, string text2) {
		int m = text1.size();
		int n = text2.size();
		// dp数组:dp[i][j]表示s1[i..] 和 s2[j..] 的最长公共子序列长度
		//vector<vector<int>> dp(m + 1, vector<int>(n + 1, -1));
		vector<int> dp(n + 1, -1);

		// 初始情况:相当于dp[0][j]
		for (int j = 0; j <= n; j++) {
			dp[j] = 0;
		}

		// 状态转移
		// 注意i、j的值,因为我们多给dp数组增加了一行、一列,此时text的下标需要注意
		for (int i = 1; i <= m; i++) {
			int ii = i - 1;
			int temp = dp[0];
			dp[0] = 0; // 更新,相当于每一行的第0列
			for (int j = 1; j <= n; j++) {
				int pre = temp; // pre 实际上对应dp[i-1][j-1]
				temp = dp[j];
				int jj = j - 1;
				if (text1[ii] == text2[jj]) // 相等时
					//dp[i][j] = dp[i - 1][j - 1] + 1;
					dp[j] = pre;
				else if (text1[ii] != text2[jj]) { // 不相等时,分三种情况讨论
					//int a = dp[i - 1][j];		// 情况1:text1[i] 不在 lcs 中
					int a = dp[j];
					//int b = dp[i][j - 1];	// 情况2:text2[j] 不在 lcs 中
					int b = dp[j - 1];
					//int c = dp[i - 1][j - 1]; // 情况3:text1[i]、text2[j] 均不在 lcs 中
					int c = pre;

					dp[i][j] = max(a, max(b, c)); // 找出这三种情况最大的
				}
			}
		}

		return dp[n];

	}
};

2 字符串删除操作

力扣第 583 题「两个字符串的删除操作」,看下题目:

image-20210713170630536

函数签名如下:

int minDistance(String s1, String s2);

题目让我们计算将两个字符串变得相同的最少删除次数,那我们可以思考一下,最后这两个字符串会被删成什么样子?

答:删除的结果就是它俩的最长公共子序列嘛!

所以,我们这一题可以直接套用上一题的代码,只需要加一点点即可:

class Solution {
public:
	int longestCommonSubsequence(string text1, string text2) {
		int m = text1.size();
		int n = text2.size();
		// dp数组:dp[i][j]表示s1[i..] 和 s2[j..] 的最长公共子序列长度
		vector<vector<int>> dp(m+1, vector<int>(n+1, -1));

		// 初始情况
		for (int j = 0; j < n; j++) {
			dp[0][j] = 0;
		}
		for (int i = 0; i < m; i++) {
			dp[i][0] = 0;
		}

		// 状态转移
		// 注意i、j的值,因为我们多给dp数组增加了一行、一列,此时text的下标需要注意
		for (int i = 1; i <= m; i++) {
			int ii = i - 1;
			for (int j = 1; j <= n; j++) {
				int jj = j - 1;
				if (text1[ii] == text2[jj]) // 相等时
					dp[i][j] = dp[i - 1][j - 1] + 1;
				else if (text1[ii] != text2[jj]) { // 不相等时,分三种情况讨论
					int a = dp[i-1][j];		// 情况1:text1[i] 不在 lcs 中
					int b = dp[i][j - 1];	// 情况2:text2[j] 不在 lcs 中
					int c = dp[i-1][j - 1]; // 情况3:text1[i]、text2[j] 均不在 lcs 中

					dp[i][j] = max(a, max(b, c)); // 找出这三种情况最大的
				}
			}
		}

		// return dp[m][n];
        int LCS = dp[m][n];
        return m - LCS + n - LCS;

	}
};

3 最小 ASCII 删除和

LeetCode的712. 两个字符串的最小ASCII删除和

image-20210713172258488

这道题,和上一道题非常类似,这回不问我们删除的字符个数了,问我们删除的字符的 ASCII 码加起来是多少。

那就不能直接复用计算最长公共子序列的函数了,但是可以依照之前的思路,稍微修改 base case 和状态转移部分即可直接写出解法代码

  • 我们的dp数组含义需要修改为:将 s1[i…] 和 s2[j…] 删除成相同字符串,最小的 ASCII 码之和为 dp(s1, i, s2, j)。
class Solution {
public:
	int longestCommonSubsequence(string text1, string text2) {
		int res = 0; // 存储ASCII的值

		int m = text1.size();
		int n = text2.size();
		// dp数组:dp[i][j]表示s1[i..] 和 s2[j..] 的最长公共子序列长度
		vector<vector<int>> dp(m + 1, vector<int>(n + 1, 0));

		// 初始情况
		for (int j = 1; j <= n; j++) {
			dp[0][j] = text2[j-1] + dp[0][j - 1];
		}
		for (int i = 1; i <= m; i++) {
			dp[i][0] = text1[i-1] + dp[i - 1][0];
		}

		// 状态转移
		// 注意i、j的值,因为我们多给dp数组增加了一行、一列,此时text的下标需要注意
		for (int i = 1; i <= m; i++) {
			int ii = i - 1;
			for (int j = 1; j <= n; j++) {
				int jj = j - 1;
				if (text1[ii] == text2[jj]) // 相等时
					dp[i][j] = dp[i - 1][j - 1];
				else if (text1[ii] != text2[jj]) { // 不相等时,分三种情况讨论
					int a = dp[i - 1][j] + text1[ii];		// 情况1:text1[i] 不在 lcs 中
					int b = dp[i][j - 1] + text2[jj];	// 情况2:text2[j] 不在 lcs 中
					int c = dp[i - 1][j - 1] + text1[ii] + text2[jj]; // 情况3:text1[i]、text2[j] 均不在 lcs 中

					dp[i][j] = min(a, min(b, c)); // 找出这三种情况最大的
				}
			}
		}

		return dp[m][n];
	}
};

4 小结

至此,三道子序列问题就解决完了,关键在于将问题细化到字符,根据每两个字符是否相同来判断他们是否在结果子序列中,从而避免了对所有子序列进行穷举。

  数据结构与算法 最新文章
【力扣106】 从中序与后续遍历序列构造二叉
leetcode 322 零钱兑换
哈希的应用:海量数据处理
动态规划|最短Hamilton路径
华为机试_HJ41 称砝码【中等】【menset】【
【C与数据结构】——寒假提高每日练习Day1
基础算法——堆排序
2023王道数据结构线性表--单链表课后习题部
LeetCode 之 反转链表的一部分
【题解】lintcode必刷50题<有效的括号序列
上一篇文章      下一篇文章      查看所有文章
加:2021-07-14 23:11:55  更:2021-07-14 23:12:39 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年5日历 -2024/5/7 20:45:02-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码