最大子段和
前缀和
利用前缀和求解,计算前缀和 s u m sum sum,枚举区间 [ l , r ] [l,r] [l,r],时间复杂度O(n^2)
int maxSubArray(vector<int>& nums) {
if(nums.empty())
return 0;
vector<int> sum(nums.size(),0);
for(int i=0;i<nums.size();i++)
if(i!=0)
sum[i]=sum[i-1]+nums[i];
else
sum[i]=nums[i];
int n = nums.size(),ans = nums[0];
for(int i=0;i<n;i++)
{
for(int j=i;j<n;j++)
{
if(i!=0)
ans=max(ans,sum[j]-sum[i-1]);
else
ans=max(ans,sum[j]);
}
}
return ans;
}
分治算法:
一个序列可以分成两段,那么一个最大子段和有三种可能,分别是在左侧区间,在右侧区间,以及横跨两个区间。如果横跨两个区间,那么需要从中间位置mid连续的向两侧扩展并求和,同时维护最大值。
最终比左端区间,右端区间以及横跨中间部分的子段和即可
int solve(int L,int R,vector<int>& A)
{
if(R-L==1)
return A[L];
int m = L + (R-L)/2;
int a = solve(L,m,A);
int b = solve(m,R,A);
int Max = max(a,b);
int tmp = 0,lhs=A[m-1],rhs=A[m];
for(int i=m-1;i>=L;i--)
{
tmp+=A[i];
lhs = max(lhs,tmp);
}
tmp = 0;
for(int i=m;i<R;i++)
{
tmp+=A[i];
rhs = max(rhs,tmp);
}
return max(Max,rhs+lhs);
}
int maxSubArray(vector<int>& nums) {
if(nums.empty())
return 0;
int ans = solve(0,nums.size(),nums);
return ans;
}
动态规划:
设置状态 d p [ i ] dp[i] dp[i]表示枚举到当前位置时的最大子段和,那么可以选择的转移状态有两种,一种是将第i个数加到前面的子段和,另一种是第i个数自己成为一个子段和。最终得到的 d p [ n ] dp[n] dp[n]不是最优解,最优解是计算的过程中产生的。
int maxSubArray(vector<int>& nums) {
if(nums.empty())
return 0;
vector<int> dp(nums.size(),0);
int ans = nums.front();
for(int i=0;i<nums.size();i++)
{
if(i!=0)
{
dp[i]=max(dp[i-1]+nums[i],nums[i]);
ans=max(ans,dp[i]);
}
else
{
dp[i] = nums[0];
}
}
return ans;
}
这里状态转移方程每次查找的状态都是上一个状态,即 d p [ i − 1 ] + n u m s [ i ] dp[i-1]+nums[i] dp[i−1]+nums[i],所以可以使用常数维持之前累加的求和项
int maxSubArray(vector<int>& nums) {
if(nums.empty())
return 0;
int res = 0;
int ans = nums.front();
for(int i=0;i<nums.size();i++)
{
if(i!=0)
{
res = max(res+nums[i],nums[i]);
ans=max(ans,res);
}
else
{
res = nums[0];
}
}
return ans;
}
观察程序可以发现,每次都是进行累加,并判断累加到
r
e
s
+
n
u
m
s
[
i
]
res+nums[i]
res+nums[i]与
n
u
m
s
[
i
]
nums[i]
nums[i]比较,所以可以直接求累加和,如果当前累加和res大于0,那么直接继续累加。如果当前求和res小于0,无论
n
u
m
s
[
i
]
nums[i]
nums[i]是否小于0都不直接更新为
n
u
m
s
[
i
]
nums[i]
nums[i]
计算的同时要维护当前的最大值
int maxSubArray(vector<int>& nums) {
if(nums.empty())
return 0;
int res = 0;
int ans = nums.front();
for(int i=0;i<nums.size();i++)
{
if(res>0)
res+=nums[i];
else
res=nums[i];
ans=max(res,ans);
}
return ans;
}
leetcode152 乘积最大子序列:
类似于上面的最大子段和的思想
需要维持两个变量分别为res1与res2
其中res1保存当前最大子段和,res2保存当前的最小子段和
如果遇到负数,那么继续相乘,最大子段和会和最小子段和调换
int maxProduct(vector<int>& nums)
{
if(nums.empty())
return 0;
int res1=1,res2=1 ;
int ans = nums[0];
for(int i=0;i<nums.size();i++)
{
if(nums[i]<0)
swap(res1,res2);
res1=max(res1*nums[i],nums[i]);
res2=min(res2*nums[i],nums[i]);
ans=max(res1,ans);
}
return ans;
}
cf Edu 63 D. Beautiful Array
题目和解答
给你n个整数a[i],然后给你一个数x,你可以将其中某个连续的子段乘上x,只能乘一次,现在让你求这个序列的最大字段和。
同样是求解最大子段和的变种问题,在计算很容易相当转移过程:
即,当前的连续序列是否需要乘以x
设置状态
d
p
[
i
]
[
0
]
dp[i][0]
dp[i][0]表示前i个序列求子序列最大的过程中,从未乘以过x(1)
设置状态
d
p
[
i
]
[
1
]
dp[i][1]
dp[i][1]表示前i个序列求子序列最大的过程中,乘以过x(2)
如果仅设置如上面两个状态方程,是有问题的
因为(2)的状态转移方程在计算第i个状态时会有两钟状态转移,类似于求解最大子段和,即
第一,对前面已经乘以x的连续子序列,继续乘以x d p [ i − 1 ] [ 1 ] + a [ i ] ∗ x dp[i-1][1]+a[i]*x dp[i−1][1]+a[i]∗x,这里要求第 i − 1 i-1 i−1个状态也是乘以过x的;(3)
第二,仅对当前的 a [ i ] a[i] a[i]乘以x,即 d p [ i − 1 ] [ 0 ] + a [ i ] ∗ x dp[i-1][0]+a[i]*x dp[i−1][0]+a[i]∗x(3)
在(3)中存在这样一个问题,可能在第 i − 1 i-1 i−1个状态之前有某个连续的子序列乘以过x,但是在第 i − 1 i-1 i−1个状态断了,即 a [ i − 1 ] a[i-1] a[i−1]没有乘以过x,因为x只能乘1次,所以这样处理是有问题的
解决方案?
将问题分解,多加一个状态
设置
d
p
[
i
]
[
0
]
dp[i][0]
dp[i][0]表示不乘以x的最大子段和
设置
d
p
[
i
]
[
1
]
dp[i][1]
dp[i][1]表示,在第i个状态之前乘以过x,但是第i个状态断了
设置
d
p
[
i
]
[
2
]
dp[i][2]
dp[i][2]表示,表示第i个状态正在乘以x
那么状态转移方程有如下
d
p
[
i
]
[
0
]
=
m
a
x
(
d
p
[
i
−
1
]
[
0
]
+
a
[
i
]
,
a
[
i
]
)
;
dp[i][0] = max(dp[i - 1][0] + a[i], a[i]);
dp[i][0]=max(dp[i−1][0]+a[i],a[i]);
d p [ i ] [ 2 ] = m a x ( d p [ i − 1 ] [ 0 ] + a [ i ] ∗ x , m a x ( d p [ i − 1 ] [ 2 ] + a [ i ] ∗ x , a [ i ] ∗ x ) ) ; dp[i][2] = max(dp[i - 1][0] + a[i] * x, max(dp[i - 1][2] + a[i] * x, a[i] * x)); dp[i][2]=max(dp[i−1][0]+a[i]∗x,max(dp[i−1][2]+a[i]∗x,a[i]∗x));
d p [ i ] [ 1 ] = m a x ( d p [ i − 1 ] [ 1 ] + a [ i ] , d p [ i − 1 ] [ 2 ] + a [ i ] ) ; dp[i][1] = max(dp[i - 1][1] + a[i], dp[i - 1][2] + a[i]); dp[i][1]=max(dp[i−1][1]+a[i],dp[i−1][2]+a[i]);
代码见上面的连接
循环子段和:
记得当时大二还是大一,参加校赛的时候做过,记忆犹新
给你n个数,首尾相连,然呢求这个环的最大子段和
其实这个问题可以借鉴一下那个分治的思想,分治算法的意义不仅仅是能找到一个logn时间复杂的解决方案,而是能把问题分解开,至于是不是分解成相似的子问题无所谓。就像上面那道题,如果能把问题分解,且做到不漏解,那么解决这些子问题后就能得到答案。
此题可以将计算方案分解成两个,即最大子段和所在的位置分别是在[1,n]之间,与横跨首位两段的区间
第一个子问题很好搞,就是裸的最大子段和。
第二个问题,如果最大子段和的区横跨首位两端,说明中间剩下的那部分肯定是最小子段和,如果把中间这些数加进去,一定会使最大子段和减小。那么,只需要求解这个序列的最小子段和,并用这n个数的总和减去这个最小子段和就是跨区间的结果了。
两个子问题取最大值即可
代码没必要写了
to be continue~