常见动态规划模型【最大子段和、LIS、LCS】

本文深入探讨了最大子段和、最长上升子序列和最长公共子序列的算法原理与实现,通过实例详细讲解了动态规划方法的应用。

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

最大子段和

例1:下面数列的最大子段和是多少
-2,11,-4,13,-5,-2

概念:给定一个由数字组成的序列,其中连续的一段子序列称为一个子段,子段中的所有数之和称为子段和,这里只考虑非空子段,即至少包含一个元素的子段。

暴力方法

  • 1.最暴力的算法,就是枚举两个端点,遍历所选出的子段求和。枚举端点复杂度为O(n2)O(n^2)O(n2),求一个子段的和,复杂度为O(n)O(n)O(n),因此时间复杂度为O(n3)O(n^3)O(n3)
  • 2.求一个子段和可以预处理前缀和进行优化,将这一部分复杂度将为O(1)O(1)O(1),总时间复杂度降为O(n2)O(n^2)O(n2)

动态规划算法

分析

  • 对于全是非正数的序列,很明显结果就是其中元素的最大值
  • 对于有正数的序列,考虑以每一个点为结尾的最大子段和,这个子段一定满足其前缀和为非负,因为如果有一个前缀是负的,那么减掉这个前缀对于这个点一定更优,并且这个子段要尽量向前延伸

实现

  • 所以我们可以使用一次扫描,记录目前统计的sumsumsum及答案ansansans。当sumsumsum加上当前位置数如果还是正数就继续累加sumsumsum,否则将sumsumsum置为0.这样可以舍去所有前缀为负数的情况,并且保证这个子段尽可能长了,每一次sumsumsum如果比ansansans大的话就更新ansansans,这样就得到了最大子段和
  • 时间复杂度为O(N)O(N)O(N)

完整实现

#include <iostream>
#include <algorithm>
using namespace std;
const int inf = 0x7fffffff;
int num[10];
int main() {
	int n;
	cin >> n;
	for (int i = 0; i < n; ++i) {
		cin >> num[i];
	}
	int ans = -inf;
	//先标记ans为num数组中最大值
	for (int i = 0; i < n; ++i) {
		ans = max(ans, num[i]);
	}
	if (ans <= 0) { //最大值小于0则输出
		cout << ans << endl;
	} else {
		int sum = 0;
		for (int i = 0; i < n; ++i) {
			//前缀和为负,将sum置为0
			if (sum + num[i] < 0) {
				sum = 0;
			} else {  //否则继续累加
				sum += num[i];
			}
			ans = max(ans, sum);
		}
	}
	cout << ans << endl;
	return 0;
}

inputinputinput:

6
-2 11 -4 13 -5 -2

outputoutputoutput:

20

最长上升子序列(LIS)

例2:在序列5,2,7,9,4,5,7,10中,最大上升子序列的长度为

概念:在原序列取任意项,不改变他们在原来数列的先后次序,得到的序列称为原序列的子序列。最长上升子序列,就是给定序列中一个最长的、数值从低到高排列的子排列,最长上升子序列不一定是唯一的。例如:序列2,1,5,3,6,4,6,3的最长上升子序列为1,3,4,6和2,3,4,6,长度均为4

分析

  • 先确定动态规划的状态,这个问题可以用序列某一项作为结尾来作为一个状态。用dp[i]dp[i]dp[i]表示一定以第iii项结尾的最长上升子序列。用a[i]a[i]a[i]表示第iii项的值,如果有j&lt;ij &lt; ij<ia[j]&lt;a[i]a[j] &lt; a[i]a[j]<a[i],那么把第i项接到第j项后面构成的子序列长度为dp[i]=dp[j]+1dp[i] = dp[j] + 1dp[i]=dp[j]+1
  • 要使dp[i]dp[i]dp[i]为以iii结尾的最长上升子序列,需要枚举所有满足条件的jjj。所以状态转移方程为:
	dp[i] = max(dp[i], dp[j] + 1), 1 <= j < i && a[j] < a[i]
  • 最后,dpdpdp数组中的最大值就是最大上升子序列的长度了
  • 时间复杂度为O(n2)O(n^2)O(n2)

根据上述状态转移方程可以得到下表:

i12345678
a[i]a[i]a[i]527945710
dp[i]dp[i]dp[i]11232345

完整实现

#include <iostream>
using namespace std;
int dp[101], a[101], n;
int LIS() {
	int ans = 0;
	for (int i = 1; i <= n; ++i) {
		dp[i] = 1;
		for (int j = 1; j < i; ++j) {
			if (a[j] < a[i]) {
				dp[i] = max(dp[i], dp[j] + 1);
			}
		}
		ans = max(ans, dp[i]);  //ans记录当前位置的最大上升子序列
	}
	return ans;
}
int main() {
	cin >> n;
	for (int i = 1; i <= n; ++i) {
		cin >> a[i];
	}
	cout << LIS() << endl;
	return 0;
}

inputinputinput:

8
5 2 7 9 4 5 7 10

outputoutputoutput:

5

最长公共子序列(LCS)

最长公共子序列:给定两个序列S1S_1S1S2S_2S2,求两者的公共子序列S3S_3S3的最长的长度

分析

  • 有了前面的基础,可以发现这个问题仍然可以按照序列的长度来划分状态,也就是S1S_1S1的前iii个字符和S2S_2S2的前jjj个字符的最长公共子序列长度,记为lcs[i][j]lcs[i][j]lcs[i][j]
  • 如果S1S_1S1的第i项,和S2S_2S2的第jjj项相等,那么S1[i]S_1[i]S1[i]S2[j]S_2[j]S2[j]作为公共子序列的末尾,则:
	lcs[i][j] = lcs[i - 1][j - 1] + 1
  • 也可以不让S1[i]S_1[i]S1[i]Si[j]S_i[j]Si[j]作为公共子序列的末尾,则:
	lcs[i][j] = max(lcs[i][j - 1], lcs[i - 1][j])

转移方程:lcs[i][j]={lcs[i−1][j−1]+1S1[i]=S2[j]max⁡{lcs[i][j−1],lcs[i−1][j]}S1[i]≠S2[j]lcs[i][j] = \begin{cases} lcs[i - 1][j - 1] + 1&amp; S_1[i] = S_2[j] \\ \max\{ lcs[i][j - 1], lcs[i - 1][j]\} &amp; S_1[i] \neq S_2[j] \end{cases}lcs[i][j]={lcs[i1][j1]+1max{lcs[i][j1],lcs[i1][j]}S1[i]=S2[j]S1[i]̸=S2[j]

举个例子,两个序列S1S_1S1 = abcfbcabcfbcabcfbc, S2S_2S2 = abfcababfcababfcab,根据状态转移方程可得下表:

lcslcslcs01(aaa)2(bbb)3(ccc)4(fff)5(bbb)6(ccc)
00000000
1(aaa)0111111
2(bbb)0122222
3(fff)0122333
4(ccc)0123334
5(aaa)0123334
6(bbb)0123344

完整实现:

#include <iostream>
#include <cstring>
#include <string>
using namespace std;
int dp[101][101];
int main() {
	string a, b;
	cin >> a >> b;
	int lena = a.size();
	int lenb = b.size();
	for (int i = 1; i <= lena; ++i) {
		for (int j = 1; j <= lenb; ++j) {
			if (a[i - 1] == b[j - 1]) {
				dp[i][j] = dp[i - 1][j - 1] + 1;
			} else {
				dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
			}
		}
	}
	cout << dp[lena][lenb] << endl;
	return 0;
}

inputinputinput:

abcdefgh
acjlfabhh

outputoutputoutput:

4

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值