题目描述
JSOI 信息学代表队一共有 NN 名候选人,这些候选人从 11 到 NN 编号。方便起见,JYY 的编号是 00 号。每个候选人都由一位编号比他小的候选人 R_iRi 推荐。如果 R_i = 0Ri=0 ,则说明这个候选人是 JYY 自己看上的。
为了保证团队的和谐,JYY 需要保证,如果招募了候选人 ii ,那么候选人 R_iRi 也一定需要在团队中。当然了,JYY 自己总是在团队里的。每一个候选人都有一个战斗值 P_iPi ,也有一个招募费用 S_iSi 。JYY 希望招募 KK 个候选人(JYY 自己不算),组成一个性价比最高的团队。也就是,这 KK 个被 JYY 选择的候选人的总战斗值与总招募费用的比值最大。
输入输出格式
输入格式:
输入一行包含两个正整数 KK 和 NN 。
接下来 NN 行,其中第 ii 行包含三个整数 S_i , P_i, R_i, 表示候选人 ii 的招募费用,战斗值和推荐人编号。
输出格式:
输出一行一个实数,表示最佳比值。答案保留三位小数。
这题是道经典题了,值得记一记(虽然自己并不会做)。我从洛谷大神那里学到了一种新方法。洛谷题解
稍加思考
树?直接设一个表示以i为根节点,选了j个子节点时的最大比值。然而这样上来就推不走了。容易联想到0/1分数规划。可以知道:。因为ans肯定大于0,所以展开移项有:。于是加上一个二分ans,每次做dp,看最终的最优解是否符合要求即可。
新的方法
习惯性地做之前看了一眼题解的文字说明,结果发现了新大陆。有大神说是可以把它转化为DFS序,直接做一个背包dp,于是开始尝试自己搞,搞到最后还是把题解抄袭学习完了....
用dfn[i]记录先序遍历节点i时的顺序,用las[i]记录i这棵子树遍历完之后,下一棵子树根节点的遍历顺序。然后直接1~N做一个0/1背包dp即可。注意此处根节点的顺序在子节点之前,且是根选了才可选儿子,所以向后更新。此外,由于树与树是独立的,所以当前树更新时,要更新紧接着它的下一棵树。实在太水怎么也写不对,所以最终还是按着题解写了.......
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
typedef long long LL;
typedef double DB;
const int MAXN=2520;
const DB eps=0.0001;
const DB INF=500000000;
int N,K,np=0,clock=0;
int last[MAXN];
int dfn[MAXN];
int las[MAXN];
DB S[MAXN];
DB P[MAXN];
DB d[MAXN];
DB f[MAXN][MAXN];
struct edge{int to,pre;}E[MAXN*2];
void addedge(int u,int v)
{
E[++np]=(edge){v,last[u]};
last[u]=np;
}
void DFS(int i)
{
dfn[i]=clock++; //遍历顺序
for(int p=last[i];p;p=E[p].pre)
{
int j=E[p].to;
DFS(j);
}
las[dfn[i]]=clock; //注意上面是先赋值再+1,所以las指向下一棵子树
}
DB check(DB mid)
{
int i,j;
for(i=0;i<=N;i++) //0/1分数规划
d[dfn[i]]=P[i]-mid*S[i];
for(i=1;i<=N+1;i++) //初始化
for(j=0;j<=K+1;j++)
f[i][j]=-INF;
for(i=0;i<=N;i++) //就是一个背包问题
for(j=0;j<=min(i,K+1);j++)
{
f[i+1][j+1]=max(f[i+1][j+1],f[i][j]+d[i]);
f[las[i]][j]=max(f[las[i]][j],f[i][j]);
}
return f[N+1][K+1];
}
int main()
{
cin>>K>>N;
int i,u;
DB w,MAXP=0;
for(i=1;i<=N;i++)
{
cin>>S[i]>>P[i]>>u;
addedge(u,i);
MAXP=max(MAXP,P[i]);
}
DFS(0); //形成DFS序
DB L=0,R=MAXP,MID;
while(R-L>eps)
{
MID=(L+R)/2.0;
if(check(MID)>=eps) L=MID;
else R=MID;
}
printf("%.3lf",L);
return 0;
}