洛谷 P6478 [NOI Online #2 提高组] 游戏 题解
前言:
这道题涉及到了很多有意思的部分,所以我会较为详细的写一篇题解。
题意:
给定一棵点数为 \(n=2m\) 的有根树,每个点有 \(0,1\) 两种边权。
现在要依次为每一个权为 \(0\) 的点找一个权为 \(1\) 的点与之配对,并对每个 \(k∈[0,m]\),求出恰有 \(k\) 对点的关系是祖先和后代的配对方案数。(称一个节点是另一个节点的后代当且仅当这个节点在另一个节点的子树内。)需要注意的是我们称两个方案不同,当且仅当某个点权为\(0\)的点所配对的点权为\(1\)的点在两个方案中不同。也就是说,点对与点对之间的配对顺序不同,并不代表它们就是不同的方案。
思路
看到恰有,我们考虑往容斥或二项式反演思考。
我们记 \(f(k)\) 表示至少有 \(k\) 对这样的祖先后代关系的配对方案数,而 \(g(k)\) 表示恰有 \(k\) 对这样的祖先后代关系的配对方案数。所以显然的:
容易发现这是二项式反演的常用形式,所以我们有:
那么我们就可以把现在需要求的问题转化为至少有 \(k\) 对点满足上述配对关系的方案数,也就是 \(f(k)\),因为题目所给是一棵树,我们考虑进行树形dp。定义 \(dp_{u,i}\) 表示 \(u\) 子树内至少存在 \(i\) 对这样的节点方案数。若我们先不考虑点 \(u\) ,且定义 \(u\) 有 \(t\) 个子节点,那么朴素的转移就为:
显然这样枚举的复杂度我们是无法接受的。看到每一部分加起来为一个定值,我们考虑用树上背包处理这个问题。
所以有一个新的转移就是:
因为是树上背包,每一次我们枚举完 \(v\) 都需要将其合并到之前已经合并过的所有子树的大集合里。
处理好所有子节点后,考虑点 \(u\) 本身,我们还可以在 \(u\) 子树(不含 \(u\))中找一个与 \(u\) 点权不同的点与 \(u\) 配对。而这种情况只会多加一对合法对。所以我们令 \(u\) 子树内点权为 \(c,(c=0\ or\ 1)\) 的点的数量为 \(size_{u,c}\),所以这一部分的贡献就表达为:
因为每一个 \(u\) 只能匹配一次,所以我们需要倒叙枚举 \(i\) ,让当前dp值从未被更新的 \(i-1\) 的阶段转移过来,否则就会出现在上一个阶段我已经单独将 \(u\) 拎出来匹配,我又匹配了一次的状况。
额,看起来第一个转移方程是 \(O(n^3)\) 无法通过 \(n\leq 5000\) 的数据,但是事实上,当我们仔细分析就会发现这是 \(O(n^2)\) 的。首先要表明一个事实,当我们在转移时,每一次枚举子树 \(v\) 时,次数是一定不会超过 \(v\) 子树本身的大小的,同理,前面也不会超过已经合并的子树的大小。所以我们来严谨分析复杂度,定义 \(son_{u,i}\)是 \(u\) 点的第 \(i\) 个子节点,\(s(t)\) 为 \(t\) 子树的大小:
考虑理解这个式子:我们每次遍历到节点 \(u\) ,然后枚举其所有子节点 \(v\) ,当我们枚举到第 \(i\) 个儿子的时候,前面的子节点(\([1,i-1]\))以及它们的子树所构成的点集构成了一个集合 \(V\)。而这一次的枚举会对时间复杂度造成 \(|V|\times s(v)\) 的贡献,枚举完子节点 \(v\) 后将 \(v\) 及其子树加入到集合 \(V\) 中。
那么具体每一次的贡献到底是多少?我们尝试拆成一次一次的最小贡献来计算,显然因为每一次枚举的时候,任意点 \(p\in V\) 和任意点 \(q\in Tree(v)\) 组成的点对都会对 \(|V|\times s(v)\) 造成值为 \(1\) 的贡献。而因为 \(p,q\) 在 \(u\) 的两个不同子节点的子树中,所以 \(LCA(p,q)=u\) ,即在枚举到子节点 \(v\) 之前一定没有产生过点对 \((p,q)\),同时又因为我们最后会将 \(Tree(v)\) 合并到 \(V\) 中,所以这也是点对 \((p,q)\) 的最后一次贡献,因为任何同属于点集 \(V\) 的点不会造成任何贡献。所以我们证明了 \(\forall_{u,v\in[1,n]}\),点对 \((u,v)\) 只会对时间复杂度造成值为 \(1\) 的贡献。
所以综上时间复杂度是由点对总量的规模决定的,即:\(O(n^2)\)。
但是这样就完了吗?这样交上去是会wa掉的,需要注意的是,我们定义的是至少有这样的 \(i\) 对,其他的怎么样我们是不用管的,所以最后还需要:
什么?你说不明白为什么要乘上后面那一坨。
那么我们换一种视角看这道题:首先注意到每一对的顺序调换后仍看做同一种方案,所以我们不妨将选\(0\)的顺序与选\(1\)的顺序看做两组排列 \(P,Q\),令选\(0\)的排列 \(P\) 是固定的顺序:\(P_1,P_2,P_3...P_m\) ,所以题目等价于有多少 \(Q\) 的排列 \(r\) 满足恰好有 \(k\) 对 \((P_i,r_i)\) 是祖先-后代关系(谁是祖先谁是后代并不重要,因为题目所给数据已经规定了这一点)。为什么可以固定排列 \(P\) 的顺序?因为你发现若全部排列 \(P\) 和 \(Q\) 的方案数是 \(m!\cdot m!\)。 而此时我们若先固定 \(P\) 不动,先把 \(Q\) 排列完方案数是 \(m!\) ,此时你再去排列 \(m\) 组点对又有 \(m!\) 中方案,总方案 \(m!\cdot m!\) ,你发现两种排列方案形成一一映射。此时注意到,第二种方法中的第二步由题意规定是无意义的,我们只需要排列 \(Q\) 就可以算出这道题的所有不同的方案,所以我们可以固定 \(P\) 的顺序。
所以当我们求出 \(dp_{1,k}\) 的时候就证明已经对于每一个不同方案找出了至少 \(k\) 对祖先-后代关系点对,那么我们只需要排列其余的 \(Q\) 中的元素,数量即为 \((m-k)!\)。
最终答案即为:
在给出代码之前说一些实现小细节,因为点对最多就只有 \(m\) 对,所以枚举的上界可以设为 \(\min\{s(u),m\}\)。此外 \(u\) 子树的大小不要先预处理出来,而是要边dp边更新,原因上文在转移时也给出了。
Code:
#include<iostream>
#include<cstring>
#include<algorithm>
#include<cstdio>
#define int long long
template<class t> inline t read(t &x){
char c=getchar();bool f=0;x=0;
while(!isdigit(c)) f|=c=='-',c=getchar();
while(isdigit(c)) x=(x<<1)+(x<<3)+(c^48),c=getchar();
if(f) x=-x;return x;
}
const int N = 5000 + 10;
const int MOD = 998244353;
struct edge{
int v,last;
}e[2 * N];
int n,cnt,head[N],f[N][N];
char a[N];
void add(int u,int v){
e[++cnt].v = v;
e[cnt].last = head[u];
head[u] = cnt;
}
int qpow(int a,int b){
int res = 1;
while(b){
if(b & 1) res = (res * a) % MOD;
a = (a * a) % MOD;
b >>= 1;
}
return res;
}
int fac[N],inv[N];
void init(){
fac[0] = 1;
for(int i = 1;i <= N;++i) fac[i] = fac[i - 1] * i % MOD;
inv[N] = qpow(fac[N],MOD - 2);
for(int i = N - 1;i >= 0;--i){
inv[i] = inv[i + 1] * (i + 1) % MOD;
}
}
int C(int a,int b){
return fac[a] * inv[b] % MOD * inv[a - b] % MOD;
}
int siz[N],color[N],g[N];
void dfs(int u,int fa){
f[u][0] = 1;
color[u] = (a[u] - '0');
for(int i = head[u]; i;i = e[i].last){
int v = e[i].v;
if(v == fa) continue;
dfs(v,u);
for(int i = 0;i <= siz[u] + siz[v];++i) g[i] = 0;
for(int i = 0;i <= std::min(siz[u],n / 2);++i)
for(int j = 0;j <= std::min(siz[v],n / 2 - i);++j)
g[i + j] = (g[i + j] + f[u][i] * f[v][j]) % MOD;
for(int i = 0;i <= siz[u] + siz[v];++i) f[u][i] = g[i];
siz[u] += siz[v],color[u] += color[v];
}
siz[u] += 1;
for(int i = std::min(color[u],siz[u] - color[u]); i;--i){
if(a[u] == '1') f[u][i] = (f[u][i] + f[u][i - 1] * (siz[u] - color[u] - (i - 1)) % MOD) % MOD;
else f[u][i] = (f[u][i] + f[u][i - 1] * (color[u] - (i - 1)) % MOD) % MOD;
}
}
signed main(){
init();
read(n);
for(int i = 1;i <= n;++i) std::cin >> a[i];
for(int i = 1;i <= n - 1;++i){
int u,v;
read(u);read(v);
add(u,v);
add(v,u);
}
dfs(1,0);
for(int i = 0;i <= n / 2;++i) f[1][i] = f[1][i] * fac[n / 2 - i] % MOD;
for(int i = 0;i <= n / 2;++i){
int ans = 0;
for(int j = i,op = 1;j <= n / 2;++j,op = -op)
ans = (ans + op * C(j,i) * f[1][j] % MOD + MOD) % MOD;
std::cout << ans << '\n';
}
return 0;
}

浙公网安备 33010602011771号