原题链接
超级有意思的网络流题!
先转化一下题目条件,要求棋盘中不存在漏水的地方,则对于每个格子的四个方向的管子必须要全部连上,这可以对应到网络流中的满流。接着由于我们要寻找最少操作次数,所以我们可以联想到费用流。
具体的建模挺巧妙地。
首先我们把格子按照黑白染色(这可以通过方格横纵坐标相加的奇偶性判断),接着将每个格子内部拆成上下左右中五个点,然后让源点连向黑格子的中点,白格子的中点连向汇点,容量为 \(\infty\),单位费用为 \(0\)。
然后对于每个格子内部按照给出的水管形状,将中间点连向上下左右四个点,容量为 \(1\),单位费用为 \(0\)。由于相邻两个格子之间的流要可以互通,所以我们不妨将两个格子的相邻结点相连,比如左侧格子的右节点和右侧格子的左节点,容量为 \(1\),单位费用为 \(0\)。
这样我们就把格子内的管子转化成了网络流中的边,就可以使用费用流了。
但我们还需要考虑如何处理格子内的管子旋转。我们可以将所有的水管类型分为五种,分别是没有管子(\(0\))、直线型管子(\(5, 10\))、\(1\) 型管子(\(1、2、4、8\)),\(L\) 型管子(\(3、6、12、9\))、\(T\) 型管子(\(7、14、13、11\))、十字型管子(\(15\))。容易发现没有管子和直线型管子根据题目都无法操作,十字型管子操作了没意义,所以我们只需考虑剩下三种。
-
\(1\) 型
我们先考虑指向上方的水管类型(其余方向的均可以通过添加偏移量进行转化,具体详见代码),它可以转一次到左和右、转两次到下,所以我们不妨分别从左、右向上连容量为 \(1\),单位费用为 \(1\) 的边,从下向上连容量为 \(1\),单位费用为 \(2\) 的边。
-
\(L\) 型
同样地我们只考虑指向上方和右方的水管类型,它转一次可以转化成左、上和右、下。可以发现这两种情况都只有一条边发生了改变,所以我们只需从左向右,从下向上连费用为 \(1\) 的边即可。此时你会发现转两下的情况已经可以通过同时进行前两中操作表示了。
-
\(T\) 型
我们直接给出连边方案,对于指向上、右、左的连边方案,我们从下到左、右分别连一条费用为 \(1\) 的边,再从下到上连费用为 \(2\) 的边即可。
我们在建图过程中记录一下格子内连向中点的边数,然后跑最小费用最大流。跑完后如果不满流就说明无解,否则输出最小费用。
#include <bits/stdc++.h>
#define rep(i, a, b) for (int i = (a); i <= (b); i ++)
#define fro(i, a, b) for (int i = (a); i >= b; i --)
#define INF 0x3f3f3f3f
#define eps 1e-6
#define lowbit(x) (x & (-x))
#define initrand srand((unsigned)time(0))
#define random(x) ((LL)rand() * rand() % (x))
#define eb emplace_back
using namespace std;
typedef long long LL;
typedef pair<int, int> PII;
typedef pair<double, int> PDI;
inline int read() {int x = 0, f = 1;char ch = getchar();while (ch < '0' || ch > '9') { if (ch == '-') f = -1; ch = getchar(); }while (ch >= '0' && ch <= '9') { x = (x << 1) + (x << 3) + (ch ^ 48); ch = getchar(); }return x * f;
}const int N = 20010, M = 200010;
int n, m, k, s, t, tmp, turn, tp;
int h[N], e[M], ne[M], w[M], f[M], idx;
int d[N], vis[N], pre[M], incf[N];
int maxflow, minc;
queue<int> q;void add(int a, int b, int c, int d, int tp) {if (tp) swap(a, b); e[idx] = b, w[idx] = c, f[idx] = d, ne[idx] = h[a], h[a] = idx ++;e[idx] = a, w[idx] = 0, f[idx] = -d, ne[idx] = h[b], h[b] = idx ++;
}bool spfa() {memset(d, 0x3f, sizeof d); memset(vis, 0, sizeof vis);while (!q.empty()) q.pop();d[s] = 0, vis[s] = 1, incf[s] = 1 << 30; q.push(s);while (!q.empty()) {int u = q.front(); q.pop();vis[u] = 0;for (int i = h[u]; ~i; i = ne[i]) {int v = e[i];if (w[i] && d[v] > d[u] + f[i]) {d[v] = d[u] + f[i], pre[v] = i, incf[v] = min(incf[u], w[i]); if (!vis[v]) vis[v] = 1, q.push(v);}}}return d[t] != 0x3f3f3f3f;
}
void upd() {int u = t;while (u != s) {int p = pre[u];w[p] -= incf[t], w[p ^ 1] += incf[t];u = e[p ^ 1];}maxflow += incf[t]; minc += incf[t] * d[t];
}// 这就是前文所说的快捷方式
inline int self(int x) { return tmp * 4 + x; }
inline int U(int x) { return x + turn * tmp; }
inline int R(int x) { return x + (turn + 1 & 3) * tmp; }
inline int D(int x) { return x + (turn + 2 & 3) * tmp; }
inline int L(int x) { return x + (turn + 3 & 3) * tmp; }int main() {memset(h, -1, sizeof h);n = read(), m = read(); int upflow = 0;tmp = n * m, t = tmp * 5 + 1, k = 1;rep(i, 1, n) rep(j, 1, m) {int x = read(); tp = i + j & 1; turn = 0;if (tp) add(s, self(k), INF, 0, 0);else add(self(k), t, INF, 0, 0);if (i != 1) add(D(k - m), U(k), 1, 0, tp);if (j != 1) add(R(k - 1), L(k), 1, 0, tp);if (x & 1) add(U(k), self(k), 1, 0, tp), upflow ++;if (x & 2) add(R(k), self(k), 1, 0, tp), upflow ++;if (x & 4) add(D(k), self(k), 1, 0, tp), upflow ++;if (x & 8) add(L(k), self(k), 1, 0, tp), upflow ++;switch(x) {case 8: turn ++;case 4: turn ++;case 2: turn ++;case 1: add(R(k), U(k), 1, 1, tp);add(L(k), U(k), 1, 1, tp);add(D(k), U(k), 1, 2, tp);break;case 9: turn ++;case 12: turn ++;case 6: turn ++;case 3:add(D(k), U(k), 1, 1, tp);add(L(k), R(k), 1, 1, tp);break;case 13: turn ++;case 14: turn ++;case 7: turn ++;case 11: add(D(k), L(k), 1, 1, tp);add(D(k), R(k), 1, 1, tp);add(D(k), U(k), 1, 2, tp);break;}k ++;} while (spfa()) upd();printf("%d\n", maxflow == upflow / 2 ? minc : -1);return 0;
}
真神奇,也真难写。