[ICPC 2014 WF] Buffed Buffet

GitHub跳转原题关系图返回列表

离散菜先做凹费用完全背包求每个整数重量的最优值,连续菜再把剩余重量写成分段二次函数最优分配,最后合并两部分答案。

OJ: luogu

题目 ID: P6893

难度:提高+/省选-

标签:动态规划背包状态设计分类讨论

日期: 2026-06-21 10:15

题意

有两类菜:

  • 离散菜:每份重量固定,只能取整数份
  • 连续菜:可以取任意实数重量

i 道菜都有初始美味值 t_i 和衰减速度 Δt_i

如果是一道离散菜,取第 n 份时的边际美味值是:

t_i - (n-1)Δt_i

如果是一道连续菜,已经取了 x 克后,再取一个无穷小 dx 的边际美味值是:

(t_i - xΔt_i)dx

要求总重量恰好为 W,并让总美味值最大。

思路

先看一个可以直接验证想法的朴素解:

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

const long double NEG_INF = -1e100L;

struct DiscreteDish {
    int weight;
    long double t;
    long double d;
};

struct ContinuousDish {
    long double t;
    long double d;
};

int n, W;
vector<DiscreteDish> discrete_dishes;
vector<ContinuousDish> continuous_dishes;
vector<long double> cont_best;
long double answer;

void solve_continuous_part() {
    cont_best.assign(W + 1, NEG_INF);
    cont_best[0] = 0;

    if (continuous_dishes.empty()) {
        return;
    }

    long double best_const = NEG_INF;
    vector<ContinuousDish> quad;

    for (size_t i = 0; i < continuous_dishes.size(); i++) {
        if (fabsl(continuous_dishes[i].d) < 1e-18L) {
            best_const = max(best_const, continuous_dishes[i].t);
        } else {
            quad.push_back(continuous_dishes[i]);
        }
    }

    if (best_const > NEG_INF / 2) {
        vector<ContinuousDish> filtered;
        for (size_t i = 0; i < quad.size(); i++) {
            if (quad[i].t > best_const + 1e-18L) {
                filtered.push_back(quad[i]);
            }
        }
        quad.swap(filtered);
    }

    sort(quad.begin(), quad.end(), [](const ContinuousDish &a, const ContinuousDish &b) {
        return a.t > b.t;
    });

    int c = (int) quad.size();
    vector<long double> pref_a(c + 1, 0), pref_b(c + 1, 0), pref_c(c + 1, 0);
    for (int i = 1; i <= c; i++) {
        long double inv = 1.0L / quad[i - 1].d;
        pref_a[i] = pref_a[i - 1] + inv;
        pref_b[i] = pref_b[i - 1] + quad[i - 1].t * inv;
        pref_c[i] = pref_c[i - 1] + quad[i - 1].t * quad[i - 1].t * inv / 2.0L;
    }

    if (best_const > NEG_INF / 2 && c == 0) {
        for (int x = 1; x <= W; x++) {
            cont_best[x] = best_const * x;
        }
        return;
    }

    for (int k = 1; k <= c; k++) {
        long double low = pref_b[k] - pref_a[k] * quad[k - 1].t;
        if (low < 0) {
            low = 0;
        }

        long double next_t;
        if (k < c) {
            next_t = quad[k].t;
            if (best_const > NEG_INF / 2) {
                next_t = max(next_t, best_const);
            }
        } else if (best_const > NEG_INF / 2) {
            next_t = best_const;
        } else {
            next_t = -1e100L;
        }

        long double high;
        if (next_t < -1e90L) {
            high = W;
        } else {
            high = pref_b[k] - pref_a[k] * next_t;
        }

        int left = (int) ceill(low - 1e-18L);
        int right = (int) floorl(high + 1e-18L);
        if (left < 0) {
            left = 0;
        }
        if (right > W) {
            right = W;
        }

        for (int x = left; x <= right; x++) {
            long double lambda = (pref_b[k] - x) / pref_a[k];
            long double val = pref_c[k] - lambda * lambda * pref_a[k] / 2.0L;
            cont_best[x] = max(cont_best[x], val);
        }
    }

    if (best_const > NEG_INF / 2) {
        long double x0 = pref_b[c] - pref_a[c] * best_const;
        if (x0 < 0) {
            x0 = 0;
        }
        long double base = pref_c[c] - best_const * best_const * pref_a[c] / 2.0L;
        int start = (int) ceill(x0 - 1e-18L);
        if (start < 0) {
            start = 0;
        }
        for (int x = start; x <= W; x++) {
            cont_best[x] = max(cont_best[x], base + (x - x0) * best_const);
        }
    }
}

