## 二小姐的烟花 #贪心 #结论 #排序 首先,如果我们选择了若干个烟花进行燃放,那么按照 $a_i$ 从小到大的顺序燃放一定是最优的。因为 $a_i$ 的和本身确定了,而这种构造方案能够获得最多的顺序对。 所以先对 $a$ 从小到大排个序。现在,我们可以很显然地猜出一个结论:最优的燃放烟花的方式一定是燃放最后的若干个烟花。证明不会证,只能感性理解。 于是我们就从后往前遍历,计算贡献。假如当前的 $a_i$ 出现了 $x$ 次,那么它会增加 $n-i+1-x$ 个逆序对。我们对这个值进行累加,再乘上 $k$,最后加上 $a_i$ 本身的值,就能得到燃放 $i\sim n$ 的烟花能获得的最大值了,最后求最值。 时间复杂度 $\mathcal O(n\log_2 n)$,瓶颈在排序。 ```cpp #include <fstream> #include <algorithm> using namespace std; using LL = long long; const LL kMaxN = 1e6 + 5; ifstream cin("flandre.in"); ofstream cout("flandre.out"); struct { LL x, id; } a[kMaxN]; LL n, k, sum, cnt, eql, p, ans; int main() { cin.tie(0)->sync_with_stdio(0); cin >> n >> k; for (LL i = 1; i <= n; i++) { cin >> a[i].x; a[i].id = i; } sort(a + 1, a + n + 1, [](auto a, auto b) { return a.x < b.x; }); p = n + 1; for (LL i = n; i >= 1; i--) { if (i == n || a[i].x != a[i + 1].x) { eql = 1; } else { eql++; } sum += a[i].x; cnt += n - i + 1 - eql; LL val = sum + k * cnt; if (val > ans) { ans = val; p = i; } } cout << ans << ' ' << n - p + 1 << '\n'; for (LL i = p; i <= n; i++) { cout << a[i].id << ' '; } return 0; } ``` ## 红美铃摸鱼 #数学 #前缀和 看到 $ \sum\limits_{l=1}^n\sum\limits_{r=l}^n\left[\left(\sum_{i=l}^r a_i\right)\times\left(\sum_{i=l}^r b_i\right) \right] $ 这个一大坨式子,我们就可以考虑拆贡献。对于 $a_i$ 和 $b_j$,如果它俩相乘的话,那么有 $l\in [1,\min(i,j)]$ 和 $r\in [\max(i,j),n]$。显然我们不能枚举 $i$ 和 $j$ 进行计算,而 $q$ 次操作每次都是对 $b$ 进行区间相加。于是我们可以想到 - [I] 预处理每个 $b_i$ 所乘上的系数,即 $\sum\limits_{j=1}^n\left(\min(i,j)\times (n-\max(i,j)+1)\times a_j\right)$。 这个式子有 $\min$ 和 $\max$,不好搞,所以我们把 $j$ 分成 $[1,i]$ 和 $[i,n]$ 的分别进行处理。 对于 $j\in [1,i]$,式子变成 $\sum\limits_{j=1}^i j\times(n-i+1)\times a_j$。我们从左往右枚举 $i$,每次添加 $j=i$ 的一个新元素。不难发现 $n-i+1$ 随着 $i$ 变大而变小,具体是每次 $i\gets i+1$ 时原来所有的 $j$ 的贡献都会减掉 $j\times a_j$。 所以我们在增加 $j=i$ 的贡献时,同时维护 $\sum\limits_{j=1}^{i-1}j\times a_j$,每次累加这个值,从总贡献当中减去。因此,我们就成功完成了 $j\in [1,i]$ 的这一部分。 对于 $j\in [i,n]$ 同理,从后往前遍历,注意 $\max$ 和 $\min$ 互换了。这些预处理都是 $\mathcal O(n)$ 的。 最后每个 $j=i$ 多累加了一次,从贡献中删去 $i(n-i+1)a_i$ 即可。这下我们就可以轻松地得出初始答案,每次查询对 $[l,r]$ 的 $b$ 值加上 $k$ 也就相当于再答案上累加了 $[l,r]$ 的系数之和乘上 $k$。这个可以通过前缀和实现 时间复杂度 $\mathcal O(n+q)$,要注意常数。 ```cpp #include <fstream> using namespace std; using LL = long long; const LL kMaxN = 5e5 + 5, kMod = 1e9 + 7; ifstream cin("meirin.in"); ofstream cout("meirin.out"); LL a[kMaxN], b[kMaxN], base[kMaxN], n, q, sum, sub, ans; int main() { cin.tie(0)->sync_with_stdio(0); cin >> n >> q; for (LL i = 1; i <= n; i++) { cin >> a[i]; sum = ((sum + (n - i + 1) * i % kMod * a[i] % kMod - sub) % kMod + kMod) % kMod; base[i] = (base[i] + sum + kMod) % kMod; sub = (sub + i * a[i] % kMod + kMod) % kMod; } for (LL i = 1; i <= n; i++) { cin >> b[i]; } sum = sub = 0; for (LL i = n; i >= 1; i--) { sum = ((sum + i * (n - i + 1) % kMod * a[i] % kMod - sub) % kMod + kMod) % kMod; base[i] = (base[i] + sum + kMod) % kMod; sub = (sub + (n - i + 1) * a[i] % kMod + kMod) % kMod; } for (LL i = 1; i <= n; i++) { base[i] = (base[i] - i * (n - i + 1) % kMod * a[i] % kMod + kMod) % kMod; ans = (ans + base[i] * b[i] % kMod + kMod) % kMod; base[i] = (base[i - 1] + base[i] + kMod) % kMod; } for (LL l, r, k; q; q--) { cin >> l >> r >> k; ans = (ans + (base[r] - base[l - 1] + kMod) % kMod * k % kMod + kMod) % kMod; cout << ans << '\n'; } return 0; } ``` ## 红魔馆 #概率与期望 #树形dp #组合数学 - [!] 以下规定 $m$ 个关键点的行走方式为路线,任意两点间的为路径。 首先我们需要先求出初始答案。按照每一条遍历方式求总和肯定是不可取的,于是仍然考虑拆贡献。本题没有对走的路线进行任何规定,所以任何两个属于 $m$ 个关键点的点都可以连起来,作为路线的一部分。 对于任意一条两个关键点连起来的路径,它会在 $\sum\limits_{i=0}^m A_{m-2}^i\times(m-2-i)!$ 条路线当中出现过。即枚举路径前有 $i$ 个关键点经过,则有 $A_{m-2}^i$ 种排列方式,剩下 $(m-2-i)$ 个点放在后面随便排。 - [I] 事实上这个数等于 $(m-1)!$,但是我赛时没管这么多,写的暴力求和。 现在我们考虑如何计算任意两个关键点之间的距离之和。考虑树形 dp,设 $dp_i$ 表示以 $i$ 为起点,到 $i$ 子树中所有关键点所需要的距离之和。若 $p_i$ 为 $i$ 子树中的关键点之和,那么有 $ dp_i=\sum_{j\in\operatorname{son}(i)}(dp_j+w(i,j)\times p_j) $ 其实就是将 $j$ 到 $j$ 子树内的关键点的距离拼上 $w(i,j)$。不过我们求的不是 $i$ 到 $i$ 子树内,而是 $i$ 到任意关键点(除 $i$ 本身)的距离和。 考虑换根,把每个 $i$ 看作根,计算 $i$ 到树内任意关键点的距离和。设其为 $res_i$。如果要将根从 $i$ 转移到 $j$ 的话,$j$ 子树内的所有关键点到根的距离就会减去 $w(i,j)$,其余的所有关键点到根的距离就会加上 $w(i,j)$,所以有 $ res_j=res_i-p_j\times w(i,j)+(m-p_i)\times w(i,j)~(j\in\operatorname{son}(i)) $ 此时我们就通过 $\mathcal O(n)$ 计算出了每个点到任意关键点之间的距离之和,那么把所有关键点的这个值累加起来即可得到路径距离总和。乘上之前的系数 $\sum\limits_{i=0}^m A_{m-2}^i\times(m-2-i)!$ 或 $(m-1)!$ 即可得到任意的路线距离总和的总和,除以总路线数量 $m!$ 即可得到初始答案。 现在考虑修改。如果我们修改一个点 $x$ 邻边边权增加 $k$,而它的邻边被任意两个关键点之间的路径经过的次数为 $a_x$,那么答案会加上 $a_x\times k$(还要乘上系数和 $m!$)。 如何求 $a_x$ 呢?如果 $x$ 是关键点,那么它会被经过 $2(m-1)$ 次($x$ 到其它关键点和其它关键点到 $x$)。其次是作为路径上的点,那么可以遍历 $x$ 的所有邻点 $i$,获取以 $x$ 根 $i$ 子树内关键点的数量(仅当 $i=\operatorname{fa}(x)$ 时有区别,其余都是 $p_i$),然后对于任意两个不同 $i$ 的数量相乘再乘 $2$(不是一正一反,而是经过了 $x$ 的两条邻边)。当然肯定不能枚举,具体操作是求和后平方,再减去每个项的平方。 现在我们就成功写出这道题了。时间复杂度 $\mathcal O(n+q)$,也是需要注意常数的一道题。 ```cpp #include <fstream> #include <forward_list> #include <utility> using namespace std; using LL = long long; const LL kMaxN = 5e5 + 5, kMod = 998244353; ifstream cin("sakuya.in"); ofstream cout("sakuya.out"); LL dp[kMaxN], res[kMaxN], ptr[kMaxN], mrk[kMaxN], fac[kMaxN], inv[kMaxN], a[kMaxN], fa[kMaxN], n, m, q, ans; forward_list<pair<LL, LL>> e[kMaxN]; void dfs1(LL x, LL f) { ptr[x] = mrk[x]; fa[x] = f; for (const auto &i : e[x]) { if (i.first == f) { continue; } dfs1(i.first, x); ptr[x] += ptr[i.first]; dp[x] = (dp[x] + dp[i.first] + ptr[i.first] * i.second % kMod) % kMod; } } void dfs2(LL x, LL f) { for (const auto &i : e[x]) { if (i.first == f) { continue; } res[i.first] = res[x] - (ptr[i.first] * i.second) + (m - ptr[i.first]) * i.second; res[i.first] = (res[i.first] % kMod + kMod) % kMod; dfs2(i.first, x); } } LL pow(LL a, LL b) { LL res = 1; for (; b; b /= 2) { b % 2 && (res = res * a % kMod); a = a * a % kMod; } return res; } LL A(LL n, LL m) { if (m > n) { return 0; } return fac[n] * inv[n - m] % kMod; } int main() { cin.tie(0)->sync_with_stdio(0); cin >> n >> m; for (LL i = 1, u, v, w; i < n; i++) { cin >> u >> v >> w; e[u].emplace_front(v, w); e[v].emplace_front(u, w); } fac[0] = inv[0] = 1; for (LL i = 1; i <= n; i++) { fac[i] = fac[i - 1] * i % kMod; } inv[n] = pow(fac[n], kMod - 2); for (LL i = n - 1; i >= 0; i--) { inv[i] = inv[i + 1] * (i + 1) % kMod; } for (LL i = 1, x; i <= m; i++) { cin >> x; mrk[x]++; } dfs1(1, 0); res[1] = dp[1]; dfs2(1, 0); for (LL i = 1; i <= n; i++) { ans = (ans + mrk[i] * res[i] % kMod) % kMod; } LL base = 0; for (LL i = 0; i <= m - 2; i++) { base = (base + A(m - 2, i) * fac[m - 2 - i] % kMod) % kMod; } for (LL i = 1; i <= n; i++) { mrk[i] && (a[i] = 2 * mrk[i] % kMod * (m - 1) % kMod); LL sum = 0, sub = 0; for (const auto &j : e[i]) { if (j.first == fa[i]) { LL x = m - ptr[i]; sum = (sum + x) % kMod; sub = (sub + x * x % kMod) % kMod; } else { LL x = ptr[j.first]; sum = (sum + x) % kMod; sub = (sub + x * x % kMod) % kMod; } } sum = (sum * sum % kMod - sub + kMod) % kMod; a[i] = (a[i] + 2 * sum % kMod) % kMod; } cin >> q; for (LL x, k; q; q--) { cin >> x >> k; ans = (ans + a[x] * k % kMod) % kMod; cout << ans * base % kMod * inv[m] % kMod << '\n'; } return 0; } ``` ## 红楼 #分治 #分块 #差分 我在考场上想出了根号分治,但是没有优化掉 $\log$。 首先题目中的修改操作可以视作以 $x$ 为一个周期,每个周期前 $y+1$ 个数加上 $k$。 - 对于 $x>\sqrt{n}$,我暴力枚举每一个周期,用线段树做区间加法。修改时间复杂度 $\mathcal O(\sqrt n\log_2 n)$,查询 $\mathcal O(\log_2 n)$。 - 对于 $x\le \sqrt{n}$,我对于每个 $x\le \sqrt n$ 都会开一颗 $\sqrt n$ 大小的线段树(维护和还有下标乘平方的和),然后对于 $x$ 线段树的前 $\min(x,y+1)$ 个数加上 $x$。查询时枚举每一个 $x$,然后先把整块的给加上,最后剩的一些对于所有 $y\ge res$ 的系数都会被限制在 $y$。修改时间复杂度 $\mathcal O(\log_2\sqrt n)$,查询时间复杂度 $\mathcal O(\sqrt{n}\log_2 \sqrt{n})$。 不难发现复杂度瓶颈在于第一种操作的修改。我们需要 $\mathcal O(1)$ 对一个区间进行加法(这样单词修改就变成了 $\sqrt n$),而查询时间复杂度可以变成 $\mathcal O(\sqrt n)$。 这是一个相当复杂的分块,需要使用复杂的差分+前缀和技巧实现。不详细说了。 对于第二种操作,修改时间复杂度太优了查询复杂度太劣,所以不开 $\mathcal O(\sqrt n)$ 的线段树了,直接 $\mathcal O(\sqrt n)$ 暴力修改前缀和数组,这样单块内查询就能做到 $\mathcal O(1)$。总时间复杂度就可以达到 $\mathcal O(m\sqrt n)$。