洛谷3349 BZOJ4455 ZJOI2016 小星星 状压dp 树形dp 容斥

17 篇文章 0 订阅
11 篇文章 0 订阅

题目链接

题意:
给你一个 n n n个点的树和一个 n n n个点 m m m条边的图,这两个 n n n的相同的,要求建立一个映射关系,使得树上的每一个点与图上的每一个点唯一对应,能对应的要求是在原来树上有连边的两个点在图上也有边直接相连。求这种对应的方案数。对 1 e 9 + 7 1e9+7 1e9+7取模。 n &lt; = 17 , m &lt; = n ( n − 1 ) n&lt;=17,m&lt;=n(n-1) n<=17,m<=n(n1)

题解:
感觉还是不太好想这个题啊,我这种辣鸡还是自己做不出来。

我们先考虑树上的一个点去对应图上的一个点。感觉 n &lt; = 17 n&lt;=17 n<=17这个数据范围一看就很状压,而这个题一看也会与选了哪些点的集合情况有关,于是我们考虑状压。但是这个题确实不是很好处理,因为一个比较好想的想法可能是一个 2 2 n 2^{2n} 22n的,你要考虑树上的哪些点对应了图上的哪些点,但是这个数据范围显然只能压一维,这就是这个题的一个思维上的难点所在了。

这个题的想法是我们枚举树上所有点对应了图上某一个点集的方案数,准确来讲是树上所有点对应图上某一个点集的子集的方案数,这里允许树上多个点对应了图上的同一个点。我们允许这样对应之后为了还能正确算出答案,我们要减去不合法的情况,于是用到了容斥原理。我们用至多对应了全集的,减去所有至多对应了全集少一个点的方案,就应该是正好全集的方案了,但是我们还多减了至多对应比全集少两个点的所有集合的方案了,依次类推,就是一个容斥原理。

我们在枚举树上所有点对应了图上的某个点集的点之后,算方案数的方法是假设1号点是这棵树的根节点,然后从这个点dfs,从深度大的点向深度小的点树形dp。具体地,我们到了一个点之后去枚举它与图上的哪一个点对应,然后用它所有子树算过的答案乘起来,这时候我们要枚举子树的根节点,然后由于要在树上有边直接相连的话图上也要能直接相连,于是我们就枚举当前点对应的图上的那个点在图上直接相连的所有点,看子树的根节点对应这些节点的方案数之和加起来。说起来可能有点不是很容易说明白,可以看代码,我在这里写一下式子。我们设 d p [ x ] [ y ] dp[x][y] dp[x][y]表示树上 x x x这个节点对应了图上 y y y这个点,那么我们有 d p [ x ] [ y ] = ∏ i ∈ s o n [ x ] ∑ y 与 z 有 边 相 连 d p [ i ] [ z ] dp[x][y]=\prod_{i\in son[x]}\sum_{y与z有边相连}dp[i][z] dp[x][y]=ison[x]yzdp[i][z]

这样这个题就可以通过容斥算出答案了,但是复杂度有点奇怪,最后我们来算一下复杂度。这个复杂度乍一看是 O ( 2 n ∗ n 4 ) O(2^n*n^4) O(2nn4)的。但是我们可以发现,在dp的时候,我们第一维枚举的是树上的边,这个在一次dfs中所有点的总和是 O ( n ) O(n) O(n)级别的,于是均摊下来是每次 O ( 1 ) O(1) O(1)级别的,于是实际复杂度应该是 O ( 2 n ∗ n 3 ) O(2^n*n^3) O(2nn3)的,但是这个复杂度算一下发现其实还是过不了,但是为什么这个题就这么过了呢?其实是因为这个题的常数很小的,你会发现,我们在dp的时候后面的集合里的数的个数是按照 O ( n ) O(n) O(n)算的,但是显然不会每一次都达到这个数量,大部分情况比 n n n要小不少,再加上很多数据图不一定是完全图,于是边数可能也不一定达到了那么多,于是最终这个题写的优秀一点就可以这样过掉。似乎我也没找到什么复杂度更优秀的做法。如果对复杂度有问题或者有复杂度更优的做法,欢迎交流。

代码:

#include <bits/stdc++.h>
using namespace std;

int n,m,hed[30],hed1[30],cnt,cnt1,mi[30],num,ji[20];
long long ans,dp[20][20];
struct node
{
	int to,next;
}a[10000],b[10000];
inline void add(int from,int to)
{
	a[++cnt].to=to;
	a[cnt].next=hed[from];
	hed[from]=cnt;
}
inline void add1(int from,int to)
{
	b[++cnt1].to=to;
	b[cnt1].next=hed1[from];
	hed1[from]=cnt1;
}
void dfs(int x,int f)
{
	for(int i=hed[x];i;i=a[i].next)
	{
		int y=a[i].to;
		if(y==f)
		continue;
		dfs(y,x);
	}
	for(int i=1;i<=num;++i)
	dp[x][ji[i]]=1;
	for(int i=hed[x];i;i=a[i].next)
	{
		int y=a[i].to;
		if(y==f)
		continue;
		for(int j=1;j<=num;++j)
		{
			long long res=0;
			for(int k=hed1[ji[j]];k;k=b[k].next)
			{
				int z=b[k].to;
				res+=dp[y][z];
			}			
			dp[x][ji[j]]*=res;
		}
	}
}
int main()
{
	scanf("%d%d",&n,&m);
	for(int i=1;i<=m;++i)
	{
		int x,y;
		scanf("%d%d",&x,&y);
		add1(x,y);
		add1(y,x);
	}
	for(int i=1;i<=n-1;++i)
	{
		int x,y;
		scanf("%d%d",&x,&y);
		add(x,y);
		add(y,x);
	}
	mi[0]=1;
	for(int i=1;i<=20;++i)
	mi[i]=mi[i-1]*2;
	for(int i=1;i<=mi[n]-1;++i)
	{
		num=0;
		memset(dp,0,sizeof(dp));
		for(int j=1;j<=n;++j)
		{
			if(i&(mi[j-1]))
			ji[++num]=j;
		}
		dfs(1,0);
		long long res=0;
		for(int j=1;j<=num;++j)
		res+=dp[1][ji[j]];
		if((n-num)&1)
		ans-=res;
		else
		ans+=res;
	}
	printf("%lld\n",ans);
	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值