洛谷【小峰の题单】网络流经典题目 (最大流最小割二分图费用流部分)

模板

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)

  1. 每天早上需要 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的边。
  2. 每天晚上会产生 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 ici。因此可以根据这个来对维修工进行拆点计算。对于第 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 jck,表示第 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", &times[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", &times[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;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值