P1121环状最大两段子段和

P1121环状最大两段子段和

题目

image-20210307115607131

假定一个小白看这道题,一步步了解相关知识

最大子段和(DP,O(n))

状态dp[i]表示以A[i]作结尾的最大连续子序列和,求出dp数组的最大值即题解

dp[1] = a[1];	//边界条件
for(int i = 2; i <= n; ++i)
{
    dp[i] = max(dp[i - 1] + a[i], a[i]);	//状态转移方程
}
sort(dp + 1, dp + n + 1);	//找到dp数组的最大值
ans1 = dp[n];

模板题P1115

拓展题PAT1045

拓展题题解

环状最大子段和

a[n]a[1]相邻,取得的最大子段分两种情况

  • 最大子段没有跨过a[n]a[1],即和非环状最大子段和的结果是一样的,直接使用DP可解,得ans1
  • 最大子段跨过了a[n]a[1],此时,问题转化为求最小子段和min1,然后我们求整个序列的和sum,得ans2 = sum - min1

ans1ans2的最大值就是题解

有些初学者难以理解ans2 = sum - min1,它的前提条件是序列是一个环状,这个环上的数字的总和是确定的,我们从这个环上剔除和最小的子段,剩下子段的和自然就是最大子段和(这里,我们需要清楚,最大子段和最小子段只有一个可以跨过序列首和尾的,因为这个转折点只有一个),举个栗子

image-20210307151917191

这个序列的和sum = 9,最小序列(未跨)和是min = a[2] + a[3] = -7,那么最大序列(跨过)和就是ans2 = sum - min = 16 = a[4] + a[5] + a[1]

最小(负数)子段和(DP,O(n))

和求最大子段和类似,状态dp[i]表示以a[i]结尾的最小子段和,分两种情况求dp[i]

  • 如果dp[i - 1] > 0那么a[i]就自立门户,dp[i] = a[i]

  • 否则,就将a[i]接在a[i - 1]后面,dp[i] = a[i] + dp[i - 1]

    下面是利用最小子段和求环状最大子段和

dp[1] = a[1];	//边界条件
sum = a[1];
for(int i = 2; i <= n; ++i)
{
    dp[i] = min(a[i], dp[i - 1] + a[i]);	//状态转移方程
    sum += a[i];
}
sort(dp + 1, dp + n + 1);
if(dp[1] < 0)
	min1 = dp[1];	//找到dp数组最小值
else
    min1 = 0;
ans2 = sum - min1;	//求得ans2

cout << max(ans1, ans2);

最大双子段和P2642

维护两个数组dp1dp2dp1[i]表示以a[i]为结尾的最大和子序列,dp2[j]表示以a[j]为首的最大和子序列,显然,它们的边界分别是dp1[1] = a[1]dp2[n] = a[n],状态转移方程套用最大子段和的即可

求出这两个数组后,我们枚举求出dp1[i] + dp2[j]的最大值就是我们找的最大双子段和,注意,要确保j > i + 1,因为这两个子段是不能连在一起的

结果是,部分超时,因为枚举的复杂度高达O(n2),这道题的规模1e6 > 1e4,我们要加以改进

image-20210307164633783

一种方法是,我们将dp[i]表示以a[i]为结尾的最大子序列和改为a[1]~a[i]范围内的最大子序列和,将求得的dp[i]经过dp[i] = max(dp[i - 1], dp[i])处理即可,以此求得的dp必是单调递增,记录一轮**O(n)**的遍历就可求出结果

dp1[1] = a[1];
for(int i = 2; i <= n; ++i)
{
    dp1[i] = max(a[i], dp1[i - 1] + a[i]);
    //加一步处理
    dp1[i] = max(dp1[i], dp1[i - 1]);
}

dp2[n] = a[n];
for(int i = n - 1; i >= 1; --i)
{
    dp2[i] = max(a[i], dp2[i + 1] + a[i]);
    //加一步处理
    dp2[i] = max(dp2[i], dp2[i + 1]);
}

然后WA了五个点,怀疑人生

image-20210307195037408

