[ZJOI2022] 树 题解
[ZJOI2022] 树 题解
知识点
计数动态规划,容斥。
题意简述
要求生成两棵树,满足一下三个条件:
- 在第一棵树中:\(1\) 为根,对于 \(i\in [2,m]\),从 \([1,i-1]\) 中选一个点作为它的父节点。
- 在第二棵树中:\(m\) 为根,对于 \(i\in [1,m-1]\),从 \([i+1,m]\) 中选一个点作为它的父节点。
- 对于 \(i \in [1,m]\),点 \(i\) 不能在两棵树中同时为叶子结点。
问 \(m \in [1,n]\) 时的方案数。
分析
\(O(n!n)\)
爆搜即可。
\(O(n^22^n)\)
暴力状压即可。
namespace Sub2 {
constexpr int N(20+10),St((1<<20)+10);
int cnt[St];
int f[N][St];
bool Check() { return n<=20; }
int Cmain() {
FOR(i,1,St-5)cnt[i]=cnt[i>>1]+(i&1);
f[1][1]=1,f[2][2]=1;
FOR(i,3,n) {
const int U((1<<(i-1))-1);
FOR(S,1,U) {
toadd(f[i][S|1<<(i-1)],mul(f[i-1][S],i-1-cnt[S]));
FOR(j,1,i-1)if(S&1<<(j-1))toadd(f[i][(S^(1<<(j-1)))|1<<(i-1)],f[i-1][S]);
}
}
static int res[N+10];
FOR(i,2,n) {
const int U((1<<i)-1);
int ans(0);
FOR(S,0,U)if(f[i][S])toadd(ans,mul(f[i][S],f[i][S]));
cout<<ans<<endl;
res[i]=add(ans,Mod-res[i-1]);
}
return 0;
}
}
\(O(n^5)\)
考虑从一个点慢慢加入时,合并第一棵树和第二棵树的状态来计数。
第一棵树始终都是一棵树,但是第二棵树在完全成型前就是一堆连通块,那么我们可以通过这个性质来设计状态:\(f_{i,j,k,q}\) 表示加入第 \(i\) 个节点时,第一棵树中有 \(j\) 个有子节点的非叶节点,有 \(k\) 个无子节点的非叶节点,而第二棵树有 \(q\) 个连通块的数量。
边界:\(f_{1,0,1,1} = 1\)。
转移方程:
-
加入叶子结点(第一棵树中,下同):(\(\to\) 代表加到,下同)
\[\forall p \in [1,q],j {q\choose q-p+1} f_{i,j,k,q} \to f_{i+1,j,k,p} \\ \forall p \in [1,q],k>1,k {q\choose q-p+1} f_{i,j,k,q} \to f_{i+1,j+1,k-1,p} \\ \] -
加入非叶节点:
\[jf_{i,j,k,q} \to f_{i+1,j,k+1,q+1} \\ kf_{i,j,k,q} \to f_{i+1,j+1,k,q+1} \\ \]
统计答案:对于 \(i\),答案为 \(\sum_{j=1}^{i-1} f_{i,j,0,1}\)。
namespace Sub3 {
constexpr int N(100+10);
int f[2][N][N][N];
int (*F)[N][N](f[0]),(*G)[N][N](f[1]);
bool Check() { return n<=50; }
int Cmain() {
F[0][1][1]=1;
FOR(i,1,n-1) {
FOR(j,1,i+1)FOR(k,0,i+1-j)FOR(q,1,i+1)G[j][k][q]=0;
FOR(j,i>1,i-1)FOR(k,0,i-j)FOR(q,1,i)if(F[j][k][q]) {
FOR(p,1,q)toadd(G[j][k][p],mul(j,C[q][q-p+1],F[j][k][q]));
if(k)FOR(p,1,q)toadd(G[j+1][k-1][p],mul(k,C[q][q-p+1],F[j][k][q]));
toadd(G[j][k+1][q+1],mul(j,F[j][k][q])),toadd(G[j+1][k][q+1],mul(k,F[j][k][q]));
}
int ans(0);
FOR(j,1,i)toadd(ans,G[j][0][1]);
cout<<ans<<endl,swap(F,G);
}
return 0;
}
}
\(O(n^4)\)
考虑优化上述算法,我们发现看起来最可优化的似乎是 \(p\) 的枚举。
那我们可以尝试在加入连通块时就给它分组,确定一组中之后都连同一个父节点,那么这时转移复杂度就减小到 \(O(1)\) 了。
\(f_{i,j,k,q}\) 表示加入第 \(i\) 个节点时,第一棵树中有 \(j\) 个有子节点的非叶节点,有 \(k\) 个无子节点的非叶节点,而第二棵树有 \(q\) 组连通块的数量。
边界:\(f_{1,0,1,1} = 1\)。
转移方程:
-
加入叶子结点:
\[q(q-1)j f_{i,j,k,q} \to f_{i+1,j,k,q-1} [q > 1]\\ qj f_{i,j,k,q} \to f_{i+1,j,k,q} \\ q(q-1)k f_{i,j,k,q} \to f_{i+1,j+1,k-1,q-1} [q > 1]\\ qk f_{i,j,k,q} \to f_{i+1,j+1,k-1,q} \\ \] -
加入非叶节点:
\[qjf_{i,j,k,q} \to f_{i+1,j,k+1,q} \\ jf_{i,j,k,q} \to f_{i+1,j,k+1,q+1} \\ qkf_{i,j,k,q} \to f_{i+1,j+1,k,q} \\ kf_{i,j,k,q} \to f_{i+1,j+1,k,q+1} \\ \]
统计答案:这种状态似乎不是很好统计,因为我们无法判断第二棵树是否成型,那么我们在统计 \(i\) 的时候要跑到 \(i-1\) 的时候模拟让它重新生成一遍,即 \(\sum_{j=1}^{i-1} (j f_{i-1,j,0,1} + f_{i-1,j-1,1,1})\)。
namespace Sub4 {
using Sub3::f;
using Sub3::F;
using Sub3::G;
bool Check() { return n<=100; }
int Cmain() {
F[0][1][1]=1;
FOR(i,1,n-1) {
FOR(j,0,i+1)FOR(k,0,i+1-j)FOR(q,1,i+1)G[j][k][q]=0;
FOR(j,i>1,i-1)FOR(k,0,i-j)FOR(q,1,i)if(F[j][k][q]) {
if(q>1)toadd(G[j][k][q-1],mul(q,q-1,j,F[j][k][q]));
toadd(G[j][k][q],mul(q,j,F[j][k][q]));
if(q>1)toadd(G[j+1][k-1][q-1],mul(q,q-1,k,F[j][k][q]));
toadd(G[j+1][k-1][q],mul(q,k,F[j][k][q]));
toadd(G[j][k+1][q],mul(q,j,F[j][k][q]));
toadd(G[j][k+1][q+1],mul(j,F[j][k][q]));
toadd(G[j+1][k][q],mul(q,k,F[j][k][q]));
toadd(G[j+1][k][q+1],mul(k,F[j][k][q]));
}
int ans(0);
FOR(j,1,i)toadd(ans,mul(j,F[j][0][1]),F[j-1][1][1]);
cout<<ans<<endl,swap(F,G);
}
return 0;
}
}
\(O(n^3)\)
法一
接下来要优化的肯定是 \(j,k\) 两维,我们可以简化它们:
\(f_{i,j,k}\) 表示加入第 \(i\) 个节点时,第一棵树中包含 \([1,i]\) 的子图有 \(j\) 个非叶结点——即包含 \([i+1,n]\) 的子图中有 \(j\) 组连通块——而第二棵树中包含 \([1,i]\) 的子图有 \(k\) 组连通块——即 \([i+1,n]\) 中有 \(k\) 个非叶结点——时的数量。这个时候转移需要带一点容斥。
边界:\(\forall i \in [1,n],f_{1,2,i} = i\),表示点在 \(i\) 组连通块中选择的方案数。
转移方程:
-
加入叶子结点:(\(\gets\) 表示加到,下同)
\[f_{i,j,k} \gets jkf_{i-1,j,k+1} - jkf_{i-1,j,k} \\ \]前者是第一棵树中为叶子,第二棵树为非叶子的总方案数,后者是容斥掉的不合法方案数:代表了在第二棵树看似为非叶子,但其实并没有子节点的方案数。
-
加入非叶节点:
\[f_{i,j,k} \gets (j-1)kf_{i-1,j-1,k} - jkf_{i-1,j,k} \\ \]同理。
(这部分如果转换成刷表来做会更快。)
统计答案:\(ans_2 = 1,ans_i = \sum_{j=1}^i jf_{j,1}\)。
namespace Sub {
int f[2][N][N];
int (*F)[N](f[0]),(*G)[N](f[1]);
int Cmain() {
FOR(i,1,n)F[1][i]=i;
cout<<"1"<<endl;
FOR(i,3,n) {
FOR(j,1,i)FOR(k,1,n)G[j][k]=mul(
k,add(
mul(
j,
add(F[j][k+1],Mod-F[j][k],F[j-1][k],Mod-F[j][k])
),
Mod-F[j-1][k]
)
);
int ans(0);
FOR(j,1,i)toadd(ans,mul(j,G[j][1]));
cout<<ans<<endl,swap(F,G);
}
return 0;
}
}
法二
我们直接从状压的部分跳过来,尝试大力容斥。
定义:
- 集合状态 \(S\) 表示在一棵树中,对于 \(\forall x \in S\),\(x\) 都是非叶结点,其余则是叶节点。
- 集合状态 \(rev_{n}(S)\) 表示将 \(S\) 倒转过来,即在一棵树中,对于 \(\forall x \in S\),\(n-x+1\) 都是非叶结点,其余则是叶节点。
- 集合 \(U_n = [1,n]\cap \mathbb{N}\)。
我们用 \(f(S)\) 表示第一棵树上状态恰好为 \(S\) 时,方案数为多少。那么从状压的部分就有:
但考试的时候打错了,然后偶然发现 \(f(S)=f(rev_n(\complement_{U_n}S))\),有:
设 \(g(S)\) 表示第一棵树上非叶结点只属于 \(S\) 的方案数,且需保证 \(1 \in S\),则有:
实际意义就是第 \(i\) 个点能取到的父节点数量的乘积。那么我们也可以表示出 \(f(S)\) 了:
那我们现在简单化一下答案式:
那么我们可以开始设计 DP 了。\(f_{i,j,k}\) 表示选了前 \(i\) 个点,且满足 \(|S \setminus T_1| = j, |S \setminus T_2| = k\) 时下式的值:
边界:\(f_{1,1,1}=1\)。
转移方程:按顺序决策是否要选第 \(i\) 个点,并且中途要枚举 \(j,k\)。
-
\(i \notin S\):那么必定有 \(i \notin T_1,j \notin T_2\),即
\[jkf_{i,j,k} \to f_{i,j,k} \\ \] -
\(i \in S\):那么要分四种情况
-
\(i \notin T_1,j \notin T_2\):
\[jkf_{i,j,k} \to f_{i,j+1,k+1} \\ \] -
\(i \notin T_1,j \in T_2\):
\[-jkf_{i,j,k} \to f_{i,j+1,k} \\ \] -
\(i \in T_1,j \notin T_2\):
\[-jkf_{i,j,k} \to f_{i,j,k+1} \\ \] -
\(i \in T_1,j \in T_2\):
\[jkf_{i,j,k} \to f_{i,j,k} \\ \]
-
统计答案:\(ans_i = \sum_{j=1}^i \sum_{k=1}^i f_{i,j,k}\)。
namespace Sub_ {
using Sub::f;
using Sub::F;
using Sub::G;
int Cmain() {
F[1][1]=1;
FOR(i,2,n) {
FOR(j,1,i)FOR(k,1,i)G[j][k]=0;
FOR(j,1,i-1)FOR(k,1,i-1)if(F[j][k]) {
const int val(mul(j*k,F[j][k]));
/*0*/
toadd(G[j][k],val);
/*1*/
toadd(G[j+1][k+1],val);
toadd(G[j][k+1],Mod-val);
toadd(G[j+1][k],Mod-val);
toadd(G[j][k],val);
}
int ans(0);
FOR(j,1,i)FOR(k,1,i)toadd(ans,G[j][k]);
cout<<ans<<endl,swap(F,G);
}
return 0;
}
}
完整代码
#define Plus_Cat "tree"
#include<bits/stdc++.h>
#define INF 0x3f3f3f3f
#define ll long long
#define RCL(a,b,c,d) memset(a,b,sizeof(c)*(d))
#define tomin(a,...) ((a)=min({(a),__VA_ARGS__}))
#define tomax(a,...) ((a)=max({(a),__VA_ARGS__}))
#define FOR(i,a,b) for(int i(a); i<=(int)(b); ++i)
#define DOR(i,a,b) for(int i(a); i>=(int)(b); --i)
#define EDGE(g,i,x,y) for(int i(g.h[x]),y(g[i].v); ~i; y=g[i=g[i].nxt].v)
#define main Main();signed main(){ios::sync_with_stdio(0);cin.tie(0),cout.tie(0);return Main();}signed Main
using namespace std;
constexpr int N(5e2+10);
#define DE(...) E(#__VA_ARGS__,__VA_ARGS__)
struct Ecat {
template<class T,class...Types>void operator ()(const char *fmt,const T a) {
return cerr<<fmt<<":"<<a<<".\n",void();
}
template<class T,class...Types>void operator ()(const char *fmt,const T a,const Types...args) {
while(*fmt^',')cerr<<*fmt++;
return cerr<<":"<<a<<", ",(*this)(++fmt,args...);
}
} E;
namespace Modular {
int Mod;
int C[N<<1][N<<1];
template<class T1,class T2>constexpr auto add(const T1 a,const T2 b) {
return a+b>=Mod?a+b-Mod:a+b;
}
template<class T1,class T2>constexpr auto mul(const T1 a,const T2 b) {
return (ll)a*b%Mod;
}
template<class T,class...Types>constexpr auto add(const T a,const Types...args) {
return add(a,add(args...));
}
template<class T,class...Types>constexpr auto mul(const T a,const Types...args) {
return mul(a,mul(args...));
}
template<class T1,class T2>T1 &toadd(T1 &a,const T2 b) {
return a=add(a,b);
}
template<class T1,class T2>T1 &tomul(T1 &a,const T2 b) {
return a=mul(a,b);
}
template<class T0,class T,class...Types>T0 &toadd(T0 &a,const T b,const Types...args) {
return toadd(a,b),toadd(a,args...);
}
template<class T0,class T,class...Types>T0 &tomul(T0 &a,const T b,const Types...args) {
return tomul(a,b),tomul(a,args...);
}
void Init(int n=(N<<1)-5) {
FOR(i,0,n) {
C[i][0]=1;
FOR(j,1,i)C[i][j]=add(C[i-1][j-1],C[i-1][j]);
}
}
} using namespace Modular;
int n;
namespace Sub2 {
constexpr int N(20+10),St((1<<20)+10);
int cnt[St];
int f[N][St];
bool Check() { return n<=20; }
int Cmain() {
FOR(i,1,St-5)cnt[i]=cnt[i>>1]+(i&1);
f[1][1]=1,f[2][2]=1;
FOR(i,3,n) {
const int U((1<<(i-1))-1);
FOR(S,1,U) {
toadd(f[i][S|1<<(i-1)],mul(f[i-1][S],i-1-cnt[S]));
FOR(j,1,i-1)if(S&1<<(j-1))toadd(f[i][(S^(1<<(j-1)))|1<<(i-1)],f[i-1][S]);
}
}
static int res[N+10];
FOR(i,2,n) {
const int U((1<<i)-1);
int ans(0);
FOR(S,0,U)if(f[i][S])toadd(ans,mul(f[i][S],f[i][S]));
cout<<ans<<endl;
res[i]=add(ans,Mod-res[i-1]);
}
return 0;
}
}
namespace Sub3 {
constexpr int N(100+10);
int f[2][N][N][N];
int (*F)[N][N](f[0]),(*G)[N][N](f[1]);
bool Check() { return n<=50; }
int Cmain() {
F[0][1][1]=1;
FOR(i,1,n-1) {
FOR(j,1,i+1)FOR(k,0,i+1-j)FOR(q,1,i+1)G[j][k][q]=0;
FOR(j,i>1,i-1)FOR(k,0,i-j)FOR(q,1,i)if(F[j][k][q]) {
FOR(p,1,q)toadd(G[j][k][p],mul(j,C[q][q-p+1],F[j][k][q]));
if(k)FOR(p,1,q)toadd(G[j+1][k-1][p],mul(k,C[q][q-p+1],F[j][k][q]));
toadd(G[j][k+1][q+1],mul(j,F[j][k][q])),toadd(G[j+1][k][q+1],mul(k,F[j][k][q]));
}
int ans(0);
FOR(j,1,i)toadd(ans,G[j][0][1]);
cout<<ans<<endl,swap(F,G);
}
return 0;
}
}
namespace Sub4 {
using Sub3::f;
using Sub3::F;
using Sub3::G;
bool Check() { return n<=100; }
int Cmain() {
F[0][1][1]=1;
FOR(i,1,n-1) {
FOR(j,0,i+1)FOR(k,0,i+1-j)FOR(q,1,i+1)G[j][k][q]=0;
FOR(j,i>1,i-1)FOR(k,0,i-j)FOR(q,1,i)if(F[j][k][q]) {
if(q>1)toadd(G[j][k][q-1],mul(q,q-1,j,F[j][k][q]));
toadd(G[j][k][q],mul(q,j,F[j][k][q]));
if(q>1)toadd(G[j+1][k-1][q-1],mul(q,q-1,k,F[j][k][q]));
toadd(G[j+1][k-1][q],mul(q,k,F[j][k][q]));
toadd(G[j][k+1][q],mul(q,j,F[j][k][q]));
toadd(G[j][k+1][q+1],mul(j,F[j][k][q]));
toadd(G[j+1][k][q],mul(q,k,F[j][k][q]));
toadd(G[j+1][k][q+1],mul(k,F[j][k][q]));
}
int ans(0);
FOR(j,1,i)toadd(ans,mul(j,F[j][0][1]),F[j-1][1][1]);
cout<<ans<<endl,swap(F,G);
}
return 0;
}
}
namespace Sub {
int f[2][N][N];
int (*F)[N](f[0]),(*G)[N](f[1]);
int Cmain() {
FOR(i,1,n)F[1][i]=i;
cout<<"1"<<endl;
FOR(i,3,n) {
FOR(j,1,i)FOR(k,1,n)G[j][k]=mul(
k,add(
mul(
j,
add(F[j][k+1],Mod-F[j][k],F[j-1][k],Mod-F[j][k])
),
Mod-F[j-1][k]
)
);
int ans(0);
FOR(j,1,i)toadd(ans,mul(j,G[j][1]));
cout<<ans<<endl,swap(F,G);
}
return 0;
}
}
namespace Sub_ {
using Sub::f;
using Sub::F;
using Sub::G;
int Cmain() {
F[1][1]=1;
FOR(i,2,n) {
FOR(j,1,i)FOR(k,1,i)G[j][k]=0;
FOR(j,1,i-1)FOR(k,1,i-1)if(F[j][k]) {
const int val(mul(j*k,F[j][k]));
/*0*/
toadd(G[j][k],val);
/*1*/
toadd(G[j+1][k+1],val);
toadd(G[j][k+1],Mod-val);
toadd(G[j+1][k],Mod-val);
toadd(G[j][k],val);
}
int ans(0);
FOR(j,1,i)FOR(k,1,i)toadd(ans,G[j][k]);
cout<<ans<<endl,swap(F,G);
}
return 0;
}
}
int main() {
#ifdef Plus_Cat
freopen(Plus_Cat ".in","r",stdin),freopen(Plus_Cat ".out","w",stdout);
#endif
cin>>n>>Mod,Init();
if(Sub2::Check())return Sub2::Cmain();
if(Sub3::Check())return Sub3::Cmain();
if(Sub4::Check())return Sub4::Cmain();
return Sub_::Cmain();
}