题解:洛谷 P4377 Talent Show

【题目来源】

洛谷:P4377 [USACO18OPEN] Talent Show G - 洛谷

【题目描述】

Farmer John 要带着他的 \(n\) 头奶牛,方便起见编号为 \(1\ldots n\),到农业展览会上去,参加每年的达牛秀!他的第 \(i\) 头奶牛重量为 \(w_i\),才艺水平为 \(t_i\),两者都是整数。

在到达时,Farmer John 就被今年达牛秀的新规则吓到了:

(一)参加比赛的一组奶牛必须总重量至少为 \(W\)(这是为了确保是强大的队伍在比赛,而不仅是强大的某头奶牛),并且。

(二)总才艺值与总重量的比值最大的一组获得胜利。

FJ 注意到他的所有奶牛的总重量不小于 \(W\),所以他能够派出符合规则(一)的队伍。帮助他确定这样的队伍中能够达到的最佳的才艺与重量的比值。

【输入】

第一行是两个整数,分别表示牛的个数 \(n\) 和总重量限制 \(W\)

\(2\)\((n+1)\) 行,每行两个整数,第 \((i + 1)\) 行的整数表示第 \(i\) 头奶牛的重量 \(w_i\) 和才艺水平 \(t_i\)

【输出】

请求出 Farmer 用一组总重量最少为 \(W\) 的奶牛最大可能达到的总才艺值与总重量的比值。

如果你的答案是 \(A\),输出 \(1000A\) 向下取整的值,以使得输出是整数(当问题中的数不是一个整数的时候,向下取整操作在向下舍入到整数的时候去除所有小数部分)。

请注意当问题的答案恰好是整数 \(x\) 时,你的程序可能会由于浮点数精度误差问题最后得到一个 \(x-\epsilon\) 的答案,向下取整后变为 \(x-1\) 导致答案错误。这种情况下你可以在输出答案前给答案加上一个极小的值 \(x\gets x+10^{-k}\) 来避免该问题。

【输入样例】

3 15
20 21
10 11
30 31

【输出样例】

1066

【核心思想】

  1. 问题分析:给定 \(n\) 头奶牛,每头有重量 \(w_i\) 和才艺值 \(t_i\),需选择一组奶牛使得总重量 \(\geq W\),且总才艺值与总重量的比值 \(\frac{\sum t_i}{\sum w_i}\) 最大化。这是一个分数规划问题,目标函数为比值形式,无法直接用传统背包求解。

  2. 算法选择

    • 二分答案 + 01背包判定:对目标比值 \(x\) 进行二分,将比值优化问题转化为判定问题——是否存在一组奶牛使得 \(\frac{\sum t_i}{\sum w_i} \geq x\)
    • 变形转化:将判定条件 \(\sum t_i \geq x \cdot \sum w_i\) 变形为 \(\sum (t_i - x \cdot w_i) \geq 0\),此时每个奶牛的"新价值"为 \(t_i - x \cdot w_i\),问题转化为在总重量 \(\geq W\) 约束下,新价值之和是否非负
  3. 关键步骤

    • 二分框架:设定比值范围 \([l, r]\)(如 \([0, 1000]\)),每次取中点 \(mid\)
    • 判定函数 \(\text{check}(mid)\)
      • 初始化 \(\text{DP}\) 数组 \(f[j]\)\(-\infty\),表示容量为 \(j\) 时的最大新价值
      • 01背包逆序更新:对每个奶牛 \(i\),从 \(W\)\(0\) 逆序遍历容量 \(j\)
        • 转移容量 \(k = \min(W, j + w_i)\)(防止越界,超过 \(W\)\(W\) 计算)
        • 状态转移:\(f[k] = \max(f[k], f[j] + t_i - mid \cdot w_i)\)
      • 返回 \(f[W] \geq 0\)(是否存在总重量 \(\geq W\) 且新价值和非负的方案)
    • 二分调整:若 \(\text{check}(mid)\) 为真,说明比值可以更大,\(l = mid\);否则 \(r = mid\)
    • 精度处理:循环至 \(r - l \leq 10^{-5}\),最终答案乘以 \(1000\) 后取整输出
  4. 时间/空间复杂度

    • 时间复杂度:\(O(\log(\frac{r-l}{\epsilon}) \cdot n \cdot W)\),二分约 \(20\) 次迭代,每次 \(01\) 背包 \(O(nW)\)
    • 空间复杂度:\(O(W)\),一维滚动数组优化
  5. 分数规划 + 背包判定的核心思想

    • 比值转差值:将 \(\frac{\sum t_i}{\sum w_i} \geq x\) 转化为 \(\sum(t_i - x \cdot w_i) \geq 0\),把分数比较转化为求和判定
    • 判定驱动优化:不直接求最优比值,而是二分猜测比值,用 \(01\) 背包判定可行性,将最优化问题转化为一系列判定问题
    • 容量上界压缩:由于只关心总重量 \(\geq W\),超过 \(W\) 的部分统一按 \(W\) 处理,压缩状态空间
    • 逆序滚动数组:01背包使用一维数组逆序更新,避免状态覆盖,空间优化至 \(O(W)\)
    • 适用于目标函数为比值形式的最优化问题,如最优比率背包、最大密度子图等

