leetcode动态规划中子序列问题求解分析


前言

前面几篇文章介绍了代码随想录中的背包问题,股票问题等,本篇文章介绍剩余的子序列问题。若有错误,望指正。
参考链接:link


1、300最长递增子序列

题目:
给你一个整数数组 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 。

思路分析

本文介绍的均是有关子序列的问题,是可以用动态规划解决的经典问题,子序列的问题最重要的就是确定dp数组的含义。本题中的 dp[i] 表示i之前包括 i 的并且以 nums[i] 为结尾的最长递增子序列长度,这是什么意思呢,我举个例子说明以下。我们将dp数组定义成这样的原因是:最长递增子序列一定是以nums数组中的某个数值结尾的,那么我们将以nums[i]结尾的所有最长递增子序列的长度求出来,最后娶个最大值就是整个nums数组的最长递增子序列的长度了。

以nums = [10,9,2,5,3,7,101,18]为例
dp[0]表示以10之前并以10为结尾的de最长递增子序列的长度,很明显dp[0]=1
dp[5]表示以7之前的序列并以7为结尾的最长递增子序列的长度,以7为结尾,从7前面挑选数使得形成的序列
是递增的,并且长度是最长的,很明显这个序列是237或者257,即dp[5]=3。
以上便是对dp数组具体含义的解释。

确定好dp数组之后就需要确定递推关系式,我们可以进行以下的分析。

以数组nums为例,为了方便理解,我不设定具体数值,假设数组长度为6,
即nums[0],nums[1],nums[2],nums[3],nums[4],nums[5]
假设已知dp[0],dp[1],dp[2],dp[3],dp[4],现在要求dp[5].
根据dp数组的含义,dp数组的定义是以nums[i]结尾的最长递增子序列的长度,前面5个dp已经知道,下面
新增加了一个数值,那么求解dp[5]的方法为:
很明显如果nums[5]>nums[0],那么dp[5]=dp[0]+1;
如果nums[5]>nums[1],那么dp[5]=dp[1]+1;
如果nums[5]>nums[2],那么dp[5]=dp[2]+1;
如果nums[5]>nums[3],那么dp[5]=dp[3]+1;
如果nums[5]>nums[4],那么dp[5]=dp[4]+1;
然后将这若干个dp[5]取个最大值不正是最终要求的以nums[5]为结尾的最长递增子序列的长度吗。

根据上面分析我们可以知道递推关系式如下:

j从0到i-1
if (nums[i] > nums[j]) dp[i] = max(dp[i], dp[j] + 1);

下面确定初始值的设置,根据dp的含义可知,dp数组要初始化为1.

代码及运行结果

代码如下:

#include<iostream>
#include<vector>
#include <iomanip>
using namespace std;

void print_dparr(vector<int>& dp)
{
	int col = dp.size();
	for (int j = 0;j < col;j++)
	{
		cout << std::left << setw(2) << dp[j] << " ";
	}
	cout << endl;
	cout << endl;
}

class solution
{
public:
	int getres(vector<int>& nums)
	{
		vector<int> dp(nums.size(), 1);
		int res = 0;
		for (int i = 1;i < nums.size();i++)
		{
			for (int j = 0;j < i;j++)
			{
				if (nums[i] > nums[j])
					dp[i] = max(dp[i], dp[j] + 1);
			}
			cout << "下标" << i << "的最长递增子序列长度为:" << endl;
			print_dparr(dp);
			/*cout << dp[i] << endl;
			cout << endl;*/
			if (dp[i] > res) res = dp[i];
		}
		cout << "结果为:" << endl;
		return res;
	}
};

int main()
{
	vector<int> nums = { 10, 9, 2, 5, 3, 7, 101, 18 };
	solution s;
	cout << s.getres(nums) << endl;
	return 0;
}

结果如下:

在这里插入图片描述

这里存在一个误区,虽然我每次将整个dp数组打印了出来,但其实每次求的只是dp[i]这一个值,实际的求解过程应该是下图:

在这里插入图片描述

2、674最长连续递增序列

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

连续递增的子序列 可以由两个下标 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 隔开。

思路分析

