洛谷线性结构训练(4)动态规划(6):P1115 最大子段和

P1115 最大子段和

题目描述
给出一段序列,选出其中连续且非空的一段使得这段和最大。

输入格式
第一行是一个正整数NN,表示了序列的长度。

第二行包含NN个绝对值不大于10000的整数A_i ,描述了这段序列。

输出格式
一个整数,为最大的子段和是多少。子段的最小长度为11。

输入输出样例
输入 #1复制
7
2 -4 3 -1 2 -4 3
输出 #1复制
4
O(n^2)复杂度的动态规划
分析

在这个题目里,求一个最大值,很容易想到动态规划。那么这个状态的定义就十分关键。一种想法是:令dp[i]表示从[0,i]这个区间内的最大子列和的值,也就是说这个最大子列和并不一定是以i为结尾的! 那么在这种情况下,我们每次多扫描一个元素时,这个最大子列和的序列只有2种可能:
1.不含有这最后一个元素
2.含有这最后一个元素,此时的最大子列和来自于最后一个元素的后缀和的其中一个。
因此,我们就可以写出这样的转移方程

dp[i]=max{dp[i-1], backsum(i)|以第i个元素为结尾的后缀和}

最后我们输出的时候只需要输出dp[N-1]就是我们要求的(不需要额外扫描)。这个算法的复杂度为O(n^2)

#include<iostream>
#include<algorithm>
#include<climits>
using namespace std;

void SumBack(int a[], int n,int pos) {
	for (int i = pos; i >= 0; i--) {
		a[i] += n;
	}
}

int main() {
	int dp[20005];
	int backsum[20005] = {};
	int n;
	cin >> n;
	int num;
	for (int i = 0; i < n; i++) {
		cin >> num;
		if (i == 0) {
			dp[0] = num;
			backsum[0] = num;
		}
		else {
			int temp = INT_MIN;
			for (int j = i; j >= 0; j--) {
				backsum[j] += num;
				temp = max(temp, backsum[j]);
			}
			dp[i] = max(dp[i - 1], temp);
		}
	}
	cout << dp[n-1];
	return 0;
}
O(n)复杂度的动态规划
分析

在上一个解答中,我们的dp的定义是:dp[i]表示从[0,i]这个区间内的最大子列和的值,并且并不一定是以i元素结尾。在那个求解的过程中,我们会发现,到了后面有很多时候会出现dp[i]连续的等于一个值,然后在不断的扫描后缀和。虽然我们求dp[N-1]方便了(就是我们要求的值),但是这个过程其实反复求了很多次无用的东西。
于是我们换一种想法,我们将dp[i]定义为:以第i个元素结尾的所有子列的和的最大的子列和。在这个时候,dp[i]就一定含有第i个元素,并且它是以第i个元素结尾的。这样写的坏处是,我们最终求我们需要的答案的时候需要遍历整个dp,但是这样也是合理的,因为最大的子列它总是有一个结束点的!这个子问题划分也不会有重复和遗漏!
那么我们的动态方程可以写为:

dp[i]=max{dp[i-1]+num,num}

这个方程的含义是:在推导下一个元素为结束的最大子列和时,它有两种情况:
1.只含有最后一个元素
2.它含有最后一个元素,并且和前面的子列和是可以连在一起得到一个更大的子列和
这两种情况互相不重复。

这样假设后,在我们最后得到答案的时候需要重新扫描整个dp,因为我们并不知道哪个节点结束会是最大子列和。这样计算的复杂度是O(N)

#include<iostream>
#include<algorithm>
using namespace std;
int main() {
	int dp[200005];
	int N,num;
	cin >> N;
	for (int i = 0; i < N; i++) {
		cin >> num;
		if (i == 0) {
			dp[0] = num;
		}
		else {
			dp[i] = max(dp[i - 1] + num, num);
		}
	}
	int res = dp[0];
	for (int i = 0; i < N; i++) {
		res = max(res, dp[i]);
	}
	cout << res;
	return 0;
}
以上的思考来源于leetcode上一个很优秀的题解

这个题解的遍历思路划分很清晰,值得学习!

解题思路:
示例: [a, b , c, d , e]

解答这类题目, 省略不掉遍历, 因此我们先从遍历方式说起

通常我们遍历子串或者子序列有三种遍历方式

1.以某个节点为开头的所有子序列: 如 [a],[a, b],[ a, b, c] … 再从以 b 为开头的子序列开始遍历 [b] [b, c]。
2.根据子序列的长度为标杆,如先遍历出子序列长度为 1 的子序列,在遍历出长度为 2 的 等等。
3.以子序列的结束节点为基准,先遍历出以某个节点为结束的所有子序列,因为每个节点都可能会是子序列的结束节点,因此要遍历下整个序列,如: 以 b 为结束点的所有子序列: [a , b] [b] 以 c 为结束点的所有子序列: [a, b, c] [b, c] [ c ]。

第一种遍历方式通常用于暴力解法, 第二种遍历方式 leetcode (5. 最长回文子串 ) 中的解法就用到了。

第三种遍历方式 因为可以产生递推关系, 采用动态规划时, 经常通过此种遍历方式, 如 背包问题, 最大公共子串 , 这里的动态规划解法也是以 先遍历出 以某个节点为结束节点的所有子序列 的思路

对于刚接触动态规划的, 我感觉熟悉第三种遍历方式是需要抓住的核心

因为我们通常的惯性思维是以子序列的开头为基准,先遍历出以 a 为开头的所有子序列,再遍历出以 b 为开头的…但是动态规划为了找到不同子序列之间的递推关系,恰恰是以子序列的结束点为基准的,这点开阔了我们的思路。

我在网上看不少解答时,直接阅读其代码,总是感觉很理解很吃力,因为好多没有写清楚,一些遍历到底代表什么意思,看了许久仍不知所以然,下面的代码中摘录了 维基中的解释,感觉比较清楚,供大家理解参考。

作者:lao-hu-8
链接:https://leetcode-cn.com/problems/maximum-subarray/solution/xiang-xi-jie-du-dong-tai-gui-hua-de-shi-xian-yi-li/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值