一棵树
题意
题解
$k=0$ 不难,略。
一个显然的想法是枚举断掉的边,然后看每条边的贡献。
贡献分为两类,一类是新加的边的块间贡献:
这个不难算,另外一类是原先的边的块内贡献。
不妨设虚线边断开,实线边把一个连通块分为大小为 $a,b$ 的两块,则这条实线边的贡献是上面的式子。
于是在枚举了虚线边之后,分子树内,祖先,既非子树也非祖先三种情况讨论,不妨设 a 是虚线边较深的端点,b 是实线边较深的端点:
可以发现,三个贡献的式子后两项都只和 a 有关,前两项都可以展开成和 b 有关的二次多项式,因此只需要维护下子树内,祖先上的 sz 的和,sz 的平方之和即可快速计算。
代金券题解
题意
与值域无关做法——贪心
做如下的一个转化,可以看成有三种操作:
- 支付 1 个代金券
- 支付 1 块钱,不获得代金券
- 支付 c 块钱,获得一个代金券
记 $X_i$ 表示第 $i$ 道菜进行操作 1 的次数,$Y_i$ 表示进行操作 3 的次数。
用最少的钱等价于用最多的代金券。因此最优解有如下性质:
- 有代金券时不会进行操作 2(代金券越靠前使用越好,否则后面可能没机会用了)
- 3 操作必定越靠前越好(感性理解是提前获得了代金券,使用的机会更多了):
下面是一种调整方法:
不妨设在第 $i$ 道菜上进行过操作 3,在第 $j(j<i)$ 道菜上进行过 $\ge c$ 次 1,2 操作。 那么把这 $c$ 次 1,2 操作移到第 $i$ 道菜上进行,并把第 $i$ 道菜上进行的操作 3 换到第 $j$ 道菜上进行,答案不变。
因此,调整完之后,必然有一个前缀,其中每道菜都尽可能地使用操作 3。然后,用完操作 3 之后,对于这道菜剩下的钱,必定会优先用操作 1,剩下用操作 2(性质 1)。
这样相当是进行一个贪心策略,优先 3,然后优先 1,最后 2.
不妨记 $R_i$ 表示用这种贪心策略吃完第 $1\sim$ 道菜,剩下的代金券个数。
因此,可以把上面的贪心策略形式化的表示为:
\[Y_i=\lfloor \frac{A_i}c\rfloor, X_i=\min(R_{i-1},A_i\bmod c), R_i=R_{i-1}+Y_i-X_i\]但是,我们知道,满足这个贪心策略的仅仅是一个前缀,那么这个贪心策略进行到哪里呢?
记 $S_i=\sum_{j\ge i} A_j$,表示 $i$ 后面菜的价值之和。
我们发现:当 $R_i\ge S_{i+1}$ 的时候,就没有必要再进行操作 3 了,因为此时 $i$ 后面后面都可以全用代金券买了。
同时,不难发现满足 $R_i\ge S_{i+1}$ 的 $i$ 是一个后缀。而我们必然在满足这一个条件的第一个 $i$ 就结束了贪心。所以,如果我们能快速判断某个位置是否符合上述条件,我们就能二分求出 $i$ 的位置。
此时我们可能还有一些代金券没有用上,所以我们还需要重新规划第 $i$ 道菜的策略。
首先:
- 如果 $R_{i-1}<S_{i+1}$,那么就必须先在第 $i$ 道菜上进行 $S_{i+1}-R_{i-1}$ 操作 3 补齐代金券。
- 如果 $R_{i-1}>S_{i+1}$,那么就可以把多出来的 $R_{i-1}-S_{i+1}$ 张券在第 $i$ 道菜上用掉。
此时,$i$ 后面花的 $S_{i+1}$ 个券分为两类:第 1 类来自于 $R_{i-1}$,第 2 类来自于 ${Y_i}$。第 1 类券我们可以把它移到第 $i$ 道菜上用,但是第 2 类不可以。
思考一下,还什么方法可能用更多的券?只能增大 $Y_i$,产生一个 2 类券,代替 $S_{i+1}$ 中某个 1 类券的位置,然后再把这个 1 类券移到第 $i$ 道菜上使用。
不妨设 $S_{i+1}$ 中有 $C$ 个 1 类券,第 $i$ 道菜还剩下 $Z$ 块钱没付。那么我们最多能进行 $\min(C,\lfloor \frac Z{c+1}\rfloor)$ 次上述操作。
复杂度 $O(qn)$,拼几个特殊性质,就能喜提 50 左右的好成绩。
上数据结构
难点在于在修改过程中维护 $R_i$,当时考场上就是没想出来这个,就寄了。官方题解中没太搞懂,网上找到一种硬维护的做法。
线段树节点上记三个数 $div,mod,use$ 分别表示区间内所有数除 $c$ 下取整之和(也就是能产生的代金券个数),模 $c$ 之和(也就是最多能使用的代金券个数),以及在初始没有代金券的情况下用贪心策略,会用掉几个代金券。
然后你神奇的发现,这个东西是可以合并的:
node operator+(const node &x, const node &y) {
return {x.div + y.div, x.mod + y.mod, x.use + y.use + min(y.mod - y.use, x.div - x.use)};
}
前两项显然,解释下第三项咋合并的,x.use + y.use
也是显然的,而 min(y.mod - y.use, x.div - x.use)
表示左边一半用剩下的代金券数量,和右边一半最多能使用的代金券个数取 min。
于是线段树二分即可。
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const char nl = '\n';
const ll MXN = 3e5 + 5;
ll n, m, c, tot, sum[MXN];
struct node {
// /c之和,%c之和,如果在当前区间上模拟,会用掉多少代金券
ll div, mod, use;
} t[MXN << 2];
node operator+(const node &x, const node &y) {
return {x.div + y.div, x.mod + y.mod, x.use + y.use + min(y.mod - y.use, x.div - x.use)};
}
#define ls p << 1
#define rs p << 1 | 1
void mdf(ll mi, ll mv, ll p = 1, ll l = 1, ll r = n) {
if (l == r) {
tot = tot - (t[p].div * c + t[p].mod) + mv;
t[p] = {mv / c, mv % c, 0};
return;
}
ll mid = (l + r) >> 1;
if (mi <= mid)
mdf(mi, mv, ls, l, mid);
else
mdf(mi, mv, rs, mid + 1, r);
t[p] = t[ls] + t[rs];
}
ll que(ll p = 1, ll l = 1, ll r = n, node lft = {0, 0, 0}, ll rht = 0) {
if (l == r) {
ll arr = t[p].div * c + t[p].mod, cur = lft.div - lft.use, ans = lft.use;
if (cur >= rht) {
ans += cur, cur -= rht, arr -= cur;
ans += min(arr / (c + 1), rht);
} else {
arr -= c * (rht - cur);
ans += rht + min(arr / (c + 1), cur);
}
return tot - ans;
}
ll mid = (l + r) >> 1;
node _lft = lft + t[ls];
ll _rht = rht + t[rs].div * c + t[rs].mod;
if (_lft.div - _lft.use >= _rht)
return que(ls, l, mid, lft, _rht);
else
return que(rs, mid + 1, r, _lft, rht);
}
int main() {
ios::sync_with_stdio(0);
cin.tie(0);
cin >> n >> m >> c;
for (ll i = 1, x; i <= n; i++) {
cin >> x;
mdf(i, x);
}
cout << que() << nl;
while (m--) {
ll x, y;
cin >> x >> y;
mdf(x, y);
cout << que() << nl;
}
return 0;
}
作者:@ethan-enhe
本文为作者原创,转载请注明出处:本文链接
阅读量: