形式化题目
给定长度为
其中
正解
思路
将两个切点的联合判断变成右侧前缀和的计数查询。
记前缀和为
枚举两个切点需要
记
下图用跨段横线标出每个前缀和覆盖的范围,区分“一段的和”和“从开头累计的和”。
两刀分别在第一格和第二格之后,即位置
反过来,若这两个条件成立,三段和分别是
从右往左扫描 bucket 记录已经扫描过的前缀和次数。处理位置
- 若
3 * pre[p] == total,把bucket[2 * pre[p]]加入答案。 - 执行
bucket[pre[p]]++,供更左侧的第一刀查询。
先查询,再插入保证不会把同一个位置同时当成两刀。尤其当总和为零时,两种切点要求的前缀和都是零,更不能反过来操作。例如三个零只有一种切法,若先插入就会额外数到自己与自己的配对。
这里用的是 rbook 里黑白气球讲的点对计数模板,只是本题要多走一步。那篇的框架是:答案是点对的集合,按右端点切成互不相交的
本题对应关系:
| 黑白气球 | 本题 |
|---|---|
| 点对 |
点对 |
| 桶里直接放元素值 | 先换成前缀和状态 |
| 合法性天生只涉及一侧(颜色不同) | 三段相等经引理拆成只含 |
| 从左扫,桶里是 |
从右扫,桶里是 |
最后一行的好处是位置关系由扫描方向自动保证,不用再比较下标;而“当前点不能和自己配对”没有捷径,仍然只能靠把累加写在入表之前。桶的语义反过来了,这条顺序不变量却没变。
扫描不包含
判断使用乘法 3 * pre[p] == total,避免总和不能被三整除时被整数除法取整误导。负数不会影响前缀和等式或哈希计数,无需假设前缀和单调。
样例说明
样例一的数组为
| 切点位置 | 1 | 2 | 3 | 4 |
|---|---|---|---|---|
| 前缀和 | 1 | 3 | 6 | 6 |
| 用途 | 不满足条件 | 第一刀 | 第二刀 | 第二刀 |
总和为 bucket[6] 正好为
代码
C++ 与 Python 使用相同的倒序计数方法,C++ 中 pre[p] 对应 Python 的 p[p-1]。
C++ 给出两份等价实现:main.cpp 用 map(红黑树,稳定 main2.cpp 用 unordered_map(期望 reserve)。两者都直接写 bucket[key],利用“键不存在时默认取值为
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const int maxn = 500000 + 5;
int n;
ll pre[maxn];
map<ll, ll> bucket;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n;
for (int i = 1; i <= n; i++) {
ll x;
cin >> x;
pre[i] = pre[i - 1] + x;
}
ll total = pre[n], ans = 0;
for (int p = n - 1; p >= 1; p--) {
// 表中只有右侧切点,先查询以保证两刀位置不同。
// map 中不存在的键默认取值为 0,无需判断是否存在。
if (3 * pre[p] == total) {
ans += bucket[2 * pre[p]];
}
bucket[pre[p]]++;
}
cout << ans << '\n';
return 0;
}#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const int maxn = 500000 + 5;
int n;
ll pre[maxn];
unordered_map<ll, ll> bucket;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n;
for (int i = 1; i <= n; i++) {
ll x;
cin >> x;
pre[i] = pre[i - 1] + x;
}
ll total = pre[n], ans = 0;
bucket.reserve(n * 2);
for (int p = n - 1; p >= 1; p--) {
// 表中只有右侧切点,先查询以保证两刀位置不同。
// 不存在的键默认取值为 0,无需判断是否存在。
if (3 * pre[p] == total) {
ans += bucket[2 * pre[p]];
}
bucket[pre[p]]++;
}
cout << ans << '\n';
return 0;
}from collections import defaultdict
import sys
from itertools import accumulate
def solve():
n,*data = map(int, sys.stdin.buffer.read().split())
# pre sum
p = list(accumulate(data))
# print(list(p))
# 桶
d = defaultdict(int)
ans = 0
for i in range(n-2,-1,-1):
val = p[i]
if 3*val == p[n-1]:
ans += d[2*val]
d[val] += 1
print(ans)
if __name__ == "__main__":
solve()复杂度
哈希表单次操作期望 map 实现则为 long long。
总结
固定一刀后,把另一刀需要满足的前缀和条件转化为次数查询。倒序扫描负责维护位置关系,先查后插负责排除同一位置,哈希表负责合并值相同的候选切点。