bzoj4753 & 洛谷P4322 [JSOI2016]最佳团体 分数规划+树形dp

题目链接:
传送门->洛谷
传送门->bzoj

题目大意:

给出一棵以 0 0 0为根的树,每个节点有两个信息 a i , b i a_i,b_i ai,bi。其中 a 0 = b 0 = 0 a_0=b_0=0 a0=b0=0
现在已选 0 0 0号节点,要再选出 K K K个节点,使得 Σ i = 1 k a i Σ i = 1 k b i \huge \frac {\Sigma^{k}_{i=1} a_i} {\Sigma^{k}_{i=1}b_i} Σi=1kbiΣi=1kai最大。
要求:若选出节点 i i i,则它的父亲 f a t h e r [ i ] father[i] father[i]也必须被选。

解题方法

看到 Σ i = 1 k a i Σ i = 1 k b i \huge \frac {\Sigma^{k}_{i=1} a_i} {\Sigma^{k}_{i=1}b_i} Σi=1kbiΣi=1kai就想到用分数规划 q w q qwq qwq
二分答案,假设当前值为 m i d mid mid,那么只需验证是否有 Σ i = 1 k a i Σ i = 1 k b i \huge \frac {\Sigma^{k}_{i=1} a_i} {\Sigma^{k}_{i=1}b_i} Σi=1kbiΣi=1kai > = m i d >=mid >=mid即可。
变形一下可得 Σ a i − m i d ∗ Σ b i \large \Sigma a_i-mid*\Sigma b_i ΣaimidΣbi > = 0 >=0 >=0
于是让每个点的权值为 a i − m i d ∗ b i a_i-mid*b_i aimidbi,在树上跑 d p dp dp

d p [ i ] [ j ] dp[i][j] dp[i][j]表示第 i i i个点的子树内,取 包括i的 {\color{red}\text{包括i的}} 包括i j j j个节点的最大权值和。
转移比较显然,扫一遍子节点,大力转移即可。
但是要注意每次算出来的答案不能直接记入 d p dp dp数组,不然会把前一个子节点的 d p dp dp值覆盖掉(体会一下),然后玄学 W A WA WA
珂以用一个 t m p tmp tmp数组来保存临时答案,最后再记录进 d p dp dp数组。
转移方程( n x t nxt nxt表示 x x x的一个子节点):
t m p [ j ] = m a x ( t m p [ j ] , d p [ x ] [ j − k ] + d p [ n x t ] [ k ] ) tmp[j]=max(tmp[j],dp[x][j-k]+dp[nxt][k]) tmp[j]=max(tmp[j],dp[x][jk]+dp[nxt][k])
并且要注意 j , k j,k j,k的范围。

Ps. m e m s e t memset memset的一个小技巧:
把double数组重置成 − I N F -INF INF的方法:

memset(a,0xf0,sizeof(a));

丑陋无比的代码:

注:这段代码在bzoj上跑不过quq,请各位dalao谨慎食用

#include<stdio.h>
#include<cstring>
#include<algorithm>
#include<math.h>
#define re register int
#define rl register ll
using namespace std;
typedef long long ll;
int read() {
	re x=0,f=1;
	char ch=getchar();
	while(ch<'0' || ch>'9') {
		if(ch=='-')	f=-1;
		ch=getchar();
	}
	while(ch>='0' && ch<='9') {
		x=10*x+ch-'0';
		ch=getchar();
	}
	return x*f;
}
const int Size=2605;
const double eps=1e-4;
int n,k,a[Size],b[Size],f[Size];
int cnt,head[Size],siz[Size];
struct Edge {
	int v,next;
} w[Size<<1];
void AddEdge(int u,int v) {
	w[++cnt].v=v;
	w[cnt].next=head[u];
	head[u]=cnt;
}
double val[Size];
double dp[Size][Size];
double tmp[Size];
void dfs(int x) {
	//dp[i][j]:在i的子树中取j个点的最大值 
	siz[x]=1;
	dp[x][1]=val[x];
	dp[x][0]=0;
	for(int i=head[x]; i; i=w[i].next) {
		int nxt=w[i].v;
		dfs(nxt);
		memset(tmp,0xf0,sizeof(tmp));
		siz[x]+=siz[nxt];
		for(re j=1; j<=siz[x]; j++) {
			int maxk=min(j-1,siz[nxt]);
			for(re k=0; k<=maxk; k++) {
				double now=dp[x][j-k]+dp[nxt][k];
				if(now>tmp[j]) {
					tmp[j]=now;
				}
			}
		}
		for(re j=1; j<=siz[x]; j++) {
			dp[x][j]=tmp[j];
		}
	}
}
bool check(double mid) {
	for(re i=1; i<=n; i++) {
		val[i]=(double)a[i]-mid*b[i];
	}
	memset(dp,0xf0,sizeof(dp));
	dfs(0);
	return dp[0][k+1]>0;
}
int main() {
	k=read();
	n=read();
	double l=0,r=0,mid;
	for(re i=1; i<=n; i++) {
		b[i]=read();
		a[i]=read();
		f[i]=read();
		AddEdge(f[i],i);
		double now=1.0*a[i]/b[i];
		if(now>r) {
			r=now;
		}
	}
	while(l+eps<=r) {
		mid=(l+r)/2;
		if(check(mid)) {
			l=mid;
		} else {
			r=mid;
		}
	}
	printf("%.3f",l);
	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值