力扣第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变量)进行空间置换。