G. Petya and Graph 解题思路
核心问题分析
题目的要求很直白:在给定的图中挑出一个子图(选一些点和一些边)。
- 收益(正权):选中一条边,就能获得它的权重 。
- 代价(负权):选中一个点,就要付出它的权重 (在题目公式中体现为减去点的权重)。
- 限制条件:如果你想选中某条边,你必须同时选中这条边的两个端点!
我们需要在这些限制下,最大化“边权总和 点权总和”。 (注意:如果什么都不选,收益为 0。所以最终答案最差也是 0)。
初看这道题,这是一个典型的 “选 A 就必须选 B”的捆绑依赖问题,且带有正收益和负代价。在算法竞赛中,只要闻到了这股味道,就必须立刻条件反射地想到图论里最经典的核武器——最大权闭合子图(Maximum Weight Closure of a Graph),也就是网络流的最小割模型。
1. 建模转化:将组合优化变为“割”
在最小割模型中,我们将全图所有的元素(无论边还是点)都看作网络流中的“节点”。我们要把它们分到两个阵营:
- 源点 阵营:代表 **“选中”**该元素。
- 汇点 阵营:代表 **“不选”**该元素。
我们如何通过连边,用“最小割(Min-Cut)”的代价来模拟题目的限制呢?
- 正收益(边):选它能赚钱。我们从源点 连一条容量为 的边到“原图的边节点 ”。 如果这条连线被割断,意味着我们放弃了这笔钱,产生 的损失。
- 负代价(点):选它要花钱。我们从“原图的点节点 ”连一条容量为 的边到汇点 。 如果这条连线被割断,意味着我们付出了代价买下了这个点,产生 的损失。
- 强制依赖关系:选了边 ,就必须选它的端点 和 。 我们从边节点 连两条容量为 (无穷大)的有向边,分别指向点节点 和 。 因为容量是无穷大,最小割绝对不可能去割这两条边!这就强制保证了:如果你不割断 (即你选了这条边拿了收益),由于 边不能割,你为了把 和 分开,就必须去割断 和 (即你必须为这两个端点掏钱)!
2. 顿悟时刻:答案怎么算?
根据上述建模,我们跑一次最大流(即最小割)。 这个“最小割”切掉的容量,实际上代表了什么? 它代表了:我们为了满足条件,不得不放弃的“正收益”,加上不得不付出的“点代价”,这两者之和的最小值。
那么,我们的最终最大收益就是:
这不仅逻辑完美闭环,而且由于 Dinic 算法在处理这种浅层二分图时速度极快,时间复杂度完全能够轻松跑过 的数据规模。
CPP 代码实现
cpp
// G. Petya and Graph
#include <bits/stdc++.h>
#define lg(x) (63 - __builtin_clzll(x))
#define all(x) (x).begin(), (x).end()
#define low_bit(x) ((x) & (-x))
#define pb push_back
#define db long double
#define int long long
#define sz(x) (int)x.size()
#define endl "\n"
using namespace std;
const int INF = 2e18;
// 标准的高效网络流 Dinic 模板
struct Dinic {
struct Edge {
int to;
int cap;
int rev;
};
vector<vector<Edge>> adj;
vector<int> level;
vector<int> ptr;
Dinic(int n) : adj(n), level(n), ptr(n) {}
// 加边操作,自带建反向边
void add_edge(int u, int v, int c) {
adj[u].push_back({v, c, (int)adj[v].size()});
adj[v].push_back({u, 0, (int)adj[u].size() - 1});
}
// BFS 建立分层图
bool bfs(int s, int t) {
fill(level.begin(), level.end(), -1);
level[s] = 0;
queue<int> q;
q.push(s);
while (!q.empty()) {
int v = q.front();
q.pop();
for (auto& edge : adj[v]) {
if (edge.cap > 0 && level[edge.to] == -1) {
level[edge.to] = level[v] + 1;
q.push(edge.to);
}
}
}
return level[t] != -1;
}
// DFS 寻找增广路 (自带当前弧优化 ptr)
int dfs(int v, int t, int pushed) {
if (pushed == 0) return 0;
if (v == t) return pushed;
for (int& cid = ptr[v]; cid < adj[v].size(); ++cid) {
auto& edge = adj[v][cid];
int tr = edge.to;
if (level[v] + 1 != level[tr] || edge.cap == 0) continue;
int tr_pushed = dfs(tr, t, min(pushed, edge.cap));
if (tr_pushed == 0) continue;
edge.cap -= tr_pushed;
adj[tr][edge.rev].cap += tr_pushed;
return tr_pushed;
}
return 0;
}
// 核心跑最大流入口
int max_flow(int s, int t) {
int flow = 0;
while (bfs(s, t)) {
fill(ptr.begin(), ptr.end(), 0);
while (int pushed = dfs(s, t, INF)) {
flow += pushed;
}
}
return flow;
}
};
void solve() {
int n, m;
cin >> n >> m;
// 网络流总节点数:源点 S(1个) + 汇点 T(1个) + 原图边节点(m个) + 原图点节点(n个)
Dinic dinic(n + m + 2);
int S = 0, T = n + m + 1;
int sum = 0; // 记录如果所有边都选,能带来的理论最大收益
// 处理“负代价(点)”
// 原图的点编号为 1~n,我们在网络流图中将其映射为 m+1 ~ m+n
for (int i = 1; i <= n; i++) {
int temp;
cin >> temp;
// 点节点连向汇点 T,容量为选这个点需要付出的代价 a_i
dinic.add_edge(i + m, T, temp);
}
// 处理“正收益(边)”及其“强制依赖关系”
// 原图的边编号为 1~m,在网络流图中直接映射为 1~m
for (int i = 1; i <= m; i++) {
int u, v, w;
cin >> u >> v >> w;
sum += w; // 累加理论总收益
// 收益入账:源点 S 连向边节点,容量为边的权重 w_i
dinic.add_edge(S, i, w);
// 强制依赖:选了边 i,就必须买下端点 u 和 v
// 连向无穷大 INF 容量的边,防止最小割切断这种依赖
dinic.add_edge(i, m + u, INF);
dinic.add_edge(i, m + v, INF);
}
// 最大权闭合子图经典公式:
// 最优解 = 全盘正收益之和 - 最小割(即最小总损失)
cout << sum - dinic.max_flow(S, T) << endl;
}
signed main() {
// 榨干最后一点 IO 性能
ios_base::sync_with_stdio(false);
cin.tie(nullptr);
int t = 1;
// cin >> t; // 本题只有单测数据
while (t--) {
solve();
}
}