The 2026 ICPC Asia East Continent Online Contest (II) 解题报告
A. All Closed
看了题解感觉思路好自然,为啥我不会?
我的线性代数还是太辣鸡了。感觉是线性代数的芝士不够导致的这题不会做,包括 E 也是。
首先,这个题目的意思就是一个异或空间的张成。定义集合 \(S\) 的张成空间为 \(\text{span}(S)\)。首先,如果加入的东西不是全局的,那么将每个 \(S_i\) 添加成 \(\text{span}(S)\) 显然是可以的。
但是现在加入的东西是全局的,那么我们就需要另做考虑。每个 \(S_i\) 至少被补成 \(\text{span}(S_i)\),那么我们就需要想到 \(\text{span}(S_i)-S_i\) 给其他 \(S_i\) 可能提供了一些基。这就其实我们考虑一下加入的东西的基。
也就是说,我们定义所有加入的东西为 \(I\)(也就是答案)。然后一开始所有 \(S\) 共有的元素为 \(T\)。\(U=I\cup T\)。那么插入完之后所有的集合共有的元素就是 \(U\) 集合中的元素。我们顺着考虑每个 \(\text{span}(S_i)-S_i\) 给 \(U\) 贡献了哪些基。
我们肯定不能直接枚举 \(\text{span}(S_i)-S_i\)。因为这样的时间复杂度是 \(\mathcal{O}(2^m)\)。那么最多 \(n\) 次,总时间复杂度变成 \(\mathcal{O}(n2^m)\) 显然是不能接受的。
这里用到了一个均摊时间复杂度的办法。首先这是一个 0/1\(m\) 维线性空间,我们分成两种情况。
- \(|\text{span}(S_i)-S_i|\ge\frac{1}{2}|\text{span}(S_i)|\)
这也就是说,差的元素更多,由于这是一个 0/1 线性空间,所以 \(\text{span}(\text{span}(S_i)-S_i)=\text{span}(S_i)\)。那么我们直接用 \(\text{span}(S_i)\) 就行了,也就是说把 \(S_i\) 中的基就是 \(\text{span}(S_i)-S_i\) 的所有基了。这样子的时间复杂度是 \(\mathcal{O}(c_i)\) 的
- \(|\text{span}(S_i)-S_i|<\frac{1}{2}|\text{span}(S_i)|\)
那么差的元素并不是很多,我们直接枚举 \(\text{span}(S_i)\) 就行了。时间复杂度还是 \(\mathcal{O}(c_i)\) 的。
所以这一步的时间复杂度最后就是 \(\mathcal{O}(m\sum c_i)\) 的了。
好我们已经初步求出了一个 \(U\)。但是这并不是最后的 \(U\)。为什么呢?因为我们最后要保证 \(U\cup \text{span}(S_i)\) 是异或封闭的,必须要 \(U\subset\text{span}(S_i)\)或者说 \(U\supset\text{span}(S_i)\)。这一点应该是比较好理解的。我们加入过程中扫那些 \(U\subset \text{span}(S_i)\) 的基,看看我们加入基之后还能不能保证这个,如果不能保证就强行让他变成 \(U\supset\text{span}(S_i)\)。这个过程最多进行 \(m\) 次。每次判断的复杂度是 \(\mathcal{O}(nm)\) 的。这一部分的总复杂度是 \(\mathcal{O}(nm^2)\) 的。
最后的总时间复杂度就是 \(\mathcal{O}(m\sum c_i+nm^2)\)。可以通过本题。
#include<bits/stdc++.h>
#define rep(I, J, K) for(int I = (int)J; I <= (int)K; ++I)
#define per(I, J, K) for(int I = (int)J; I >= (int)K; --I)
using namespace std;
const int N = 2e5 + 10, M = 22;
int n, m;
struct Linear_Basis {
int a[M];
int sz = 0;
vector<int>bs;
void insert(int x) {
per(i, m - 1, 0) {
if((x >> i) & 1) {
if(a[i]) {
x ^= a[i];
} else {
a[i] = x; ++ sz;
bs.push_back(x);
return;
}
}
}
}
bool expr(int x) {
per(i, m - 1, 0) {
if((x >> i) & 1) {
if(!a[i]) return 0;
x ^= a[i];
}
}
return 1;
}
};
struct _Set {
int sz = 0;
vector<int>s;
Linear_Basis lb;
unordered_set<int>_S;
void init() {
cin >> sz;
rep(i, 1, sz) {
int p;
cin >> p;
s.push_back(p);
lb.insert(p);
_S.insert(p);
}
}
void insert(int x) {
if(_S.count(x)) return;
lb.insert(x);
s.push_back(x); ++ sz;
_S.insert(x);
}
}S[N], U;
int hav[1 << M];
int main() {
ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
cin >> n >> m;
rep(i, 1, n) S[i].init();
rep(i, 1, n) for(auto &x : S[i].s) hav[x] ++;
rep(i, 0, (1 << m) - 1) if(hav[i] == n) U.insert(i);
// for(auto &x : U.s) cout << x << ' ';
// cout << '\n';
rep(i, 1, n) {
if(((1 << S[i].lb.sz) - S[i].sz) * 2 > (1 << S[i].lb.sz)) {
// 差的更多
for(auto &x : S[i].lb.bs) if(!U.lb.expr(x)) U.insert(x);
} else {
// 直接枚举 span(S_i)
int sz = S[i].lb.sz;
rep(sta, 0, (1 << sz) - 1) {
int now = 0;
rep(j, 0, sz - 1)
if((sta >> j) & 1)
now ^= S[i].lb.bs[j];
if(S[i]._S.count(now)) continue;
U.insert(now);
}
}
}
// for(auto &x : U.s) cout << x << ' ';
// cout << '\n';
int prev_sz = 0;
unordered_set<int>lef;
rep(i, 1, n) lef.insert(i);
vector<int>lst;
for(int v : U.lb.bs) lst.push_back(v);
while(1) {
// 看看现在 lef 中有多少不能满足 U \subset span S_i
unordered_set<int>tmp = lef;
vector<int>now;
for(auto x : tmp) {
bool flag = 1;
for(int v : lst) {
if(!S[x].lb.expr(v)) {
// 表示不了 U 中的某个基
flag = 0;
break;
}
}
if(!flag) {
lef.erase(x);
for(auto v : S[x].lb.bs) {
if(!U.lb.expr(v)) {
// cout << v << '\n';
U.insert(v);
now.push_back(v);
}
}
}
}
lst = now;
// cout << prev_sz << ' ' << U.sz << endl;
if(prev_sz == U.sz) break;
prev_sz = U.sz;
}
vector<int>finans;
int sz = U.lb.sz;
rep(i, 0, (1 << sz) - 1) {
int now = 0;
rep(j, 0, sz - 1)
if((i >> j) & 1) now ^= U.lb.bs[j];
if(hav[now] != n) finans.push_back(now);
}
cout << finans.size() << '\n';
for(auto &x : finans) cout << x << ' ';
cout << '\n';
return 0;
}
F. Folding Game of Ohto Ai
首先我们做这样一个考虑。我们将操作的直径长度一样的东西合起来看。首先,如果一个东西的直径长度是偶数,那么树的中心肯定是边的中心,那么经过一次操作之后直径肯定变短,所以没什么好合起来看的。如果一个东西的直径长度是奇数,我们能够想到,最后总归是把所有深度为 \(\frac{\text{len}}{2}\) 的链中层数相等的点全部合并起来。那么就做完了。我们能够想到,长度不同的直径量级是 \(\sqrt{n}\) 的,那么我们直接每轮暴力 \(\mathcal{O}(n)\) 做就可以了,总时间复杂度为 \(\mathcal{O}(n\sqrt{n})\)。
#include<bits/stdc++.h>
#define rep(I, J, K) for(int I = (int)J; I <= (int)K; ++I)
#define per(I, J, K) for(int I = (int)J; I >= (int)K; --I)
using namespace std;
const int N = 5e4 + 10;
int T;
vector<int>G[N];
int dis[N], res, n;
vector<int>_p;
vector<int>_dep[N];
int e;
bool flag = 0, ok[N];
int fa[N], sz[N];
int findf(int x) { return (fa[x] == x) ? x : (fa[x] = findf(fa[x])); }
void merge(int x, int y) {
int fx = findf(x), fy = findf(y);
if(fx != fy) {
if(sz[fx] > sz[fy]) swap(fx, fy);
fa[fx] = fy;
sz[fy] += sz[fx];
set<int>_to;
for(int v : G[fx]) _to.insert(findf(v));
for(int v : G[fy]) _to.insert(findf(v));
G[fy].clear();
for(int v : _to) if(v != fy) G[fy].push_back(v);
}
}
bool vis[N];
void dfs1(int u, int F) {
vis[u] = 1;
for(int v : G[u]) {
v = findf(v);
if(vis[v]) continue;
dis[v] = dis[u] + 1;
// cerr << '!' << u << ' ' << v << ' ' << dis[v] << endl;
dfs1(v, u);
}
}
void dfs2(int u, int F) {
vis[u] = 1;
_p.push_back(u);
if(u == e) {
flag = 1;
return;
}
for(int v : G[u]) {
v = findf(v);
if(vis[v]) continue;
dfs2(v, u);
if(flag) return;
}
_p.pop_back();
}
int need = 0;
void dfs3(int u, int F) {
vis[u] = 1;
if(dis[u] == need) {
++res;
ok[u] = 1;
return;
}
for(int v : G[u]) {
v = findf(v);
if(vis[v]) continue;
dfs3(v, u);
ok[u] |= ok[v];
}
}
void work() {
cin >> n; res = 0;
rep(i, 1, n) fa[i] = i, sz[i] = 1;
rep(i, 1, n) G[i].clear();
rep(i, 1, n - 1) {
int x, y;
cin >> x >> y;
G[x].push_back(y);
G[y].push_back(x);
}
while(1) {
// 先计算直径
rep(i, 1, n) dis[i] = 0;
rep(i ,1, n) vis[i] = 0;
dfs1(findf(1), -1);
int maxx = 0, maxi = 0;
rep(i, 1, n) if(dis[i] > maxx) maxx = dis[i], maxi = i;
dis[maxi] = 0;
rep(i ,1, n) vis[i] = 0;
dfs1(maxi, -1);
maxx = 0;
int B;
rep(i, 1, n) if(dis[i] > maxx) maxx = dis[i], B = i;
// cerr << maxx << '\n';
if(maxx == 0) break;
e = B, flag = 0;
_p.clear();
rep(i ,1, n) vis[i] = 0;
dfs2(maxi, -1);
// cerr << maxx << ' ' << maxi << ' ' << B << '\n';
if(maxx & 1) {
// 直接对 maxi, B 操作就完了
++res;
for(int i = 0, j = _p.size() - 1; i < j; ++i, --j) merge(_p[i], _p[j]);
} else {
// 不然的话找到中间的节点。
need = maxx / 2;
int mid = _p[need];
// cerr << mid << '\n';
dis[mid] = 0;
rep(i ,1, n) vis[i] = 0;
dfs1(mid, -1);
--res;
rep(i, 1, n) ok[i] = 0;
rep(i ,1, n) vis[i] = 0;
dfs3(mid, -1);
rep(i, 1, need) _dep[i].clear();
rep(i, 1, n) if(ok[i]) _dep[dis[i]].push_back(i);//, cerr << '!' << dis[i] << ' ' << i << '\n';
rep(i, 1, need) {
// 把_dep[i] 中的所有点全部合并
rep(j, 1, _dep[i].size() - 1) merge(_dep[i][j], _dep[i][0]);
}
}
// rep(i, 1, n) cerr << i << ':' << fa[i] << ' ';
// cerr << '\n';
// system("pause");
}
cout << res << '\n';
}
int main() {
ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
cin >> T;
while(T--) work();
return 0;
}
可惜这个代码过不了 CF,qoj 评测机还是太吊了。
合并那里直接不去重了。过了 CF
#include<bits/stdc++.h>
#define rep(I, J, K) for(int I = (int)J; I <= (int)K; ++I)
#define per(I, J, K) for(int I = (int)J; I >= (int)K; --I)
using namespace std;
const int N = 5e4 + 10;
int T;
vector<int>G[N];
int dis[N], res, n;
int tim = 0;
vector<int>_p;
vector<int>_dep[N];
int e;
bool flag = 0, ok[N];
int fa[N], sz[N];
int findf(int x) { return (fa[x] == x) ? x : (fa[x] = findf(fa[x])); }
void merge(int x, int y) {
int fx = findf(x), fy = findf(y);
if(fx != fy) {
if(sz[fx] > sz[fy]) swap(fx, fy);
fa[fx] = fy;
sz[fy] += sz[fx];
for(int v : G[fx]) G[fy].push_back(findf(v));
}
}
int vis[N];
void dfs1(int u) {
vis[u] = tim;
for(int v : G[u]) {
v = findf(v);
if(vis[v] == tim) continue;
dis[v] = dis[u] + 1;
dfs1(v);
}
}
void dfs2(int u) {
vis[u] = tim;
_p.push_back(u);
if(u == e) {
flag = 1;
return;
}
for(int v : G[u]) {
v = findf(v);
if(vis[v] == tim) continue;
dfs2(v);
if(flag) return;
}
_p.pop_back();
}
int need = 0;
void dfs3(int u) {
vis[u] = tim;
if(dis[u] == need) {
++res;
ok[u] = 1;
return;
}
for(int v : G[u]) {
v = findf(v);
if(vis[v] == tim) continue;
dfs3(v);
ok[u] |= ok[v];
}
}
void work() {
cin >> n; res = 0;
rep(i, 1, n) fa[i] = i, sz[i] = 1;
rep(i, 1, n) G[i].clear();
rep(i, 1, n - 1) {
int x, y;
cin >> x >> y;
G[x].push_back(y);
G[y].push_back(x);
}
while(1) {
++tim;
rep(i, 1, n) dis[i] = 0;
dfs1(findf(1));
int maxx = 0, maxi = 0;
rep(i, 1, n) if(dis[i] > maxx) maxx = dis[i], maxi = i;
dis[maxi] = 0;
++tim;
dfs1(maxi);
maxx = 0;
int B;
rep(i, 1, n) if(dis[i] > maxx) maxx = dis[i], B = i;
if(maxx == 0) break;
e = B, flag = 0;
_p.clear();
++tim;
dfs2(maxi);
if(maxx & 1) {
++res;
for(int i = 0, j = _p.size() - 1; i < j; ++i, --j) merge(_p[i], _p[j]);
} else {
need = maxx / 2;
int mid = _p[need];
dis[mid] = 0;
++tim;
dfs1(mid);
--res;
rep(i, 1, n) ok[i] = 0;
rep(i ,1, n) vis[i] = 0;
++tim;
dfs3(mid);
rep(i, 1, need) _dep[i].clear();
rep(i, 1, n) if(ok[i]) _dep[dis[i]].push_back(i);
rep(i, 1, need)
rep(j, 1, _dep[i].size() - 1) merge(_dep[i][j], _dep[i][0]);
}
}
cout << res << '\n';
}
int main() {
ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
cin >> T;
while(T--) work();
return 0;
}
H. Hidden Track (Easy Version)
为啥这题要叫 Easy Version /yiw
题面非常唬人,其实考察的是翻译题面的能力。
首先大体读一遍,是要你求出隐藏的一个 \(0,1,...,n-1\) 的排列,并且保证 \(p_1<p_n\)。然后你的每次询问可以给出一个 \(m\) 和一个 \(v\)。什么作用先暂且不谈。反正给的 \(m\) 构造出了一个集合 \(S\),然后把 \(S\) “异或”上 \(v\)。这里异或的意思是集合一个类似于异或的操作:一个集合异或这个数,就是看这个集合里有没有这个数,如果有就把他去掉,如果没有就把他加上。然后返回给你的值就是说,排列中两个相邻的元素有且仅有一个在 \(S\) 中的相邻的二元组数目,并且还要 \(\mod 3\)。那么我们逐层翻译:
第一个集合有什么特殊性?翻译第一个集合:只保留 \(m\) 的二进制位后有奇数个 \(1\) 的数。那代到 \(c(S)\) 中,就是说排列中的两个相邻元素中有且仅有一个只保留 \(m\) 的二进制位后有奇数个 \(1\)。那是不是就是只保留 \(m\) 的二进制位后二进制位下的 \(1\) 数目奇偶性不相等?我们再加入 \(v\)。首先如果 \(v\) 不等于任何 \(p_i\),显然不会影响结果。但是如果 \(v=p_i\) 呢?因为 \(v=p_i\) 和 \(v=p_{i+1}\) 的效果是一样的,所以我们只需要考虑其中一个。那么就是在 \(S\) 异或 \(v\) 的情况下考虑。那么考虑一下影响的情况。就加入 \(v=p_i\),那么 \(p_{i-1}\) 如果原本和他奇偶性不相等记作 \(p\),\(p_{i+1}\) 原本和他奇偶性不相等记作 \(q\)。那么就是说。
- \(p\and q\) 这样弄贡献为 \(-2=1\pmod3\)
- \(p\or q\) 这样弄,贡献为 \(0\)
- \(\neg(p\and q)\) 这样弄贡献为 \(2\)
有这样的结论感觉非常好啊,因为刚好用到了题目里对 \(3\) 取模的性质。
我们自然而然的能想到,令 \(m\) 等于 \(2^i,i\in[0,k)\)。这里的 \(k\) 就是题目中的 \(k=\left\lceil\log_2n\right\rceil\)。那么我们不就弄清楚了每个数字左右两边的每一位有几个是 \(1\) 了吗。那么我们直接枚举开头的数,根据开头的数我们就能还原整个序列,最后验证一下答案合不合法就可以。
然后我们回头注意一些细节:首先如果没有 \(p_{i-1}\) 或者没有 \(p_{i+1}\) 怎么办?那么如果贡献是 \(1\) 就是原本相等现在不相等了。如果贡献是 \(-1\) 就是原本不相等现在相等了。
然后我们还有一个巨大的问题就是:现在的提问次数是 \((n+1)k\) 略超出了题目要求的 \(nk\) 我们应该如何解决呢?
那么其实对我们的要求就是漏问一个点。
第一种做法,直接随机漏问一个点,我们还是枚举起点,然后记录下来每条链(如果走到漏问的点那么就停下来)。然后如果两个链能拼起来变成一个没有错误的链,那么就是对的。时间复杂度为 \(\mathcal{O}(n^2k)\),询问次数为 \(nk\)。
第二种做法,我们观察上面的贡献表,我们能发现,边上的点是没有贡献为 \(0\) 的点的,如果说中间的点一定有贡献为 \(0\) 的点,那么我们不就找到了一个合法起点了吗?而这一点是正确的。因为两边的数不可能完全相等,所以一定存在两边的数某一位不相等,而这一位的贡献一定是 \(0\)。那么我们在 \(0,1,2,..,n-1\) 依次寻找,肯定能找到一个合法的端点,然后扩展就行了。这种方法把最后一个端点舍弃了,最后询问次数为 \(nk\)。时间复杂度为 \(\mathcal{O}(nk)\)。
#include<bits/stdc++.h>
#define rep(I, J, K) for(int I = (int)J; I <= (int)K; ++I)
#define per(I, J, K) for(int I = (int)J; I >= (int)K; --I)
using namespace std;
const int N = 1e3 + 10, MAXK = 15;
int n, a[N], b[MAXK], B[N][MAXK], vis[N];
int Query(int m, int v) {
cout << "? " << m << ' ' << v << endl;
int c;
cin >> c;
return c;
}
void Print() {
cout << "! ";
rep(i, 1, n) cout << a[i] << ' ';
cout << endl;
}
void query(int i, int K, bool &flag) {
vis[i] = 1;
rep(j, 0, K - 1) {
int now = Query(1 << j, i);
int u = (now - b[j] + 3) % 3;
if(u == 0) flag = 0;
B[i][j] = u;
}
return;
}
void work() {
cin >> n;
if(n == 1) {
cout << "! 0" << endl;
return;
}
rep(i, 0, n) vis[i] = a[i] = 0;
int K = ceil(1.0 * log2(n)), st = -1;
rep(i, 0, K - 1) b[i] = Query(1 << i, -1);
rep(i, 0, n - 1) {
bool flag = 1;
query(i, K, flag);
if(flag) {
st = i;
break;
}
}
// 找到起点 st
a[1] = st;
// 手推 a2
rep(i, 0, K - 1) a[2] |= (1 << i) * (((st >> i) & 1) ^ (B[st][i] == 2));
rep(i, 2, n - 1) {
bool flag = 0;
if(!vis[a[i]]) query(a[i], K, flag);
rep(j, 0, K - 1) {
int u = (B[a[i]][j] == 1) ? (((a[i] >> j) & 1) == 1 ? 0 : 2) : (B[a[i]][j] == 0 ? 1 : (((a[i] >> j) & 1) == 1 ? 2 : 0));
u -= (a[i - 1] >> j) & 1;
assert(u != 2);
a[i + 1] |= u * (1 << j);
}
}
Print();
return;
}
int main() {
ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
int T;
cin >> T;
while(T--) work();
return 0;
}
I. Island
补题。赛时就没开这题。
首先的一个 trick 我就没有想到。我们可以把删边的过程反过来,看成每条边有 \(\frac{1}{2}\) 的几率加入到图中,然后去进行一个计算。
这个 trick 的好处就在于,我们这样就把问题转化为了求 \(n-\sum\limits_{i=1}^{n+k-2} p_i\),而 \(p_i\) 就是第 \(i\) 条边将两个不同的连通分量链接起来的概率。我们可以任意指定一个加边顺序,我们不难想到先让 \(n-1\) 条树边加入,而每一条树边的加入都一定导致了两个不同的连通分量合并,所以说前面 \(p_i=\frac{1}{2}, 1\leq i\leq n-1\)。那么我们再依次考虑一下叶子之间的边。我们肯定希望这个东西能够递推下去,类似于一个树形 dp 的形状?
似乎能够想到?对于一个节点,我们只维护他的最左边的叶子和最右边的叶子。这样是不是就能从下往上做了?
但是我们应该维护具体什么东西呢?我们就考虑假如说现在 \(u\) 有两个子节点 \(u_1,u_2\),他们最左边的叶子分别是 \(L_{u_1}, L_{u_2}\),最右边的叶子分别是 \(R_{u_1}, R_{u_2}\)。我们就是要考虑这个时候加入了 \((L_{u_2}, R_{u_1})\) 这条边将两个不同连通分量连接起来的概率。
我们考虑一下他们本来已经是一个连通分量的概率,那么就一定要从 \(R_{u_1}\) 能走到 \(u\) 这个点,并且一定能从 \(L_{u_2}\) 走到 \(u\) 这个点。那么我们已经大概清楚了,我们就需要记录最左最右的叶子能走到我这个点的概率。
这个东西感觉上应该是比较好维护的,我们定义 \(dp_{i,1}, dp_{i,2}\) 分别表示左叶子走到我的概率,右叶子能走到我的概率。
比如说 \(u\) 有 \(k\) 个儿子,分别是 \(v_1,v_2, v_3,...,v_k\)。考虑将每个儿子并起来的过程。首先 \(dp_{u,1}=\frac{1}{2}dp_{v_1,1}, dp_{i,2}=\frac{1}{2}dp_{v_1,2}\)。这就是说 \((v_1,u)\) 这条边有 \(\frac{1}{2}\) 的几率断掉。这就是 \(v_1\) 的过程。如果 \(v_2\) 这个时候过来了?首先更新一下对答案的贡献,那么就是 \(\frac{1}{2}-\frac{1}{4} dp_{v_1,2}\times dp_{v_2,1}\),
在这个过程中我们发现我们还应该维护一个左儿子能走到右儿子的概率,假如说这是 \(dp_{u,3}\) 吧。我们看看能不能完成这个东西的转移了。还是 \(v_2\) 过来了,对于答案的贡献并没有变化。但是左儿子能走到根节点的概率变了,因为我有可能左儿子穿过了 \(v_1\) 的一些路径走到了 \(v_2\)(这是有可能的!)那么 \(dp_{u,1}=dp_{u,1} + (1-dp_{u,1})\times dp_{u,3}\times\frac{1}{2}\times dp_{v_2,1}\)。感觉这个递推式子完全没有问题。那么右边的也应该类似吧?考虑一下 \(dp_{u,2}=\frac{1}{2}dp_{v_2,2} + (1-\frac{1}{2}dp_{v_2,2})\times dp_{v_2,3}\times \frac{1}{2}\times dp_{u,2}\)。好这两个递推式子应该是没有问题了?那么我们想一下 \(dp_{u,3}\) 应该发生什么变化?
\(dp_{u,3}=dp_{u,3}\times\frac{1}{2}\times dp_{v_2, 3}+dp_{u,1}\times dp_{v_2,3}-?\)
前半部分是不经过 \(u\) 的答案,后半部分是经过 \(u\) 的答案。但是我们需要考虑一下容斥,也就是两个都经过的答案我们应该减掉。
考虑一下 \(?\) 的东西我们应该怎么求。
似乎比较困难了,我们看看能不能通过再维护一些东西来算出来 \(?\)。这里遇到了一个难点,因为到 \(u\) 和经过新加入的这条边很难抽象成一个独立的过程,所以我们能不能考虑容斥一下, 我们考虑去计算左儿子和右儿子在一个连通分量,但是这个连通分量里不包括 \(u\) 的概率?假如说这个东西记作 \(dp_{u,4}\) 吧,那么我们能通过 \(dp_{u,3}-dp_{u,4}\) 计算出 \(?\) 所代表的结果。
好,那么问题转化为了加入 \(v_2\) 这个点之后,\(dp_{u,4}\) 应该如何计算?
\(dp_{u,4}=dp_{u,4}\times\frac{1}{2}\times dp_{v_2,3}\)。
这个竟然出乎意料的好算!
那么问题应该是已经结束了。
太结束了。

