【二次扫描与换根】总结

No.0 前言#

前置知识:树形DP。

No.1 引入&思想#

换根 DP 通常与树形 DP 相联系,在进行树形 DP 时,根节点通常是确定的,解决方案大多是从根节点开始递归。但一类题目不会指定根结点,且根结点的变化会对答案产生影响,暴力枚举每一个节点为根节点又会在时间复杂度上出问题,这时可以考虑使用换根 DP 解决。

换根 DP 的思路是使用两次 DFS,第一次确定以某一节点为根节点时的答案,第二次以刚才确定的根节点出发,在每次递归前都自顶向下进行状态转移,用父节点的状态更新子节点的状态,计算出“换根”后的结果。

接下来以例题为背景分析换根 DP 的思路。

No.2 例题#

[POI2008] STA-Station#

题意清晰明了,不再重复。

考虑转换根节点会发生什么。

如图,当树的根节点从5变成4后,原以4为根的子树的节点深度全部减一,除开这些节点其他节点深度全部加一。由此推导得出状态转移方程:dpv=dpusiv+(nsiv),其中 uv 的父亲节点,dp 代表总深度, siu 代表以 u 为根的子树节点个数。

分析第二个 DFS ,也就是换根的实现过程:

void Tr_DP(ll u, ll fa) {
	for (int i = 0; i < Gra[u].size(); i ++){
		ll v = Gra[u][i];
		if (v == fa){
			continue;
		}
		dp[v] = dp[u] - si[v] + (n - si[v]);//根据父节点u求出以v为根时的答案
		Tr_DP(v, u);//已得到以v为根时的答案,根据此继续解决v的子节点
	}
}

[APIO2014] 连珠线#

题目大意: 开始只有一个结点。两种操作:可以用一条红线将一个新点连向旧点。或者是选择一条红线,用一个新点和两条蓝线连接原先红线连接的两个点。现在给出最终的连线情况,问蓝线可能的最大长度是多少。

分析:因为始终出现的新节点会只连上一个旧节点,或在两个已经有关系的旧点中间出现,可以分析得出最后的图为一棵树(严谨看待,如果题目没有说明,应该会出现将树认成图的情况)。

所以蓝线的连线情况只可能为两种: fausonson1uson2

但如图,如果为后者,也就是 edge2,3 ,edge2,6 为蓝边,就可以 3 或 6 号节点为根,将原图转换为前者,所以我们只考虑第一种情况。

考虑设计状态:

dpi,0i 节点为蓝线中的 u 节点时最大值

dpi,1i 节点只与子节点用红线连接最大值

dpi,2i 节点为蓝线中的父节点时最大值

不难发现,dpi,1,dpi,2 都与接下来的转移结果没有影响,换句话说, i 节点为蓝线中的父节点时可以与多条蓝线连接,所以可以合并为: dpi,1i 节点不为蓝线中的 u 节点时最大值。

对于 dpi,0i 节点与子节点连线可为红线,也可为蓝线,所以:

dpi,0=v,fav=umax(dpv,0,dpv,1+weightu,v)

对于 dpi,1i 节点与其中一个且仅有一个子节点连线为蓝线肯定选择对答案贡献最大的一个,与其他子节点连线可为红线可为蓝线中的fa,所以:

dpi,1=dpi,0+max(dpv,0+weightu,vmax(dpv,0,dpv,1+weightu,v))

记录 mx 数组为 max(dpv,0+weightu,vmax(dpv,0,dpv,1+weightu,v))p 数组为 v 节点的位置,mn 为次大的 dpv,0+weightu,vmax(dpv,0,dpv,1+weightu,v)

根据状态转移方程可以轻松得出以某一结点为根时的答案,接下来就考虑换根。

考虑两种情况:

情况一:换的子节点不为根节点的 p 数组的值,即这个点不是根节点为蓝边中间节点时的儿子,就应该将 dpu,0v 的贡献减掉,同时,对于新的根节点,同样可以与旧的根节点组成蓝边,需重新计算贡献。

情况二:换的子节点是根节点的 p 数组的值,即这个点是根节点为蓝边中间节点时的儿子,那么在减掉此点贡献的同时,需重新计算 dpi,1,即采用次大值更新。这也是为什么要记录次大值数组 mn

两次 DFS 代码:

void Tr_dp(ll u,ll fa) {
	mx[u] = mn[u] = -0x3f3f3f3f3f3f;
	for (int i = 0; i < Gra[u].size(); i ++){
		ll v = Gra[u][i].v, w = Gra[u][i].w;
		if (v == fa){
			continue;
		}
		Tr_dp(v, u);
		ll k = max(dp[v][0], dp[v][1] + w);
		dp[u][0] += k;
		if (dp[v][0] + w - k > mx[u]){
			mn[u] = mx[u];
			mx[u] = dp[v][0] + w - k;
			p[u] = v;
		}
		else if (dp[v][0] + w - k > mn[u]){
			mn[u] = dp[v][0] + w - k;
		}
	}
	dp[u][1] = dp[u][0] + mx[u];
}

