[ZJOI2018]线图 状态压缩Dp暴力+剪枝 树Hash

本文深入探讨了线图转换的复杂性,通过手玩分析揭示了不同阶线图的特性,如点数变化、度数关系及特殊形态。提出了针对特定阶数的算法策略,包括基于人类智慧的直观理解、暴力搜索有根树匹配以及状态压缩DP解决有标号子树匹配问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

luogu4337 && bzoj5211 线图

题目传送门:
luogu
bzoj

分析

可怜日常劝退题。
好久没有写这么硬核的题了。

手玩:人类的智慧

这道题的重点就在于手玩,玩着玩着才能玩出一些名堂来。
在这里插入图片描述
比如这张图。
L ( G ) L(G) L(G)
在这里插入图片描述
没啥可说的。
L 2 ( G ) L^2(G) L2(G)
在这里插入图片描述
观察二阶的图,发现图中的每一个点
稍微重标号一下下
( 1 , 2 , 3 ) − > 1 , ( 2 , 3 , 4 ) − > 2 , ( 1 , 2 , 4 ) − > 3 , ( 2 , 4 , 5 ) − > 4 (1,2,3)->1, (2,3,4)->2,(1,2,4)->3,(2,4,5)->4 (1,2,3)>1,(2,3,4)>2,(1,2,4)>3,(2,4,5)>4
在这里插入图片描述
L 3 L3 L3
在这里插入图片描述
首先从一次变换来看:
1.点数变成边数。
2. G G G一条边 ( u , v ) (u,v) (u,v)的在 L ( G ) L(G) L(G)的度数为 d u + d v − 2 d_u+d_v-2 du+dv2
暂时,也只能挖掘这么多的性质。
从骗分的角度来看;
L ( G ) : L(G): L(G):输出边数
L 2 ( G ) : L^2(G): L2(G):每一个“弯折( &lt; u , v &gt; , &lt; v , w &gt; &lt;u,v&gt;,&lt;v,w&gt; <u,v>,<v,w>)”在 L ( G ) L(G) L(G)中都是一条边。每个点 u u u可以有 C d u 2 C_{d_u}^2 Cdu2种“弯折”,所以 A n s ( L 2 ( G ) ) = ∑ C d u 2 Ans(L^2(G))=\sum C_{d_u}^2 Ans(L2(G))=Cdu2
L 3 ( G ) : L^3(G): L3(G):看成 L 2 ( L ( G ) ) L^2(L(G)) L2(L(G)) L ( G ) L(G) L(G)的每个点的度数是 d u + d v − 2 d_u+d_v-2 du+dv2
所以 A n s ( L 3 ( G ) ) = ∑ m C d u + d v − 2 2 Ans(L^3(G))=\sum_mC_{d_u+d_v-2}^2 Ans(L3(G))=mCdu+dv22
L 4 ( G ) : L^4(G): L4(G):问题有些棘手,仍然将其看成 L 3 ( L ( G ) ) L^3(L(G)) L3(L(G)),不过我们不能再仅仅靠度数了,我们得挖掘一下图的形态。
首先换一个角度考虑 L 3 ( G ) L^3(G) L3(G)
我们发现, L 3 ( G ) L^3(G) L3(G)的点数实际上是不同的三条边的联通子图个数(边有标号)。
那么一共有环、链和鸡爪三种形态,注意选边的顺序是有影响的。
鸡爪: ∑ 3 C d u 3 \sum 3C_{d_u}^3 3Cdu3
环和链: ∑ ( u , v ) ( d u − 1 ) ( d v − 1 ) \sum_{(u,v)} (d_u-1)(d_v-1) (u,v)(du1)(dv1)
再考虑 G → L ( G ) G\to L(G) GL(G)
原图上的某个点 u u u和围着它的一圈边。这一圈边在 L ( G ) L(G) L(G)上构成了一张完全导出子图,而反过来说,整张图就是由这一张张导出子图构成的。
因此计算 L ( G ) L(G) L(G) ( d u − 1 ) ( d v − 1 ) (d_u-1)(d_v-1) (du1)(dv1)贡献时,任意一个完全子图的边两两匹配,每个点 u u u的贡献就是 1 2 ( ( ∑ v d u + d v − 2 ) 2 − ∑ ( u , v ) ( d u + d v − 2 ) 2 ) \frac{1}{2}((\sum_v d_u+d_v-2)^2-\sum_{(u,v)}(d_u+d_v-2)^2) 21((vdu+dv2)2(u,v)(du+dv2)2)
d 1 ( u , v ) = d u + d v − 1 , d 2 ( u ) = ∑ d 1 ( u , v ) − 1 d_1(u,v)=d_u+d_v-1,d_2(u)=\sum d_1(u,v)-1 d1(u,v)=du+dv1,d2(u)=d1(u,v)1
那么总贡献
1 2 ∑ u d 2 ( u ) 2 − ∑ ( u , v ) ( d 1 ( u , v ) ) \frac{1}{2}\sum_u d_2(u)^2-\sum_{(u,v)}(d_1(u,v)) 21ud2(u)2(u,v)(d1(u,v))
后面的 1 2 \frac{1}{2} 21不见了是因为每条边在两个点处各被减去了一次。
然后剩下的 ∑ 3 C d u 3 = ∑ ( u , v ) 3 C d 1 ( u , v ) 3 \sum 3C_{d_u}^3=\sum_{(u,v)}3C_{d_1(u,v)}^3 3Cdu3=(u,v)3Cd1(u,v)3
加起来就好啦。

