题解:洛谷 P11963 环线
【题目来源】
洛谷:P11963 [GESP202503 六级] 环线 - 洛谷
【题目描述】
小 A 喜欢坐地铁。地铁环线有 \(n\) 个车站,依次以 \(1,2,⋯,n\) 标号。车站 \(i (1≤i<n)\) 的下一个车站是车站 \(i+1\)。特殊地,车站 \(n\) 的下一个车站是车站 \(1\)。
小 A 会从某个车站出发,乘坐地铁环线到某个车站结束行程,这意味着小 A 至少会经过一个车站。小 A 不会经过一个车站多次。当小 A 乘坐地铁环线经过车站 \(i\) 时,小 A 会获得 \(a_i\) 点快乐值。请你安排小 A 的行程,选择出发车站与结束车站,使得获得的快乐值总和最大。
【输入】
第一行,一个正整数 \(n\),表示车站的数量。
第二行,\(n\) 个整数 \(a_i\),分别表示经过每个车站时获得的快乐值。
【输出】
一行,一个整数,表示小 A 能获得的最大快乐值。
【输入样例】
4
-1 2 3 0
【输出样例】
5
【核心思想】
-
问题分析:给定 \(n\) 个车站的环形排列,每个车站有快乐值 \(a_i\),求选择一个不重复经过任何车站的连续段(长度 \(\geq 1\) 且 \(\leq n\)),使得快乐值总和最大。这是一个环形最大子段和问题,关键在于将环形转化为线性,并用单调队列优化滑动窗口最小值查询。
-
算法选择:
- 环形数组 + 前缀和:将数组复制一份接在原数组后,实现环形效果
- 单调队列(滑动窗口最小值):维护长度为 \(n\) 的滑动窗口,窗口内前缀和的最小值与当前前缀和的差即为该窗口内的最大子段和
-
关键步骤:
- 初始化:读取 \(n\) 和 \(a[1..n]\),复制数组 \(a[i+n] = a[i]\)(\(i = 1..n\)),构建长度为 \(2n\) 的环形数组
- 前缀和:\(sa[i] = sa[i-1] + a[i]\)(\(i = 1..2n\))
- 单调队列求最大子段和(遍历 \(i\) 从 \(1\) 到 \(2n\)):
- 维护窗口大小:若队首 \(q[hh] < i - n\),则 \(hh++\)(窗口长度不能超过 \(n\),避免绕环超过一圈)
- 更新答案:若队列非空,\(maxn = \max(maxn, sa[i] - sa[q[hh]])\),即当前前缀和减去窗口内最小前缀和
- 维护单调递增:若队尾 \(sa[q[tt]] \geq sa[i]\),则 \(tt--\)(保持队列中前缀和单调递增,确保队首最小);将 \(i\) 入队
- 输出答案 \(maxn\)
-
时间/空间复杂度:
- 时间复杂度:\(O(n)\),每个元素最多入队出队一次
- 空间复杂度:\(O(n)\),环形数组、前缀和数组和单调队列
-
单调队列的核心思想:
- 滑动窗口最小值:队列中维护的是可能成为未来最小值的前缀和下标,队首始终是当前窗口内的最小值
- 单调性维护:新元素 \(sa[i]\) 入队前,弹出队尾所有 \(\geq sa[i]\) 的元素,因为这些元素在 \(i\) 之前不可能成为最小值(\(i\) 更晚出现且值更小)
- 环形转化:复制数组将环形问题转化为线性滑动窗口问题,窗口大小限制为 \(n\) 保证不重复经过同一车站
- 最大子段和公式:\(\max(sa[i] - sa[j])\) 其中 \(i - j \leq n\),即固定右端点 \(i\),找窗口内最小左端点 \(sa[j]\)
- 适用于环形数组最大子段和、滑动窗口最值、连续子数组优化类问题
【算法标签】
普及 #单调队列
【代码详解】
#include <bits/stdc++.h>
using namespace std;
#define int long long // 定义宏,将int替换为long long类型
const int N = 200005; // 定义数组的最大大小
int n; // 输入的数字个数
int a[N * 2]; // 存储输入的数字,并复制一份实现环形处理
int sa[N * 2]; // 前缀和数组
int maxn = -2e18; // 记录最大子段和,初始化为极小值
int q[N * 2]; // 单调队列,用于维护窗口最小值
signed main() {
cin >> n; // 输入数字个数
// 输入数字并复制一份实现环形处理
for (int i = 1; i <= n; i++) {
int x;
cin >> x;
a[i] = x;
a[i + n] = x; // 复制一份,实现环形数组
}
// 计算前缀和数组
for (int i = 1; i <= 2 * n; i++) {
sa[i] = sa[i - 1] + a[i];
}
// 初始化单调队列
int hh = 0, tt = -1; // 队列头尾指针
// 遍历前缀和数组,寻找最大子段和
for (int i = 1; i <= 2 * n; i++) {
// 维护窗口大小不超过n
while (hh <= tt && q[hh] < i - n) hh++;
// 计算当前窗口的最大子段和
if (hh <= tt) {
maxn = max(maxn, sa[i] - sa[q[hh]]);
}
// 维护单调递增队列
while (hh <= tt && sa[q[tt]] >= sa[i]) tt--;
q[++tt] = i;
}
cout << maxn << endl; // 输出最大子段和
return 0;
}
【运行结果】
4
-1 2 3 0
5
浙公网安备 33010602011771号