前言:

这道题涉及到了很多有意思的部分,所以我会较为详细的写一篇题解。

题意:

给定一棵点数为 \(n=2m\) 的有根树,每个点有 \(0,1\) 两种边权。

现在要依次为每一个权为 \(0\) 的点找一个权为 \(1\) 的点与之配对,并对每个 \(k∈[0,m]\),求出恰有 \(k\) 对点的关系是祖先和后代的配对方案数。(称一个节点是另一个节点的后代当且仅当这个节点在另一个节点的子树内。)需要注意的是我们称两个方案不同,当且仅当某个点权为\(0\)的点所配对的点权为\(1\)的点在两个方案中不同。也就是说,点对与点对之间的配对顺序不同,并不代表它们就是不同的方案。

思路

看到恰有,我们考虑往容斥或二项式反演思考。

我们记 \(f(k)\) 表示至少有 \(k\) 对这样的祖先后代关系的配对方案数,而 \(g(k)\) 表示恰有 \(k\) 对这样的祖先后代关系的配对方案数。所以显然的:

\[f(k)=\sum_{i=k}^m\dbinom{i}{k}g(i)
\]

容易发现这是二项式反演的常用形式,所以我们有:

\[g(k)=\sum_{i=k}^m(-1)^{i-k}\dbinom{i}{k}f(k)
\]

那么我们就可以把现在需要求的问题转化为至少有 \(k\) 对点满足上述配对关系的方案数,也就是 \(f(k)\),因为题目所给是一棵树,我们考虑进行树形dp。定义 \(dp_{u,i}\) 表示 \(u\) 子树内至少存在 \(i\) 对这样的节点方案数。若我们先不考虑点 \(u\) ,且定义 \(u\) 有 \(t\) 个子节点,那么朴素的转移就为:

\[dp_{u,i}\leftarrow\sum_{x_1+x_2+...x_t=i}\prod_{i=1}^tdp_{v_i,x_i}
\]

显然这样枚举的复杂度我们是无法接受的。看到每一部分加起来为一个定值,我们考虑用树上背包处理这个问题。

所以有一个新的转移就是:

\[dp_{u,i}\leftarrow\sum_v\sum_{0\leq j\leq i}dp_{u,j}\times dp_{v,i-j}
\]

因为是树上背包,每一次我们枚举完 \(v\) 都需要将其合并到之前已经合并过的所有子树的大集合里。

处理好所有子节点后,考虑点 \(u\) 本身,我们还可以在 \(u\) 子树(不含 \(u\))中找一个与 \(u\) 点权不同的点与 \(u\) 配对。而这种情况只会多加一对合法对。所以我们令 \(u\) 子树内点权为 \(c,(c=0\ or\ 1)\) 的点的数量为 \(size_{u,c}\),所以这一部分的贡献就表达为:

\[dp_{u,i}\leftarrow dp_{u,i-1}\times \max(0,size_{u,1-c}-(i-1))
\]

因为每一个 \(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\) 子树的大小:

\[T(n)=\sum_u\sum_{v=son_{u,i}}\left(\sum_{j=1}^{i-1}\left(s(son_{u,j})\right)\times s(v)\right)
\]

考虑理解这个式子:我们每次遍历到节点 \(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\) 对,其他的怎么样我们是不用管的,所以最后还需要:

\[dp_{1,k}\leftarrow dp_{1,k}\times (m-k)!
\]

什么?你说不明白为什么要乘上后面那一坨。

那么我们换一种视角看这道题:首先注意到每一对的顺序调换后仍看做同一种方案,所以我们不妨将选\(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)!\)。

最终答案即为:

