乔乔和牛牛逛超市
将商品的内部数量和端点数量拆成闭合图节点,用最小割求最大总收益。
OJ: shumeng
题目 ID: CSP202006E
难度:提高+/省选-
标签:网络流最小割最大权闭合子图建模
日期: 2026-07-31 16:21
形式化题目
有
- 只有
被购买, 才可以被购买; - 只有
被购买, 的购买个数才可以恰好是 或 。
求满足依赖约束时的最大总收益。
思路
区间内购买数量的取值很多,但每个商品只需要保留三种最优选择:不买、买内部数量、买端点数量。
压缩为两种选择
端点收益为
建闭合图
对商品
- 节点
表示选择内部最优,权值 ; - 节点
表示把内部选择切换到端点,权值 ,并连强制边 。
于是不选两个节点表示不买,只选节点
把正权点从源连入、负权点连向汇,最大权闭合子图的答案是正权和减最小割。
先看一个递归枚举全部购买数量的朴素解:
cpp
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-07-31 16:21
* update_at: 2026-08-17 22:39
*/
// brute.cpp:小数据暴力解,递归枚举每种商品的不买或所有合法购买数量。
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 15;
// 一条依赖关系:type 1 表示买 y 必须先买 x;type 2 表示 y 取端点数量时必须先买 x
struct Relation {
int type, x, y;
};
int n, m;
int left_bound[MAXN], right_bound[MAXN], a[MAXN], b[MAXN], c[MAXN];
int amount[MAXN]; // amount[i] 表示第 i 种商品购买的个数,0 表示不买
long long best_answer = 0;
vector<Relation> relation;
// 第 i 种商品购买 x 个时的收益
long long get_value(int i, int x) {
return 1LL * a[i] * x * x + 1LL * b[i] * x + c[i];
}
// 递归枚举每一种商品的选择:不买,或买 [left, right] 中的任意整数数量
void dfs(int position, long long value) {
if (position > n) {
// 所有商品选择完毕后,逐条检查依赖关系是否满足
for (int i = 0; i < (int)relation.size(); i++) {
Relation now = relation[i];
if (now.type == 1 && amount[now.y] > 0 && amount[now.x] == 0) return;
if (now.type == 2 && (amount[now.y] == left_bound[now.y] || amount[now.y] == right_bound[now.y])
&& amount[now.x] == 0) return;
}
best_answer = max(best_answer, value);
return;
}
amount[position] = 0; // 这一层选择"不买"
dfs(position + 1, value);
for (int x = left_bound[position]; x <= right_bound[position]; x++) {
amount[position] = x;
dfs(position + 1, value + get_value(position, x));
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> m;
for (int i = 1; i <= n; i++) cin >> left_bound[i] >> right_bound[i] >> a[i] >> b[i] >> c[i];
for (int i = 1; i <= m; i++) {
Relation now;
cin >> now.type >> now.x >> now.y;
relation.push_back(now);
}
dfs(1, 0);
cout << best_answer << '\n';
return 0;
}代码
cpp
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-07-31 16:21
* update_at: 2026-08-17 22:39
*/
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 10005;
const int MAXV = MAXN * 2 + 5;
const int MAXE = 500000;
const long long INF = (long long)4e18;
struct Edge {
int to, next;
long long flow;
};
int n, m, source, sink, edge_count = 1;
int head[MAXV], level[MAXV], current[MAXV]; // current 为当前弧优化指针
Edge edge[MAXE];
// 加一条 from -> to 容量为 flow 的边,并添加反向零容量边
void add_edge(int from, int to, long long flow) {
edge[++edge_count] = {to, head[from], flow};
head[from] = edge_count;
edge[++edge_count] = {from, head[to], 0};
head[to] = edge_count;
}
// BFS 分层,判断是否还有增广路
bool bfs() {
queue<int> q;
memset(level, -1, sizeof(level));
level[source] = 0;
q.push(source);
while (!q.empty()) {
int node = q.front();
q.pop();
for (int i = head[node]; i; i = edge[i].next) {
if (edge[i].flow > 0 && level[edge[i].to] == -1) {
level[edge[i].to] = level[node] + 1;
q.push(edge[i].to);
}
}
}
return level[sink] != -1;
}
// DFS 沿分层图找增广路,limit 为当前允许通过的最大流量
long long dfs(int node, long long limit) {
if (node == sink) return limit;
for (int &i = current[node]; i; i = edge[i].next) {
int to = edge[i].to;
if (edge[i].flow == 0 || level[to] != level[node] + 1) continue;
long long pushed = dfs(to, min(limit, edge[i].flow));
if (pushed) {
edge[i].flow -= pushed;
edge[i ^ 1].flow += pushed;
return pushed;
}
}
return 0;
}
// 最大流 = 最小割
long long dinic() {
long long result = 0, pushed;
while (bfs()) {
memcpy(current, head, sizeof(head));
while ((pushed = dfs(source, INF)) != 0) result += pushed;
}
return result;
}
// 二次收益函数 a*x^2 + b*x + c
long long get_value(int a, int b, int c, long long x) {
return 1LL * a * x * x + 1LL * b * x + c;
}
// 求开区间 (left, right) 内部整数点上的最大收益,对应"内部数量"选择
long long get_inner_best(int left, int right, int a, int b, int c) {
long long answer = -(long long)4e18;
long long candidates[4];
candidates[0] = left + 1;
candidates[1] = right - 1;
candidates[2] = left + 1;
candidates[3] = right - 1;
if (a < 0) {
// 开口向下时对称轴附近收益最大,检查对称轴两侧整数
long long denominator = -2LL * a;
long long numerator = b;
long long floor_value = numerator >= 0 ? numerator / denominator : -((-numerator + denominator - 1) / denominator);
candidates[2] = floor_value;
candidates[3] = floor_value + 1;
}
for (int i = 0; i < 4; i++) {
// 把候选点裁剪回开区间内部
long long x = max((long long)left + 1, min((long long)right - 1, candidates[i]));
answer = max(answer, get_value(a, b, c, x));
}
return answer;
}
// 给节点加权:正权点从源连入并累加总正权,负权点连向汇
void add_weight(int node, long long value, long long &sum) {
if (value > 0) {
add_edge(source, node, value);
sum += value;
} else if (value < 0) {
add_edge(node, sink, -value);
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> m;
source = 2 * n + 1;
sink = source + 1;
long long positive_sum = 0;
// 每种商品建两个节点:i 表示内部收益,i+n 表示切换到端点收益
for (int i = 1; i <= n; i++) {
int left, right, a, b, c;
cin >> left >> right >> a >> b >> c;
long long endpoint = max(get_value(a, b, c, left), get_value(a, b, c, right));
long long inner = get_inner_best(left, right, a, b, c);
add_weight(i, inner, positive_sum);
add_weight(i + n, endpoint - inner, positive_sum);
add_edge(i + n, i, INF); // 强制:选端点必须连带选内部
}
// 依赖关系转成闭合图强制边
for (int i = 1; i <= m; i++) {
int type, x, y;
cin >> type >> x >> y;
if (type == 1) add_edge(y, x, INF); // 买 y 必须先买 x
else add_edge(y + n, x, INF); // y 取端点时必须先买 x
}
// 最大权闭合子图答案 = 正权和 - 最小割
cout << positive_sum - dinic() << '\n';
return 0;
}复杂度
图有
总结
区间内的二次收益先压缩为内部和端点两种选择;依赖条件再转为闭合图中的强制边。这样原问题成为标准最大权闭合子图。