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+1leqslant 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
",f[0][m]);
43     return 0;
44 }
View Code

然而这题有更优秀的(优化)做法。复杂度(看做$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
",f[0][m]);
42     return 0;
43 }
View Code

树中每个点对相当于只会被在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序上选择$isim 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
",f[1][m]);
44     return 0;
45 }
View Code

还有一种转二叉树的做法,不想了解。

这篇题解赶完了。

原文地址:https://www.cnblogs.com/saigyouji-yuyuko/p/10700556.html