题解 [JSOI2016][LuoguP4322]最佳团体
前置:分数规划
参考:https://oi-wiki.org/misc/frac-programming/
分数规划用来求一个分式的极值:给出 a i , b i a_i,b_i ai,bi,求一组 w i ∈ { 0 , 1 } w_i\in\{0,1\} wi∈{0,1} 使 ∑ i = 1 n a i w i ∑ i = 1 n b i w i \displaystyle\frac{\sum_{i=1}^n a_iw_i}{\sum_{i=1}^n b_iw_i} ∑i=1nbiwi∑i=1naiwi 最小化或最大化,再加上一些奇怪的限制。
二分求解
最大值:二分一个答案 m i d mid mid,则:
∑ i = 1 n a i w i ∑ i = 1 n b i w i > m i d ⟹ ∑ i = 1 n a i w i − m i d × ∑ i = 1 n b i w i > 0 ⟹ ∑ i = 1 n w i ( a i − m i d × b i ) > 0 \displaystyle\frac{\sum_{i=1}^n a_iw_i}{\sum_{i=1}^n b_iw_i}>mid \\ \implies\sum_{i=1}^n a_iw_i - mid \times\sum_{i=1}^n b_iw_i>0 \\ \implies\sum_{i=1}^nw_i(a_i-mid \times b_i) > 0 ∑i=1nbiwi∑i=1naiwi>mid⟹i=1∑naiwi−mid×i=1∑nbiwi>0⟹i=1∑nwi(ai−mid×bi)>0
之后求 ∑ i = 1 n w i ( a i − m i d × b i ) \sum_{i=1}^nw_i(a_i-mid \times b_i) ∑i=1nwi(ai−mid×bi) 的最大值就行了。(比 0 0 0 大则 m i d mid mid 可行)
最小值同理,求求 ∑ i = 1 n w i ( a i − m i d × b i ) \sum_{i=1}^nw_i(a_i-mid \times b_i) ∑i=1nwi(ai−mid×bi) 的最小值。
例题
给出 a i , b i a_i,b_i ai,bi,求一组 w i ∈ { 0 , 1 } w_i\in\{0,1\} wi∈{0,1} 使 ∑ i = 1 n a i w i ∑ i = 1 n b i w i \displaystyle\frac{\sum_{i=1}^n a_iw_i}{\sum_{i=1}^n b_iw_i} ∑i=1nbiwi∑i=1naiwi 最大化。
把 w i ( a i − m i d × b i ) w_i(a_i-mid \times b_i) wi(ai−mid×bi) 作为第 i i i 个物品的权值,选择所有 > 0 >0 >0 的物品。
const int N = 1e5 + 10;
const double eps = 1e-6;
int n; double a[N], b[N];
bool check(double mid){
double res = 0;
for(int i = 1; i <= n; ++ i)
if(a[i] - mid * b[i] > 0) s += a[i] - mid * b[i];
return s > 0;
}
double solve(){
double l = 0, r = 1e9;
while(l + eps < r){
double mid = (l + r) / 2;
if(check(mid)) l = mid; else r = mid;
}
return l;
}
前置:树形依赖背包
O ( n 2 m ) O(n^2m) O(n2m) 解
状态 F [ x , t ] F[x,t] F[x,t]:在以 x x x 为根的子树中选 t t t 个物品的最大收益。
技巧:如有多个节点为根,新建超级根 0 0 0。
转移方程: F [ x , t ] = max ∑ i = 1 p c i = t − 1 { ∑ i = 1 p F [ y i , c i ] } + W e i x F[x,t]=\max\limits_{\sum_{i=1}^p~c_i=t-1} \bigg\{\displaystyle\sum\limits_{i=1}^pF[y_i,c_i]\bigg\}+Wei_x F[x,t]=∑i=1p ci=t−1max{i=1∑pF[yi,ci]}+Weix
const int N = 310;
int n, m, Wei[N], dp[N][N];//总物品数、选择物品数、物品重量、DP数组
vector<int> Edge[N];//图
void dfs(int x){
dp[x][0] = 0;
for(int i = 0; i < Edge[x].size(); ++ i){
int y = Edge[x][i];
dfs(y);
for(int t = m; t >= 0; -- t)//当前背包体积
for(int j = 0; j <= t; ++ j)//组内物品
dp[x][t] = max(dp[x][t], dp[x][t-j] + dp[y][j]);
}
if(x)//超级根 0 不用占物品数
for(int t = m; t > 0; -- t)
dp[x][t] = dp[x][t-1] + Wei[x];
return ;
}
O ( n + n m ) O(n+nm) O(n+nm) 解
把节点重新编号,使得每棵子树的编号最大的节点为它的根。使用后序遍历解决。
void dfs(int x){
siz[x] = 1;
for(int i = 0; i < g[x].size(); ++ i){
int y = g[x][i]; dfs(y);
siz[x] += siz[y];
}
pos[++cnt] = x;//pos[x]为新编号 x 的节点在原来的编号
}
然后按着编号顺序 dp 即可。dp 到一个节点,其子节点已经都被 dp 过了。
for(int i = 1; i <= cnt; ++ i)
for(int j = 1; j <= k; ++ j)
f[i][j] = max(f[i-1][j-1]+val[pos[i]], f[i-siz[pos[i]]][j]);
正文
JSOI2016]最佳团体
给你一棵树,每个节点有 a i , b i a_i,b_i ai,bi,选择 k k k 个节点使得 ∑ i = 1 k a i ∑ i = 1 k b i \displaystyle\frac{\sum_{i=1}^k a_i}{\sum_{i=1}^k b_i} ∑i=1kbi∑i=1kai 最大化。要求此 k k k 个节点组成一棵树,且根节点为 0 0 0,边方向与原树相同。
1 ≤ n , k ≤ 2500 1\leq n,k \leq 2500 1≤n,k≤2500
转化为 01 分数规划。令 a i − m i d × b i a_i-mid\times b_i ai−mid×bi 为每个点权值,跑树形背包即可。
这里可以使用预处理时间戳的树形背包,复杂度做到 O ( n k log v a l ) O(nk\log val) O(nklogval)。
const int N = 2510;
int k, n, siz[N], pos[N], cnt;
double f[N][N], val[N];
pair<int, int> p[N];
vector<int> g[N];
void dfs(int x){
siz[x] = 1;
for(int i = 0; i < g[x].size(); ++ i){
int y = g[x][i]; dfs(y);
siz[x] += siz[y];
}
pos[cnt] = x;
}
bool check(double mid){
for(int i = 0; i <= cnt; ++ i)
for(int j = 1; j <= k; ++ j) f[i][j] = -1e9;
for(int i = 1; i <= n; ++ i)
val[i] = (double) p[i].first - mid * p[i].second;
for(int i = 1; i <= cnt; ++ i)
for(int j = 1; j <= k; ++ j)
f[i][j] = max(f[i-1][j-1]+val[pos[i]], f[i-siz[pos[i]]][j]);
return f[cnt][k] > 0;//用cnt,不能用n(因为n不包括节点0)
}
double solve(){
dfs(0); ++ k;
double l = 0, r = 1e5;
while(l + 1e-5 <= r){
double mid = (l + r) / 2;
if(check(mid)) l = mid; else r = mid;
}
return l;
}