【BZOJ3167】SAO(HEOI2013)-高维树形DP+组合数学

本文介绍了一种使用高维树形动态规划和组合数学解决特定拓扑排序问题的方法,针对有向边的树结构,通过类树形背包的技巧,将子树逐一合并至根节点,计算拓扑排序方案数。

测试地址:SAO
题目大意: 给定一棵树边是有向边的树(边不一定从根指向儿子),求拓扑序数量。 n ≤ 1000 n\le 1000 n1000
做法: 本题需要用到高维树形DP+组合数学。
如果边都是从根连向儿子的话,简单组合就可以树形DP O ( n ) O(n) O(n)做了,问题是这道题并不是这样的,而是一个“类树”的结构,但因为它还是棵树所以我们还是考虑树形DP。
考虑用类似树形背包的做法,把子树一一合并到根上。令 f ( i , j ) f(i,j) f(i,j)为在以 i i i为根的子树中,点 i i i在拓扑序中的位置为 j j j的拓扑序方案数,那么我们一开始令 f ( i , 1 ) = 1 f(i,1)=1 f(i,1)=1,其它都是 0 0 0。那么在合并一棵子树时,这棵子树的根要求比点 i i i先取或者后取,因为这两个限制对称,所以在这里我们只讨论要求子树根比点 i i i先取的情况。
考虑枚举两个值 j , k j,k j,k j j j表示点 i i i在合并前的拓扑序中的位置, k k k表示在当前子树的拓扑序中比点 i i i先取的点的数目,我们来考虑贡献。首先,因为满足性质的拓扑序可以随便选,因此贡献乘上一个 f ( i , j ) f(i,j) f(i,j)。接着,因为要求子树根要比点 i i i先取,所以当且仅当 p ≤ k p\le k pk p p p表示子树根在当前子树的拓扑序中的位置)时才满足条件,此时所有的 f ( s o n , p ) f(son,p) f(son,p)都是可以选的,那么贡献乘上一个 ∑ p = 1 k f ( s o n , p ) \sum_{p=1}^k f(son,p) p=1kf(son,p)。接着就是合并的环节了,当前子树拓扑序的前 k k k个塞到点 i i i之前,方案数为 C j + k − 1 k C_{j+k-1}^k Cj+k1k,剩下的 s i z ( s o n ) − k siz(son)-k siz(son)k个塞到点 i i i之后,同理得到方案数为 C s i z ( i ) + s i z ( s o n ) − j − k s i z ( s o n ) − k C_{siz(i)+siz(son)-j-k}^{siz(son)-k} Csiz(i)+siz(son)jksiz(son)k。那么令 n o w ( x ) now(x) now(x)为合并后拓扑序中点 i i i在第 x x x位的方案数,那么一对 j , k j,k j,k n o w ( j + k ) now(j+k) now(j+k)的贡献为:
f ( i , j ) ⋅ [ ∑ p = 1 k f ( s o n , p ) ] ⋅ C j + k − 1 k ⋅ C s i z ( i ) + s i z ( s o n ) − j − k s i z ( s o n ) − k f(i,j)\cdot [\sum_{p=1}^k f(son,p)]\cdot C_{j+k-1}^k\cdot C_{siz(i)+siz(son)-j-k}^{siz(son)-k} f(i,j)[p=1kf(son,p)]Cj+k1kCsiz(i)+siz(son)jksiz(son)k
类似地处理完要求 s o n son son i i i后取的情况,预处理组合数,再顺便算一个前缀和(要求那个 ∑ \sum )就可以很快地直接DP了。而因为这个DP合并子树的方式和树形背包极其相似,我们可以用类似树形背包的方法证明复杂度是 O ( n 2 ) O(n^2) O(n2)的(考虑每对点都在其LCA处恰好被计算一次),我个人把这种高维树形DP称为类树形背包。这样我们就解决了这一题。
以下是本人代码:

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll mod=1000000007;
int T,n,first[1010],tot;
int siz[1010];
ll C[1010][1010],f[1010][1010],sum[1010][1010],now[1010];
struct edge
{
	int v,next;
	bool type;
}e[2010];

void insert(int a,int b,bool type)
{
	e[++tot].v=b;
	e[tot].type=type;
	e[tot].next=first[a];
	first[a]=tot;
}

ll CC(int n,int m)
{
	if (n<0||m<0||n<m) return 0;
	else return C[n][m];
}