我们大概上能够猜到问题在哪。前面的式子我好像根本没有考虑独立!
但是 AI 肯定了我们大框架正确,我们重新思考一下这些东西怎么维护。
首先重头考虑,还是对于答案的贡献,我们现在合并的这条边,首先他有 \(\frac{1}{2}\) 的几率存在,如果存在的话,有 \(1-\frac{1}{2}dp_{u,2}\times dp_{v_2,1}\) 的概率让答案减一。
考虑完了答案,我们逐个考虑 \(dp\)
- \(dp_{u,1}\) 刚才没有考虑到如果 \(u\) 已经不和左儿子链接了,这个东西和左儿子能走到右儿子的事件并非独立啊。但是 \(dp_{u,1}=dp_{u,1}+dp_{u,4}\times\frac{1}{4}\times dp_{v_2,1}\)
- \(dp_{u,2}=\frac{1}{2}dp_{v_2,2}+dp_{v_2,4}\times\frac{1}{2}\times dp_{u,2}+(dp_{v_2,3}- dp_{v_2,4})\times\frac{1}{4}\times dp_{u,2}\)。
- \(dp_{u,3}=dp_{u,3}\times\frac{1}{2}\times dp_{v_2,3}+dp_{u,1}\times\frac{1}{2}\times dp_{v_2,2}-(dp_{u,3}-dp_{u,4})\times\frac{1}{4}\times(dp_{v_2,3}- dp_{v_2,4})\)
- \(dp_{u,4}=dp_{u,4}\times\frac{1}{4}\times (dp_{v_2,3}+dp_{v_2,4})\) 。
经历了反复修改,终于是对了。/kx
#include<bits/stdc++.h>
#define rep(I, J, K) for(int I = (int)J; I <= (int)K; ++I)
#define per(I, J, K) for(int I = (int)J; I >= (int)K; --I)
using namespace std;
const int N = 5e5 + 10;
const int P = 998244353;
typedef long long ll;
ll qpow(ll x, int y) {
ll res = 1;
while(y) {
if(y & 1) res = res * x % P;
x = x * x % P;
y >>= 1;
}
return res;
}
const ll inv2 = qpow(2, P - 2), inv4 = qpow(4, P - 2);
int n;
vector<int>G[N];
ll dp[N][5];
ll ans = 0;
void dfs(int u) {
if(G[u].empty()) {
dp[u][0] = dp[u][1] = dp[u][2] = 1;
dp[u][3] = 0;
return;
}
rep(i, 0, G[u].size() - 1) {
int v = G[u][i];
dfs(v);
if(i == 0) {
dp[u][0] = inv2 * dp[v][0] % P;
dp[u][1] = inv2 * dp[v][1] % P;
dp[u][2] = dp[v][2];
dp[u][3] = (dp[v][3] + (dp[v][2] - dp[v][3]) * inv2) % P;
} else {
ll d0 = dp[u][0], d1 = dp[u][1], d2 = dp[u][2], d3 = dp[u][3];
ans = (ans - inv2 * (1 - inv2 * dp[u][1] % P * dp[v][0] % P) % P + P) % P;
dp[u][0] = (d0 + d3 * inv4 % P * dp[v][0] % P) % P;
dp[u][1] = (inv2 * dp[v][1] + dp[v][3] * inv2 % P * d1 % P + (dp[v][2] - dp[v][3] + P) % P * inv4 % P * d1 % P) % P;
dp[u][2] = (d2 * inv2 % P * dp[v][2] % P + d0 * inv2 % P * dp[v][1] % P - (d2 - d3 + P) * inv4 % P * (dp[v][2] - dp[v][3] + P) % P + P) % P;
dp[u][3] = d3 * inv4 % P * (dp[v][2] + dp[v][3]) % P;
}
}
}
void work() {
cin >> n;
rep(i, 1, n) G[i].clear();
rep(i, 1, n) {
int t;
cin >> t;
while(t--) {
int x;
cin >> x;
G[i].push_back(x);
}
}
ans = n - 1ll * (n - 1) * inv2; ans = (ans % P + P) % P;
dfs(1);
cout << ans << '\n';
}
int main() {
ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
int T;
cin >> T;
while(T--) work();
return 0;
}

浙公网安备 33010602011771号