@WangYixu
2018-06-21T05:20:47.000000Z
字数 5176
阅读 1163
OI 笔记 算法 网络流

EK算法基于一种贪心的思想,每次在图中寻找“增广路”(不是二分图里的那个)。
首先要对网络流重新构建,计算每条边容量与流量之差,简称残量,构成残量网络。

如图就是一个残量网络。
残量网络中的从源点到汇点的有向路径称为增广路,对于增广路,将其中的每个弧都增大最小残量的流量,那么,流量会增大并且不会违法。图中红边即为一条增广路。
这个操作称为增广。

找增广路时应该使用bfs。
无法增广时获得最大流。
代码:
struct Edge {int from, to, cap, flow;/*起点*终点*容量*流量*/Edge(int f = 0, int t = 0, int c = 0, int fl = 0) : from(f), to(t), cap(c), flow(fl) {}};int n, m;int head[N], next[M], cnt = 1; //邻接表Edge e[M]; //邻接表int p[N]; //入弧int a[N]; //入弧的可改进量int q[Q], hd, tl; //bfs队列//加边,正反两条边,正边是原弧,反边是反流量弧//cnt从1开始,有边p^1为反边inline void adde(int from, int to, int cap) {++cnt;e[cnt] = Edge(from, to, cap, 0);next[cnt] = head[from];head[from] = cnt;++cnt;e[cnt] = Edge(to, from, 0, 0);next[cnt] = head[to];head[to] = cnt;}inline int EK(int s, int t) {int flow = 0; //初始化流量int tmp;while (1) { //增广memset(a, 0, sizeof(a)); //初始化改进量hd = tl = 0; //初始化队列q[tl++ % Q] = s; //源点入队a[s] = INF; //设为INF,一是标记访问过,二是防止干扰while (hd != tl) { //bfstmp = q[hd++ % Q];for(int i = head[tmp]; i; i = next[i]) {if(!a[e[i].to] && e[i].cap > e[i].flow) { //没访问过(防止在环上不断绕圈)且还有残量p[e[i].to] = i; //记录入弧和计算可改进量a[e[i].to] = min(a[tmp], e[i].cap - e[i].flow);q[tl++ % Q] = e[i].to;}}if(a[t]) break; //找到增广路,退出bfs}if(!a[t]) break; //无增广路,结束for(int u = t; u != s; u = e[p[u]].from) { //修改残量网络e[p[u]].flow += a[t]; //入弧流量增加e[p[u]^1].flow -= a[t]; //反弧流量减少}flow += a[t]; //更新最大流}return flow;}
据说,反弧的作用是防止找到一条不够好的增广路而掐断了最好的增广路,类似于一剂“后悔药”。
但模拟的结果是几乎不会找到一条不好的增广路,所以我认为反弧是为了使最大流满足斜对称性()。
传统的EK算法复杂度为,实在是太慢了。所以我们考虑一个更好的算法——Dinic。
EK之所以慢,是因为这个算法每次只增广一条路,所以我们考虑一次性增广多条路。
我们考虑减少bfs的次数,转为使用dfs增广,bfs只是用来限制dfs的序。
每次bfs构造分层图,然后严格逐层增广,就可以达到的时间复杂度,实践中表现更好。
inline int Dinic() {int f = 0;while (bfs()) {f += dfs(s, INF);}return f;}inline bool bfs() {memset(d, 0, sizeof(d));qhd = qtl = 0;q[qtl] = s;qtl++;d[s] = 1;int tmp;while (qhd < qtl) {tmp = q[qhd % Q]; qhd++;for (register int i = head[tmp]; i; i = next[i]) {if (!d[to[i]] && cap[i] - flow[i] > 0) {d[to[i]] = d[tmp] + 1;q[qtl % Q] = to[i]; qtl++;}}if (d[t]) return true;}return false;}int dfs(int x, int maxf) {if (x == t) return maxf;int f = 0, tmpf;for (register int i = head[x]; i; i = next[i]) {if (d[to[i]] == d[x] + 1 && cap[i] - flow[i] > 0) {tmpf = dfs(to[i], min(maxf, cap[i] - flow[i]));if (tmpf > 0) {flow[i] += tmpf;flow[i^1] -= tmpf;f += tmpf;maxf -= tmpf;}}}return f;}
其实就是在找增广路的时候加上最短路即可
//数据结构基本不变struct Edge {int from, to, w, cap, flow; // 这里添加了费用wEdge(int a = 0, int b = 0, int c = 0, int d = 0, int e = 0) :from(a), to(b), w(c), cap(d), flow(e) {}} e[M];int head[N], next[M], cnt = 1;int n, m, s, t;int a[N], p[N];int q[N], hd, tl, dis[N], inq[N]; // SPFA相关int flow, waste;inline void adde(int from, int to, int w, int flow) {cnt++;next[cnt] = head[from];e[cnt] = Edge(from, to, w, flow, 0);head[from] = cnt;cnt++;next[cnt] = head[to];e[cnt] = Edge(to, from, -w, 0, 0); // 反边费用为相反数head[to] = cnt;}inline void EK(int s, int t) {flow = waste = 0; // 初始化流量和费用while (1) { // 找增广路//初始化memset(inq, 0, sizeof(inq));memset(dis, 0x3f, sizeof(dis));memset(a, 0, sizeof(a));memset(p, 0, sizeof(p));hd = tl = 0;q[tl++] = s;dis[s] = 0;inq[s] = 1;a[s] = INF;////SPFA 过程int tmp;while (hd != tl) {tmp = q[hd++ % N];inq[tmp] = 0;for (int i = head[tmp]; i; i = next[i]) {//注意这里的条件与之前不同,这里是使用SPFA,所以一个点可以经过多次//而由于最短路的性质,不会原地绕圈//由于增广路的性质,这样不会丢失解,还能保证费用最小if (e[i].cap > e[i].flow && dis[tmp] + e[i].w < dis[e[i].to]) {dis[e[i].to] = dis[tmp] + e[i].w;a[e[i].to] = min(a[tmp], e[i].cap - e[i].flow);p[e[i].to] = i;if (!inq[e[i].to]) {q[tl++ % N] = e[i].to;inq[e[i].to] = 1;}}}}//if (dis[t] == INF) break; // 无法增广 结束// 更新for (int i = t; i != s; i = e[p[i]].from) {e[p[i]].flow += a[t];e[p[i]^1].flow -= a[t];}flow += a[t];waste += a[t] * dis[t];//}}
添加超级源点与汇点,边容量为1,跑最大流。