void dfs_discrete(int idx, int used_weight, long double cur_value) {
    if (used_weight > W) {
        return;
    }
    if (idx == (int) discrete_dishes.size()) {
        if (continuous_dishes.empty()) {
            if (used_weight == W) {
                answer = max(answer, cur_value);
            }
        } else if (cont_best[W - used_weight] > NEG_INF / 2) {
            answer = max(answer, cur_value + cont_best[W - used_weight]);
        }
        return;
    }

    DiscreteDish dish = discrete_dishes[idx];
    int max_cnt = (W - used_weight) / dish.weight;
    long double add = 0;

    for (int cnt = 0; cnt <= max_cnt; cnt++) {
        dfs_discrete(idx + 1, used_weight + cnt * dish.weight, cur_value + add);
        add += dish.t - cnt * dish.d;
    }
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    // brute.cpp:小数据暴力解,用来帮助理解题意并辅助对拍。
    // 暴力枚举每种离散菜取多少份,再把剩余重量交给连续菜做解析最优分配。
    cin >> n >> W;
    for (int i = 1; i <= n; i++) {
        char type;
        cin >> type;
        if (type == 'D') {
            DiscreteDish dish;
            cin >> dish.weight >> dish.t >> dish.d;
            discrete_dishes.push_back(dish);
        } else {
            ContinuousDish dish;
            cin >> dish.t >> dish.d;
            continuous_dishes.push_back(dish);
        }
    }

    solve_continuous_part();

    answer = NEG_INF;
    dfs_discrete(0, 0, 0);

    if (answer <= NEG_INF / 2) {
        cout << "impossible\n";
    } else {
        cout << fixed << setprecision(9) << (double) answer << '\n';
    }

    return 0;
}

这题要把离散菜和连续菜拆开看。

离散菜部分:
如果某道离散菜每份重量是 c,取 k 份的总收益是:

k t - Δt * k(k-1) / 2

它是一个关于 k 的凹二次函数,所以属于“凹费用完全背包”。
按重量模 c 分组后,可以把转移式整理成斜率单调的直线查询,用单调队列做到每道菜 O(W)O(W)

连续菜部分:
设剩余重量是 X
每道连续菜的收益函数都是:

f_i(x) = t_i x - Δt_i x^2 / 2

sum x_i = X 下最大化这些凹函数和,最优解满足所有真正被取到的菜具有相同边际收益。
于是连续菜的最优值是一个分段二次函数,可以按 t_i 排序后一次性预处理出每个整数剩余重量 0..W 的最优值。

最后枚举离散菜用了多少整数重量 used,答案就是:

离散最优[used] + 连续最优[W-used]

如果根本没有连续菜,那就必须由离散菜恰好凑出 W,否则输出 impossible

DP 转移方程

核心状态:

disc[w] 为离散菜重量 w 的最大美味

核心转移:

ans=max_w disc[w]+cont[W-w]

答案收束:

W 重量恰好凑满的最大值

代码

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

const long double NEG_INF = -1e100L;

struct DiscreteDish {
    int weight;
    long double t;
    long double d;
};

struct ContinuousDish {
    long double t;
    long double d;
};

struct Line {
    long double m, b;
};

int n, W;
vector<DiscreteDish> discrete_dishes;
vector<ContinuousDish> continuous_dishes;
vector<long double> dp, ndp;
vector<long double> cont_best;

long double value_of_line(const Line &line, long double x) {
    return line.m * x + line.b;
}

long double intersect_x(const Line &a, const Line &b) {
    return (a.b - b.b) / (b.m - a.m);
}

void add_line(deque<Line> &q, Line cur) {
    if (!q.empty() && fabsl(q.back().m - cur.m) < 1e-18L) {
        if (q.back().b >= cur.b) {
            return;
        }
        q.pop_back();
    }
    while (q.size() >= 2) {
        Line a = q[q.size() - 2];
        Line b = q[q.size() - 1];
        if (intersect_x(a, b) >= intersect_x(b, cur) - 1e-18L) {
            q.pop_back();
        } else {
            break;
        }
    }
    q.push_back(cur);
}

void solve_discrete_knapsack() {
    dp.assign(W + 1, NEG_INF);
    dp[0] = 0;

    for (size_t idx = 0; idx < discrete_dishes.size(); idx++) {
        int cost = discrete_dishes[idx].weight;
        long double t = discrete_dishes[idx].t;
        long double d = discrete_dishes[idx].d;

        ndp = dp;

        for (int r = 0; r < cost; r++) {
            vector<long double> old;
            for (int pos = r; pos <= W; pos += cost) {
                old.push_back(dp[pos]);
            }

            deque<Line> q;
            int cnt = (int) old.size();

            for (int m = 0; m < cnt; m++) {
                if (old[m] > NEG_INF / 2) {
                    Line cur;
                    cur.m = d * m;
                    cur.b = old[m] - d * m * m / 2.0L - (t + d / 2.0L) * m;
                    add_line(q, cur);
                }

                while (q.size() >= 2 && value_of_line(q[0], m) <= value_of_line(q[1], m) + 1e-18L) {
                    q.pop_front();
                }

                if (!q.empty()) {
                    long double best =
                        -d * m * m / 2.0L + (t + d / 2.0L) * m + value_of_line(q[0], m);
                    int pos = r + m * cost;
                    ndp[pos] = max(ndp[pos], best);
                }
            }
        }

        dp.swap(ndp);
    }
}

