动态规划专练:力扣第718、1143题

力扣第718题-最长重复子数组

1.本题是一道考察动态规划的经典问题,设置一个二维dpnums1Size + 1nums2Size + 1数组,来记录长度为i的nums1和长度为j的nums2的最长公共子数组长度,元素初始化为0。当nums1i - 1 == nums2j - 1时说明当前元素相同,此时的最长公共子数组长度dpij就等于dpi - 1j - 1 + 1。每次循环都更新当前最长的公共子数组长度res。完整代码如下:

cpp 复制代码
 1. int findLength(int* nums1, int nums1Size, int* nums2, int nums2Size) {
 2.     // dp[i][j]:nums1前i个、nums2前j个元素,以nums1[i-1]、nums2[j-1]结尾的最长公共子数组长度
 3.     int dp[nums1Size + 1][nums2Size + 1];
 4.     // 初始化dp数组全部置0
 5.     for (int i = 0; i <= nums1Size; i++){
 6.         memset(dp[i], 0, sizeof(dp[i]));
 7.     }
 8.  
 9.     int res = 0;
10.     // 遍历nums1每一位
11.     for (int i = 1; i <= nums1Size; i++){
12.         // 遍历nums2每一位
13.         for (int j = 1; j <= nums2Size; j++){
14.             // 当前两数字相等,公共子数组长度 = 左上角dp值 + 1
15.             if (nums1[i - 1] == nums2[j - 1]){
16.                 dp[i][j] = dp[i - 1][j - 1] + 1;
17.             }
18.             // 不相等时dp[i][j]保持0,更新全局最大长度
19.             res = fmax(res, dp[i][j]);
20.         }
21.     }
22.  
23.     return res;
24. }

该算法时间复杂度和空间复杂度均为O(nums1Size * nums2Size)。

2.可以看到递推公式中当前项的dp只和上一层的有关,所以可以将二维dp数组改为一维动态dp数组。需要注意的是此时的内层循环就需要逆序遍历,防止元素被重复计算,同时当nums1i - 1 != nums2j - 1时说明连续子数组在这里断掉了,需要将当前dp值置零。完整代码如下:

cpp 复制代码
 1. int findLength(int* nums1, int nums1Size, int* nums2, int nums2Size) {
 2.     // 一维滚动dp数组,dp[j]表示nums1前i个、nums2前j个以末尾元素结尾的最长公共子数组长度
 3.     int dp[nums2Size + 1];
 4.     // 数组初始化为0
 5.     memset(dp, 0, sizeof(dp));
 6.  
 7.     int res = 0;
 8.     // 遍历nums1每一个元素
 9.     for (int i = 1; i <= nums1Size; i++){
10.         // 倒序遍历nums2,防止dp[j-1]提前被覆盖
11.         for (int j = nums2Size; j >= 1; j--){
12.             if (nums1[i - 1] == nums2[j - 1]){
13.                 // 当前元素匹配,继承左上方dp[j-1]的值并+1
14.                 dp[j] = dp[j - 1] + 1;
15.             } else {
16.                 // 元素不匹配,以当前位置结尾的公共子数组长度归零
17.                 dp[j] = 0;
18.             }
19.             // 更新全局最长公共子数组长度
20.             res = fmax(res, dp[j]);
21.         }
22.     }
23.  
24.     return res;
25. }

该算法时间复杂度为O(nums1Size * nums2Size),空间复杂度为O(nums2Size)。

力扣第1143题-最长公共子序列

1.本题和力扣第718题-最长重复子数组比较相似,区别在于本题的公共子序列不要求连续,这就代表最长公共子序列的值在dp数组中可以继承而不是清零。当text1i - 1 == text2j - 1时递推公式仍为dpij = dpi - 1j - 1 + 1,而不相等时就要比较上方或者左边的较大值来继承(从这两个方向前进一步都可以到达当前位置,所以有两种情况),递推公式为dpij = fmax(dpi - 1j, dpij - 1)。完整代码如下:

