载谭费用流对偶
一些你可能需要的前置芝士:
线性规划问题 线性规划问题是如下的问题:
有 \(n\) 个非负变量 \(x\),以及 \(m\) 个约束,形如:
要求最大化 \(\sum_i c_ix_i\) 的值,记做:
一个线性规划的对偶问题是如下的问题:
有 \(m\) 个非负变量 \(y\),以及 \(n\) 个约束,形如:
要求最小化 \(\sum_i b_iy_i\) 的值,记做:
关于线性规划问题,我们有如下的定理:
弱对偶定理 线性规划问题的解总是小于等于其对偶问题的解。
证明
值得注意的是,弱对偶定理对所有的规划问题都成立,包括但不限于整数规划等。
强对偶定理 线性规划的解等于其对偶问题的解。
这个定理证明较为复杂,这里略去。
不同于弱对偶定理,强对偶定理只对线性规划成立。
我们先来考虑一个线性规划的扩展形式:如何对 \(\sum_ia_ix_i=b_i\) 的条件做对偶?
解决方案是,我们把它拆成如下两个条件:
然后记上面那个条件对偶后的变量为 \(y_1\),下面的为 \(y_2\),那么最后要最小化的就是 \(b_i(y_1-y_2)\),于是我们记 \(\varphi_i=y_1-y_2\),此时 \(\varphi_i\) 就没有 \(\geq 0\) 的限制了。
现在来考虑最大费用循环流的线性对偶形式:
于是直接对偶问题可得:
注意到这个形式下,\(x_{u,v}\) 可以直接取到最小值,于是就等价于
于是所有这种形式的问题都可以用最大费用循环流建图。
下面是例题。
Problem(「ZJOI2013」防守战线).
长为 \(n\) 的序列,初始全为 \(0\),在 \(i\) 处加 \(1\) 有 \(c_i\) 的代价。有 \(m\) 个限制,形如 \([l_i,r_i]\) 的区间和必须 \(\geq d_i\)。求最小代价。
\(n\leq 1000,m\leq 10000\)
sol.
记前缀和为 \(S_i\),于是总的代价为
直接拿着跑最大费用循环流即可。可能需要注意一下建图的方式,根据实际测试,不同的建图方式速度差异足足有 \(30\) 倍。
#include <bits/stdc++.h>
#define nya(neko...) fprintf(stderr, neko)
constexpr int INF = 0x3f3f3f3f;
constexpr int maxn = 1005;
constexpr int maxm = 10005;
int n, m;
namespace Flow {
int s, t, tot = 1, minCost, maxFlow;
int first[maxn], cur[maxn];
struct Edge { int to, nxt, cap, w; } e[maxn + maxm << 2];
inline void Add(int u, int v, int cap, int w) {
e[++tot] = { v, first[u], cap, w };
first[u] = tot;
}
inline void Adde(int u, int v, int cap, int w) {
Add(u, v, cap, w), Add(v, u, 0, -w);
}
int phi[maxn]; bool inque[maxn];
inline void SPFA() {
memset(phi, INF, sizeof phi);
std::queue<int> q;
phi[s] = 0, q.push(s), inque[s] = 1;
while(!q.empty()) {
int u = q.front(); inque[u] = 0, q.pop();
for(int i = first[u]; i; i = e[i].nxt) {
int v = e[i].to;
if(e[i].cap && phi[v] > phi[u] + e[i].w) {
phi[v] = phi[u] + e[i].w;
if(!inque[v]) inque[v] = 1, q.push(v);
}
}
}
}
inline int cost(int id) { // reduced cost
return phi[e[id ^ 1].to] - phi[e[id].to] + e[id].w;
}
int dis[maxn];
inline bool dij() {
memset(dis, INF, sizeof dis);
using node = std::pair<int, int>;
std::priority_queue<node, std::vector<node>, std::greater<node>> q;
q.emplace(dis[s] = 0, s);
while(!q.empty()) {
auto o = q.top(); q.pop();
int u = o.second;
if(dis[u] != o.first) continue;
for(int i = first[u]; i; i = e[i].nxt) {
int v = e[i].to;
if(e[i].cap && dis[v] > dis[u] + cost(i)) {
dis[v] = dis[u] + cost(i);
q.emplace(dis[v], v);
}
}
}
return dis[t] < INF;
}
bool vis[maxn];
inline int DFS(int u, int flow) {
if(u == t) return flow;
vis[u] = 1;
int res = 0;
for(int &i = cur[u]; i; i = e[i].nxt) {
int v = e[i].to;
if(vis[v] || !e[i].cap || dis[v] != dis[u] + cost(i)) continue;
int f = DFS(v, std::min(flow, e[i].cap));
e[i].cap -= f, e[i ^ 1].cap += f;
flow -= f, res += f;
minCost += f * e[i].w;
if(!flow) break;
}
vis[u] = 0;
if(flow) vis[u] = 1;
return res;
}
inline void Dinic() {
SPFA();
while(dij()) {
memcpy(cur, first, sizeof cur);
memset(vis, false, sizeof vis);
while(int x = DFS(s, INF)) maxFlow += x;
for(int i = 0; i <= n; ++i) // all nodes
if(dis[i] < INF) phi[i] += dis[i];
}
}
}
using Flow::s;
using Flow::t;
using Flow::Adde;
int deg[maxn];
int main() {
scanf("%d%d", &n, &m), s = n + 1, t = n + 2;
for(int i = 1, c; i <= n; ++i) {
scanf("%d", &c), Adde(i, i - 1, INF, 0);
deg[i] += c, deg[i - 1] -= c;
}
for(int i = 1, l, r, d; i <= m; ++i) {
scanf("%d%d%d", &l, &r, &d);
Adde(r, l - 1, INF, -d);
}
for(int i = 0; i <= n; ++i) {
if(deg[i] >= 0) Adde(s, i, deg[i], 0);
else Adde(i, t, -deg[i], 0);
}
Flow::Dinic(), printf("%d\n", -Flow::minCost);
}
Problem(CF1307G).
\(n\) 个点的带权有向图。\(q\) 组询问,每次给出一个 \(X\),你可以增加每条边的边权,要求总的变化量不超过 \(X\),求 \(1\) 到 \(n\) 的最短路的最大值。
\(n\leq 50,q\leq 10^5\)
sol.
考虑最大费用循环流的一个特化:有源汇最大费用可行流,其形式如下:
其对偶形式即为:
这道题中,我们考虑最小费用可行流,这就是
这个形式非常像题目的形式。我们令 \(s=1,t=n\),并定义 \(\varphi_1\) 为 \(0\),其他点的 \(\varphi_u\) 为 \(1\) 号点到它的最短路 \(dis_{1,u}\),于是 \(x_{u,v}\) 就是我们题目中要求的变化量。然后令 \(c_{u,v}=1\) 就有:
于是我们就有:
然后注意到 \(F\leq m\),我们预先处理出每个 \(F\) 的结果,然后不难注意到 \(Flow(s,t,F)\) 关于 \(F\) 是凸的,于是可以三分出答案。
#include <queue>
#include <cstdio>
#include <cstring>
constexpr int maxn = 55;
constexpr int maxm = maxn * maxn;
constexpr int INF = 0x3f3f3f3f;
int n, m, tot = 1, first[maxn];
struct Edge { int to, nxt, cap, w; } e[maxm << 1];
inline void Add(int u, int v, int cap, int w) {
e[++tot] = { v, first[u], cap, w };
first[u] = tot;
}
inline void Adde(int u, int v, int cap, int w) {
Add(u, v, cap, w), Add(v, u, 0, -w);
}
int dis[maxn], pre[maxn];
inline bool SPFA() {
static bool inque[maxn];
static std::queue<int> q;
memset(dis, INF, sizeof dis);
q.push(1), dis[1] = 0, inque[1] = 1;
while(!q.empty()) {
int u = q.front(); q.pop(), inque[u] = 0;
for(int i = first[u]; i; i = e[i].nxt) {
int v = e[i].to;
if(e[i].cap && dis[v] > dis[u] + e[i].w) {
dis[v] = dis[u] + e[i].w, pre[v] = i;
if(!inque[v]) q.push(v), inque[v] = 1;
}
}
}
return dis[n] < INF;
}
int q, R, Flow[maxm];
int main() {
scanf("%d%d", &n, &m);
for(int i = 1, u, v, w; i <= m; ++i) {
scanf("%d%d%d", &u, &v, &w);
Adde(u, v, 1, w);
}
R = 0;
while(SPFA()) {
++R, Flow[R] = Flow[R - 1] + dis[n];
for(int u = n; u != 1; u = e[pre[u] ^ 1].to)
--e[pre[u]].cap, ++e[pre[u] ^ 1].cap;
}
scanf("%d", &q);
for(int X; q --> 0;) {
scanf("%d", &X);
static auto calc = [](int mid, int X) {
return 1. * (Flow[mid] + X) / mid;
};
int l = 1, r = R;
while(l < r) {
int mid = l + r >> 1;
if(calc(mid, X) > calc(mid + 1, X)) l = mid + 1;
else r = mid;
}
printf("%.8lf\n", calc(l, X));
}
}