将一个点拆成两个点,有向边转化成二分图,跑最大匹配。
解释:先将每个点看作一条路径,每个匹配将两条路径合为一条。由于匹配的性质,这些路径不会有交。

二分图最大匹配
每条匹配建一条容量为1的弧,超级源点向每个“英国飞行员”建一条容量为1的弧,每个“外籍飞行员”向超级汇点建一条容量为1的弧。
暴力搜索输出方案。
inline void print() {for (int i = head[0]; i; i = next[i]) {if (e[i].flow == 1) {printf("%d ", e[i].to);for (int j = head[e[i].to]; j; j = next[j])if (e[j].flow == 1) printf("%d\n", e[j].to);}}}int main() {scanf("%d %d", &n, &m);for (int i = 0, j = 0; i != -1 && j != -1;) {scanf("%d %d", &i, &j);adde(i, j, 1);}for (int i = 1; i <= n; ++i) adde(0, i, 1);for (int i = 1; i <= m; ++i) adde(n+i, n+m+1, 1);printf("%d\n", EK(0, n+m+1));print();return 0;}
经典套路,直接做。
int main() {scanf("%d%d", &n, &m);s = 1; t = (n<<1|1) + 1;for (int i = 1, x, y; i <= m; ++i) {scanf("%d%d", &x, &y);adde(x<<1, y<<1|1, 1);}for (int i = 1; i <= n; ++i) {adde(s, i<<1, 1);adde(i<<1|1, t, 1);}int ans = n - Dinic();// 以下为输出方案,仅对较弱数据有效,慎重参考int j = 2;for (int i = 1; i <= ans; ++i) {for (; j < t; j += 2) if (!vis[j]) {print(j);break;}printf("\n");}printf("%d\n", ans);return 0;}
考虑将能连成完全平方数的数建一条从较小的数连向较大的数的边,将问题转化为最小路径覆盖。动态加边。
int main() {int ans = 0;while(1) {m++;adde(s, m << 1, 1);adde(m << 1 | 1, t, 1);for (int i = sqrt(2*m + 1); i; --i) {int k = i * i - m;if (k > 0 && k < m) {adde(k<<1, m<<1|1, 1);}}ans = ans + 1 - Dinic();if (ans > n) {printf("%d\n", m - 1);for (int i = 1; i <= n; ++i) {for (int j = 1; j <= m; ++j) {if (!vis[j]) {print(j);printf("\n");break;}}}break;}}return 0;}
这个题也可以贪心,这启示我们不要被已有算法束缚。
不可做!!!!!
考虑转化问题,将其转化为选出一组点,使得任意两点间不能攻击。
考虑将棋盘黑白染色后,每个节点只能到达与其异色的节点,那么,可以将黑点连向S,白点连向T,两个能攻击的点之间连一条边,容量为INF(?)。
原问题转化为求二分图最小割(=最大匹配),再用所有点减去最小割即可。
处理走马有小技巧。
// 走马方式int XX[] = {-2, -2, -1, -1, +1, +1, +2, +2};int YY[] = {-1, +1, -2, +2, -2, +2, -1, +1};int main() {scanf("%d%d", &n, &m);for (int i = 1, x, y; i <= m; ++i) {scanf("%d%d", &x, &y);a[x][y] = 1;}s = 0; t = n * n + 1;for (int i = 1; i <= n; ++i) {for (int j = 1; j <= n; ++j) {int k = (i-1) * n + j;if (!a[i][j]) {if ((i + j) & 1) {adde(s, k, 1);// 走马小技巧for (int aha = 0, wx, wy; aha <= 7; ++aha) {wx = i+XX[aha]; wy = j+YY[aha];if (!a[wx][wy] && wx > 0 && wx <= n && wy > 0 && wy <= n)adde(k, (wx-1)*n+wy, 1);}}else adde(k, t, 1);}}}printf("%d\n", n * n - Dinic() - m); // 这里一定要减去m!!!return 0;}