题解:P1121 环状最大两段子段和_动态规划dp_算法竞赛_C++函数模板

P1121 环状最大两段子段和

Link:Luogu - P1121

题目描述

给出一段长度为 n n n 的环状序列 a a a,即认为 a 1 a_1 a1 a n a_n an 是相邻的,选出其中连续不重叠且非空的两段使得这两段和最大。

输入格式

第一行是一个整数 n n n,表示序列的长度。

第二行有 n n n 个整数,描述序列 a a a,第 i i i 个数字表示 a i a_i ai

输出格式

一行一个整数,为最大的两段子段和是多少。

样例 #1

样例输入 #1

7
2 -4 3 -1 2 -4 3

样例输出 #1

9

提示

数据规模与约定

对于全部的测试点,保证 2 ≤ n ≤ 2 × 1 0 5 2 \leq n \leq 2 \times 10^5 2n2×105 − 1 0 4 ≤ a i ≤ 1 0 4 -10^4 \leq a_i \leq 10^4 104ai104


解题思路

发现最后选的策略就两种情况:(0表示不选,+表示选)
一、00++++00+++++000
二、++00+++0000++++
对于情况一,可以求两段不相交的最大子序列和(+的部分),再加起来;
对于情况二,可以求两段不相交的最小子序列和(0的部分),再用总和减去,就是答案。

现在问题来了:如何求一个序列中两段不相交的最大/最小子序列和?
以求最大值为例:可以想象成把原序列砍成左右两部分,然后分别求左、右两部分的最大子段和,最后相加。

so,设 f [ i ] f[i] f[i] 表示原序列 a [ 1 ∼ i ] a[1 \sim i] a[1i] 的最大子段和, g [ i ] g[i] g[i] 表示 a [ n ∼ i ] a[n \sim i] a[ni] 的最大子段和(注意 g g g 数组是倒着的),
然后枚举原序列中间的分割点 k k k,对得到的所有答案取 max 即可:
a n s = m a x { f [ i ] + g [ i + 1 ] } , 1 < = i < = n − 1 ans = max\{f[i] + g[i + 1]\},1<=i<=n-1 ans=max{f[i]+g[i+1]},1<=i<=n1(值域很好理解,要是够到 n n n,那么就会访问到 g [ n + 1 ] g[n+1] g[n+1],这个地方未被赋值,会出现错误)

这样思路就很明确了。 n < = 2 × 1 0 5 , a [ i ] < = 1 0 4 n<=2 \times 10^5,a[i]<=10^4 n<=2×105,a[i]<=104,所以 n × a [ i ] < = 2 × 1 0 9 n \times a[i]<=2 \times 10^9 n×a[i]<=2×109,用 int 足够。
实现过程可以用函数模板,达到一函数两用的效果。复杂度稳定 O ( n ) O(n) O(n),但常数略大。

注意两种特殊的情况,需要特判:

  1. 数列里只有一个正数(其实是 0 0 0 也可以),其余全是负数。这样的话我们找不到两个连续的正数,所以答案应该是正数+最大的负数。
  2. 数列里全是负数,此时计算情况二时,会出现用 总和-所有负数和=总和+|所有负数和|=0 的情况。也就是说只要输入的全是负数,其输出永远是 0 0 0,答案显然不对。

Code

前方高能!
函数模板闪亮登场!!!这样可以实现一个函数,求最大值、最小值通用的效果,优雅且简单!更能装13!

具体详见代码注释。

#include <bits/stdc++.h>
using namespace std;
const int maxn = 2e5 + 7;

int n, a[maxn];

struct Max{ int operator() (int x, int y){ return (x > y? x : y); } };
struct Min{ int operator() (int x, int y){ return (x < y? x : y); } };

template <typename _Compare>
int work(_Compare cmp){
	vector <int> f(n + 1), g(n + 1);
	f[1] = a[1]; // 初始化,避免比较时和f[0]=0出现比较出现错误。下面的循环直接从2开始即可
	for(int i = 2; i <= n; i ++) f[i] = cmp(f[i - 1] + a[i], a[i]); // 此时的f[i]表示1~i中以i结尾的最大(或最小)子段和
	for(int i = 2; i <= n; i ++) f[i] = cmp(f[i], f[i - 1]); // 此时的f[i]表示1~i中的最大(或最小)子段和
	g[n] = a[n]; // 初始化,避免比较时和g[n+1]=0出现比较出现错误。下面的循环直接从n-1开始即可
	for(int i = n-1; i >= 1; i --) g[i] = cmp(g[i + 1] + a[i], a[i]); // 此时的g[i]表示n~i中以i结尾的最大(或最小)子段和
	for(int i = n-1; i >= 1; i --) g[i] = cmp(g[i + 1], g[i]); // 此时g[i]表示n~i中的最大(或最小)子段和
	
	int ans = cmp(0, -1) + INT_MIN;
    /*上面是一个骚操作:当cmp为max函数时,ans=INT_MIN+0,正好是求最大值时的初始最小值
	  当cmp为min函数时,ans=INT_MIN-1,由于int自然溢出的特性,此时ans的值应为INT_MAX,正好是求最小值时的初始最大值*/
    for(int i = 1; i <= n - 1; i ++) ans = cmp(ans, f[i] + g[i + 1]);
	return ans;
}

void solve()
{
	cin >> n;
	int sum = 0, positive = 0; // sum记录Σa[i],positive记录其中非负数的个数(包括0和正整数)
	for(int i = 1; i <= n; i ++){
        cin >> a[i], sum += a[i];
        positive += (a[i] >= 0);
    }
    if(positive == 0){ // 全是负数,答案为两个最大的负数和
        sort(a + 1, a + n + 1, greater<int>()); // 从大到小排序
        cout << a[1] + a[2] << '\n'; // 题目保证n>=2
    }
    else if(positive == 1){ // 只有一个非负数:答案为这个非负数+最大的负数
        sort(a + 1, a + n + 1, greater<int>()); // 从大到小排序
        // 排序完后,a[1]一定是那个非负数,a[2]一定是那个最大的负数
        cout << a[1] + a[2] << '\n';
    }
    else{ // 正常情况
        cout << max(work(Max()), sum - work(Min())) << '\n';
    }
}

signed main()
{
	ios :: sync_with_stdio(false), cin.tie(nullptr), cout.tie(nullptr);
	solve();
	return 0;
}

用函数作为参数的方法详见下面几篇文章:

  1. https://blog.csdn.net/zj1131190425/article/details/88528948 - C语言风格的函数指针传参
  2. https://blog.csdn.net/p942005405/article/details/84755007 - C++11起,可以用 function 类模板实现函数传参
  3. https://www.cnblogs.com/sherlock-lin/p/16200741.html - 拓展:介绍可变参数模板及函数传参(C++11起)

End

这里是 YLCHUP,谢谢大家!

广告:文章同步到本人洛谷博客;个人洛谷账号:ylch

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值