树形DP 学习总结
DP毕竟是算法中最精妙的部分,理解并玩得花哨还是需要一定的时间积累
之前对普通的DP也不敢说掌握,只能说略懂皮毛
在学习树形DP 的同时也算是对DP有了更深的理解吧
DP的关键就在于状态的定义以及找转移
首先要考虑清楚状态,状态要能够很好地并且完整地描述子问题
其次考虑最底层的状态,这些状态一般是最简单的情况或者是边界情况
再就是考虑某一个状态能从哪些子状态转移过来,同时还要考虑转移的顺序,确保子问题已经解决
树形DP很多时候就是通过子节点推父亲节点的状态
还是通过题目来加强理解吧
1.HDU 1520
题意:给一棵树,选最多的结点使得选择的结点不存在直接的父子关系
很容易想到一个结点有两个状态:选或者不选
所以自然地想到状态dp[u][0/1]表示u子树内的最佳答案,u的状态为选或者不选
初始化自然是叶子结点dp[u][0]=0,dp[u][1]=w[u]
转移则可以考虑依次考虑
u不选的时候:u的儿子可以任意选或者不选,所以dp[u][0]+=max(dp[v][0],dp[v][1])
u选的时候:u的儿子必定不能选,所以dp[u][1]+=dp[v][0] 然后dp[u][1]+=w[u]表示加上u这个点
答案自然就是max(dp[rt][0],dp[rt][1])了
#include"cstdio" #include"queue" #include"cmath" #include"stack" #include"iostream" #include"algorithm" #include"cstring" #include"queue" #include"map" #include"set" #include"vector" #include"bitset" #define LL long long #define ull unsigned long long #define mems(a,b) memset(a,b,sizeof(a)) #define mdzz int mid=(L+R)>>1 #pragma comment(linker, "/STACK:1024000000,1024000000") using namespace std; const int N = 6005; const int M = 1e5+5; const int MOD = 998244353; const int INF = 0x3f3f3f3f; int tot; int first[N],w[N],deg[N]; int dp[N][2]; struct node{ int e,next; node(){} node(int a,int b):e(a),next(b){} }edge[M]; void init(){ tot=0; mems(first,-1); mems(deg,0); mems(dp,0); } void addedge(int u,int v){ edge[tot]=node(v,first[u]); first[u]=tot++; } void dfs(int u){ dp[u][0]=0; dp[u][1]=w[u]; for(int i=first[u];i!=-1;i=edge[i].next){ int v=edge[i].e; dfs(v); dp[u][0]+=max(dp[v][1],dp[v][0]); dp[u][1]+=dp[v][0]; } } int n; int main(){ //freopen("in.txt","r",stdin); while(~scanf("%d",&n)){ init(); for(int i=1;i<=n;i++) scanf("%d",&w[i]); int u,v; while(1){ scanf("%d%d",&v,&u); if(!v&&!u) break; addedge(u,v);deg[v]++; } int rt; for(int i=1;i<=n;i++) if(!deg[i]){ dfs(rt=i); break; } printf("%d\n",max(dp[rt][0],dp[rt][1])); } return 0; }
2.POJ 1436
题意:选中一个点则与其相连的边将被覆盖,问最少选几个点使得树中所有边均被覆盖
和上一个题很类似
同样状态设为dp[u][0/1]
初始的底层状态自然是dp[u][0]=0,dp[u][1]=1;
考虑一个非叶子结点和它儿子的所有连边
如果当前结点不选,那这些边只能由其儿子保护,所以dp[u][0]+=dp[v][1]
如果当前结点选,那这些边已被保护,其儿子选和不选都行,所以dp[u][1]+=min(dp[v][0],dp[v][1])
答案自然是min(dp[rt][0],dp[rt][1])
#include"cstdio" #include"queue" #include"cmath" #include"stack" #include"iostream" #include"algorithm" #include"cstring" #include"queue" #include"map" #include"set" #include"vector" #include"bitset" #define LL long long #define ull unsigned long long #define mems(a,b) memset(a,b,sizeof(a)) #define mdzz int mid=(L+R)>>1 #pragma comment(linker, "/STACK:1024000000,1024000000") using namespace std; const int N = 1505; const int M = 1e5+5; const int MOD = 998244353; const int INF = 0x3f3f3f3f; int tot; int first[N],deg[N]; int dp[N][2]; struct node{ int e,next; node(){} node(int a,int b):e(a),next(b){} }edge[M]; void init(){ tot=0; mems(first,-1); mems(deg,0); } void addedge(int u,int v){ edge[tot]=node(v,first[u]); first[u]=tot++; } void dfs(int u){ dp[u][0]=0; dp[u][1]=1; for(int i=first[u];i!=-1;i=edge[i].next){ int v=edge[i].e; dfs(v); dp[u][0]+=dp[v][1]; dp[u][1]+=min(dp[v][0],dp[v][1]); } } int n,k,u,v; int main(){ //freopen("in.txt","r",stdin); while(~scanf("%d",&n)){ init(); for(int i=1;i<=n;i++){ scanf("%d:(%d)",&u,&k); for(int j=0;j<k;j++){ scanf("%d",&v); addedge(u,v);deg[v]++; } } int rt; for(int i=0;i<n;i++) if(!deg[i]){ dfs(rt=i); break; } printf("%d\n",min(dp[rt][0],dp[rt][1])); } return 0; }
3.URAL 1018
题意:树中每个点有权值,问只留下k个点剩下的最大权值和是多少?留下的点还是构成一棵树
树形背包
状态定义成dp[u][i]表示u子树内剩i个点的最大权值
考虑叶子结点:dp[u][0]=0,dp[u][1]=w[u]
考虑非叶子结点的一个状态dp[u][i],对于当前的一个儿子v,枚举一个k表示从这个儿子里取几个结点,维护一个最大值
其实我们这里的状态是三维的,表示u子树的前j个子树取了i个结点的答案
我们使用滚动数组把j这一维滚掉
这里简化了题目,每一个结点固定只有两个儿子,用一般做法做肯定也是没问题的
还有要注意的地方就是这里根是一定要保留的
处理方法就是对于状态dp[u][1]直接赋值,枚举时候i从2开始,这样就可以默认根已取
#include"cstdio" #include"queue" #include"cmath" #include"stack" #include"iostream" #include"algorithm" #include"cstring" #include"queue" #include"map" #include"set" #include"vector" #include"bitset" #define LL long long #define ull unsigned long long #define mems(a,b) memset(a,b,sizeof(a)) #define mdzz int mid=(L+R)>>1 #pragma comment(linker, "/STACK:1024000000,1024000000") using namespace std; const int N = 105; const int M = 1e5+5; const int MOD = 998244353; const int INF = 0x3f3f3f3f; int tot; int first[N],w[N]; int dp[N][N],sz[N]; int ls[N],rs[N]; struct node{ int e,next; node(){} node(int a,int b):e(a),next(b){} }edge[M]; void init(){ tot=0; mems(first,-1); mems(w,-1); //mems(deg,0); mems(dp,0); mems(ls,-1); mems(rs,-1); } void addedge(int u,int v){ edge[tot]=node(v,first[u]); first[u]=tot++; edge[tot]=node(u,first[v]); first[v]=tot++; } void dfs1(int u,int fa){ sz[u]=1; for(int i=first[u];i!=-1;i=edge[i].next){ int v=edge[i].e; if(v==fa) continue; dfs1(v,u); sz[u]+=sz[v]; if(ls[u]==-1) ls[u]=v; else rs[u]=v; } } void dfs(int u){ int f=0; dp[u][0]=0;dp[u][1]=w[u]; if(ls[u]!=-1) dfs(ls[u]),f=1; if(rs[u]!=-1) dfs(rs[u]),f=1; if(!f) return; for(int i=2;i<=sz[u];i++) for(int j=0;j<=sz[ls[u]];j++) if(i-1>=j) dp[u][i]=max(dp[u][i],dp[ls[u]][j]+dp[rs[u]][i-1-j]+w[u]); } int n,k; int main(){ //freopen("in.txt","r",stdin); while(~scanf("%d%d",&n,&k)){ init(); int u,v,x,rt=1;w[rt]=0; for(int i=1;i<n;i++){ scanf("%d%d%d",&u,&v,&x); addedge(u,v); if(w[v]==-1) w[v]=x; else w[u]=x; } dfs1(rt,-1); dfs(rt); printf("%d\n",dp[rt][k+1]); } return 0; }
4.HDU 2196
题意:对于树中的每一个结点,输出树中与其距离最远的结点的距离值
经典的树形DP问题
状态定义为dp[u][0/1]为u到其子树内结点的最远距离、次远距离
这样一轮dp下来,可以想到对于根来说,dp[rt][0]就是它的答案
但是对于其它结点来说只得到了其子树内的答案,而我们需要的是其对于整棵树的信息
这里需要再一次dfs,相当于反过来从根往叶子再dp一次,通过根的答案推其余结点的答案
这里之所以要维护一个次大值,就是对于一个结点u的儿子v,
若u的最远距离是经过u的,那v的答案应该是v子树内的答案和u的次大值比较,否则v的答案和u的最大值比较
画个图就明白了
#include"cstdio" #include"queue" #include"cmath" #include"stack" #include"iostream" #include"algorithm" #include"cstring" #include"queue" #include"map" #include"set" #include"vector" #include"bitset" #define LL long long #define ull unsigned long long #define mems(a,b) memset(a,b,sizeof(a)) #define mdzz int mid=(L+R)>>1 #pragma comment(linker, "/STACK:1024000000,1024000000") using namespace std; const int N = 1e4+5; const int M = 2e4+5; const int MOD = 998244353; const int INF = 0x3f3f3f3f; int tot; int first[N]; int mx[N][2],id[N][2]; struct node{ int e,next,w; node(){} node(int a,int b,int c):e(a),next(b),w(c){} }edge[M]; void init(){ tot=0; mems(first,-1); mems(mx,0); mems(id,-1); } void addedge(int u,int v,int w){ edge[tot]=node(v,first[u],w); first[u]=tot++; edge[tot]=node(u,first[v],w); first[v]=tot++; } void dfs1(int u,int fa){ for(int i=first[u];i!=-1;i=edge[i].next){ int v=edge[i].e; if(v==fa) continue; dfs1(v,u); if(mx[v][0]+edge[i].w>=mx[u][0]){ mx[u][1]=mx[u][0]; id[u][1]=id[u][0];id[u][0]=v; mx[u][0]=mx[v][0]+edge[i].w; } else if(mx[v][0]+edge[i].w>mx[u][1]) mx[u][1]=mx[v][0]+edge[i].w,id[u][1]=v; } } void dfs2(int u,int fa){ for(int i=first[u];i!=-1;i=edge[i].next){ int v=edge[i].e; if(v==fa) continue; if(id[u][0]==v){ if(mx[v][1]<mx[u][1]+edge[i].w){ mx[v][1]=mx[u][1]+edge[i].w; id[v][1]=u; } } else{ if(mx[v][1]<mx[u][0]+edge[i].w){ mx[v][1]=edge[i].w+mx[u][0]; id[v][1]=u; } } if(mx[v][0]<mx[v][1]){ swap(mx[v][0],mx[v][1]); swap(id[v][0],id[v][1]); } dfs2(v,u); } } int n,u,w; int main(){ //freopen("in.txt","r",stdin); while(~scanf("%d",&n)){ init(); for(int i=2;i<=n;i++){ scanf("%d%d",&u,&w); addedge(i,u,w); } dfs1(1,-1); dfs2(1,-1); for(int i=1;i<=n;i++) printf("%d\n",mx[i][0]); } return 0; }
5.POJ 2152
题意:树中每个结点有两个值:w[i]表示在i建设防火设施的价格,d[i]表示与i最近的防火设施的距离上限,求满足所有d[i]的最小花费
算是一道比较难的树形dp,状态和普通的树形DP略有不同
树形dp很多时候是把一个结点及其子树看成一个整体,然后考虑这个结点的状态进行转移
考虑到数据范围N<=1000,可以定义状态dp[u][i]表示u依靠i时,保证子树内安全的最小花费
为了转移方便,再定义all[u]表示保证u的安全的最小花费
其实可以理解为all[u]是在dp[u][i]中取了个最优值
要确定一个点是否能被u依靠就需要知道u与该点的距离
所以先n^2处理树中任意两点的距离
考虑叶子结点:dp[u][i]=w[i]
考虑一个非叶子结点u,先枚举它依靠的结点i
再考虑u的儿子v,v可以选择依靠或者不依靠i,则dp[u][i]+=min(dp[v][i]-c[i],all[v])
对于u的每一个i更新u的最优解all[u]
对于u的孙子k是不需要考虑的,因为k依靠i的情况在解决v的时候已经考虑到了,所以不会有重复计算的情况
#include"cstdio" #include"queue" #include"cmath" #include"stack" #include"iostream" #include"algorithm" #include"cstring" #include"queue" #include"map" #include"set" #include"vector" #include"bitset" #define LL long long #define ull unsigned long long #define mems(a,b) memset(a,b,sizeof(a)) #define mdzz int mid=(L+R)>>1 #define ls pos<<1 #define rs pos<<1|1 #define lson L,mid,pos<<1 #define rson mid+1,R,pos<<1|1 #pragma comment(linker, "/STACK:1024000000,1024000000") using namespace std; const int N = 1e3+5; const int M = 2e3+5; const int MOD = 998244353; const int INF = 0x3f3f3f3f; int tot; int first[N]; int dp[N][N],all[N]; int n,u,v,x; int cost[N],d[N],dis[N][N]; struct node{ int e,next,w; node(){} node(int a,int b,int c):e(a),next(b),w(c){} }edge[M]; void init(){ tot=0; mems(first,-1); mems(all,INF); mems(dp,INF); } void addedge(int u,int v,int w){ edge[tot]=node(v,first[u],w); first[u]=tot++; edge[tot]=node(u,first[v],w); first[v]=tot++; } void dfs1(int rt,int u,int fa){ for(int i=first[u];i!=-1;i=edge[i].next){ int v=edge[i].e; if(v==fa) continue; dis[rt][v]=dis[rt][u]+edge[i].w; dfs1(rt,v,u); } } void dfs2(int u,int fa){ for(int i=first[u];i!=-1;i=edge[i].next){ int v=edge[i].e; if(v==fa) continue; dfs2(v,u); } for(int k=1;k<=n;k++) if(dis[u][k]<=d[u]){ dp[u][k]=cost[k]; for(int i=first[u];i!=-1;i=edge[i].next){ int v=edge[i].e; if(v==fa) continue; dp[u][k]+=min(dp[v][k]-cost[k],all[v]); } all[u]=min(all[u],dp[u][k]); } } int T; int main(){ //freopen("in.txt","r",stdin); for(int i=0;i<N;i++) dis[i][i]=0; scanf("%d",&T); while(T--){ init(); scanf("%d",&n); for(int i=1;i<=n;i++) scanf("%d",&cost[i]); for(int i=1;i<=n;i++) scanf("%d",&d[i]); for(int i=1;i<n;i++){ scanf("%d%d%d",&u,&v,&x); addedge(u,v,x); } for(int i=1;i<=n;i++) dfs1(i,i,-1); dfs2(1,-1); printf("%d\n",all[1]); } return 0; }
6.POJ 3162
题意:对于树中每一个结点i找到另一个结点使得两者距离dp[i]最远,最后输出一段最长区间的长度,区间maxv-minv<=M
只是在树形dp上加了点东西而已,用线段树+two pointer维护就好了
#include"cstdio" #include"queue" #include"cmath" #include"stack" #include"iostream" #include"algorithm" #include"cstring" #include"queue" #include"map" #include"set" #include"vector" #include"bitset" #define LL long long #define ull unsigned long long #define mems(a,b) memset(a,b,sizeof(a)) #define mdzz int mid=(L+R)>>1 #define ls pos<<1 #define rs pos<<1|1 #define lson L,mid,pos<<1 #define rson mid+1,R,pos<<1|1 #pragma comment(linker, "/STACK:1024000000,1024000000") using namespace std; const int N = 1e6+5; const int M = 2e6+5; const int MOD = 998244353; const int INF = 0x3f3f3f3f; int tot; int first[N]; int dp[N][2],id[N][2]; struct node{ int e,next,w; node(){} node(int a,int b,int c):e(a),next(b),w(c){} }edge[M]; void init(){ tot=0; mems(first,-1); } void addedge(int u,int v,int w){ edge[tot]=node(v,first[u],w); first[u]=tot++; edge[tot]=node(u,first[v],w); first[v]=tot++; } void update(int u){ if(dp[u][0]<dp[u][1]){ swap(dp[u][0],dp[u][1]); swap(id[u][0],id[u][1]); } } void dfs1(int u,int fa){ dp[u][1]=dp[u][0]=0; for(int i=first[u];i!=-1;i=edge[i].next){ int v=edge[i].e; if(v==fa) continue; dfs1(v,u); if(edge[i].w+dp[v][0]>dp[u][1]){ dp[u][1]=edge[i].w+dp[v][0]; id[u][1]=v; } update(u); } } void dfs2(int u,int fa){ for(int i=first[u];i!=-1;i=edge[i].next){ int v=edge[i].e; if(v==fa) continue; if(id[u][0]==v){ if(dp[u][1]+edge[i].w>dp[v][1]){ dp[v][1]=dp[u][1]+edge[i].w; id[v][1]=u; } } else{ if(dp[u][0]+edge[i].w>dp[v][1]){ dp[v][1]=dp[u][0]+edge[i].w; id[v][1]=u; } } update(v); dfs2(v,u); } } int minv[N<<2],maxv[N<<2]; void build(int L,int R,int pos){ if(L==R){ minv[pos]=maxv[pos]=dp[L][0]; return; } mdzz; build(lson); build(rson); minv[pos]=min(minv[ls],minv[rs]); maxv[pos]=max(maxv[ls],maxv[rs]); } int query_min(int l,int r,int L,int R,int pos){ if(l<=L&&R<=r) return minv[pos]; mdzz; int ans=INF; if(l<=mid) ans=min(ans,query_min(l,r,lson)); if(r>mid) ans=min(ans,query_min(l,r,rson)); return ans; } int query_max(int l,int r,int L,int R,int pos){ if(l<=L&&R<=r) return maxv[pos]; mdzz; int ans=-INF; if(l<=mid) ans=max(ans,query_max(l,r,lson)); if(r>mid) ans=max(ans,query_max(l,r,rson)); return ans; } int n,m,u,v; int main(){ //freopen("in.txt","r",stdin); while(~scanf("%d%d",&n,&m)){ init(); for(int i=2;i<=n;i++){ scanf("%d%d",&u,&v); addedge(i,u,v); } dfs1(1,-1); dfs2(1,-1); int ans=0; build(1,n,1); int l=1,r=1; while(1){ if(l>r||l>n||r>n) break; int mxv=query_max(l,r,1,n,1); int miv=query_min(l,r,1,n,1); if(mxv-miv<=m){ ans=max(ans,r-l+1); r++; } else l++; } printf("%d\n",ans); } return 0; }
7.codeforces 219D
题意:树边有方向,选择一个点,翻转最少路径,使得其能到达其余所有的点,输出所有可能的答案
可以将翻转理解为一种花费,那不翻转就是花费0,翻转则为1
可以定义dp[u]表示u到所有点的距离和,那dp[u]最小的就是答案
依旧先考虑u的子树内的答案
考虑一个叶子:dp[u]=0;
考虑一个非叶子u以及其的一个儿子v:很容易想到dp[u]+=dp[v]+w[u,v]
一次dfs下来后rt的答案已知,接下来通过rt来推其余结点对于整棵树的答案
考虑结点u及其一个儿子v,v到v子树内的答案已知,v到u除v子树的结点的答案是dp[u]-dp[v]-w[u,v]
所以dp[v]+=dp[u]-dp[v]-w[u,v]+w[v,u]
#include"cstdio" #include"queue" #include"cmath" #include"stack" #include"iostream" #include"algorithm" #include"cstring" #include"queue" #include"map" #include"set" #include"vector" #include"bitset" #define LL long long #define ull unsigned long long #define mems(a,b) memset(a,b,sizeof(a)) #define mdzz int mid=(L+R)>>1 #pragma comment(linker, "/STACK:1024000000,1024000000") using namespace std; const int N = 2e5+5; const int M = 4e5+5; const int MOD = 998244353; const int INF = 0x3f3f3f3f; int tot; int first[N]; int dp[N],sz[N]; struct node{ int e,next,w; node(){} node(int a,int b,int c):e(a),next(b),w(c){} }edge[M]; void init(){ tot=0; mems(first,-1); } void addedge(int u,int v){ edge[tot]=node(v,first[u],0); first[u]=tot++; edge[tot]=node(u,first[v],1); first[v]=tot++; } void dfs1(int u,int fa){ dp[u]=0;sz[u]=1; for(int i=first[u];i!=-1;i=edge[i].next){ int v=edge[i].e; if(v==fa) continue; dfs1(v,u); sz[u]+=sz[v]; dp[u]+=dp[v]+edge[i].w; } } void dfs2(int u,int fa){ for(int i=first[u];i!=-1;i=edge[i].next){ int v=edge[i].e; if(v==fa) continue; dp[v]+=(dp[u]-dp[v]-edge[i].w)+(edge[i].w^1); dfs2(v,u); } } int n,u,v; int main(){ init(); scanf("%d",&n); for(int i=1;i<n;i++){ scanf("%d%d",&u,&v); addedge(u,v); } dfs1(1,-1); dfs2(1,-1); int ans=INF; for(int i=1;i<=n;i++) ans=min(ans,dp[i]); //for(int i=1;i<=n;i++) cout<<i<<' '<<dp[i]<<endl; printf("%d\n",ans); for(int i=1;i<=n;i++) if(ans==dp[i]) printf("%d ",i); return 0; }
8.POJ 1155
题意:树中每个叶子有点权表示收入,边权表示花费。选择某些叶子后,不必要的边可删掉,最多选择几个点使得花费>=收入
状态很多时候是根据要求的东西来定义的
这里定义dp[u][i]为子树u取i个叶子的最优解
由于不选叶子的话是不需要任何花费的,所以容易想到dp[u][0]=0
考虑一个叶子,其第二维只有0/1两种取值,1的话明显是dp[u][1]=w[u]
考虑一个非叶子,第二维1~sz[u]都是无法确定的状态,而且考虑到结果可能是负值,而且我们需要的是一个最大值,所以初始化为-INF
对于一个结点u的一个儿子v,枚举在这个儿子中取的叶子数k,维护个最优解就好了
#include"cstdio" #include"queue" #include"cmath" #include"stack" #include"iostream" #include"algorithm" #include"cstring" #include"queue" #include"map" #include"set" #include"vector" #include"bitset" #define LL long long #define ull unsigned long long #define mems(a,b) memset(a,b,sizeof(a)) #define mdzz int mid=(L+R)>>1 #define ls pos<<1 #define rs pos<<1|1 #define lson L,mid,pos<<1 #define rson mid+1,R,pos<<1|1 #pragma comment(linker, "/STACK:1024000000,1024000000") using namespace std; const int N = 3e3+5; const int M = 2e3+5; const int MOD = 998244353; const int INF = 0x3f3f3f3f; struct node{ int e,next,w; node(){} node(int a,int b,int c):e(a),next(b),w(c){} }edge[N<<1]; int tot; int first[N],w[N],dp[N][N],sz[N]; void init(){ tot=0; mems(first,-1); mems(w,0); } void addedge(int u,int v,int w){ edge[tot]=node(v,first[u],w); first[u]=tot++; edge[tot]=node(u,first[v],w); first[v]=tot++; } void dfs1(int u,int fa){ int f=0;sz[u]=0;dp[u][0]=0; for(int i=first[u];i!=-1;i=edge[i].next){ int v=edge[i].e; if(v==fa) continue; f=1; dfs1(v,u); sz[u]+=sz[v]; } if(!f){ sz[u]=1;dp[u][1]=w[u]; } else{ for(int i=1;i<=sz[u];i++) dp[u][i]=-INF; } } void dfs2(int u,int fa){ for(int i=first[u];i!=-1;i=edge[i].next){ int v=edge[i].e; if(v==fa) continue; dfs2(v,u); for(int j=sz[u];j>=1;j--) for(int k=1;k<=sz[v];k++) if(j>=k) dp[u][j]=max(dp[u][j],dp[u][j-k]+dp[v][k]-edge[i].w); } } int n,m,u,v,k; int main(){ //freopen("in.txt","r",stdin); while(~scanf("%d%d",&n,&m)){ init(); for(int i=1;i<=n-m;i++){ scanf("%d",&k); for(int j=0;j<k;j++){ scanf("%d%d",&u,&v); addedge(i,u,v); } } for(int i=n-m+1;i<=n;i++) scanf("%d",&w[i]); dfs1(1,-1); dfs2(1,-1); for(int i=m;i>=0;i--) if(dp[1][i]>=0){ printf("%d\n",i); break; } } return 0; }
9.HDU 1011
题意:树中每一个结点有一个花费一个收益,问K元能取得的最大收益是多少
明显的树形背包,一个结点可以看成一个物品
XJB搞搞就行了
有个坑是a/20向上取整不能写成(a-1)/20+1,a可能是0
#include"cstdio" #include"queue" #include"cmath" #include"stack" #include"iostream" #include"algorithm" #include"cstring" #include"queue" #include"map" #include"set" #include"vector" #include"bitset" #define LL long long #define ull unsigned long long #define mems(a,b) memset(a,b,sizeof(a)) #define mdzz int mid=(L+R)>>1 #define ls pos<<1 #define rs pos<<1|1 #define lson L,mid,pos<<1 #define rson mid+1,R,pos<<1|1 #pragma comment(linker, "/STACK:1024000000,1024000000") using namespace std; const int N = 105; const int M = 2e3+5; const int MOD = 998244353; const int INF = 0x3f3f3f3f; struct node{ int e,next; node(){} node(int a,int b):e(a),next(b){} }edge[N<<1]; int tot,n,m; int first[N],c[N],p[N],dp[N][N]; void init(){ tot=0; mems(first,-1); mems(dp,0); } void addedge(int u,int v){ edge[tot]=node(v,first[u]); first[u]=tot++; edge[tot]=node(u,first[v]); first[v]=tot++; } void dfs1(int u,int fa){ int x=(c[u]+19)/20; for(int i=x;i<=m;i++) dp[u][i]=p[u]; for(int i=first[u];i!=-1;i=edge[i].next){ int v=edge[i].e; if(v==fa) continue; dfs1(v,u); for(int j=m;j>=x;j--) for(int k=1;k<=j-x;k++) dp[u][j]=max(dp[u][j],dp[u][j-k]+dp[v][k]); } } int main(){ //freopen("in.txt","r",stdin); while(~scanf("%d%d",&n,&m)){ if(n==-1&&m==-1) break; init(); for(int i=1;i<=n;i++) scanf("%d%d",&c[i],&p[i]); int u,v; for(int i=1;i<n;i++){ scanf("%d%d",&u,&v); addedge(u,v); } if(!m){ puts("0"); continue; } dfs1(1,-1); printf("%d\n",dp[1][m]); } return 0; }
10.POJ 1947
题意:问最少需要破坏多少条边能产生一个大小为K的块
dp[u][i]表示u的子树中产生一个i大小的块需要破坏的最少边数
num[u]表示u的儿子数
这题考虑的方向不同平常
考虑块的大小1~sz[u] (0的大小是不合法的状态)
若要在u的子树中产生一个1大小的块,初始情况应该是u和所有儿子的连边断开,所以dp[u][1]=num[u]
其实这样处理相当于对于每一个状态来说u都是默认取了的
考虑一个结点u及其一个儿子v,对于状态dp[u][i] 枚举在v中取的结点数k
最后要枚举一个块的根,因为我们之前处理状态的时候是默认根取,所以最优解还可能在子树中
如果加一维状态[0/1]表示u是否取的话应该可以避免这个问题,不过也没有去尝试了
#include"cstdio" #include"queue" #include"cmath" #include"stack" #include"iostream" #include"algorithm" #include"cstring" #include"queue" #include"map" #include"set" #include"vector" #include"bitset" #define LL long long #define ull unsigned long long #define mems(a,b) memset(a,b,sizeof(a)) #define mdzz int mid=(L+R)>>1 #define ls pos<<1 #define rs pos<<1|1 #define lson L,mid,pos<<1 #define rson mid+1,R,pos<<1|1 #pragma comment(linker, "/STACK:1024000000,1024000000") using namespace std; const int N = 155; const int M = 2e3+5; const int MOD = 998244353; const int INF = 0x3f3f3f3f; struct node{ int e,next; node(){} node(int a,int b):e(a),next(b){} }edge[N<<1]; int tot,n,m; int first[N],dp[N][N],sz[N],num[N]; void init(){ tot=0; mems(first,-1); mems(dp,INF); } void addedge(int u,int v){ edge[tot]=node(v,first[u]); first[u]=tot++; edge[tot]=node(u,first[v]); first[v]=tot++; } void dfs1(int u,int fa){ sz[u]=1;num[u]=0; for(int i=first[u];i!=-1;i=edge[i].next){ int v=edge[i].e; if(v==fa) continue; dfs1(v,u); num[u]++; sz[u]+=sz[v]; } } void dfs2(int u,int fa){ dp[u][1]=num[u]; for(int i=first[u];i!=-1;i=edge[i].next){ int v=edge[i].e; if(v==fa) continue; dfs2(v,u); for(int j=m;j>=1;j--) for(int k=1;k<=sz[v];k++) if(j-k>=1) dp[u][j]=min(dp[u][j],dp[u][j-k]+dp[v][k]-1); } } int main(){ //freopen("in.txt","r",stdin); while(~scanf("%d%d",&n,&m)){ init(); int u,v; for(int i=1;i<n;i++){ scanf("%d%d",&u,&v); addedge(u,v); } dfs1(1,-1); dfs2(1,-1); /*for(int i=1;i<=n;i++){ cout<<i<<":"<<endl; for(int j=1;j<=sz[i];j++) cout<<dp[i][j]<<' '; cout<<endl; }*/ int ans=dp[1][m]; for(int i=2;i<=n;i++) ans=min(ans,dp[i][m]+1); printf("%d\n",ans); } return 0; }
11.HDU 1561
题意:每个结点有一个权值,问选择K个结点(必须按路径选择)获得的最大权值
典型的树形背包
由于不是任意选,我们只需要在状态转移的时候变通一下就好了
对于u来说,我要选择其子孙的话前提是u选择
所以对第二维i==1的时候单独处理,而且之后不再更新这个值就能保证u是默认被选择的
#include"cstdio" #include"queue" #include"cmath" #include"stack" #include"iostream" #include"algorithm" #include"cstring" #include"queue" #include"map" #include"set" #include"vector" #include"bitset" #define LL long long #define ull unsigned long long #define mems(a,b) memset(a,b,sizeof(a)) #define mdzz int mid=(L+R)>>1 #define ls pos<<1 #define rs pos<<1|1 #define lson L,mid,pos<<1 #define rson mid+1,R,pos<<1|1 #pragma comment(linker, "/STACK:1024000000,1024000000") using namespace std; const int N = 205; const int M = 2e3+5; const int MOD = 998244353; const int INF = 0x3f3f3f3f; struct node{ int e,next; node(){} node(int a,int b):e(a),next(b){} }edge[N]; int tot,n,m; int first[N],dp[N][N],w[N]; void init(){ tot=0; mems(first,-1); mems(dp,0); } void addedge(int u,int v){ edge[tot]=node(v,first[u]); first[u]=tot++; //edge[tot]=node(u,first[v]); // first[v]=tot++; } void dfs(int u){ dp[u][1]=w[u]; for(int i=first[u];i!=-1;i=edge[i].next){ int v=edge[i].e; dfs(v); for(int j=m;j>=2;j--) for(int k=0;k<=m;k++) if(j-1>=k) dp[u][j]=max(dp[u][j],dp[u][j-k]+dp[v][k]); } } int main(){ //freopen("in.txt","r",stdin); w[0]=0; while(scanf("%d%d",&n,&m)&&(n||m)){ init();m++; int rt=0,u; for(int i=1;i<=n;i++){ scanf("%d%d",&u,&w[i]); addedge(u,i); } dfs(rt); printf("%d\n",dp[rt][m]); } return 0; }
12.HDU 4003
题意:树中每条边有花费,有k个机器人,问遍历所有的点的最少花费(可以回头,每次只能从s出发
这一题的状态定义很有意思
dp[u][i]表示遍历完u的子树后有i个机器人没有回到u结点的最优解
对于每一棵子树都是需要遍历完的,所以必须选择一个方案
为了保证至少选择一个方案,所以在考虑当前儿子v的时候先将答案加上一个状态,这里是加上没有机器人留在v子树的方案
然后再对v做一个背包,若有更优解则初始放置的选择将被替换掉
其实相当于对于子树v有k件物品来选择,必须选择一件
#include"cstdio" #include"queue" #include"cmath" #include"stack" #include"iostream" #include"algorithm" #include"cstring" #include"queue" #include"map" #include"set" #include"vector" #include"bitset" #define LL long long #define ull unsigned long long #define mems(a,b) memset(a,b,sizeof(a)) #define mdzz int mid=(L+R)>>1 #pragma comment(linker, "/STACK:1024000000,1024000000") using namespace std; const int N = 1e4+5; const int M = N<<1; const LL MOD = 998244353; const int INF = 0x3f3f3f3f; struct node{ int e,next,w; node(){} node(int a,int b,int c):e(a),next(b),w(c){} }edge[N<<1]; int tot; int first[N],dp[N][12]; void init(){ tot=0; mems(first,-1); mems(dp,0); } void addedge(int u,int v,int w){ edge[tot]=node(v,first[u],w); first[u]=tot++; edge[tot]=node(u,first[v],w); first[v]=tot++; } int n,s,m; void dfs(int u,int fa){ for(int i=first[u];i!=-1;i=edge[i].next){ int v=edge[i].e; if(v==fa) continue; dfs(v,u); for(int j=m;j>=0;j--){ dp[u][j]+=dp[v][0]+edge[i].w*2; for(int k=1;k<=j;k++) dp[u][j]=min(dp[u][j],dp[v][k]+dp[u][j-k]+edge[i].w*k); } } } int main(){ //freopen("in.txt","r",stdin); while(~scanf("%d%d%d",&n,&s,&m)){ init(); int u,v,w; for(int i=1;i<n;i++){ scanf("%d%d%d",&u,&v,&w); addedge(u,v,w); } dfs(s,-1); printf("%d\n",dp[s][m]); } return 0; }
13.HDU 4276
题意:每个点有点权,边有花费,问T时间能否从1到达n,若能,能获取的最大权值是多少
首先想到树中两点的路径是唯一的,既然要从1到达n,先考虑最短路径能否到达
之后剩余的时间作为背包容量,因为其余的点都是非必须的,所以去了必须还要回来,故每条非必要的边花费都需要*2
所以做法就是先把总时间减去1到n的路径距离,并把路径上的花费置零
然后对于每一个结点做背包,对于u的儿子v枚举一个花费k,注意边的花费要double
#include"cstdio" #include"queue" #include"cmath" #include"stack" #include"iostream" #include"algorithm" #include"cstring" #include"queue" #include"map" #include"set" #include"vector" #include"bitset" #define LL long long #define ull unsigned long long #define mems(a,b) memset(a,b,sizeof(a)) #define mdzz int mid=(L+R)>>1 #pragma comment(linker, "/STACK:1024000000,1024000000") using namespace std; const int N = 105; const int M = N<<1; const LL MOD = 998244353; const int INF = 0x3f3f3f3f; struct node{ int e,next,w; node(){} node(int a,int b,int c):e(a),next(b),w(c){} }edge[N<<1]; int tot; int first[N],dp[N][N*5],pre[N],id[N],w[N],inq[N],dis[N]; void init(){ tot=0; mems(first,-1); mems(dp,0); mems(inq,0); mems(dis,INF); } void addedge(int u,int v,int W){ edge[tot]=node(v,first[u],W); first[u]=tot++; edge[tot]=node(u,first[v],W); first[v]=tot++; } int n,m,cnt; queue<int>q; void bfs(int s){ while(!q.empty()) q.pop(); pre[s]=1;dis[s]=0; inq[s]=1; q.push(s); while(!q.empty()){ int u=q.front();q.pop();inq[u]=0; if(u==n) break; for(int i=first[u];i!=-1;i=edge[i].next){ int v=edge[i].e; if(!inq[v]&&dis[u]+edge[i].w<dis[v]){ dis[v]=dis[u]+edge[i].w; q.push(v); inq[u]=1; pre[v]=u; id[v]=i; } } } cnt=0; int u=n; while(u!=1){ cnt+=edge[id[u]].w; edge[id[u]].w=edge[id[u]^1].w=0; u=pre[u]; } } void dfs2(int u,int fa){ for(int i=0;i<=m;i++) dp[u][i]=w[u]; for(int i=first[u];i!=-1;i=edge[i].next){ int v=edge[i].e; if(v==fa) continue; dfs2(v,u); for(int j=m;j>=0;j--) for(int k=0;k<=j;k++) if(j-edge[i].w*2-k>=0) dp[u][j]=max(dp[u][j],dp[v][k]+dp[u][j-edge[i].w*2-k]); } } int u,v,W; int main(){ //freopen("in.txt","r",stdin); while(~scanf("%d%d",&n,&m)){ init(); for(int i=1;i<n;i++){ scanf("%d%d%d",&u,&v,&W); addedge(u,v,W); } for(int i=1;i<=n;i++) scanf("%d",&w[i]); bfs(1); if(cnt>m){ printf("Human beings die in pursuit of wealth, and birds die in pursuit of food!\n"); continue; } m-=cnt; dfs2(1,-1); /*for(int i=1;i<=n;i++){ cout<<i<<':'<<endl; for(int j=0;j<=m;j++) cout<<dp[i][j]<<' ';cout<<endl; }*/ printf("%d\n",dp[1][m]); } return 0; }
14.HDU 3586
题意:给一个限制m,切断的路径权值和不超过m,单个边权值也不超过k,求最小的k使得所有叶子和根不相连
二分一个k
对于一个确定的k,dp[u]表示u的叶子全部和u分离需要的最小花费
考虑叶子节点:dp[u]=INF(不合法状态
考虑非叶子结点:一开始是没有和儿子相连,所以dp[u]=0
考虑u的一个儿子v,若(u,v)这条边是<=lim,则可以选择在消除这条边或者是在v的子树中消除,两者取一个最优解
最后判断dp[1]是否小于给定的m
#include"cstdio" #include"queue" #include"cmath" #include"stack" #include"iostream" #include"algorithm" #include"cstring" #include"queue" #include"map" #include"set" #include"vector" #include"bitset" #define LL long long #define ull unsigned long long #define mems(a,b) memset(a,b,sizeof(a)) #define mdzz int mid=(L+R)>>1 #pragma comment(linker, "/STACK:1024000000,1024000000") using namespace std; const int N = 1005; const int M = N<<1; const LL MOD = 998244353; const int INF = 0x3f3f3f; struct node{ int e,next,w; node(){} node(int a,int b,int c):e(a),next(b),w(c){} }edge[N<<1]; int tot,n,m; int first[N],dp[N]; void init(){ tot=0; mems(first,-1); } void addedge(int u,int v,int w){ edge[tot]=node(v,first[u],w); first[u]=tot++; edge[tot]=node(u,first[v],w); first[v]=tot++; } void dfs(int u,int fa,int lim){ dp[u]=0;int f=0; for(int i=first[u];i!=-1;i=edge[i].next){ int v=edge[i].e; if(v==fa) continue; dfs(v,u,lim);f=1; int tmp; if(edge[i].w<=lim) tmp=edge[i].w; else tmp=INF; dp[u]+=min(tmp,dp[v]); } if(!f) dp[u]=INF; } bool check(int mid){ mems(dp,INF); dfs(1,-1,mid); return dp[1]<=m; } int u,v,w; int main(){ //freopen("in.txt","r",stdin); while(scanf("%d%d",&n,&m)&&(n||m)){ init(); for(int i=1;i<n;i++){ scanf("%d%d%d",&u,&v,&w); addedge(u,v,w); } int low=1,high=INF,mid,ans=-1; while(low<=high){ mid=(low+high)>>1; if(check(mid)){ ans=mid; high=mid-1; } else low=mid+1; } printf("%d\n",ans); } return 0; }
15. POJ 3107
题意:输出树中的结点k,删去k后产生的最大的联通块最小
直接两次dp就好了,第一次处理子树第二次考虑父亲除去当前结点产生的最大块,维护个最大值
#include"cstdio" #include"queue" #include"cmath" #include"stack" #include"iostream" #include"algorithm" #include"cstring" #include"queue" #include"map" #include"set" #include"vector" #include"bitset" #define LL long long #define ull unsigned long long #define mems(a,b) memset(a,b,sizeof(a)) #define mdzz int mid=(L+R)>>1 #pragma comment(linker, "/STACK:1024000000,1024000000") using namespace std; const int N = 50005; const int M = N<<1; const LL MOD = 998244353; const int INF = 0x3f3f3f; struct node{ int e,next,w; node(){} node(int a,int b):e(a),next(b){} }edge[N<<1]; int tot,n,m; int first[N],dp[N],sz[N]; vector<int> ans; void init(){ tot=0; ans.clear(); mems(first,-1); } void addedge(int u,int v){ edge[tot]=node(v,first[u]); first[u]=tot++; edge[tot]=node(u,first[v]); first[v]=tot++; } void dfs1(int u,int fa){ sz[u]=1;dp[u]=0; for(int i=first[u];i!=-1;i=edge[i].next){ int v=edge[i].e; if(v==fa) continue; dfs1(v,u); sz[u]+=sz[v]; dp[u]=max(dp[u],sz[v]); } } void dfs2(int u,int fa){ for(int i=first[u];i!=-1;i=edge[i].next){ int v=edge[i].e; if(v==fa) continue; dp[v]=max(dp[v],n-sz[v]); dfs2(v,u); } } int main(){ //freopen("in.txt","r",stdin); while(~scanf("%d",&n)){ init(); int u,v; for(int i=1;i<n;i++){ scanf("%d%d",&u,&v); addedge(u,v); } dfs1(1,-1); dfs2(1,-1); int cnt=INF; for(int i=1;i<=n;i++) cnt=min(cnt,dp[i]); //for(int i=1;i<=n;i++) cout<<i<<' '<<dp[i]<<endl; for(int i=1;i<=n;i++) if(dp[i]==cnt) ans.push_back(i); for(int i=0;i<ans.size();i++){ if(i) printf(" "); printf("%d",ans[i]); } puts(""); } return 0; }
16.POJ 3140
题意:选择一条树边断开,使得分成的两部分的总点权差最小,输出最小值
就直接预处理每一个点及其子树的总点权
枚举一个点和其父亲断开,取个最优值就好了
#include"cstdio" #include"queue" #include"cmath" #include"stack" #include"iostream" #include"algorithm" #include"cstring" #include"queue" #include"map" #include"set" #include"vector" #include"bitset" #define LL long long #define ull unsigned long long #define mems(a,b) memset(a,b,sizeof(a)) #define mdzz int mid=(L+R)>>1 #pragma comment(linker, "/STACK:1024000000,1024000000") using namespace std; const int N = 1e5+5; const int M = N<<1; const LL MOD = 998244353; const LL INF = 1e18; struct node{ int e,next; node(){} node(int a,int b):e(a),next(b){} }edge[N<<1]; int tot,n,m; int first[N]; LL sz[N],num[N],cnt; void init(){ tot=0;cnt=0; mems(first,-1); } void addedge(int u,int v){ edge[tot]=node(v,first[u]); first[u]=tot++; edge[tot]=node(u,first[v]); first[v]=tot++; } void dfs(int u,int fa){ sz[u]=num[u]; for(int i=first[u];i!=-1;i=edge[i].next){ int v=edge[i].e; if(v==fa) continue; dfs(v,u); sz[u]+=sz[v]; } } int u,v,cas=1; int main(){ //freopen("in.txt","r",stdin); while(scanf("%d%d",&n,&m)&&(n||m)){ init(); for(int i=1;i<=n;i++) scanf("%lld",&num[i]),cnt+=num[i]; for(int i=1;i<n;i++){ scanf("%d%d",&u,&v); addedge(u,v); } dfs(1,-1); LL ans=INF; for(int i=2;i<=n;i++){ LL tmp=cnt-sz[i]*2; if(tmp<0) tmp*=-1; ans=min(ans,tmp); } printf("Case %d: %lld\n",cas++,ans); } return 0; }
17. POJ 2486
题意:从树根1走K步能获得的最大点权,可以走回头路
dp[u][i][0/1]表示从u出发走i步,最终回到u/不回到u的最优值
考虑一个叶子结点:dp[u][0][0]=w[u];
考虑一个非叶子结点u及其一个儿子v,枚举一个k表示对于这个儿子v走的步数
有3种结果:留在v子树,回到u,留在u其它子树
画图就能看出三种情况的转移:
留在v子树:状态分裂为dp[u][j-k-1][0],dp[v][k][1] 花费为1步
回到u:状态分裂为dp[u][j-k-2][0],dp[v][k][0] 花费为2步
留在其他子树:状态分裂为dp[u][j-k-2][1],dp[v][k][0] 花费为2步
#include"cstdio" #include"queue" #include"cmath" #include"stack" #include"iostream" #include"algorithm" #include"cstring" #include"queue" #include"map" #include"set" #include"vector" #include"bitset" #define LL long long #define ull unsigned long long #define mems(a,b) memset(a,b,sizeof(a)) #define mdzz int mid=(L+R)>>1 #pragma comment(linker, "/STACK:1024000000,1024000000") using namespace std; const int N = 105; const int M = N<<1; const LL MOD = 998244353; const int INF = 0x3f3f3f3f; struct node{ int e,next,w; node(){} node(int a,int b):e(a),next(b){} }edge[N<<1]; int tot; int first[N],dp[N][N<<1][2],w[N]; void init(){ tot=0; mems(first,-1); mems(dp,0); } void addedge(int u,int v){ edge[tot]=node(v,first[u]); first[u]=tot++; edge[tot]=node(u,first[v]); first[v]=tot++; } int n,m; void dfs(int u,int fa){ for(int i=0;i<=m;i++) dp[u][i][0]=dp[u][i][1]=w[u]; for(int i=first[u];i!=-1;i=edge[i].next){ int v=edge[i].e; if(v==fa) continue; dfs(v,u); for(int j=m;j>=1;j--) for(int k=0;k<=j;k++){ if(j>=k+2) dp[u][j][0]=max(dp[u][j][0],dp[v][j-k-2][0]+dp[u][k][0]); if(j>=k+1) dp[u][j][1]=max(dp[u][j][1],dp[v][j-k-1][1]+dp[u][k][0]); if(j>=k+2) dp[u][j][1]=max(dp[u][j][1],dp[v][j-k-2][0]+dp[u][k][1]); } } } int main(){ //freopen("in.txt","r",stdin); while(~scanf("%d%d",&n,&m)){ init(); for(int i=1;i<=n;i++) scanf("%d",&w[i]); int u,v; for(int i=1;i<n;i++){ scanf("%d%d",&u,&v); addedge(u,v); } dfs(1,-1); printf("%d\n",max(dp[1][m][0],dp[1][m][1])); } return 0; }
18. HDU 4044
题意:每个点有ki种选择,每种选择对应一种一种花费一种收益 问拥有m元,令x为1到所有叶子(不含1)的路径的点权和最小值
求x的最大值
一开始的想法是的定义dp[u][i][0/1]为u子树花i元,u选或者不选的答案
但是后面会发现这样定义状态的话对于dp[u][i][1]的合并不是很好处理,因为我没有记录u选择的是哪个方案
看了题解后学了一个新姿势
定义dp[u][i]为u不选的时候花i元的最优解
预处理一个w[u][i]表示u结点花i元最多能获得的权值
考虑一个叶子结点:dp[u][i]=w[u][i]
考虑一个非叶子结点不选时候:每一个儿子v加进来的时候,状态都分裂为dp[v][k]和dp[u][j-k]
对于每一个枚举的k来说,答案是min(dp[v][k],dp[u][j-k]) 维护这个答案的最大值就是dp[u][j]
考虑完u不选的情况后,再对u单独做一次背包,枚举u的花费,取个最优值就是答案
#include"cstdio" #include"queue" #include"cmath" #include"stack" #include"iostream" #include"algorithm" #include"cstring" #include"queue" #include"map" #include"set" #include"vector" #include"bitset" #define LL long long #define ull unsigned long long #define mems(a,b) memset(a,b,sizeof(a)) #define mdzz int mid=(L+R)>>1 #pragma comment(linker, "/STACK:1024000000,1024000000") using namespace std; const int N = 1e3+5; const int M = 205; const LL MOD = 998244353; const int INF = 0x3f3f3f3f; struct node{ int e,next; node(){} node(int a,int b):e(a),next(b){} }edge[N<<1]; int tot; int first[N],dp[N][M],kind[N],chose[N][M]; void init(){ tot=0; mems(first,-1); mems(dp,INF); mems(chose,0); } void addedge(int u,int v){ edge[tot]=node(v,first[u]); first[u]=tot++; edge[tot]=node(u,first[v]); first[v]=tot++; } int T,n,m; void dfs1(int u,int fa){ int f=0; for(int i=first[u];i!=-1;i=edge[i].next){ int v=edge[i].e; if(v==fa) continue; dfs1(v,u); f++; for(int j=m;j>=0;j--){ int tmp=0; for(int k=0;k<=j;k++) tmp=max(tmp,min(dp[u][j-k],dp[v][k])); dp[u][j]=tmp; } } if(!f){ for(int i=0;i<=m;i++) dp[u][i]=chose[u][i]; return; } for(int j=m;j>=0;j--) for(int k=0;k<=j;k++) dp[u][j]=max(dp[u][j],dp[u][j-k]+chose[u][k]); } int main(){ //freopen("in.txt","r",stdin); scanf("%d",&T); while(T--){ init(); scanf("%d",&n); int u,v,k; for(int i=1;i<n;i++){ scanf("%d%d",&u,&v); addedge(u,v); } scanf("%d",&m); for(int i=1;i<=n;i++){ scanf("%d",&k); for(int j=1;j<=k;j++){ scanf("%d%d",&u,&v); chose[i][u]=max(chose[i][u],v); } for(int j=1;j<=m;j++) chose[i][j]=max(chose[i][j],chose[i][j-1]); } dfs1(1,-1); printf("%d\n",dp[1][m]); } return 0; }
19.HDU 5758
题意:点有点权边有花费,点权只能获得一次,花费每次经过都要扣除,可以走回头路,问能拿到的最大价值
参考Apple Tree可以知道需要加一维[0/1]表示是否回到u
定义dp[u][0/1]表示从u出发不回/回到u的最优解
求一个结点对于整棵树的信息一般都是先处理子树内的信息,再第二次dfs处理父亲对答案的影响
先考虑子树内:
考虑一个叶子结点:dp[u][0]=dp[u][1]=w[u]
考虑一个非叶子u和他的一个儿子v:
对于dp[u][0]来说,v的贡献只能是dp[v][0]-2*w[u,v],如果这个值小于0我必然不走v
对于dp[u][1]来说,如果不停在v内,v的贡献也是dp[v][0]-2*w[u,v],如果停在u则状态分裂为dp[u][0]+dp[v][1]-w[u,v]
再考虑fa对u的影响
考虑fa对u的影响时一般需要把u对fa的影响先排除
对于dp[fa][0]来说u的影响只能是max(0,dp[u][0]-2*w[fa,u]),直接减去就行了
对于dp[fa][1]来说有两种情况
若最终不停在u,则和dp[fa][0]一样处理
若停在u,则需要对fa再做一次排除u后的背包
这里将fa对u的影响作为参数下传,为的是保证在推u的时候fa的值是对于整棵树的,而其余结点是对于其子树的
之所以这样做是因为用fa更新u的时候,排除u对fa的影响之后fa相当于u的一棵新子树
#include"cstdio" #include"queue" #include"cmath" #include"stack" #include"iostream" #include"algorithm" #include"cstring" #include"queue" #include"map" #include"set" #include"vector" #include"bitset" #define LL long long #define ull unsigned long long #define mems(a,b) memset(a,b,sizeof(a)) #define mdzz int mid=(L+R)>>1 #pragma comment(linker, "/STACK:1024000000,1024000000") using namespace std; const int N = 1e5+50; const int M = N<<1; const LL MOD = 998244353; const int INF = 0x3f3f3f3f; struct node{ int e,next,w; node(){} node(int a,int b,int c):e(a),next(b),w(c){} }edge[N<<1]; int tot,n,m; int first[N],dp[N][2],w[N],id[N]; void init(){ tot=0; for(int i=1;i<=n;i++) first[i]=-1; } void addedge(int u,int v,int W){ edge[tot]=node(v,first[u],W); first[u]=tot++; edge[tot]=node(u,first[v],W); first[v]=tot++; } void dfs1(int u,int fa){ dp[u][0]=dp[u][1]=w[u]; id[u]=-1; for(int i=first[u];i!=-1;i=edge[i].next){ int v=edge[i].e; if(v==fa) continue; dfs1(v,u); int tmp=max(0,dp[v][0]-2*edge[i].w); dp[u][1]+=tmp; if(dp[u][1]<dp[u][0]+max(0,dp[v][1]-edge[i].w)){ dp[u][1]=dp[u][0]+max(0,dp[v][1]-edge[i].w); id[u]=v; } dp[u][0]+=tmp; } } int ans[N]; void dfs2(int u,int fa,int f0,int f1){ int t[2]={dp[u][0],dp[u][1]}; int idd=id[u]; t[1]+=f0; if(t[1]<t[0]+f1){ t[1]=t[0]+f1; idd=fa; } t[0]+=f0; ans[u]=max(t[0],t[1]); for(int i=first[u];i!=-1;i=edge[i].next){ int v=edge[i].e; if(v==fa) continue; if(v==idd){ int tmp0=w[u]+f0,tmp1=w[u]+f1; for(int k=first[u];k!=-1;k=edge[k].next){ int vv=edge[k].e; if(vv==fa||vv==v) continue; int tmp=max(0,dp[vv][0]-2*edge[k].w); tmp1+=tmp; tmp1=max(tmp1,tmp0+max(0,dp[vv][1]-edge[k].w)); tmp0+=tmp; } tmp0=max(0,tmp0-2*edge[i].w); tmp1=max(0,tmp1-edge[i].w); dfs2(v,u,tmp0,tmp1); } else{ int tmp=max(0,dp[v][0]-2*edge[i].w); int tmp0=max(0,t[0]-tmp-2*edge[i].w); int tmp1=max(0,t[1]-tmp-edge[i].w); dfs2(v,u,tmp0,tmp1); } } } int T,u,v,W,cas=1; int main(){ //freopen("in.txt","r",stdin); //freopen("pending.txt","w",stdout); scanf("%d",&T); while(T--){ scanf("%d",&n); init(); for(int i=1;i<=n;i++) scanf("%d",&w[i]); for(int i=1;i<n;i++){ scanf("%d%d%d",&u,&v,&W); addedge(u,v,W); } dfs1(1,-1); dfs2(1,-1,0,0); printf("Case #%d:\n",cas++); for(int i=1;i<=n;i++) printf("%d\n",ans[i]); } return 0; }
20.NUBT 1638
题意:建图略麻烦,抽象出来就是说树中选m个结点,其中rt到每一个叶子的路径上被选的结点数不超过k,问获得的最大权值是多少
定义dp[u][i][j][0/1]表示u子树选i个,最多的路径选了j个,u选/不选
dp值全部初始化为-1表示不合法状态
考虑一个叶子结点:选的话是dp[u][1][1][1]=w[u] 不选dp[u][0][0][0]=0
考虑一个非叶子结点u和他的一个儿子v:
对于状态dp[u][i][j][0],可能转移过来的状态有dp[u][i-k][j][0]+dp[v][k][0~j][0/1]或者dp[u][i-k][0~j][0]+dp[v][k][j][0/1]
对于状态dp[u][i][j][1],有dp[u][i-k][j][1]+dp[v][k][0~j-1][0/1]或dp[u][i-k][0~j][1]+dp[v][k][j-1][0/1](之所以v是j-1为上限是因为u选了的话合并后v的最长链长度必然+1,这样做是为了限制最长链在j范围内)
#include"cstdio" #include"queue" #include"cmath" #include"stack" #include"iostream" #include"algorithm" #include"cstring" #include"queue" #include"map" #include"set" #include"vector" #include"bitset" #define LL long long #define ull unsigned long long #define mems(a,b) memset(a,b,sizeof(a)) #define mdzz int mid=(L+R)>>1 #pragma comment(linker, "/STACK:1024000000,1024000000") using namespace std; const int N = 120; const int M = N<<1; const LL MOD = 998244353; const int INF = 0x3f3f3f3f; struct Point{ int x,y; }p[N<<2]; bool cmp(Point a,Point b){ return a.x<b.x; } struct Node{ int y,l,r; }point[N]; bool cmp1(Node a,Node b){ if(a.y==b.y) return a.l<b.l; return a.y>b.y; } struct node{ int e,next; }edge[N<<1]; int first[N],cnt,tot; int dp[N][N][11][2],w[N],sz[N]; void init(){ tot=0;cnt=0; mems(first,-1); mems(dp,-1); } void addedge(int u,int v){ edge[tot]=(node){v,first[u]}; first[u]=tot++; } void build(int l,int r,int fa){ for(int i=l+1;i<=r;i++){ if(p[l].y==p[i].y){ addedge(fa,++cnt); w[cnt]=p[i].x-p[l].x; //cout<<fa<<' '<<cnt<<' '<<w[cnt]<<endl; build(l+1,i-1,cnt); build(i,r,fa); return ; } else if(p[i].y>p[l].y){ build(l+1,i-1,fa); build(i,r,fa); } } } int n,m,K; void dfs(int u){ dp[u][1][1][1]=w[u]; dp[u][0][0][0]=0; sz[u]=1; for(int i=first[u];i!=-1;i=edge[i].next){ int v=edge[i].e; dfs(v); sz[u]+=sz[v]; for(int j=sz[u];j>=0;j--) for(int k=0;k<=K;k++) for(int p=0;p<=min(sz[v],j);p++) for(int kk=0;kk<=k;kk++){ if(dp[u][j-p][k][0]!=-1&&(dp[v][p][kk][0]!=-1||dp[v][p][kk][1]!=-1)) dp[u][j][k][0]=max(dp[u][j][k][0],dp[u][j-p][k][0]+max(dp[v][p][kk][0],dp[v][p][kk][1])); if(dp[u][j-p][kk][0]!=-1&&(dp[v][p][k][0]!=-1||dp[v][p][k][1]!=-1)) dp[u][j][k][0]=max(dp[u][j][k][0],dp[u][j-p][kk][0]+max(dp[v][p][k][0],dp[v][p][k][1])); if(kk>=1&&dp[u][j-p][k][1]!=-1&&(dp[v][p][kk-1][0]!=-1||dp[v][p][kk-1][1]!=-1)) dp[u][j][k][1]=max(dp[u][j][k][1],dp[u][j-p][k][1]+max(dp[v][p][kk-1][0],dp[v][p][kk-1][1])); if(k>=1&&dp[u][j-p][kk][1]!=-1&&(dp[v][p][k-1][0]!=-1||dp[v][p][k-1][1]!=-1)) dp[u][j][k][1]=max(dp[u][j][k][1],dp[u][j-p][kk][1]+max(dp[v][p][k-1][0],dp[v][p][k-1][1])); } } } int cas=1; int main(){ //freopen("in.txt","r",stdin); while(~scanf("%d%d%d",&n,&m,&K)){ init(); for(int i=1;i<=n;i++) scanf("%d%d",&p[i].x,&p[i].y); sort(p+1,p+1+n,cmp); build(1,n,0); int rt=0; dfs(rt); int ans=-1; for(int i=0;i<K;i++) ans=max(ans,dp[rt][m][i][0]); printf("Case %d: %d\n",cas++,ans); } return 0; }
21.HDU 5758
题意:边花费1,可以从一个点瞬移到另一个点,问瞬移次数最少的前提下遍历所有边的最小花费
可以知道瞬移次数是(叶子数+1)/2
定义dp[u][0]为遍历完u后的最小花费(只是第一次dp,答案不一定是最终答案
考虑非叶子u和他的一个儿子v:
若v有偶数个儿子,则要在瞬移次数最少的情况下遍历(u,v)这条边,就必须有两个结点和v外的结点配对,所以(u,v)对答案贡献2
同理奇数的时候贡献1
如果总叶子数是偶数那答案已经出来了
但是如果总叶子数是奇数这样的答案可能会 偏大
其实奇数的情况就是在偶数的情况加了一条没有分叉的单链
所以再定义dp[u][1]为遍历u后的最小花费(第二次dp,独立与第一次dp
对于一个u,枚举这条单链出现的儿子v:
如果这个儿子就是一条单链,并且u是这条单链的起点,则dp[u][1]=min(dp[u][1],dp[u][0])
如果不是儿子v不是单链则把这条单链从v中剔除,则v中的叶子数的奇偶就变化了
dp[u][1]先减去dp[v][0],再减去奇偶变化的影响d,再加上单链存在与v的dp值dp[v][1],维护最优解
最后若总叶子数是偶数则取第一次dp的结果,否则取第二次的
#include"cstdio" #include"queue" #include"cmath" #include"stack" #include"iostream" #include"algorithm" #include"cstring" #include"queue" #include"map" #include"set" #include"vector" #include"bitset" #define LL long long #define ull unsigned long long #define mems(a,b) memset(a,b,sizeof(a)) #define mdzz int mid=(L+R)>>1 #pragma comment(linker, "/STACK:1024000000,1024000000") using namespace std; const int N = 1e5+5; const int M = N<<1; const LL MOD = 998244353; const int INF = 0x3f3f3f3f; struct node{ int e,next; }edge[N<<1]; int first[N],cnt,tot; LL dp[N][2]; int sz[N],deg[N]; void init(){ tot=0; mems(first,-1); mems(deg,0); mems(dp,INF); } void addedge(int u,int v){ edge[tot]=(node){v,first[u]}; first[u]=tot++; edge[tot]=(node){u,first[v]}; first[v]=tot++; } void dfs(int u,int fa){ dp[u][0]=0;sz[u]=0; int flag=0; for(int i=first[u];i!=-1;i=edge[i].next){ int v=edge[i].e; if(v==fa) continue; flag++; dfs(v,u); sz[u]+=sz[v]; dp[u][0]+=dp[v][0]; if(sz[v]&1) dp[u][0]++; else dp[u][0]+=2; } for(int i=first[u];i!=-1;i=edge[i].next){ int v=edge[i].e; if(v==fa) continue; if(flag>1&&sz[v]==1) dp[u][1]=min(dp[u][1],dp[u][0]); if(dp[v][1]==INF) continue; LL tmp=dp[u][0]-dp[v][0]+dp[v][1]; if(sz[v]&1) tmp++; else tmp--; dp[u][1]=min(dp[u][1],tmp); } if(!flag) sz[u]=1; } int n,m,u,v,T; int main(){ //freopen("in.txt","r",stdin); scanf("%d",&T); while(T--){ init(); scanf("%d",&n); for(int i=1;i<n;i++){ scanf("%d%d",&u,&v); addedge(u,v); deg[v]++;deg[u]++; } int rt=1,cnt=0; for(int i=1;i<=n;i++) { if(deg[i]!=1) rt=i; else cnt++; } dfs(rt,-1); printf("%lld\n",dp[rt][cnt&1]); } return 0; }
22.codeforces Round #322(Div.2) F
题意:把叶子平分染成两种颜色,其余点随便染,求最少有对相邻的点颜色不同 输入保证叶子是偶数个
定义dp[u][i][0/1]表示u子树染i个儿子0色,u染0/1色
这题经典的地方在于叶子和非叶子的初始情况不一样
先dp值全部赋INF表示不合法
对于叶子来说:dp[u][1][0]=0,dp[u][0][1]=0,其余两个状态不合法
考虑非叶子u:
非叶子u的颜色对第二维是没影响的,所以dp[u][0][1]=dp[u][0][0]=0
由于这里是必须要选择儿子的情况,之前有些题目是儿子可以不选
必须选的情况的处理方法有两种,第一种是先强制放一种状态,再枚举其他状态看看是否更优,有就替换,没有就保留原来的
或者说是用一个变量去存放儿子的最优情况,然后把这个最优情况强制合并到原状态里
这里我采用的是第二种方法
#include"cstdio" #include"queue" #include"cmath" #include"stack" #include"iostream" #include"algorithm" #include"cstring" #include"queue" #include"map" #include"set" #include"vector" #include"bitset" #define LL long long #define ull unsigned long long #define mems(a,b) memset(a,b,sizeof(a)) #define mdzz int mid=(L+R)>>1 #pragma comment(linker, "/STACK:1024000000,1024000000") using namespace std; const int N = 5005; const int M = N<<1; const LL MOD = 998244353; const int INF = 0x3f3f3f3f; struct node{ int e,next; }edge[N<<1]; int first[N],tot; int dp[N][N][2],deg[N],leaf[N]; void init(){ tot=0; mems(first,-1); mems(dp,INF); mems(deg,0); } void addedge(int u,int v){ edge[tot]=(node){v,first[u]}; first[u]=tot++; edge[tot]=(node){u,first[v]}; first[v]=tot++; } int n,u,v; void dfs(int u,int fa){ dp[u][0][1]=0; dp[u][0][0]=0; leaf[u]=0; int flag=0; for(int i=first[u];i!=-1;i=edge[i].next){ int v=edge[i].e; if(v==fa) continue; dfs(v,u); flag=1; leaf[u]+=leaf[v]; for(int j=leaf[u];j>=0;j--){ int miv[2]={INF,INF}; for(int k=0;k<=min(leaf[v],j);k++){ miv[0]=min(miv[0],min(dp[u][j-k][0]+dp[v][k][1]+1,dp[u][j-k][0]+dp[v][k][0])); miv[1]=min(miv[1],min(dp[u][j-k][1]+dp[v][k][1],dp[u][j-k][1]+dp[v][k][0]+1)); } dp[u][j][0]=miv[0]; dp[u][j][1]=miv[1]; } } if(!flag){ leaf[u]=1; dp[u][0][0]=INF; dp[u][1][0]=0; } } int main(){ init(); scanf("%d",&n); for(int i=1;i<n;i++){ scanf("%d%d",&u,&v); addedge(u,v); deg[v]++;deg[u]++; } if(n==2) return 0*puts("1"); int rt,cnt=0; for(int i=1;i<=n;i++){ if(deg[i]!=1) rt=i; else cnt++; } dfs(rt,-1); //for(int i=1;i<=n;i++) //for(int j=0;j<=leaf[i];j++) printf("i_%d j_%d %d %d\n",i,j,dp[i][j][0],dp[i][j][1]); printf("%d\n",min(dp[rt][cnt/2][0],dp[rt][cnt/2][1])); return 0; }
23.COJ 1793
题意:对于每个点给一个限制xi表示xi选了才能选i,现在最多选m个人,问在这些限制下最多能选几个
如果连边(xi->i)一眼看过去结构很像树,因为每个结点只有一个父亲
但是这里可能会构成强联通分量
不过很容易想到把强联通分量缩点后也是可以构成树或者森林的
森林我们可以建立一个虚根0来合并成一棵树
一开始有个地方没想明白,就是对于一个强联通分量来说可能去掉一个点后还是强联通分量,所以一个强联通分量内我选择几个人是不好判断的
但是仔细想想题目给的条件,每个点的入度只能为1,而这样构成的强联通分量只能是一个简单环
而对于一个简单环来说我只能全取或者全不取
所以问题就转化为了一个m的背包,每个点的sz即使花费也是价值,问能拿到的最大价值是多少
这里有个地方需要变通
这里是选了父亲才能选择儿子,所以对于一个结点u以及他的儿子v来说,我要把v加进来我首先得保证u被选
所以我枚举v的容量k的时候始终保证j-k也就是剩余的容量始终是大于sz[u]的,这样就能保证u始终被选
#include"cstdio" #include"queue" #include"cmath" #include"stack" #include"iostream" #include"algorithm" #include"cstring" #include"queue" #include"map" #include"set" #include"vector" #include"bitset" #define LL long long #define ull unsigned long long #define mems(a,b) memset(a,b,sizeof(a)) #define mdzz int mid=(L+R)>>1 #pragma comment(linker, "/STACK:1024000000,1024000000") using namespace std; const int N = 1e3+5; const int M = N<<1; const LL MOD = 998244353; const int INF = 0x3f3f3f3f; struct node{ int s,e,next; }edge[M]; int tot,id; int first[N]; int block,tp,n,m; int low[N],dfn[N],ins[N],deg[N]; int st[N],belong[N],sz[N]; int dp[N][N]; void init(int n){ tot=0;tp=0;block=0;id=0; for(int i=0;i<=n;i++){ first[i]=-1; dfn[i]=0; ins[i]=0; deg[i]=0; for(int j=0;j<=m;j++) dp[i][j]=0; } } void addedge(int u,int v){ edge[tot]=(node){u,v,first[u]}; first[u]=tot++; //edge[tot]=(node){u,first[v]}; //first[v]=tot++; } void tarjan(int u){ low[u]=dfn[u]=++id; ins[u]=1; st[++tp]=u; for(int i=first[u];i!=-1;i=edge[i].next){ int v=edge[i].e; if(!dfn[v]){ tarjan(v); low[u]=min(low[u],low[v]); } else if(ins[v]) low[u]=min(low[u],dfn[v]); } if(low[u]==dfn[u]){ block++; sz[block]=0; int v; do{ v=st[tp--]; ins[v]=0; belong[v]=block; sz[block]++; } while(v!=u); } } void rebuild(){ int tot2=tot; tot=0; for(int i=0;i<=n;i++) first[i]=-1; for(int i=0;i<tot2;i++){ int u=belong[edge[i].s]; int v=belong[edge[i].e]; if(u==v) continue; deg[v]++; addedge(u,v); } } void dfs(int u,int fa){ for(int i=sz[u];i<=m;i++) dp[u][i]=sz[u]; for(int i=first[u];i!=-1;i=edge[i].next){ int v=edge[i].e; if(v==fa) continue; dfs(v,u); for(int j=m;j>=sz[u];j--) for(int k=0;k<=j-sz[u];k++) dp[u][j]=max(dp[u][j],dp[v][k]+dp[u][j-k]); } } int u; int main(){ //freopen("in.txt","r",stdin); while(~scanf("%d%d",&n,&m)){ init(n); for(int i=1;i<=n;i++){ scanf("%d",&u); addedge(u,i); } for(int i=1;i<=n;i++) if(!dfn[i]) tarjan(i); rebuild(); int rt=0;sz[0]=0; for(int i=1;i<=block;i++) if(!deg[i]) addedge(rt,i); dfs(rt,-1); printf("%d\n",dp[rt][m]==-1?0:dp[rt][m]); } return 0; }