数据结构与算法---动态规划---最长公共子序列

本文介绍最长公共子序列(LCS)问题的多种解决方案,包括递归、动态规划及其优化方法,如滚动数组和一维数组优化,并分析每种方法的时间和空间复杂度。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

最长公共子序列(LCS)

最长公共子序列,Longest Common Subsequence,LCS

问:求两个序列的最长公共子序列长度?

[1, 3, 5, 9, 10]和[1, 4, 9, 10]的最长公共子序列是[1, 9, 10],长度为3

1143. 最长公共子序列

是否为最值问题?

能否使用DP?
  1. 原问题分解为子问题

  2. 无后效性

使用DP步骤
  1. 确定含义
    假设两个数组分别是:nums1、nums2
    i∈[1, nums1.length];
    j∈[1, nums2.length];
    假设d(i, j)是 nums1的前i个元素nums2的前j个元素的最长公共子序列长度
    dp(2, 3)代表[1, 3]与[1, 4, 9]的最长公共子序列长度,明显为1
    dp(4, 4)代表[1, 3, 5, 9]与[1, 4, 9, 10]的最长公共子序列长度,为2
    则,求出dp(nums1.length, nums2.length)的最长公共子序列长度即可

  2. 边界值
    dp(i, 0)、dp(0, j)初始值都是0

  3. 状态转移方程
    在这里插入图片描述

如果是两个元素相等,则在前面的基础上+1即可
如果两个元素不相等,则也有可能会将最长公共子序列扩大,
例如[1, 3, 5, 9, 10],与[1, 4]的最大公共子序列为1,dp(5, 2) = 1
[1, 3, 5, 9, 10]与[1, 4, 9]增加的新数字10跟9不相等,但是最大公共子序列为2,dp(5, 3) = 2,是前i个元素,与nums2[3 - 1]组合而成的
因此,可以得出转移方程:

如果nums1[i - 1] = nums2[j - 1],那么dp(i, j) = dp(i - 1, j - 1) + 1;
如果nums1[i - 1] != nums2[j - 1],那么dp(i, j) = max{dp(i - 1, j), dp(i, j - 1)}


方法一:递归

首先,我们尝试递归解决该问题。

package dynamicProgramming;

//最长公共子序列
public class LCS {
	public static void main(String[] args)
	{
		System.out.println(lcs(new int[] {1, 3, 5, 9, 10}, new int[] {1, 4, 9, 10}));
	}
	
	/**递归*/
	static int lcs(int[] nums1, int[] nums2)
	{
		if (nums1 == null || nums1.length == 0) return 0;
		if (nums2 == null || nums2.length == 0) return 0;
		return lcs(nums1, nums1.length, nums2, nums2.length);
	}
	
	static int lcs(int[] nums1, int i, int[] nums2, int j)
	{
		if (i == 0 || j == 0) return 0;
		if (nums1[i - 1] == nums2[j - 1]) {
			return lcs(nums1, i - 1, nums2, j - 1) + 1;
		}else {
			return Math.max(lcs(nums1, i - 1, nums2, j), lcs(nums1, i, nums2, j - 1));
		}
	}
}

复杂度分析

空间复杂度:O(k),k=min{n, m},n、m是2个序列的长度

当i == 0 或者j == 0的时候,函数调用就结束了,递归深度就是m、n的最小值

时间复杂度:O(2^n),当n = m时

方法二:DP

package dynamicProgramming;

//最长公共子序列
public class LCS {
	public static void main(String[] args)
	{
		System.out.println(lcs(new int[] {1, 3, 5, 9, 10}, new int[] {1, 4, 9, 10}));
	}
	
	static int lcs(int[] nums1, int[] nums2)
	{
		if (nums1 == null || nums1.length == 0) return 0;
		if (nums2 == null || nums2.length == 0) return 0;
		
		int[][] dp = new int[nums1.length + 1][nums2.length + 1];
		
		for (int i = 1; i <= nums1.length; i++) {
			for (int j = 1; j <= nums2.length; j++) {
				if (nums1[i - 1] == nums2[j - 1]) {
					dp[i][j] = dp[i - 1][j - 1] + 1;
				}else {
					dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
				}
			}
		}
		return dp[nums1.length][nums2.length];
	}
}

复杂度分析

空间复杂度:O(n * m)
时间复杂度:O(n * m)


方法三:DP优化-滚动数组

dp数组的计算结果如下:

在这里插入图片描述
如何看该图呢?
举个例子
dp(2, 5) D = D,所以dp(2, 5) = dp(1, 4) + 1 = 1 + 1 = 2;
dp(2, 6) D != A,所以dp(2, 6) = max{dp(2, 5), dp(1, 6)} = 2;

通过观察,我们可以将dp[nums1.length][nums2.length]二维数组进行修改,改成滚动数组即可
当然,更优化的做法是使用一个一维数组即可。
一步一步来,我们先看下使用滚动数组进行优化

