bzoj1065 NOI2008奥运物流 (dp,树上背包,推式子)

3 篇文章 0 订阅


题目描述~

简化一下题意就是:

给定一颗基环树(n个点,n条边),设每个点的权值为w[i] 则 w[i]=c[i]+k*sigma(w[son[i]]),最多能修改m个点,使得w[1]最大


QWQ一开始看这个题,没啥头绪

只是有一些奇怪的想法:


1.如果修改,一定是将一个点的后继修改到1

2.每一个点的贡献.....应该是可以推式子吧~


带着这两个想法,我开始研究论文。


首先,考虑到根是一个环,所以一个点对1的贡献一定是会不断迭代转移的

我们可以手推一下式子:




这样我们就得出了每个点对于R(1)的贡献


同时,我们也能得到一个性质就是,只要我们固定下了环长,那么只需要让分子最大,R(1)也就最大了


所以,我们枚举环的长度,即修改环上某个点的,这样1-klen确定,只需分子尽可能大。现在我们有m次修改机会,


根据公式,显然每次修改都应该把修改点的后继直接设置为1。


考虑dp,f[i][j][k]表示以i为根的子树修改了j次,i到1的距离为k,即可得到状态转移方程



f[i][j][dep]=max {∑ max(f[v][J][dep+1],f[v][J][1])+(c[i]*(k^dep)),v为i儿子,∑J=j;//i不向根连边

        ∑ max(f[v][J][2],f[v][J][1])+(c[i]*k),v为i儿子,∑J=j; }//i向根连边



枚举长度len,以1的后继开始,依次作为断点,直到重新回到1为止


而对于每一个断点,因为环已经确定,所以不能再修改环上点的后继,只能改变一个非环上点的后继。


可是可是QWQ就算是想出了这样的dp转移方程,TLE也是在所难免的

QAQ然后,我们可以发现,在转移的时候,对于f[i][j][dep]无非是求了一个f[son[i]][j][dep+1]和f[son[i]][j][1]的max

所以,我们令g[i][j][dep]=max(g[i][j][dep+1],g[i][j][1]);


则上面的式子可以理解为,对于j,选择一种划分方案,使∑ g[i][J][k],(∑J=j) 最大


即可用01背包优化该方程,令总体积为j,然后有 son个物品,每一个物品的价值是g[son[i]][p][k],体积是p,要求正好装满背包的最大收益


QAQ而在背包的时候,我们可以用ff[i][j]表示以i为根的子树修改了j次的最大收益来统计结果和更新答案。


接着,我们来对着代码来进一步理解:


首先我们看一个主要的dp部分(树形dp)

void dp(int x,int dep)
{
	for (int i=2;i<=n;i++) if (fa[i]==x) dp(i,dep+1);
	for (int d=min(2,dep);d<=dep;d++)
	{
		memset(ff,0,sizeof(ff));
		for (int son=2;son<=n;son++)
		{
			if (fa[son]==x)
			{
				for (int j=m;j>=0;j--)
				  for (int k=j;k>=0;k--)
				    ff[j]=max(ff[j],ff[k]+g[son][j-k][d]); //01背包QWQ感觉题解错了 
			}
		}
	    for (int j=0;j<=m;j++) f[x][j][d]=ff[j]+c[x]*k[d];
	}
	if (dep>1)
	{
		memset(ff,0,sizeof(ff));
		for (int son=2;son<=n;son++)
		{
			if (fa[son]==x)
			{
				for (int j=m;j>=0;j--)
				  for (int k=j;k>=0;k--)
				    ff[j]=max(ff[j],ff[k]+g[son][j-k][1]); //01背包QWQ感觉题解错了 
			}
		}
	    for (int j=1;j<=m;j++) f[x][j][1]=ff[j-1]+c[x]*k[1];
	}
	for (int j=0;j<=m;j++)
	  for (int d=0;d<dep;d++)
	    g[x][j][d]=max(f[x][j][d+1],f[x][j][1]);
}


首先,树形dp的经典套路,先dfs到底,再进行计算:

for (int i=2;i<=n;i++) if (fa[i]==x) dp(i,dep+1);

对于任何一个点,都可以不修改他的后继

也就是直接由他的儿子修改或者不修改转移

因为考虑到他的儿子可能会被修改,导致他的深度枚举他的深度,

for (int d=min(2,dep);d<=dep;d++)
	{
		memset(ff,0,sizeof(ff));
		for (int son=2;son<=n;son++)
		{
			if (fa[son]==x)
			{
				for (int j=m;j>=0;j--)
				  for (int k=j;k>=0;k--)
				    ff[j]=max(ff[j],ff[k]+g[son][j-k][d]); //01背包QWQ感觉题解错了 
			}
		}
	    for (int j=0;j<=m;j++) f[x][j][d]=ff[j]+c[x]*k[d];
	}


k数组是预处理的的一个幂次方

因为当前点不修改,所以ff[j]可以到m   (这里用的是01背包常用的小技巧,将数组降维)

QAQ需要注意的是,这里千万不要忘记重新用ff数组 和 c数组,更新当前节点的f[x][j][d],以便于之后的g和答案的更新


同时,如果这个点不是dep为1的点,就说明他原本的后继不是1,那么他的后继就可以修改,则:

