2/4 以边为对象的树形dp练习等学习总结

image
一个不知道有啥意义的图。

以边为对象的树形dp习题

力扣 687. 最长同值路径

\(H_2O是\!页,禾\!少 扌\!卓\)

\(dp[i]\) 为以 \(i\) 为根的子树中,最长的同值路径。

如果 \(c[u]==c[ls]\)\(dp[u]=\max(dp[u],dp[ls]+1)\)

rs 同理。

答案统计

\(ans=\max\lbrace dp[u]\rbrace\)

如果 \(c[ls]==c[rs]\)\(ans=\max(ans,dp[ls]+dp[rs]+1\rbrace)\)

CF1923E Count Paths

\(O(n^2)\) 解法

\(dp_{u,i}\) 表示 \(u\) 的子树内一个颜色为 \(i\) 的点到 \(u\) 的链且路径上没有其他颜色为 \(i\) 的链数量。

如何状态转移?当点 \(u\) 加入一个儿子 \(v\) 时,把 \(v\) 中的链和 \(u\) 拼起来,此时 \(ans+=dp [v,c[u]]\),加入是把其与 \(u\) 中另一条链拼起来,此时 \(ans+=dp[u,x] \times dp[v,x]\),然后需要让 \(dp[u,x]\) 增加 \(dp[v,x]\) 也就是把链算入 \(u\) 的子树贡献中。

\(O(n\log^2{n})\) 正解

注意到将 \(dp[v,x]\) 合并到 \(dp[u,x]\) 上复杂度较高。注意到由于两个状态只要一个为空就不产生变化,因此一个点 \(v\) 的状态数至多为 \(v\) 的子树大小。那么我们使用if else书上启发式合并,每次将小的状态暴力插入大的状态中并更新贡献,并用 map 维护一个点的所有状态。

复杂度是多少呢?每个点至多被插入 \(\log{n}\) 次,加上 map 的 \(\log{n}\) 就是 \(O(n \log^2{n})\)

注意以下几个易错点:

  • 整个过程中因为链一定经过了点 \(u\) 所以 \(dp[u,col[u]]\) 始终需要为 0。
  • 算完所有贡献之后再算从点 \(u\) 自己开始的链,也就是让 \(dp[u,c[u]]\) 为 1。

代码没调出来TwT,写的另一种代码好些的写法。

\(O(n)\) 解法

注意到每种颜色是独立的。这里指的是无论两个颜色相同的节点之间是什么颜色,都不会干扰到后面的统计答案。可以直接把每种颜色分开算,但效率低下,我的做法就是一次dfs统计完答案。

\(cnt[a]\) 表示当前 \(a\) 这种颜色出现并且可以与之合法匹配的节点的个数,然后对整棵树进行dfs。流程如下:

  • 一、在搜索到点 \(u\) 时,答案加上 \(cnt[c[u]]\)

  • 二、遍历 \(u\) 的子节点时,把 \(cnt[c[u]]\) 设为 1。因为路径上不能有其他点的颜色和起点终点相同,所以 \(u\) 的子孙不可能经过 \(u\)\(u\) 的祖先和其他节点匹配,只能与 \(u\) 匹配。只能贡献 1 种方案。

  • 三、在遍历完 \(u\) 的所有子树后,把 \(cnt[c[u]]\) 的值设为其在遍历子树前的值加 1。 因为如果令根节点为 \(u\),按顺序考虑 \(u\) 的子树,每棵子树只有顶部的点(上面没有颜色与之相同的点)到现在的点的路径是合法的,子树与子树之间也是只有顶部的点合法,加 1 就表示加上 \(u\) 这个顶部的点。

每种方案恰好被计算一次。dfs 时间复杂度 \(O(n)\)

AC code
#include <bits/stdc++.h>
using namespace std;
long long ans;
int a[200005],cnt[200005];
vector<int> g[200005];
void dfs(int x,int fa){
	int tmp=cnt[a[x]];
	ans+=tmp;
	for(auto i:g[x]){
		if(i==fa) continue;
		cnt[a[x]]=1;
		dfs(i,x);
	}
	cnt[a[x]]=tmp+1;
}
int main(){
	ios::sync_with_stdio(0);
	cin.tie(0),cout.tie(0);
	int T;
	cin>>T;
	while(T--){
		int n;
		cin>>n;
		memset(cnt,0,sizeof cnt);
		for(int i=1;i<=n;i++) g[i].clear();
		for(int i=1;i<=n;i++) cin>>a[i];
		for(int i=1,x,y;i<n;i++){
			cin>>x>>y;
			g[x].push_back(y);
			g[y].push_back(x);
		}
		ans=0;
		dfs(1,0);
		cout<<ans<<"\n";
	}
	return 0;
}

ICPC 2021 上海站 G.Edge Groups

全称:The 2021 ICPC Asia Shanghai Regional Programming Contest G.Edge Groups(神仙长度)

推式子可得,边数为 \(n\)\(n\) 为偶数) 的菊花图中,可行的方案有 \(n!!\) 种(表示\(n\times(n-2)\times(n-4)\times...\times 2\))。

\(dp[u]\) 表示 \(u\) 节点以及他的子树可组合的方案数,\(cnt[u]\) 为当前节点组合的有效边数。
如果 \(cnt[u]\) 是奇数,那么边无法两两配对,这时 \(u\rightarrow v\) 这条边就只能和 \(v\) 的子树的边做配对了,在计算 \(u\) 的方案数时就要忽略 \(u\rightarrow v\) 这条边。

状态转移:

异常简单,但推式子很难。

\[dp[u]=\bigg(\prod_{v\in son(u)}dp[v]\bigg)\times cnt[u]!! \]

记得取模,开long long。

AC code
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int mod=998244353;
vector<int> g[200005];
int dp[200005],cnt[200005];
void dfs(int x,int fa){
    dp[x]=1;
    for(auto y:g[x]){
        if(y==fa) continue;
        dfs(y,x);
        if(cnt[y]%2==0) cnt[x]++;
        dp[x]=dp[x]*dp[y]%mod;
    }
    for(int i=1;i<=cnt[x];i+=2) dp[x]=dp[x]*i%mod;
}
signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0),cout.tie(0);
    int n;
	cin>>n;
    for(int i=1,x,y;i<n;i++){
        cin>>x>>y;
        g[x].push_back(y);
        g[y].push_back(x);
    }
    dfs(1,0);
    cout<<dp[1];
    return 0;
}

树上背包 \(O(nm^2)\)

下面是例题:[CTSC1997] 选课 的 \(O(nm^2)\) 方法。

\(dp[u][i][j]\) 为在 \(u\) 这里,前 \(i\) 棵子树,共计选择 \(j\) 门课,共可以获得的最大贡献。

然后就是类似背包的转移。

核心代码
for (int i = Head[now]; i; i = G[i].Next){
    if (G[i].v == father)
        continue;
    dfs(G[i].v, now, V - v[now]);
    for (int j = m; j >= v[now]; --j)
        for (int k = 0; k <= j - v[now]; ++k)
            f[now][j] = max(f[now][j], f[now][j - k - v[now]] + f[G[i].v][k] + w[now]);
}

收获

  • 练习了以边为对象的树形dp

  • 初步学习树形背包

posted @ 2026-02-04 18:54  Frums  阅读(6)  评论(0)    收藏  举报