Codeforces 1252B

Codeforces 1252B

Tag:树上DP,数学优化,图论,动态规划


题目分析

题目大意

给定一个无向树,求任意两个路径的终点都不向连的路径集个数。其中点数小于等于1e5

做法猜测

树上的问题,还是计数,复杂度要求在O(nlogn)之内,直接考虑树上DP。


动态规划设计

前置说明

记当前节点为 u u u,子节点为 v i v_{i} vi
使用的编译指令为

g++ filename.cpp -o filename.exe -O2 -std=c++11

预处理头文件与常量定义为

#include<bits/stdc++.h>
using namespace std;
const int N = 1e5+17,M = 3e5+17;

存图方式为链式前向星,其中存图的代码可以参考下述

int n,tails,fr[N],to[M],nxt[M];
void add(int f,int t){
	nxt[++tails] = fr[f];
	fr[f] = tails;
	to[tails] = t;
	return;
}

遍历的方式为

void DFS(int u,int fa){
	for(int zj=fr[u],v;zj;zj=nxt[zj]){
		if((v=to[zj]) == fa)	continue;
		DFS(v,start);
	}
	return;
}

状态说明

记本节点为节点 u u u,其子节点为 v v v
记每个节点存在的下列状态分别为 0 / 1 / 2 0/1/2 0/1/2
同时使用 F [ N ] [ 3 ] F[N][3] F[N][3]来记录每个状态

long long MOD = 1e9+7,F[N][4];

不要问我怎么出来的这个状态,Three Hours淦出来的

“自闭”状态
定义 u u u 节点已经连接向两个子节点,且其他节点均处在满足题意的状态为状态0。

“不满”状态
定义 u u u 节点已经连接了不足两个子节点;且如果 u u u 节点连向父节点后,以 u u u 为根的树满足题目中的要求的状态为状态1。
简单来说,就是如果 u u u 与其父节点连接时满足题意的状态。

“自信”状态
定义 u u u 节点在不考虑父节点的情况下,满足题意的状态为状态2。

状态分析

状态转移方程的确立

“自闭”状态

一个“自闭”状态的形成一定有两个“不满”状态影响,且其他子节点都在“自信”状态中。
于是我们得到了初步的计算式:
F [ u ] [ 0 ] = ∑ v 1 , v 2 ∈ s o n u , v 1 ≠ v 2 ( F [ v 1 ] [ 1 ] ∗ F [ v 2 ] [ 1 ] ∗ ∏ v ∈ s o n u , v ≠ v 1 , v ≠ v 2 F [ v ] [ 2 ] ) F[u][0] = \sum_{v1,v2 \in son_{u} , v1 \ne v2} (F[v1][1] * F[v2][1] * \prod_{v \in son_{u},v \ne v1, v \ne v2}F[v][2]) F[u][0]=v1,v2sonu,v1=v2(F[v1][1]F[v2][1]vsonu,v=v1,v=v2F[v][2])

“不满”状态
一个“不满”状态可能的组成情况有下列两种:

  • u u u 与一个 v v v 的“不满”状态相连接,其它节点均处在“自信”状态。
  • u u u 自成一体,所有 v v v 都处在“自闭”状态。
    这个状态需要说明一下,如果有 v v v 不处在“自闭”状态,则即使 u u u 与其父节点相连的情况下,这个子树 v v v一定与 u u u 各处在一个路径的端点,而此两点相连,不满足题意。因此所有 v v v 均应当处在“自闭”状态。

F [ u ] [ 1 ] = ∑ v 1 ∈ s o n u ( F [ v 1 ] [ 1 ] ∗ ∏ v ∈ s o n u , v ≠ v 1 F [ v ] [ 2 ] ) + ∏ v ∈ s o n u F [ v ] [ 0 ] F[u][1] = \sum_{v1 \in son_{u}}(F[v1][1]* \prod_{v \in son_{u},v \ne v1}F[v][2]) + \prod_{v \in son_{u}}F[v][0] F[u][1]=v1sonu(F[v1][1]vsonu,v=v1F[v][2])+vsonuF[v][0]

“自信”状态

一个“自信”状态的构成,也可以分为两部分。

  • 这个节点处在“自闭”状态。
  • 这个节点处在所有 v v v 都“自闭”的状态。
  • 这个节点处在与一个“不满”子节点相连,其他子节点均“自闭”的状态

