[USACO3.3] 商店购物 Shopping Offers

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

把最多 5 种商品的购买数量压成 base-6 状态,把优惠包和单买都当成转移,在所有合法购买状态上做最短路式动态规划。

OJ: luogu

题目 ID: P2732

难度:普及+/提高

标签:动态规划状态压缩状态设计记忆化搜索

日期: 2026-06-21 09:39

题意

商店里有若干种优惠包,每个优惠包会把若干件商品打包出售。

最后顾客只关心 b 种商品,每种商品给出:

  • 商品编号
  • 需要购买的件数
  • 单买价格

要求恰好买到这些商品,不能多买,并让总花费最小。

思路

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

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

const int MAXS = 110;
const int MAXB = 5;
const int MAXO = 120;
const int MAXSTATE = 8000;
const int INF = 1e9;

struct RawOffer {
    int item_cnt;
    int code[6];
    int num[6];
    int price;
} raw_offer[MAXS];

struct Offer {
    int num[MAXB];
    int price;
} offer[MAXO];

int s, b;
int target_code[MAXB], need[MAXB], single_price[MAXB];
int pow6[6];
int offer_cnt;
int memo[MAXSTATE];
bool vis[MAXSTATE];

int get_id(int code) {
    for (int i = 0; i < b; i++) {
        if (target_code[i] == code) {
            return i;
        }
    }
    return -1;
}

void add_offer(int cnt[], int price) {
    offer_cnt++;
    for (int i = 0; i < b; i++) {
        offer[offer_cnt].num[i] = cnt[i];
    }
    offer[offer_cnt].price = price;
}

int dfs(int state) {
    if (state == 0) {
        return 0;
    }
    if (vis[state]) {
        return memo[state];
    }
    vis[state] = true;

    int rem[MAXB] = {0};
    int x = state;
    for (int i = 0; i < b; i++) {
        rem[i] = x % 6;
        x /= 6;
    }

    int ans = INF;

    for (int k = 1; k <= offer_cnt; k++) {
        bool ok = true;
        int next_state = state;

        for (int i = 0; i < b; i++) {
            if (offer[k].num[i] > rem[i]) {
                ok = false;
                break;
            }
            next_state -= offer[k].num[i] * pow6[i];
        }

        if (ok) {
            ans = min(ans, dfs(next_state) + offer[k].price);
        }
    }

    memo[state] = ans;
    return ans;
}

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

    // brute.cpp:小数据暴力解,用来帮助理解题意并辅助对拍。
    // 从目标购买状态出发,递归尝试最后一次使用哪一个优惠包或单买方案。
    cin >> s;
    for (int i = 1; i <= s; i++) {
        cin >> raw_offer[i].item_cnt;
        for (int j = 0; j < raw_offer[i].item_cnt; j++) {
            cin >> raw_offer[i].code[j] >> raw_offer[i].num[j];
        }
        cin >> raw_offer[i].price;
    }

    cin >> b;
    for (int i = 0; i < b; i++) {
        cin >> target_code[i] >> need[i] >> single_price[i];
    }

    for (int i = 1; i <= s; i++) {
        int cnt[MAXB] = {0};
        bool bad = false;
        bool useful = false;

        for (int j = 0; j < raw_offer[i].item_cnt; j++) {
            int id = get_id(raw_offer[i].code[j]);
            if (id == -1) {
                bad = true;
                break;
            }
            cnt[id] += raw_offer[i].num[j];
            useful = true;
        }

        if (!bad && useful) {
            add_offer(cnt, raw_offer[i].price);
        }
    }

    for (int i = 0; i < b; i++) {
        int cnt[MAXB] = {0};
        cnt[i] = 1;
        add_offer(cnt, single_price[i]);
    }

    pow6[0] = 1;
    for (int i = 1; i <= b; i++) {
        pow6[i] = pow6[i - 1] * 6;
    }

    int target_state = 0;
    for (int i = 0; i < b; i++) {
        target_state += need[i] * pow6[i];
    }

    cout << dfs(target_state) << '\n';
    return 0;
}

因为最多只会买 5 种商品,而且每种需要的数量也不大,所以最自然的状态就是:

(c1, c2, c3, c4, c5)

表示当前已经买了多少件。

为了让程序更好写,可以把它编码成一个 6 进制数:

state = c1 + c2 * 6 + c3 * 6^2 + ...

这样每个状态都唯一对应一种购买情况。

接下来把所有“可用的购买方式”统一起来:

  1. 题目给出的优惠包
  2. 每种商品单独买 1 件

