P11963 [GESP202503 六级] 环线

记录127

#include<bits/stdc++.h>
using namespace std;
#define ll long long //定义长整型别名ll,防止数据累加时溢出
const int N=2e5+10;//定义常量N,表示车站的最大数量
ll a[N]; //定义数组a,用来存储每个车站的快乐值
int main(){ //方法一:数学推导(总和 - 最小子段和)分类讨论法 
	ios::sync_with_stdio(false);
	cin.tie(0);
	int n; //变量n,表示车站的总数
	cin>>n;
	ll sum=0; //定义变量sum,用来记录所有车站快乐值的总和
	ll max_val=INT_MIN;//定义变量max_val,记录数组中的最大单点值(处理全负数的情况)
	for(int i=1;i<=n;i++){//循环读入每个车站的快乐值
		cin>>a[i];//读入第i个车站的快乐值
		sum+=a[i];//累加到总和中
		max_val=max(max_val,a[i]);//更新最大单点值
	}
	if(max_val<0){ //如果所有车站的快乐值都是负数
		cout<<max_val; //直接输出最大的那个负数(只经过一个车站) 
		return 0; //程序提前结束
	}
	ll max_sum=a[1];//定义max_sum,表示不跨越接头时的最大子段和,初始化为第一个元素
	ll cur_max=a[1];//定义cur_max,表示以当前车站结尾的最大子段和
	ll min_sum=a[1];//定义min_sum,表示不跨越接头时的最小子段和,初始化为第一个元素
	ll cur_min=a[1];//定义cur_min,表示以当前车站结尾的最小子段和
	for(int i=2;i<=n;i++){//从第二个车站开始遍历,进行动态规划
		cur_max=max(a[i],cur_max+a[i]);;//状态转移:要么从当前车站重新开始,要么延续前面的子段
		max_sum=max(max_sum,cur_max);//更新全局的最大子段和
		cur_min=min(a[i],cur_min+a[i]);//状态转移:求最小子段和,要么重新开始,要么延续
		min_sum=min(min_sum,cur_min);//更新全局的最小子段和
	}
	//核心公式:比较“不跨越接头的最大值”和“跨越接头的最大值(总和-最小子段和)”
	ll ans=max(max_sum,sum-min_sum);
	cout<<ans;//输出最终能获得的最大快乐值
	return 0;// 结束程序
}

题目传送门https://www.luogu.com.cn/problem/P11963


前言

我是一名专注信奥赛(CSP-J/S、NOIP)的教练。

  • 如果你觉得这篇题解对你有帮助,欢迎点击关注我的CSDN账号,我会持续更新高质量算法解析。
  • 我深知算法思维的构建远比单纯通过题目更重要,本系列题解不局限于AC代码的堆砌,而是致力于拆解题目背后的逻辑链条与核心知识点
  • 备赛路上若遇瓶颈,欢迎随时评论或私信,我将甄选典型疑难问题,通过视频讲解或撰写专项文章的形式,为你提供深度答疑。

核心解题思路

这道题是经典的“环形数组最大子段和”问题。由于地铁线路是一个环,小 A 的行程只有两种可能的情况:

  1. 不跨越首尾(不绕圈):行程完全在 11 到 nn 的线性区间内。这等同于求普通数组的最大子段和
  2. 跨越首尾(绕圈):行程从某个车站 ii 出发,经过 n 号车站,再回到 1 号车站,最终在车站 jj 结束。这意味着小 A 跳过了中间的一段连续车站(从 j+1 到 i−1 )。为了让绕圈的收益最大,被跳过的这段车站的快乐值总和必须最小。因此,这种情况下的最大收益等于 数组总和 - 最小子段和

综合这两种情况,我们只需要利用 Kadane 算法(动态规划)分别求出最大子段和与最小子段和,再取较大值即可。同时,需要特别处理所有车站快乐值均为负数的边界情况。


代码分块详细解释

1. 头文件、常量定义与输入预处理

#include<bits/stdc++.h>
using namespace std;
#define ll long long // 定义长整型别名 ll,防止多个大数累加时发生整数溢出
const int N=2e5+10; // 定义常量 N,表示车站的最大数量
ll a[N]; // 定义数组 a,用来存储每个车站的快乐值