F [ u ] [ 2 ] = F [ u ] [ 0 ] + ∏ v ∈ s o n u F [ v ] [ 0 ] + ∑ v 1 ∈ s o n u ( F [ v 1 ] [ 1 ] ∗ ∏ v ∈ s o n u , v ≠ v 1 F [ v ] [ 0 ] ) F[u][2] = F[u][0] + \prod_{v \in son_{u}}F[v][0] + \sum_{v1 \in son_{u}}(F[v1][1] *\prod_{v \in son_{u},v \ne v1}F[v][0]) F[u][2]=F[u][0]+vsonuF[v][0]+v1sonu(F[v1][1]vsonu,v=v1F[v][0])

初始状态转移的时空复杂度
时间复杂度

对于每一个节点,我们朴素计算每个单独的计算单元。

∏ v ∈ s o n u F [ v ] [ 2 ] \prod_{v \in son_{u}}F[v][2] vsonuF[v][2]

∏ v ∈ s o n u F [ v ] [ 0 ] \prod_{v \in son_{u}}F[v][0] vsonuF[v][0]

时间复杂度为 O ( s o n u ) O(son_{u}) O(sonu)

然后枚举 v 1 v1 v1,计算出

∑ v 1 ∈ s o n u ( F [ v 1 ] [ 1 ] ∗ ∏ v ∈ s o n u , v ≠ v 1 F [ v ] [ 0 ] ) \sum_{v1 \in son_{u}}(F[v1][1] *\prod_{v \in son_{u},v \ne v1}F[v][0]) v1sonu(F[v1][1]vsonu,v=v1F[v][0])

∑ v 1 ∈ s o n u ( F [ v 1 ] [ 1 ] ∗ ∏ v ∈ s o n u , v ≠ v 1 F [ v ] [ 2 ] ) \sum_{v1 \in son_{u}}(F[v1][1]* \prod_{v \in son_{u},v \ne v1}F[v][2]) v1sonu(F[v1][1]vsonu,v=v1F[v][2])

时间复杂度为 O ( s o n u ) O(son_{u}) O(sonu)

枚举 v 1 v1 v1 的同时,枚举 v 2 v2 v2,计算

∑ v 1 , v 2 ∈ s o n u , v 1 ≠ v 2 ( F [ v 1 ] [ 1 ] ∗ F [ v 2 ] [ 1 ] ∗ ∏ v ∈ s o n u , v ≠ v 1 , v ≠ v 2 F [ v ] [ 2 ] ) \sum_{v1,v2 \in son_{u} , v1 \ne v2} (F[v1][1] * F[v2][1] * \prod_{v \in son_{u},v \ne v1, v \ne v2}F[v][2]) v1,v2sonu,v1=v2(F[v1][1]F[v2][1]vsonu,v=v1,v=v2F[v][2])

时间复杂度 O ( s o n u 2 ) O(son_{u}^{2}) O(sonu2)

总时间复杂度 O ( n 2 ) O(n^{2}) O(n2)
显然超时。

空间复杂度

So easy,I won’t want to do this


动态规划方程的一种优化方式

基本思想

借鉴之前优化连乘算式的方法,我们考虑一个一个加入新的元素,并计算新加入元素的贡献。

举个例子

目标方程

∑ v 1 ∈ s o n u ( F [ v 1 ] [ 1 ] ∗ ∏ v ∈ s o n u , v ≠ v 1 F [ v ] [ 0 ] ) \sum_{v1 \in son_{u}}(F[v1][1] *\prod_{v \in son_{u},v \ne v1}F[v][0]) v1sonu(F[v1][1]vsonu,v=v1F[v][0])

变量声明

v 0 v0 v0 为新遍历到的节点
s o n 1 u son1_{u} son1u 为在 v 0 v0 v0 前进入的子节点集合

优化分析

我们假设新加入了一个子节点 v 0 v0 v0

那么考虑 v 0 v0 v0 对在上式中 v 1 v1 v1 取其它节点的贡献

F [ v 0 ] [ 0 ] ∗ ∑ v 2 ∈ s o n 1 u ( F [ v 2 ] [ 1 ] ∗ ∏ v ∈ s o n 1 u , v ≠ v 2 F [ v ] [ 0 ] ) F[v0][0] * \sum_{v2 \in son1_{u}}(F[v2][1]*\prod_{v \in son1_{u},v \ne v2}F[v][0]) F[v0][0]v2son1u(F[v2][1]vson1u,v=v2F[v][0])

考虑 v 0 v0 v0 对上式中 v 1 v1 v1 v 0 v0 v0 时的贡献

F [ v 0 ] [ 1 ] ∗ ∏ v 2 ∈ s o n 1 u F [ v 2 ] [ 0 ] F[v0][1] *\prod_{v2 \in son1_{u}}F[v2][0] F[v0][1]v2son1uF[v2][0]

