树形DP题型复盘
一、最长路径问题
(1)边
权最长路径
1.树的最长路径—树形DP
题目链接
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 20010;
int e[N],ne[N],h[N],w[N],idx=0;
int n;
int res=0;
void add(int a,int b,int c)
{
e[idx]=b,w[idx]=c,ne[idx]=h[a],h[a]=idx++;
}
int dfs(int u,int fa)
{
int d1=0,d2=0;
for(int i=h[u];i!=-1;i=ne[i])
{
int j=e[i];
if(j==fa)continue;
int d=dfs(j,u)+w[i];
if(d>=d1)d2=d1,d1=d;
else if(d>d2)d2=d;
}
res=max(d1+d2,res);
return d1;
}
int main()
{
cin>>n;
memset(h, -1, sizeof h);
for(int i=1;i<n;i++)
{
int a,b,c;
cin>>a>>b>>c;
add(a,b,c);
add(b,a,c);
}
dfs(1,-1);
cout<<res<<endl;
}
2.大臣的旅费—非负权值贪心写法
题目链接
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 100010;
int n;
int h[N],e[N*2],w[N*2],ne[N*2],idx;
int dist[N];
void add(int a,int b,int c){
e[idx]=b,w[idx]=c,ne[idx]=h[a],h[a]=idx++;
}
void dfs(int u,int father,int distance){
dist[u]=distance;
for(int i=h[u];i!=-1;i=ne[i]){
int j=e[i];
if(j!=father)
dfs(j,u,distance+w[i]);
}
}
int main()
{
cin>>n;
memset(h,-1,sizeof h);
for(int i=0;i<n-1;i++){
int a,b,c;
cin>>a>>b>>c;
add(a,b,c);
add(b,a,c);
}
dfs(1,-1,0);
int u=1;
for(int i=2;i<=n;i++){
if(dist[u]<dist[i])u=i;
}
dfs(u,-1,0);
for(int i=1;i<=n;i++)
if(dist[u]<dist[i])
u=i;
printf("%lld\n",dist[u]*10+(dist[u]+1ll)*dist[u]/2);
}
(2).点权最长路径
1.树上子链—树形DP
题目链接
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
#define int long long
const int N = 200010,INF=0x3f3f3f3f;
int e[N],ne[N],w[N],h[N],idx=0;
int n;
int f[N];
int res=-INF;
void add(int a,int b)
{
e[idx]=b,ne[idx]=h[a],h[a]=idx++;
}
void dfs(int u,int fa)
{
f[u]=w[u];
for(int i=h[u];i!=-1;i=ne[i])
{
int j=e[i];
if(j==fa)continue;
dfs(j,u);
res=max(res,f[u]+f[j]);
f[u]=max(f[u],f[j]+w[u]);
}
res=max(res,f[u]);
}
signed main()
{
cin>>n;
memset(h,-1,sizeof h);
for(int i=1;i<=n;i++)cin>>w[i];
for(int i=1;i<n;i++)
{
int a,b;
cin>>a>>b;
add(a,b);
add(b,a);
}
dfs(1,-1);
cout<<res<<endl;
}
二、有依赖的背包问题
(1)点权依赖问题
1.有依赖的背包问题—树形DP
题目链接
这道题比较难思考的地方在于按照体积划分,如果像金明的预算方案那一题去进行二进制枚举的话是一定会超时的,因此这个题不按方案划分,按照体积划分。
同时因为依赖性,每次根节点必须选择后才能选择子节点,在最前边加上
f
[
u
]
[
v
[
u
]
]
=
w
[
u
]
f[u][v[u]]=w[u]
f[u][v[u]]=w[u]
有些人是这样写的
f
o
r
(
i
n
t
i
=
v
[
u
]
;
i
<
=
m
;
i
+
+
)
f
[
u
]
[
i
]
=
w
[
u
]
for(int i=v[u];i<=m;i++) f[u][i]=w[u]
for(inti=v[u];i<=m;i++)f[u][i]=w[u]
后面的其实不需要处理,根据贪心思路,体积越小价值越大一定最优,因此只需要让 f [ u ] [ v [ u ] ] = w [ u ] f[u][v[u]]=w[u] f[u][v[u]]=w[u]即可
#include <iostream>
#include <vector>
using namespace std;
const int N = 110;
vector<int>g[N];
int f[N][N];
int n,m;
int v[N],w[N];
void dfs(int u)
{
f[u][v[u]]=w[u];
for(int x : g[u])
{
dfs(x);
for(int j=m;j>=v[u];j--)
for(int k=v[u];k<=j;k++)
f[u][j]=max(f[u][j],f[u][k]+f[x][j-k]);
}
}
int main()
{
int root;
cin>>n>>m;
for(int i=1;i<=n;i++)
{
int p;
cin>>v[i]>>w[i]>>p;
if(p==-1)root=i;
else g[p].push_back(i);
}
dfs(root);
cout<<f[root][m]<<endl;
}
2.选课—超级源点优化的树形DP
题目链接
这里的0点作为超级源点,因此计算时需要把 m + 1 m+1 m+1
#include <iostream>
#include <vector>
using namespace std;
const int N = 310;
vector<int> g[N];
int f[N][N];
int w[N];
int n,m;
void dfs(int u)
{
f[u][1]=w[u];
for(int v : g[u])
{
dfs(v);
for(int j=m+1;j>=1;j--)
for(int k=1;k<=j;k++)
f[u][j]=max(f[u][j],f[u][k]+f[v][j-k]);
}
}
int main()
{
cin>>n>>m;
for(int i=1;i<=n;i++)
{
int p;
cin>>p>>w[i];
g[p].push_back(i);
}
dfs(0);
cout<<f[0][m+1]<<endl;
}
(2)边权依赖问题
1.二叉苹果树
题目链接
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 210;
int e[N],h[N],ne[N],w[N],idx=0;
int n,m;
int f[N][N];
void add(int a,int b,int c)
{
e[idx]=b,w[idx]=c,ne[idx]=h[a],h[a]=idx++;
}
void dfs(int u,int fa)
{
for(int i=h[u];i!=-1;i=ne[i])
{
int j=e[i];
if(j==fa)continue;
dfs(j,u);
for(int j=m;j>=1;j--)
for(int k=0;k<j;k++)
f[u][j]=max(f[u][j],f[u][j-k-1]+f[e[i]][k]+w[i]);
}
}
int main()
{
memset(h, -1, sizeof h);
cin>>n>>m;
for(int i=1;i<n;i++)
{
int a,b,c;
cin>>a>>b>>c;
add(a,b,c);
add(b,a,c);
}
dfs(1,-1);
cout<<f[1][m]<<endl;
}
三、换根DP
1.树的中心
题目链接
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 20010,INF=0x3f3f3f3f;
int n;
int e[N],h[N],ne[N],w[N],idx=0;
int d1[N],d2[N],p[N],up[N];
bool is_leaf[N];
void add(int a,int b,int c)
{
e[idx]=b,w[idx]=c,ne[idx]=h[a],h[a]=idx++;
}
int dfs_d(int u,int fa)
{
d1[u]=d2[u]=-INF;
for(int i=h[u];i!=-1;i=ne[i])
{
int j=e[i];
if(j==fa)continue;
int d=dfs_d(j,u)+w[i];
if(d>=d1[u])
{
d2[u]=d1[u];
d1[u]=d;
p[u]=j;
}
else if(d>d2[u])d2[u]=d;
}
if(d1[u]==-INF)
{
is_leaf[u]=true;
d1[u]=d2[u]=0;
}
return d1[u];
}
int dfs_u(int u,int fa)
{
for(int i=h[u];i!=-1;i=ne[i])
{
int j=e[i];
if(j==fa)continue;
if(p[u]==j)up[j]=max(up[u],d2[u])+w[i];
else up[j]=max(up[u],d1[u])+w[i];
dfs_u(j,u);
}
}
int main()
{
cin>>n;
memset(h,-1,sizeof h);
for(int i=1;i<n;i++)
{
int a,b,c;
cin>>a>>b>>c;
add(a,b,c);
add(b,a,c);
}
dfs_d(1,-1);
dfs_u(1,-1);
int res=INF;
for(int i=1;i<=n;i++)
{
if(is_leaf[i])res=min(res,up[i]);
else res=min(res,max(up[i],d1[i]));
}
cout<<res<<endl;
}
2.Tree
题目链接
来自大佬题解:
#include <iostream>
#include <cstring>
#include <algorithm>
#include <vector>
using namespace std;
#define int long long
const int N = 2000010,mod=1e9+7;
vector<int>g[N];
int n;
int f[N];
int res[N];
int qmi(int a,int b)
{
int res=1;
while(b)
{
if(b&1)res=res*a%mod;
a=a*a%mod;
b>>=1;
}
return res;
}
void dfs1(int u,int fa)
{
f[u]=1;
for(auto j : g[u])
{
if(j==fa)continue;
dfs1(j,u);
f[u]=f[u]*(f[j]+1)%mod;
}
}
void dfs2(int u,int fa)
{
if(fa==-1)res[u]=f[u];
//如果是mod的整数倍没法进行逆元运算,直接暴力求出
else if((f[u]+1)%mod==0)
{
dfs1(u,-1);
res[u]=f[u];
}
else
{
int t=res[fa]%mod*qmi(f[u]+1,mod-2)%mod;
res[u]=f[u]*(t+1)%mod;
}
for(auto j : g[u])
{
if(j==fa)continue;
dfs2(j,u);
}
}
signed main()
{
scanf("%lld",&n);
for(int i=1;i<n;i++)
{
int a,b;
scanf("%lld%lld",&a,&b);
g[a].push_back(b);
g[b].push_back(a);
}
dfs1(1,-1);
dfs2(1,-1);
for(int i=1;i<=n;i++)printf("%lld\n",res[i]);
}
3.[USACO 2018 Feb G]Directory Traversal
题目链接
#include <iostream>
#include <cstring>
#include <algorithm>
#include <vector>
using namespace std;
#define int long long
const int N = 100010;
int n,m;
vector<int>g[N];
int w[N];
int dist[N],leaf[N];
//dist[i]表示根节点到i的距离
//leaf[i]表示i节点的子节点中叶子节点的数量
bool is_leaf[N];
int f[N];
int res,root;
//从根节点开始,叶子节点都在他的下面
void dfs1(int u)
{
if(is_leaf[u])leaf[u]=1,f[root]+=dist[u]-1;
for(auto v:g[u])
{
//这里加一是 多加一个 /
dist[v]=dist[u]+w[v]+1;
dfs1(v);
leaf[u]+=leaf[v];
}
}
void dfs2(int u)
{
for(auto v:g[u])
{
//换根dp转移方程
f[v]=f[u]-(w[v]+1)*leaf[v]+(leaf[root]-leaf[v])*3;
res=min(f[v],res);
dfs2(v);
}
}
signed main()
{
cin>>n;
for(int i=1;i<=n;i++)
{
string s;cin>>s;
w[i]=s.size();
if(s=="bessie")root=i,w[root]=0;
cin>>m;
if(!m)is_leaf[i]=true;
for(int j=1;j<=m;j++)
{
int x;cin>>x;
g[i].push_back(x);
}
}
dfs1(root);
res=f[root];
dfs2(root);
cout<<res<<endl;
}
4.Accumulation Degree
题目链接
#include <iostream>
#include <cstring>
#include <algorithm>
#include <vector>
using namespace std;
#define int long long
typedef pair<int,int>PII;
const int N = 400010,mod=998244353;
int f[N];
int n,m;
int e[N],ne[N],h[N],val[N],idx=0;
void add(int a,int b,int c)
{
e[idx]=b,val[idx]=c,ne[idx]=h[a],h[a]=idx++;
}
void dfs1(int u,int fa)
{
for(int i=h[u];i!=-1;i=ne[i])
{
int v=e[i],w=val[i];
if(v==fa)continue;
dfs1(v,u);
//判断是否为叶子节点
if(ne[h[v]]==-1)f[u]+=w;
else f[u]+=min(f[v],w);
}
}
void dfs2(int u,int fa)
{
for(int i=h[u];i!=-1;i=ne[i])
{
int v=e[i],w=val[i];
if(v==fa)continue;
f[v]+=min(w,f[u]-min(f[v],w));
dfs2(v,u);
}
}
void solve()
{
memset(h,-1,sizeof h);
memset(f,0,sizeof f);
idx=0;
cin>>n;
for(int i=1;i<n;i++)
{
int a,b,c;
cin>>a>>b>>c;
add(a,b,c);
add(b,a,c);
}
dfs1(1,-1);
dfs2(1,-1);
int res=0;
for(int i=1;i<=n;i++)res=max(res,f[i]);
cout<<res<<endl;
}
signed main()
{
int T;
cin>>T;
while(T--)solve();
}
四、树形状态机模型
1.删除一些边符合要求最值问题
Rinne Loves Edges
题目链接
f
[
i
]
:
f[i]:
f[i]:表示 以i为根节点到不了任何一个叶子节点的最小代价
状态转移
选择删除u和v连着的边 即
W
u
v
W_{uv}
Wuv
选择删除使得v到不了任何一个子节点的代价,即
f
[
v
]
f[v]
f[v]
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
#define int long long
const int N = 2000010;
int w[N],e[N],ne[N],h[N],idx=0;
int n,m,s;
int f[N];
void add(int a,int b,int c)
{
e[idx]=b,w[idx]=c,ne[idx]=h[a],h[a]=idx++;
}
void dfs(int u,int fa)
{
for(int i=h[u];i!=-1;i=ne[i])
{
int j=e[i];
if(j==fa)continue;
dfs(j,u);
f[u]+=min(f[j],w[i]);
}
if(ne[h[u]]==-1&&u!=s)f[u]=1e18;
}
signed main()
{
cin>>n>>m>>s;
memset(h,-1,sizeof h);
for(int i=1;i<=m;i++)
{
int a,b,c;
cin>>a>>b>>c;
add(a,b,c);
add(b,a,c);
}
dfs(s,-1);
cout<<f[s]<<endl;
}
2.点
相互看到的状态机模型
电话网络
题目链接
f
[
i
]
[
0
]
:
f[i][0]:
f[i][0]: 第i个结点的父结点被选
f
[
i
]
[
1
]
:
f[i][1]:
f[i][1]: 第i个节点本身被选
f
[
i
]
[
2
]
:
f[i][2]:
f[i][2]: 第i个结点有一个子节点被选
状态转移
f
[
u
]
[
0
]
+
=
m
i
n
(
f
[
v
]
[
1
]
,
f
[
v
]
[
2
]
)
f[u][0] += min(f[v][1],f[v][2])
f[u][0]+=min(f[v][1],f[v][2])
f
[
u
]
[
1
]
+
=
m
i
n
(
f
[
v
]
[
1
]
,
f
[
v
]
[
2
]
,
f
[
v
]
[
0
]
)
f[u][1]+= min(f[v][1],f[v][2],f[v][0])
f[u][1]+=min(f[v][1],f[v][2],f[v][0])
f
[
u
]
[
2
]
f[u][2]
f[u][2]的转移较为复杂,只需要一个子节点被选即可,所以选择一个最好的子节点来选,具体看代码
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 20002;
int e[N],h[N],ne[N],idx=0;
int n;
int f[N][3];
void add(int a,int b)
{
e[idx]=b,ne[idx]=h[a],h[a]=idx++;
}
void dfs(int u,int fa)
{
f[u][1]=1;
for(int i=h[u];i!=-1;i=ne[i])
{
int j=e[i];
if(j==fa)continue;
dfs(j,u);
f[u][0]+=min(f[j][1],f[j][2]);
f[u][1]+=min({f[j][0],f[j][1],f[j][2]});
}
f[u][2]=0x3f3f3f3f;
for(int i=h[u];i!=-1;i=ne[i])
{
int j=e[i];
if(j==fa)continue;
f[u][2]=min(f[u][2],f[u][0]-min(f[j][1],f[j][2])+f[j][1]);
}
}
int main()
{
cin>>n;
memset(h,-1,sizeof h);
int a,b;
for(int i=1;i<n;i++)
{
cin>>a>>b;
add(a,b);
add(b,a);
}
dfs(1,-1);
cout<<min(f[1][1],f[1][2])<<endl;
}
3.边
相互看到的状态机模型
Strategic game
题目链接
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 1510;
int n;
int e[N],ne[N],h[N],idx=0;
bool st[N];
int f[N][2];
void add(int a,int b)
{
e[idx]=b,ne[idx]=h[a],h[a]=idx++;
}
void dfs(int u)
{
f[u][0]=0;
f[u][1]=1;
for(int i=h[u];i!=-1;i=ne[i])
{
int j=e[i];
dfs(j);
f[u][0]+=f[j][1];
f[u][1]+=min(f[j][0],f[j][1]);
}
}
int main()
{
while(cin>>n)
{
idx=0;
memset(h, -1, sizeof h);
memset(f,0,sizeof f);
memset(st,0,sizeof st);
int id,cnt;
for(int i=0;i<n;i++)
{
scanf("%d:(%d)",&id,&cnt);
while(cnt--)
{
int ver;
cin>>ver;
add(id,ver);
st[ver]=true;
}
}
int root=0;
while(st[root])root++;
dfs(root);
cout<<min(f[root][0],f[root][1])<<endl;
}
}
4.数目划分树形DP
[HAOI2015]树上染色
题目链接
大佬题解:
#include <iostream>
#include <cstring>
#include <algorithm>
#include <vector>
using namespace std;
#define int long long
typedef pair<int,int>PII;
const int N = 2030;
vector<PII>g[N];
int f[N][N];
int n,m;
int cnt[N];
int cal(int v,int w,int i,int j)
{
return w*j*(m-j)+w*(n-m-cnt[v]+j)*(cnt[v]-j);
}
void dfs(int u,int fa)
{
cnt[u]=1;
for(auto [v,w]:g[u])
{
if(v==fa)continue;
dfs(v,u);
for(int i=cnt[u];i>=0;i--)
{
for(int j=cnt[v];j>=0;j--)
{
f[u][i+j]=max(f[u][i+j],f[u][i]+f[v][j]+cal(v,w,i,j));
}
}
cnt[u]+=cnt[v];
}
}
signed main()
{
cin>>n>>m;
for(int i=1;i<n;i++)
{
int a,b,c;
cin>>a>>b>>c;
g[a].push_back({b,c});
g[b].push_back({a,c});
}
dfs(1,-1);
cout<<f[1][m]<<endl;
}
蓝魔法师
题目链接
没想到在这里还能看到学长题解hhhh
#include <iostream>
#include <cstring>
#include <algorithm>
#include <vector>
using namespace std;
#define int long long
typedef pair<int,int>PII;
const int N = 2030,mod=998244353;
vector<int>g[N];
int f[N][N];
//f[i][j]表示以i为根节点,每个连通块大小等于j的方案数
int n,m;
int cnt[N];
void dfs(int u,int fa)
{
f[u][1]=cnt[u]=1;
for(auto v:g[u])
{
if(v==fa)continue;
dfs(v,u);
int sum=0;
for(int i=1;i<=min(cnt[v],m);i++)sum=(sum+f[v][i])%mod;
for(int i=min(m,cnt[u]);i;i--)
{
for(int j=min(m,cnt[v]);j;j--)
{
if(i+j<=m)f[u][i+j]=(f[u][i+j]+f[u][i]*f[v][j])%mod;
}
f[u][i]=f[u][i]*sum%mod;
}
cnt[u]+=cnt[v];
}
}
signed main()
{
cin>>n>>m;
for(int i=1;i<n;i++)
{
int a,b;
cin>>a>>b;
g[a].push_back(b);
g[b].push_back(a);
}
dfs(1,-1);
int res=0;
for(int i=1;i<=m;i++)(res+=f[1][i])%=mod;
cout<<res<<endl;
}