int main(){ 
    ios::sync_with_stdio(false);
    cin.tie(0); // 关闭输入输出同步,提升读取速度
    int n; // 变量 n,表示车站的总数
    cin>>n;
    ll sum=0; // 定义变量 sum,用来记录所有车站快乐值的总和
    ll max_val=INT_MIN; // 定义变量 max_val,记录数组中的最大单点值(用于处理全负数情况)
    for(int i=1;i<=n;i++){ // 循环读入每个车站的快乐值
        cin>>a[i]; // 读入第 i 个车站的快乐值
        sum+=a[i]; // 累加到总和中
        max_val=max(max_val,a[i]); // 更新最大单点值
    }
  • 作用:完成数据读入,同时顺带计算出数组总和 sum 以及数组中的最大值 max_val

2. 边界特判:全负数情况

    if(max_val<0){ // 如果所有车站的快乐值都是负数
        cout<<max_val; // 直接输出最大的那个负数(小 A 只经过一个车站) 
        return 0; // 程序提前结束
    }
  • 作用:处理极端边界。如果所有数都是负数,绕圈(总和 - 最小子段和)会导致算出 0(即跳过所有车站),但题目要求“至少经过一个车站”,因此直接返回最大的负数。

3. 核心逻辑:Kadane 算法求最大与最小子段和

    ll max_sum=a[1]; // 定义 max_sum,表示不跨越接头时的最大子段和,初始化为第一个元素
    ll cur_max=a[1]; // 定义 cur_max,表示以当前车站结尾的最大子段和
    ll min_sum=a[1]; // 定义 min_sum,表示不跨越接头时的最小子段和,初始化为第一个元素
    ll cur_min=a[1]; // 定义 cur_min,表示以当前车站结尾的最小子段和
    
    for(int i=2;i<=n;i++){ // 从第二个车站开始遍历,进行动态规划
        // 状态转移:要么从当前车站重新开始,要么延续前面的子段
        cur_max=max(a[i],cur_max+a[i]);
        max_sum=max(max_sum,cur_max); // 更新全局的最大子段和
        
        // 状态转移:求最小子段和,要么重新开始,要么延续
        cur_min=min(a[i],cur_min+a[i]);
        min_sum=min(min_sum,cur_min); // 更新全局的最小子段和
    }
  • 作用:在一次 O(N)的线性遍历中,同时维护并计算出“最大连续子段和”与“最小连续子段和”。

4. 结果计算与输出

    // 核心公式:比较“不跨越接头的最大值”和“跨越接头的最大值(总和-最小子段和)”
    ll ans=max(max_sum,sum-min_sum);
    cout<<ans; // 输出最终能获得的最大快乐值
    return 0; // 结束程序
}
  • 作用:将两种情况的最优解进行比较,输出全局最大值。

核心逻辑总结表

代码模块核心变量/操作精炼作用解决的痛点
数据预处理sum 与 max_val累加总和与记录最大单点值为后续公式计算提供基础数据,并提前拦截全负数边界
全负数特判if(max_val<0)直接输出最大负数并终止避免“总和-最小子段和”计算出 0,违反“至少经过一站”的题意
最大子段和cur_max 与 max_sumKadane 算法求最大连续和解决不跨越首尾接头时的常规最优路径问题
最小子段和cur_min 与 min_sumKadane 算法求最小连续和找出被跳过的“代价最小”的区间,为绕圈情况做准备
环形状态合并max(max_sum, sum-min_sum)比较线性最大值与环形最大值完美覆盖环形数组的所有可能路径,得出全局最优解

补充

方法二:拆环为链 + 滑动窗口

#include<bits/stdc++.h>
using namespace std;
#define ll long long//定义长整型别名ll,防止数据累加时溢出
const int N=4e5+10;//定义常量N,因为要拆环为链,数组长度开到4e5足够
ll a[N];//定义数组a,用来存储拆环后的2n个车站快乐值
ll pre[N];//定义前缀和数组pre,pre[i]表示前i个车站的快乐值总和
int q[N];//定义单调队列q,用来存储前缀和数组的下标
int n;//定义全局变量n,表示车站的总数
int main(){//方法二:拆环为链 + 滑动窗口
	ios::sync_with_stdio(false);
	cin.tie(0);
	cin>>n;//读入车站的数量n
	for(int i=1;i<=n;i++){//循环读入每个车站的快乐值
		cin>>a[i];//读入第i个车站的快乐值
		a[i+n]=a[i];//将数组复制一份接在后面,实现拆环为链
	}
	for(int i=1;i<=2*n;i++){//计算长度为2n的新数组的前缀和
		pre[i]=pre[i-1]+a[i];//当前前缀和等于上一个前缀和加上当前车站的快乐值
	}
	int head=1,tail=1;//初始化单调队列的头尾指针,队列中先放入下标0
	q[1]=0;//前缀和pre[0]为0,将其下标0先放入队列
	ll ans=-2e18;//定义答案ans,初始化为一个极小值
	for(int i=1;i<=2*n;i++){//遍历扩展后的数组前缀和
		while(head<=tail&&q[head]<i-n)head++;//如果队头下标超出了长度n的窗口范围,弹出队头
		ans=max(ans,pre[i]-pre[q[head]]);//用当前前缀和减去窗口内最小的前缀和,更新最大快乐值
		while(head<=tail&&pre[i]<pre[q[tail]])tail--;//维护队列单调性,如果当前前缀和比队尾小,弹出队尾
		q[++tail]=i;//将当前下标i加入队尾
	}
	cout<<ans<<"\n";//输出最终能获得的最大快乐值
	return 0;//程序结束
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值