树形DP
- 没有上司的舞会
家常便饭了,写了好几遍,没啥好说的,正常独立集问题。
int head[B];
int cnt;
struct node
{int v,nxt;
}e[B<<1];
void modify(int u,int v)
{e[++cnt].nxt=head[u];e[cnt].v=v;head[u]=cnt;
}
int a[B];
int f[B][5];
int n;
void dfs(int u,int pre)
{for (int i=head[u];i;i=e[i].nxt){int v=e[i].v;if (v==pre) continue;dfs(v,u);f[u][1]+=f[v][0];f[u][0]+=max(f[v][0],f[v][1]);}
}
void work()
{cin>>n;for (int i=1;i<=n;i++) f[i][1]=read();for (int i=1;i<n;i++){int v=read(),u=read();modify(u,v);modify(v,u); }dfs(1,0);cout<<max(f[1][0],f[1][1]);
}
- 选课
树上背包问题。
说说自己犯的错误
- 式子推得太慢
- 滚动数组特别容易记混状态,尤其是背包,因为是从大到小更新,如果在枚举到大数的时候发生转移却用的小的,那么更新的答案必然是错误的,这个问题我调了一下午,就是滚动数组的错误,一定切记,对于逆序的滚动数组问题,一定好想好当前所需状态是否已经更新,而不是借用上一维的状态,这种错误最容易犯在背包上!!
- 这个题目优化点在于,用一个虚拟点来连接森林,这样就不用再写一遍了
#include<bits/stdc++.h>
using namespace std;
int read(){int x;scanf("%d",&x);return x;}
const int B=1e6+10;
const int inf=0x3f3f3f3f;
int T;
int n,m;
int f[309][309];
int dp[309][309];
int a[B];
int head[B];
int cnt;
struct node
{int v,nxt;
}e[B<<1];
void modify(int u,int v)
{e[++cnt].nxt=head[u];e[cnt].v=v;head[u]=cnt;
}
int fa[B];
int vis[B];
void dfs(int u,int pre)
{f[u][1]=a[u];for (int i=head[u];i;i=e[i].nxt){int v=e[i].v;if (v==pre) continue;dfs(v,u);}for (int i=head[u];i;i=e[i].nxt){int v=e[i].v;if (v==pre) continue; for (int j=m;j>=1;j--){for (int k=1;k<=j;k++)dp[u][j]=max(dp[u][j],dp[u][j-k]+f[v][k]);//f[u][j]=dp[u][j-1]+a[u];//因为我们的j是倒序枚举,先更新大的在更新小的,那么dp[u][j-1]并不是当前已经对v//点进行操作完毕的 dp,而是上一维度 }for (int j=1;j<=m;j++) f[u][j]=dp[u][j-1]+a[u];//放在外面就是正确的,这是因为用的是当前维度下的dp,滚动数组容易反的错误 }
}
int ans[B];
int num;
int F[B];
void work()
{cin>>n>>m;for (int i=1;i<=n;i++){fa[i]=read();a[i]=read();modify(fa[i],i);modify(i,fa[i]);}for (int i=1;i<=n;i++){if (!fa[i]){dfs(i,0);ans[++num]=i;}}for (int i=1;i<=num;i++){for (int j=m;j>=1;j--){int x=ans[i];for (int k=0;k<=min(m,j);k++)F[j]=max(F[j],F[j-k]+f[x][k]);} }cout<<F[m];
}
int main()
{T=1;while (T--) work();return 0;
}
- 积蓄程度
题面:见ACwing287
思路:
换根DP的常规思路
- 有下往上递推一遍
- 再由上往下递推一遍
我们先来想如果根节点固定的时候如何做,比较容易的DP,其实也就是递推,没有任何决策问题。
d [ u ] d[u] d[u] 表示以 u u u 为根节点的最小流量。通过题目可以知道,一条路径上的流量取决于最小流量边,所以这是一个维护最小值的问题。
转移有
d [ u ] = ∑ min { d [ v ] , w } d[u]=\sum \min\{d[v],w\} d[u]=∑min{d[v],w}
再来考虑如何换根。
先来考虑由根节点换到儿子会发生什么变化
我们会发现, d [ 1 ] d[1] d[1] 中需要去掉 d [ 2 ] d[2] d[2] 做出的贡献,即 d [ 1 ] − min { d [ 2 ] , w } d[1]-\min\{d[2],w\} d[1]−min{d[2],w}
d [ 2 ] d[2] d[2] 需要加入 d [ 1 ] d[1] d[1] 的贡献,即 d [ 2 ] + min { d [ 1 ] , w } d[2]+\min\{d[1],w\} d[2]+min{d[1],w}
我来思考,如果是在树中间换根怎么办,我们发现,父亲节点还需要同时加入父亲父亲的节点情况。
比如绿色部分,可是这条边比较难表示,不容易找到 w w w,如果我们看成 2 2 2 节点就是根节点,不再是父亲节点的时候,此时的值刚好就是 2 2 2 当根的值,我们用 f [ 2 ] f[2] f[2] 表示 2 2 2 为根的最大值。那么在转移的时候我们就不需要考虑由父亲父亲的情况。转移由
f [ v ] = d [ v ] + min { w , f [ u ] − min { w , d [ v ] } } f[v]=d[v]+\min\{w,f[u]-\min\{w,d[v]\}\} f[v]=d[v]+min{w,f[u]−min{w,d[v]}}
对于度数只为1的还需要特别注意,度数唯一不一定是叶子结点,也可能是根节点,当换根时,根节点变成叶子结点, f [ u ] − min { w , d [ v ] } f[u]-\min\{w,d[v]\} f[u]−min{w,d[v]} 变成 0 0 0 ,实际上应该直接加入边值 w w w,所以需要特别判断。
换根时候的注意点
- 必须一直保持根和儿子换,而不是父亲和儿子换。
- 根节点和叶子结点要注意
#include<bits/stdc++.h>
using namespace std;
int read(){int x;scanf("%d",&x);return x;}
const int B=1e6+10;
const int inf=0x3f3f3f3f;
int T;
int n;
int head[B],cnt;
struct node
{int v,nxt,w;
}e[B<<1];
void modify(int u,int v,int w)
{e[++cnt].nxt=head[u];e[cnt].v=v;e[cnt].w=w;head[u]=cnt;
}
int f[B];
int d[B];
int du[B];
void dfs1(int u,int pre)
{for (int i=head[u];i;i=e[i].nxt){int v=e[i].v;int w=e[i].w;if (v==pre) continue;dfs1(v,u);if (du[v]!=1) d[u]+=min(d[v],w);else d[u]+=w;}
}
void dfs2(int u,int pre)
{for (int i=head[u];i;i=e[i].nxt){int w=e[i].w;int v=e[i].v;if (v==pre) continue;if (du[u]==1) f[v]=d[v]+w;else f[v]=d[v]+min(w,f[u]-min(d[v],w));dfs2(v,u);}
}
void work()
{cin>>n;cnt=0;memset(head,0,sizeof(head));for (int i=1;i<=n;i++){f[i]=0;d[i]=0;du[i]=0;}for (int i=1;i<n;i++){int u=read(),v=read(),w=read();modify(u,v,w); du[u]++; du[v]++;modify(v,u,w);}dfs1(1,0); f[1]=d[1];dfs2(1,0);int ans=0;for (int i=1;i<=n;i++) ans=max(ans,f[i]);cout<<ans<<"\n";
}
int main()
{T=read();while (T--) work();return 0;
}
/*
1
3
1 2 1
2 3 1
*/