分析:问题的转化

至于更大的 K K K,人类智慧就不太够用了。所以只能慢慢用 L L L展开,大概可以得60pts+?
其实在推导的时候已经有了一些眉目,我们发现, L 2 ( G ) L^2(G) L2(G)中的点数和”弯折“对应, L 3 ( G ) L^3(G) L3(G)中的点数和三条边的联通子图对应,那么 L k ( G ) L^{k}(G) Lk(G)中的点数和不超过k条边的联通子图一一对应
考虑暴力搜索出所有点数的不超过 k + 1 k+1 k+1的有根树(k=9的时候只有1205种),用这些有根树去匹配原树。
假设有 T T T棵树那么答案就是
∑ T w i t i \sum_Tw_it_i Twiti
w i w_i wi是这棵树变换之后的点数, t i t_i ti是这棵树在原树中的出现次数。
w i w_i wi用人类智慧+暴力变换可以搞出来。复杂度 O ( 能 过 ) O(能过) O()
t i t_i ti实际上是一个有根树的匹配问题,采用状态压缩Dp

状态压缩Dp

f i , j f_{i,j} fi,j表示原树上的 i i i子树匹配 T T T j j j子树的方案数。
方程。
考虑子树合并,合并的时候用一个 g S g_S gS数组表示 j j j子树的儿子们的匹配情况为 S S S的方案数,枚举当前节点用哪一个子树匹配。
g S = ∑ g S − p f s o n , p g_S=\sum g_{S-p}f_{son,p} gS=gSpfson,p
然后 f ’ i , j = g a l l f’_{i,j}=g_{all} fi,j=gall
这样其实还有一点问题。
有根树和有标号的树是不等价的,但是在状态压缩Dp的时候我们强行给儿子标号了。但实际上,如果两个儿子是本质相同的,匹配的相对顺序是不打紧的。具体地来说,如果有 x x x组本质相同的儿子子树,每组的个数分别是 c n t 1 , c n t 2 ⋯ c n t x cnt_1,cnt_2\cdots cnt_x cnt1,cnt2cntx,那么我们重复计数了 ∏ i x c n t i ! \prod_i^x cnt_i! ixcnti!次。
在树 H a s h Hash Hash处理出这个 ∏ i x c n t i ! \prod_i^x cnt_i! ixcnti!,然后计算完之后逆元乘上去即可。
有一个小小的常数优化,我们可以先挖掉所有的叶子节点,最后再把叶子节点放上去匹配。假设原树的当前儿子有 s o n son son个, T T T子树的非叶子儿子有 p p p个,叶子节点有 l f lf lf个,那么贡献就是 C s o n − p l f l f ! C_{son-p}^{lf} lf! Csonplflf!
乘上那个 l f lf lf阶乘是因为状态压缩Dp的部分是有标号儿子,之后转成无标号的操作在 ∏ i x c n t i ! \prod_i^x cnt_i! ixcnti!一步中除掉了。两部分分开写会比较清晰。

代码

真的硬核,肝了半天。

