福建 NOI 2025 省队集训做题记录
| 场次 | \(\quad\qquad\text{A}\qquad\quad\) | 完成情况 | \(\quad\qquad\text{B}\qquad\quad\) | 完成情况 | \(\quad\qquad\text{C}\qquad\quad\) | 完成情况 |
|---|---|---|---|---|---|---|
| \(\text{Day 1}\) | 琥峪枫 | \(\color{green}\checkmark\) | 篱莘龙 | \(\color{green}\checkmark\) | 棽荠郁 | \(\color{green}\checkmark\) |
| \(\text{Day 2}\) | Change | \(\color{green}\checkmark\) | Toxic | Tree | \(\color{green}\checkmark\) | |
| \(\text{Day 3}\) | 沙湾 | \(\color{green}\checkmark\) | 二叉虚树 | 未来的我 | \(\color{green}\checkmark\) | |
| \(\text{Day 4}\) | 程序攻击 | 灯泡翻转 | \(\color{green}\checkmark\) | 星球大战 | ||
| \(\text{Day 5}\) | 逛公园 | 快递公司 | 小 C 的比赛 | |||
| \(\text{Day 6}\) | 逛公园 | 快递公司 | 小 C 的比赛 | |||
| \(\text{Day 7}\) | 逛公园 | 快递公司 | 小 C 的比赛 |
\(\color{blue}\mathbf{Round\;1}\)
琥峪枫
有一棵 \(h\) 层的完全二叉树,共有 \(n=2^h-1\) 个节点,每个点都有一个隐藏的正整数点权 \(a_i\),并且每个点的编号不确定。
你一次询问需要给定一个点的编号 \(x\) 和距离 \(d(d\ge1)\),交互库会返回每个与 \(x\) 的距离恰为 \(d\) 的点的点权之和。你需要在 \(2n+3\) 次操作内得到所有点的点权和,并且每个点作为 \(x\) 的次数均不超过 \(4\)。
\(\texttt{Data Range:}\;h\le15\texttt{; Time Limit: 2000ms; Memory Limit: 512 MiB}\)。
记一次询问为 \((x,d)\)。对于每个点 \(x\) 询问 \((x,1)\) 即可得到 \(3\sum\limits_{i=1}^{n}a_i-a_{\mathit{rt}}-2\sum\limits_{\mathit{deg}_i=1}a_i\),而 \(\sum\limits_{\mathit{deg}_i=1}a_i\) 可以通过询问 \((\mathit{rt},h-1)\) 来实现。
为了得到 \(\mathit{rt}\) 和 \(a_{\mathit{rt}}\),我们可以通过询问所有 \((*,h+1)\) 来得到所有深度 \(\le2\) 的节点,再通过询问 \((*,h)\) 来得到根及它的两个邻点 \(a,b\),那么 \(a_{\mathit{rt}}\) 就等于 \((a,1)+(b,1)-(\mathit{rt},2)\)。
但是这样 \(\mathit{rt}\) 会被询问 \(5\) 次,所以可以将 \((\mathit{rt},h-1)\) 用 \((a,h)+(b,h)\) 代替,同时减少一次询问,此时询问次数为 \(2n+4\)。为了再减少一次询问,只需要当确定 \(n-1\) 的深度都 \(>2\) 时不再询问 \((n,h+1)\) 即可。
点击查看代码
#include <bits/stdc++.h>
#include "tree.h"
using namespace std;
long long solve(int, int h) {
int n = (1 << h) - 1;
vector<long long> val(n + 1);
for (int i = 1; i <= n; ++i) val[i] = ask(i, 1);
vector< pair<int, long long> > top;
for (int i = 1; i < n; ++i) {
if (!ask(i, h + 1)) top.push_back({i, ask(i, h)});
}
if (top.size() < 3) top.push_back({n, ask(n, h)});
int rt = 0;
for (auto [x, y] : top) {
if (!y) rt = x;
}
top.erase(find(top.begin(), top.end(), make_pair(rt, 0ll)));
return (accumulate(val.begin(), val.end(), 0ll) + 2 * (top[0].second + top[1].second) + (val[top[0].first] + val[top[1].first] - ask(rt, 2)) / 2) / 3;
}
篱莘龙
有 \(n\) 个数对 \((a_i,b_i)\),保证 \(1\le a_i,b_i\le2n\) 且 \(a_i,b_i\) 互不相同。对于每个 \(1\le k\le n\) 求出,如果只考虑编号不超过 \(k\) 的数对,最多选出多少个数对使得不存在两个不同的数对 \((a_i,b_i),(a_j,b_j)\) 使得 \(a_i>b_j\)。
\(\texttt{Data Range:}\;n\le10^6\texttt{; Time Limit: 3000ms; Memory Limit: 1024 MiB}\)。
发现 \(a_i>b_i\) 的数对 \((a_i,b_i)\) 最多选择 \(1\) 个,所以我们应在最大化 \(a_i<b_i\) 的数对数量的前提下考虑是否能再选取一个 \(a_i>b_i\) 的数对。
这即所有区间 \([a_i,b_i]\) 有交,设它们都经过 \(x\),则使用线段树维护覆盖 \(x\) 的线段的数量即可得到选取 \(a_i<b_i\) 的数对的最多数量。此时能够再选取数对 \((a_j,b_j)\) 当且仅当 \([b_j,a_j]\) 是所有 \([a_i,b_i]\) 的交的子集,因此 \([b_j,a_j]\) 中必然不存在其它区间的端点。
可以简单维护所有这样的区间,并把它们的贡献都记录在左端点上,那么最后能够再选取一个这样的区间当且仅当存在一个位置 \(x\) 使得覆盖它的区间数量最大且其为一个 \(b_i<a_i\) 的左端点,时间复杂度 \(\mathcal{O}(n\log n)\)。
点击查看代码
#include <bits/stdc++.h>
using namespace std;
const int N = 1e6 + 5;
pair<int, int> mx[1 << 22];
int lz[1 << 22];
void pushdown(int k) {
if (lz[k]) {
mx[k << 1].first += lz[k], mx[k << 1 | 1].first += lz[k];
lz[k << 1] += lz[k], lz[k << 1 | 1] += lz[k];
lz[k] = 0;
}
}
void pushup(int k) {
mx[k] = max(mx[k << 1], mx[k << 1 | 1]);
}
void modify(int k, int l, int r, int x, int y) {
if (l > y || r < x) return;
if (l >= x && r <= y) {
++mx[k].first, ++lz[k];
return;
}
pushdown(k);
int mid = (l + r) >> 1;
modify(k << 1, l, mid, x, y), modify(k << 1 | 1, mid + 1, r, x, y);
pushup(k);
}
void add(int k, int l, int r, int x, int v) {
if (l == r) {
mx[k].second += v;
return;
}
pushdown(k);
int mid = (l + r) >> 1;
if (x <= mid) add(k << 1, l, mid, x, v);
else add(k << 1 | 1, mid + 1, r, x, v);
pushup(k);
}
pair<int, int> seg[1 << 22];
void Modify(int k, int l, int r, int x, int y) {
if (l == r) {
seg[k] = {y, x};
return;
}
int mid = (l + r) >> 1;
if (x <= mid) Modify(k << 1, l, mid, x, y);
else Modify(k << 1 | 1, mid + 1, r, x, y);
seg[k] = max(seg[k << 1], seg[k << 1 | 1]);
}
pair<int, int> Query(int k, int l, int r, int x, int y) {
if (l > y || r < x || seg[k].first < y) return {0, 0};
if (l >= x && r <= y) return seg[k];
int mid = (l + r) >> 1;
return max(Query(k << 1, l, mid, x, y), Query(k << 1 | 1, mid + 1, r, x, y));
}
void del(int x, int n) {
while (1) {
pair<int, int> p = Query(1, 1, 2 * n, 1, x);
if (p.first < x) break;
add(1, 1, 2 * n, p.second, -1);
Modify(1, 1, 2 * n, p.second, 0);
}
}
int tr[N << 1];
signed main() {
int n; scanf("%*d%d", &n);
for (int i = 1; i <= 2 * n; ++i) tr[i] = 1e9;
for (int i = 1, a, b; i <= n; ++i) {
scanf("%d%d", &a, &b);
if (a <= b) {
modify(1, 1, 2 * n, a, b);
for (int j = a; j; j -= j & -j) tr[j] = min(tr[j], a);
for (int j = b; j; j -= j & -j) tr[j] = min(tr[j], b);
del(a, n), del(b, n);
} else {
swap(a, b);
int mn = 1e9;
for (int j = a; j <= 2 * n; j += j & -j) mn = min(mn, tr[j]);
if (mn > b) {
add(1, 1, 2 * n, a, 1);
Modify(1, 1, 2 * n, a, b);
}
}
printf("%d\n", mx[1].first + mx[1].second);
}
return 0;
}
棽荠郁
给定一棵 \(n\) 个节点的树,边有边权(可能为负),求在所有 \(n\) 阶排列 \(p\) 中,\(\sum\limits_{i=1}^{n}\operatorname{dis}(p_i,p_{i\bmod n+1})\) 的最小值。
\(\texttt{Data Range:}\;n\le2\times10^5\texttt{; Time Limit: 2000ms; Memory Limit: 512 MiB}\)。
先考虑判断一个边权系数(显然需要为正整数)是否合法,可以证明,一个边权系数可能被取到当且仅当对于每个点 \(x\),设它相邻的所有边的系数构成集合 \(S\),那么 \(2\max\limits_{x\in S}x\le\sum\limits_{x\in S}x+1\),使用朴素的背包 dp,记录当前节点的 \(\max\limits_{x\in S}x\) 和 \(\sum\limits_{x\in S}x\) 即可做到多项式复杂度。
瓶颈在于背包的合并,发现两个下标分别为 \(0\sim a,0\sim b\) 的 dp 数组 \(a,b\) 可以等价为一个下标为 \(0\sim a+b\) 的数组 \(c\),其中 \(c_i=\min\limits_{0\le j\le a,0\le k\le b,|j-k|\le i\le j+k}(b_j+c_k)\),初始时的数组 \(f_x\) 下标范围为 \(0\sim1\) 且满足 \(f_{x,0}=f_{x,1}=0\),转移结束后只需整体加上一个正比例函数并将 \(f_{x,0}\) 设为 \(+\inf\) 即可(\(x\ne\mathit{rt}\))。
不难使用数学归纳法证明 \(f_{x,*}\) 具有凸性。所以可以使用平衡树维护差分数组,这里我们选用非旋 Treap 并使用树上启发式合并维护,时间复杂度 \(\mathcal{O}(n\log^2n)\)。
点击查看代码
#include <bits/stdc++.h>
using namespace std;
const int N = 2e5 + 5, M = N * 20;
const long long inf = 1e18;
vector< pair<int, int> > G[N];
struct Node {
int ls, rs, siz;
unsigned int key;
long long val, sum, lz;
} p[M];
int tot;
mt19937 rnd;
vector<int> rub;
int newNode(long long x) {
int id;
if (rub.empty()) id = ++tot, assert(tot < M);
else id = rub.back(), rub.pop_back();
p[id].ls = p[id].rs = 0, p[id].siz = 1;
p[id].key = rnd(), p[id].val = p[id].sum = x;
p[id].lz = 0;
return id;
}
void addTag(int x, long long v) {
p[x].lz += v, p[x].val += v, p[x].sum += v * p[x].siz;
}
void pushdown(int x) {
if (p[x].lz) {
if (p[x].ls) addTag(p[x].ls, p[x].lz);
if (p[x].rs) addTag(p[x].rs, p[x].lz);
p[x].lz = 0;
}
}
void update(int x) {
p[x].siz = p[p[x].ls].siz + p[p[x].rs].siz + 1;
p[x].sum = p[p[x].ls].sum + p[p[x].rs].sum + p[x].val;
}
void modifyL(int x, long long v) {
pushdown(x);
if (!p[x].ls) p[x].val += v;
else modifyL(p[x].ls, v);
update(x);
}
vector<long long> val;
void Dfs(int x, int &y) {
if (!x || !y) return;
pushdown(x);
if (p[x].ls) Dfs(p[x].ls, y);
if (y) val.push_back(p[x].val), --y;
if (p[x].rs) Dfs(p[x].rs, y);
}
vector<long long> get(int x, int y) {
val.clear(), Dfs(x, y);
assert(is_sorted(val.begin(), val.end()));
return val;
}
vector<long long> rev(vector<long long> x) {
for (auto &v : x) v = -v;
reverse(x.begin(), x.end());
return x;
}
void Del(int x) {
if (!x) return;
rub.push_back(x);
Del(p[x].ls), Del(p[x].rs);
}
void split(int x, long long y, int &a, int &b) {
if (!x) {
a = b = 0;
return;
}
pushdown(x);
if (p[x].val <= y) {
a = x, split(p[x].rs, y, p[a].rs, b);
} else {
b = x, split(p[x].ls, y, a, p[b].ls);
}
update(x);
}
void Split(int x, int y, int &a, int &b) {
if (!x) {
a = b = 0;
return;
}
pushdown(x);
if (p[p[x].ls].siz + 1 <= y) {
a = x, Split(p[x].rs, y - p[p[x].ls].siz - 1, p[a].rs, b);
} else {
b = x, Split(p[x].ls, y, a, p[b].ls);
}
update(x);
}
int merge(int a, int b) {
if (!a || !b) return a + b;
pushdown(a), pushdown(b);
if (p[a].key < p[b].key) {
p[a].rs = merge(p[a].rs, b);
update(a);
return a;
}
p[b].ls = merge(a, p[b].ls);
update(b);
return b;
}
void insert(int &x, long long y) {
int a, b;
split(x, y, a, b);
x = merge(merge(a, newNode(y)), b);
}
long long preMin(int &x) {
int a, b;
split(x, 0, a, b);
long long res = p[a].sum;
x = merge(a, b);
return res;
}
int zero(int x) {
int rt = 0;
while (x--) rt = merge(rt, newNode(0));
return rt;
}
struct Info {
long long v;
int rt;
};
int resize(int x, int y) {
int a, b;
Split(x, p[x].siz - y, a, b);
Del(a);
return b;
}
int Minkov(vector<long long> val, int rt) {
for (auto x : val) insert(rt, x);
return rt;
}
Info merge(Info x, Info y) {
int a, b, c, d;
split(x.rt, 0, a, b), split(y.rt, 0, c, d);
long long S = p[a].siz + p[b].siz + p[c].siz + p[d].siz, sum = x.v + p[a].sum + y.v + p[c].sum;
if (p[a].siz > p[c].siz) swap(x, y), swap(a, c), swap(b, d);
Info res;
if (p[b].siz <= p[d].siz) {
res.rt = merge(merge(resize(Minkov(rev(get(b, p[c].siz - p[a].siz)), c), p[c].siz - p[a].siz), zero(p[a].siz * 2)), Minkov(get(b, p[b].siz), d));
Del(a), Del(b);
} else {
res.rt = merge(resize(Minkov(rev(get(b, p[c].siz - p[a].siz)), c), p[c].siz - p[a].siz), zero(p[a].siz * 2));
res.rt = merge(res.rt, Minkov(get(d, p[d].siz), b));
Del(a), Del(d);
}
res.v = sum - preMin(res.rt);
assert(p[res.rt].siz == S);
return res;
}
Info dfs(int x, int fa = 0, int len = 0) {
Info cur;
cur.v = 0, cur.rt = newNode(0);
for (auto [v, w] : G[x]) {
if (v == fa) continue;
cur = merge(cur, dfs(v, x, w));
}
if (x == 1) printf("%lld\n", cur.v * 2);
else {
addTag(cur.rt, len), modifyL(cur.rt, cur.v - inf), cur.v = inf;
}
return cur;
}
signed main() {
int T; scanf("%*d%d", &T);
while (T--) {
int n; scanf("%d", &n);
for (int i = 1; i <= n; ++i) G[i].clear();
for (int i = 1, x, y, w; i < n; ++i) {
scanf("%d%d%d", &x, &y, &w);
G[x].push_back({y, w});
G[y].push_back({x, w});
}
tot = 0, rub.clear(), dfs(1);
}
return 0;
}
\(\color{blue}\mathbf{Round\;2}\)
\(\text{Change}\)
给定长度为 \(n\) 的整数数组 \(a\),每次操作你可以选择下标区间 \([l,r]\),并令所有 \(a_{l+3i+j}\) 加上 \((-1)^j(l+3i+j\le r,i\in\mathbb{N},j\in\{0,1\})\),求最少操作次数使得 \(a_i\) 全为 \(0\) 或输出无解。\(T\) 组询问,每组询问的 \(n\) 相同。
\(\texttt{Data Range:}\;T=30,n\le3\times10^4\texttt{; Time Limit: 1000ms; Memory Limit: 1024 MiB}\)。
将 \(a\) 中的每个元素均取反,并将加上改为减去,则显然有解的充要条件是 \(a\) 的前缀和不小于 \(0\)。
将 \(a\) 超出下标范围内的数视作 \(0\),令 \(b_i=a_{i-2}+a_{i-1}+a_i(1\le i\le n+2)\)。此时操作等价于选择 \(l\equiv r\pmod3\) 的两个下标 \(l,r(1\le l\le r)\) 并令 \(b_l\to b_l-1,b_r\to b_r+1(r\le n+2)\) 或 \(b_l\to b_l-1,b_{r+1}\to b_{r+1}-1,b_{r+2}\to b_{r+2}-1(r\le n)\),目标即为将 \(b\) 变为全 \(0\) 数列(只需考虑 \(i\le n\) 的部分)。
初始时,先令答案为 \(\sum\limits_{i=1}^{n}\max(b_i,0)\),最后根据进行操作 \(2\) 的情况减小答案,我们需要让操作 \(2\) 结束后仍有 \(a\) 的前缀和不小于 \(0\),因此我们可以计算出每个前缀中元素被减的最多次数并差分得到每个位置的操作次数。
点击查看代码
#include <bits/stdc++.h>
using namespace std;
const int N = 3e4 + 10;
long long a[N], b[N], c[N];
signed main() {
freopen("trans.in", "r", stdin);
freopen("trans.out", "w", stdout);
int T, n; scanf("%d%d", &T, &n);
while (T--) {
int fl = 1;
for (int i = 1; i <= n; ++i) {
scanf("%lld", &a[i]), a[i] = -a[i], a[i] += a[i - 1], fl &= (a[i] >= 0);
}
if (!fl) {
puts("-1");
continue;
}
long long res = 0;
for (int i = 1; i <= n; ++i) {
b[i] = max(a[i] - a[max(i - 3, 0)], 0ll), res += b[i];
}
for (int i = n - 1; i; --i) a[i] = min(a[i], a[i + 1]);
for (int i = n; i; --i) a[i] -= a[i - 1];
long long c1 = 0, c2 = 0;
for (int i = n - 1; i; --i) {
long long t = min(a[i], c2);
res -= t * 2, c2 -= t, a[i] -= t;
t = min(a[i], c1);
res -= t, c1 -= t, a[i] -= t;
t = min(a[i], b[i + 1]);
res -= t, a[i] -= t, b[i + 1] -= t;
t = min(b[i], b[i + 1]);
c2 += t, b[i] -= t, b[i + 1] -= t;
c1 += b[i + 1];
}
printf("%lld\n", res);
}
return 0;
}
\(\text{Tree}\)
给定一棵 \(n\) 个点的树,每个点上有一个数对 \((a_i,b_i)\),保证 \(a,b\) 分别构成了一个排列。初始时每个点上有 \(1\) 个人,人们都向根的方向移动,每个人会等子树内的人都到达后再移动。当两个人第一次见面于点 \(x\) 时,会在双方的背包中放入数对 \((a_x,b_x)\),求所有人数对中严格逆序对的数量之和。
同时,还有 \(q\) 次询问,每次询问给定不超过 \(2\) 个人,询问当这些节点上没有人时,上述问题的答案。
\(\texttt{Data Range:}\;n,q\le1.5\times10^5\texttt{; Time Limit: 3500ms; Memory Limit: 256 MiB}\)。
答案即为有序对 \((a,b,c)\) 的个数,满足 \(\operatorname{LCA}(a,c)=\operatorname{LCA}(b,c)\),且 \(\operatorname{LCA}(a,b)\) 与 \(\operatorname{LCA}(a,c)\) 上面的数对构成了一组严格逆序对。
我们可以对于每个 \(a\) 计算出其对应的 \((a,b,c)\) 的个数,这可以在树上 dfs 的时候维护所有它的祖先对应的数对以及出现次数来实现,并记作 \(f_a\)。
具体应该怎么维护
将所有关于这个集合的操作离线处理,使用 cdq 分治计算出每个数对前与它构成逆序对的数对个数,再前缀和即可。当未删除节点时,答案即为 \(\sum\limits_{i=1}^{n}f_i\),当删除一个点的时候,我们除去 \(2f_i\) 以及它作为 \(c\) 的情况数,后者只需要对上述问题进行转置即可。
当删去两个点的时候,我们需要用总方案数分别减去两个点对应的情况数再加上同时包含这两个点的数对个数。假设 \(g(x,y)\) 表示 \(\sum\limits_{i\ne x}h(\operatorname{LCA}(x,i),y)\),其中 \(h(x,y)\) 表示 \(x\) 和 \(y\) 上的数对是否构成一组逆序对,那么需要额外加上的情况数就是 \(g(p,\operatorname{LCA}(p,q))+g(q,\operatorname{LCA}(p,q))-g(\operatorname{LCA}(p,q),\operatorname{LCA}(p,q))\),再次使用 cdq 分治解决即可。
注意实现和常数优化,总时间复杂度 \(\mathcal{O}(n\log^2n)\)。
点击查看代码
#include <bits/stdc++.h>
using namespace std;
const int N = 1.5e5 + 5;
int n, a[N], b[N], siz[N];
vector<int> G[N];
long long ans[N];
struct cdq {
int cnt, imp[N << 3];
tuple<int, int, long long> p[N << 3];
long long val[N << 3], sum[N];
void modify(int x, long long y) {
for (; x <= n; x += x & -x) sum[x] += y;
}
long long query(int x) {
long long res = 0;
for (; x; x -= x & -x) res += sum[x];
return res;
}
void solve(int l, int r) {
if (l == r) return;
int mid = (l + r) >> 1;
solve(l, mid), solve(mid + 1, r);
vector< tuple<int, int, long long> > v1, v2;
for (int i = l; i <= mid; ++i) {
if (get<2>(p[i])) v1.push_back({get<0>(p[i]), n - get<1>(p[i]) + 1, get<2>(p[i])});
}
for (int i = mid + 1; i <= r; ++i) {
if (imp[i]) v2.push_back({get<0>(p[i]), n - get<1>(p[i]) + 1, i});
}
sort(v1.begin(), v1.end());
sort(v2.begin(), v2.end());
int j = 0;
for (int i = 0; i < v2.size(); ++i) {
while (j < v1.size() && get<0>(v1[j]) < get<0>(v2[i])) {
modify(get<1>(v1[j]), get<2>(v1[j])), ++j;
}
val[get<2>(v2[i])] += query(get<1>(v2[i]) - 1);
}
for (int i = 0; i < j; ++i) {
modify(get<1>(v1[i]), -get<2>(v1[i]));
}
reverse(v1.begin(), v1.end()), reverse(v2.begin(), v2.end());
j = 0;
for (int i = 0; i < v2.size(); ++i) {
while (j < v1.size() && get<0>(v1[j]) > get<0>(v2[i])) {
modify(n - get<1>(v1[j]) + 1, get<2>(v1[j])), ++j;
}
val[get<2>(v2[i])] += query(n - get<1>(v2[i]));
}
for (int i = 0; i < j; ++i) {
modify(n - get<1>(v1[i]) + 1, -get<2>(v1[i]));
}
}
void modify(int x, int y, long long v) {
++cnt;
p[cnt] = {x, y, v};
}
void init(int id) {
if (id == 1) {
for (int i = 1; i <= cnt; ++i) {
if (get<2>(p[i])) imp[i] = 1;
}
} else if (id == 2) {
for (int i = 1; i <= cnt; ++i) {
imp[i] = 1;
}
} else {
for (int i = 1; i <= cnt; ++i) {
if (!(get<2>)(p[i])) imp[i] = 1;
}
}
if (id == 2) {
reverse(p + 1, p + cnt + 1);
}
solve(1, cnt);
if (id == 2) {
reverse(val + 1, val + cnt + 1);
}
if (id == 1) {
for (int i = 1; i <= cnt; ++i) val[i] = val[i - 1] + 1ll * val[i] * get<2>(p[i]);
}
}
} ds1, ds2, ds3;
long long S[N];
void preDfs(int x, int fa = 0) {
siz[x] = 1;
for (auto v : G[x]) {
if (v != fa) preDfs(v, x), siz[x] += siz[v];
}
S[x] = 1ll * siz[x] * (siz[x] - 1);
for (auto v : G[x]) {
if (v != fa) S[x] -= 1ll * siz[v] * (siz[v] - 1);
}
}
void dfs1(int x, int fa = 0) {
ds1.modify(a[x], b[x], siz[x] - 1);
ans[x] = ds1.cnt;
ds1.modify(a[x], b[x], 1);
for (auto v : G[x]) {
if (v == fa) continue;
ds1.modify(a[x], b[x], -siz[v]);
dfs1(v, x);
ds1.modify(a[x], b[x], siz[v]);
}
ds1.modify(a[x], b[x], -siz[x]);
}
int Fa[N], pos1[N], pos2[N], pos3[N], pos4[N];
long long dlt[N];
void dfs2(int x, int fa = 0) {
Fa[x] = fa;
if (fa) {
ds2.modify(a[fa], b[fa], 0);
pos3[x] = ds2.cnt;
}
ds2.modify(a[x], b[x], S[x]);
pos1[x] = ds2.cnt;
for (auto v : G[x]) {
if (v == fa) continue;
dfs2(v, x);
}
ds2.modify(a[x], b[x], 0);
pos2[x] = ds2.cnt;
if (fa) {
ds2.modify(a[fa], b[fa], 0);
pos4[x] = ds2.cnt;
}
}
void dfs3(int x, int fa = 0) {
for (auto v : G[x]) {
if (v == fa) continue;
dlt[v] += dlt[x], dfs3(v, x);
}
}
long long Delta(int x) {
return ans[x] * 2 + dlt[x];
}
int dep[N], son[N], top[N];
void dfs4(int x) {
for (auto v : G[x]) {
if (v != Fa[x]) dep[v] = dep[x] + 1, dfs4(v), ((siz[v] > siz[son[x]]) && (son[x] = v));
}
}
void dfs5(int x) {
for (auto v : G[x]) {
if (v != Fa[x]) top[v] = (v == son[x] ? top[x] : v), dfs5(v);
}
}
int LCA(int x, int y) {
while (top[x] != top[y]) {
if (dep[top[x]] < dep[top[y]]) swap(x, y);
x = Fa[top[x]];
}
return (dep[x] < dep[y] ? x : y);
}
long long res[N];
vector< pair<int, int> > qr[N];
void Common(int x, int y, int id) {
int l = LCA(x, y);
qr[x].push_back({l, id}), qr[y].push_back({l, id}), qr[l].push_back({l, -id});
}
void dfs6(int x, int fa = 0) {
ds3.modify(a[x], b[x], siz[x] - 1);
for (auto &[v, w] : qr[x]) {
ds3.modify(a[v], b[v], 0);
v = ds3.cnt;
}
ds3.modify(a[x], b[x], 1);
for (auto v : G[x]) {
if (v == fa) continue;
ds3.modify(a[x], b[x], -siz[v]);
dfs6(v, x);
ds3.modify(a[x], b[x], siz[v]);
}
ds3.modify(a[x], b[x], -siz[x]);
}
signed main() {
freopen("second.in", "r", stdin);
freopen("second.out", "w", stdout);
int q; scanf("%d%d", &n, &q);
for (int i = 1; i <= n; ++i) {
scanf("%d%d", &a[i], &b[i]);
}
for (int i = 1, x, y; i < n; ++i) {
scanf("%d%d", &x, &y);
G[x].push_back(y);
G[y].push_back(x);
}
preDfs(1), dfs1(1);
ds1.init(1);
for (int i = 1; i <= n; ++i) ans[i] = ds1.val[ans[i]];
long long sum = accumulate(ans + 1, ans + n + 1, 0ll);
dfs2(1), ds2.init(2);
for (int i = 2; i <= n; ++i) {
dlt[i] = (ds2.val[pos1[Fa[i]]] - ds2.val[pos2[Fa[i]]]) - (ds2.val[pos3[i]] - ds2.val[pos4[i]]);
}
dfs3(1);
for (int i = 1; i <= n; ++i) {
dlt[i] += ds2.val[pos1[i]] - ds2.val[pos2[i]];
}
dfs4(dep[1] = 1), dfs5(top[1] = 1);
res[0] = sum;
for (int i = 1; i <= q; ++i) {
int opt; scanf("%d", &opt);
if (opt == 1) {
int x; scanf("%d", &x);
res[i] = sum - Delta(x);
} else {
int x, y; scanf("%d%d", &x, &y);
res[i] = sum - Delta(x) - Delta(y);
Common(x, y, i);
}
}
dfs6(1), ds3.init(3);
for (int i = 1; i <= n; ++i) {
for (auto [v, w] : qr[i]) {
int id = abs(w);
res[id] += 2 * (w / id) * ds3.val[v];
}
}
for (int i = 0; i <= q; ++i) printf("%lld\n", res[i]);
return 0;
}
\(\color{blue}\mathbf{Round\;3}\)
沙湾
有 \(c\) 种共 \(n\) 个普通石子和 \(m\) 个特殊石子,其中第 \(i\) 种石子的个数恰好为 \(a_i\),同时给定长度为 \(n+1\) 的数列 \(w_i\),并且对于第 \(i(1\le i\le n)\) 种石子给定长度为 \(a_i\) 的数列 \(v_{i,j}\)。
假设从左往右的第一个特殊石子编号为 \(x\),最右边的石子编号为 \(y\),上一个与其种类不同的(可以为特殊石子)石子右侧有 \(z\) 个石子,则它的权值为 \(w_x\times v_{y,z}\)(特别的,如果最右边的石子为特殊石子,权值为 \(w_x\))。
求在所有可能的情况下,权值之和对 \(998244353\) 取模后的结果。
\(\texttt{Data Range:}\;c\le n\le2\times10^5,m\le30\texttt{; Time Limit: 1000ms; Memory Limit: 512 MiB}\)。
最右边的石子为特殊石子的情况是平凡的。
先对 \(v_i\) 进行后缀差分,再枚举 \(y,z(z\le a_y)\),那么就不需要考虑第 \(n-z\) 个石子不为 \(y\) 的限制。先对于普通石子定序,再选取特殊石子的位置,可知此时对应的贡献为 \(\sum\limits_{i=1}^{n+1}w_iv_{y,z}\dbinom{n-z}{a_1,a_2,\cdots,a_{y-1},a_y-z,a_{y+1},\cdots,a_{n}}\dbinom{n+m-i-z}{m-1}\)。
当 \(y,z\) 确定时,\(v_{y,z}\dbinom{n-z}{a_1,a_2,\cdots,a_{y-1},a_y-z,a_{y+1},\cdots,a_{n}}\) 也是确定的,记对于每个 \(z\),所有这样的系数之和为 \(f_z\)。
记 \(g\) 为 \(f\) 和 \(w\) 的卷积,则答案为 \(\sum\limits_{i=1}^{n+1}g_i\dbinom{n+m-i}{m-1}\),使用 NTT 计算即可做到 \(\mathcal{O}((n+m)\log(n+m))\)。
点击查看代码
#include <bits/stdc++.h>
using namespace std;
using poly = vector<int>;
const int N = 2e5 + 5, P = 998244353, G = 3, iG = (P + 1) / 3;
int qpow(int x, int y = P - 2) {
int res = 1;
while (y) {
if (y & 1) res = 1ll * res * x % P;
x = 1ll * x * x % P;
y >>= 1;
}
return res;
}
int fac[N << 1], inv[N << 1], iFac[N << 1], w[N], cnt[N];
vector<int> val[N];
int C(int x, int y) {
if (x < 0 || y < 0 || x < y) return 0;
return 1ll * fac[x] * iFac[y] % P * iFac[x - y] % P;
}
int iC(int x, int y) {
return 1ll * iFac[x] * fac[y] % P * fac[x - y] % P;
}
int coef[N];
void NTT(vector<int> &f, int ty) {
int n = f.size();
vector<int> tr(n);
for (int i = 1; i < n; ++i) {
tr[i] = (tr[i >> 1] >> 1) | ((i & 1) ? n >> 1 : 0);
}
for (int i = 0; i < n; ++i) {
if (i < tr[i]) swap(f[i], f[tr[i]]);
}
for (int len = 2; len <= n; len <<= 1) {
int buf = qpow(ty ? G : iG, (P - 1) / len);
for (int i = 0; i < n; i += len) {
int cur = 1;
for (int j = i; j < i + (len >> 1); ++j) {
int t1 = (f[j] + 1ll * f[j + (len >> 1)] * cur + P) % P, t2 = (f[j] - 1ll * f[j + (len >> 1)] * cur % P + P) % P;
f[j] = t1, f[j + (len >> 1)] = t2;
cur = 1ll * cur * buf % P;
}
}
}
if (!ty) {
int tinv = qpow(n);
for (auto &x : f) x = 1ll * x * tinv % P;
}
}
signed main() {
freopen("sand.in", "r", stdin);
freopen("sand.out", "w", stdout);
for (int i = fac[0] = iFac[0] = 1; i < N << 1; ++i) {
fac[i] = 1ll * fac[i - 1] * i % P;
inv[i] = (i == 1 ? 1 : 1ll * (P - P / i) * inv[P % i] % P);
iFac[i] = 1ll * iFac[i - 1] * inv[i] % P;
}
int n, m, c; scanf("%d%d%d", &n, &m, &c);
for (int i = 1; i <= n + 1; ++i) scanf("%d", &w[i]);
for (int i = 1; i <= c; ++i) scanf("%d", &cnt[i]);
for (int i = 1; i <= c; ++i) {
val[i].resize(cnt[i] + 1);
for (int j = 1; j <= cnt[i]; ++j) scanf("%d", &val[i][j]);
for (int j = cnt[i]; j; --j) {
val[i][j] = (val[i][j] - val[i][j - 1] + P) % P;
}
}
int mul = 1, res = 0;
for (int i = 1, s = 0; i <= c; ++i) {
mul = 1ll * mul * C(s + cnt[i], cnt[i]) % P;
s += cnt[i];
}
for (int i = 1, pre = 1ll * mul * C(n + m - 1, m - 1) % P; pre; ++i) {
int cur = 1ll * mul * C(n + m - 1 - i, m - 1) % P;
res = (res + 1ll * (pre - cur + P) * w[i]) % P;
pre = cur;
}
for (int i = 1; i <= c; ++i) {
for (int j = 1; j <= cnt[i]; ++j) {
int t = 1ll * mul * iC(n, cnt[i]) % P * C(n - j, cnt[i] - j) % P * val[i][j] % P;
coef[j] = (coef[j] + t) % P;
}
}
poly a(coef, coef + n + 1), b(w, w + n + 1);
int len = a.size() + b.size(), tlen = 1;
while (tlen < len) tlen <<= 1;
a.resize(tlen), b.resize(tlen);
NTT(a, 1), NTT(b, 1);
for (int i = 0; i < tlen; ++i) a[i] = 1ll * a[i] * b[i] % P;
NTT(a, 0);
for (int i = 1; i <= n + 1; ++i) {
res = (res + 1ll * (i < a.size() ? a[i] : 0) * C(n + m - i, m - 1)) % P;
}
printf("%d\n", res);
return 0;
}
未来的我
有两个数列 \(a,b\),初始时均为空,你需要支持 \(m\) 次操作,设当前数组长度为 \(n\):
- 给定 \(x,c,p,q,u,v\),分别在数列 \(a,b\) 的第 \(x\) 个数和第 \(x+1\) 个数之间插入 \(c\) 个数,其中在 \(a\) 中插入的第 \(i\) 个数为 \(pq^{i-1}\),在 \(b\) 中插入的第 \(i\) 个数为 \(u+v(i-1)\)。
- 给定 \(l,r\),分别删除数列 \(a,b\) 中的区间 \([l,r]\) 中的元素。
- 给定 \(l,r\),分别翻转数列 \(a,b\) 中的区间 \([l,r]\) 中的元素。
- 给定 \(l,r\),询问 \(\sum\limits_{i=l}^{r}a_ib_i\) 对 \(10^9+7\) 取模后的结果。
- 给定 \(l,r,u,v\),将数列 \(b\) 的第 \(i(l\le i\le r)\) 个数加上 \(u+v(i-l)\)。
\(\texttt{Data Range}:\;m\le3\times10^5,\max n\le10^9\texttt{; Time Limit: 3000ms; Memory Limit: 1024 MiB}\)。
由于 \(a\) 和 \(b\) 的修改是 “同步” 的,即修改不会改变 \(a\) 和 \(b\) 之间的对应关系,所以使用平衡树(如非旋 Treap)维护即可,为了保证一只 \(\log\) 的复杂度,需要在节点上维护:
- 当前节点的左右儿子以及对应的数列长度,以及子树内的长度总和;
- 当前节点对应的 \(a\) 的首项,末项,以及公比及其逆元;
- 当前节点对应的 \(b\) 的首项以及公差;
- 当前节点对应的对 \(b\) 的修改的懒标记(首项及公差);
- 当前节点以及子树内对应的数列的 \(a_i\times b_i\) 之和;
- 当前节点的数列对应的 \(a_i\) 之和与 \(a_i\times i\) 之和,以及翻转过后 \(a_i\times i\) 的和。
- 当前节点子树内对应的 \(a_i\) 之和与 \(a_i\times i\) 之和,以及翻转过后 \(a_i\times i\) 的和。
- 当前节点翻转操作所对应的懒标记。
注意我们需要给懒标记钦定顺序,例如强制要求翻转操作在对 \(b\) 修改的懒标记之前下传。操作时可能需要分裂至多 \(2\) 个节点,此时重新计算这 \(4\) 个新节点对应的有关 \(a\) 数组的信息即可(即上述信息的第 \(2,6,7\) 条)。而 \(\sum\limits_{i=0}^{n-1}pq^i,\sum\limits_{i=0}^{n-1}ipq^i,\sum\limits_{i=0}^{n-1}(n-1-i)pq^i\) 均可在 \(\mathcal{O}(\log P)\) 的复杂度内计算,在此不多赘述,时间复杂度 \(\mathcal{O}(m\log P)\)。
点击查看代码
#include <bits/stdc++.h>
using namespace std;
const int N = 3e5 + 5, P = 1e9 + 7;
int qpow(int x, int y = P - 2) {
int res = 1;
while (y) {
if (y & 1) res = 1ll * res * x % P;
x = 1ll * x * x % P;
y >>= 1;
}
return res;
}
struct Node {
int ls, rs, cnt, siz;
int p, q, rp, rq, u, v, lzu, lzv, val, sum;
int c0, c1, rc1, v0, v1, rv1;
bool rev;
} p[N << 2];
int tot;
mt19937 rnd;
void getC(int x) {
p[x].rp = 1ll * p[x].p * qpow(p[x].q, p[x].cnt - 1) % P, p[x].rq = qpow(p[x].q);
assert(p[x].q && p[x].cnt);
if (p[x].q == 1) {
p[x].c0 = p[x].cnt;
p[x].c1 = p[x].rc1 = 1ll * p[x].cnt * (p[x].cnt - 1) / 2 % P;
} else {
p[x].c0 = 1ll * (qpow(p[x].q, p[x].cnt) - 1) * qpow(p[x].q - 1) % P;
p[x].c1 = (1ll * qpow(p[x].q, p[x].cnt) * p[x].cnt - 1ll * p[x].c0 * p[x].q % P + P) % P * qpow(p[x].q - 1) % P;
p[x].rc1 = (1ll * (p[x].cnt - 1) * p[x].c0 - p[x].c1 + P) % P;
}
p[x].c0 = 1ll * p[x].c0 * p[x].p % P;
p[x].c1 = 1ll * p[x].c1 * p[x].p % P;
p[x].rc1 = 1ll * p[x].rc1 * p[x].p % P;
p[x].val = p[x].sum = (1ll * p[x].c0 * p[x].u + 1ll * p[x].c1 * p[x].v) % P;
}
void update(int x) {
p[x].siz = p[p[x].ls].siz + p[p[x].rs].siz + p[x].cnt;
p[x].sum = ((long long)p[p[x].ls].sum + p[p[x].rs].sum + p[x].val) % P;
int SL = p[p[x].ls].siz, SM = p[x].cnt, SR = p[p[x].rs].siz;
int SML = SM + SL, SMR = SM + SR;
p[x].v0 = ((long long)p[p[x].ls].v0 + p[x].c0 + p[p[x].rs].v0) % P;
p[x].v1 = (p[p[x].ls].v1 + p[x].c1 + 1ll * p[x].c0 * SL + p[p[x].rs].v1 + 1ll * p[p[x].rs].v0 * SML) % P;
p[x].rv1 = (p[p[x].rs].rv1 + p[x].rc1 + 1ll * p[x].c0 * SR + p[p[x].ls].rv1 + 1ll * p[p[x].ls].v0 * SMR) % P;
}
int newNode(int x, int y, int s) {
++tot;
p[tot].p = x, p[tot].q = y;
p[tot].cnt = s;
getC(tot), update(tot);
return tot;
}
void rev(int x) {
swap(p[x].ls, p[x].rs), p[x].rev ^= 1;
swap(p[x].c1, p[x].rc1), swap(p[x].v1, p[x].rv1);
p[x].u = (p[x].u + 1ll * (p[x].cnt - 1) * p[x].v) % P;
p[x].v = (P - p[x].v) % P;
swap(p[x].p, p[x].rp), swap(p[x].q, p[x].rq);
p[x].lzu = (p[x].lzu + 1ll * (p[x].siz - 1) * p[x].lzv) % P;
p[x].lzv = (P - p[x].lzv) % P;
}
void add(int x, int u, int v) {
p[x].u = (p[x].u + u + 1ll * p[p[x].ls].siz * v) % P;
p[x].v = (p[x].v + v) % P;
p[x].lzu = (p[x].lzu + u) % P;
p[x].lzv = (p[x].lzv + v) % P;
p[x].val = (p[x].val + 1ll * p[x].c0 * (u + 1ll * p[p[x].ls].siz * v % P) + 1ll * p[x].c1 * v) % P;
p[x].sum = (p[x].sum + 1ll * p[x].v0 * u + 1ll * p[x].v1 * v) % P;
}
void pushdown(int x) {
if (p[x].rev) {
if (p[x].ls) rev(p[x].ls);
if (p[x].rs) rev(p[x].rs);
p[x].rev = 0;
}
if (p[x].lzu || p[x].lzv) {
if (p[x].ls) add(p[x].ls, p[x].lzu, p[x].lzv);
if (p[x].rs) add(p[x].rs, (p[x].lzu + 1ll * p[x].lzv * (p[p[x].ls].siz + p[x].cnt)) % P, p[x].lzv);
p[x].lzu = p[x].lzv = 0;
}
}
void split(int x, int y, int &a, int &b) {
if (!x) {
a = b = 0;
return;
}
pushdown(x);
if (y <= p[p[x].ls].siz) {
b = x, split(p[x].ls, y, a, p[b].ls);
} else if (y >= p[p[x].ls].siz + p[x].cnt) {
a = x, split(p[x].rs, y - p[p[x].ls].siz - p[x].cnt, p[a].rs, b);
} else {
int cl = ++tot;
p[cl] = p[x], p[x].rs = p[cl].ls = 0;
p[cl].u = (p[x].u + 1ll * p[x].v * (y - p[p[x].ls].siz)) % P;
p[cl].p = 1ll * p[x].p * qpow(p[x].q, y - p[p[x].ls].siz) % P;
p[cl].cnt = p[x].cnt - (y - p[p[x].ls].siz), p[x].cnt = y - p[p[x].ls].siz;
a = x, b = cl;
getC(x), getC(cl);
update(cl);
}
update(x);
}
int merge(int x, int y) {
if (!x || !y) return x + y;
pushdown(x), pushdown(y);
int ret;
if (rnd() % (p[x].siz + p[y].siz) < p[x].siz) {
p[x].rs = merge(p[x].rs, y), update(x), ret = x;
} else {
p[y].ls = merge(x, p[y].ls), update(y), ret = y;
}
return ret;
}
void add(int &rt, int l, int r, int u, int v) {
int a, b, c;
split(rt, l - 1, a, b);
split(b, r - l + 1, b, c);
add(b, u, v);
rt = merge(merge(a, b), c);
}
signed main() {
freopen("future.in", "r", stdin);
freopen("future.out", "w", stdout);
int q, rt = 0; scanf("%*d%d", &q);
while (q--) {
int opt; scanf("%d", &opt);
if (opt == 1) {
int x, c, p, q, u, v, a, b; scanf("%d%d%d%d%d%d", &x, &c, &p, &q, &u, &v);
split(rt, x, a, b);
rt = merge(merge(a, newNode(p, q, c)), b);
add(rt, x + 1, x + c, u, v);
} else if (opt == 2) {
int l, r; scanf("%d%d", &l, &r);
int a, b, c;
split(rt, l - 1, a, b);
split(b, r - l + 1, b, c);
rt = merge(a, c);
} else if (opt == 3) {
int l, r; scanf("%d%d", &l, &r);
int a, b, c;
split(rt, l - 1, a, b);
split(b, r - l + 1, b, c);
rev(b);
rt = merge(merge(a, b), c);
} else if (opt == 4) {
int l, r; scanf("%d%d", &l, &r);
int a, b, c;
split(rt, l - 1, a, b);
split(b, r - l + 1, b, c);
int res = p[b].sum;
rt = merge(merge(a, b), c);
printf("%d\n", res);
} else {
int l, r, u, v; scanf("%d%d%d%d", &l, &r, &u, &v);
add(rt, l, r, u, v);
}
}
return 0;
}
\(\color{blue}\mathbf{Round\;4}\)
灯泡翻转
求在所有长度为 \(n\),值域为 \([0,2^m)\) 的整数序列中,选取若干个数得到的异或和的最大值之和对 \(998244353\) 取模后的结果。
\(\texttt{Data Range:}\;n\le10^9,m\le5\times10^6\texttt{; Time Limit: 1000ms; Memory Limit: 512MiB}\)。
假设这 \(n\) 个数在 \(\mathbb{F}_2^m\) 空间的秩为 \(r\),并记其线性基行简化阶梯形矩阵后最高位自由元分别为 \(a_1,a_2,\cdots,a_r(a_i<a_{i+1})\),则此时其对应的方案数为 \(\prod\limits_{i=0}^{r-1}\left(2^n-2^i\right)\times\prod\limits_{i=1}^{r}2^{a_i-(i-1)}\),异或最大值的期望为 \(\dfrac{1}{2}\left(2^{a_r+1}-1+\sum\limits_{i=1}^{r}2^{a_i}\right)\),因此答案为
接下来枚举 \(r\),将 \(\dfrac{1}{2}\prod\limits_{i=0}^{r-1}\left(2^n-2^i\right)\) 提取出来后,分别计算以下三个式子的值:
考虑一个长度为 \(n\) 的 \(01\) 串且恰有 \(m\) 个 \(1\),其中第 \(i\) 位为 \(1\) 当且仅当 \(i-1\in a\),则 \(\prod\limits_{i=1}^{r}2^{a_i-(i-1)}\) 即为 \(2\) 的顺序对个数次幂。根据 \(q\)-binomial 的组合意义,式 \(1\) 等于 \(\displaystyle{m\brack r}_2\)。
$q$-binomial 的组合意义
\(\displaystyle{n\brack m}_q\) 的一种组合意义是:在所有长度为 \(n\),且恰有 \(m\) 个 \(1\) 的 \(01\) 串 \(S\) 中,记 \(1\le i<j\le n\) 且 \(S_i=0,S_j=1\) 的数对 \((i,j)\) 个数为 \(c\),全部 \(S\) 对应的 \(q^c\) 之和。
类似的,将式 \(2\) 转化并使用组合意义:
式子的后半部分表示一次性在 \(m\) 个元素中选取 \(r+1\) 个,而 \(r+1\) 为其中再选取 \(r\) 个作为 \(a\) 的方案数,\(2^r\) 表示额外元素带来顺序对数量的变动。
对于式 \(3\),我们枚举 \(a_r\),并使用 \(q\)-binomial 的上指标求和,可以得到其等于 \(\displaystyle(2^m-2^{m-r}+1){m\brack r}_2+(2^r-1){m\brack r+1}\)。
点击查看代码
ceshi

浙公网安备 33010602011771号