高手专项训练-树形DP

A. 联合权值

【题目描述】

给出一颗 n n n 个点的,点从 0 0 0 编号,不带权值有根树,给出每个点的父亲,以 0 0 0 号点为根,一个满足要求的联通块为一个联合联通块,求其中有多少的这样的联合联通块。

这个联通块如图要求分为六部分如下要求,每一个部分为一条链(由边组成的链),长度不限:

  • 0 0 0 部分可到达第 1 1 1 部分。
  • 1 1 1 部分可到达第 2 2 2 部分。
  • 1 1 1 部分可到达第 3 3 3 部分。
  • 1 1 1 部分可到达第 4 4 4 部分。
  • 4 4 4 部分可到达第 5 5 5 部分。
  • 4 4 4 部分可到达第 6 6 6 部分。
  • 这六部分的六个路径是不相交的。(也就是说,他们没有公共的边。)
  • 要求 1 , 2 , 3 , 4 , 5 , 6 1,2,3,4,5,6 1,2,3,4,5,6 0 0 0 的子树中, 2 , 3 , 4 , 5 , 6 2,3,4,5,6 2,3,4,5,6 1 1 1的子树中,以此类推。
    在这里插入图片描述
    答案对 1 0 9 + 7 10^9+7 109+7 取模。
【输入格式】

一行 n − 1 n-1 n1 个整数,为 1 1 1 ~ n − 1 n-1 n1 号点的父亲(不输入 )。

【输出格式】

一行一个整数,表示答案。

【数据范围与提示】

对于 100 % 100\% 100% 的数据, 1 ≤ n ≤ 2000 1 \leq n \leq 2000 1n2000

想法

将点分成五种:

  1. 像图中2,3,5,6中的链上的点
  2. 像4这样连通双链的点
  3. 像1 → \rightarrow 4路径上的点
  4. 1上那个三叉点
  5. 0 → \rightarrow 1路径上的点

f [ i ] [ j ] f[i][j] f[i][j] ( 1 ≤ j ≤ 5 1 \leq j \leq 5 1j5) 表示当前联通块顶点 i i i 为类型 j j j 的方案数。
分别对每一种点进行dp:

f [ u ] [ 1 ] = 1 + ∑ v ⊆ s o n [ u ] f [ v ] [ 1 ] f[u][1]=1+ \sum_{v \subseteq son[u]} f[v][1] f[u][1]=1+vson[u]f[v][1]
直接从子节点处转移上来
f [ u ] [ 2 ] = ∑ v 1 ⊆ s o n [ u ] ∑ v 2 ⊆ s o n [ u ] f [ v 1 ] [ 1 ] f [ v 2 ] [ 1 ] f[u][2]=\sum_{v_1 \subseteq son[u]}\sum_{v_2 \subseteq son[u]}f[v_1][1]f[v_2][1] f[u][2]=v1son[u]v2son[u]f[v1][1]f[v2][1]
由子节点的不同的两条链转移过来,这里的 v 1 v_1 v1, v 2 v_2 v2 是无序的(可以假定 v 1 v_1 v1 u u u 的儿子中的次序小于 v 2 v_2 v2),采用前缀和优化
f [ u ] [ 3 ] = f [ u ] [ 2 ] + ∑ v ⊆ s o n [ u ] f [ v ] [ 3 ] f[u][3]=f[u][2]+\sum_{v \subseteq son[u]} f[v][3] f[u][3]=f[u][2]+vson[u]f[v][3]
为了方便统计,将2类点加入3类点进行统计,再将子节点的贡献加上
s u m 1 = ∑ v 1 ⊆ s o n [ u ] ∑ v 2 ⊆ s o n [ u ] ∑ v 3 ⊆ s o n [ u ] f [ v 1 ] [ 1 ] f [ v 2 ] [ 1 ] f [ v 3 ] [ 3 ] sum1=\sum_{v_1 \subseteq son[u]}\sum_{v_2 \subseteq son[u]}\sum_{v_3 \subseteq son[u]}f[v_1][1]f[v_2][1]f[v_3][3] sum1=v1son[u]v2son[u]v3son[u]f[v1][1]f[v2][1]f[v3][3]
s u m 2 = ∑ v 1 ⊆ s o n [ u ] ∑ v 2 ⊆ s o n [ u ] ∑ v 3 ⊆ s o n [ u ] f [ v 1 ] [ 1 ] f [ v 2 ] [ 3 ] f [ v 3 ] [ 1 ] sum2=\sum_{v_1 \subseteq son[u]}\sum_{v_2 \subseteq son[u]}\sum_{v_3 \subseteq son[u]}f[v_1][1]f[v_2][3]f[v_3][1] sum2=v1son[u]v2son[u]v3son[u]f[v1][1]f[v2][3]f[v3][1]
s u m 3 = ∑ v 1 ⊆ s o n [ u ] ∑ v 2 ⊆ s o n [ u ] ∑ v 3 ⊆ s o n [ u ] f [ v 1 ] [ 3 ] f [ v 2 ] [ 1 ] f [ v 3 ] [ 1 ] sum3=\sum_{v_1 \subseteq son[u]}\sum_{v_2 \subseteq son[u]}\sum_{v_3 \subseteq son[u]}f[v_1][3]f[v_2][1]f[v_3][1] sum3=v1son[u]v2son[u]v3son[u]f[v1][3]f[v2][1]f[v3][1]
f [ u ] [ 4 ] = s u m 1 + s u m 2 + s u m 3 f[u][4]=sum1+sum2+sum3 f[u][4]=sum1+sum2+sum3
跟图中一样,枚举此点的三个子节点,将其中一个作为3类点,剩下两个作为1类点,将方案数进行统计,同理,这里的 v 1 v_1 v1, v 2 v_2 v2, v 3 v_3 v3 是无序的,采用前缀和优化【这个地方很容易写错,我当时写的时候代码挂了好久,最后发现是这个地方出锅了(悲伤.jpg)】
f [ u ] [ 5 ] = ∑ v ⊆ s o n [ u ] f [ v ] [ 4 ] + ∑ v ⊆ s o n [ u ] f [ v ] [ 5 ] f[u][5]=\sum_{v \subseteq son[u]} f[v][4]+\sum_{v \subseteq son[u]} f[v][5] f[u][5]=vson[u]f[v][4]+vson[u]f[v][5]
一种使 u u u 的子节点作为图中1部分最下面的那个点,另一种使 v v v作为0到1路径上的一点,分别进行转移即可