\[g(k) = \sum_{i=k}^m(-1)^{i-k}\dbinom{i}{k}dp_{1,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;
}

洛谷 P6478 [NOI Online #2 提高组] 游戏 题解的更多相关文章

  1. 洛谷 P6478 - [NOI Online #2 提高组] 游戏(二项式反演+树形 dp)

    题面传送门 没错这就是我 boom0 的那场 NOIOL 的 T3 一年前,我在 NOIOL #2 的赛场上折戟沉沙,一年后,我从倒下的地方爬起. 我成功了,我不再是从前那个我了 我们首先假设 A 拥 ...

  2. 洛谷 P6570 - [NOI Online #3 提高组] 优秀子序列(集合幂级数+多项式)

    洛谷题面传送门 首先 \(3^n\) 的做法就不多说了,相信对于会状压 dp+会枚举子集的同学来说不算困难(暴论),因此这篇博客将着重讲解 \(2^nn^2\) 的做法. 首先如果我们把每个 \(a_ ...

  3. 洛谷P1003 铺地毯 noip2011提高组day1T1

    洛谷P1003 铺地毯 noip2011提高组day1T1 洛谷原题 题目描述 为了准备一个独特的颁奖典礼,组织者在会场的一片矩形区域(可看做是平面直角坐标系的第一象限)铺上一些矩形地毯.一共有 n ...

  4. 洛谷-神奇的幻方-NOIP2015提高组复赛

    题目描述 幻方是一种很神奇的N*N矩阵:它由数字1,2,3,--,N*N构成,且每行.每列及两条对角线上的数字之和都相同. 当N为奇数时,我们可以通过以下方法构建一个幻方: 首先将1写在第一行的中间. ...

  5. 洛谷 P1541 乌龟棋 & [NOIP2010提高组](dp)

    传送门 解题思路 一道裸的dp. 用dp[i][j][k][kk]表示用i个1步,j个2步,k个3步,kk个4步所获得的最大价值,然后状态转移方程就要分情况讨论了(详见代码) 然后就是一开始统计一下几 ...

  6. 洛谷 P1525 关押罪犯 & [NOIP2010提高组](贪心,种类并查集)

    传送门 解题思路 很显然,为了让最大值最小,肯定就是从大到小枚举,让他们分在两个监狱中,第一个不符合的就是答案. 怎样判断是否在一个监狱中呢? 很显然,就是用种类并查集. 种类并查集的讲解——团伙(很 ...

  7. 洛谷 P5019 铺设道路 & [NOIP2018提高组](贪心)

    题目链接 https://www.luogu.org/problem/P5019 解题思路 一道典型的贪心题. 假设从左往右填坑,如果第i个深与第i+1个,那么第i+1个就不需要额外填: 如果第i+1 ...

  8. 洛谷P1063 能量项链 [2006NOIP提高组]

    P1063 能量项链 题目描述 在Mars星球上,每个Mars人都随身佩带着一串能量项链.在项链上有N颗能量珠.能量珠是一颗有头标记与尾标 记的珠子,这些标记对应着某个正整数.并且,对于相邻的两颗珠子 ...

  9. 「洛谷P1080」「NOIP2012提高组」国王游戏 解题报告

    P1080 国王游戏 题目描述 恰逢 \(H\)国国庆,国王邀请\(n\)位大臣来玩一个有奖游戏.首先,他让每个大臣在左.右手上面分别写下一个整数,国王自己也在左.右手上各写一个整数.然后,让这 \( ...

  10. BZOJ5285 & 洛谷4424 & UOJ384:[HNOI/AHOI2018]寻宝游戏——题解

    https://www.lydsy.com/JudgeOnline/problem.php?id=5285 https://www.luogu.org/problemnew/show/P4424 ht ...

随机推荐

  1. CentOS、Ubuntu安装jdk11方法

    CentOS: sudo yum install java-11-openjdk -y Ubuntu sudo apt-get install openjdk-11-jre -y 检查版本: java ...

  2. 用户空间的系统调用是如何链接到内核空间的系统调用的——MIT6.S081学习记录

    用户态的sysinfo(),首先系统会从user/user.h里找到声明,随后由链接到 usys.S 中的汇编代码来实现的.usys.S是通过usys.pl生成的.usys.S 文件定义了所有系统调用 ...

  3. WPF 基于Transform实现画布超出边界触发计算

    有些场景需要对画布边界做界限控制,此时需要计算画布的四个方向的界限和极值 先看效果图: 画布在通过RenderTransform 做变换,由于在变换的过程中,实际的宽高没有改变,需要通过Transfo ...

  4. 面试题-Thread.sleep(0)的作用是什么

      就是线程等待的意思.由于Java采用抢占式的线程调度算法,因此可能会出现某条线程常常获取到CPU控制权的情况,为了让某些优先级比较低的线程也能获取到CPU控制权,可以使用Thread.sleep( ...

  5. IntelliJ IDEA 2023.1 破解教程mac,windows,linux均适用/JetBrains产品全版本激活

    前言 该激活方式不限于IDEA,同样也适用于JetBrains 全家桶的所有工具, 包括 IntelliJ IDEA.Pycharm.WebStorm.PhpStorm.AppCode.Datagri ...

  6. 【转载】Refletor源码分析

    Refletor源码分析 Informer 通过对 APIServer 的资源对象执行 List 和 Watch 操作,把获取到的数据存储在本地的缓存中,其中实现这个的核心功能就是 Reflector ...

  7. 如何最大化客户生命周期价值?APMDR 模型在袋鼠云的落地实践

    相信大家都认可一个观点:不论是 To B 还是 To C,用户是企业的核心资源,是互联网产品中最重要的价值之一.因此,深入挖掘用户价值成为现在大部分企业运营的关键. 之前我们为大家介绍过如何利用 RF ...

  8. ArcObject SDK 015 出图

    1.核心出图代码 出图主要是靠IExport接口,继承该接口的类如下图所示. 出不同格式的图,实例化不同的类即可.例如导出jpg格式的图片的代码如下. private void Export(stri ...

  9. MyBatis 动态 SQL 与缓存机制深度解析

    在Java持久层技术体系中,MyBatis凭借其灵活的SQL映射和强大的动态SQL能力,成为企业级应用开发的首选框架.本文从动态SQL核心语法.缓存实现原理.性能优化及面试高频问题四个维度,结合源码与 ...

  10. donNet 文件上传下载进度计算(一段代码体现数学在编码中的重要位置)

    上传进度: var 每次成功增加的进度 = Convert.ToDouble(文件已上传大小) / Convert.ToDouble(文件总大小); var 当前进度 = (每次成功增加的进度 *10 ...