void solve_continuous_part() {
    cont_best.assign(W + 1, NEG_INF);
    cont_best[0] = 0;

    if (continuous_dishes.empty()) {
        return;
    }

    long double best_const = NEG_INF;
    vector<ContinuousDish> quad;

    for (size_t i = 0; i < continuous_dishes.size(); i++) {
        if (fabsl(continuous_dishes[i].d) < 1e-18L) {
            best_const = max(best_const, continuous_dishes[i].t);
        } else {
            quad.push_back(continuous_dishes[i]);
        }
    }

    if (best_const > NEG_INF / 2) {
        vector<ContinuousDish> filtered;
        for (size_t i = 0; i < quad.size(); i++) {
            if (quad[i].t > best_const + 1e-18L) {
                filtered.push_back(quad[i]);
            }
        }
        quad.swap(filtered);
    }

    sort(quad.begin(), quad.end(), [](const ContinuousDish &a, const ContinuousDish &b) {
        return a.t > b.t;
    });

    int c = (int) quad.size();
    vector<long double> pref_a(c + 1, 0), pref_b(c + 1, 0), pref_c(c + 1, 0);

    for (int i = 1; i <= c; i++) {
        long double inv = 1.0L / quad[i - 1].d;
        pref_a[i] = pref_a[i - 1] + inv;
        pref_b[i] = pref_b[i - 1] + quad[i - 1].t * inv;
        pref_c[i] = pref_c[i - 1] + quad[i - 1].t * quad[i - 1].t * inv / 2.0L;
    }

    if (best_const > NEG_INF / 2 && c == 0) {
        for (int x = 1; x <= W; x++) {
            cont_best[x] = best_const * x;
        }
        return;
    }

    for (int k = 1; k <= c; k++) {
        long double low = pref_b[k] - pref_a[k] * quad[k - 1].t;
        if (low < 0) {
            low = 0;
        }

        long double next_t;
        if (k < c) {
            next_t = quad[k].t;
            if (best_const > NEG_INF / 2) {
                next_t = max(next_t, best_const);
            }
        } else if (best_const > NEG_INF / 2) {
            next_t = best_const;
        } else {
            next_t = -1e100L;
        }

        long double high;
        if (next_t < -1e90L) {
            high = W;
        } else {
            high = pref_b[k] - pref_a[k] * next_t;
        }

        int left = (int) ceill(low - 1e-18L);
        int right = (int) floorl(high + 1e-18L);
        if (left < 0) {
            left = 0;
        }
        if (right > W) {
            right = W;
        }

        for (int x = left; x <= right; x++) {
            long double lambda = (pref_b[k] - x) / pref_a[k];
            long double val = pref_c[k] - lambda * lambda * pref_a[k] / 2.0L;
            cont_best[x] = max(cont_best[x], val);
        }
    }

    if (best_const > NEG_INF / 2) {
        long double x0 = pref_b[c] - pref_a[c] * best_const;
        if (x0 < 0) {
            x0 = 0;
        }
        long double base = pref_c[c] - best_const * best_const * pref_a[c] / 2.0L;
        int start = (int) ceill(x0 - 1e-18L);
        if (start < 0) {
            start = 0;
        }
        for (int x = start; x <= W; x++) {
            cont_best[x] = max(cont_best[x], base + (x - x0) * best_const);
        }
    }
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    cin >> n >> W;
    for (int i = 1; i <= n; i++) {
        char type;
        cin >> type;
        if (type == 'D') {
            DiscreteDish dish;
            cin >> dish.weight >> dish.t >> dish.d;
            discrete_dishes.push_back(dish);
        } else {
            ContinuousDish dish;
            cin >> dish.t >> dish.d;
            continuous_dishes.push_back(dish);
        }
    }

    solve_discrete_knapsack();
    solve_continuous_part();

    long double answer = NEG_INF;

    if (continuous_dishes.empty()) {
        answer = dp[W];
    } else {
        for (int used = 0; used <= W; used++) {
            if (dp[used] <= NEG_INF / 2 || cont_best[W - used] <= NEG_INF / 2) {
                continue;
            }
            answer = max(answer, dp[used] + cont_best[W - used]);
        }
    }

    if (answer <= NEG_INF / 2) {
        cout << "impossible\n";
    } else {
        cout << fixed << setprecision(9) << (double) answer << '\n';
    }

    return 0;
}

复杂度

设总重量为 W

  • 离散菜部分复杂度 O(离散菜数量W)O(离散菜数量 * W)
  • 连续菜部分预处理复杂度 O(连续菜数量log连续菜数量+W)O(连续菜数量 log 连续菜数量 + W)

总空间复杂度 O(W)O(W)

总结

这题最关键的是不要把两类菜混在一起做。

  • 离散菜是“凹费用背包”
  • 连续菜是“分段二次最优分配”

拆开以后各自处理,再在总重量上合并,结构就清楚了。

一图流解析

这张图把本题的建模、关键转移、实现检查和训练方法压缩到一页,适合读完正文后复盘。

一图流解析