时间复杂度 O ( n ) O(n) O(n)

代码
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<vector>
using namespace std;
const long long mod=1e9+7;
vector<int> son[2001];
int n,x;
long long f[2001][6],ans,pre1,pre3,pre11,pre13,pre31;
void dp(int u){
	for(int v:son[u]) dp(v);
	f[u][1]=1;
	for(int v:son[u])
	    f[u][1]+=f[v][1];
	pre1=0;
	for(int v:son[u]){
		f[u][2]=(f[u][2]+1ll*f[v][1]*pre1)%mod;
		pre1=(pre1+f[v][1])%mod;
	}
	f[u][3]=f[u][2];
	for(int v:son[u]){
		f[u][3]=(f[u][3]+f[v][3])%mod;
	}
	pre1=pre3=pre11=pre13=pre31=0;
	for(int v:son[u]){//这里小心点写
		f[u][4]=(f[u][4]+1ll*pre13*f[v][1]+1ll*pre31*f[v][1]+1ll*pre11*f[v][3])%mod;
		pre13=(pre13+1ll*pre1*f[v][3])%mod;
		pre11=(pre11+1ll*pre1*f[v][1])%mod;
		pre31=(pre31+1ll*pre3*f[v][1])%mod;
		pre1=(pre1+f[v][1])%mod;
		pre3=(pre3+f[v][3])%mod;
	}
	for(int v:son[u]){
		f[u][5]=(f[u][5]+f[v][4])%mod;
		f[u][5]=(f[u][5]+f[v][5])%mod;
	}
}
int main(){
	//freopen("block.in","r",stdin);
	//freopen("block.out","w",stdout);
	while(scanf("%d",&x)!=EOF){
		n+=1;
		son[x].push_back(n);
	}
	/*scanf("%d",&n);
	for(int i=1;i<=n;++i){
		scanf("%d",&x);
		son[x].push_back(i);
	}*/
	dp(0);
	for(int i=0;i<=n;++i)
	    ans=(ans+f[i][5])%mod;
	printf("%lld\n",ans);
	return 0;
}

B. 动漫排序

【题目描述】

柯柯最近迷上了日本动漫,每天都有无数部动漫的更新等着他去看,所以他必须将所有的动漫排个顺序。共有 n n n 部动漫等待排序。

当然,虽然有无数部动漫,但除了 1 1 1 号动漫,每部动漫都有且仅有一部动漫是它的前传(父亲),也就是说,所有的动漫形成一个树形结构。