将以上两个式子相加即可得到加入一个节点 v 0 v0 v0 后的结果。

F [ v 0 ] [ 0 ] ∗ ∑ v 2 ∈ s o n 1 u ( F [ v 2 ] [ 1 ] ∗ ∏ v ∈ s o n 1 u , v ≠ v 2 F [ v ] [ 0 ] ) + F [ v 0 ] [ 1 ] ∗ ∏ v 2 ∈ s o n 1 u F [ v 2 ] [ 0 ] ) F[v0][0] * \sum_{v2 \in son1_{u}}(F[v2][1]*\prod_{v \in son1_{u},v \ne v2}F[v][0]) + F[v0][1] *\prod_{v2 \in son1_{u}}F[v2][0]) F[v0][0]v2son1u(F[v2][1]vson1u,v=v2F[v][0])+F[v0][1]v2son1uF[v2][0])

此时我们发现一个重要的事情,即上面

∑ v 2 ∈ s o n 1 u ( F [ v 2 ] [ 1 ] ∗ ∏ v ∈ s o n 1 u , v ≠ v 2 F [ v ] [ 0 ] ) \sum_{v2 \in son1_{u}}(F[v2][1]*\prod_{v \in son1_{u},v \ne v2}F[v][0]) v2son1u(F[v2][1]vson1u,v=v2F[v][0])

不正是我们没有加入 v 0 v0 v0 时的结果吗?

所以我们不妨设计这样两个东西

s 1 = ∑ v 2 ∈ s o n 1 u ( F [ v 2 ] [ 1 ] ∗ ∏ v ∈ s o n 1 u , v ≠ v 2 F [ v ] [ 0 ] ) s1 = \sum_{v2 \in son1_{u}}(F[v2][1]*\prod_{v \in son1_{u},v \ne v2}F[v][0]) s1=v2son1u(F[v2][1]vson1u,v=v2F[v][0])

s 2 = ∏ v ∈ s o n 1 u F [ v ] [ 0 ] s2 = \prod_{v \in son1_{u}}F[v][0] s2=vson1uF[v][0]

代码实现

于是在代码中,我们就可以这样维护这两个变量

void DFS(int u,int fa){
	long long s1=0,s2=1;//Notice: s2 = 1
	for(int zj=fr[u],v;zj;zj=nxt[zj]){
		if((v=to[zj])==fa)	continue;
		DFS(v,u);
		s1 = (s1*F[v][0] + s2*F[v][1])%MOD;
		s2 = (s2*F[v][0])%MOD;
	}
}
时空复杂度

时间复杂度

由于每次遍历到新的子节点都做了常数次操作,因此我们

推广一下

简单改名

我们通过改改名字就能实现下面这个式子

∑ v 1 ∈ s o n u ( F [ v 1 ] [ 1 ] ∗ ∏ v ∈ s o n u , v ≠ v 1 F [ v ] [ 2 ] ) \sum_{v1 \in son_{u}}(F[v1][1] *\prod_{v \in son_{u},v \ne v1}F[v][2]) v1sonu(F[v1][1]vsonu,v=v1F[v][2])

记住这个式子👆叫做 w 2 w2 w2
记住这个式子👆叫做 w 2 w2 w2
记住这个式子👆叫做 w 2 w2 w2

这么简单我就直接上代码了

void DFS(int u,int fa){
	long long w2=0,w3=1,s1=0,s2=1;
	for(int zj=fr[u],v;zj;zj=nxt[zj]){
		if((v=to[zj])==fa)	continue;
		else				DFS(v,u);
		w2 = (w2*F[v][2] + w3*F[v][1])%MOD;
		w3 = (w3*F[v][2])%MOD;
		s1 = (s1*F[v][0] + s2*F[v][1])%MOD;
		s2 = (s2*F[v][0])%MOD;
	}
}
双变量枚举

做完上面的这些工作之后,我们就剩下一个式子没有处理了。
先把它给供起来,真的脑筋急转弯

∑ v 1 , v 2 ∈ s o n u , v 1 ≠ v 2 ( F [ v 1 ] [ 1 ] ∗ F [ v 2 ] [ 1 ] ∗ ∏ v ∈ s o n u , v ≠ v 1 , v ≠ v 2 F [ v ] [ 2 ] ) \sum_{v1,v2 \in son_{u} , v1 \ne v2} (F[v1][1] * F[v2][1] * \prod_{v \in son_{u},v \ne v1, v \ne v2}F[v][2]) v1,v2sonu,v1=v2(F[v1][1]F[v2][1]vsonu,v=v1,v=v2F[v][2])

