树形DP
P1352 没有上司的舞会(树形DP入门题)
题意:给定一棵树,每个节点都有自己的权值,父子节点之间只能选择一个,问怎样可以使得整棵树选到的权值最大?
思路:设
d
p
[
u
]
[
0
/
1
]
dp[u][0/1]
dp[u][0/1], 0 表示不选择 u 节点,1 表示选择 u 节点。可以这样转移:
- d p [ u ] [ 0 ] = ∑ m a x ( d p [ v ] [ 0 ] , d p [ v ] [ 1 ] ) dp[u][0]=\sum max(dp[v][0],dp[v][1]) dp[u][0]=∑max(dp[v][0],dp[v][1])
- d p [ u ] [ 1 ] = w [ u ] + ∑ d p [ v ] [ 0 ] dp[u][1]=w[u]+ \sum dp[v][0] dp[u][1]=w[u]+∑dp[v][0]
#include <bits/stdc++.h>
#define ll long long
using namespace std;
const int maxn=6000+10,maxm=5e5+10;
int n;
int a[maxn];
vector<int> e[maxn];
int in[maxn];
int dp[maxn][2];
void dfs(int u,int fa=-1)
{
for(auto v: e[u])
{
if(v==fa) continue;
dfs(v,u);
dp[u][0]+=max(dp[v][0],dp[v][1]);
dp[u][1]+=dp[v][0];
}
dp[u][1]+=a[u];
}
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;++i) scanf("%d",&a[i]);
for(int i=1;i<=n-1;++i)
{
int u,v;
scanf("%d%d",&v,&u);
e[u].push_back(v);
in[v]++;
}
int u=0;
for(int i=1;i<=n;++i)
if(in[i]==0) u=i;
dfs(u);
ll ans=max(dp[u][0],dp[u][1]);
printf("%lld\n",ans);
return 0;
}
Strategic game POJ - 1463 (树形DP入门题)
链接:http://poj.org/problem?id=1463
题意:给定一颗有向树,问至少需要选择多少个点,才能使得所有的边都和选择的点相连
思路:对点只有选和不选两种情况,设
d
p
[
u
]
[
0
/
1
]
dp[u][0/1]
dp[u][0/1] ,0表示不选,1表示选择
- d p [ u ] [ 0 ] = ∑ d p [ v ] [ 1 ] dp[u][0]=\sum dp[v][1] dp[u][0]=∑dp[v][1]
- d p [ u ] [ 1 ] = 1 + ∑ m i n ( d p [ v ] [ 0 ] , d p [ v ] [ 1 ] ) dp[u][1]=1+\sum min(dp[v][0],dp[v][1]) dp[u][1]=1+∑min(dp[v][0],dp[v][1])
#include <cstdio>
#include <vector>
#include <cstring>
#define ll long long
using namespace std;
const int maxn=3000+10,maxm=5e5+10;
int n;
vector<int> e[maxn];
int dp[maxn][2],in[maxn];
void dfs(int u,int fa=-1)
{
for(int i=0;i<e[u].size();++i)
{
int v=e[u][i];
if(v==fa) continue;
dfs(v,u);
dp[u][0]+=dp[v][1];
dp[u][1]+=min(dp[v][0],dp[v][1]);
}
dp[u][1]++;
}
int main()
{
while(~scanf("%d",&n))
{
memset(dp,0,sizeof(dp));
memset(in,0,sizeof(in));
for(int i=1;i<=n;++i) e[i].clear();
for(int i=1;i<=n;++i)
{
int u,v,x;
scanf("%d:(%d)",&u,&x);
++u;
for(int j=1;j<=x;++j)
{
scanf("%d",&v);
++v;
e[u].push_back(v),in[v]++;
}
}
int rt=-1;
for(int i=1;i<=n;++i)
if(in[i]==0) rt=i;
dfs(rt);
printf("%d\n",min(dp[rt][0],dp[rt][1]));
}
return 0;
}
/*
8
0:(2) 1 2
1:(2) 3 4
2:(3) 5 6 7
3:(0)
4:(0)
5:(0)
6:(0)
7:(0)
*/
Computer HDU - 2196 (树形DP经典题)
链接:http://acm.hdu.edu.cn/showproblem.php?pid=2196
题意:给定一个无向联通图,求离每个点最远的点的距离
思路:设 d p [ u ] [ 0 ] dp[u][0] dp[u][0] 表示在以 u u u 为根的子树中距离 u u u 最远的距离, d p [ u ] [ 1 ] dp[u][1] dp[u][1] 表示在以 u 为根的子树中距离 u 第二远的距离。 d p [ u ] [ 2 ] dp[u][2] dp[u][2] 表示通过 u 的父节点,得到距离 u 的最远距离
- d p [ u ] [ 0 / 1 ] dp[u][0/1] dp[u][0/1] 中 u 通过 v 来更新,先往下走,在更新。
-
d
p
[
u
]
[
2
]
dp[u][2]
dp[u][2] 中 v 通过 u 来更新,先更新,再往下走。
d
p
[
v
]
[
2
]
dp[v][2]
dp[v][2] 有两种获得途径,一是父节点往上走,二是父节点不通过 v 往下走 。(这也是为什么要求两个最远和次远距离的原因)
d p [ v ] [ 2 ] = m a x ( d p [ u ] [ 0 ] + w , d p [ u ] [ 2 ] + w ) dp[v][2]=max(dp[u][0]+w,dp[u][2]+w) dp[v][2]=max(dp[u][0]+w,dp[u][2]+w) (最远距离不通过 v )
d p [ v ] [ 2 ] = m a x ( d p [ u ] [ 1 ] + w , d p [ u ] [ 2 ] + w ) dp[v][2]=max(dp[u][1]+w,dp[u][2]+w) dp[v][2]=max(dp[u][1]+w,dp[u][2]+w) (最远距离通过v,用次远距离来更新)
实现:最远和次远距离如何更新?设 d=dis[v][0]+w,,先用当前距离 d 更新最远距离d0,不能对最远距离做更新时,用 d 来更新 d1
PS:好像可以树分治,下次在学,咕咕咕。(update:2020年10月24日,点分治不行,点分治只能计算所有路径不能更新回去)
#include <bits/stdc++.h>
#define fi first
#define se second
#define ll long long
using namespace std;
const int maxn=1e4+5;
int n;
vector<pair<int,int> > e[maxn];
ll dis1[maxn],dis2[maxn],dis3[maxn];
int son[maxn];
void dfs1(int u,int fa)
{
ll d=0,d1=0,d2=0;
for(auto x: e[u])
{
int v=x.fi,w=x.se;
if(v==fa) continue;
dfs1(v,u);
d=dis1[v]+w;
if(d1<=d)
{
d2=d1;
d1=d;
son[u]=v;
}
else if(d2<d) d2=d;
}
dis1[u]=d1,dis2[u]=d2;
}
void dfs2(int u,int fa)
{
for(auto x: e[u])
{
int v=x.fi,w=x.se;
if(v==fa) continue;
if(son[u]==v) dis3[v]=max(dis2[u]+w,dis3[u]+w);
else dis3[v]=max(dis1[u]+w,dis3[u]+w);
dfs2(v,u);
}
}
int main()
{
while(~scanf("%d",&n))
{
for(int i=1; i<=n; ++i) e[i].clear();
memset(dis1,0,sizeof(dis1));
memset(dis2,0,sizeof(dis2));
memset(dis3,0,sizeof(dis3));
for(int v=2; v<=n; ++v)
{
int u,w;
scanf("%d%d",&u,&w);
e[u].push_back({v,w});
e[v].push_back({u,w});
}
dfs1(1,0);
dfs2(1,0);
for(int i=1; i<=n; ++i)
printf("%lld\n",max(dis1[i],dis3[i]));
}
return 0;
}
Accumulation Degree POJ - 3585 (树形DP经典入门题)
链接:http://poj.org/problem?id=3585
题意:给定一颗无根树,每条边都有边权,让你求所有叶节点到每个点的最大流量之和
思路:每个点的流量总共两种来源,一是来自父节点,二是来自所有子节点。设 d p [ u ] [ 0 / 1 ] dp[u][0/1] dp[u][0/1] , d p [ u ] [ 0 ] dp[u][0] dp[u][0] 代表所有来自子节点的流量, d p [ u ] [ 1 ] dp[u][1] dp[u][1] 表示来自父节点的流量
- d p [ u ] [ 0 ] = ∑ m i n ( d p [ v ] [ 0 ] , w ) dp[u][0]= \sum min(dp[v][0],w) dp[u][0]=∑min(dp[v][0],w)
- 当 u 为根,且入度为 1 时,它的流量是无穷的, d p [ v ] [ 1 ] = w dp[v][1]=w dp[v][1]=w,(这里我设 1 为根)
- 当 u 为根,且入度大于 1 时,它的流量受另一条边控制。这种情况和 u 不为 根时相同, d p [ v ] [ 1 ] = m i n ( d p [ u ] [ 0 ] + d p [ u ] [ 1 ] − m i n ( d p [ v ] [ 0 ] , w ) , w ) dp[v][1] = min(dp[u][0]+dp[u][1]- min(dp[v][0],w),w) dp[v][1]=min(dp[u][0]+dp[u][1]−min(dp[v][0],w),w)
#include <cstdio>
#include <vector>
#include <cstring>
#define fi first
#define se second
#define ll long long
using namespace std;
const int maxn=2e5+10,maxm=5e5+10,inf=1e9;
int t,n;
int head[maxn],cnt;
struct Edge
{
int nxt,to,w;
} edges[maxn<<1];
void add(int u,int v,int w)
{
edges[++cnt].to=v;
edges[cnt].w=w;
edges[cnt].nxt=head[u];
head[u]=cnt;
}
ll dp1[maxn],dp2[maxn];
int in[maxn];
void dfs1(int u,int fa)
{
for(int i=head[u]; i!=-1; i=edges[i].nxt)
{
int v=edges[i].to,w=edges[i].w;
if(v==fa) continue;
dfs1(v,u);
if(in[v]==1) dp1[u]+=w;
else dp1[u]+=min(dp1[v],1ll*w);
}
}
void dfs2(int u,int fa=-1)
{
for(int i=head[u]; i!=-1; i=edges[i].nxt)
{
int v=edges[i].to,w=edges[i].w;
if(v==fa) continue;
ll flow=min(1ll*w,dp1[v]);
if(in[1]==1&&u==1) dp2[v]=w;
else dp2[v]+=min(dp1[u]+dp2[u]-flow,1ll*w);
dfs2(v,u);
}
}
int main()
{
scanf("%d",&t);
while(t--)
{
scanf("%d",&n);
cnt=0;
memset(head,-1,sizeof(head));
memset(dp1,0,sizeof(dp1));
memset(dp2,0,sizeof(dp2));
memset(in,0,sizeof(in));
for(int i=1; i<=n-1; ++i)
{
int u,v,w;
scanf("%d%d%d",&u,&v,&w);
add(u,v,w);
add(v,u,w);
in[u]++,in[v]++;
}
dfs1(1,0);
dfs2(1,0);
ll ans=0;
for(int i=1; i<=n; ++i) ans=max(ans,dp1[i]+dp2[i]);
printf("%lld\n",ans);
}
return 0;
}
D. Distance in Tree CF161D
链接:https://codeforces.com/problemset/problem/161/D
题意:给定一颗无根树,求两个点之间距离为 k 的对数。
思路:设
d
p
[
u
]
[
i
]
[
0
/
1
]
dp[u][i][0/1]
dp[u][i][0/1] 分别表示以 u 为根的子树中距离 u 为 i 的节点数,和 u 的祖先中距离 u 为 i 个节点数,最终答案就是
∑
d
p
[
u
]
[
k
]
[
0
]
+
d
p
[
u
]
[
k
]
[
1
]
\sum dp[u][k][0] +dp[u][k][1]
∑dp[u][k][0]+dp[u][k][1]
- d p [ u ] [ i ] [ 0 ] = d p [ v ] [ i − 1 ] [ 0 ] dp[u][i][0] =dp[v][i-1][0] dp[u][i][0]=dp[v][i−1][0],注意初始值 dp[u][0][0]=1,这里不赋值对往上推有影响。(这里的细节卡了挺久的)
- d p [ v ] [ i ] [ 1 ] = d p [ u ] [ i − 1 ] [ 1 ] + d p [ u ] [ i − 1 ] [ 0 ] − d p [ v ] [ i − 2 ] [ 0 ] dp[v][i][1]=dp[u][i-1][1]+dp[u][i-1][0]-dp[v][i-2][0] dp[v][i][1]=dp[u][i−1][1]+dp[u][i−1][0]−dp[v][i−2][0], 与 v v v 距离为 k k k 的点为,它的父节点距离为 k − 1 k-1 k−1 的点,同时要删去经过 v 重复计数的点。
#include <cstdio>
#include <vector>
#include <cstring>
#define ll long long
using namespace std;
const int maxn=5e4+10,maxm=500+10,inf=1e9;
int n,k;
vector<int> e[maxn];
int dp[maxn][maxm][2];
void dfs1(int u,int fa=-1)
{
dp[u][0][0]=1;
for(auto v: e[u])
{
if(v==fa) continue;
dfs1(v,u);
for(int i=1;i<=k;++i)
dp[u][i][0]+=dp[v][i-1][0];
}
}
void dfs2(int u,int fa=-1)
{
for(auto v: e[u])
{
if(v==fa) continue;
dp[v][1][1]=1;
for(int i=2;i<=k;++i)
dp[v][i][1]=dp[u][i-1][1]+dp[u][i-1][0]-dp[v][i-2][0];
dfs2(v,u);
}
}
int main()
{
scanf("%d%d",&n,&k);
for(int i=1;i<=n-1;++i)
{
int u,v;
scanf("%d%d",&u,&v);
e[u].push_back(v);
e[v].push_back(u);
}
dfs1(1);
dfs2(1);
ll ans=0;
for(int i=1;i<=n;++i)
ans+=dp[i][k][0]+dp[i][k][1];
printf("%lld\n",ans/2);
return 0;
}
P2986 [USACO10MAR]Great Cow Gathering G
链接:https://www.luogu.com.cn/problem/P2986
题意:给定一颗无根树,每个点有点权,每条边有边权。一个点移动到另一个点的代价为 点权 乘上 距离。让你选择一个点作为中心,让其他点到这个点的代价和最小。求出这个代价和
思路:我们可以设 1 为根,然后计算出其他点到 1 的代价和,然后从根倒推回去。假设从 u 推到 v ,可以先让原本 v 到 u 的点退回来,然后让其他的点达到 v 。
- 设 d i s 1 [ u ] dis1[u] dis1[u] 表示以 u 为根的子树中,所有点到 u 的代价。 d i s 1 [ u ] = ∑ d i s 1 [ v ] + w × n u m [ v ] dis1[u]=\sum dis1[v]+w \times num[v] dis1[u]=∑dis1[v]+w×num[v],同时更新数量 n u m [ u ] = ∑ n u m [ v ] num[u]=\sum num[v] num[u]=∑num[v]
- 设 d i s 2 [ u ] dis2[u] dis2[u] 表示到达 1 后倒推回来,那么 d i s 2 [ v ] = d i s [ u ] − n u m [ v ] × w + ( s u m − n u m [ v ] ) × w dis2[v]=dis[u] - num[v]\times w + (sum-num[v])\times w dis2[v]=dis[u]−num[v]×w+(sum−num[v])×w
#include <cstdio>
#include <vector>
#include <cstring>
#define ll long long
using namespace std;
const int maxn=1e5+10,maxm=500+10;
int head[maxn],cnt;
struct Edge
{
ll nxt,to,w;
}edges[maxn<<1];
void add(int u,int v,int w)
{
edges[++cnt].to=v;
edges[cnt].w=w;
edges[cnt].nxt=head[u];
head[u]=cnt;
}
ll n,num[maxn];
ll dis[maxn][2];
void dfs1(int u,int fa=-1)
{
for(int i=head[u];i!=-1;i=edges[i].nxt)
{
int v=edges[i].to,w=edges[i].w;
if(v==fa) continue;
dfs1(v,u);
dis[u][0]+=dis[v][0]+w*num[v];
num[u]+=num[v];
}
}
void dfs2(int u,int fa=-1)
{
for(int i=head[u];i!=-1;i=edges[i].nxt)
{
ll v=edges[i].to,w=edges[i].w;
if(v==fa) continue;
dis[v][1]=dis[u][1]-num[v]*w+(num[1]-num[v])*w;
dfs2(v,u);
}
}
int main()
{
scanf("%lld",&n);
for(int i=1;i<=n;++i) scanf("%lld",&num[i]);
cnt=0;
memset(head,-1,sizeof(head));
for(int i=1;i<=n-1;++i)
{
ll u,v,w;
scanf("%lld%lld%lld",&u,&v,&w);
add(u,v,w);
add(v,u,w);
}
dfs1(1);
dfs2(1);
ll ans=9e18;
for(int i=1;i<=n;++i)
ans=min(ans,dis[1][0]+dis[i][1]);
printf("%lld\n",ans);
return 0;
}
D. Fish eating fruit 2019沈阳网络预选赛(树形DP || 点分治)
链接:https://www.jisuanke.com/contest/3007/challenges
题意:给定一棵树求所有点对,模3意义下的总权值之和。
思路:
- 设 d p [ u ] [ k ] dp[u][k] dp[u][k] 表示以 u u u 为根的子树中,所有子节点与 u u u 的距离模 3 为 k k k 的总和
- 设 n u m [ u ] [ k ] num[u][k] num[u][k] 表示以 u u u 为根的子树中,所有子节点与 u u u 的距离模 3 为 k k k 的子节点数
- 以子节点来更新父节点。每次计算以 u 为根的子树中,一个节点为 v,另个一节点在与 v 不同的子树中的贡献。这样就需要先对答案做更新,然后再把当前遍历到的 v 节点,转移到 u 上。
- 计算答案:相当于计算所有路径路径类似 x - u - v - y 形式的所有方案,把它分成了3份,x - u 这一段的总和为
d
p
[
u
]
[
j
]
dp[u][j]
dp[u][j],连接的数量为
n
u
m
[
v
]
[
k
]
num[v][k]
num[v][k],v - y 这段的总和为
d
p
[
v
]
[
j
]
dp[v][j]
dp[v][j] ,另一边连接的节点数为
n
u
m
[
u
]
[
k
]
num[u][k]
num[u][k]
u - v 这段为: n u m [ u ] [ j ] × n u m [ v ] [ k ] × w num[u][j]\times num[v][k] \times w num[u][j]×num[v][k]×w
在更新前计算答案: d p [ u ] [ j ] × n u m [ v ] [ k ] + d p [ v ] [ k ] + n u m [ u ] [ j ] dp[u][j]\times num[v][k] + dp[v][k]+num[u][j] dp[u][j]×num[v][k]+dp[v][k]+num[u][j], d p [ u ] [ j ] dp[u][j] dp[u][j]此时并不包含子树 v 的信息。 - 转移方程: d p [ u ] [ ( i + w ) % 3 ] = ∑ d p [ v ] [ i ] + w × n u m [ v ] [ i ] dp[u][ (i+w)\%3]=\sum dp[v][i] + w \times num[v][i] dp[u][(i+w)%3]=∑dp[v][i]+w×num[v][i]
- n u m [ u ] [ ( i + w ) % 3 ] = ∑ n u m [ v ] [ j ] num[u][(i+w)\%3]=\sum num[v][j] num[u][(i+w)%3]=∑num[v][j]
#include <cstdio>
#include <vector>
#include <cstring>
#define ll long long
using namespace std;
const int maxn=1e4+10,maxm=500+10,mod=1e9+7;
int head[maxn],cnt;
struct Edge
{
int nxt,to,w;
}edges[maxn<<1];
void add(int u,int v,int w)
{
edges[++cnt].to=v;
edges[cnt].w=w;
edges[cnt].nxt=head[u];
head[u]=cnt;
}
ll ans[3],dp[maxn][3],num[maxn][3];
void dfs(int u,int fa=-1)
{
for(int i=head[u];i!=-1;i=edges[i].nxt)
{
int v=edges[i].to,w=edges[i].w;
if(v==fa) continue;
dfs(v,u);
for(int j=0;j<=2;++j)
{
for(int k=0;k<=2;++k)
{
int x=(j+k+w)%3;
ans[x]=(ans[x]+num[u][j]*num[v][k]%mod*w%mod)%mod;
ans[x]=(ans[x]+num[u][j]*dp[v][k]%mod+num[v][k]*dp[u][j]%mod)%mod;
}
}
for(int j=0;j<=2;++j)
{
int x=(j+w)%3;
dp[u][x]=(dp[u][x]+dp[v][j]+num[v][j]*w%mod)%mod;
num[u][x]+=num[v][j];
}
}
}
int n;
int main()
{
while(~scanf("%d",&n))
{
memset(ans,0,sizeof(ans));
memset(dp,0,sizeof(dp));
memset(num,0,sizeof(num));
cnt=0;
for(int i=1;i<=n;++i) head[i]=-1;
for(int i=1;i<=n-1;++i)
{
int u,v,w;
scanf("%d%d%d",&u,&v,&w);
u++,v++;
add(u,v,w);
add(v,u,w);
}
for(int i=1;i<=n;++i) num[i][0]=1;
dfs(1);
for(int i=0;i<=2;++i)
printf("%lld%c",ans[i]*2%mod,i==2?'\n':' ');
}
return 0;
}
如果把问题化简一下,只求所有点对之间的路径之和,可以这样计算
#include <cstdio>
#include <vector>
#include <cstring>
#define ll long long
using namespace std;
const int maxn=1e4+10,maxm=500+10,mod=1e9+7;
int head[maxn],cnt;
struct Edge
{
int nxt,to,w;
}edges[maxn<<1];
void add(int u,int v,int w)
{
edges[++cnt].to=v;
edges[cnt].w=w;
edges[cnt].nxt=head[u];
head[u]=cnt;
}
ll ans,dp[maxn],num[maxn];
void dfs(int u,int fa=-1)
{
num[u]++;//初始化可以提出来
for(int i=head[u];i!=-1;i=edges[i].nxt)
{
int v=edges[i].to,w=edges[i].w;
if(v==fa) continue;
dfs(v,u);
ans=(ans+num[u]*num[v]*w+dp[u]*num[v]+dp[v]*num[u]);
dp[u]+=dp[v]+w*num[v];
num[u]+=num[v];
}
}
int n;
int main()
{
while(~scanf("%d",&n))
{
memset(dp,0,sizeof(dp));
memset(num,0,sizeof(num));
cnt=0,ans=0;
for(int i=1;i<=n;++i) head[i]=-1;
for(int i=1;i<=n-1;++i)
{
int u,v,w;
scanf("%d%d%d",&u,&v,&w);
u++,v++;
add(u,v,w);
add(v,u,w);
}
dfs(1);
printf("%lld\n",ans*2);
}
return 0;
}
暴力解法
#include <cstdio>
#include <vector>
#include <cstring>
#define ll long long
using namespace std;
const int maxn=5e4+10,maxm=500+10,inf=1e9;
int n,k;
vector<pair<int,int> > e[maxn];
ll ans[3];
int dis[maxn];
void dfs(int u,int fa=-1)
{
for(auto x : e[u])
{
int v=x.first,w=x.second;
if(v==fa) continue;
dis[v]=dis[u]+w;
dfs(v,u);
}
}
int main()
{
scanf("%d",&n);
for(int i=1;i<=n-1;++i)
{
int u,v,w;
scanf("%d%d%d",&u,&v,&w);
u++,v++;
e[u].push_back({v,w});
e[v].push_back({u,w});
}
for(int i=1;i<=n;++i)
{
memset(dis,0,sizeof(dis));
dfs(i);
for(int i=1;i<=n;++i)
ans[dis[i]%3]+=dis[i];
}
printf("%lld %lld %lld\n",ans[0],ans[1],ans[2]);
return 0;
}