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

一个不知道有啥意义的图。
以边为对象的树形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\) 这条边。
状态转移:
异常简单,但推式子很难。
记得取模,开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
-
初步学习树形背包

浙公网安备 33010602011771号