[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\)

转移方程:

  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} \\ \]

  2. 加入非叶节点:

    \[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\)

转移方程:

  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} \\ \]

  2. 加入非叶节点:

    \[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\) 组连通块中选择的方案数。

转移方程:

  1. 加入叶子结点:(\(\gets\) 表示加到,下同)

    \[f_{i,j,k} \gets jkf_{i-1,j,k+1} - jkf_{i-1,j,k} \\ \]

    前者是第一棵树中为叶子,第二棵树为非叶子的总方案数,后者是容斥掉的不合法方案数:代表了在第二棵树看似为非叶子,但其实并没有子节点的方案数。

  2. 加入非叶节点:

    \[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\) 时,方案数为多少。那么从状压的部分就有:

\[ans_n = \sum_{S \in U_n} f(S)f(rev_n(\complement_{U_n}S)) \]

但考试的时候打错了,然后偶然发现 \(f(S)=f(rev_n(\complement_{U_n}S))\),有:

\[ans_n = \sum_{S\in U_n}f^2(S) \]

\(g(S)\) 表示第一棵树上非叶结点只属于 \(S\) 的方案数,且需保证 \(1 \in S\),则有:

\[g(S) = \prod_{i=2}^n\sum_{j\in S} [j<i] \]

实际意义就是第 \(i\) 个点能取到的父节点数量的乘积。那么我们也可以表示出 \(f(S)\) 了:

\[\begin{aligned} f(S) & = \sum_{T \subset S , 1 \notin T} (-1)^{|T|} g(S \setminus T) \\ f(S) & = \sum_{T \subset S , 1 \notin T} (-1)^{|T|} (\prod_{i=2}^n\sum_{j\in S \setminus T} [j<i])\\ \end{aligned} \]

那我们现在简单化一下答案式:

\[\begin{aligned} ans_n & = \sum_{S\in U_n} f^2(S) \\ & = \sum_{S\in U_n} \sum_{T_1 \subset S , 1 \notin T_1} \sum_{T_2 \subset S , 1 \notin T_2} (-1)^{|T_1|+|T_2|} (\prod_{i=2}^n\sum_{j\in S \setminus T_1} [j<i]) (\prod_{i=2}^n\sum_{j\in S \setminus T_1} [j<i]) \\ & = \sum_{S\in U_n} \sum_{T_1 \subset S , 1 \notin T_1} \sum_{T_2 \subset S , 1 \notin T_2} (-1)^{|T_1|+|T_2|} \prod_{i=2}^n(\sum_{j\in S \setminus T_2} [j<i]) (\sum_{j\in S \setminus T_2} [j<i]) \\ \end{aligned} \]

那么我们可以开始设计 DP 了。\(f_{i,j,k}\) 表示选了前 \(i\) 个点,且满足 \(|S \setminus T_1| = j, |S \setminus T_2| = k\) 时下式的值:

\[\sum_{S\in U_i} \sum_{T_1 \subset S , 1 \notin T_1} \sum_{T_2 \subset S , 1 \notin T_2} (-1)^{|T_1|+|T_2|} \prod_{x=2}^i(\sum_{y\in S \setminus T_2} [y<x]) (\sum_{y\in S \setminus T_2} [y<x]) \\ \]

边界:\(f_{1,1,1}=1\)

转移方程:按顺序决策是否要选第 \(i\) 个点,并且中途要枚举 \(j,k\)

  1. \(i \notin S\):那么必定有 \(i \notin T_1,j \notin T_2\),即

    \[jkf_{i,j,k} \to f_{i,j,k} \\ \]

  2. \(i \in S\):那么要分四种情况

    1. \(i \notin T_1,j \notin T_2\)

      \[jkf_{i,j,k} \to f_{i,j+1,k+1} \\ \]

    2. \(i \notin T_1,j \in T_2\)

      \[-jkf_{i,j,k} \to f_{i,j+1,k} \\ \]

    3. \(i \in T_1,j \notin T_2\)

      \[-jkf_{i,j,k} \to f_{i,j,k+1} \\ \]

    4. \(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();
}

posted @ 2025-03-13 21:11  Add_Catalyst  阅读(15)  评论(0)    收藏  举报