【算法标签】

提高+ #01背包

【代码详解】

#include <bits/stdc++.h>
using namespace std;

const int N = 255;           // 最大物品数量
int n, W;                     // n: 物品数量, W: 背包容量
int w[N], t[N];               // w: 物品重量, t: 物品价值
double f[1005];               // DP数组,f[j]表示容量为j时的最大价值

// 检查函数:判断是否存在解使得单位重量的价值至少为x
bool check(double x)
{
    // 初始化DP数组为负无穷
    for (int i = 1; i <= W; i++)
        f[i] = -1e9;

    // 0-1背包动态规划
    for (int i = 1; i <= n; i++)
    {
        for (int j = W; j >= 0; j--)
        {
            int k = min(W, j + w[i]);  // 防止数组越界
            f[k] = max(f[k], f[j] + t[i] - x * w[i]);  // 状态转移方程
        }
    }

    return f[W] >= 0;  // 判断是否存在可行解
}

// 二分查找最大单位价值
double find()
{
    double l = 0, r = 1000;  // 二分查找范围

    // 精度控制循环
    while (r - l > 1e-5)
    {
        double mid = (l + r) / 2;
        if (check(mid))      // 如果当前x可行
            l = mid;         // 尝试更大的x
        else
            r = mid;         // 尝试更小的x
    }

    return r;  // 返回最大值(注意这里是取整,所以返回r)
}

int main()
{
    // 输入物品数量和背包容量
    cin >> n >> W;

    // 输入每个物品的重量和价值
    for (int i = 1; i <= n; i++)
        cin >> w[i] >> t[i];

    // 计算并输出结果(乘以1000后取整)
    int ans = int(find() * 1000);
    cout << ans << endl;

    return 0;
}
// 使用acwing模板二刷
#include <bits/stdc++.h>
using namespace std;

const int N = 255;           // 最大物品数量
int n, W;                     // n: 物品数量, W: 背包容量
int w[N], t[N];               // w: 物品重量数组, t: 物品价值数组
double f[1005];               // DP数组,f[j]表示容量为j时的最大价值

// 检查函数:判断是否存在解使得单位重量的价值至少为x
bool check(double x)
{
    // 初始化DP数组为负无穷(表示不可达)
    for (int i = 1; i <= W; i++)
        f[i] = -1e9;

    // 0-1背包动态规划
    for (int i = 1; i <= n; i++)
    {
        // 逆序遍历背包容量
        for (int j = W; j >= 0; j--)
        {
            // 计算放入当前物品后的容量(防止越界)
            int k = min(W, j + w[i]);
            // 状态转移:比较放入和不放入当前物品的价值
            f[k] = max(f[k], f[j] + t[i] - x * w[i]);
        }
    }

    // 判断最终结果是否非负
    return f[W] >= 0;
}

// 二分查找函数:寻找最大单位价值
double find()
{
    double l = 0, r = 1000;   // 二分查找的初始范围
    double eps = 1e-5;        // 精度控制

    // 二分查找循环
    while (r - l > eps)
    {
        double mid = (l + r) / 2;
        if (check(mid))        // 如果当前x可行
            l = mid;           // 尝试更大的x
        else
            r = mid;           // 尝试更小的x
    }

    return l;                 // 返回最大单位价值
}

int main()
{
    // 输入物品数量和背包容量
    cin >> n >> W;

    // 输入每个物品的重量和价值
    for (int i = 1; i <= n; i++)
        cin >> w[i] >> t[i];

    // 计算并输出结果(乘以1000后取整)
    int ans = int(find() * 1000);
    cout << ans << endl;

    return 0;
}

【运行结果】

3 15
20 21
10 11
30 31
1066
posted @ 2026-08-29 09:37  团爸讲算法  阅读(5)  评论(0)    收藏  举报