错在求解dp的两步不能同时在一趟遍历中走完,因为如果我们求解dp[i]表示以a[i]结尾的最大子序列和时,前面的dp[j]j < i)已经处理成a[1]~a[i]范围内的最大子序列和,那么求得的dp[i]就有可能是错误的(dp[i - 1] + a[i]dp[i - 1]代表的最大子序列和可能和a[i]不连续)

AC代码

#include <bits/stdc++.h>
using namespace::std;
const int maxn = 1e6 + 5;
int a[maxn], dp1[maxn], dp2[maxn], ans;
int main()
{
    int n;
    cin >> n;
    for(int i = 1; i <= n; ++i)
        cin >> a[i];


    dp1[1] = a[1];
    for(int i = 2; i <= n; ++i)
        dp1[i] = max(a[i], dp1[i - 1] + a[i]);
    for(int i = 2; i <= n; ++i)
        dp1[i] = max(dp1[i], dp1[i - 1]);
    

    dp2[n] = a[n];
    for(int i = n - 1; i >= 1; --i)
        dp2[i] = max(a[i], dp2[i + 1] + a[i]);
    for(int i = n - 1; i >= 1; --i)
        dp2[i] = max(dp2[i], dp2[i + 1]);


    ans = dp1[1] + dp2[3];  //相当于i = 2的情况
    for(int i = 3; i <= n - 1; ++i)
    {
        ans = max(ans, dp1[i - 1] + dp2[i + 1]);
    }

    cout << ans;
    return 0;
}

主问题(环状+两子段)

解决了上面几个子问题,这题的解决方法就呼之欲出了

注意题意,这两个子段可以相邻,两个子段的长度和必须大于2

#include <bits/stdc++.h>
using namespace::std;
const int maxn = 2e5 + 5;
int dp1[maxn], dp2[maxn], a[maxn], sum;
int mdp1[maxn], mdp2[maxn], mmin;
int main()
{
    int n, ans1, ans2;
    cin >> n;
    for(int i = 1; i <= n; ++i)
    {
        cin >> a[i];
        sum += a[i];    //顺便求和
    }

    //如果最大双子段都没有跨过分界
    //按照最大双子段和去求即可
    //注意本题这两个子段可以相邻,注意枚举时的改动
    dp1[1] = a[1];
    for(int i = 2; i <= n; ++i)
        dp1[i] = max(a[i], dp1[i - 1] + a[i]);
    for(int i = 2; i <= n; ++i)
        dp1[i] = max(dp1[i], dp1[i - 1]);
    dp2[n] = a[n];
    for(int i = n - 1; i >= 1; --i)
        dp2[i] = max(a[i], dp2[i + 1] + a[i]);
    for(int i = n -  1; i >= 1; --i)
        dp2[i] = max(dp2[i], dp2[i + 1]);
    ans1 = dp1[1] + dp2[n];
    for(int i = 2; i <= n - 1; ++i)
        ans1 = max(ans1, dp1[i] + dp2[i + 1]);

    //如果最大双子段跨过分界
    //我们求出最小双子段和min,用sum - min即可
    mdp1[1] = a[1];
    for(int i = 2; i <= n; ++i)
        mdp1[i] = min(min(a[i], 0), mdp1[i - 1] + a[i]);
    for(int i = 2; i <= n; ++i)
        mdp1[i] = min(mdp1[i], mdp1[i - 1]);
    mdp2[n] = a[n];
    for(int i = n - 1; i >= 1; --i)
        mdp2[i] = min(min(a[i], 0), a[i] + mdp2[i + 1]);
    for(int i = n - 1; i >= 1; --i)
        mdp2[i] = min(mdp2[i], mdp2[i + 1]);
    mmin = mdp1[1] + mdp2[2];
    for(int i = 2; i <= n - 1; ++i)
        mmin = min(mmin, mdp1[i] + mdp2[i + 1]);
    ans2 = sum - mmin;

    //确保全为非正数的情况下两个子序列至少共有两个元素
    sort(a + 1, a + n + 1);
    if(a[n] <= 0)
        cout << a[n - 1] + a[n];
    else
        cout << max(ans1, ans2);
    return 0;
}
  • 6
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值