而动漫的顺序必须满足以下两个限制:

  • 一部动漫的所有后继(子孙)都必须排在它的后面。
  • 对于同一部动漫的续集(孩子),柯柯喜爱度高的须排在前面。

光排序柯柯还不爽,他想知道一共有多少种排序方案,并且输出它 m o d 10007 mod 10007 mod10007 的答案。

【输入格式】

第一行 T T T 表示数据组数。

接下来每组数据,第一行一个正整数 n n n,表示有多少部动漫等待排序。

接下来 n n n 行每行,第一个数 t o t tot tot 表示这部动漫有多少部续集,接下来 t o t tot tot 个数按照柯柯喜爱从大到小给出它的续集的编号。

【输出格式】

每组数据一行一个数,表示答案 m o d 10007 mod 10007 mod10007 的结果。

【数据范围与提示】

对于 100 % 100\% 100% 的数据, 1 ≤ n ≤ 2000 1 \leq n \leq 2000 1n2000 1 ≤ T ≤ 10 1 \leq T \leq 10 1T10

想法

观察题目,发现每一部动漫的后继只有它的子节点中喜爱值最大的那一部漫画和它的兄弟中排在它后一位喜爱值的漫画两种可能,那我们可以将原树改造成一棵二叉树,方便我们进行dp。

然后我们对每个节点进行分析:如果它没有子节点(叶子节点),那么直接将它的方案数赋为1;如果它有一个子节点,它别无选择地直接将子节点的状态转移过来。

现在主要是处理有两个子节点的节点:思考一下,左右两个子树的方案数并不影响,可以将两棵子树的方案数相乘,表示在不干扰原子树内的方案顺序的方案数,然后我们还可以将两个子树内的方案数打乱重组,设子树 u u u 的节点数为 s i z [ u ] siz[u] siz[u],则由数学知识可得,打乱重组的方案数为 C s i z [ u ] − 1 m i n ( s i z [ v 1 ] , s i z [ v 2 ] ) C_{siz[u]-1}^{min(siz[v_1],siz[v_2])} Csiz[u]1min(siz[v1],siz[v2]),将 s i z [ u ] siz[u] siz[u]减1的原因是 u u u 必然比子节点先看,那么只剩下 s i z [ u ] − 1 siz[u]-1 siz[u]1 个位置插入剩下的节点。

组合数可以用 O ( n 2 ) O(n^2) O(n2) 的时间进行初始化,那么总的时间复杂度为 O ( n 2 + T n ) O(n^2+Tn) O(n2+Tn),因为本题的模数为质数,当然可以选择使用阶乘+阶乘逆元计算组合数,时间复杂度为 O ( T n ) O(Tn) O(Tn)

代码
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const long long mod=10007;
struct node{
	int nxt,v;
}e[40001];
int cnt,head[40001];
int n,T,siz[10001],f[10001];
long long ans,c[1001][1001];//记得开long long
void add(int u,int v){//链式前向星存储边的信息
	e[++cnt].nxt=head[u];
	e[cnt].v=v;
	head[u]=cnt;
}
void dfs(int u){
	int tot=0,p[3];
	siz[u]=1;
	for(int i=head[u];i;i=e[i].nxt){
		p[++tot]=e[i].v;
		dfs(e[i].v);
		siz[u]+=siz[e[i].v];
	}
	if(tot==0){
		f[u]=1;return;
	}
	if(tot==1){
		f[u]=f[p[1]];
		return;
	}
	f[u]=(((f[p[1]]*f[p[2]])%mod)*c[siz[u]-1][min(siz[p[1]],siz[p[2]])])%mod;//核心
}
int main(){
	//freopen("a.in","r",stdin);
	//freopen("a.out","w",stdout);
	scanf("%d",&T);
	for(int i=0;i<=1000;++i)
	    for(int j=0;j<=i;++j){
	    	if(!j) c[i][j]=1;
	    	else c[i][j]=(c[i-1][j]+c[i-1][j-1])%mod;
		}
	while(T--){
		cnt=0;
		memset(head,0,sizeof(head));
		memset(f,0,sizeof(f));
		scanf("%d",&n);
		for(int i=1,tot;i<=n;++i){
			scanf("%d",&tot);
			if(!tot) continue;
			int lst=i,x;
			while(tot--){
				scanf("%d",&x);
				add(lst,x);
				lst=x;
			}
		}
		dfs(1);
		printf("%lld\n",f[1]);
	}
	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值