原题链接:B.Link with Railway Company
题目大意:
给出一棵 n n n 个点, n − 1 n - 1 n−1 条边的树,每个点代表一个城市,每条边可以修建一条铁路,花费为 c i c_i ci。
现在你有 m m m 个修建线路的方案,线路是从 a i a_i ai 城市到 b i b_i bi 城市的最短路径,修筑这条线路后你会获得 x i x_i xi 的价值,维护费是 y i y_i yi,即净利润为( x i − y i x_i - y_i xi−yi)。
当且仅当在 a i a_i ai 到 b i b_i bi 这条最短路上的所有边都修筑了一条铁路,一个方案才能完成。
你可以任选一些边,在上面花费 c i c_i ci 的价格修筑铁路,然后任选一些方案获得收入。
问你在你任选一些方案修筑铁路后,你能获得的最大价值为多少。
解题思路:
很典型的最大权闭合子图题目,我们把题目转化为下图:
左部点的点权为正值,对应方案的利润。
右部点的点权为负值,对应铁路的修建费。
闭合子图即选取一个点时,与其相连的所有点都必须选取。
那么题意就变成了,我们可以任意选取一部分的左部点,同时与左部有边相连的的右部点也必须被选取,问你选取的点最大点权和为多少。
考虑建立流网络,我们用最小割解决。
- 源点向所有正权点连一条容量为点权的边。
- 所有负权点向汇点连一条容量为点权绝对值的边。
- 保留原图中的边,容量为正无穷。
在流网络上跑最小割,答案就是 所有正权点的点权和
−
-
− 最小割 。
如果最小割割去的边为 S → V S\rightarrow V S→V 的边,那么就代表这一个点(方案)不选,如果割去的边为 V → T V \rightarrow T V→T 的边,那么就代表这一个点(铁路)要选。
这里不讲证明,根据直觉解释一下为什么这么做,和这么做的正确性。
- 我们源点 S S S 向所有方案连了一条边,我们可以看作是我们可以获得的总金额,我们所有铁路向汇点 T T T 连了一条边,我们可以看作是我们的总花费。
- 考虑最小割的情况,因为最小权闭合子图的最小割都是简单割,我们要么会割去 S → V S \rightarrow V S→V 的边,要么会割去 V → T V \rightarrow T V→T 的边,不会割去那些流量为正无穷的边。
- 如果割去 S → V S \rightarrow V S→V 的边,流量为 X X X,我们可以想象成我们本来能赚到 X X X 元,但我们都把所有的 X X X 元花在了修建铁路上,因此我们的 ∑ X \sum X ∑X 要减去 X X X。
- 如果割去的是 V → T V \rightarrow T V→T 的边,流量为 Y Y Y ,我们可以想象成那些所有的没被割去的 S → V S \rightarrow V S→V 的边,本来能赚 ∑ X \sum X ∑X 元,但是有一部分 Y Y Y 花到了修建铁路上,所以我们要减去 Y Y Y。
- 对于残量网络而言,显然答案就是那些没被割去的边 S → V S \rightarrow V S→V 剩余容量之和,转化成最终答案,就是所有能赚的钱,减去这张图的最小割了。
回到原题来,我们就让方案和该方案所有要修建的铁路连一条正无穷的边,然后源点 S S S 与方案,铁路与汇点 T T T 按照上述方式相连就好了。
但是我们会发现节点非常多,要连的边条数可能会达到 O ( n m ) O(nm) O(nm) 的量级,显然不现实,考虑用什么方式优化一下建图。
注意到题目中方案需求构建的铁路线路在树上呈链状形式,我们先把所有边权转化到点权上,考虑用树剖(重链剖分)和线段树维护每一条链,优化边数至 O ( m log 2 n ) O(m \log^2 n) O(mlog2n)条,最后建立流网络即可。
具体细节看代码即可。
时间复杂度: O ( n 2 m log 2 n ) O(n^2m\log^2n) O(n2mlog2n)
AC代码:
#include <bits/stdc++.h>
#define YES return void(cout << "Yes\n")
#define NO return void(cout << "No\n")
using namespace std;
using u64 = unsigned long long;
using PII = pair<int, int>;
using i64 = long long;
template<class Ty>
struct maxFlow {
const Ty INF = numeric_limits<Ty>::max();
const int inf = INT32_MAX;
int S, T, n; Ty MF = 0;
struct Edge {
int v, nxt; Ty f;
Edge() : Edge(0, 0, 0) {};
Edge(int v, int nxt, Ty f) : v(v), nxt(nxt), f(f) {};
};
vector<int> dist, cur, h;
vector<Edge> E;
maxFlow(int _) : n(_) { init(_); };
void init(int _) {
dist.resize(_ + 1);
cur.resize(_ + 1);
h.resize(_ + 1);
E.resize(2);
}
void add(int u, int v, Ty f) {
E.emplace_back(v, h[u], f), h[u] = int(E.size()) - 1;
}
void addEdge(int u, int v, Ty f) {
add(u, v, f), add(v, u, 0);
}
bool BFS() {
dist.assign(n + 1, inf);
queue<int> que;
dist[S] = 0, cur[S] = h[S];
que.push(S);
while (que.size()) {
int u = que.front(); que.pop();
for (int i = h[u]; i; i = E[i].nxt) {
auto& [v, nxt, f] = E[i];
if (f && dist[v] > dist[u] + 1) {
dist[v] = dist[u] + 1;
cur[v] = h[v];
que.push(v);
}
}
}
return dist[T] != inf;
}
Ty DFS(int u, Ty flow) {
if (u == T) return flow;
Ty last = flow;
for (int i = cur[u]; i && last; i = E[i].nxt) {
cur[u] = i;
auto& [v, nxt, f] = E[i];
if (f && dist[v] == dist[u] + 1) {
Ty cost = DFS(v, min(f, last));
if (!cost) dist[v] = INF;
E[i].f -= cost, E[i ^ 1].f += cost;
last -= cost;
}
}
return flow - last;
}
void Dinic() {
while (BFS()) {
Ty flow = DFS(S, INF);
MF += flow;
}
}
};
maxFlow<int> F(1e5);
const int N = 1e5 + 10;
vector<PII> g[N];
//重链剖分基本模板
int son[N], fa[N], siz[N], dep[N], arr[N];
void DFS1(int u) {
siz[u] = 1, dep[u] = u[fa][dep] + 1;
for (auto& [v, w] : g[u]) {
if (v == fa[u]) continue;
fa[v] = u, DFS1(v);
siz[u] += siz[v];
if (siz[v] > siz[son[u]]) son[u] = v;
arr[v] = w;
}
}
int dfn[N], top[N], id[N], idx = 0;
void DFS2(int u, int tp) {
top[u] = tp, dfn[u] = ++idx, id[idx] = u;
if (son[u]) DFS2(son[u], tp);
for (auto& [v, w] : g[u]) {
if (v == fa[u] || v == son[u]) continue;
DFS2(v, v);
}
}
#define lson k << 1, l, mid
#define rson k << 1 | 1, mid + 1, r
const int INF = INT32_MAX;
void build(int k, int l, int r) {
if (l == r) {
//叶子节点(铁路)向汇点T连边 容量为花费C
F.addEdge(k, F.T, arr[id[l]]);
return;
}
int mid = l + r >> 1;
build(lson), build(rson);
int ls = k << 1, rs = k << 1 | 1;
F.addEdge(k, ls, INF);
F.addEdge(k, rs, INF);
}
// 线段树优化建图
void connect(int k, int l, int r, int x, int y, int u) {
if (l >= x && r <= y) {
//方案和线段树节点连边
F.addEdge(u, k, INF);
return;
}
int mid = l + r >> 1;
if (x <= mid) connect(lson, x, y, u);
if (y > mid) connect(rson, x, y, u);
}
int n, m;
void addLink(int u, int x, int y) {
while (top[x] != top[y]) {
if (dep[top[x]] < dep[top[y]]) swap(x, y);
connect(1, 1, n, dfn[top[x]], dfn[x], u);
x = fa[top[x]];
}
if (dep[x] > dep[y]) swap(x, y);
//点权转化到了边上,所以祖先x是不能算入的
connect(1, 1, n, dfn[x] + 1, dfn[y], u);
}
void solve() {
cin >> n >> m;
for (int i = 1, u, v, w; i <= n - 1; ++i) {
cin >> u >> v >> w;
g[u].emplace_back(v, w);
g[v].emplace_back(u, w);
}
//先跑一遍树剖
DFS1(1), DFS2(1, 1);
F.S = 0, F.T = n * 4;
//把线段树图建出来
build(1, 1, n);
//idx为 每个方案对应的点的编号
idx = n * 4;
int sum = 0;
for (int i = 1, u, v, x, y; i <= m; ++i) {
cin >> u >> v >> x >> y;
//利润为负显然不选
if (x - y <= 0) continue;
sum += x - y;
//否则源点S和方案连一条边 容量为利润
F.addEdge(F.S, ++idx, x - y);
//方案和所有铁路连一条边
addLink(idx, u, v);
}
//跑最大流
F.Dinic();
//方案利润和减去最小割即可
cout << sum - F.MF << '\n';
}
signed main() {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
int t = 1; //cin >> t;
while (t--) solve();
return 0;
}