断环成链后把 A_i+A_j+(j-i) 拆成 (A_i-i)+(A_j+j),用单调队列在长度 N/2 的窗口里取最大左端点值,O(N) 求出最大代价。
OJ: roj
题目 ID: 3151
难度:提高
标签:环形结构动态规划单调队列推导
创建: 2026-10-01 21:10
更新: 2026-10-10 11:30
形式化题目
环形公路上均匀分布
仓库
求
输入样例:
5
1 8 6 2 5输出样例:
15样例里的环形公路如下:节点内标出库存量,边上的
graph LR
W1["仓库1<br/>A=1"]
W2["仓库2<br/>A=8"]
W3["仓库3<br/>A=6"]
W4["仓库4<br/>A=2"]
W5["仓库5<br/>A=5"]
W1 ---|1| W2
W2 ---|1| W3
W3 ---|1| W4
W4 ---|1| W5
W5 ---|1| W1
读图时注意
正解
思路
朴素做法与瓶颈。 数据范围是 A 是 0 起始下标的库存数组,长度 n):
best = max(A[i] + A[j] + min(j-i, n-(j-i)) for i in range(n) for j in range(i+1, n))这是
第一步:断环成链,把环形距离变成下标差。
两个方向的距离之和恒为
那么,这样做会不会落下某一对仓库?设一对仓库编号
链上满足
N = 5 K = N // 2 = 2 链长 = N + K = 7
下标 1 2 3 4 5 6 7
库存 1 8 6 2 5 1 8
来源 A A A A A 复 复下标
要注意距离小于
第二步:把
第三步:滑动窗口最大值用单调队列。
右端点
- 新的左端点
入队前,先弹出队尾所有 值不大于 的下标——它们既更小、又更早过期,永远不可能再当选; - 再看队首是否已滑出窗口(下标
),是则弹出; - 此时队首就是窗口内最优左端点,算出候选值并更新答案。
样例的完整扫描轨迹。表里的下标都换算成
| 合法左端点窗口 | 队列中的下标 | 候选值 |
当前最优 | ||
|---|---|---|---|---|---|
第
正确性要点。 每一步入队弹出的队尾元素既比新元素更早过期、值又不更大,因此删除它们不改变任何后续窗口的最优值;
被弹出的队首元素已不在窗口内。故循环过程中队首始终是当前窗口内
边界。 当 best 初始化成
正文与代码的对应关系是直接的:parse_chain() 完成第一步的断环成链,
best_cost() 里的 weight = chain[i] - i 就是 chain[j] + j 就是 while 分别完成入队弹出与窗口过期,queue[0] 就是上表里的“队首”。
代码
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-10-10 11:30
* update_at: 2026-10-10 11:30
*/
// 环形仓库配对:断环成链 + 单调队列,求 max(A[i] + A[j] + (j - i))。
#include <cstdio>
#include <vector>
typedef long long ll;
int main() {
ll n;
if (scanf("%lld", &n) != 1) return 0; // 空输入安全返回
std::vector<ll> a(n);
for (ll i = 0; i < n; i++) scanf("%lld", &a[i]);
ll reach = n / 2; // 环上每一对仓库总有一个方向距离不超过 n // 2
std::vector<ll> chain;
for (ll i = 0; i < n; i++) chain.push_back(a[i]);
for (ll i = 0; i < reach; i++) chain.push_back(a[i]); // 跨过编号 n 的那一对也落进同一条链
std::vector<ll> q(chain.size() + 1); // 存左端点下标,权值 A[i] - i 从队首到队尾严格递减
ll head = 0, tail = 0;
ll best = 0; // A[i] >= 1,所以 0 是安全的初值
for (ll j = 1; j < (ll)chain.size(); j++) {
ll i = j - 1;
ll weight = chain[i] - i;
while (tail > head && chain[q[tail - 1]] - q[tail - 1] <= weight) {
tail--; // 队尾更小又更早过期,不再可能当选
}
q[tail++] = i;
while (q[head] < j - reach) head++; // 距离超过 reach 的左端点滑出窗口
ll cost = chain[q[head]] - q[head] + chain[j] + j;
if (cost > best) best = cost;
}
printf("%lld\n", best);
return 0;
}#!/usr/bin/env python3
# Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
# rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
# rainboy的学习导航网站: https://idx.roj.ac.cn
# create_at: 2026-10-01 21:10
# update_at: 2026-10-01 21:10
import re
import sys
from array import array
from collections import deque
NUMBER_RE = re.compile(rb"\d+") # 题面只出现非负整数,按数字串切分比 split 省一大半内存
def parse_chain() -> tuple[array, int]:
"""读入仓库数 n 与库存 A,返回“断环成链”后的库存序列以及窗口允许的最大下标差。"""
buf = sys.stdin.buffer.read()
values = array("i", (int(m.group()) for m in NUMBER_RE.finditer(buf)))
del buf # 原始字节只用于切分,尽早释放
n = values[0]
reach = n // 2 # 环上每一对仓库总有一个方向距离不超过 n // 2
chain = values[1:n + 1] + values[1:1 + reach] # 跨过编号 n 的那一对也落进同一条链
del values
return chain, reach
def best_cost(chain: array, reach: int) -> int:
"""求链上 max(A[i] + A[j] + (j - i)),只保留下标差 1 <= j - i <= reach 的配对。
距离并进点权后,右端点 j 只需在滑动窗口 [j - reach, j - 1] 里问
“谁的 A[i] - i 最大”,取窗口最大值正是单调队列的职责。
"""
queue: deque[int] = deque() # 存左端点下标,链上权值 A[i] - i 从队首到队尾严格递减
best = 0 # A[i] >= 1,所以 0 是安全的初值,n = 1 时直接输出它
for j in range(1, len(chain)):
i = j - 1 # j 左边最近的仓库,一定在窗口内
weight = chain[i] - i # 该左端点的 f(i)
while queue and chain[queue[-1]] - queue[-1] <= weight:
queue.pop() # 队尾更小又更早过期,不再可能当选
queue.append(i)
while queue[0] < j - reach: # 距离超过 reach 的左端点滑出窗口
queue.popleft()
cost = chain[queue[0]] - queue[0] + chain[j] + j
if cost > best:
best = cost
return best
def solve() -> None:
chain, reach = parse_chain() # n = 1 时链长为 1,扫描循环不执行,答案就是 best 的初值 0
print(best_cost(chain, reach))
if __name__ == "__main__":
solve()复杂度
- 时间复杂度:
。断环成链是 ,单调队列扫描链长 , 且每个下标最多入队、出队各一次。 - 空间复杂度:
。链 用 array('i')存放,占字节;队列最多同时容纳 个下标。
总结
这道题的核心是把“环形 + 距离”这两个麻烦逐个消掉:
- 环 → 用
断环成链,把环形距离变成链上下标差; - 距离 → 把
拆给左右端点,让 与 各自独立; - 取最大值 → 窗口两端单调右移,用单调队列把每个右端点的
降到摊还 。
只要题目求的是最大值而不是计数,第 1 步的重复枚举就完全无害——这是断环成链能直接用在本题的关键前提。
第 2 步的代数拆分同样常见:把“两个点之间的耦合项”(这里是距离