cpp 复制代码
 1. int longestCommonSubsequence(char* text1, char* text2) {
 2.     // 获取两个字符串长度
 3.     int len1 = strlen(text1);
 4.     int len2 = strlen(text2);
 5.     // dp[i][j]:text1前i个字符、text2前j个字符的最长公共子序列长度
 6.     int dp[len1 + 1][len2 + 1];
 7.     // 将dp数组全部初始化为0
 8.     for (int i = 0; i <= len1; i++){
 9.         memset(dp[i], 0, sizeof(dp[i]));
10.     }
11.  
12.     // 遍历text1每个字符
13.     for (int i = 1; i <= len1; i++){
14.         // 遍历text2每个字符
15.         for (int j = 1; j <= len2; j++){
16.             if (text1[i - 1] == text2[j - 1]){
17.                 // 字符相等,公共子序列长度等于左上角值+1
18.                 dp[i][j] = dp[i - 1][j - 1] + 1;
19.             } else {
20.                 // 字符不等,取上方或左方较大值
21.                 dp[i][j] = fmax(dp[i - 1][j], dp[i][j - 1]);
22.             }
23.         }
24.     }
25.  
26.     // 两字符串全部字符对应的最长公共子序列结果
27.     return dp[len1][len2];
28. }

该算法时间复杂度和空间复杂度均为O(len1 * len2)。

2.本题也可以使用一维动态dp数组,内层循环由于在字符不等的情况下必须比较同行左边的和上一次当前位置的值,所以dpj - 1需要使用已经更新后的值,必须使用正序遍历。同时为了避免元素被重复使用,需要一个记录之前元素的变量pre和一个记录当前元素的变量cur来辅助(之前都是通过逆序来解决)。完整代码如下:

cpp 复制代码
 1. int longestCommonSubsequence(char* text1, char* text2) {
 2.     int len1 = strlen(text1);
 3.     int len2 = strlen(text2);
 4.     // 一维滚动dp数组,dp[j]代表text1前i个字符、text2前j个字符的LCS长度
 5.     int dp[len2 + 1];
 6.     memset(dp, 0, sizeof(dp));
 7.  
 8.     for (int i = 1; i <= len1; i++){
 9.         // pre保存dp[j-1]更新前的值,等价二维dp[i-1][j-1]
10.         int pre = dp[0];
11.         for (int j = 1; j <= len2; j++){
12.             // 记录更新前的dp[j],作为下一轮j+1的pre
13.             int cur = dp[j];
14.             if (text1[i - 1] == text2[j - 1]){
15.                 // 字符匹配,取左上角pre+1
16.                 dp[j] = pre + 1;
17.             } else {
18.                 // 不匹配,取上方旧dp[j]或左侧新dp[j-1]最大值
19.                 dp[j] = fmax(dp[j], dp[j - 1]);
20.             }
21.             pre = cur;
22.         }
23.     }
24.  
25.     return dp[len2];
26. }

该算法时间复杂度为O(len1 * len2),空间复杂度为O(len2)。

3.遍历方向由状态转移方程中最严苛的依赖限制唯一决定。只要推导分支中存在任何对当前行(新数据,如dpij-1)的依赖,就强制要求正序遍历。在此强制正序的前提下,为解决同时需要上一行旧数据(如dpi-1j-1)造成的读写冲突,不改变遍历方向,而是通过引入标量缓存(即pre变量)进行空间置换。

相关推荐
ZhouDevin1 小时前
算法论文/数据集3——CLD(TMLR2025)压缩训练集,仅保留对验证集有益的样本
人工智能·深度学习·算法·计算机视觉
Tim_101 小时前
【LeetCode】29、两数相除
算法·leetcode·职场和发展
船厂电气自动化ai大模型2 小时前
AI大模型与数学/第63课:矩阵定义、矩阵加法、标量乘法(逐级精讲)
数据结构·人工智能·深度学习·线性代数·算法
余额瞒着我当琳2 小时前
算法修炼 chapter 2 双指针进阶、盛最多水的容器、有效三角形的个数、两数之和、三数之和、四数之和
算法
你压到我腿毛了6662 小时前
C语言冒泡算法(Bubble sort)
c语言·数据结构·算法
靠沿3 小时前
贪心算法专题(三)
算法·贪心算法
super大力张3 小时前
MOE基于结构的药物设计(七):Ligand R-Vectors——如何判断配体可以从哪里继续生长?
数据库·算法·cadd·moe·svl·基于结构的药物设计
lvwangshu4 小时前
P6534 [COCI 2015/2016 #1] UZASTOPNI 等差树列 题解
动态规划·图论·题解·性质题
Tongsr4 小时前
别只混淆代码:用 Kaleido 加固整个 Android Release AAB
前端·算法·github
高亦真4 小时前
今天是学习嵌入式的第34天
linux·学习·算法