07.17 网络流
P4249
双倍经验 CF1264E,后续把三元组全部看成无序。
一个三元环与三个点有关,如果转而统计不合法的三元组,一定恰存在一个 \(u\) 使得 \(u \to v\) 以及 \(u \to w\) 的边都存在。因此若 \(u\) 的出边条数为 \(deg_u\),其对答案的贡献为 \(deg_u(deg_u-1)/2\)。
当度数增加 \(1\) 时,对答案贡献 \(deg_u-1\)。因此对于 \(u\) 向汇点连边权为 \(deg_u-1 \sim n-2\) 的边,相当于差分建图。
一条未出现的边相当于给 \(u\) 或 \(v\) 的度数增加一。
因此 \(u\) 对汇点连 \(n-1-deg_u\) 条费用为 \(deg_u-1 \sum n-2\),流量为 \(1\) 的边,对于每条未出现的边,新建 \(x\),\(u \to x,u \to y\) 流量为 \(1\),费用为 \(0\),源点到 \(x\) 流量为 \(1\),费用为 \(0\)。
算是很巧妙的拆贡献,无论是把贡献放到 \(u\) 还是差分建图。
int main() {
int n; scanf("%d", &n);
std::vector<std::vector<int>> a(n, std::vector<int>(n, 2));
std::vector<int> deg(n);
int s = n, t = n + 2, ans = 0;
MF S(n + 3 + n * n, s, t);
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++)
scanf("%d", &a[i][j]), deg[i] += (a[i][j] == 1);
a[i][i] = 0, ans += deg[i] * (deg[i] - 1) / 2;
for (int j = deg[i]; j < n; j++)
S.link(i, t, 1, j);
}
int cnt = n + 2;
std::vector<std::vector<int>> id(n, std::vector<int>(n, -1));
for (int i = 0; i < n; i++) {
for (int j = i + 1; j < n; j++) if (a[i][j] == 2) {
S.link(s, id[i][j] = ++cnt, 1, 0);
S.link(id[i][j], i, 1, 0);
S.link(id[i][j], j, 1, 0);
}
}
ans += S.mcmf().second;
printf("%d\n", n * (n - 1) * (n - 2) / 6 - ans);
for (int i = 0; i < n; i++)
for (int j = i + 1; j < n; j++) if (id[i][j] != -1) {
int x = S.e[id[i][j]][1].v, y = i + j - x;
if (!S.e[id[i][j]][1].w)
a[x][y] = 1, a[y][x] = 0;
else
a[y][x] = 1, a[x][y] = 0;
}
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++)
printf("%d ", a[i][j]);
printf("\n");
}
}
P4174
双倍经验 CF1082G。
考虑在一张二分图上跑最小割。最小割模型形如:左边和右边中至少一边产生贡献。
考虑在左边放代价,右边放贡献。则若贡献被选中,则所有代价都不必付出;若所有代价都付出,则贡献可以不被选中。取反,让右边被割表示无贡献,不割表示有贡献,则若有贡献,则所有代价都必须被付出(右边没有被割断);若无贡献,则左边随意。
显然这道题可以这样做。
每个物体只有两种状态,要么选,要么不选;
给定所有物体被选择的状态,则问题能够得到唯一的答案。
感性理解,如果某个组在代价处就已经割断了,通过最小割,我们增加了代价,但保住了收益,如果割掉收益,说明这个组的收益不优,宁愿不要它,于是抵消了收益,但没有代价。
int main() {
int n, m; scanf("%d %d", &n, &m);
std::vector<int> a(n);
int s = n + m, t = n + m + 1;
MF S(n + m + 2, s, t);
LL ans = 0;
for (int i = 0, x; i < n; i++) {
scanf("%d", &x);
S.link(s, i, x);
}
for (int i = n, u, v, w; i < n+m; i++) {
scanf("%d %d %d", &u, &v, &w), --u, --v, ans += w;
S.link(i, t, w), S.link(u, i, inf), S.link(v, i, inf);
}
printf("%lld\n", ans - S.mf());
}
P1251
费用流,大小为 \(1\) 的流代表一张餐巾。
买餐巾:\(s \to i\) 连大小正无穷,单位费用为 \(p\) 的边。
新餐巾和旧餐巾不能混用,于是拆点,把旧餐巾和新餐巾拆成两个点。
一天用过的餐巾会变旧,因此拆成早晚的点。
旧餐巾可以沿用,因此晚上的旧餐巾连第二天晚上的旧餐巾。
早上多余的新餐巾事实上不优,因为每天购买餐巾的钱一样。所以可以看成早上只有新餐巾,晚上只有旧餐巾。因此每天晚上向 \(x\) 天后早上连边,容量无限,表示去洗。
每天需要有 \(r_i\) 张餐巾,因此每天早上向汇点连容量为 \(r_i\) 的边。同时,每天会产生 \(r_i\) 张旧餐巾,因此每天从源点连 \(r_i\) 的边到晚上。
拆早晚的想法非常厉害,尤其是把旧餐巾看作产生的一种东西,分开看新餐巾被送往汇点、旧餐巾从源点产生。
int main() {
int n; scanf("%d", &n);
std::vector<int> r(n);
for (int &x : r) scanf("%d", &x);
int p, m1, f1, m2, f2;
scanf("%d %d %d %d %d", &p, &m1, &f1, &m2, &f2);
int s = 2 * n, t = s + 1;
MCMF S(2 * n + 2, s, t);
for (int i = 0; i < n; i++) {
int x = 2 * i, y = x + 1;
S.link(s, y, r[i], 0), S.link(x, t, r[i], 0);
S.link(s, x, inf, p);
if (i + m1 < n) {
S.link(y, 2 * (i + m1), inf, f1);
}
if (i + m2 < n) {
S.link(y, 2 * (i + m2), inf, f2);
}
if (i + 1 < n)
S.link(y, y + 2, inf, 0);
}
printf("%lld\n", S.mcmf().second);
}
P2754
建分层图,地球为源点月球为汇点,一边枚举时间一边跑最大流,至满流为止。
int main() {
int n, m, k; scanf("%d %d %d", &n, &m, &k);
n += 2;
std::vector<int> r(m);
std::vector<std::vector<int>> x(m);
std::vector<int> fa(n);
std::function<int(int)> find = [&](int u) {
return fa[u] == u ? u : fa[u] = find(fa[u]);
};
std::iota(fa.begin(), fa.end(), 0);
for (int i = 0, t; i < m; i++) {
scanf("%d %d", &r[i], &t);
x[i].resize(t);
for (int &t : x[i]) {
scanf("%d", &t), ++t;
fa[find(t)] = find(x[i][0]);
}
}
if (find(0) != find(1))
return printf("0\n"), 0;
int s = 1, t = 0;
auto get = [&](int u, int v) -> int {
return v * n + u;
};
MF S(5 * k * n * n, s, t);
int sum = 0;
for (int f = 1; ; ++f) {
S.link(get(0, f), t, inf);
for (int i = 1; i < n; i++)
S.link(get(i, f-1), get(i, f), inf);
for (int i = 0; i < m; i++) {
int u = get(x[i][(f - 1) % x[i].size()], f - 1), v = get(x[i][f % x[i].size()], f);
S.link(u, v, r[i]);
}
sum += S.mf();
if (sum >= k) {
printf("%d\n", f);
return 0;
}
}
}
P2762
思路同 P4174。
如何构造最小割呢,从源点出发走还有流量的边 bfs,会把点划分成两个点集,所有位于两个点集之间的边即为割边。
在这道题中,最后一遍 bfs 时已经确定了最小割,因此可以用最后一遍 bfs 的结果直接确定可达集。
int main() {
int n, m; scanf("%d %d", &n, &m);
LL sum = 0;
int s = n + m, t = s + 1;
MF S(n + m + 2, s, t);
for (int i = 0, x, p; i < n; ++i) {
scanf("%d", &x), sum += x, S.link(s, i, x);
char ch;
while (1) {
scanf("%d%c", &p, &ch), --p;
S.link(i, p + n, 1e9);
if (ch == '\n')
break;
}
}
for (int i = n, x; i < n + m; i++) {
scanf("%d", &x);
S.link(i, t, x);
}
LL ans = sum - S.mf();
for (int i = 0; i < n; i++) if (S.dis[i])
printf("%d ", i + 1);
printf("\n");
for (int i = n; i < n + m; i++) if (S.dis[i])
printf("%d ", i - n + 1);
printf("\n%lld\n", ans);
}
P2763
因为变量重名调了半天。
int main() {
int n, m; scanf("%d %d", &n, &m);
std::vector<int> a(n);
int s = n + m, t = s + 1;
int sum = 0;
MF S(t + 1, s, t);
for (int i = 0; i < n; i++) {
scanf("%d", &a[i]), sum += a[i];
S.link(s, i, a[i]);
}
for (int i = n, r, x; i < n + m; i++) {
scanf("%d", &r);
while (r--) {
scanf("%d", &x), --x;
S.link(x, i, 1);
}
S.link(i, t, 1);
}
if (S.mf() < sum) return printf("No Solution!\n"), 0;
for (int i = 0; i < n; i++) {
printf("%d: ", i + 1);
int cnt = 0;
for (const auto &[v, w, id] : S.e[i]) if (!w && cnt < a[i])
printf("%d ", v + 1 - n), ++cnt;
printf("\n");
}
}
P2764
最大流只能最大。于是我们最大化有后继的点的数目。这就成了一个匹配问题。
int main() {
int n, m; scanf("%d %d", &n, &m);
int s = 2 * n, t = 2 * n + 1;
MF S(t + 1, s, t);
for (int i = 0; i < n; i++)
S.link(s, i << 1, 1), S.link(i << 1 | 1, t, 1);
for (int i = 0, u, v; i < m; i++) {
scanf("%d %d", &u, &v), --u, --v;
S.link(u << 1, v << 1 | 1, 1);
}
int ans = n - S.mf();
std::vector<int> nxt(n, -1), in(n);
for (int i = 0; i < n; i++) {
for (const auto &[v, w, id] : S.e[i << 1]) if (!w && v != s)
nxt[i] = v / 2, in[v / 2] = 1;
}
for (int i = 0; i < n; i++) if (!in[i]) {
int u = i;
while (u != -1) {
printf("%d ", u + 1), u = nxt[u];
}
printf("\n");
}
printf("%d\n", ans);
}
本文来自博客园,作者:purplevine,转载请注明原文链接:https://www.cnblogs.com/purplevine/p/18306732