此处建议大家手动推一下这个式子的优化。

还是老规矩,考虑新加入节点对原先节点的影响。

符号说明

v 0 v0 v0 新加入节点
s o n 1 u son1_{u} son1u v 0 v0 v0 之前加入的节点

开工动手

既然原始式子中取了两个变量,那我们不妨假设 v 1 < v 2 v1 < v2 v1<v2
如果原始式子中的 v 2 v2 v2 v 0 v0 v0 的话,我们考虑其对答案更新的贡献

F [ v 0 ] [ 1 ] ∗ ∑ v 3 ∈ s o n 1 u ( F [ v 3 ] [ 1 ] ∗ ∏ v ∈ s o n 1 u , v ≠ v 3 F [ v ] [ 2 ] ) F[v0][1] * \sum_{v3 \in son1_{u}} (F[v3][1] * \prod_{v \in son1_{u},v \ne v3}F[v][2]) F[v0][1]v3son1u(F[v3][1]vson1u,v=v3F[v][2])

如果原始式子中 v 1 v1 v1 v 2 v2 v2 均不取 v 0 v0 v0 的话,考虑其对答案更新的贡献

F [ v 0 ] [ 2 ] ∗ ∑ v 1 , v 2 ∈ s o n 1 u , v 1 ≠ v 2 ( F [ v 1 ] [ 1 ] ∗ F [ v 2 ] [ 1 ] ∗ ∏ v ∈ s o n 1 u , v ≠ v 1 , v ≠ v 2 F [ v ] [ 2 ] ) F[v0][2] * \sum_{v1,v2 \in son1_{u} , v1 \ne v2} (F[v1][1] * F[v2][1] * \prod_{v \in son1_{u},v \ne v1, v \ne v2}F[v][2]) F[v0][2]v1,v2son1u,v1=v2(F[v1][1]F[v2][1]vson1u,v=v1,v=v2F[v][2])

奇妙发现1

我们看下面这个式子跟 w 2 w2 w2 有啥关系吗?

F [ v 0 ] [ 1 ] ∗ ∑ v 3 ∈ s o n 1 u ( F [ v 3 ] [ 1 ] ∗ ∏ v ∈ s o n 1 u , v ≠ v 3 F [ v ] [ 2 ] ) F[v0][1] * \sum_{v3 \in son1_{u}} (F[v3][1] * \prod_{v \in son1_{u},v \ne v3}F[v][2]) F[v0][1]v3son1u(F[v3][1]vson1u,v=v3F[v][2])

嘻嘻嘻,相信你也发现了,上面这个式子等价于下面的这个

F [ v 0 ] [ 1 ] ∗ ∑ v 3 ∈ s o n 1 u ( F [ v 3 ] [ 1 ] ∗ ∏ v ∈ s o n 1 u , v ≠ v 3 F [ v ] [ 2 ] )    ⟺    F [ v 0 ] [ 1 ] ∗ w 2 F[v0][1] * \sum_{v3 \in son1_{u}} (F[v3][1] * \prod_{v \in son1_{u},v \ne v3}F[v][2]) \iff F[v0][1]*w2 F[v0][1]v3son1u(F[v3][1]vson1u,v=v3F[v][2])F[v0][1]w2

奇妙发现2

我们看下面这个式子是什么

∑ v 1 , v 2 ∈ s o n 1 u , v 1 ≠ v 2 ( F [ v 1 ] [ 1 ] ∗ F [ v 2 ] [ 1 ] ∗ ∏ v ∈ s o n 1 u , v ≠ v 1 , v ≠ v 2 F [ v ] [ 2 ] ) \sum_{v1,v2 \in son1_{u} , v1 \ne v2} (F[v1][1] * F[v2][1] * \prod_{v \in son1_{u},v \ne v1, v \ne v2}F[v][2]) v1,v2son1u,v1=v2(F[v1][1]F[v2][1]vson1u,v=v1,v=v2F[v][2])

这不正是在 v 0 v0 v0 之前的所有点的结果吗?

我们再拿个 w 1 w1 w1 来维护这个结果,于是可以得到下段程序

void DFS(int u,int fa){
	long long w1=0,w2=0,w3=1,s1=0,s2=1;
	for(int zj=fr[u],v;zj;zj=nxt[zj]){
		if((v=to[zj])==fa)	continue;
		else				DFS(v,u);
		w1 = (w1*F[v][2] + w2*F[v][1])%MOD;
		w2 = (w2*F[v][2] + w3*F[v][1])%MOD;
		w3 = (w3*F[v][2])%MOD;
		s1 = (s1*F[v][0] + s2*F[v][1])%MOD;
		s2 = (s2*F[v][0])%MOD;
	}
}