本题相比于300最长上升子序列有了一点改变,但是在大致思路上还是一致。首先确定一点就是,数组的最长连续递增序列一定是以数组中的某个元素为结尾的,所以我们只需要将以每个元素为结尾的最长连续递增序列的长度计算出来,取最大值即可。对于dp[i]的定义还是下标i之前以nums[i]为结尾的最长连续递增序列的长度。下面开始分析dp的递推关系式,我还是通过一个具体例子进行分析:

假设nums= [1,3,5,4,7],首先我们可以确定dp[0]=1,dp[1]=2,dp[3]=3,dp[4]=1
那么我们怎么求解dp[5](我们可以直接手算出来,但是为了得到递推关系式,我们还是需要确定dp[5]和前面
几个dp的关系)
根据dp数组的定义可以知道,dp[5]表示以nums[5]为结尾的最长连续递增序列的长度,请注意这个连续,既然
是连续的,那么我们只需要比较nums[5]与nums[4]的大小,如果nums[5]>nums[4]那么dp[5]=dp[4]+1,
如果nums[5]<nums[4]那么此时以nums[5]结尾的最长连续递增序列不就是nums[5]本身吗,即dp[5]=1,
其中dp[4]就是这种情况。

根据上述分析我们可以知道dp数组的递推关系式如下:

if(nums[i]>nums[i-1])
	dp[i]=dp[i-1]+1;

关于dp数组的初始化,和300题一致,全部初始化为1即可。

代码及运行结果

代码如下:

#include<iostream>
#include<vector>
#include <iomanip>
using namespace std;

void print_dparr(vector<int>& dp)
{
	int col = dp.size();
	for (int j = 0;j < col;j++)
	{
		cout << std::left << setw(2) << dp[j] << " ";
	}
	cout << endl;
	cout << endl;
}

class solution
{
public:
	int getres(vector<int>& nums)
	{
		vector<int> dp(nums.size(), 1);
		int res = 0;
		for (int i = 1;i < nums.size();i++)
		{
			if (nums[i] > nums[i - 1])
				dp[i] = dp[i - 1] + 1;
			if (dp[i] > res)
				res = dp[i];

			cout << "下标" << i << "结尾的最长连续递增序列的长度为:" << endl;
			print_dparr(dp);
		}
		cout << "最终结果为:" << endl;
		return res;
	}
};

int main()
{
	vector<int> nums = { 1,3,5,4,7 };
	solution s;
	cout << s.getres(nums) << endl;
	return 0;
}

运行结果为:

在这里插入图片描述

3、718最长重复子数组

题目:
给两个整数数组 A 和 B ,返回两个数组中公共的、长度最长的子数组的长度。
示例:
输入:
A: [1,2,3,2,1]
B: [3,2,1,4,7]
输出:3
解释:长度最长的公共子数组是 [3, 2, 1] 。

思路分析

本题相比于上面两题的不同之处在于出现了两个数组,但还是一个关于子序列的问题,经过上面两题的分析我们知道一般将dp数组定义为以nums数组中某个元素为结尾的子序列长度,而本题中有两个数组,自然会想到定义一个二维dp数组dp[i][j] ,其含义为数组A以下标i-1为结尾的子序列和数组B以下标j-1为结尾的子序列的公共最长子序列的长度(这里之所以式下标i-1和j-1是因为这样进行初始化比较方便,具体原因可以看代码随想录,在这不多做赘述)。那么我们可以确定递推关系式为:

if(A[i]==B[j])
	dp[i+1][j+1]=dp[i][j]+1;

代码及运行结果

代码为:

#include<iostream>
#include<vector>
#include <iomanip>
using namespace std;

void print_dparr(vector<int>& dp)
{
	int col = dp.size();
	for (int j = 0;j < col;j++)
	{
		cout << std::left << setw(2) << dp[j] << " ";
	}
	cout << endl;
	cout << endl;
}

