把每张钞票看成独立物品,做 2 维 DP:dp[a][b] 表示最终分给 Alice 金额为 a、Bob 金额为 b 时最多能保留多少张原主人不变的钞票,答案是总张数减去最多保留张数。
OJ: luogu
题目 ID: P4026
难度:提高+/省选-
标签:动态规划背包状态设计分类讨论
日期: 2026-06-21 10:28
题意
三个人互相之间有循环债务,同时每个人手里有一些不同面额的钞票。
现在要通过重新分配这些钞票来清偿债务,要求每个人最终持有的钱数恰好等于清债后的目标金额。
代价是“发生交换的钞票张数”。
求最少需要交换多少张钞票;如果无解,输出 impossible。
思路
先看一个可以直接验证想法的朴素解:
#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_atarget_btarget_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]
代码
#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 状态数是
在本题数据范围内可以通过。
总结
这题的关键是把“最少交换张数”转成“最多保留多少张原主人不变的钞票”。
一旦换这个角度,题目就变成非常标准的逐张物品分配 DP。
一图流解析
这张图把本题的建模、关键转移、实现检查和训练方法压缩到一页,适合读完正文后复盘。