合并计算结果

由上面式子

F [ u ] [ 0 ] = ∑ v 1 , v 2 ∈ s o n u , v 1 ≠ v 2 ( F [ v 1 ] [ 1 ] ∗ F [ v 2 ] [ 1 ] ∗ ∏ v ∈ s o n u , v ≠ v 1 , v ≠ v 2 F [ v ] [ 2 ] ) F[u][0] = \sum_{v1,v2 \in son_{u} , v1 \ne v2} (F[v1][1] * F[v2][1] * \prod_{v \in son_{u},v \ne v1, v \ne v2}F[v][2]) F[u][0]=v1,v2sonu,v1=v2(F[v1][1]F[v2][1]vsonu,v=v1,v=v2F[v][2])

F [ u ] [ 1 ] = ∑ v 1 ∈ s o n u ( F [ v 1 ] [ 1 ] ∗ ∏ v ∈ s o n u , v ≠ v 1 F [ v ] [ 2 ] ) + ∏ v ∈ s o n u F [ v ] [ 0 ] F[u][1] = \sum_{v1 \in son_{u}}(F[v1][1]* \prod_{v \in son_{u},v \ne v1}F[v][2]) + \prod_{v \in son_{u}}F[v][0] F[u][1]=v1sonu(F[v1][1]vsonu,v=v1F[v][2])+vsonuF[v][0]

F [ u ] [ 2 ] = F [ u ] [ 0 ] + ∏ v ∈ s o n u F [ v ] [ 0 ] + ∑ v 1 ∈ s o n u ( F [ v 1 ] [ 1 ] ∗ ∏ v ∈ s o n u , v ≠ v 1 F [ v ] [ 0 ] ) F[u][2] = F[u][0] + \prod_{v \in son_{u}}F[v][0] + \sum_{v1 \in son_{u}}(F[v1][1] *\prod_{v \in son_{u},v \ne v1}F[v][0]) F[u][2]=F[u][0]+vsonuF[v][0]+v1sonu(F[v1][1]vsonu,v=v1F[v][0])

可以得到下面这段代码

void DFS(int u,int fa){
	long long w1=0,w2=0,w3=1,s1=0,s2=1;
	for(int zj=fr[u],v;zj;zj=nxt[zj]){
		if((v=to[zj])==fa)	continue;
		else				DFS(v,u);
		w1 = (w1*F[v][2] + w2*F[v][1])%MOD;
		w2 = (w2*F[v][2] + w3*F[v][1])%MOD;
		w3 = (w3*F[v][2])%MOD;
		s1 = (s1*F[v][0] + s2*F[v][1])%MOD;
		s2 = (s2*F[v][0])%MOD;
	}
	F[u][0] = w1;	
	F[u][1] = (w2 + s2)%MOD;;
	F[u][2] = (F[u][0] + s1 + s2)%MOD;
}

完整代码

主函数那么简单,我就不用说明了吧

#include<bits/stdc++.h>
using namespace std;
const int N = 1e5+17,M = 3e5+17;
long long MOD = 1e9+7,F[N][4];
int n,tails,fr[N],to[M],nxt[M];
void add(int f,int t){
	nxt[++tails] = fr[f];
	fr[f] = tails;
	to[tails] = t;
}
void DFS(int u,int fa){
	long long w1=0,w2=0,w3=1,s1=0,s2=1;
	for(int zj=fr[u],v;zj;zj=nxt[zj]){
		if((v=to[zj])==fa)	continue;
		else				DFS(v,u);
		w1 = (w1*F[v][2] + w2*F[v][1])%MOD;
		w2 = (w2*F[v][2] + w3*F[v][1])%MOD;
		w3 = (w3*F[v][2])%MOD;
		s1 = (s1*F[v][0] + s2*F[v][1])%MOD;
		s2 = (s2*F[v][0])%MOD;
	}
	F[u][0] = w1;	
	F[u][1] = (w2 + s2)%MOD;;
	F[u][2] = (F[u][0] + s1 + s2)%MOD;
}
int main(){
	scanf("%d",&n);
	for(int i=1,p1,p2;i<n;++i){
		scanf("%d %d",&p1,&p2);
		add(p1,p2),add(p2,p1);
	}
	DFS(1,0),printf("%I64d",F[1][2]);
	return 0;
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值