class solution
{
public:
	int getres(vector<int>& A, vector<int>& B)
	{
		vector<vector<int>> dp(A.size() + 1, vector<int>(B.size() + 1, 0));
		int res = 0;
		for (int i = 1;i <= A.size();i++)
		{
			for (int j = 1;j <= B.size();j++)
			{
				if (A[i - 1] == B[j - 1])
				{
					dp[i][j] = dp[i - 1][j - 1] + 1;
				}
				if (dp[i][j] > res)
					res = dp[i][j];
			}
			cout << "A以下标" << i - 1 << "为结尾的序列与B的最长共同子数组的长度" << endl;
			print_dparr(dp[i]);

		}
		cout << "最终结果为:" << endl;
		return res;
	}
};

int main()
{
	vector<int> A = { 1, 2, 3, 2, 1 };
	vector<int> B = { 3, 2, 1, 4, 7 };
	solution s;
	cout << s.getres(A, B) << endl;
	return 0;
}

运行结果为:

在这里插入图片描述

4、1143最长公共子序列

题目:
给定两个字符串 text1 和 text2,返回这两个字符串的最长公共子序列的长度。
一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。
例如,“ace” 是 “abcde” 的子序列,但 “aec” 不是 “abcde” 的子序列。两个字符串的「公共子序列」是这两个字符串所共同拥有的子序列。
若这两个字符串没有公共子序列,则返回 0。
示例 1:
输入:text1 = “abcde”, text2 = “ace” 输出:3 解释:最长公共子序列是 “ace”,它的长度为 3。

思路分析

本题也涉及到两个字符串,自然会想到将dp数组定义为二维数组,但是此时dp数组的定义有了一定改变,按照之前的思路我们可以将dp[i][j] 定义为text1的以下标i-1为结尾的子序列和text2以j-1为结尾的子序列的最长公共子序列的长度,但是这样会使得递推关系式的计算比较麻烦,具体可以参考本文的第一题和第二题,因此需要从另一个角度定义dp数组,可以将dp[i][j]定义为0-i-1的text1字符串和0到j-1的text2字符串的最长公共子序列。因此可以分析它的递推关系式为:

很显然递推关系式分为两种情况
1.text1[i-1]=text2[j-1]
2.text1[i-1]!=text2[j-1]
如果text1[i-1]=text2[j-1]
则dp[i][j]=dp[i-1][j-1]+1;
如果text1[i-1]!=text2[j-1]
此时就需要进行一定分析,虽然text1[i-1]!=text2[j-1],但是有可能text1[i-1]=text2[j-2]
例如text1="abcb" text2="abbd"
其中text1[3]!=text2[3],此时的dp[4][4]=3
此时就可以比较"abcd""abb"以及"abc""abbd",然后取最大值就是dp[4][4]。
故dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);

代码及运行结果

代码如下:

#include<iostream>
#include<vector>
#include <iomanip>
using namespace std;

void print_dparr(vector<int>& dp)
{
	int col = dp.size();
	for (int j = 0;j < col;j++)
	{
		cout << std::left << setw(2) << dp[j] << " ";
	}
	cout << endl;
	cout << endl;
}

class solution
{
public:
	int getres(string& text1, string& text2)
	{
		vector<vector<int>> dp(text1.size() + 1, vector<int>(text2.size() + 1, 0));
		int res = 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;
				else
					dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
				if (dp[i][j] > res)
					res = dp[i][j];
			}
			cout << "text1下标" << i - 1 << "之前的序列与text2的最长公共子序列长度" << endl;
			print_dparr(dp[i]);
		}
		return res;
	}
};

int main()
{
	string text1 = "abcde";
	string text2 = "ace";
	solution s;
	cout << s.getres(text1, text2) << endl;
	return 0;
}

运行结果如下:

在这里插入图片描述

5、53.最大子序和

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

示例: 输入: [-2,1,-3,4,-1,2,1,-5,4] 输出: 6 解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。

思路分析

很显然dp数组可以定义为:dp[i] 表示以nums[i]为结尾的连续子序列的最大和,那么可以分析得到如下的递推关系式:

以数组nums=[-2,1,-3,4,-1,2,1,-5,4]为例。
显然dp[0]=-2,
dp[1]=1,dp[1]表示以nums[1]为结尾的连续子序列最大和,nums[1]前面还有一个nums[0],为什么dp[1]
不是nums[0]+nums[1]而实nums[1]呢,因为nums[1]>nums[0]+nums[1],所以很显然可以知道递推
关系式为:
dp[i]=max(dp[i-1]+nums[i],nums[i])

