模板
DINIC
int n, m, s, t;
int dep[N], head[N], cur[N], cnt = 1;
struct E{
int to, nxt, w;
}e[M << 1];
void add(int u, int v, int w)
{
e[++ cnt] = {v, head[u], w};
head[u] = cnt;
e[++ cnt] = {u, head[v], 0};
head[v] = cnt;
}
bool bfs()
{
memset(dep, 0, sizeof dep);
queue<int> q;
q.push(s);
dep[s] = 1, cur[s] = head[s];
while(!q.empty())
{
int u = q.front(); q.pop();
for(int i = head[u]; i; i = e[i].nxt)
{
int v = e[i].to, w = e[i].w;
if(!dep[v] && w > 0)
{
q.push(v);
cur[v] = head[v];
dep[v] = dep[u] + 1;
if(v == t) return true;
}
}
}
return false;
}
LL dfs(int u = s, LL flow = inf)
{
if(u == t) return flow;
LL left = flow;
for(int i = cur[u]; i && left /*attention*/; i = e[i].nxt)
{
cur[u] = i;
int v = e[i].to, w = e[i].w;
if(dep[v] == dep[u] + 1 && w > 0)
{
int c = dfs(v, min(left, w));
if(!c) dep[v] = 0;
left -= c, e[i].w -= c, e[i ^ 1].w += c;
}
}
return flow - left;
}
LL dinic()
{
LL maxflow = 0, flow = 0;
while(bfs())
while(flow = dfs()) maxflow += flow;
return maxflow;
}
MCMF
struct E{
int to, nxt;
LL w, c;
}e[M << 1];
int n, m, s, t, cnt = 1;
bool vis[N];
LL maxflow, mincost, dis[N], incf[N];
int pre[N], head[N];
void init()
{
cnt = 1;
maxflow = mincost = 0;
memset(head, 0, sizeof head);
memset(pre, 0, sizeof pre);
}
void add(int u, int v, LL w, LL c)
{
e[++ cnt] = {v, head[u], w, c};
head[u] = cnt;
e[++ cnt] = {u, head[v], 0, -c};
head[v] = cnt;
}
bool spfa()
{
for(int i = 0; i <= t; i ++) dis[i] = inf;
memset(vis, 0, sizeof vis);
queue<int> q;
q.push(s);
dis[s] = 0, vis[s] = true, incf[s] = inf;
while(!q.empty())
{
int u = q.front(); q.pop();
vis[u] = false;
for(int i = head[u]; i; i = e[i].nxt)
{
int v = e[i].to;
if(e[i].w && dis[v] > dis[u] + e[i].c)
{
dis[v] = dis[u] + e[i].c;
incf[v] = min(incf[u], e[i].w);
pre[v] = i;
if(!vis[v])
{
vis[v] = true;
q.push(v);
}
}
}
}
return dis[t] != inf;
}
void MCMF()
{
while(spfa())
{
for(int i = t; i != s; i = e[pre[i] ^ 1].to) //i表示的是节点 表示当前的增广路径
{
e[pre[i] ^ 1].w += incf[t];
e[pre[i]].w -= incf[t];
}
maxflow += incf[t];
mincost += incf[t] * dis[t];
}
}
最大流最小割
P2766 最长不下降子序列问题
f [ i ] f[i] f[i]表示以 i i i结尾最长的序列长度。设整个序列的最长长度为 l e n len len。因为有限制每个位置的使用,每个位置需要拆成两个点,中间连一条边表示该位置可使用的次数。s连向 f [ i ] = 1 f[i]=1 f[i]=1的点的入点, f [ i ] = l e n f[i]=len f[i]=len的点的出点连向汇点。某个点和源点/汇点连边的边权也由它限制的使用次数决定。对于问题3,只需要改变1和n的限制次数为inf即可。
最后需要注意一点:如果
l
e
n
len
len为1,那么1和n都会被使用无穷次,因为会出现这样的网络流s~
1
i
n
1_{in}
1in~
1
o
u
t
1_{out}
1out~t, s~
n
i
n
n_{in}
nin~
n
o
u
t
n_{out}
nout~t。且连边为
i
n
f
inf
inf。但是其实
l
e
n
len
len为1的情况,问题2和问题3的方案数都是
n
n
n,即每个位置的数字单独组成一个长度为
l
e
n
=
1
len=1
len=1的序列。
**note: ** 其实求方案数就是求最大流,主要看连边的条件。
//dinic()模板
int a[510];
int f[510];
int main()
{
int n;
scanf("%d", &n);
for(int i = 1; i <= n; i ++) scanf("%d", &a[i]);
int ans = 1;
s = 0, t = 2 * n + 1;
for(int i = 1; i <= n; i ++)
{
f[i] = 1;
for(int j = 1; j < i; j ++)
if(a[j] <= a[i])
f[i] = max(f[i], f[j] + 1);
ans = max(f[i], ans);
}
printf("%d\n", ans);
if(ans == 1)
{
printf("%d\n%d\n",n, n);
return 0;
}
for(int i = 1; i <= n; i ++)
{
if(f[i] == 1) add(s, i, 1);
if(f[i] == ans) add(i + n, t, 1);
add(i, i + n, 1);
for(int j = 1; j < i; j ++)
if(a[j] <= a[i] && f[j] == f[i] - 1)
add(j + n, i, 1);
}
printf("%d\n", dinic());
cnt = 1; memset(head, 0, sizeof head);
for(int i = 1; i <= n; i ++)
{
if(i == 1 || i == n)
{
add(i, i + n, inf);
if(f[i] == 1) add(s, i, inf);
if(f[i] == ans) add(i + n, t, inf);
}
else
{
add(i, i + n, 1);
if(f[i] == 1) add(s, i, 1);
if(f[i] == ans) add(i + n, t, 1);
}
for(int j = 1; j < i; j ++)
{
if(a[j] <= a[i] && f[j] == f[i] - 1)
add(j + n, i, 1);
}
}
printf("%d\n", dinic());
return 0;
}
P2172 [国家集训队]部落战争
最小路径覆盖–》入度为0的点为路径终点–》路径终点越少则路径越少–》入度为0的点越少–》匹配点最多–》总点数-最大匹配
#include<bits/stdc++.h>
#define LL int
using namespace std;
int n, m, r, c;
char ch[60][60];
int id[60][60];
const int N = 3e3 + 10;
vector<int> e[N];
bool vis[N];
int mat[N];
bool dfs(int u)
{
for(auto v : e[u])
{
if(vis[v]) continue;
vis[v] = true;
if(!mat[v] || dfs(mat[v]))
{
mat[v] = u;
return true;
}
}
return false;
}
int main()
{
scanf("%d%d%d%d", &n, &m, &r, &c);
int count = 0;
int dx[4] = {r, r, c, c};
int dy[4] = {-c, c, -r, r};
for(int i = 1; i <= n; i ++)
{
scanf("%s", ch[i] + 1);
for(int j = 1; j <= m; j ++)
if(ch[i][j] == '.') id[i][j] = ++ count;
}
for(int i = 1; i <= n; i ++)
for(int j = 1; j <= m; j ++)
if(id[i][j])
{
for(int k = 0; k < 4; k ++)
{
int nx = i + dx[k], ny = j + dy[k];
if(nx > n || ny > m || ny < 1 || !id[nx][ny]) continue;
e[id[i][j]].push_back(id[nx][ny]);
}
}
int ans = count;
for(int i = 1; i <= count; i ++)
{
for(int j = 1; j <= count; j ++) vis[j] = false;
if(dfs(i)) ans --;
}
printf("%d\n", ans);
return 0;
}
P5934 [清华集训2012]最小生成树
(二倍经验 P5039 [SHOI2010]最小生成树)
当前边
(
u
,
v
,
L
)
(u, v, L)
(u,v,L)要成为最小生成树中的边,删剩下的边中边权小于
L
L
L的必然会被选中为最小生成树的边,因此删剩下的边中边权小于
L
L
L的边必然不可让
(
u
,
v
)
(u, v)
(u,v)连通。因此可将
u
u
u作为源点,
v
v
v作为汇点,将边权小于
L
L
L的边连起来,求最小割。
再加上最大生成树的相应的最小割就是答案。
//dinic模板
struct EE{
int u, v, w;
bool operator<(const EE& x){return w < x.w; }
}ee[200005 * 2];
int main()
{
scanf("%d%d", &n, &m);
for(int i = 1; i <= m; i ++)
scanf("%d%d%d", &ee[i].u, &ee[i].v, &ee[i].w);
sort(ee + 1, ee + 1 + m);
int u, v, l; scanf("%d%d%d", &u, &v, &l);
s = u, t = v;
for(int i = 1; i <= m; i ++)
{
if(ee[i].w >= l) break;
add(ee[i].u, ee[i].v, 1), add(ee[i].v, ee[i].u, 1);
}
int tmp = dinic();
cnt = 1; memset(head, 0, sizeof head);
for(int i = m; i >= 1; i --)
{
if(ee[i].w <= l) break;
add(ee[i].u, ee[i].v, 1), add(ee[i].v, ee[i].u, 1);
}
printf("%d\n", tmp + dinic());
return 0;
}
P1646 [国家集训队]happiness(加点)
(二倍经验 P4313 文理分科)
每个人只能选文科或者理科,很容易想到文科和理科必须被割开,用最小割方法。源点表示文科,汇点表示理科,只要不选择那些割边,文科理科就不会连通。接下来是两个人
x
,
y
x, y
x,y共同选择文科会增加额外的喜悦值
e
x
t
r
a
extra
extra,可以想到如果想得到
e
x
t
r
a
extra
extra的喜悦值,
x
,
y
x, y
x,y都得选文科,换句话说,
x
,
y
x,y
x,y都不能选理科。
因此可以新建一个点
e
x
t
r
a
_
n
o
d
e
extra\_node
extra_node和源点连接一条边权为
e
x
t
r
a
extra
extra的边,并且
e
x
t
r
a
_
n
o
d
e
extra\_node
extra_node和
x
,
y
x,y
x,y分别连接一条
i
n
f
inf
inf的边,在求最小割的时候,只有
x
,
y
x,y
x,y到汇点的连边被割开
e
x
t
r
a
extra
extra才可能被选上。
//dinic模板
int id[110][110];
int main()
{
scanf("%d%d", &n, &m);
int ans = 0, num = 0;
t = n * m + 2 * n * (m - 1) + 2 * (n - 1) * m + 1;
for(int i = 1; i <= n; i ++) for(int j = 1; j <= m; j ++) id[i][j] = ++ num;
for(int i = 1; i <= n; i ++)
for(int j = 1; j <= m; j ++)
{
int x; scanf("%d", &x); ans += x;
add(s, id[i][j], x);
}
for(int i = 1; i <= n; i ++)
for(int j = 1; j <= m; j ++)
{
int x; scanf("%d", &x); ans += x;
add(id[i][j], t, x);
}
for(int i = 1; i < n; i ++)
for(int j = 1; j <= m; j ++)
{
int x; scanf("%d", &x); ans += x;
++ num;
add(s, num, x), add(num, id[i][j], inf), add(num, id[i + 1][j], inf);
}
for(int i = 1; i < n; i ++)
for(int j = 1; j <= m; j ++)
{
int x; scanf("%d", &x); ans += x;
++ num;
add(num, t, x), add(id[i][j], num, inf), add(id[i + 1][j], num, inf);
}
for(int i = 1; i <= n; i ++)
for(int j = 1; j < m; j ++)
{
int x; scanf("%d", &x); ans += x;
++ num;
add(s, num, x), add(num, id[i][j], inf), add(num, id[i][j + 1], inf);
}
for(int i = 1; i <= n; i ++)
for(int j = 1; j < m; j ++)
{
int x; scanf("%d", &x); ans += x;
++ num;
add(num, t, x), add(id[i][j], num, inf), add(id[i][j + 1], num, inf);
}
printf("%d\n", ans - dinic());
return 0;
}
P4304 [TJOI2013]攻击装置(最大独立集)
最小点集:选择最少的点,使得每条边都被覆盖。
最大独立集:选择最多的点,使得所有点都没有公共边。
最小点集=最大匹配,最大独立集 = 所有点-最小点集。
#include<bits/stdc++.h>
#define LL long long
using namespace std;
const int N = 210, M = 1e5;
int n, m, t;
int mat[M];
bool vis[M], g[N][N];
inline int id(int x, int y){return (x - 1) * m + y;}
int dx[8] = {1, 1, -1, -1, 2, 2, -2, -2};
int dy[8] = {2, -2, 2, -2, 1, -1, 1, -1};
vector<int> e[M];
bool dfs(int u)
{
for(auto i : e[u])
{
if(vis[i]) continue;
vis[i] = true;
if(!mat[i] || dfs(mat[i]))
{
mat[i] = u;
return true;
}
}
return false;
}
int main()
{
scanf("%d", &n); m = n;
int one = 0;
for(int i = 1; i <= n; i ++)
{
char ch[210];
scanf("%s", ch + 1);
for(int j = 1; j <= m; j ++)
{
ch[j] -= '0';
g[i][j] = ch[j];
one += g[i][j];
}
}
int ans = 0;
for(int i = 1; i <= n; i ++)
{
for(int j = 1; j <= m; j ++)
{
if(g[i][j] || (i + j) % 2) continue;
int l = id(i, j);
for(int k = 0; k < 8; k ++)
{
int x = i + dx[k], y = j + dy[k];
if(x < 1 || y < 1 || x > n || y > m || g[x][y]) continue;
e[l].push_back(id(x, y));
}
}
}
for (int i = 1; i <= n; i ++ )
for(int j = 1; j <= m; j ++ )
{
if (g[i][j] || (i + j) % 2) continue;
memset(vis, 0, sizeof vis);
if(dfs(id(i, j))) ans ++;
}
printf("%d", n * m - one - ans);
return 0;
}
费用流
P4016 负载平衡问题
源点向大于平均值的点连边权为 a b s ( a [ i ] − a v e r ) abs(a[i] - aver) abs(a[i]−aver)的边,小于平均值的点向汇点连边权为 a b s ( a [ i ] − a v e r ) abs(a[i]-aver) abs(a[i]−aver)的边。相邻的点再连一条边权为 i n f inf inf的边。这样网络流就是 ∑ a [ i ] − a v e r \sum{a[i]-aver} ∑a[i]−aver, i i i为权值大于 a v e r aver aver的点。大点流出,小点接受,最后相当于每个点都剩下0。而在网络流中每个流量表示搬运一个货物,而搬运一个货物会花费一个搬运量,因此每个流量都伴随着一个运输代价。因此一条边上的单位代价为1。
//mcmf模板
int a[N];
int main()
{
scanf("%d", &n);
int sum = 0;
for(int i = 0; i < n; i ++) {scanf("%d", &a[i]); sum += a[i];}
sum /= n, t = n, s = n + 1;
for(int i = 0; i < n; i ++)
{
if(a[i] > sum) add(s, i, a[i] - sum, 0);
if(a[i] < sum) add(i, t, sum - a[i], 0);
add(i, (i - 1 + n) % n, inf, 1);
add(i, (i + 1) % n, inf, 1);
}
MCMF();
printf("%d\n", mincost);
return 0;
}
P1251 餐巾计划问题
设 a i a_i ai表示每天需要的干净餐巾数量,在费用流中,一般是求解费用,而最大流是已知的, 在这题中,我们知道的全局常量只有:每天干净毛巾的数量之和 s u m = ∑ a i sum = \sum{a_i} sum=∑ai。因此最后从源点连向汇点的最大流为 s u m sum sum。
现在从题意中的操作进行连边:(以下边没有说明的,容量为无穷大,代价为0)
- 每天早上需要
a
i
a_i
ai个干净的新餐巾,
z
i
z_i
zi表示早上。干净的新毛巾可以有3个来源:可以重新购买(从源点向
z
i
z_{i}
zi连代价为
p
p
p的边 ),从前
n
n
n天或前
m
m
m天的旧毛巾洗干净。
但可以确定的是:最终必然要获得 a i a_i ai个,因此可从 z i z_i zi向汇点连 a i a_i ai的代价为 p p p的边。 - 每天晚上会产生 a i a_i ai个脏餐巾, w i w_i wi表示晚上。脏毛巾可以有很多个去处:攒着留着后面再洗(从 w i w_i wi向 w i + 1 w_{i + 1} wi+1连边 )送到快洗店( 从 w i w_i wi向 z i + m z_{i + m} zi+m连代价为 f f f的边) 或送到慢洗店去(从 w i w_i wi向 z i + n z_{i + n} zi+n连代价为 s s s的边)。但是可以确定的是:每天晚上都会产生 a i a_i ai个,因此可以从源点向 w i w_i wi连 a i a_i ai的边。
//mcmf模板
int r[2010];
int main()
{
scanf("%d", &n);
for(int i = 1; i <= n; i ++) scanf("%d", &r[i]);
int p, f, nn, ss;
t = 2 * n + 1;
scanf("%d%d%d%d%d", &p, &m, &f, &nn, &ss);
for(int i = 1; i <= n; i ++)
{
if(i + nn <= n) add(i + n, i + nn, inf, ss);
if(i + m <= n) add(i + n, i + m, inf, f);
add(s, i, inf, p), add(i, t, r[i], 0), add(s, i + n, r[i], 0);
if(i < n) add(i + n, i + 1 + n, inf, 0);
}
MCMF();
printf("%lld\n", mincost);
return 0;
}
P4013 数字梯形问题
建图,第
i
i
i个点数字为
a
i
a_i
ai,将其拆成入点和出点。
每个点入点向出点连边;
s向顶部m个入点连边;
底部n + m - 1个出点向汇点连边;
每个出点向其左下角和右下角的入点连边。
如果没有说明,每条边的代价为0,容量为
i
n
f
inf
inf。
问题1: 梯形的顶至底的 m 条路径互不相交。
每个点入点向出点连边,容量为1,限制每个点只能使用一次;
s向顶部m个入点连边;
底部n + m - 1个出点向汇点连边,价值为
a
i
a_i
ai。
每个出点向其左下角和右下角的入点连边,容量为1,价值为
a
i
a_i
ai。
问题2和问题3类似问题1,问题2只要改变每个点入点向出点连边为 i n f inf inf,问题3在问题2基础上使每个出点向其左下角和右下角的入点连边的容量为 i n f inf inf。
//mcmf模板
int a[50][50];
int id[50][50];
int main()
{
scanf("%d%d", &m, &n);
int num = 0;
for(int i = 1; i <= n; i ++)
for(int j = 1; j <= m + i - 1; j ++)
{
scanf("%d", &a[i][j]);
id[i][j] = ++ num;
}
s = 0, t = num * 2 + 1;
for(int i = 1; i <= n; i ++)
for(int j = 1; j <= m + i - 1; j ++)
{
if(i == 1) add(s, id[i][j], 1, 0);
if(i == n) add(id[i][j] + num, t, 1, -a[i][j]);
else add(id[i][j] + num, id[i + 1][j], 1, -a[i][j]), add(id[i][j] + num, id[i + 1][j + 1], 1, -a[i][j]);
add(id[i][j], id[i][j] + num, 1, 0);
}
MCMF();
printf("%lld\n", -mincost);
init();
for(int i = 1; i <= n; i ++)
for(int j = 1; j <= m + i - 1; j ++)
{
if(i == 1) add(s, id[i][j], 1, 0);
if(i == n) add(id[i][j] + num, t, inf, -a[i][j]);
else add(id[i][j] + num, id[i + 1][j], 1, -a[i][j]), add(id[i][j] + num, id[i + 1][j + 1], 1, -a[i][j]);
add(id[i][j], id[i][j] + num, inf, 0);
}
MCMF();
printf("%lld\n", -mincost);
init();
for(int i = 1; i <= n; i ++)
for(int j = 1; j <= m + i - 1; j ++)
{
if(i == 1) add(s, id[i][j], 1, 0);
if(i == n) add(id[i][j] + num, t, inf, -a[i][j]);
else add(id[i][j] + num, id[i + 1][j], inf, -a[i][j]), add(id[i][j] + num, id[i + 1][j + 1], inf, -a[i][j]);
add(id[i][j], id[i][j] + num, inf, 0);
}
MCMF();
printf("%lld\n", -mincost);
return 0;
}
P3159 [CQOI2012]交换棋子
对于某个位置表示的点,流量表示1经过这个点的次数,流向这个点将1送到这个位置来,流出这个点表示将1从这个位置送走。
因此对于起始为1,终止为0的点,流出去的1要比流进来的1多1个,因为会把这个点原来的1流走;同理对于起始为0,终止为1的点,流出去的1要比流进来的1少1个,因为这个点要留下一个1。
而对于原来颜色相同的位置,流进来的1和流出去的1数量相同。
容量限制:
现在来考虑每个点交换次数的限制。比如现在将1从a点转移到b点,那么a点和b点的交换次数是1,而从a到b的路径上经过的点交换次数是2。因此对于一个点,作为路径起点,终点和路径中间点是不同的。因此一个点可以拆成3个点
l
,
m
i
d
,
r
l,mid,r
l,mid,r,
l
,
m
i
d
,
r
l,mid,r
l,mid,r中间连边,交换次数可以用经过此节点内部边数表示:对于路径起点,从
m
i
d
mid
mid流向
r
r
r,只经过一条边,表示交换次数为1;对于路径终点,从
l
l
l连向
m
i
d
mid
mid,只经过一条边,表示交换次数为1。对于路径中间点,从
l
l
l连向
m
i
d
mid
mid,再从
m
i
d
mid
mid连向
r
r
r,经过两条边,表示交换次数为2。下图橙,蓝,绿分别表示交换路径起点,中间点和绿色点,可以更好地理解拆点的作用。
实现:
对于起始图位置是1的点
i
i
i,连
(
s
,
m
i
d
,
1
,
0
)
的边
(s,mid,1,0)的边
(s,mid,1,0)的边,相当于将s作为交换路径的起点,
i
i
i作为交换路径的中间点;对于终止图位置为1的点,连
(
m
i
d
,
t
,
1
,
0
)
(mid, t, 1, 0)
(mid,t,1,0)的边,相当于将t作为交换路径的终点,
i
i
i作为交换路径的中间点。
对于起始图为1,终止图为0的点,连
(
l
,
m
i
d
,
c
/
2
,
0
)
,
(
m
i
d
,
r
,
(
c
+
1
)
/
2
,
0
)
(l,mid,c/2,0),(mid, r, (c + 1)/2, 0)
(l,mid,c/2,0),(mid,r,(c+1)/2,0)的边。
对于起始图为0,终止图为1的点,连
(
l
,
m
i
d
,
(
c
+
1
)
/
2
,
0
)
,
(
m
i
d
,
r
,
c
/
2
,
0
)
(l, mid,(c + 1) / 2,0),(mid,r,c/2,0)
(l,mid,(c+1)/2,0),(mid,r,c/2,0)的边。
对于起始图和终止图相同的点,连
(
l
,
m
i
d
,
c
/
2
,
0
)
,
(
m
i
d
,
r
,
c
/
2
,
0
)
(l,mid,c/2,0),(mid,r,c/2,0)
(l,mid,c/2,0),(mid,r,c/2,0)的边。
对于相邻的点,连
(
r
i
,
l
j
,
i
n
f
,
0
)
(r_i,l_j,inf,0)
(ri,lj,inf,0)的边。
//mcmf模板
char st[50][50], ed[50][50], li[50][50];
int id[50][50];
int dx[8] = {-1, 1, 0, 0, 1, -1, 1, -1}, dy[8] = {0, 0, -1, 1, 1, -1, -1, 1};
int main()
{
scanf("%d%d", &n, &m);
int num = 0;
for(int i = 1; i <= n; i ++) for(int j = 1; j <= m; j ++) id[i][j] = ++ num;
s = 0, t = num * 3 + 1;
int one1 = 0, one2 = 0;
for(int i = 1; i <= n; i ++) scanf("%s", st[i] + 1);
for(int i = 1; i <= n; i ++) scanf("%s", ed[i] + 1);
for(int i = 1; i <= n; i ++) scanf("%s", li[i] + 1);
for(int i = 1; i <= n; i ++) for(int j = 1; j <= m; j ++) li[i][j] -= '0';
for (int i = 1; i <= n; i ++ )
for (int j = 1; j <= m; j ++ )
{
if (st[i][j] == '1')
one1 ++, add(s, id[i][j] + num, 1, 0);
if (ed[i][j] == '1')
one2 ++, add(id[i][j] + num, t, 1, 0);
}
if(one1 != one2)
{
puts("-1"); return 0;
}
for(int i = 1; i <= n; i ++)
{
for(int j = 1; j <= m; j ++)
{
if(st[i][j] == ed[i][j])
{
add(id[i][j], id[i][j] + num, li[i][j] / 2, 0);
add(id[i][j] + num, id[i][j] + 2 * num, li[i][j] / 2, 0);
}
if(st[i][j] == '1' && ed[i][j] == '0')
{
add(id[i][j], id[i][j] + num, li[i][j] / 2, 0);
add(id[i][j] + num, id[i][j] + 2 * num, (li[i][j] + 1) / 2, 0);
}
else if(st[i][j] == '0' && ed[i][j] == '1')
{
add(id[i][j], id[i][j] + num, (li[i][j] + 1) / 2, 0);
add(id[i][j] + num, id[i][j] + 2 * num, li[i][j] / 2, 0);
}
for(int k = 0; k < 8; k ++)
{
int nx = i + dx[k], ny = j + dy[k];
if(nx < 1 || ny < 1 || nx > n || ny > m) continue;
add(id[i][j] + 2 * num, id[nx][ny], inf, 1);
}
}
}
MCMF();
// cout << maxflow << endl;
if(maxflow != one1) puts("-1");
else printf("%d\n", mincost);
return 0;
}
P4014 分配问题
最大权完美匹配直接用费用流切,最最模板的连边方式。
//mcmf模板
int a[110][110];
int main()
{
scanf("%d", &n);
s = 0, t = 2 * n + 1;
for(int i = 1; i <= n; i ++) for(int j = 1; j <= n; j ++) scanf("%d", &a[i][j]);
for(int i = 1; i <= n; i ++)
{
add(s, i, 1, 0), add(i + n, t, 1, 0);
for(int j = 1; j <= n; j ++)
add(i, j + n, 1, a[i][j]);
}
MCMF();
printf("%lld\n", mincost);
init();
for(int i = 1; i <= n; i ++)
{
add(s, i, 1, 0), add(i + n, t, 1, 0);
for(int j = 1; j <= n; j ++)
add(i, j + n, 1, -a[i][j]);
}
MCMF();
printf("%lld\n", -mincost);
return 0;
}
P2153 [SDOI2009] 晨跑(模板题)
int main()
{
scanf("%d%d", &n, &m);
s = 1, t = 2 * n;
add(1, 1 + n, inf, 0), add(n, 2 * n, inf, 0);
for(int i = 2; i < n; i ++) add(i, i + n, 1, 0);
for(int i = 1; i <= m; i ++)
{
int a, b, c; scanf("%d%d%d", &a, &b, &c);
add(a + n, b, 1, c);
}
MCMF();
printf("%lld %lld\n", maxflow, mincost);
return 0;
}
P2053 [SCOI2007] 修车(多重匹配)
m个维修员,每个人拆成n个点,每个点的等待时间都不同,前n * m个点表示维修员, 后2n个点表示顾客拆点,连边为1,表示每个顾客只需要修一辆车。
假设某个维修员修了num辆车,每辆车的时间为 c 1 , c 2 , . . . c n u m c_1,c_2,...c_{num} c1,c2,...cnum,那么第一个车主人只需要等待自己的车被维修的时间,第二个车主人需要等待第一辆车和自己的车维修的时间…因此每辆车导致别人等待的时间可能被计算多次,容易发现倒数第 i i i辆车导致别人等待的时间为 i ∗ c i i * c_i i∗ci。因此可以根据这个来对维修工进行拆点计算。对于第 i i i个维修工,将其拆成 n n n个点,第 i i i个维修工拆出的第 j j j个点,表示他正在修他所修的所有车中的倒数第 j j j辆。若将第 i i i个维修工的第 j j j个点和第 k k k辆车连边,就表示第 k k k辆车是第 i i i个维修工维修的倒数第 j j j辆车,此连边都总时间的贡献为 j ∗ c k j*c_k j∗ck,表示第 k k k辆车会让后面 j j j个人都等 c k c_k ck长的时间。
note: m个左部点和n个右部点匹配,并且一个左部点可以匹配多个右部点,并且已匹配右部点的左部点再次匹配的花费会改变,因此可以将一个左部点拆成n个右部点。拆点之后需要根据题意给每个左部点不同的连边方式。一个点的权值会被重复计算,就直接考虑这个点贡献权值的次数。
int times[10][100];
int main()
{
scanf("%d%d", &m, &n);
t = m * n + 2 * n + 1;
for(int i = 1; i <= n; i ++) for(int j = 1; j <= m; j ++) scanf("%d", ×[j][i]);
int num = 0;
for(int i = 1; i <= m; i ++)
for(int j = 1; j <= n; j ++)
{
num ++;
add(s, num, 1, 0);
for(int k = 1; k <= n; k ++)
add(num, n * m + k, 1, j * times[i][k]);
}
for(int i = 1; i <= n; i ++) add(num + i, num + i + n, 1, 0), add(num + i + n, t, 1, 0);
MCMF();
// cout << maxflow << ' ' << mincost << endl;
printf("%.2lf\n", mincost * 1.0 / n);
return 0;
}
P2050 [NOI2012] 美食节(多重匹配+动态开点)
这道题是P2053 [SCOI2007] 修车的升级版,但是每个菜有多道,时间和空间都超限了,但是有很多厨师的拆点都是无效的:厨师的拆点一共有100 * 800 = 80000个,而菜品只有800道,有很多个厨师的拆点并不会被用到,而且如果厨师的第 i i i个拆点在某次增广路中没有被使用到,那么他的第 i + 1 , i + 2 , . . . , n i+1,i+2,...,n i+1,i+2,...,n个点都不会被使用,因为花费的时间更长。
因此可以在找到一条增广路时,记录此时匹配的是哪个厨师的第 i i i个拆点,然后加入此厨师的第 i + 1 i+1 i+1个拆点,并将其他菜品与此新拆点连边。
note: 多重匹配中拆点如果有序,可以动态开点,先使用更优的点。
#include <bits/stdc++.h>
#define LL int
using namespace std;
const LL inf = 0x3f3f3f3f;
const int N = 1004, M = N * N;
struct E{
int to, nxt;
LL w, c;
}e[M << 1];
int n, m, s, t, cnt = 1;
bool vis[N];
LL maxflow, mincost, dis[N], incf[N];
int pre[N], head[N];
void init()
{
cnt = 1;
maxflow = mincost = 0;
memset(head, 0, sizeof head);
memset(pre, 0, sizeof pre);
}
void add(int u, int v, LL w, LL c)
{
e[++ cnt] = {v, head[u], w, c};
head[u] = cnt;
e[++ cnt] = {u, head[v], 0, -c};
head[v] = cnt;
}
bool spfa()
{
for(int i = 0; i <= t; i ++) dis[i] = inf;
memset(vis, 0, sizeof vis);
queue<int> q;
q.push(s);
dis[s] = 0, vis[s] = true, incf[s] = inf;
while(!q.empty())
{
int u = q.front(); q.pop();
vis[u] = false;
for(int i = head[u]; i; i = e[i].nxt)
{
int v = e[i].to;
if(e[i].w && dis[v] > dis[u] + e[i].c)
{
dis[v] = dis[u] + e[i].c;
incf[v] = min(incf[u], e[i].w);
pre[v] = i;
if(!vis[v])
{
vis[v] = true;
q.push(v);
}
}
}
}
return dis[t] != inf;
}
int bl[1010];
int ck_cnt[110];
int times[50][110]; //第i种菜 第j个厨师
int venum[50];
int sum, cur;
void MCMF()
{
while(spfa() && maxflow != sum) //每条增广路最少做出一个菜,最多找800条增广路,最多会多出800个厨师节点
{
for(int i = t; i != s; i = e[pre[i] ^ 1].to) //i表示的是节点 表示当前的增广路径
{
e[pre[i] ^ 1].w += incf[t];
e[pre[i]].w -= incf[t];
}
maxflow += incf[t];
mincost += incf[t] * dis[t];
//找到此增广路的终点及其对应的厨师编号ck 更新到了厨师的第ck_cnt[ck]层
int node = e[pre[t] ^ 1].to, ck = bl[node];
bl[++ cur] = ck;
ck_cnt[ck] ++;
add(cur, t, 1, 0);
for(int i = 1; i <= n; i ++) add(i, cur, 1, ck_cnt[ck] * times[i][ck]);
}
}
int main()
{
scanf("%d%d", &n, &m);
for(int i = 1; i <= n; i ++) {scanf("%d", &venum[i]); sum += venum[i];}
for(int i = 1; i <= n; i ++) for(int j = 1; j <= m; j ++) scanf("%d", ×[i][j]);
for(int i = 1; i <= n; i ++)
{
add(s, i, venum[i], 0);
for(int j = 1; j <= m; j ++)
{
add(i, j + n, 1, times[i][j]);
bl[j + n] = j, ck_cnt[j] = 1;
}
}
t = 1000;
for(int j = 1; j <= m; j ++) add(j + n, t, 1, 0);
cur = m + n;
MCMF();
printf("%lld\n", mincost);
return 0;
}
P3980 [NOI2008] 志愿者招募(负容量)
其实可以简单地理解成偏移量。很容易就知道人数对应流量,最难思考的是每天最小人数的限制。
最大流问题一般都是水从源点流,中间有一些河流限制容量;而这道题中间边限制的竟然是要达到多少流量。因此可以想象中间没有河流,而是坑,即容量限制为负数,相当于把流量留在坑里了,最后只要把坑都填平就好,相当于最后的流量还是0。
但是网络流里不允许产生负权,因此我们对负容量限制做一个正值偏移,加上 i n f inf inf的限制就能保证为正数了。
//mcmf模板
int main()
{
scanf("%d%d", &n, &m);
s = 0, t = n + 2;
add(s, 1, inf, 0), add(n + 1, t, inf, 0);
for(int i = 1; i <= n; i ++)
{
int x; scanf("%d", &x); add(i, i + 1, inf - x, 0);
}
for(int i = 1; i <= m; i ++)
{
int u, v, w; scanf("%d%d%d", &u, &v, &w);
add(u, v + 1, inf, w);
}
MCMF();
printf("%lld\n", mincost);
return 0;
}