void Tr_DP(int u, int fa, int val){
	int k = max(dp2[fa][0], dp2[fa][1] + val);
	dp[u][0] += k;
	if (fa && dp2[fa][0] + val - k > mx[u]){
		mn[u] = mx[u];
		mx[u] = dp2[fa][0] + val - k;
		p[u] = fa;
	}
	else if (fa && dp2[fa][0] + val - k > mn[u]){
		mn[u] = dp2[fa][0] + val - k;
	}
	dp[u][1] = dp[u][0] + mx[u];
	ans = max(ans, dp[u][0]);
	for (int i = 0; i < Gra[u].size(); i ++){
	 	ll v = Gra[u][i].v, w = Gra[u][i].w;
		if (v == fa){
			continue;
		}
		dp2[u][0] = dp[u][0] - max(dp[v][0], dp[v][1] + w);
		if (v == p[u]){
			dp2[u][1] = dp2[u][0] + mn[u];
		}
		else {
			dp2[u][1] = dp2[u][0] + mx[u];
		}
		Tr_DP(v, u, w);
	}
}

[SHOI2014] 概率充电器#

前置知识:概率

大概题意:给你一棵树,树上每一点有直接通电的概率,每条边有导电的概率。求出期望有电节点数。

分析:

不难发现能使一个节点 u 导电有三种情况:

节点 u 自己生电。

儿子节点给节点 u 导电。

父亲节点给节点 u 导电。

难点来了:如何状态转移使 DP 没有后效性?

因为我们发现根节点不受父亲的影响,考虑先忽略父亲节点给节点 u 导电的概率,求出另两种情况使节点 u 导电的概率,也就是从叶结点出发到根节点进行递归。然后再从根节点出发考虑父节点的贡献。

dpu:u 个节点在考虑自身生电的概率其子树导电的概率以后,没电的概率。

u 节点的一个子节点 v 单独拿出来考虑,发现 v 节点不能给 u 节点导电有两种情况:自身没电以及路上断电,相加即为节点 u 不会受到 v 的电流影响的概率。

dpu=(1cou)v,fav=u(dpv+(1dpv)(1weightu,v))

其中 cou 即为 u 能单独生电的概率。

所以我们能够发现除开节点 v ,节点 u 不能生电的概率为 dpudpv+(1dpv)(1weightu,v)

然后考虑 u 节点父节点对 u 的影响。

dp2u:u 个节点在考虑其父节点导电的概率以后,没电的概率。

所以父节点除开 u 节点的影响后没电的概率为 dp2udpudpu+(1dpu)(1weightfau,u), 设为 p

所以 dp2u,即第 u 个节点在考虑其父节点导电的概率以后,没电的概率就为父节点本身没电的概率乘上路上断电的概率。

所以 dp2u=p+(1p)(1weightfau,u)

所以 u 节点没电的概率就为 dpudp2u,有电的概率就为 1dpudp2u

最终的答案即使所有节点有电的概率和。

PS: 注意运算时分母不能为 0。

两次 DFS 代码:

void Tr_dp(ll u, ll fa) {
	dp[u] = 1.0 - co[u] * 1.0 / 100;
	for (int i = 0; i < Gra[u].size(); i ++){
		ll v = Gra[u][i].v;
		db w = Gra[u][i].w * 1.0 / 100;
		if (v == fa){
			continue;
		}
		Tr_dp(v, u);
		dp[u] *= (dp[v] + (1 - dp[v]) * (1 - w));
	}
}

void Tr_DP(ll u, ll fa) {
	db te;
	for (int i = 0; i < Gra[u].size(); i ++){
		ll v = Gra[u][i].v;
		db w = Gra[u][i].w * 1.0 / 100;
		if (v == fa){
			db t;
			if (dp[u] + (1 - dp[u]) * (1 - w) == 0){
				t = dp2[fa] * dp[fa];
			}
			else {
				t = dp2[fa] * dp[fa] / (dp[u] + (1 - dp[u]) * (1 - w));
			}
			dp2[u] = (t + (1 - t) * (1 - w));
			break;
		}
	}
	if (fa == 0){
		dp2[u] = 1;
	}
	for (int i = 0; i < Gra[u].size(); i ++){
		ll v = Gra[u][i].v;
		if (v == fa){
			continue;
		}
		Tr_DP(v, u);
	}
}

No.3 总结#

二次扫描与换根的题目算 DP,需要大量的思维难度,也需要笔和草稿纸的配合。

努力ing

posted @   faith_xy  阅读(39)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 单元测试从入门到精通
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律
more_horiz
keyboard_arrow_up dark_mode palette
选择主题
menu
点击右上角即可分享
微信分享提示