机试不会考裸的最大连续子序列和,而是会考一些变形,可能需要自己去转换成最大连续子序列和,要灵活变通,不要以为背了模板就没事了,比如这里的题型训练我就没想到用最大连续子序列和🤦
今天也是为了cc,努力奋斗的一天ヾ(≧▽≦*)o
1. 问题描述
最大连续子序列和问题如下:
给定一个数字序列 A 1 A_1 A1, A 2 A_2 A2,…, A n A_n An,求i,j(1≤i≤j≤n),使得 A i A_i Ai+…+ A j A_j Aj最大,输出这个最大和。
2. 暴力解决方案
3. 动态规划解决方案
下面介绍动态规划的做法,复杂度为 O ( n ) O(n) O(n)。
3.1 步骤1——状态定义
令状态dp[i]
表示以A[i]
作为末尾的连续序列的最大和(这里是说A[i]必须作为连续序列的末尾)。
其实与我们的LIS的dp数组的定义很像很像,都是以XX结尾的。
以样例为例:序列-2 11 -4 13 -5 -2,下标分别记为0,1,2,3,4,5,那么
通过设置这么一个dp
数组,要求的最大和其实就是dp[0]
,dp[1]
,…,dp[n-1]
中的最大值(因为到底以哪个元素结尾未知),下面想办法求解dp
数组。
3.2 步骤2——状态转移方程
作如下考虑,因为dp[i]
要求是必须以A[i]
结尾的连续序列,那么只有两种情况:
- 这个最大和的连续序列只有一个元素,即以A[i]开始,以A[i]结尾。
- 这个最大和的连续序列有多个元素,即从前面某处A[p]开始(p<i),一直到A[i]结尾。
对于情况1,最大和就是A[i]
本身。
对于情况2,最大和是dp[i-1]+A[i]
,即A[p]+...+A[i-1]+A[i] = dp[i-1] + A[i]
。由于只有这两种情况,于是得到状态转移方程:
dp[i] = max{ A[i] , dp[i-1]+A[i] }
这个式子只和i和i之前的元素有关,且边界为dp[0]=A[0],由此从小到大枚举i,即可得到整个dp数组。接着输出dp[0],dp[1],…,dp[n-1]中的最大值即为最大连续子序列的和。
怎么样,是不是很神奇?只用 O ( n ) O(n) O(n)的时间复杂度就解决了原先需要 O ( n 2 ) O(n^2) O(n2)复杂度问题,这就是动态规划的魅力。
4. 代码
#include<cstdio>
#include<algorithm>
using namespace std;
const int maxn = 10010;
int A[maxn],dp[maxn]; //A[i]存放以A[i]结尾的连续序列的最大和
int main(){
int n;
scanf("%d",&n);
for(int i=0;i<n;++i){ //读入序列
scanf("%d",&A[i]);
}
//边界
dp[0] = A[0];
for(int i=1;i<n;++i){
//状态转移矩阵
dp[i] = max(A[i],A[i]+dp[i-1]);
}
//dp[i]存放以A[i]结尾的连续序列的最大和,需要遍历i得到最大的才是结果
int k=0;
for(int i=1;i<n;++i){
if(dp[i] > dp[k]){
k = i;
}
}
printf("%d\n",dp[k]);
return 0;
}
5. 状态的无后效性⭐⭐⭐⭐⭐
状态的无后效性:
- 当前状态记录了历史信息,一旦当前状态确定,就不会再改变,且未来的决策只能在已有的一个或若干个状态的基础上进行,历史信息只能通过已有的状态去影响未来的决策。
例如:
- 宇宙的历史可以看作一个关于时间的线性序列,对每一个时刻来说,宇宙的现状就是这个时刻的状态,显然宇宙过去的信息蕴含在当前状态中,并只能通过当前状态来影响下一个状态,因此从这个角度来说宇宙的关于时间的状态具有无后效性。
- 针对本节的问题来说,每次计算状态dp[i],都只会涉及dp[i-1],而不直接用到dp[i-1]蕴含的历史信息。
对动态规划可解的问题,总会有很多涉及状态的方式,但并不是所有状态都具有无后效性,因此必须设计一个拥有无后效性的状态以及相应的状态转移方程,否则动态规划就没有办法得到正确结果。事实上, 如何设计状态和状态转移方程,才是动态规划的核心,而它们也是动态规划最难的地方。
做DP题的关键,就是寻找一个好的状态。
总结一下,动态规划问题的时间复杂度由两部分组成:状态数量和状态转移复杂度,往往程序总的复杂度为它们的乘积。