P3780 [SDOI2017]苹果树
题目大意:
给定一个有根树,每个节点有权值 \(v_i\),节点值至多取 \(a_i\) 次,选儿子节点一定要同时选父亲节点一次,取 \(k\) 个节点值。
除此之外,还可以取一条最长链,求最大权值。
思路:
拿到这道题,先转化成理解的形式:树上背包 \(dp\) +一堆不知道什么东西。
————————————————————————
此时需要一个知识点: 树的后序遍历:
其实就是求弹栈的顺序,此时有两个特殊性质可以利用:
- 子树 \(dfs\) 序连续 ,与前序遍历相同
- 任意一个点 \(i\) 的子树在这个序列上都是一个区间,而且 \(i\) 是区间的右端点。
————————————————————————
最长链一定是从根节点到叶子的一条路径。又因为点权是正的,肯定取越多越好。
对于所有点有一个隐藏的性质:
我如果要第二次取这个节点的值,首先要取一次值才行,这跟题目中要取儿子一定先取父亲的逻辑关系是一样的。
我们进行拆点:如果 \(a_i > 1\) ,\(i\) 拆成两个点:第一个 \(a_i=1\) ,第二个点是 \(i\) 的儿子节点 \(i'\),\(a_{i'}=a_i-1\),且不和其他点连接。
这样就可以直接枚举路径,考虑 \(dp\) ,有 \(dp[i][j]\) 表示决策到了后序遍历第 \(i\) 个点,选取 \(j\) 个物品的最大收益。
那么我们枚举这个物品选还是不选,如果不选的话,它的子树全部不可选,而不可选的子树是一段区间,只能从 \(dp[i-size_i][j]\) 转移过来
则有转移方程:
\[dp[i][j]=max^{a_i}_{k=1}(dp[i-1][j-k],dp[i-size_i][j]) \]我们后续跑一遍 \(dp\) ,然后把邻接表反转一遍再跑一遍,此时 \(dp[i][k]\) 就是再后序遍历前 \(i\) 个点中选择 \(k\) 个物品的最大收益的答案。
玄学卡常:
- 运用单向边存储
- 利用一维数组模拟二维,减少访问,节约时间。
- 不要写双端队列,尽量手写
// P3780 [SDOI2017]苹果树
#include<bits/stdc++.h>
const int N=4e4+10,K=5e5+10,NK=6e7+10;
using namespace std;
inline void clear(vector <int>& ve){vector<int>emp; swap(emp,ve);}
vector<int> v[N];
int w[N],a[N],fa[N],dfn1[N],dfn2[N],df1,df2,sizes[N],line[N];
int dp1[NK],dp2[NK],nfd1[N],nfd2[N],q1[K<<1],q2[K<<1];
bool lf[N];
int T,n,k,res,cnt,h,head=1,tail=0;
void init(){
for(int i=0;i<=cnt;i++) clear(v[i]),lf[i]=line[i]=sizes[i]=0;
for(int i=0;i<=(cnt+1)*(k+1);i++) dp1[i]=dp2[i]=0;
df1=df2=res=cnt=h=0;
}
void dypr(int *dfn,int *dp){//dp过程,单调队列进行手写优化
for(int i=1;i<=cnt;i++){
int v=dfn[i];head=tail=1; q1[1]=q2[1]=0;
for(int j=1;j<=k;j++){
if(q1[head]<j-a[v]) head++;//剩余更多,更加优秀
int val=dp[(i-1)*(k+1)+j]-j*w[v]; //这里是运用了二维转一维的写法
dp[i*(k+1)+j]=max(q2[head]+j*w[v],dp[(i-sizes[v])*(k+1)+j]);//单调队列优化转移
while(head<=tail&&q2[tail]<=val) tail--;
q1[++tail]=j; q2[tail]=val;
}
}
}
void dfs1(int x){
sizes[x]=1;
for(int i=0;i<v[x].size();i++){
int y=v[x][i];
dfs1(y);
sizes[x]+=sizes[y];
}
dfn1[++df1]=x;
nfd1[x]=df1;//后序遍历,记录入点
}
void dfs2(int x){//逆邻接表
for(int i=v[x].size()-1;i>=0;i--){
int y=v[x][i]; line[y]=line[x]+w[y];
dfs2(y);
}
dfn2[++df2]=x;
nfd2[x]=df2;
}
int main(){
cin>>T;
while(T--){
scanf("%d%d",&n,&k); cnt=n;
for(int i=1;i<=n;i++) scanf("%d%d%d",&fa[i],&a[i],&w[i]),lf[fa[i]]=true;
for(int i=1;i<=n;i++){//拆点,加边
v[fa[i]].push_back(i);
if(a[i]>1){
a[++cnt]=a[i]-1; a[i]=1;
w[cnt]=w[i]; v[i].push_back(cnt);
}
}
line[1]=w[1];
dfs1(1); dfs2(1);
dypr(dfn1,dp1); dypr(dfn2,dp2);
for(int i=1;i<=n;i++){
if(lf[i]) continue;
for(int j=0;j<=k;j++){
res=max(res,dp1[(nfd1[i]-1)*(k+1)+j]+line[i]+dp2[(nfd2[i]-sizes[i])*(k+1)+(k-j)]);
res=max(res,dp1[(nfd1[i]-1)*(k+1)+j]+line[i]+dp2[(nfd2[i]-sizes[i])*(k+1)+(k-j)]);
}
}
printf("%d\n",res);
init();
}
system("pause");
return 0;
}