已经 2 周没写博客了- 这也是 2019 的第一篇博客,一个新的开始
引入
- 最小割的概念:给定一个图,含源点 和汇点 ,每条有向边有容量
- 选出一些边删掉,使得没有从 到 的路径
- 求选出的边的容量和的最小值
- 最大流和最小割都是线性规划问题中的一类
- 最小割可以看成是最大流的对偶问题
- 定理:最大流 最小割
- 证明
- 显然最小割不能小于最大流
- 我们知道,跑完最大流之后,残余网络中没有从 到 的路径
- 图被分为两个集合
- 根据流量平衡条件得到,流出 所在集合的流量等于流入 所在集合的流量
- 而横跨 所在集合和 所在集合的所有边构成一个割,且等于最大流
- 于是这个割就是最小割
- 证毕
- 最小割在题目中最常见的模型就是互斥选择模型(有这个模型更好的定义请在评论区指出)
- 例如: 个变量,每个变量取 有一定代价,取 有一定代价,外加 个约束 ,表示 和 取不同值有额外代价 ,求最小代价和
- 下面我在 BZOJ 上选出了 道最小割题
- 这 道题都是互斥选择的模型
(1)[BZOJ1943][Shoi2007]Vote 善意的投票
题意
- 即为引入中举的栗子
- 个变量,第 个变量为
- 当 取值等于 时不产生代价,否则产生 的代价
- 有 个二元组
- 表示 取值不同则会产生 的代价
- 求最小代价和
做法
- 源点 汇点
- 对于所有的 ,对第 个变量 建点
- 如果
- 则由 向 连容量为 的边
- 由 向 连容量为 的边
- 否则由 向 连容量为 的边
- 向 连容量为 的边
- 这时候边 和 显然有且仅有一条边被割掉
- 可以看成如果保留边 则 取
- 否则 取
- 考虑处理变量间的影响
- 对于二元组 ,连边 和 ,容量都为
- 这样的含义是,如果保留了边 和 边 ,那么还需要额外割掉边 ,否则存在一条 到 的路径
- 同理
- 实际上容量为 的边没必要建,在上面只是为了便于理解而建出来
- 求一遍最小割即为答案
- 此外,如果二元组 是限制 必须等于 ,则只需要把 和 的容量设为 (表示该边割不掉)即可
代码
#include <cmath>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
#define For(i, a, b) for (i = a; i <= b; i++)
#define Edge(u) for (int e = adj[u], v = go[e]; e; e = nxt[e], v = go[e])
#define NF(u) for (int &e = cur[u], v = go[e]; e; e = nxt[e], v = go[e])
inline int read()
{
int res = 0; bool bo = 0; char c;
while (((c = getchar()) < '0' || c > '9') && c != '-');
if (c == '-') bo = 1; else res = c - 48;
while ((c = getchar()) >= '0' && c <= '9')
res = (res << 3) + (res << 1) + (c - 48);
return bo ? ~res + 1 : res;
}
template <class T>
inline T Min(const T &a, const T &b) {return a < b ? a : b;}
const int N = 610, M = 6e5 + 5, INF = 0x3f3f3f3f;
int n, m, ecnt = 1, nxt[M], adj[N], go[M], cap[M], S, T, lev[N],
len, que[N], cur[N], ans;
void add_edge(int u, int v, int w)
{
nxt[++ecnt] = adj[u]; adj[u] = ecnt; go[ecnt] = v; cap[ecnt] = w;
nxt[++ecnt] = adj[v]; adj[v] = ecnt; go[ecnt] = u; cap[ecnt] = 0;
}
bool bfs()
{
int i;
For (i, S, T) cur[i] = adj[i], lev[i] = -1;
lev[que[len = 1] = S] = 0;
For (i, 1, len)
{
int u = que[i];
Edge(u) if (cap[e] && lev[v] == -1)
{
lev[que[++len] = v] = lev[u] + 1;
if (v == T) return 1;
}
}
return 0;
}
int dinic(int u, int flow)
{
if (u == T) return flow;
int res = 0, delta;
NF(u) if (cap[e] && lev[u] < lev[v])
{
delta = dinic(v, Min(cap[e], flow - res));
if (delta)
{
cap[e] -= delta; cap[e ^ 1] += delta;
res += delta; if (res == flow) break;
}
}
if (res != flow) lev[u] = -1;
return res;
}
int main()
{
int i, x, y;
n = read(); m = read();
S = 1; T = n + 2;
For (i, 1, n)
{
x = read();
if (x) add_edge(1 + i, T, 1);
else add_edge(S, 1 + i, 1);
}
while (m--) x = read(), y = read(),
add_edge(x + 1, y + 1, 1), add_edge(y + 1, x + 1, 1);
while (bfs()) ans += dinic(S, INF);
std::cout << ans << std::endl;
return 0;
}
(2)[BZOJ2127]happiness
题意
- 的变量矩阵
- 对于任意的
- 如果 则有 的收益
- 如果 则有 的收益
- 对于每个相邻(有且仅有一条公共边)的格子
- 如果 则有 的收益
- 如果 则有 的收益
- ,
做法
- 转化问题
- 如果 则有 的代价
- 如果 则有 的代价
- 对于每个相邻(有且仅有一条公共边)的格子
- 如果 或 则有 的代价
- 如果 或 则有 的代价
- 求
- 发现与上一题不同的地方就在于 c 和 d 不相等
- 所以我们考虑设
- 由 向点 连容量为 的边
- 点 向 连容量为 的边
- 对于每个 ( )
- 由点 向点 连容量为 的边
- 由点 向点 连容量为 的边
- 为了得出正确性,对两个相邻点 ( )进行一波讨论
- 如果两个变量都取 ,那么必然导致 和 被割掉
- 产生的代价为
- 都取 同理
- 如果 取 而 取 ,则必然导致边 被割掉
- 产生的代价为
- 联合上 和 的定义,就容易得出正确性
代码
#include <cmath>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
#define For(i, a, b) for (i = a; i <= b; i++)
#define Edge(u) for (int e = adj[u], v = go[e]; e; e = nxt[e], v = go[e])
#define NF(u) for (int &e = cur[u], v = go[e]; e; e = nxt[e], v = go[e])
inline int read()
{
int res = 0; bool bo = 0; char c;
while (((c = getchar()) < '0' || c > '9') && c != '-');
if (c == '-') bo = 1; else res = c - 48;
while ((c = getchar()) >= '0' && c <= '9')
res = (res << 3) + (res << 1) + (c - 48);
return bo ? ~res + 1 : res;
}
template <class T>
inline T Min(const T &a, const T &b) {return a < b ? a : b;}
const int E = 105, N = 1e4 + 10, M = 12e4 + 12, INF = 0x3f3f3f3f;
int n, m, a[E][E], b[E][E], ecnt = 1, S, T, nxt[M], adj[N], go[M], cap[M],
len, que[N], lev[N], cur[N], c[E][E], d[E][E], s[E][E], t[E][E];
int which(int x, int y) {return (x - 1) * m + y + 1;}
void add_edge(int u, int v, int w)
{
nxt[++ecnt] = adj[u]; adj[u] = ecnt; go[ecnt] = v; cap[ecnt] = w;
nxt[++ecnt] = adj[v]; adj[v] = ecnt; go[ecnt] = u; cap[ecnt] = 0;
}
bool bfs()
{
int i;
For (i, S, T) lev[i] = -1, cur[i] = adj[i];
lev[que[len = 1] = S] = 0;
For (i, 1, len)
{
int u = que[i];
Edge(u) if (cap[e] && lev[v] == -1)
{
lev[que[++len] = v] = lev[u] + 1;
if (v == T) return 1;
}
}
return 0;
}
int dinic(int u, int flow)
{
if (u == T) return flow;
int res = 0, delta;
NF(u) if (cap[e] && lev[u] < lev[v])
{
delta = dinic(v, Min(cap[e], flow - res));
if (delta)
{
cap[e] -= delta; cap[e ^ 1] += delta;
res += delta; if (res == flow) break;
}
}
if (res != flow) lev[u] = -1;
return res;
}
int maxflow()
{
int res = 0;
while (bfs()) res += dinic(S, INF);
return res;
}
int main()
{
int i, j, x, sum = 0;
n = read(); m = read();
S = 1; T = n * m + 2;
For (i, 1, n) For (j, 1, m) sum += (a[i][j] = read());
For (i, 1, n) For (j, 1, m) sum += (b[i][j] = read());
For (i, 1, n - 1) For (j, 1, m)
a[i][j] += (c[i][j] = x = read()), sum += x;
For (i, 1, n - 1) For (j, 1, m)
b[i][j] += (d[i][j] = x = read()), sum += x;
For (i, 1, n) For (j, 1, m - 1)
a[i][j] += (s[i][j] = x = read()), sum += x;
For (i, 1, n) For (j, 1, m - 1)
b[i][j] += (t[i][j] = x = read()), sum += x;
For (i, 1, n) For (j, 1, m)
add_edge(S, which(i, j), a[i][j]),
add_edge(which(i, j), T, b[i][j]);
For (i, 1, n - 1) For (j, 1, m)
add_edge(which(i, j), which(i + 1, j), c[i][j]),
add_edge(which(i + 1, j), which(i, j), d[i][j]);
For (i, 1, n) For (j, 1, m - 1)
add_edge(which(i, j), which(i, j + 1), s[i][j]),
add_edge(which(i, j + 1), which(i, j), t[i][j]);
std::cout << sum - maxflow() << std::endl;
return 0;
}
(3)[BZOJ1976][BeiJing2010组队]能量魔方 Cube
题意
- 给定一个 立方体
- 其中一些格子已经填充了 P 字符或 N 字符
- 要在剩下的格子内填充 P 字符或 N 字符
- 如果相邻(有一个公共面)的格子字符不相同则产生 的收益
- 求最大收益
做法
- 还是转化问题
- 理想情况是所有相邻的格子的字符都不相同
- 这时候产生的收益为
- 否则相邻两个同字符的格子有 的代价
- 答案就是 减去最小代价
- 和(1)差不多,但这里已经填充的格子不能再改变
- 所以只需要把 到一个格子的边容量由 改为 即可
- 一个格子到 的边同理
- 然后对于相邻两个格子 和 ,如果 和 同字符则产生代价
- 与前两题不同,前两题都是不同时产生代价
- 但这个立方体是个二分图
- 所以对立方体黑白染色之后,这个限制就可以解决了
- 具体地
- 如果 的 、 、 坐标之和为奇数且 初始已经填充了 N ,或者坐标之和为偶数且初始已经填充了 P ,则连边
- 如果 的 、 、 坐标值和为奇数且 初始已经填充了 P ,或者坐标之和为偶数且初始已经填充了 N ,则连边
- 否则连边
- 容量为 的边只是为了便于理解而连的,实际上可以不连
- 可以得出,保留边 表示如果 的三个坐标值之和为奇数则填充 N ,否则填充 P
- 边 反之
- 对于相邻两个格子 和 ,连边 , 由于 的三个坐标值之和与 的三个坐标值之和显然有不同的奇偶性,故这可以表示填充相同字符时产生代价
代码
#include <cmath>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
#define For(i, a, b) for (i = a; i <= b; i++)
#define Edge(u) for (int e = adj[u], v = go[e]; e; e = nxt[e], v = go[e])
#define NF(u) for (int &e = cur[u], v = go[e]; e; e = nxt[e], v = go[e])
template <class T>
inline T Min(const T &a, const T &b) {return a < b ? a : b;}
const int E = 45, N = 65535, M = 2097151, INF = 0x3f3f3f3f,
dx[] = {-1, 1, 0, 0, 0, 0}, dy[] = {0, 0, -1, 1, 0, 0},
dz[] = {0, 0, 0, 0, -1, 1};
int n, S, T, ecnt = 1, nxt[M], adj[N], go[M], cap[M], len, que[N],
cur[N], lev[N], ans;
char s[E][E][E];
void add_edge(int u, int v, int w)
{
nxt[++ecnt] = adj[u]; adj[u] = ecnt; go[ecnt] = v; cap[ecnt] = w;
nxt[++ecnt] = adj[v]; adj[v] = ecnt; go[ecnt] = u; cap[ecnt] = 0;
}
int which(int x, int y, int z)
{
return 1 + (x - 1) * n * n + (y - 1) * n + z;
}
bool bfs()
{
int i;
For (i, S, T) cur[i] = adj[i], lev[i] = -1;
lev[que[len = 1] = S] = 0;
For (i, 1, len)
{
int u = que[i];
Edge(u) if (cap[e] && lev[v] == -1)
{
lev[que[++len] = v] = lev[u] + 1;
if (v == T) return 1;
}
}
return 0;
}
int dinic(int u, int flow)
{
if (u == T) return flow;
int res = 0, delta;
NF(u) if (cap[e] && lev[u] < lev[v])
{
int delta = dinic(v, Min(cap[e], flow - res));
if (delta)
{
cap[e] -= delta; cap[e ^ 1] += delta;
res += delta; if (res == flow) break;
}
}
if (res != flow) lev[u] = -1;
return res;
}
int main()
{
int i, j, k, h;
std::cin >> n;
For (i, 1, n) For (j, 1, n)
scanf("%s", s[i][j] + 1);
S = 1; T = n * n * n + 2;
For (i, 1, n) For (j, 1, n) For (k, 1, n)
{
if (i + j + k & 1)
{
if (s[i][j][k] == 'N') add_edge(S, which(i, j, k), INF);
if (s[i][j][k] == 'P') add_edge(which(i, j, k), T, INF);
}
else
{
if (s[i][j][k] == 'P') add_edge(S, which(i, j, k), INF);
if (s[i][j][k] == 'N') add_edge(which(i, j, k), T, INF);
}
For (h, 0, 5)
{
int x = i + dx[h], y = j + dy[h], z = k + dz[h];
if (x < 1 || x > n || y < 1 || y > n || z < 1 || z > n) continue;
add_edge(which(i, j, k), which(x, y, z), 1);
}
}
ans = 3 * n * n * (n - 1);
while (bfs()) ans -= dinic(S, INF);
std::cout << ans << std::endl;
return 0;
}
(4)[BZOJ2132]圈地计划
题意
- 的变量矩阵
- 取 有 的收益
- 取 有 的收益
- 任意相邻(一条公共边)的 (注意, 和 会计算两次),如果 和 的取值不同则有 的额外收益
- 求最大收益
- ,
做法
- (2)(3)的思想结合
- 还是转化问题并黑白染色
- 如果 为奇数,则
- 否则
- 相邻的 ( 和 需要进行两次)连边
- 答案为
代码
#include <cmath>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
#define For(i, a, b) for (i = a; i <= b; i++)
#define Edge(u) for (int e = adj[u], v = go[e]; e; e = nxt[e], v = go[e])
#define NF(u) for (int &e = cur[u], v = go[e]; e; e = nxt[e], v = go[e])
inline int read()
{
int res = 0; bool bo = 0; char c;
while (((c = getchar()) < '0' || c > '9') && c != '-');
if (c == '-') bo = 1; else res = c - 48;
while ((c = getchar()) >= '0' && c <= '9')
res = (res << 3) + (res << 1) + (c - 48);
return bo ? ~res + 1 : res;
}
template <class T>
inline T Min(const T &a, const T &b) {return a < b ? a : b;}
const int N = 1e4 + 10, M = 2e5 + 5, dx[] = {-1, 1, 0, 0},
dy[] = {0, 0, -1, 1}, INF = 0x3f3f3f3f;
int n, m, ecnt = 1, S, T, nxt[M], adj[N], go[M], cap[M], len, que[N],
lev[N], cur[N];
int which(int x, int y) {return (x - 1) * m + y + 1;}
void add_edge(int u, int v, int w)
{
nxt[++ecnt] = adj[u]; adj[u] = ecnt; go[ecnt] = v; cap[ecnt] = w;
nxt[++ecnt] = adj[v]; adj[v] = ecnt; go[ecnt] = u; cap[ecnt] = 0;
}
bool bfs()
{
int i;
For (i, S, T) cur[i] = adj[i], lev[i] = -1;
lev[que[len = 1] = S] = 0;
For (i, 1, len)
{
int u = que[i];
Edge(u) if (cap[e] && lev[v] == -1)
{
lev[que[++len] = v] = lev[u] + 1;
if (v == T) return 1;
}
}
return 0;
}
int dinic(int u, int flow)
{
if (u == T) return flow;
int res = 0, delta;
NF(u) if (cap[e] && lev[u] < lev[v])
{
int delta = dinic(v, Min(cap[e], flow - res));
if (delta)
{
cap[e] -= delta; cap[e ^ 1] += delta;
if ((res += delta) == flow) break;
}
}
if (res != flow) lev[u] = -1;
return res;
}
int maxflow()
{
int res = 0;
while (bfs()) res += dinic(S, INF);
return res;
}
int main()
{
int i, j, k, x, sum = 0;
n = read(); m = read();
S = 1; T = n * m + 2;
For (i, 1, n) For (j, 1, m)
{
sum += (x = read());
if (i + j & 1) add_edge(S, which(i, j), x);
else add_edge(which(i, j), T, x);
}
For (i, 1, n) For (j, 1, m)
{
sum += (x = read());
if (i + j & 1) add_edge(which(i, j), T, x);
else add_edge(S, which(i, j), x);
}
For (i, 1, n) For (j, 1, m)
{
x = read();
For (k, 0, 3)
{
int tx = i + dx[k], ty = j + dy[k];
if (tx < 1 || tx > n || ty < 1 || ty > m) continue;
add_edge(which(i, j), which(tx, ty), x);
add_edge(which(tx, ty), which(i, j), x);
sum += x;
}
}
std::cout << sum - maxflow() << std::endl;
return 0;
}
(5)[BZOJ3218]a + b Problem
题意
- 给定 个格子,要求染成黑白两色
- 第 个格子染成黑色有 的收益
- 第 个格子染成白色有 的收益
- 如果第 个格子为黑色且存在一个 使得 且格子 为白色则有 的代价
- 求总收益减去总代价的最大值
- 数据范围见原题
做法
- 还是一样, 向每个 连边,容量为 ,每个 向 连边,容量为
- 边 保留表示 格子为黑,否则为白
- 关键就在于如何限制「存在一个 」
- 我们能够想到对 建虚拟点 ,由 向 连一条容量为 的边
- 然后由 向所有 的 连边,容量 (割不掉)
- 这时候只要有至少一个满足条件的 使得边 保留,且边 保留,我们就必须割边
- 这样答案就是
- 这样建图边数是 的,在 的规模下时空复杂度都过不去
- 但可以发现这 条边大部分都是 向 内 在 内的点连的边
- 使用主席树优化建图
- 维护一棵可持久化线段树,第 个历史版本为前 个格子的状态,下标为 的值
- 父亲向子节点连 的边
- 向历史版本 内 的值区间为 的所有点连边时,只需要连向 在线段树上拆出的 个区间
- 点数和边数都是
- 一个细节:建立主席树时,到达叶子节点后必须将该叶子节点连向上一个版本的对应叶子,否则如果有多个重复的
值时只能处理到最近一次出现的位置,在 UOJ 上
可以得到80 分的高分
代码
#include <cmath>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
#define For(i, a, b) for (i = a; i <= b; i++)
#define Edge(u) for (int e = adj[u], v = go[e]; e; e = nxt[e], v = go[e])
#define NF(u) for (int &e = cur[u], v = go[e]; e; e = nxt[e], v = go[e])
inline int read()
{
int res = 0; bool bo = 0; char c;
while (((c = getchar()) < '0' || c > '9') && c != '-');
if (c == '-') bo = 1; else res = c - 48;
while ((c = getchar()) >= '0' && c <= '9')
res = (res << 3) + (res << 1) + (c - 48);
return bo ? ~res + 1 : res;
}
template <class T>
inline T Min(const T &a, const T &b) {return a < b ? a : b;}
typedef long long ll;
const int N = 5005, M = 1e6 + 5, INF = 0x3f3f3f3f;
int n, a[N], b[N], w[N], l[N], r[N], p[N], rt[N], ecnt = 1, nxt[M],
adj[M], go[M], cap[M], len, que[M], cur[M], lev[M], ToT, to[N], S, T;
struct node
{
int lc, rc;
} Tr[M];
void add_edge(int u, int v, int w)
{
nxt[++ecnt] = adj[u]; adj[u] = ecnt; go[ecnt] = v; cap[ecnt] = w;
nxt[++ecnt] = adj[v]; adj[v] = ecnt; go[ecnt] = u; cap[ecnt] = 0;
}
void ins(int y, int &x, int l, int r, int i)
{
Tr[x = ++ToT] = Tr[y];
if (l == r) return add_edge(x, to[i] = ++ToT, INF),
add_edge(x, y, INF);
int mid = l + r >> 1;
if (a[i] <= mid) ins(Tr[y].lc, Tr[x].lc, l, mid, i);
else ins(Tr[y].rc, Tr[x].rc, mid + 1, r, i);
if (Tr[x].lc) add_edge(x, Tr[x].lc, INF);
if (Tr[x].rc) add_edge(x, Tr[x].rc, INF);
}
void linkedge(int x, int l, int r, int s, int e, int u)
{
if (!x) return;
if (l == s && r == e) return add_edge(u, x, INF);
int mid = l + r >> 1;
if (e <= mid) linkedge(Tr[x].lc, l, mid, s, e, u);
else if (s >= mid + 1) linkedge(Tr[x].rc, mid + 1, r, s, e, u);
else linkedge(Tr[x].lc, l, mid, s, mid, u),
linkedge(Tr[x].rc, mid + 1, r, mid + 1, e, u);
}
bool bfs()
{
int i;
For (i, 1, ToT) cur[i] = adj[i], lev[i] = -1;
lev[que[len = 1] = S] = 0;
For (i, 1, len)
{
int u = que[i];
Edge(u) if (cap[e] && lev[v] == -1)
{
lev[que[++len] = v] = lev[u] + 1;
if (v == T) return 1;
}
}
return 0;
}
ll dinic(int u, ll flow)
{
if (u == T) return flow;
ll res = 0, delta;
NF(u) if (cap[e] && lev[u] < lev[v])
{
delta = dinic(v, Min(1ll * cap[e], flow - res));
if (delta)
{
cap[e] -= delta; cap[e ^ 1] += delta;
res += delta; if (res == flow) break;
}
}
if (res != flow) lev[u] = -1;
return res;
}
int main()
{
int i;
ll ans = 0;
n = read();
For (i, 1, n) a[i] = read(), b[i] = read(), w[i] = read(),
l[i] = read(), r[i] = read(), p[i] = read(),
ans += b[i] + w[i];
S = 1; T = ToT = 2;
For (i, 1, n) ins(rt[i - 1], rt[i], 0, 1e9, i);
For (i, 1, n) add_edge(S, to[i], b[i]), add_edge(to[i], T, w[i]);
For (i, 1, n) add_edge(to[i], ++ToT, p[i]),
linkedge(rt[i - 1], 0, 1e9, l[i], r[i], ToT);
while (bfs()) ans -= dinic(S, 1e18);
std::cout << ans << std::endl;
return 0;
}
(6)[BZOJ3144][Hnoi2013]切糕
题意
- 给定一个 的立方体, 是高度(层数)
- 个变量,形如
- 规定相邻 的 需要满足
- 求
- 的最小值
做法
- 此题和前面五题的区别:变量的取值不仅仅有两个
- 我们考虑对变量 建立 个点,从 一直到
- 考虑如何使得 时仍然有 到 的路径( 实际上就是 )
- 发现我们要做的就是对于每个
- 连边
- 讨论一波即可得到正确性
- 答案即为最小割
代码-
#include <cmath>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
inline int read() {
int res = 0; bool bo = 0; char c;
while (((c = getchar()) < '0' || c > '9') && c != '-');
if (c == '-') bo = 1; else res = c - 48;
while ((c = getchar()) >= '0' && c <= '9')
res = (res << 3) + (res << 1) + (c - 48);
return bo ? ~res + 1 : res;
}
const int W = 45, N = 3e5 + 5, INF = 0x3f3f3f3f;
int P, Q, R, D, S, T, val[W][W][W], ecnt = 1, nxt[N], adj[N], go[N], cap[N],
lev[N], len, que[N], dx[] = {-1, 1, 0, 0}, dy[] = {0, 0, -1, 1};
void add_edge(int u, int v, int w) {
nxt[++ecnt] = adj[u]; adj[u] = ecnt; go[ecnt] = v; cap[ecnt] = w;
nxt[++ecnt] = adj[v]; adj[v] = ecnt; go[ecnt] = u; cap[ecnt] = 0;
}
bool bfs() {
int i; memset(lev, -1, sizeof(lev));
lev[que[len = 1] = S] = 0;
for (i = 1; i <= len; i++) {
int u = que[i];
for (int e = adj[u], v; e; e = nxt[e])
if (cap[e] > 0 && lev[v = go[e]] == -1) {
lev[que[++len] = v] = lev[u] + 1;
if (v == T) return 1;
}
}
return 0;
}
int dinic(int u, int flow) {
if (u == T) return flow;
int res = 0, delta;
for (int e = adj[u], v; e; e = nxt[e])
if (cap[e] > 0 && lev[u] < lev[v = go[e]]) {
delta = dinic(v, min(cap[e], flow - res));
if (delta) {
cap[e] -= delta; cap[e ^ 1] += delta;
res += delta; if (res == flow) break;
}
}
if (res != flow) lev[u] = -1;
return res;
}
int solve() {
int ans = 0;
while (bfs()) ans += dinic(S, INF);
return ans;
}
int main() {
int i, j, k, h; P = read(); Q = read();
R = read(); D = read(); S = 1; T = P * Q * (R + 1) + 2;
for (k = 1; k <= R; k++) for (i = 1; i <= P; i++)
for (j = 1; j <= Q; j++) val[i][j][k] = read();
for (i = 1; i <= P; i++) for (j = 1; j <= Q; j++) {
int x = (i - 1) * Q + j + 1;
add_edge(S, x, INF); for (k = 1; k <= R; k++)
add_edge(P * Q * (k - 1) + x, P * Q * k + x, val[i][j][k]);
add_edge(P * Q * R + x, T, INF);
}
for (i = 1; i <= P; i++) for (j = 1; j <= Q; j++)
for (h = 0; h < 4; h++) {
int x = i + dx[h], y = j + dy[h];
if (x < 1 || x > P || y < 1 || y > Q) continue;
for (k = D + 1; k <= R + 1; k++)
add_edge(P * Q * (k - 1) + (i - 1) * Q + j + 1,
P * Q * (k - D - 1) + (x - 1) * Q + y + 1, INF);
}
printf("%d\n", solve());
return 0;
}
(7)[BZOJ1497][NOI2006]最大获利
题意
- 给定一个边和点都带权的无向图,求一个子图
- 使得边权和减去点权的结果最大
- 点数
- 边数
- 权值
做法
- 从这题开始,我们引入最小割,也是互斥选择的一个经典模型:最大权闭合子图
- 定义:在一个有向图中选出一个点集 ,满足不存在边 使得 ,求选出的所有点的权值和的最大值,这些点的权值可正可负
- 先说建图:源点向所有正权点连容量为该点权值的边,所有负权点向汇点连容量为该点权值相反数的边,对于原图中每条边 ,连边
- 最大权闭合子图的点权和就是所有正权点的权值和减去最小割
- 证明:保留边 表示选点 ,保留边 表示不选点
- 由于最优性,我们只需要对满足 为正权, 为负权且 能到达 的点 进行讨论
- 显然这时候选 而不选 是不行的
- 这个限制条件也在建图上表现了出来:选 而不选 表示边 和 都保留,这时候 还是能到达
- 证毕
- 最大权闭合子图可以解决许多含有「选了 则必须选 」的最优化问题
- 此外,如果要输出点集,则最大权闭合子图为残余网络上从 出发能到达的所有点构成的点集(最小割分出的 所在的集合)
- 在此题中,可以建立模型:选了边 就必须选点 和
- 于是把边抽象成点,边 对应的点分别向点 和 连边
- 答案为最大权闭合子图
代码
#include <cmath>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
#define For(i, a, b) for (i = a; i <= b; i++)
#define Edge(u) for (int e = adj[u], v = go[e]; e; e = nxt[e], v = go[e])
#define NF(u) for (int &e = cur[u], v = go[e]; e; e = nxt[e], v = go[e])
inline int read()
{
int res = 0; bool bo = 0; char c;
while (((c = getchar()) < '0' || c > '9') && c != '-');
if (c == '-') bo = 1; else res = c - 48;
while ((c = getchar()) >= '0' && c <= '9')
res = (res << 3) + (res << 1) + (c - 48);
return bo ? ~res + 1 : res;
}
template <class T>
inline T Min(const T &a, const T &b) {return a < b ? a : b;}
typedef long long ll;
const int N = 1e5 + 5, M = 1e6 + 5;
const ll INF = 1e18;
int n, m, ecnt = 1, nxt[M], adj[N], go[M], S, T, len, que[N],
lev[N], cur[N];
ll cap[M];
void add_edge(int u, int v, ll w)
{
nxt[++ecnt] = adj[u]; adj[u] = ecnt; go[ecnt] = v; cap[ecnt] = w;
nxt[++ecnt] = adj[v]; adj[v] = ecnt; go[ecnt] = u; cap[ecnt] = 0;
}
bool bfs()
{
int i;
For (i, S, T) cur[i] = adj[i], lev[i] = -1;
lev[que[len = 1] = S] = 0;
For (i, 1, len)
{
int u = que[i];
Edge(u) if (cap[e] && lev[v] == -1)
{
lev[que[++len] = v] = lev[u] + 1;
if (v == T) return 1;
}
}
return 0;
}
ll dinic(int u, ll flow)
{
if (u == T) return flow;
ll res = 0, delta;
NF(u) if (cap[e] && lev[u] < lev[v])
{
delta = dinic(v, Min(cap[e], flow - res));
if (delta)
{
cap[e] -= delta; cap[e ^ 1] += delta;
res += delta; if (res == flow) break;
}
}
if (res != flow) lev[u] = -1;
return res;
}
int main()
{
int i, x, y, z;
ll ans = 0;
n = read(); m = read();
S = 1; T = n + m + 2;
For (i, 1, n) x = read(), add_edge(1 + m + i, T, x);
For (i, 1, m) x = read(), y = read(), z = read(),
add_edge(S, 1 + i, z), add_edge(1 + i, 1 + m + x, INF),
add_edge(1 + i, 1 + m + y, INF), ans += z;
while (bfs()) ans -= dinic(S, INF);
std::cout << ans << std::endl;
return 0;
}
(8)[BZOJ3996][TJOI2015]线性代数
题意
- 给出一个 的矩阵 和一个 的矩阵
- 求出一个 的 矩阵
- 使得 最大
- 其中 为 的转置
- 输出
- ,
做法
- 发现问题等价于选 有 的代价,同时选 和 有 的收益
- 求总收益减去总代价的最大值
- 很容易建立出「选了 则必须选 和 」的模型
- 转成最大权闭合子图模型,上最小割直接艹掉
代码
#include <cmath>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
#define For(i, a, b) for (i = a; i <= b; i++)
#define Edge(u) for (int e = adj[u], v = go[e]; e; e = nxt[e], v = go[e])
#define NF(u) for (int &e = cur[u], v = go[e]; e; e = nxt[e], v = go[e])
using namespace std;
inline int read() {
int res = 0; bool bo = 0; char c;
while (((c = getchar()) < '0' || c > '9') && c != '-');
if (c == '-') bo = 1; else res = c - 48;
while ((c = getchar()) >= '0' && c <= '9')
res = (res << 3) + (res << 1) + (c - 48);
return bo ? ~res + 1 : res;
}
const int N = 505, M = 1e6 + 5, INF = 0x3f3f3f3f;
int n, b[N][N], c[N], ecnt = 1, nxt[M], adj[M], go[M], cap[M],
lev[M], cur[M], len, que[M], S, T, sum;
void add_edge(int u, int v, int w) {
nxt[++ecnt] = adj[u]; adj[u] = ecnt;
go[ecnt] = v; cap[ecnt] = w;
nxt[++ecnt] = adj[v]; adj[v] = ecnt;
go[ecnt] = u; cap[ecnt] = 0;
}
bool bfs() {
int i;
For (i, S, T) lev[i] = -1, cur[i] = adj[i];
lev[que[len = 1] = S] = 0;
For (i, 1, len) {
int u = que[i];
Edge(u)
if (cap[e] && lev[v] == -1) {
lev[que[++len] = v] = lev[u] + 1;
if (v == T) return 1;
}
}
return 0;
}
int dinic(int u, int flow) {
if (u == T) return flow;
int res = 0, delta;
NF(u) if (cap[e] && lev[u] < lev[v]) {
delta = dinic(v, min(cap[e], flow - res));
if (delta) {
cap[e] -= delta; cap[e ^ 1] += delta;
res += delta; if (res == flow) break;
}
}
if (res != flow) lev[u] = -1;
return res;
}
int solve() {
int res = 0;
while (bfs()) res += dinic(S, INF);
return res;
}
int main() {
int i, j, tot = 1;
n = read();
For (i, 1, n) For (j, 1, n) sum += (b[i][j] = read());
For (i, 1, n) c[i] = read();
S = 1; T = (n * (n + 1) >> 1) + n + 2;
For (i, 1, n) For (j, i, n) {
tot++;
add_edge(S, tot, i == j ? b[i][j] : b[i][j] + b[j][i]);
add_edge(tot, 1 + (n * (n + 1) >> 1) + i, INF);
if (i != j) add_edge(tot, 1 + (n * (n + 1) >> 1) + j, INF);
}
For (i, 1, n) add_edge(1 + (n * (n + 1) >> 1) + i, T, c[i]);
cout << sum - solve() << endl;
return 0;
}
(9)[BZOJ4873][Shoi2017]寿司餐厅
题意
- 较长,见原题
做法
- 从「每个 只能选一次」开始讨论
- 考虑选出的 来自的位置集合需要满足的条件
- 抽象到二维平面上长这样(选出的
位置集合即为蓝色部分)
- 简单地说,对于 ,如果选了 就必须选 和
- 再考虑花费的钱数 ,对于 只需要对于每个 将 减去 即可
- 而对于 则需要对于每种代号新建一个点,点权为代号的二次方乘上 的相反数,对于每个 由 向代号 连边
- 仍然是最大权闭合子图,最小割解决
代码
#include <cmath>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
inline int read()
{
int res = 0; bool bo = 0; char c;
while (((c = getchar()) < '0' || c > '9') && c != '-');
if (c == '-') bo = 1; else res = c - 48;
while ((c = getchar()) >= '0' && c <= '9')
res = (res << 3) + (res << 1) + (c - 48);
return bo ? ~res + 1 : res;
}
template <class T>
inline T Min(const T &a, const T &b) {return a < b ? a : b;}
typedef long long ll;
const int E = 105, N = 1e4 + 5, M = 4e5 + 5, INF = 0x3f3f3f3f;
int n, m, a[E], d[E][E], S, T, tot = 1, id[E][E], ecnt = 1, nxt[M],
adj[N], go[M], cap[M], len, que[N], cur[N], lev[N];
ll ans;
void add_edge(int u, int v, int w)
{
nxt[++ecnt] = adj[u]; adj[u] = ecnt; go[ecnt] = v; cap[ecnt] = w;
nxt[++ecnt] = adj[v]; adj[v] = ecnt; go[ecnt] = u; cap[ecnt] = 0;
}
bool bfs()
{
for (int i = S; i <= T; i++)
cur[i] = adj[i], lev[i] = -1;
lev[que[len = 1] = S] = 0;
for (int i = 1; i <= len; i++)
{
int u = que[i];
for (int e = adj[u], v = go[e]; e; e = nxt[e], v = go[e])
if (cap[e] && lev[v] == -1)
{
lev[que[++len] = v] = lev[u] + 1;
if (v == T) return 1;
}
}
return 0;
}
ll dinic(int u, ll flow)
{
if (u == T) return flow;
ll res = 0, delta;
for (int &e = cur[u], v = go[e]; e; e = nxt[e], v = go[e])
if (cap[e] && lev[u] < lev[v])
{
delta = dinic(v, Min(1ll * cap[e], flow - res));
if (delta)
{
cap[e] -= delta; cap[e ^ 1] += delta;
res += delta; if (res == flow) break;
}
}
if (res < flow) lev[u] = -1;
return res;
}
int main()
{
n = read(); m = read();
for (int i = 1; i <= n; i++) a[i] = read();
S = 1; T = 1002 + (n * (n + 1) >> 1);
for (int i = 1; i <= n; i++)
for (int j = i; j <= n; j++)
d[i][j] = read(), id[i][j] = ++tot;
for (int i = 1; i <= n; i++) d[i][i] -= a[i];
for (int i = 1; i <= n; i++)
for (int j = i; j <= n; j++)
if (d[i][j] > 0) add_edge(S, id[i][j], d[i][j]), ans += d[i][j];
else if (d[i][j] < 0) add_edge(id[i][j], T, -d[i][j]);
for (int i = 1; i <= 1000; i++)
add_edge(1 + (n * (n + 1) >> 1) + i, T, m * i * i);
for (int i = 1; i <= n; i++)
for (int j = i + 1; j <= n; j++)
{
add_edge(id[i][j], id[i + 1][j], INF);
add_edge(id[i][j], id[i][j - 1], INF);
}
for (int i = 1; i <= n; i++)
add_edge(id[i][i], 1 + (n * (n + 1) >> 1) + a[i], INF);
while (bfs()) ans -= dinic(S, 1e18);
std::cout << ans << std::endl;
return 0;
}
(10)[BZOJ4501]旅行
题意
- 给定一个 点 边的 DAG
- 从 开始,每次等概率随机地选一条出边走过去,如果没有出边则结束旅行
- 删掉一些边,有 个约束形如 ,表示第 条边被删掉则第 条边也必须删掉,其中第 条边和第 条边有共同起点
- 求删边后从 出发走过的路径长度的期望的最大值
- , , ,图可能有重边
做法
- 这是一道综合性较强的非常不错的题,所以放在了最后讲
- 我们知道,对于 DAG ,如果 表示从 走到终态的期望步数,那么
- 按照拓扑序逆序 DP
- 其中 为点 的度数
- 而如果 表示合法删边后从 走到终态的期望步数最大值,
- 则我们要求的就是 的出边的一个子集,使得这个子集满足某些限制( 在子集内则 必须在子集内),使得选出的所有边的终点的 值的平均值最大,这个最大值加上 就是
- 我们现在想解决的问题转化成「最大平均权闭合子图」
- 看到最优化目标是分数,我们很容易想到分数规划
- 二分答案 后,将权值全部减去 之后求最大权闭合子图,判断其是否大于 ,如果大于 则平均值大于 ,反之亦然
- 注意终态的 为
- 最后答案
代码
#include <cmath>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
#define For(i, a, b) for (i = a; i <= b; i++)
#define Rof(i, a, b) for (i = a; i >= b; i--)
#define Edge(u) for (int e = adj[u], v = go[e]; e; e = nxt[e], v = go[e])
#define Edge2(u) for (int e = adj2[u]; e; e = nxt2[e])
#define Edge3(u) for (int e = adj3[u], v = go3[e]; e; e = nxt3[e], v = go3[e])
#define Edge4(u) for (int e = adj4[u], v = go4[e]; e; e = nxt4[e], v = go4[e])
#define NF(u) for (int &e = cur[u], v = go4[e]; e; e = nxt4[e], v = go4[e])
inline int read()
{
int res = 0; bool bo = 0; char c;
while (((c = getchar()) < '0' || c > '9') && c != '-');
if (c == '-') bo = 1; else res = c - 48;
while ((c = getchar()) >= '0' && c <= '9')
res = (res << 3) + (res << 1) + (c - 48);
return bo ? ~res + 1 : res;
}
template <class T>
inline T Min(const T &a, const T &b) {return a < b ? a : b;}
const int N = 60, M = 4005, L = 1e5 + 5;
const double INF = 1e20;
int n, m, k, ecnt, nxt[M], adj[N], st[M], go[M], ecnt2, nxt2[M],
adj2[N], w1[M], w2[M], He, Ta, Q[N], d[N], ecnt3, nxt3[M], adj3[N],
go3[M], S, T, tot, _u[M], ecnt4, nxt4[L], adj4[M], go4[L], len, que[M],
cur[M], lev[M], to[M];
double f[N], cap4[L];
void add_edge(int u, int v)
{
nxt[++ecnt] = adj[u]; adj[u] = ecnt; st[ecnt] = u; go[ecnt] = v;
d[u]++;
}
void add_edge2(int u, int v, int w)
{
nxt2[++ecnt2] = adj2[u]; adj2[u] = ecnt2; w1[ecnt2] = v; w2[ecnt2] = w;
}
void add_edge3(int u, int v)
{
nxt3[++ecnt3] = adj3[u]; adj3[u] = ecnt3; go3[ecnt3] = v;
}
void add_edge4(int u, int v, double w)
{
nxt4[++ecnt4] = adj4[u]; adj4[u] = ecnt4; go4[ecnt4] = v; cap4[ecnt4] = w;
nxt4[++ecnt4] = adj4[v]; adj4[v] = ecnt4; go4[ecnt4] = u; cap4[ecnt4] = 0;
}
bool bfs()
{
int i;
For (i, 1, tot + 2) cur[i] = adj4[i], lev[i] = -1;
lev[que[len = 1] = S] = 0;
For (i, 1, len)
{
int u = que[i];
Edge4(u) if (cap4[e] > 1e-8 && lev[v] == -1)
{
lev[que[++len] = v] = lev[u] + 1;
if (v == T) return 1;
}
}
return 0;
}
double dinic(int u, double flow)
{
if (u == T) return flow;
double res = 0, delta;
NF(u) if (cap4[e] > 1e-8 && lev[u] < lev[v])
{
delta = dinic(v, Min(cap4[e], flow - res));
if (delta > 1e-8)
{
cap4[e] -= delta; cap4[e ^ 1] += delta;
res += delta; if (fabs(res - flow) <= 1e-8) break;
}
}
if (fabs(res - flow) > 1e-8) lev[u] = -1;
return res;
}
double maxflow()
{
double res = 0;
while (bfs()) res += dinic(S, INF);
return res;
}
bool check(int uc, double mid)
{
int i;
S = tot + 1; T = tot + 2; ecnt4 = 1;
For (i, 1, tot + 2) adj4[i] = 0;
double res = 0;
For (i, 1, tot)
{
int u = go[_u[i]]; to[_u[i]] = i;
if (f[u] - mid > 0) add_edge4(S, i, f[u] - mid),
res += f[u] - mid;
else add_edge4(i, T, mid - f[u]);
}
Edge2(uc) add_edge4(to[w1[e]], to[w2[e]], INF);
return res - maxflow();
}
int main()
{
int i, x, y;
n = read(); m = read(); k = read();
while (m--) x = read(), y = read(), add_edge(x, y), add_edge3(y, x);
while (k--) x = read(), y = read(), add_edge2(st[x], x, y);
For (i, 1, n) if (!d[i]) Q[++Ta] = i;
while (He < Ta)
{
int u = Q[++He];
tot = 0;
Edge3(u) if (!(--d[v])) Q[++Ta] = v;
Edge(u) _u[++tot] = e;
if (!tot) continue;
double l = 0, r = n;
while (r - l > 1e-10)
{
double mid = (l + r) / 2;
if (check(u, mid)) l = mid;
else r = mid;
}
f[u] = r + 1;
}
printf("%.10lf\n", f[1]);
return 0;
}