memset(ff,0,sizeof(ff));
		for (int son=2;son<=n;son++)
		{
			if (fa[son]==x)
			{
				for (int j=m-1;j>=0;j--)
				  for (int k=j;k>=0;k--)
				    ff[j]=max(ff[j],ff[k]+g[son][j-k][1]); //01背包QWQ感觉题解错了 
			}
		}
	    for (int j=1;j<=m;j++) f[x][j][1]=ff[j-1]+c[x]*k[1];


这时候的对于j 我们需要从m-1开始枚举,因为我们需要修改当前点

同时更新f数组的时候,也是用ff[j-1]来更新咯


最后在dp 的时候,更新一个g数组就OK了


而在主程序

for (int len=2;len;len++)
  {
  	 if (now==1) break;
  	 int front = fa[now];
  	 sum=0;
  	 fa[now]=1;
  	 memset(f,0,sizeof(f));
  	 memset(g,0,sizeof(g));
  	 for (int son=2;son<=n;son++) if (fa[son]==1) dp(son,1);
  	 memset(ff,0,sizeof(ff));
  	 for (int son=2;son<=n;son++) {
  	 	if (fa[son]==1)
  	 	for (int j=m;j>=0;j--)
			for (int k=j;k>=0;k--)
			  ff[j]=max(ff[j],ff[k]+f[son][j-k][1]);
  	 }
  	 for (int j=0;j<m;j++) sum=max(sum,ff[j]);
  	 if (front == 1) sum=max(sum,ff[m]);
  	 ans=max(ans,(sum+c[1])/(1-k[len]));
  	 fa[now]=front;
  	 now=fa[now];
  }


首先将now复制成fa[1],然后每次将now赋值成fa[now]表示依次将环上的点作为断点


记得初始化f和g


然后从每个与1相连的点开始 dfs进行dp


然后用同样的方法更新ff数组,然后用ff[0]~ff[m-1]更新ans  (因为本身断环就需要一次修改)

若front==1 也就是当前点本来就是接在1上的

不需要消耗次数

所以只有front==1的时候,才可以用ff[m]更新


最后把fa[now]复原


QWQ大致就是这样了


上代码

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<cmath>

using namespace std;

inline int read()
{
   int x=0,f=1;char ch=getchar();
   while (!isdigit(ch)){if (ch=='-') f=-1;ch=getchar();}
   while (isdigit(ch)){x=(x<<1)+(x<<3)+ch-'0';ch=getchar();}
   return x*f;
}

const int maxn = 110;

double f[maxn][maxn][maxn],g[maxn][maxn][maxn],ff[maxn];
double k[maxn];
int n,m,fa[maxn];
double c[maxn];
int now;
double ans,sum;

void dp(int x,int dep)
{
	for (int i=2;i<=n;i++) if (fa[i]==x) dp(i,dep+1);
	for (int d=min(2,dep);d<=dep;d++)
	{
		memset(ff,0,sizeof(ff));
		for (int son=2;son<=n;son++)
		{
			if (fa[son]==x)
			{
				for (int j=m;j>=0;j--)
				  for (int k=j;k>=0;k--)
				    ff[j]=max(ff[j],ff[k]+g[son][j-k][d]); //01背包QWQ感觉题解错了 
			}
		}
	    for (int j=0;j<=m;j++) f[x][j][d]=ff[j]+c[x]*k[d];
	}
	if (dep>1)
	{
		memset(ff,0,sizeof(ff));
		for (int son=2;son<=n;son++)
		{
			if (fa[son]==x)
			{
				for (int j=m-1;j>=0;j--)
				  for (int k=j;k>=0;k--)
				    ff[j]=max(ff[j],ff[k]+g[son][j-k][1]); //01背包QWQ感觉题解错了 
			}
		}
	    for (int j=1;j<=m;j++) f[x][j][1]=ff[j-1]+c[x]*k[1];
	}
	for (int j=0;j<=m;j++)
	  for (int d=0;d<dep;d++)
	    g[x][j][d]=max(f[x][j][d+1],f[x][j][1]);
}

int main()
{
  scanf("%d%d%lf",&n,&m,&k[1]);
  k[0]=1;
  for (int i=2;i<=n;i++) k[i]=k[i-1]*k[1];
  for (int i=1;i<=n;i++)
  {
  	int p;
  	scanf("%d",&p);
  	fa[i]=p;
  }
  now=fa[1];
  for (int i=1;i<=n;i++) scanf("%lf",&c[i]);
  for (int len=2;len;len++)
  {
  	 if (now==1) break;
  	 int front = fa[now];
  	 sum=0;
  	 fa[now]=1;
  	 memset(f,0,sizeof(f));
  	 memset(g,0,sizeof(g));
  	 for (int son=2;son<=n;son++) if (fa[son]==1) dp(son,1);
  	 memset(ff,0,sizeof(ff));
  	 for (int son=2;son<=n;son++) {
  	 	if (fa[son]==1)
  	 	for (int j=m;j>=0;j--)
			for (int k=j;k>=0;k--)
			  ff[j]=max(ff[j],ff[k]+f[son][j-k][1]);
  	 }
  	 for (int j=0;j<m;j++) sum=max(sum,ff[j]);
  	 if (front == 1) sum=max(sum,ff[m]);
  	 ans=max(ans,(sum+c[1])/(1-k[len]));
  	 fa[now]=front;
  	 now=fa[now];
  }
  printf("%.2lf\n",ans);
  return 0;
}


  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值