[SHOI2008] 循环的债务

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

把每张钞票看成独立物品,做 2 维 DP:dp[a][b] 表示最终分给 Alice 金额为 a、Bob 金额为 b 时最多能保留多少张原主人不变的钞票,答案是总张数减去最多保留张数。

OJ: luogu

题目 ID: P4026

难度:提高+/省选-

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

日期: 2026-06-21 10:28

题意

三个人互相之间有循环债务,同时每个人手里有一些不同面额的钞票。

现在要通过重新分配这些钞票来清偿债务,要求每个人最终持有的钱数恰好等于清债后的目标金额。

代价是“发生交换的钞票张数”。
求最少需要交换多少张钞票;如果无解,输出 impossible

思路

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

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

const int INF_NEG = -1000000000;

struct Bill {
    int value;
    int owner;
};

int x1, x2, x3;
int cnt[3][6];
int deno[6] = {100, 50, 20, 10, 5, 1};
vector<Bill> bills;
int target_a, target_b, target_c;
int total_bills;
int answer_stay;

void dfs(int idx, int a, int b, int c, int stay) {
    if (idx == (int) bills.size()) {
        if (a == target_a && b == target_b && c == target_c) {
            answer_stay = max(answer_stay, stay);
        }
        return;
    }

    int v = bills[idx].value;
    int owner = bills[idx].owner;

    if (a + v <= target_a) {
        dfs(idx + 1, a + v, b, c, stay + (owner == 0));
    }
    if (b + v <= target_b) {
        dfs(idx + 1, a, b + v, c, stay + (owner == 1));
    }
    if (c + v <= target_c) {
        dfs(idx + 1, a, b, c + v, stay + (owner == 2));
    }
}

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

    // brute.cpp:小数据暴力解,用来帮助理解题意并辅助对拍。
    // 把每张钞票看成独立物品,枚举它最终留给 Alice / Bob / Cynthia 的哪一个人。
    cin >> x1 >> x2 >> x3;
    for (int i = 0; i < 3; i++) {
        for (int j = 0; j < 6; j++) {
            cin >> cnt[i][j];
        }
    }

    int init_money[3] = {0, 0, 0};
    for (int i = 0; i < 3; i++) {
        for (int j = 0; j < 6; j++) {
            init_money[i] += cnt[i][j] * deno[j];
            total_bills += cnt[i][j];
            for (int t = 0; t < cnt[i][j]; t++) {
                bills.push_back({deno[j], i});
            }
        }
    }

    target_a = init_money[0] - x1 + x3;
    target_b = init_money[1] + x1 - x2;
    target_c = init_money[2] + x2 - x3;

    if (target_a < 0 || target_b < 0 || target_c < 0) {
        cout << "impossible\n";
        return 0;
    }

    answer_stay = INF_NEG;
    dfs(0, 0, 0, 0, 0);

    if (answer_stay == INF_NEG) {
        cout << "impossible\n";
    } else {
        cout << total_bills - answer_stay << '\n';
    }

    return 0;
}

这题最适合的视角不是“钱怎么传”,而是:

每张钞票最后归谁?

如果一张钞票最后还留在原主人手里,那么它就没有被交换。
所以目标就变成:

在满足三个人最终金额要求的前提下,尽量让更多钞票留在原主人手里。

设三个人最终应该持有的钱分别是:

  • target_a
  • target_b
  • target_c

这三个值可以从输入债务直接算出来。

接着把每张钞票看成一个独立物品。
做一个二维 DP:

dp[a][b] = 当前已经处理完若干张钞票,最终分给 Alice 金额为 a、分给 Bob 金额为 b 时,最多有多少张钞票没换主人

那么分给 Cynthia 的金额就由“已处理总金额 - a - b”唯一确定。

处理一张钞票时有三种选择:最终给 Alice、Bob 或 Cynthia。
如果给的人正好是原主人,那么“保留原主人不变的张数”就加一。

