Network 网络流
二分图
二分图的判断
- 二染色法:图可以被分为两个集合,每条边的两个端点都可以被划分到两个集合中;这两个集合可以被染成两个颜色
- 偶环法:根据二分图的定义,二分图中不可能存在奇数长度的环
//偶环法
#include<bits/stdc++.h>
using namespace std;
using ll = long long;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int t;
cin >> t;
while(t--) {
int n, m, ok = 1;
cin >> n >> m;
vector<vector<int> > g(n + 1);
vector<int> vis(n + 1, 0), dep(n + 1, 0);
for(int i = 1;i <= m;i++) {
int u, v;
cin >> u >> v;
g[u].emplace_back(v);
g[v].emplace_back(u);
}
function<void(int, int, int)> dfs = [&](int x, int fa, int depp) {
vis[x] = 1;
dep[x] = depp;
for(int to : g[x]) {
if(to == fa) continue;
if(vis[to] == 1) {
if((dep[x] - dep[to]) % 2 == 0) {
ok = 0;
}
continue;
}
dfs(to, x, depp + 1);
}
};
for(int i = 1;i <= n && ok;i++) if(vis[i] == 0) dfs(i, 0, 0);
if(ok) cout << "YES" << '\n';
else cout << "NO" << '\n';
}
return 0;
}
二分图最大匹配
- 匈牙利算法(O(V * E))
二分图带权最大匹配(KM算法)
- 当权值为1时求得的匹配为二分图的最大匹配(O(3))
const int maxn = 105;
const int inf = 0x3f3f3f3f;
int n, m, t, nx, ny;//Attention: nx, ny
vector<int> match(maxn, 0), lx(maxn, 0), ly(maxn, 0);
vector<int> slack(maxn, 0), vis_x(maxn, 0), vis_y(maxn, 0);
vector<vector<int> > g(maxn, vector<int>(maxn, 0));
bool dfs(int x) {
vis_x[x] = 1;
for (int y = 1; y <= ny; y++) {
if (vis_y[y]) continue;
int tmp = lx[x] + ly[y] - g[x][y];
if (tmp == 0) {
vis_y[y] = 1;
if (match[y] == -1 || dfs(match[y])) {
match[y] = x;
return true;
}
}
else if (slack[y] > tmp) {
slack[y] = tmp;
}
}
return false;
}
int KM() {
match = vector<int>(maxn, -1);
ly = vector<int>(maxn, 0);
int res = 0;
for (int i = 1; i <= nx; i++) {
lx[i] = -inf;
for (int j = 1; j <= ny; j++) {
if (g[i][j] > lx[i]) {
lx[i] = g[i][j];
}
}
}
for (int x = 1; x <= nx; x++) {
for (int i = 1; i <= ny; i++) slack[i] = inf;
while (true) {
vis_x = vector<int>(maxn, 0);
vis_y = vector<int>(maxn, 0);
if (dfs(x)) break;
int d = inf;
for (int i = 1; i <= ny; i++) if (!vis_y[i] && d > slack[i]) d = slack[i];
for (int i = 1; i <= nx; i++) if (vis_x[i]) lx[i] -= d;
for (int i = 1; i <= ny; i++) {
if (vis_y[i]) ly[i] += d;
else slack[i] -= d;
}
}
}
for (int i = 1; i <= ny; i++) {
if (match[i] != -1) {
res += g[match[i]][i];
}
}
return res;
}
Flow
Dinic求最大流
层次网络中,汇点的层次就代表着源点到汇点的最短距离,在每次构建层次网络时,汇点的层次必然会比上一次至少多1,因为在每次的dfs中,所有的最短路径都被找了出来,并且经过流量调整后当前所有的最短路径均被阻塞,因此在下一次构建层次网络时,没有办法再找到这么短的路径了,也就是说源点到汇点的最短距离至少增加1,也就是汇点的层次至少加1。而层次的上界显然为顶点的个数,因此最外层的while循环至多遍历O(V)次。内层的bfs需要O(E)的时间,dfs为找所有的增广路,由对EK算法的分析不难得出,找到所有的增广路不会超过O(VE)的时间,因此Dinic算法的复杂度上界为O(V^2E),这个性能要优于EK算法,而且Dinic算法的实现也很简便,我认为是解题的首选算法。
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const int inf = 0x7fffffff;
const int maxn = 1e5 + 7;
struct Edge {
int to, w, ne;
Edge() {}
Edge(int a, int b, int c) {
to = a, w = b, ne = c;
}
}edge[maxn << 2];
vector<int> head(maxn, -1), dep(maxn, -1);
int tot = -1, n, m, s, t;
void addd(int fr, int to, int w) {
edge[++tot] = Edge(to, w, head[fr]);
head[fr] = tot;
}
void add(int fr, int to, int w) {
addd(fr, to, w), addd(to, fr, 0);
}
int dfs(int u, int flow) {
if(u == t) return flow;
int ret = 0;
for(int i = head[u]; i != -1; i = edge[i].ne) {
int to = edge[i].to;
if(dep[to] == dep[u] + 1 && edge[i].w > 0) {
int fl = dfs(to, min(flow, edge[i].w));
flow -= fl, ret += fl;
edge[i].w -= fl, edge[i ^ 1].w += fl;
if (!flow) break;
}
}
if(!ret) dep[u] = -1;
return ret;
}
bool bfs() {
dep = vector<int>(maxn, -1);
queue<int> q;
q.push(s);
dep[s] = 0;
while(q.size()) {
int now = q.front();
q.pop();
for(int i = head[now]; i != -1; i = edge[i].ne) {
int to = edge[i].to;
if(dep[to] == -1 && edge[i].w > 0) {
dep[to] = dep[now] + 1;
q.push(to);
}
}
}
return dep[t] != -1;
}
ll dinic() {
ll max_flow = 0;
while(bfs()) {
max_flow += dfs(s, inf);
}
return max_flow;
}
int main() {
scanf("%d %d %d %d", &n, &m, &s, &t);//起点必须从1开始
for(int i = 0; i < m; i++) {
int u, v, w;
scanf("%d %d %d", &u, &v, &w);
add(u, v, w);
}
cout << dinic() << '\n';
return 0;
}
ISAP求最大流
Sap算法是对Dinic算法一个小的优化,在Dinic算法中,每次都要进行一次bfs来更新层次网络,这未免有些过于浪费,因为有些点的层次实际上是不需要更新的。Sap算法就采取一边找增广路,一边更新层次网络的策略。注意在Sap算法中源点的层次应该是最高的,一定要有Gap优化,不然这个算法的性能就不尽如人意了。Sap算法的复杂度上界和Dinic一样也是O(V^2E)
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const int inf = 1 << 30;
const int N = 1e6 + 40;
int idx = 1, n, m, s, t;
int dep[N << 1], gap[N << 1], head[N << 1];
struct edge {
int to;
int next;
int val;
} e[N << 1];
void add(int u, int v, int d) {
idx++;
e[idx].to = v;
e[idx].val = d;
e[idx].next = head[u];
head[u] = idx;
}
void bfs() {
memset(dep, -1, sizeof(dep));
memset(gap, 0, sizeof(gap));
dep[t] = 0, gap[0] = 1;
queue<int>q;
q.push(t);
while (q.size()) {
int u = q.front();
q.pop();
for (int i = head[u]; i; i = e[i].next) {
int v = e[i].to;
if (dep[v] != -1)
continue;
q.push(v);
dep[v] = dep[u] + 1;
gap[dep[v]]++;
}
}
return;
}
ll maxflow;
int dfs(int u, int flow) {
if (u == t) {
maxflow += flow;
return flow;
}
int used = 0;
for (int i = head[u]; i; i = e[i].next) {
int d = e[i].to;
if (e[i].val && dep[d] + 1 == dep[u]) {
int minn = dfs(d, min(e[i].val, flow - used));
if (minn) {
e[i].val -= minn;
e[i ^ 1].val += minn;
used += minn;
}
if (used == flow)
return used;
}
}
-- gap[dep[u]];
if (gap[dep[u]] == 0)
dep[s] = n + 1;
dep[u] ++;
gap[dep[u]] ++;
return used;
}
ll ISAP() {
maxflow = 0;
bfs();
while (dep[s] <= n)
dfs(s, inf);
return maxflow;
}
int main() {
scanf("%d%d%d%d", &n, &m, &s, &t);
for (int i = 1; i <= m; i++) {
int u, v, w;
scanf("%d %d %d", &u, &v, &w);
add(u, v, w);
add(v, u, 0);
}
printf("%lld\n", ISAP());
return 0;
}
HLPP求最大流
HLPP算法即最高标号预流推进算法,它的特点时并不采取找增广路的思想,而是不断地在可行流中找到那些仍旧有盈余的节点,将其盈余的流量推到周围可接纳流量的节点中,具体什么意思呢?对于一个最大流而言,除了源点和汇点以外所有的其他节点都应该满足流入的总流量等于流出的总流量,如果首先让源点的流量都尽可能都流到其相邻的节点中,这个时候相邻的节点就有了盈余,即它流入的流量比流出的流量多,所以要想办法将这些流量流出去。这种想法其实很自然,如果不知道最大流求解的任何一种算法,要手算最大流的时候,采取的策略肯定会是这样,将能流的先流出去,遇到容量不足的边就将流量减少,直到所有流量都流到了汇点。
但是这样做肯定会遇到一个问题,会不会有流量从一个节点流出去然后又流回到这个节点?如果这个节点是源点的话这么做是没问题的,因为有的时候通过某些节点是到达不了汇点的,这个时候要将流量流回到源点,但是其他情况就可能会造成循环流动,因此需要用到层次网络,只在相邻层次间流动。HLPP(Highest Label Preflow Push)最高标签预流推进算法是处理网络最大流里两种常用方法——增广路&预流推进中,预流推进算法的一种。其他科学家证明了其复杂度是紧却的O(n^2 · sqrt(m))。在随机数据中不逊色于普通的增广路算法,而在精心构造的数据中无法被卡,所以是一种可以替代Dinic的方法(随我怎么说,代码又长又难调,所以还是Dinic好啊)
但无论怎样,wikiwiki里面已经承认HLPP是现在最优秀的网络流算法了。
那么预流推进这个大门类里面,思想都差不多。大抵上就是我们对每个点记录超额流(Extra FlowExtra Flow) ,即允许流在非源点暂时存储,并伺机将超额流推送出去。不可推送的,就会流回源点。那么最终答案显然存储在Extra[T]里面。但同时这也有一个问题,就是会出现两个点相互推送不停的情况。为了防止这样,我们采用最高标号的策略,给每个点一个高度,对于一个点uu以及它的伴点集合{v},当且仅当hu=hv+1 时才可以推送流。并且我们对于源点S,设置hS=N,并对于S实行无限制推送。那么最后的答案就保存在Extra[T]里面 。但有时,我们发现有个点是”谷“,即周围点的高度都比它高,但是它有超额流。那么我们此时考虑拔高它的高度,即**重贴标签(relabel)**操作。
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const int inf = 0x3f3f3f3f;
const int maxn = 2e3 + 5;
const ll INF = 0x3f3f3f3f3f3f3f3fll;
struct HLPP {
struct Edge {
int v, rev;
ll cap;
};
int n, sp, tp, lim, ht, lcnt;
ll exf[maxn];
vector<Edge> G[maxn];
vector<int> hq[maxn], gap[maxn], h, sum;
void init(int nn, int s, int t) {
sp = s, tp = t, n = nn, lim = n + 1, ht = lcnt = 0;
for (int i = 1; i <= n; ++i)
G[i].clear(), exf[i] = 0;
}
void add_edge(int u, int v, ll cap) {
G[u].push_back({v, int(G[v].size()), cap});
G[v].push_back({u, int(G[u].size()) - 1, 0});
}
void update(int u, int nh) {
++lcnt;
if (h[u] != lim)
--sum[h[u]];
h[u] = nh;
if (nh == lim)
return;
++sum[ht = nh];
gap[nh].push_back(u);
if (exf[u] > 0)
hq[nh].push_back(u);
}
void relabel() {
queue<int> q;
for (int i = 0; i <= lim; ++i)
hq[i].clear(), gap[i].clear();
h.assign(lim, lim), sum.assign(lim, 0), q.push(tp);
lcnt = ht = h[tp] = 0;
while (!q.empty()) {
int u = q.front();
q.pop();
for (Edge &e : G[u])
if (h[e.v] == lim && G[e.v][e.rev].cap)
update(e.v, h[u] + 1), q.push(e.v);
ht = h[u];
}
}
void push(int u, Edge &e) {
if (!exf[e.v])
hq[h[e.v]].push_back(e.v);
ll df = min(exf[u], e.cap);
e.cap -= df, G[e.v][e.rev].cap += df;
exf[u] -= df, exf[e.v] += df;
}
void discharge(int u) {
int nh = lim;
if (h[u] == lim)
return;
for (Edge &e : G[u]) {
if (!e.cap)
continue;
if (h[u] == h[e.v] + 1) {
push(u, e);
if (exf[u] <= 0)
return;
} else if (nh > h[e.v] + 1)
nh = h[e.v] + 1;
}
if (sum[h[u]] > 1)
update(u, nh);
else {
for (; ht >= h[u]; gap[ht--].clear())
for (int &i : gap[ht])
update(i, lim);
}
}
ll hlpp() {
exf[sp] = INF, exf[tp] = -INF, relabel();
for (Edge &e : G[sp])
push(sp, e);
for (; ~ht; --ht) {
while (!hq[ht].empty()) {
int u = hq[ht].back();
hq[ht].pop_back();
discharge(u);
if (lcnt > (n << 2))
relabel();
}
}
return exf[tp] + INF;
}
} hp;
signed main() {
int n, m, s, t, u, v, w;
scanf("%d %d %d %d", &n, &m, &s, &t);
hp.init(n, s, t); //总点数,起点,终点
for(int i = 1;i <= m;i++) {
scanf("%d %d %d", &u, &v, &w);
hp.add_edge(u, v, w);
}
cout << hp.hlpp() << '\n';
return 0;
}
最小费用最大流(SPFA)
#include<bits/stdc++.h>
using namespace std;
using ll = long long;
const int maxm = 5e5 + 7;
const int maxn = 1e3 + 6;
const int inf = 0x7f7f7f7f;
vector<int > head(maxm, -1);
vector<int > pre(maxn, 0), cost(maxn, inf), flow(maxn, inf), vis(maxn, 0), last(maxn, 0);
queue<int> q;
struct Edge {
int ne, to, cost, v;
//cost:
//v: capacity
Edge() {}
Edge(int a, int b, int c, int d) : ne(a), to(b), cost(c), v(d) {}
}edge[maxm];
int tot = -1, n, m;//第一条边的编号必须为0
void add(int fr, int to, int v, int cost) {
edge[++tot] = Edge(head[fr], to, cost, v);
head[fr] = tot;
}
bool spfa(int st, int ed) {
cost = flow = vector<int>(n + 1, inf);
q.push(st);
vis[st] = 1, cost[st] = 0, pre[ed] = 0;
while(q.size()) {
int now = q.front();
q.pop();
vis[now] = 0;
for(int i = head[now];i != -1;i = edge[i].ne) {
int to = edge[i].to;
if(cost[to] > cost[now] + edge[i].cost && edge[i].v > 0) {
cost[to] = cost[now] + edge[i].cost;
flow[to] = min(flow[now], edge[i].v);
pre[to] = now;
last[to] = i;
if(vis[to] == 0) {
vis[to] = 1;
q.push(to);
}
}
}
}
return pre[ed];
}
void mcmf() {
int maxFlow = 0;
int minCost = 0;
while(spfa(1, n)) {
int tmp = n;
maxFlow += flow[n];
minCost += flow[n] * cost[n];
while(tmp != 1) {
edge[last[tmp]].v -= flow[n];
edge[last[tmp] ^ 1].v += flow[n];
tmp = pre[tmp];
}
}
cout << maxFlow << ' ' << minCost << '\n';
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> m;
for(int i = 1;i <= m;i++) {
int u, v, c, w;
cin >> u >> v >> c >> w;
add(u, v, c, w);
add(v, u, 0, -w);
}
mcmf();
return 0;
}