二小姐的烟花
首先,如果我们选择了若干个烟花进行燃放,那么按照 从小到大的顺序燃放一定是最优的。因为 的和本身确定了,而这种构造方案能够获得最多的顺序对。
所以先对 从小到大排个序。现在,我们可以很显然地猜出一个结论:最优的燃放烟花的方式一定是燃放最后的若干个烟花。证明不会证,只能感性理解。
于是我们就从后往前遍历,计算贡献。假如当前的 出现了 次,那么它会增加 个逆序对。我们对这个值进行累加,再乘上 ,最后加上 本身的值,就能得到燃放 的烟花能获得的最大值了,最后求最值。
时间复杂度 ,瓶颈在排序。
#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;
}红美铃摸鱼
看到
这个一大坨式子,我们就可以考虑拆贡献。对于 和 ,如果它俩相乘的话,那么有 和 。显然我们不能枚举 和 进行计算,而 次操作每次都是对 进行区间相加。于是我们可以想到
- [I] 预处理每个 所乘上的系数,即 。
这个式子有 和 ,不好搞,所以我们把 分成 和 的分别进行处理。
对于 ,式子变成 。我们从左往右枚举 ,每次添加 的一个新元素。不难发现 随着 变大而变小,具体是每次 时原来所有的 的贡献都会减掉 。
所以我们在增加 的贡献时,同时维护 ,每次累加这个值,从总贡献当中减去。因此,我们就成功完成了 的这一部分。
对于 同理,从后往前遍历,注意 和 互换了。这些预处理都是 的。
最后每个 多累加了一次,从贡献中删去 即可。这下我们就可以轻松地得出初始答案,每次查询对 的 值加上 也就相当于再答案上累加了 的系数之和乘上 。这个可以通过前缀和实现
时间复杂度 ,要注意常数。
#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;
}红魔馆
- [!] 以下规定 个关键点的行走方式为路线,任意两点间的为路径。
首先我们需要先求出初始答案。按照每一条遍历方式求总和肯定是不可取的,于是仍然考虑拆贡献。本题没有对走的路线进行任何规定,所以任何两个属于 个关键点的点都可以连起来,作为路线的一部分。
对于任意一条两个关键点连起来的路径,它会在 条路线当中出现过。即枚举路径前有 个关键点经过,则有 种排列方式,剩下 个点放在后面随便排。
- [I] 事实上这个数等于 ,但是我赛时没管这么多,写的暴力求和。
现在我们考虑如何计算任意两个关键点之间的距离之和。考虑树形 dp,设 表示以 为起点,到 子树中所有关键点所需要的距离之和。若 为 子树中的关键点之和,那么有
其实就是将 到 子树内的关键点的距离拼上 。不过我们求的不是 到 子树内,而是 到任意关键点(除 本身)的距离和。
考虑换根,把每个 看作根,计算 到树内任意关键点的距离和。设其为 。如果要将根从 转移到 的话, 子树内的所有关键点到根的距离就会减去 ,其余的所有关键点到根的距离就会加上 ,所以有
此时我们就通过 计算出了每个点到任意关键点之间的距离之和,那么把所有关键点的这个值累加起来即可得到路径距离总和。乘上之前的系数 或 即可得到任意的路线距离总和的总和,除以总路线数量 即可得到初始答案。
现在考虑修改。如果我们修改一个点 邻边边权增加 ,而它的邻边被任意两个关键点之间的路径经过的次数为 ,那么答案会加上 (还要乘上系数和 )。
如何求 呢?如果 是关键点,那么它会被经过 次( 到其它关键点和其它关键点到 )。其次是作为路径上的点,那么可以遍历 的所有邻点 ,获取以 根 子树内关键点的数量(仅当 时有区别,其余都是 ),然后对于任意两个不同 的数量相乘再乘 (不是一正一反,而是经过了 的两条邻边)。当然肯定不能枚举,具体操作是求和后平方,再减去每个项的平方。
现在我们就成功写出这道题了。时间复杂度 ,也是需要注意常数的一道题。
#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;
}红楼
我在考场上想出了根号分治,但是没有优化掉 。
首先题目中的修改操作可以视作以 为一个周期,每个周期前 个数加上 。
- 对于 ,我暴力枚举每一个周期,用线段树做区间加法。修改时间复杂度 ,查询 。
- 对于 ,我对于每个 都会开一颗 大小的线段树(维护和还有下标乘平方的和),然后对于 线段树的前 个数加上 。查询时枚举每一个 ,然后先把整块的给加上,最后剩的一些对于所有 的系数都会被限制在 。修改时间复杂度 ,查询时间复杂度 。
不难发现复杂度瓶颈在于第一种操作的修改。我们需要 对一个区间进行加法(这样单词修改就变成了 ),而查询时间复杂度可以变成 。
这是一个相当复杂的分块,需要使用复杂的差分+前缀和技巧实现。不详细说了。
对于第二种操作,修改时间复杂度太优了查询复杂度太劣,所以不开 的线段树了,直接 暴力修改前缀和数组,这样单块内查询就能做到 。总时间复杂度就可以达到 。