// luogu-judger-enable-o2
#include<bits/stdc++.h>
#define fi first
#define se second
#define pb push_back
#define mp std::make_pair
const int N = 5e3 + 10, M = 1e5 + 10, P = 998244353;
typedef std::pair<int, int>pa;
int ri() {
	char c = getchar(); int x = 0, f = 1; for(;c < '0' || c > '9'; c = getchar()) if(c == '-') f = -1;
	for(;c >= '0' && c <= '9'; c = getchar()) x = (x << 1) + (x << 3) - '0' + c; return x * f;
}
int fix(int x) {return (x >> 31 & P) + x;}
int add(int a, int b) {return a += b, a >= P ? a - P : a;}
int mul(int a, int b) {return 1LL * a * b % P;}
int Pow(int x, int k) {
	int r = 1;
	for(;k; x = mul(x, x), k >>= 1)
		if(k & 1)
			r = mul(r, x);
	return r;
}
int bin[21], Lg[2049], cnt[2049], A[1 << 21], g[1 << 21], C[N][21];
int f[N][15], pr[N], to[N << 1], nx[N << 1], fac[21], tp, K, Ans, n, k;
void ins(int u, int v) {to[++tp] = v; nx[tp] = pr[u]; pr[u] = tp;}
void adds(int u, int v) {ins(u, v); ins(v, u);}
std::vector<pa>E;
struct Tree {
	int p[15], fa[15], lf[15], sz[15], same[15], st[30], k; bool all;
	std::vector<int>T[15];
	void Ins(int u, int f) {
		p[f] |= bin[u]; 
		fa[u] = f; 
		E.pb(mp(u, f));
		T[u].pb(f);
		T[f].pb(u);
	}
	void clear() {
		for(int i = 0;i < k; ++i)
			lf[i] = p[i] = 0, T[i].clear();
		E.clear();
	}
	int Build() {
		int u = 0, cnt = 0;
		for(int i = 1;i <= k - 1 << 1; ++i)
			if(!st[i]) {
				if(!u) return -1;
				u = fa[u];
			}
			else {
				Ins(++cnt, u);
				u = cnt;
				if(u >= k) return -1;
			}
		return cnt + 1;
	}
	std::string Uni(int u, int t, int fa) {
		if(all) sz[u] = 1;
		std::vector<std::string>x;
		for(int v : T[u])
			if((bin[v] & t) && v != fa) {
				x.pb(Uni(v, t, u));
				if(all) sz[u] += sz[v];
			}
		std::sort(x.begin(), x.end());
		std::string res = "";
		for(int i = 0;i < x.size(); ++i)
			res += '1' + x[i] + '0';
		if(all) {
			int cnt = 1; same[u] = 1;
			for(int i = 1;i < x.size(); ++i)
				if(x[i] == x[i - 1]) 
					++cnt;
				else
					same[u] = mul(same[u], fac[cnt]), cnt = 1;
			same[u] = Pow(mul(same[u], fac[cnt]), P - 2);
		}
		return res;
	}
	int Hash(std::string A) {
		int res = 0;
		for(int i = 0;i < A.size(); ++i)
			if(A[i] == '1')
				res |= bin[i];
		return res;
	}
	int Sub(int t) {
		std::string A = Uni(Lg[t&-t], t, -1);
		if(A.size() != cnt[t] - 1 << 1)
			return -1;
		return Hash(A);
	}
	void Dp(int u, int fa) {
		int son = 0;
		for(int i = pr[u]; i; i = nx[i])
			if(to[i] != fa)
				Dp(to[i], u), ++son;
		for(int rt = 0; rt < k; ++rt) {
			if(sz[rt] == 1) continue;
			if(son < cnt[p[rt]]) {
				f[u][rt] = 0;
				continue;
			}
			for(int k = p[rt]; k; k = (k - 1) & p[rt])
				g[k] = 0;
			g[0] = 1;
			for(int i = pr[u];i; i = nx[i]) {
				if(to[i] == fa) continue;
				for(int t = p[rt]; t; t = (t - 1) & p[rt])
					for(int u = t, lb; u; u ^= lb)
						lb = u&-u,
						g[t] = add(g[t], mul(g[t ^ lb], f[to[i]][Lg[lb]]));
			}
			f[u][rt] = mul(mul(g[p[rt]], same[rt]), C[son - cnt[p[rt]]][lf[rt]]);
		}
	}
	int Calct() {
		for(int i = 0;i < k; ++i)
			for(int t = p[i], lb; t; t ^= lb)
				if(sz[Lg[lb = t&-t]] == 1)
					++lf[i], p[i] ^= lb;
		Dp(1, 0); int ans = 0;
		for(int i = 1;i <= n; ++i)
			ans = add(ans, f[i][0]);
		return ans;
	}
}nw;
namespace Calcw {
	int d[M], d2[M]; std::vector<int>B[M];
	int c2(int x) {return (1LL * x *  (x - 1) >> 1) % P;}
	int c3(int x) {return mul(mul(x, x - 1), x - 2);}
	int sqr(int x) {return mul(x, x);}
	int Work(int n, int k) {
		int res = 0;
		if(k == 1) return E.size();
		for(int i = 0;i < n; ++i)
			d[i] = d2[i] = 0;
		for(pa p : E)
			++d[p.fi], ++d[p.se];
		if(k == 2) {
			for(int i = 0;i < n; ++i)
				res = add(res, c2(d[i]));
			return res;
		}
		if(k == 3) {
			for(pa p : E)
				res = add(res, c2(d[p.fi] + d[p.se] - 2));
			return res;
		}
		if(k == 4) {
			for(int i = 0;i < E.size(); ++i) {
				int u = E[i].fi, v = E[i].se;
				int d1 = d[u] + d[v] - 2;
				res = add(res, fix(c3(d1) - mul(sqr(d1 - 1), 2)));
				d2[u] = add(d2[u], d1 - 1);
				d2[v] = add(d2[v], d1 - 1);
			}
			for(int i = 0;i < n; ++i)
				res = add(res, sqr(d2[i]));
			return mul(res, P + 1 >> 1);
		}
		int cnt = 0;
		for(int i = 0;i < n; ++i)
			B[i].clear();
		for(pa p : E) {
			B[p.fi].pb(cnt);
			B[p.se].pb(cnt++);
		}
		E.clear();
		for(int i = 0;i < n; ++i)
			for(int j = 0;j < B[i].size(); ++j)
				for(int k = 0;k < j; ++k)
					E.pb(mp(B[i][j], B[i][k]));
		return Work(cnt, k - 1);
	}
}	
void Dfs(int x, int k) {
	if(x <= k - 1 << 1) {
		nw.st[x] = 0; Dfs(x + 1, k);
		nw.st[x] = 1; Dfs(x + 1, k);
		return ;
	}
	nw.k = k; nw.clear();
	if(nw.Build() != k) return ;
	nw.all = true;
	int s = nw.Hash(nw.Uni(0, bin[k] - 1, -1)), w;
	nw.all = false;
	if(~A[s]) return ;
	w = Calcw::Work(k, K);
	for(int t = 0, u;t < bin[k] - 1; ++t)
		if(~(u = nw.Sub(t)))
			w = fix(w - A[u]);
	A[s] = w;
	Ans = add(Ans, mul(w, nw.Calct()));
}
void Pre() {
	memset(A, -1, sizeof(A));
	bin[0] = 1;
	for(int i = 1;i <= 20; ++i) {
		bin[i] = bin[i - 1] << 1;
		if(i <= 11) Lg[bin[i]] = i;
	}
	for(int i = 0;i < 2048; ++i)
		cnt[i] = cnt[i >> 1] + (i & 1);
	for(int i = 0;i <= 5000; ++i) {
		C[i][0] = 1;
		for(int j = 1;j <= i && j <= 20; ++j)
			C[i][j] = add(C[i - 1][j], C[i - 1][j - 1]);
	}
	fac[0] = 1; 
	for(int i = 1;i <= 20; ++i) fac[i] = mul(fac[i - 1], i);
	for(int i = 0;i <= 5000; ++i)
		for(int j = 0;j <= 20; ++j)
			C[i][j] = mul(C[i][j], fac[j]);
}
int main() {
	Pre();
	n = ri(); K = ri();
	for(int i = 1;i < n; ++i)
		adds(ri(), ri());
	for(int i = 1;i <= K + 1; ++i)
		Dfs(1, i);
	printf("%d\n", Ans);
	return 0;
}
/*
5 5 
1 2 
2 3 
2 5
3 4
*/
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值