最终如果 dp[target_a][target_b] 可达,那么:

最少交换张数 = 总张数 - 最多保留张数

DP 转移方程

核心状态:

dp[a][b] 为 Alice 金额 a、Bob 金额 b 时最多保留数

核心转移:

每张钞票分别尝试给 A/B/C,给原主人则 +1

答案收束:

总张数-dp[target_a][target_b]

代码

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

const int INF_NEG = -1000000000;

struct Bill {
    int value;
    int owner;
};

int x1, x2, x3;
int cnt[3][6];
int deno[6] = {100, 50, 20, 10, 5, 1};
vector<Bill> bills;
vector<int> dp, ndp;

int id(int a, int b, int lim_b) {
    return a * (lim_b + 1) + b;
}

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

    cin >> x1 >> x2 >> x3;
    for (int i = 0; i < 3; i++) {
        for (int j = 0; j < 6; j++) {
            cin >> cnt[i][j];
        }
    }

    int init_money[3] = {0, 0, 0};
    int total_value = 0;
    int total_bills = 0;

    for (int i = 0; i < 3; i++) {
        for (int j = 0; j < 6; j++) {
            init_money[i] += cnt[i][j] * deno[j];
            total_value += cnt[i][j] * deno[j];
            total_bills += cnt[i][j];
            for (int t = 0; t < cnt[i][j]; t++) {
                bills.push_back({deno[j], i});
            }
        }
    }

    int target_a = init_money[0] - x1 + x3;
    int target_b = init_money[1] + x1 - x2;
    int target_c = init_money[2] + x2 - x3;

    if (target_a < 0 || target_b < 0 || target_c < 0 ||
        target_a + target_b + target_c != total_value) {
        cout << "impossible\n";
        return 0;
    }

    dp.assign((target_a + 1) * (target_b + 1), INF_NEG);
    ndp.assign((target_a + 1) * (target_b + 1), INF_NEG);
    dp[id(0, 0, target_b)] = 0;

    int processed_value = 0;

    for (size_t idx = 0; idx < bills.size(); idx++) {
        fill(ndp.begin(), ndp.end(), INF_NEG);
        int v = bills[idx].value;
        int owner = bills[idx].owner;

        int max_a = min(target_a, processed_value);
        for (int a = 0; a <= max_a; a++) {
            int low_b = processed_value - a - target_c;
            if (low_b < 0) {
                low_b = 0;
            }
            int high_b = processed_value - a;
            if (high_b > target_b) {
                high_b = target_b;
            }
            for (int b = low_b; b <= high_b; b++) {
                int cur = dp[id(a, b, target_b)];
                if (cur == INF_NEG) {
                    continue;
                }

                if (a + v <= target_a) {
                    int &ref = ndp[id(a + v, b, target_b)];
                    ref = max(ref, cur + (owner == 0));
                }

                if (b + v <= target_b) {
                    int &ref = ndp[id(a, b + v, target_b)];
                    ref = max(ref, cur + (owner == 1));
                }

                int next_c = processed_value + v - a - b;
                if (next_c <= target_c) {
                    int &ref = ndp[id(a, b, target_b)];
                    ref = max(ref, cur + (owner == 2));
                }
            }
        }

        processed_value += v;
        dp.swap(ndp);
    }

    int stay_best = dp[id(target_a, target_b, target_b)];
    if (stay_best == INF_NEG) {
        cout << "impossible\n";
    } else {
        cout << total_bills - stay_best << '\n';
    }

    return 0;
}

复杂度

设总金额不超过 S,这里 S <= 1000

DP 状态数是 O(targetatargetb)O(target_a * target_b),总复杂度约为:

O(总钞票数targetatargetb)O(总钞票数 * target_a * target_b)

在本题数据范围内可以通过。

总结

这题的关键是把“最少交换张数”转成“最多保留多少张原主人不变的钞票”。

一旦换这个角度,题目就变成非常标准的逐张物品分配 DP。

一图流解析

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

一图流解析