void dp(int v,int fa)
{
	siz[v]=1;
	memset(f[v],0,sizeof(f[v]));
	f[v][1]=1;
	for(int i=first[v];i;i=e[i].next)
		if (e[i].v!=fa)
		{
			int y=e[i].v;
			dp(y,v);
			for(int j=1;j<=siz[v]+siz[y];j++)
				now[j]=0;
			if (e[i].type)
			{
				for(int j=1;j<=siz[v];j++)
					for(int k=1;k<=siz[y];k++)
						now[j+k]=(now[j+k]+f[v][j]*sum[y][k]%mod*CC(j+k-1,k)%mod*CC(siz[v]+siz[y]-j-k,siz[y]-k))%mod;
			}
			else
			{
				for(int j=1;j<=siz[v];j++)
					for(int k=0;k<siz[y];k++)
						now[j+k]=(now[j+k]+f[v][j]*(sum[y][siz[y]]-sum[y][k]+mod)%mod*CC(j+k-1,k)%mod*CC(siz[v]+siz[y]-j-k,siz[y]-k))%mod;
			}
			siz[v]+=siz[y];
			for(int j=1;j<=siz[v];j++)
				f[v][j]=now[j];
		}
	sum[v][0]=0;
	for(int i=1;i<=siz[v];i++)
		sum[v][i]=(sum[v][i-1]+f[v][i])%mod;
}

int main()
{
	C[0][0]=1;
	for(int i=1;i<=1000;i++)
	{
		C[i][0]=1;
		for(int j=1;j<=i;j++)
			C[i][j]=(C[i-1][j-1]+C[i-1][j])%mod;
	}
	
	scanf("%d",&T);
	while(T--)
	{
		memset(first,0,sizeof(first));
		tot=0;
		
		scanf("%d",&n);
		for(int i=1;i<n;i++)
		{
			int a,b;
			char s;
			scanf("%d %c%d",&a,&s,&b);
			if (s=='>') swap(a,b);
			insert(a,b,0);
			insert(b,a,1);
		}
		
		dp(0,-1);
		printf("%lld\n",sum[0][n]);
	}
	
	return 0;
}
题目描述 有一个 $n$ 个点的棋盘,每个点上有一个数字 $a_i$,你需要从 $(1,1)$ 走到 $(n,n)$,每次只能往右或往下走,每个格子只能经过一次,路径上的数字和为 $S$。定义一个点 $(x,y)$ 的权值为 $a_x+a_y$,求所有满足条件的路径中,所有点的权值和的最小值。 输入格式 第一行一个整数 $n$。 接下来 $n$ 行,每行 $n$ 个整数,表示棋盘上每个点的数字。 输出格式 输出一个整数,表示所有满足条件的路径中,所有点的权值和的最小值。 数据范围 $1\leq n\leq 300$ 输入样例 3 1 2 3 4 5 6 7 8 9 输出样例 25 算法1 (树形dp) $O(n^3)$ 我们可以先将所有点的权值求出来,然后将其看作是一个有权值的图,问题就转化为了在这个图中求从 $(1,1)$ 到 $(n,n)$ 的所有路径中,所有点的权值和的最小值。 我们可以使用树形dp来解决这个问题,具体来说,我们可以将这个图看作是一棵树,每个点的父节点是它的前驱或者后继,然后我们从根节点开始,依次向下遍历,对于每个节点,我们可以考虑它的两个儿子,如果它的两个儿子都被遍历过了,那么我们就可以计算出从它的左儿子到它的右儿子的路径中,所有点的权值和的最小值,然后再将这个值加上当前节点的权值,就可以得到从根节点到当前节点的路径中,所有点的权值和的最小值。 时间复杂度 树形dp的时间复杂度是 $O(n^3)$。 C++ 代码 算法2 (动态规划) $O(n^3)$ 我们可以使用动态规划来解决这个问题,具体来说,我们可以定义 $f(i,j,s)$ 表示从 $(1,1)$ 到 $(i,j)$ 的所有路径中,所有点的权值和为 $s$ 的最小值,那么我们就可以得到如下的状态转移方程: $$ f(i,j,s)=\min\{f(i-1,j,s-a_{i,j}),f(i,j-1,s-a_{i,j})\} $$ 其中 $a_{i,j}$ 表示点 $(i,j)$ 的权值。 时间复杂度 动态规划的时间复杂度是 $O(n^3)$。 C++ 代码
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值