1 条题解

  • 0
    @ 2026-5-10 16:00:04

    题解 P4322

    前置:分数规划

    参考:https://oi-wiki.org/misc/frac-programming/

    分数规划用来求一个分式的极值:给出 ai,bia_i,b_i,求一组 wi{0,1}w_i\in\{0,1\} 使 $\displaystyle\frac{\sum_{i=1}^n a_iw_i}{\sum_{i=1}^n b_iw_i}$ 最小化或最大化,再加上一些奇怪的限制。

    二分求解

    最大值:二分一个答案 midmid,则:

    $$\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=1nwi(aimid×bi)\sum_{i=1}^nw_i(a_i-mid \times b_i) 的最大值就行了。(比 00 大则 midmid 可行)

    最小值同理,求求 i=1nwi(aimid×bi)\sum_{i=1}^nw_i(a_i-mid \times b_i) 的最小值。

    例题

    给出 ai,bia_i,b_i,求一组 wi{0,1}w_i\in\{0,1\} 使 $\displaystyle\frac{\sum_{i=1}^n a_iw_i}{\sum_{i=1}^n b_iw_i}$ 最大化。

    wi(aimid×bi)w_i(a_i-mid \times b_i) 作为第 ii 个物品的权值,选择所有 >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(n2m)O(n^2m)

    状态 F[x,t]F[x,t]:在以 xx 为根的子树中选 tt 个物品的最大收益。

    技巧:如有多个节点为根,新建超级根 00

    转移方程:$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$

    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+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]最佳团体

    给你一棵树,每个节点有 ai,bia_i,b_i,选择 kk 个节点使得 $\displaystyle\frac{\sum_{i=1}^k a_i}{\sum_{i=1}^k b_i}$ 最大化。要求此 kk 个节点组成一棵树,且根节点为 00,边方向与原树相同。

    1n,k25001\leq n,k \leq 2500

    转化为 01 分数规划。令 aimid×bia_i-mid\times b_i 为每个点权值,跑树形背包即可。

    这里可以使用预处理时间戳的树形背包,复杂度做到 O(nklogval)O(nk\log val)

    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;
    }
    
    • 1

    信息

    ID
    6418
    时间
    1000ms
    内存
    256MiB
    难度
    10
    标签
    递交数
    9
    已通过
    3
    上传者