二分是个神奇的算法,它能让你难以维护的数据突然变得十分容易维护……
洛谷P4322 [JSOI2016]最佳团体 \color{green}{\texttt{洛谷P4322 [JSOI2016]最佳团体}} 洛谷P4322 [JSOI2016]最佳团体
[Problem] \color{blue}{\texttt{[Problem]}} [Problem]
-
有一个有 N + 1 N+1 N+1 个点的树,每个节点有两个值: p i , s i ( 1 ≤ i ≤ N ) p_{i},s_{i}(1 \leq i \leq N) pi,si(1≤i≤N)。
-
你需要选 t + 1 t+1 t+1 个点,如果一个点选了,则其父亲必须要选。
-
一个选择方案集合 T T T 的总价值为:
∑ i ∈ T p i ∑ i ∈ T s i \dfrac{\sum\limits_{i \in T} p_i}{\sum\limits_{i \in T} s_i} i∈T∑sii∈T∑pi
-
求总价值的最大值。 1 ≤ t ≤ N ≤ 2500 , 1 ≤ p i , s i ≤ 1 × 1 0 4 ( 1 ≤ i ≤ N ) , Father ( i ) < i 1 \leq t \leq N \leq 2500,1 \leq p_i,s_i \leq 1 \times 10^4(1 \leq i \leq N),\texttt{Father}(i)<i 1≤t≤N≤2500,1≤pi,si≤1×104(1≤i≤N),Father(i)<i。节点从 0 0 0 编号到 N N N。
[Solution] \color{blue}{\texttt{[Solution]}} [Solution]
是不是感觉总价值的那个式子特别难维护?
我们可以记 mid \texttt{mid} mid 表示总价值大于等于 mid \texttt{mid} mid。我们发现 mid \texttt{mid} mid 越大,越难以满足,所以我们可以二分答案。
改写一个总价值那个式子:
∑ i ∈ T p i ∑ i ∈ T s i ≥ mid ∑ i ∈ T p i ≥ mid × ∑ i ∈ T s i ∑ i ∈ T p i − mid × ∑ i ∈ T s i ≥ 0 ∑ i ∈ T ( p i − mid × s i ) ≥ 0 \begin{aligned} \dfrac{\sum\limits_{i \in T} p_i}{\sum\limits_{i \in T}s_i} &\geq \texttt{mid}\\ \sum\limits_{i \in T} p_i &\geq \texttt{mid} \times \sum \limits_{i \in T}s_i\\ \sum\limits_{i \in T} p_i - \texttt{mid} \times \sum \limits_{i \in T} s_i & \geq 0\\ \sum\limits_{i \in T} \left (p_i - \texttt{mid} \times s_i \right ) & \geq 0 \end{aligned} i∈T∑sii∈T∑pii∈T∑pii∈T∑pi−mid×i∈T∑sii∈T∑(pi−mid×si)≥mid≥mid×i∈T∑si≥0≥0
改每个点的价值为 p i − mid × s i p_i -\texttt{mid} \times s_i pi−mid×si,原题被转化为了一个树上 dp 的问题。只要最后的总价值 ≥ 0 \geq 0 ≥0,代表 mid \texttt{mid} mid 是一个可行答案。
如何树上 dp?很简单,记
f
i
,
j
f_{i,j}
fi,j 表示在
i
i
i 的子树中选
j
j
j 个点时的最大总价值,原题被转化为了一个树上背包问题,可以用类似 选课
一题的方法解决。
[code] \color{blue}{\texttt{[code]}} [code]
struct edge{//链式前向星
int next,to;//的模板
}e[2510];int h[2510],tot;
inline void add(int a,int b){
e[++tot]=(edge){h[a],b};h[a]=tot;
}
double p[2510],s[2510],w[2510];
double f[2510][2510];//dp用数组
int n,t,sze[2510];double l,r,mid;
inline void dp(int u){
f[u][1]=w[u];sze[u]=1;//init
for(int i=h[u];i;i=e[i].next){
register int to=e[i].to;
dp(to);//先递归计算儿子值
sze[u]+=sze[to];//子树大小
for(int k=min(sze[u],t+1);k>=1;k--)
for(int l=0;l<=min(sze[to],k-1);l++)
f[u][k]=max(f[u][k],f[u][k-l]+f[to][l]);
// 该状态转移方程的含义是:在u当前加入的所有儿子中,to这个子树选l个点,其它儿子所对应的子树共选k-l个点时的最大总价值
// 注意就像01背包一样,k必须是倒序的
}
}
const int inf=0x3f3f3f3f;
inline bool check(double mid){
for(int i=1;i<=n;i++)
w[i]=p[i]-mid*s[i];
w[0]=0;//计算每个人贡献
for(int i=0;i<=n;i++)
for(int j=1;j<=t+1;j++)
f[i][j]=-inf;
dp(0);//从根开始进行dp
return f[0][t+1]>=0;
}
const double eps=1e-4;
int main(){
t=read();n=read();
for(int i=1;i<=n;i++){
s[i]=read();p[i]=read();
add(read(),i);//建图
}
l=0.0;r=10000.0;
while (l+eps<r){
mid=(l+r)/2.0;
if (check(mid)) l=mid;
else r=mid;
}
printf("%.3lf",l);
return 0;
}