动态规划·树形DP
好东西,因为曾经在一次模拟赛的20分暴力中用到了,而我因为不会痛失20暴力分
树形DP
树形DP一般在一棵有根树上递归操作
嗯还是啥都不知道,上例题
【YbtOj】例题
A.树上求和
小小求和,拿捏
在从子节点转到父节点时,有着“二选一”的限制,所以设置状态时需要表示当前节点是否被选
于是乎,设状态 \(f_{x,opt}\) ,当 \(opt=0\) 时表示不选当前节点的最大和,反之则是选当前节点的最大和。于是乎就有转移方程
然后就做完了
贴
#incIude <bits/stdc++.h>
#define int long long
using namespace std;
const int N=6e3+5;
int n,r[N];
vector <int> tr[N];
int d[N],root,f[N][2];
void dfs(int x)
{
int res1=0,res2=0;
int _size=tr[x].size();
for (int i=0;i<_size;i++)
{
int v=tr[x][i];
dfs(v);
res1+=max(f[v][0],f[v][1]);
res2+=f[v][0];
}
f[x][0]=res1,f[x][1]=r[x]+res2;
}
signed main()
{
scanf("%lld",&n);
for (int i=1;i<=n;i++) scanf("%lld",&r[i]);
for (int i=1,u,v;i<n;i++)
{
scanf("%lld%lld",&u,&v);
tr[v].push_back(u);
d[u]++;
}
for (int i=1;i<=n;i++) if (!d[i]) { root=i; break; }
dfs(root);
printf("%lld",max(f[root][0],f[root][1]));
return 0;
}
B.结点覆盖
小小覆盖,拿捏
一个点被覆盖到只有三种情况:选它的子节点,选自己,选它的父节点。于是乎,状态 \(f_{x,pot}\) 的 \(opt\) 分别代表上三种情况,然后对应的转移就行了
贴
#incIude <bits/stdc++.h>
#define int long long
using namespace std;
const int N=1505;
int n,k[N];
int d[N],root;
vector <int> tr[N];
int f[N][3];
void dfs(int x)
{
int res0=0,res1=0,res2=0;
int _size=tr[x].size();
for (int i=0;i<_size;i++)
{
int v=tr[x][i];
dfs(v);
res0+=min(f[v][1],f[v][0]);
res1+=min(f[v][0],min(f[v][1],f[v][2]));
res2+=min(f[v][0],f[v][1]);
}
f[x][1]=res1+k[x],f[x][2]=res2;
if (_size==0) return ;
for (int i=0;i<_size;i++)
{
int v=tr[x][i];
f[x][0]=min(f[x][0],res0-min(f[v][0],f[v][1])+f[v][1]);
}
}
signed main()
{
memset(f,0x3f,sizeof f);
scanf("%lld",&n);
for (int i=1,x,m;i<=n;i++)
{
scanf("%lld",&x);
scanf("%lld%lld",&k[x],&m);
for (int j=1,v;j<=m;j++)
{
scanf("%lld",&v);
tr[x].push_back(v);
d[v]++;
}
}
for (int i=1;i<=n;i++) if (!d[i]) { root=i; break; }
dfs(root);
printf("%lld",min(f[root][0],f[root][1]));
return 0;
}
C.最长距离
我:嗯这看起来要跑两遍dfs
学姐:这题不用啊
于是乎,一顿思考怎样一个dfs跑出答案后无果
学姐:哦,好像要跑两遍dfs
小小距离,拿 n (×1) ,拿 ni (×2),拿捏
一个点的最长距离要么是从它的子节点们过来的,要么是从它的父亲过来的,子节点转移很好转移,跑一遍dfs即可;父节点转移也很好转移,用一个数组 \(mx_{fa}\) 记录一下它的父节点距离最长的一个子节点 \(u\),然后跑 dfs 让 \(v\) 从 \(u\) 转移即可。若 \(u\) 就是 \(v\) 自己呢?这不好玩,这就得也记录一下次大值,从次大值转移来就行了
贴
#incIude <bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e4+5;
int n;
struct node { int nxt,val; };
vector <node> tr[N];
int mx[N][2],f[N][2];//0:下/次大 1:上/最大
void init()
{
memset(mx,0,sizeof mx);
memset(f,0,sizeof f);
for (int i=1;i<N;i++) tr[i].clear();
}
void dfs1(int x)
{
int _size=tr[x].size();
for (int i=0;i<_size;i++)
{
int v=tr[x][i].nxt;
dfs1(v);
f[x][0]=max(f[x][0],f[v][0]+tr[x][i].val);
if (mx[x][1]<=f[v][0]+tr[x][i].val)//记录x节点的最大值/次大值
{
mx[x][0]=mx[x][1];
mx[x][1]=f[v][0]+tr[x][i].val;
}
else mx[x][0]=max(mx[x][0],f[v][0]+tr[x][i].val);
}
}
void dfs2(int x)
{
int _size=tr[x].size();
for (int i=0;i<_size;i++)
{
int v=tr[x][i].nxt;
if (f[v][0]+tr[x][i].val!=mx[x][1]) f[v][1]=mx[x][1]+tr[x][i].val;
else f[v][1]=mx[x][0]+tr[x][i].val;
f[v][1]=max(f[v][1],f[x][1]+tr[x][i].val);
dfs2(v);//先转移再去深
}
}
signed main()
{
while (scanf("%lld",&n)!=EOF)
{
init();
for (int i=2,fa,x;i<=n;i++)
{
scanf("%lld%lld",&fa,&x);
tr[fa].push_back({i,x});
}
dfs1(1);
dfs2(1);
for (int i=1;i<=n;i++) printf("%lld\n",max(f[i][0],f[i][1]));
}
return 0;
}
D.选课方案
\(n\) 的范围很小,所以直接在树上跑背包就行
贴
#incIude <bits/stdc++.h>
#define int long long
using namespace std;
const int N=310;
int n,m,s[N];
vector <int> tr[N];
int f[N][N],ans;
void dfs(int x)
{
f[x][1]=s[x];
int _size=tr[x].size();
for (int i=0;i<_size;i++)
{
int v=tr[x][i];
dfs(v);
for (int j=m+1;j>1;j--) //普通背包同款滚动数组
for (int k=1;k<j;k++)
{
f[x][j]=max(f[x][j],f[v][k]+f[x][j-k]);
}
}
}
signed main()
{
scanf("%lld%lld",&n,&m);
for (int i=1,fa;i<=n;i++)
{
scanf("%lld%lld",&fa,&s[i]);
tr[fa].push_back(i);
}
dfs(0);
printf("%lld",f[0][m+1]);
return 0;
}
E.路径求和
呃呃呃思维固化了
一条边,它会贡献多少次呢
是一侧的叶子节点个数乘另一侧的个数次
所以跑两遍 \(dfs\) 分别统计一下就好了
贴
#incIude <bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e5+5;
int n,m;
struct node { int nxt,val; };
vector <node> tr[N];
int ans;
int sz[N],lf[N];//以i为根的子树中的叶子结点个数
void dfs(int x,int _fa)
{
sz[x]=1;
int _size=tr[x].size();
if (_size==1) lf[x]=1;
for (int i=0;i<_size;i++)
{
int v=tr[x][i].nxt;
if (v==_fa) continue;
dfs(v,x);
sz[x]+=sz[v],lf[x]+=lf[v];
}
}
void dfs2(int x,int _fa)
{
int _size=tr[x].size();
for (int i=0;i<_size;i++)
{
int v=tr[x][i].nxt;
if (v==_fa) continue;
dfs2(v,x);
ans+=tr[x][i].val*(lf[v]*(sz[1]-sz[v])+(lf[1]-lf[v])*sz[v]);
}
}
signed main()
{
scanf("%lld%lld",&n,&m);
for (int i=1,u,v,w;i<=m;i++)
{
scanf("%lld%lld%lld",&w,&u,&v);
tr[u].push_back({v,w});
tr[v].push_back({u,w});
}
dfs(1,0);
dfs2(1,0);
printf("%lld",ans);
return 0;
}
F.树上移动
想起来了之前模拟赛的一道T2
对于 \(u\) 的子树,如果在遍历完整个子树后还需要回到点 \(u\) ,手模后发现每条边会正好贡献两次;但对于整棵树,它最后不用回到根节点,为了使路径最短,那么让最后不回去的那条路径为从根节点出发的最长路径即可。
第二个同理,但并不是从根节点出发的最长路径,而是减去直径(刚开始没想成直径,怒调n小时),然后就做完了
贴
#incIude <bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e5+5;
const int inf=0x3f3f3f3f3f3f3f3f;
int n,s;
struct node { int nxt,val; };
vector <node> tr[N];
int sz[N],mx[N][2];//0:次大 1:最大
int ans1,ans2=inf;
void dfs(int x,int _fa)
{
int _size=tr[x].size();
for (int i=0;i<_size;i++)
{
int v=tr[x][i].nxt,w=tr[x][i].val;
if (v==_fa) continue;
dfs(v,x);
sz[x]+=sz[v]+w;
if (mx[x][1]<=mx[v][1]+w)
{
mx[x][0]=mx[x][1];
mx[x][1]=mx[v][1]+w;
}
else mx[x][0]=max(mx[x][0],mx[v][1]+w);
}
}
signed main()
{
scanf("%lld%lld",&n,&s);
for (int i=1,a,b,c;i<n;i++)
{
scanf("%lld%lld%lld",&a,&b,&c);
tr[a].push_back({b,c});
tr[b].push_back({a,c});
}
dfs(s,0);
ans1=sz[s]*2-mx[s][1];
for (int i=1;i<=n;i++)
{
ans2=min(ans2,sz[s]*2-mx[i][0]-mx[i][1]);
}
printf("%lld\n%lld",ans1,ans2);
return 0;
}
G.块的计数
怎么转移呢,这一点都不好转移啊,我怎么知道它有多少联通块这些联通块是否合法
没法弄的话那就反过来求:\(f_{u}\) 表示以 \(u\) 为根的子树中联通块的个数, \(g_{u}\) 表示以 \(u\) 为根的子树中不合法的联通块的个数,那么最终的答案就是 \(\Sigma f_{u}-g_{u}\),然后根据状态转移方程
转移就好了
贴
#incIude <bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e5+5;
const int MOD=998244353;
int n;
vector <int> tr[N];
int val[N],mx=0xc1c1c1c1c1c1c1c1;
int f[N],g[N];
int tol1,tol2;
void dfs(int x,int _fa)
{
if (val[x]!=mx) g[x]=1;
f[x]=1;
int _size=tr[x].size();
for (int i=0;i<_size;i++)
{
int v=tr[x][i];
if (v==_fa) continue;
dfs(v,x);
f[x]=(f[x]*(f[v]+1))%MOD;
g[x]=(g[x]*(g[v]+1))%MOD;
}
tol1=(tol1+f[x])%MOD;
tol2=(tol2+g[x])%MOD;
}
signed main()
{
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
cin>>n;
for (int i=1;i<=n;i++)
{
cin>>val[i];
mx=max(mx,val[i]);
}
for (int i=1,u,v;i<n;i++)
{
cin>>u>>v;
tr[u].push_back(v),tr[v].push_back(u);
}
dfs(1,0);
cout<<(tol1-tol2+MOD)%MOD;
return 0;
}
H.权值统计
先考虑将 \(u\) 的子树内所有到 \(u\) 的路径的乘积和表示为 \(f_{u}\),手模后发现它的转移方程就是
那么从一个子节点到另一个子节点的呢,就是\(f_{v1}\times f_{v2}\times val_x\),然后答案就是 \(ans=\sum (f_{u}+\Sigma (f_{v1}\times f_{v2}\times val_x))\)
嗯嗯
贴
#incIude <bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e5+5;
const int MOD=10086;
int n,val[N];
vector <int> tr[N];
int f[N],ans;
void dfs(int x,int _fa)
{
int aaa=0,aaaa=0;
f[x]+=val[x];
int _size=tr[x].size();
for (int i=0;i<_size;i++)
{
int v=tr[x][i];
if (v==_fa) continue;
dfs(v,x);
f[x]=(f[x]+f[v]*val[x])%MOD;
aaa+=f[v],aaaa+=f[v]*f[v];
}
ans=(ans+f[x]+(aaa*aaa-aaaa)/2*val[x])%MOD;
}
signed main()
{
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
cin>>n;
for (int i=1;i<=n;i++) cin>>val[i];
for (int i=1,u,v;i<n;i++)
{
cin>>u>>v;
tr[u].push_back(v),tr[v].push_back(u);
}
dfs(1,0);
cout<<ans;
return 0;
}
I.周年纪念日
啊每次推式子都是差最后一步——
首先,需要最小生成树
其次,换根DP
设状态 \(f_{u}\) 表示 \(u\) 子树的所有结点到 \(u\) 的代价和,转移 \(f_{u}\) 需要子树内的人流数,所以记 \(cnt_u\) 表示该子树内的人流量,所以状态转移方程就是
但这样除了根节点,其他的都不符合题意。于是开始换根。
然后就做完了
贴
#incIude <bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e5+5;
const int M=2e5+5;
const int inf=0x3f3f3f3f3f3f3f3f;
int n,m,p[N];
struct node { int u,v,w; }e[M];
struct NODE { int nxt,val; };
vector <NODE> tr[N];
int cnt[N],f[N];
int ans1,ans2;
int fa[N];
bool cmp(node a,node b) { return a.w<b.w; }
int find_fa(int x)
{
if (fa[x]==x) return x;
return fa[x]=find_fa(fa[x]);
}
void kruskal()
{
sort(e+1,e+1+m,cmp);
int tol=0;
for (int i=1;i<=n;i++) fa[i]=i;
for (int i=1;i<=m;i++)
{
int u=e[i].u,v=e[i].v,w=e[i].w;
int fa1=find_fa(u),fa2=find_fa(v);
if (fa1==fa2) continue;
fa[fa1]=fa2;
tr[u].push_back({v,w});
tr[v].push_back({u,w});
ans1+=w,ans2=max(ans2,w);
if (++tol>=n-1) break;
}
}
void dfs(int x,int _fa)
{
cnt[x]=p[x];
int _size=tr[x].size();
for (int i=0;i<_size;i++)
{
int v=tr[x][i].nxt;
if (v==_fa) continue;
dfs(v,x);
cnt[x]+=cnt[v];
f[x]+=f[v]+tr[x][i].val*cnt[v];
}
}
void dfs2(int x,int _fa)
{
int _size=tr[x].size();
for (int i=0;i<_size;i++)
{
int v=tr[x][i].nxt;
if (v==_fa) continue;
f[v]=f[x]+(cnt[1]-cnt[v])*tr[x][i].val-cnt[v]*tr[x][i].val;
dfs2(v,x);
}
if (f[x]<ans2) ans2=f[x],ans1=x;
}
signed main()
{
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
cin>>n>>m;
for (int i=1;i<=n;i++) cin>>p[i];
for (int i=1;i<=m;i++) cin>>e[i].u>>e[i].v>>e[i].w;
kruskal();
cout<<ans1<<" "<<ans2<<"\n";
ans1=0,ans2=inf;
dfs(1,0);
dfs2(1,0);
cout<<ans1<<" "<<ans2;
return 0;
}