校验值排序后两端反向配对可得闭式;固定左端点校验值随长度单调不减,倍增定位上界再二分求最长可行段,贪心切出最少段数。
OJ: roj
题目 ID: 3017
难度:省选/NOI-
标签:倍增贪心二分排序python
创建: 2026-10-01 10:25
更新: 2026-10-07 13:50
形式化题目
给定整数
对任意整数集合
目标:把
样例
| 候选前缀 | 排序后 | 配对 | 校验值 | ||
|---|---|---|---|---|---|
| 不足一对 | ✓ | ✓ | |||
| ✓ | ✓ | ||||
| ✓ | ✓ | ||||
| ✓ | ✓ | ||||
| ✗ | ✓ |
正解
思路
朴素做法与瓶颈:最直接的思路是枚举所有切点组合(
观察一:校验值有闭式——排序后两端反向配对。 把
每个选中的数在所有对里恰好出现一次,所以
即把
即最小的
观察二:段越长,校验值不减。 往集合里加一个元素
倍增先定位,再二分收尾。 直接在
| 阶段 | 验证长度 | 校验值 | 结果 | 更新 |
|---|---|---|---|---|
| 倍增 | ✓ | |||
| 倍增 | ✓ | |||
| 二分 | ✗ | |||
| 收束 | — | — | 第一段取前 4 个数 |
两次倍增只用两段短验证就把候选范围压到 lo 全程是已验证可行的长度、hi 只会收缩,最终
二分的下界是单元素段:map(mul, ...) 求和(
贪心正确性。 设当前左端点的最长可行右端点为
代码
Python 版:
#!/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 11:25
# update_at: 2026-10-01 11:25
import sys
from operator import mul
INPUT = sys.stdin.buffer
def get_value(seg: list[int], m: int) -> int:
"""段的校验值:排序后最小的 k 个与最大的 k 个反向配对,平方差之和。
k = min(m, len(seg)//2)。平方差之和用 (a²+b²-2ab) 展开,
三段求和都走 map(mul, ...) 的 C 层循环,避免 Python 逐对循环。
"""
seg.sort()
k = min(m, len(seg) >> 1)
small = seg[:k]
large = seg[len(seg) - k:] # 不能写 seg[-k:]:k=0 时它等于整段
large.reverse() # 第 i 小配第 i 大
return (
sum(map(mul, small, small))
+ sum(map(mul, large, large))
- 2 * sum(map(mul, small, large))
)
def solve() -> None:
cases = int(INPUT.readline())
out: list[str] = []
for _ in range(cases):
n, m, limit = map(int, INPUT.readline().split())
A = list(map(int, INPUT.readline().split()))
# 贪心从左到右切段:每段尽量长(前缀校验值随长度单调不减)。
segments = 0
left = 0
while left < n:
span = n - left
lo, hi = 1, 2 # 单元素段校验值为 0,必可行
# 倍增:长度 2,4,8,... 验证到失败或越界,lo 恒为已知可行长度
while hi <= span and get_value(A[left:left + hi], m) <= limit:
lo = hi
hi <<= 1
hi = min(hi, span)
# 二分:在 (lo, hi] 内找最长可行长度
while lo < hi:
mid = (lo + hi + 1) >> 1
if get_value(A[left:left + mid], m) <= limit:
lo = mid
else:
hi = mid - 1
left += lo
segments += 1
out.append(str(segments))
sys.stdout.write("\n".join(out) + "\n")
if __name__ == "__main__":
solve()C++ 版(同一算法):
/**
* 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-06 14:17
* update_at: 2026-10-06 14:17
*/
// main.cpp:贪心切最少段 + 倍增定位 + 二分收尾,每段校验值用「排序后最小 k 个配最大 k 个」闭式计算。
#include <iostream>
#include <algorithm>
using namespace std;
typedef long long ll;
const int MAXN = 500005;
ll a[MAXN]; // 输入的数列 A,0 基下标
ll buf[MAXN]; // 校验值计算用的临时缓冲区:把待验证的段拷进来再排序
ll n, m, limit;
// 计算 a[left .. left+len-1] 的校验值。
// 排序后取最小的 k 个与最大的 k 个反向配对,k = min(m, len/2),
// 校验值 = 各对差的平方之和;这就是「每对差的平方之和最大」的闭式。
ll calc_value(ll left, ll len) {
for (ll i = 0; i < len; i++) {
buf[i] = a[left + i];
}
sort(buf, buf + len);
ll k = min(m, len / 2);
ll res = 0;
for (ll i = 0; i < k; i++) {
ll d = buf[len - 1 - i] - buf[i];
res += d * d;
}
return res;
}
// 从 left 出发,贪心地取最长可行段:先按长度 1,2,4,... 倍增验证,
// 失败后再在「最后可行长度」和「首个失败长度」之间二分,确定段长。
ll longest_segment(ll left) {
ll span = n - left;
ll lo = 1; // lo 恒为已验证可行的段长(单元素段校验值为 0,必可行)
ll hi = 2;
while (hi <= span && calc_value(left, hi) <= limit) {
lo = hi;
hi <<= 1;
}
if (hi > span) {
hi = span;
}
while (lo < hi) {
ll mid = (lo + hi + 1) / 2;
if (calc_value(left, mid) <= limit) {
lo = mid;
} else {
hi = mid - 1;
}
}
return lo;
}
void solve() {
ll kase;
cin >> kase;
while (kase--) {
cin >> n >> m >> limit;
for (ll i = 0; i < n; i++) {
cin >> a[i];
}
ll segments = 0;
ll left = 0;
while (left < n) {
left += longest_segment(left);
segments++;
}
cout << segments << "\n";
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
solve();
return 0;
}get_value 就是观察一的闭式(注意"最大的 seg[len(seg) - k:] 而非 seg[-k:],因为 seg[-0:] 等于整段);solve 的主循环是"贪心切段",段内先倍增后二分,lo 恒为已知可行长度、hi 先截到剩余长度,结束时 left += lo 直接跳到下一段起点。
复杂度
设一段长度为 map(mul, ...) 与原地 sort() 压缩常数,真实数据 10 组全部输出一致。
总结
- 校验值不必枚举配对方式:排序后最小的
个配最大的 个( ),闭式 ,一次排序即得。 - 集合只增不减时校验值单调不减,"前缀
"是前真后假的谓词——这把"逐个扫右端点"换成倍增 + 二分。 - 单调谓词下贪心取最长可行段即为最少段数;整体时间
、空间 。