luogu2014 选课[树形背包][优化成$O(n^2)$的方法]
https://www.luogu.org/problemnew/show/P2014
树形背包的裸题。。当版子好了。
$f[i][j][k]$表示子树$i$选前$j$个孩子,共$k$个后代节点时的最大价值。然后$j$那一维是可以滚动的(但同时也要注意枚举变成了倒序),所以可以去掉。
$f[i][j]$表示子树$i$共选$k$个后代节点时的最大价值。
然后每个点可以抽象为一个背包,他的每个孩子包含一组物品,一组物品中包括以孩子为子树,选v个其后代节点形成的最大价的共v+1个物品(1指的是只有孩子自己)。对于每个孩子,只能选他的一种状态情形,或者不选。所以就是一个分组背包啦。
但是注意,子树的根必须强制选上。所以可以以他为初态,也就是后代节点=0的状态。写一下伪代码。
$dp$ $i$
$f_{i,0}=w_i$初态
$for$ $j=1$ $\sim$ $son_i$
$for$ $k=$(倒序)$sum_i -1$ $\sim$ $1$
$for$ $v=0$ $\sim$ $sum_j -1$
$if$ $v+1\leqslant k$
$f_{i,k}=max\{f_{i,k-v-1}+f_{j,v}\}$
然后复杂度由于是每个点都被考虑一次的,最坏是$O(N^3)$。(看做$N,M$同阶)
1 #include<iostream> 2 #include<cstdio> 3 #include<cstring> 4 #include<cmath> 5 #include<algorithm> 6 #include<queue> 7 #define dbg(x) cerr<<#x<<" = "<<x<<endl 8 #define ddbg(x,y) cerr<<#x<<" = "<<x<<" "<<#y<<" = "<<y<<endl 9 using namespace std; 10 typedef long long ll; 11 template<typename T>inline char MIN(T&A,T B){return A>B?A=B,1:0;} 12 template<typename T>inline char MAX(T&A,T B){return A<B?A=B,1:0;} 13 template<typename T>inline T _min(T A,T B){return A<B?A:B;} 14 template<typename T>inline T _max(T A,T B){return A>B?A:B;} 15 template<typename T>inline T read(T&x){ 16 x=0;int f=0;char c;while(!isdigit(c=getchar()))if(c=='-')f=1; 17 while(isdigit(c))x=x*10+(c&15),c=getchar();return f?x=-x:x; 18 } 19 const int N=300+7; 20 int f[N][N],Head[N],Next[N<<1],to[N<<1],w[N],sum[N],tot; 21 int n,m; 22 inline void Addedge(int x,int y){ 23 to[++tot]=y,Next[tot]=Head[x],Head[x]=tot; 24 to[++tot]=x,Next[tot]=Head[y],Head[y]=tot; 25 } 26 #define j to[tmp] 27 void dp(int i,int fa){ 28 sum[i]=1; 29 for(register int tmp=Head[i];tmp;tmp=Next[tmp])if(j!=fa)dp(j,i),sum[i]+=sum[j]; 30 f[i][0]=w[i]; 31 for(register int tmp=Head[i];tmp;tmp=Next[tmp])if(j!=fa){ 32 for(register int k=sum[i]-1;k;--k){ 33 for(register int v=0;v<=sum[j]-1;++v) 34 if(v+1<=k)MAX(f[i][k],f[i][k-v-1]+f[j][v]); 35 } 36 } 37 } 38 #undef j 39 int main(){//freopen("test.in","r",stdin);//freopen("test.out","w",stdout); 40 read(n),read(m);int x; 41 for(register int i=1;i<=n;++i)read(x),read(w[i]),Addedge(x,i); 42 dp(0,0);printf("%d\n",f[0][m]); 43 return 0; 44 }
然而这题有更优秀的(优化)做法。复杂度(看做$N,M$同阶)都是$O(N^2)$.
1.根据上面的一种针对性优化。
由于上面每次合并$i$的答案可以视作是子树$j$和$i$的已合并部分做一个$f[i,k+v]<--f[i,k]+f[j,v]$。同时又有“每个点费用都是$1$”这样一个隐含的特殊条件,
所以如果每次合并都枚举整棵树大小的费用未免浪费。把枚举改成:枚举$i$已合并部分点的个数$k$和价值$f[i,k]$,再枚举子树$j$的点个数$v$和价值$f[j,v]$,更新之。
就是这样。比较一下和上面的这个的区别。
1 #include<iostream> 2 #include<cstdio> 3 #include<cstring> 4 #include<cmath> 5 #include<algorithm> 6 #include<queue> 7 #define dbg(x) cerr<<#x<<" = "<<x<<endl 8 #define ddbg(x,y) cerr<<#x<<" = "<<x<<" "<<#y<<" = "<<y<<endl 9 using namespace std; 10 typedef long long ll; 11 template<typename T>inline char MIN(T&A,T B){return A>B?A=B,1:0;} 12 template<typename T>inline char MAX(T&A,T B){return A<B?A=B,1:0;} 13 template<typename T>inline T _min(T A,T B){return A<B?A:B;} 14 template<typename T>inline T _max(T A,T B){return A>B?A:B;} 15 template<typename T>inline T read(T&x){ 16 x=0;int f=0;char c;while(!isdigit(c=getchar()))if(c=='-')f=1; 17 while(isdigit(c))x=x*10+(c&15),c=getchar();return f?x=-x:x; 18 } 19 const int N=10000+7; 20 int f[N][N],Head[N],Next[N<<1],to[N<<1],w[N],sum[N],tot; 21 int n,m; 22 inline void Addedge(int x,int y){ 23 to[++tot]=y,Next[tot]=Head[x],Head[x]=tot; 24 to[++tot]=x,Next[tot]=Head[y],Head[y]=tot; 25 } 26 #define j to[tmp] 27 void dp(int i,int fa){ 28 sum[i]=1;f[i][0]=w[i]; 29 for(register int tmp=Head[i];tmp;tmp=Next[tmp])if(j!=fa){ 30 dp(j,i); 31 for(register int k=sum[i]-1;~k;--k) 32 for(register int v=0;v<=sum[j]-1;++v) 33 MAX(f[i][k+v+1],f[i][k]+f[j][v]); 34 sum[i]+=sum[j]; 35 } 36 } 37 #undef j 38 int main(){//freopen("test.in","r",stdin);freopen("test.out","w",stdout); 39 read(n),read(m);int x; 40 for(register int i=1;i<=n;++i)read(x),read(w[i]),Addedge(x,i); 41 dp(0,0);printf("%d\n",f[0][m]); 42 return 0; 43 }
树中每个点对相当于只会被在LCA处合并$f[lca,k+v]$枚举一次(可以把枚举的个数$k,v$看做子树中的编号)。于是枚举了$O(n^2)$个点对。所以是平方复杂度。
但是,这只是针对性的优化,也就是说,如果改成每个点都有一个费用且不一定为1,这样每个点做一次分组背包时就要完全枚举费用这一维了,没有办法用点对优化。
2.更高效的dfs序优化。
给定一棵 $n $个节点的树,$1$ 号节点是根节点。每个点有一个物品,价格为 $c_i$ ,价值为 $v_i$ 。
要买一个点上的物品,必须先把它父节点的物品给买了。求 $m$ 元钱能买到的最大价值。$n,m ≤ 2000$。
这时无法用点对优化。因为树上dp是按照一定顺序(dfs序)进行的,所以考虑转化到dfs序列上处理。设得到的dfs序中,$i$对应原序列点编号$p_i$,这个$p_i$子树dfs序上右端点设为$r_i$。
设$f_{i,j}$为dfs序上选择$i\sim n$中的点且满足树形要求的、费用为$j$的最大价值。
则$f_{i,j}=max(f_{i+1,j-cost_{p_i}}+value_{p_i},f_{r_i+1,j})$。
注意是倒序以使得先处理所有子代再处理子树根的。相当于决定当前子树的根如果选,那么他的子树内部和后面的dfs序都可以随便选。如果不选,那子树这一段的dfs序都不可选,直接从另一颗子树中继承过来。
可知若后面的点$i+1$若满足树形依赖的要求,则dp了$i$之后$i$只可能包含这个子树没选和$i$和$i+1$都选了($i+1 \sim r_i$满足树形依赖,则选$i$后也应当满足依赖关系)的情况。
1 #include<iostream> 2 #include<cstdio> 3 #include<cstring> 4 #include<algorithm> 5 #include<cmath> 6 #define dbg(x) cerr << #x << " = " << x <<endl 7 using namespace std; 8 typedef long long ll; 9 typedef double db; 10 typedef pair<int,int> pii; 11 template<typename T>inline T _min(T A,T B){return A<B?A:B;} 12 template<typename T>inline T _max(T A,T B){return A>B?A:B;} 13 template<typename T>inline char MIN(T&A,T B){return A>B?(A=B,1):0;} 14 template<typename T>inline char MAX(T&A,T B){return A<B?(A=B,1):0;} 15 template<typename T>inline void _swap(T&A,T&B){A^=B^=A^=B;} 16 template<typename T>inline T read(T&x){ 17 x=0;int f=0;char c;while(!isdigit(c=getchar()))if(c=='-')f=1; 18 while(isdigit(c))x=x*10+(c&15),c=getchar();return f?x=-x:x; 19 } 20 const int N=2000+7; 21 int f[N][N],c[N],val[N]; 22 int n,m,cnt; 23 struct thxorz{int to,nxt;}G[N<<1]; 24 int Head[N],id[N],ed[N],tot; 25 inline void Addedge(int x,int y){ 26 G[++tot].to=y,G[tot].nxt=Head[x],Head[x]=tot; 27 G[++tot].to=x,G[tot].nxt=Head[y],Head[y]=tot; 28 } 29 #define y G[j].to 30 inline void dfs(int x,int fa){ 31 id[++cnt]=x; 32 for(register int j=Head[x];j;j=G[j].nxt)if(y^fa)dfs(y,x); 33 ed[x]=cnt; 34 } 35 #undef y 36 int main(){//freopen("test.in","r",stdin);freopen("test.ans","w",stdout); 37 read(n);read(m); 38 for(register int i=1,x;i<=n;++i)read(x),read(val[i]),Addedge(x,i),c[i]=1;//read(c[i]); 39 dfs(0,0); 40 for(register int i=cnt;i;--i) 41 for(register int j=c[id[i]];j<=m;++j) 42 f[i][j]=_max(f[i+1][j-c[id[i]]]+val[id[i]],f[ed[id[i]]+1][j]);//dbg(i),dbg(j),dbg(f[i][j]); 43 printf("%d\n",f[1][m]); 44 return 0; 45 }
还有一种转二叉树的做法,不想了解。
这篇题解赶完了。