确定dp(i, j),能否只存储dp(i - 1, j - 1)和dp(i , j - 1)和dp(i - 1, j)三个值呢?

答:不可以
比如,dp(4, 5),你存储了dp(3, 4)、dp(4, 4)、dp(3, 5)
等到到了dp(5, 2)的时候,你已经把dp(4, 1)、dp(4, 2)扔了,没有了,因此,三个值是不能够满足找到dp(i, j)的。

package dynamicProgramming;

//最长公共子序列
public class LCS {
	public static void main(String[] args)
	{
		System.out.println(lcs2(new int[] {1, 3, 5, 9, 10}, new int[] {1, 4, 9, 10}));
	}
	
	/**DP滚动数组*/
	static int lcs2(int[] nums1, int[] nums2)
	{
		if (nums1 == null || nums1.length == 0) return 0;
		if (nums2 == null || nums2.length == 0) return 0;
		
		int[][] dp = new int[2][nums2.length + 1];
		
		for (int i = 1; i <= nums1.length; i++) {
			for (int j = 1; j <= nums2.length; j++) {
				if (nums1[i - 1] == nums2[j - 1]) {
					dp[i % 2][j] = dp[(i - 1) % 2][j - 1] + 1;
				}else {
					dp[i % 2][j] = Math.max(dp[(i - 1) % 2][j], dp[i % 2][j - 1]);
				}
			}
		}
		return dp[nums1.length % 2][nums2.length];
	}
}

复杂度分析

空间复杂度:O(2m)
时间复杂度:O(n
m)


方法四:DP优化-一维数组

在使用一维数组存储的时候,需要将j位置新算出的值,覆盖掉之前的j位置值
有个问题是,dp(2, 4)需要先保存一下,不然,找不到了。

在这里插入图片描述

package dynamicProgramming;

//最长公共子序列
public class LCS {
	public static void main(String[] args)
	{
		System.out.println(lcs3(new int[] {1, 3, 5, 9, 10}, new int[] {1, 4, 9, 10}));
	}
	
	/**DP一维数组*/
	static int lcs3(int[] nums1, int[] nums2)
	{
		if (nums1 == null || nums1.length == 0) return 0;
		if (nums2 == null || nums2.length == 0) return 0;
		
		int[] dp = new int[nums2.length + 1];
		
		for (int i = 1; i <= nums1.length; i++) {
			int cur = 0;
			for (int j = 1; j <= nums2.length; j++) {
				int leftTop = cur;//先把cur的值给leftTop
				cur = dp[j];//然后再改cur
				if (nums1[i - 1] == nums2[j - 1]) {
					dp[j] = leftTop + 1;
				}else {
					dp[j] = Math.max(dp[j], dp[j - 1]);
				}
			}
		}
		return dp[nums2.length];
	}
}

复杂度分析

空间复杂度:O(m)
时间复杂度:O(n*m)


能不能继续优化了?

方法五:DP优化-一维数组-最短的作为列

我们现在的空间复杂度为O(m)
如果n > m,也就是nums1.length > nums2.length O(m)没问题
但,如果n < m,或者n 远远小于m
比如,nums1.length = n = 5,nums2.length = m = 100000,此时,空间复杂度就是O(m) = O(100000)
其实,nums1,nums2谁做列都是可以的,也就是,此时如果是nums1做列,空间复杂度为O(5)
这就说明,我们可以将nums.length小的作为列。

package dynamicProgramming;

//最长公共子序列
public class LCS {
	public static void main(String[] args)
	{
		System.out.println(lcs4(new int[] {1, 3, 5, 9, 10}, new int[] {1, 4, 9, 10}));
	}
	
	/**DP一维数组,数组长度小的为列*/
	static int lcs4(int[] nums1, int[] nums2)
	{
		if (nums1 == null || nums1.length == 0) return 0;
		if (nums2 == null || nums2.length == 0) return 0;
		int[] rowsNums = nums1;
		int[] colsNums = nums2;
		if (nums1.length < nums2.length) {
			rowsNums = nums2;
			colsNums = nums1;
		}
		
		int[] dp = new int[colsNums.length + 1];
		
		for (int i = 1; i <= rowsNums.length; i++) {
			int cur = 0;
			for (int j = 1; j <= colsNums.length; j++) {
				int leftTop = cur;//先把cur的值给leftTop
				cur = dp[j];//然后再改cur
				if (rowsNums[i - 1] == colsNums[j - 1]) {
					dp[j] = leftTop + 1;
				}else {
					dp[j] = Math.max(dp[j], dp[j - 1]);
				}
			}
		}
		return dp[colsNums.length];
	}
}

复杂度分析

空间复杂度:O(min{n, m})
时间复杂度:O(n*m)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值