铲雪
stripe_python · · 算法·理论
“铲雪”类问题出自本人的模拟赛,这类题目的一般形式如:给你序列
本文可能并不会给出严谨的正确性证明,更多的是叙述做法。
序列版本
给你长度为
n 的数组a 。一次操作将一个子区间上所有元素减1 。求元素全变成
0 的最少操作数。
原题链接:P1969 [NOIP 2013 提高组] 积木大赛、P5019 [NOIP 2018 提高组] 铺设道路、P3078 [USACO13MAR] Poker Hands S。
:::success[题解]
发现操作瓶颈在最大数。因为比它小的数都到
对于位置
:::
:::info[代码]
const int N = 1e5 + 5;
int n, a[N];
void _main() {
cin >> n;
for (int i = 1; i <= n; i++) cin >> a[i];
int res = a[1];
for (int i = 2; i <= n; i++) res += max(0, a[i] - a[i - 1]);
cout << res;
}
:::
变式
给你长度为
n 的数组a 。一次操作将一个子区间上所有元素减1 ,或者将下标为奇数 / 偶数的元素减1 。求元素全变成
0 的最少操作数。
原题链接:P6631 [ZJOI2020] 序列。
::::success[题解] 定义第一类操作为“直线”,二三类操作为“跳线”。
只有直线是简单的。加入跳线以后,考虑向相邻位置转移。
从两个数开始考虑:
- 若
a_1,a_2 >0 ,从a_1 开始一条直线。 - 若
a_1>0,a_2=0 ,从a_1 开始一条跳线。 - 若
a_1=0 ,跳过决策。
考虑推广到
设一段连续极长的非
::::info[代码]
const int N = 1e5 + 5;
int n, a[N];
void _main() {
cin >> n; a[n + 1] = 0;
for (int i = 1; i <= n; i++) cin >> a[i];
long long res = 0, x = 0, y = 0, nxt = 0, z = 0;
for (int i = 1; i <= n; i++) {
int d = x + y - a[i + 1];
if (d > 0) {
if (x < d) y += x - d, d = x;
if (y < d) x += y - d, d = y;
x -= d, y -= d, a[i + 1] -= d, z = d;
}
a[i + 1] -= x + y, res += a[i];
int h = min(a[i], a[i + 1]);
x += h, a[i] -= h, a[i + 1] -= h, nxt += a[i];
a[i + 1] += z, res -= z, z = 0, swap(y, nxt);
} cout << res << '\n';
}
::::
环上版本
给你长度为
n 的首尾相接的环a 。一次操作将一个环上的子区间所有元素减1 。求元素全变成
0 的最少操作数。
原题链接:AT_arc136_c [ARC136C] Circular Addition。
::::success[题解]
根据上面那题的启发,猜想答案是
不难发现这是错的。但是可以往差分上想。由于这是个环,一次操作实际上使得差分数组一个点减一,一个点加一。则答案的下界为
显然这不是答案。记
猜想答案为
设一个全为
- 当
p<q 时,注意到0 \notin a ,则一次操作将整个环减少一,此时q \gets q -1 ,p 不变。重复操作至p=q 。
:::success[为什么
- 当
p>q 时,一次操作选取一个最大区间减一,则p \gets p-1 ,q 最多减一。重复操作至p=q 。
现在只需给出
- 当最大区间的数目为
1 时,一次操作将其减一,p \gets p-1, q \gets q-1 ; - 当最大区间的数目不为
1 时,可以推出0 \notin a 。找到一个最短的包含所有\max a 的区间,重复操作直到p=q=0 。
综上所述,答案为
::::
:::info[代码]
const int N = 2e5 + 5;
int n, a[N];
void _main() {
cin >> n;
for (int i = 1; i <= n; i++) cin >> a[i];
long long p = 0;
for (int i = 1; i <= n; i++) p += abs(a[i] - a[(i + 1) % n + 1]);
cout << max<long long>(*max_element(a + 1, a + n + 1), p / 2);
}
:::
树上版本
给你节点数为
n 的无根树,点i 的权值为a_i 。一次操作将一条树链上的所有点权减1 。求点权全变成
0 的最少操作数。
原题链接:P7246 手势密码。
::::success[题解]
记
可以发现,
所以在决策等效时,子树向根提供的路径数越多越好。
第二点,考虑路径什么时候“拐弯”。比如:
如果按照子树决策的思路,
这样就可以 DFS 了。记当前节点为
则不满足最优决策时,
为了保证向上延伸的路径尽量多,考虑瓶颈。
别忘了更新答案。对于节点
::::
::::info[代码]
int opt, n, seed, a[N], u[N], v[N];
int tot = 0, head[N];
struct Edge {
int next, to;
} edge[N << 1];
inline void add_edge(int u, int v) {
edge[++tot].next = head[u], edge[tot].to = v, head[u] = tot;
}
long long ans;
long long dfs(int u, int fa) {
long long s = 0, m = 0;
for (int j = head[u]; j != 0; j = edge[j].next) {
int v = edge[j].to;
if (v == fa) continue;
long long x = dfs(v, u);
s += x, m = max(m, x);
}
long long l = max(0LL, min({1LL * a[u], s - a[u], s / 2, s - m}));
ans += max(0LL, a[u] - s + l) - l;
return a[u] - l;
}
void _main() {
cin >> opt;
if (opt == 1) {
cin >> n;
for (int i = 1; i <= n; i++) cin >> a[i];
for (int i = 1; i < n; i++) cin >> u[0] >> v[0], add_edge(u[0], v[0]), add_edge(v[0], u[0]);
} else {
cin >> seed >> n;
Generate::n = n, Generate::seed = seed, Generate::RS(a, u, v);
for (int i = 1; i < n; i++) add_edge(u[i], v[i]), add_edge(v[i], u[i]);
}
dfs(1, -1);
cout << ans;
}
::::