代码及运行结果

代码如下:

#include<iostream>
#include<vector>
#include <iomanip>
using namespace std;

void print_dparr(vector<int>& dp)
{
	int col = dp.size();
	for (int j = 0;j < col;j++)
	{
		cout << std::left << setw(3) << dp[j] << " ";
	}
	cout << endl;
	cout << endl;
}

class solution
{
public:
	int getres(vector<int>& nums)
	{
		vector<int> dp(nums.size(), 0);
		dp[0] = nums[0];
		int res = 0;
		for (int i = 1;i < nums.size();i++)
		{
			dp[i] = max(dp[i - 1] + nums[i], nums[i]);
			if (dp[i] > res)
				res = dp[i];
			cout << "以下标" << i << "为结尾的最大子序和为:" << endl;
			print_dparr(dp);
		}
		cout << "最终结果为:" << endl;
		return res;
	}
};

int main()
{
	vector<int> nums = { -2,1,-3,4,-1,2,1,-5,4 };
	solution s;
	cout << s.getres(nums) << endl;
	return 0;
}

运行结果如下:

在这里插入图片描述

115不同的子序列

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

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

题目数据保证答案符合 32 位带符号整数范围。

思路分析

本题有一定难度,主要难点在于递推关系式的确定,首先将dp数组定义为:dp[i][j]表示以下标0到下标i-1的字符串s的子序列中出现以j-1为结尾的字符串t的个数。下面分析其递推关系式:

首先很显然可以根据s[i-1]和t[j-1]是否相等来进行分析
假设s[i-1]=t[j-1],以s="ACE",t="AC"为例
按照之前几道题目的分析思维可能会想到dp[3][2]=dp[2][1]=0,但是很显然这是错的,因为dp[3][2]=1
导致这种错误的原因是我们想当然的以为s[i-1]等于t[j-1]就可以将s[i-1]与t[j-1]删掉,但是实际情况
是计算以下标i-1为结尾的字符串s的子序列中出现以j-1为结尾的字符串t的个数时,遇到s[i-1]=t[j-1]
相等的情况时,此时的个数由两部分组成,一部分为dp[i-1][j-1],一部分为dp[i-1][j],因为我们是在
s中取寻找t,因此dp[3][2]=dp[2][1]+dp[2][2]=1;
假设s[i-1]!=t[j-1],此时的情况就比较简单了,相当于两个数不匹配,那么自然而然地会用第i-2个数进行
匹配,即此时的dp[i][j]=dp[i-1][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]

初始化的方法如下:

以s="abc",t="bab"为例
dp[1][1]=dp[0][1]=0
以s="abc",t="aab"为例
dp[1][1]=dp[0][0]+dp[0][1]=1
显然dp[0][j]=0,dp[i][0]=1

代码及运行结果

代码如下:

#include<iostream>
#include<vector>
#include <iomanip>
using namespace std;

void print_dparr(vector<int>& dp)
{
	int col = dp.size();
	for (int j = 0;j < col;j++)
	{
		cout << std::left << setw(3) << dp[j] << " ";
	}
	cout << endl;
	cout << endl;
}

class solution
{
public:
	int getres(string& s, string& t)
	{
		vector<vector<int>> dp(s.size() + 1, vector<int>(t.size() + 1, 0));
		for (int i = 0;i < dp.size();i++) dp[i][0] = 1;
		for (int i = 1;i < dp[0].size();i++) dp[0][i] = 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];
			}
			cout << "下标0到下标" << i - 1 << "的s的子序列中出现以下标0到下标j-1的t的次数为:" << endl;
			print_dparr(dp[i]);
		}
		cout << "最终结果为:" << endl;
		return dp[s.size()][t.size()];
	}
};

int main()
{
	string s = "rabbbit";
	string t = "rabbit";
	solution sol;
	cout << sol.getres(s, t) << endl;
	return 0;
}

运行结果如下:

在这里插入图片描述

7、1035不相交的线

思路分析

代码及运行结果


总结

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值