于是每个优惠包都可以看成一次状态转移:

  • 它让某几种商品的购买数增加
  • 总花费增加这个优惠包的价格

正式做法是正向 DP:

dp[state] = 买到 state 这个状态所需的最小花费

从全 0 状态开始,枚举每个优惠包,尝试转移到下一个状态。
如果某个优惠包会让某种商品买超了,就不能使用。

最终目标状态就是“每种商品都刚好买够”的那个编码值。

代码

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

const int MAXS = 110;
const int MAXB = 5;
const int MAXO = 120;
const int MAXSTATE = 8000;
const int INF = 1e9;

struct RawOffer {
    int item_cnt;
    int code[6];
    int num[6];
    int price;
} raw_offer[MAXS];

struct Offer {
    int num[MAXB];
    int price;
} offer[MAXO];

int s, b;
int target_code[MAXB], need[MAXB], single_price[MAXB];
int pow6[6];
int offer_cnt;
int dp[MAXSTATE];

int get_id(int code) {
    for (int i = 0; i < b; i++) {
        if (target_code[i] == code) {
            return i;
        }
    }
    return -1;
}

void add_offer(int cnt[], int price) {
    offer_cnt++;
    for (int i = 0; i < b; i++) {
        offer[offer_cnt].num[i] = cnt[i];
    }
    offer[offer_cnt].price = price;
}

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

    cin >> s;
    for (int i = 1; i <= s; i++) {
        cin >> raw_offer[i].item_cnt;
        for (int j = 0; j < raw_offer[i].item_cnt; j++) {
            cin >> raw_offer[i].code[j] >> raw_offer[i].num[j];
        }
        cin >> raw_offer[i].price;
    }

    cin >> b;
    for (int i = 0; i < b; i++) {
        cin >> target_code[i] >> need[i] >> single_price[i];
    }

    for (int i = 1; i <= s; i++) {
        int cnt[MAXB] = {0};
        bool bad = false;
        bool useful = false;

        for (int j = 0; j < raw_offer[i].item_cnt; j++) {
            int id = get_id(raw_offer[i].code[j]);
            if (id == -1) {
                bad = true;
                break;
            }
            cnt[id] += raw_offer[i].num[j];
            useful = true;
        }

        if (!bad && useful) {
            add_offer(cnt, raw_offer[i].price);
        }
    }

    // 把原价单买也当成普通优惠包,这样状态转移就统一了。
    for (int i = 0; i < b; i++) {
        int cnt[MAXB] = {0};
        cnt[i] = 1;
        add_offer(cnt, single_price[i]);
    }

    pow6[0] = 1;
    for (int i = 1; i <= b; i++) {
        pow6[i] = pow6[i - 1] * 6;
    }

    int total_state = pow6[b];
    int target_state = 0;
    for (int i = 0; i < b; i++) {
        target_state += need[i] * pow6[i];
    }

    for (int i = 0; i < total_state; i++) {
        dp[i] = INF;
    }
    dp[0] = 0;

    for (int state = 0; state < total_state; state++) {
        if (dp[state] == INF) {
            continue;
        }

        int cur[MAXB] = {0};
        int x = state;
        bool valid = true;
        for (int i = 0; i < b; i++) {
            cur[i] = x % 6;
            x /= 6;
            if (cur[i] > need[i]) {
                valid = false;
                break;
            }
        }
        if (!valid) {
            continue;
        }

        for (int k = 1; k <= offer_cnt; k++) {
            int next_state = state;
            bool ok = true;

            for (int i = 0; i < b; i++) {
                if (cur[i] + offer[k].num[i] > need[i]) {
                    ok = false;
                    break;
                }
                next_state += offer[k].num[i] * pow6[i];
            }

            if (ok) {
                dp[next_state] = min(dp[next_state], dp[state] + offer[k].price);
            }
        }
    }

    cout << dp[target_state] << '\n';
    return 0;
}

复杂度

因为最多只有 5 种商品,每种数量按 0..5 处理,状态总数最多是:

6^5 = 7776

设可用优惠包总数为 m,则:

  • 时间复杂度 O(65m5)O(6^5 * m * 5)
  • 空间复杂度 O(65)O(6^5)

总结

这题的关键不是优惠怎么选,而是先把“购买数量”变成小状态。

一旦把每种商品当前已买数量看成状态,题目就变成非常标准的“小状态最小花费 DP”。

一图流解析

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

一图流解析