[刷题记录] luogu网络流24题 及 网络流心得体会 及 经典模型不定期更新

信息汇总表格

题目考察知识点
飞行员配对方案问题最大匹配最大流
分配问题最大完备匹配最大/最小费用流
运输问题多重匹配最大/最小费用流
小结Ⅰ
数字梯形问题//
最小路径覆盖问题
魔术球问题解的存在性残余网络上继续求解
圆桌问题最大流
试题库问题
深海机器人问题特殊边权/点权拆点
航空路线问题
火星探险问题
小结Ⅱ
太空飞行计划问题最大权闭合子图
方格取数问题限制绑定及输出方案
骑士共存问题最大流
小结Ⅰ、Ⅱ补充
最长不下降子序列问题特殊建图建分层图
孤岛营救问题
汽车加油行驶问题
[CTSC1999]家园 / 星际转移问题
餐巾计划问题
小结Ⅲ
最长k可重区间集问题特殊建图一流对多流
最长k可重线段集问题
负载平衡问题//
软件补丁问题

飞行员配对方案问题

luogu-P2756

左部 m m m 个点表示外籍飞行员,右部 n − m n-m nm 个点表示英国飞行员,二者能好好配合就连一条流量为无穷( 1 1 1也可以)的边。

源点向左部点连边,右部点向汇点连边,流量 1 1 1,表示该点只能被选择一次。答案即源点到汇点的最大流。

如果一条左右部边被流,意味着该边连接的两个人是一个配对。

绝对不能建成左右部点流量为 1 1 1 而与源/汇点流量无穷(二者同时存在),因为这样就不能限制每个点只能选一次,这种建图是限制每种关系只能选一次。

#include <bits/stdc++.h>
using namespace std;
#define inf 0x3f3f3f3f
#define maxn 105
struct node { int to, nxt, flow; }E[maxn * maxn];
int head[maxn], cur[maxn], dep[maxn];
int n, m, cnt = -1, s, t;
queue < int > q;

void addedge( 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 ) );
    memcpy( cur, head, sizeof( head ) );
    dep[s] = 1; q.push( 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;
            if( ! dep[v] and E[i].flow ) {
                dep[v] = dep[u] + 1;
                q.push( v );
            }
        }
    }
    return dep[t];
}

int dfs( int u, int cap ) {
    if( ! cap or u == t ) return cap;
    int flow = 0;
    for( int i = cur[u];~ i;i = E[i].nxt ) {
        cur[u] = i; int v = E[i].to;
        if( dep[v] == dep[u] + 1 ) {
            int w = dfs( v, min( cap, E[i].flow ) );
            if( ! w ) continue;
            E[i ^ 1].flow += w;
            E[i].flow -= w;
            flow += w;
            cap -= w;
            if( ! cap ) break;
        }
    }
    return flow;
}

int main() {
    memset( head, -1, sizeof( head ) );
    scanf( "%d %d", &m, &n );
    int u, v;
    while( scanf( "%d %d", &u, &v ) ) {
        if( ! ~ u and ! ~ v ) break;
        addedge( u, v, inf );
    }
    s = 0, t = n + 1;
    for( int i = 1;i <= m;i ++ ) addedge( s, i, 1 );
    for( int i = m + 1;i <= n;i ++ ) addedge( i, t, 1 );
    int ans = 0;
    while( bfs() ) ans += dfs( s, inf );
    printf( "%d\n", ans );
    for( int i = 1;i <= m;i ++ ) {
        for( int j = head[i];~ j;j = E[j].nxt )
            if( E[j].to > i and E[j ^ 1].flow ) { printf( "%d %d\n", i, E[j].to ); break; }
    }
    return 0;
}

分配问题

luogu-P4014

建图同上一题,只不过左部点到右部点的边还要带个边权,表示选择这对工作关系的“花费”。

选择的人和工件是不带来”花费“的,只有工作关系带来“花费”。

最大费用流将最小费用流 SPFA \text{SPFA} SPFA 的松弛符号或“花费”变号即可。

#include <bits/stdc++.h>
using namespace std;
#define int long long
#define inf 0x3f3f3f3f
#define maxn 205
#define maxm 50000
int n, cnt, s, t;
bool vis[maxn];
int lst[maxn], dis[maxn], head[maxn];
int c[maxn][maxn];
struct node { int to, nxt, flow, cost; }E[maxm];
queue < int > q;

void addedge( int u, int v, int flow, int cost ) {
    E[++ cnt] = { v, head[u], flow, cost }; head[u] = cnt;
    E[++ cnt] = { u, head[v], 0, -cost }; head[v] = cnt;
}

bool SPFA() {
    memset( lst, -1, sizeof( lst ) );
    memset( dis, 0x3f, sizeof( dis ) );
    dis[s] = 0; q.push( s );
    while( ! q.empty() ) {
        int u = q.front(); q.pop(); vis[u] = 0;
        for( int i = head[u];~ i;i = E[i].nxt ) {
            int v = E[i].to;  
            if( dis[v] > dis[u] + E[i].cost and E[i].flow ) {
                dis[v] = dis[u] + E[i].cost; lst[v] = i;
                if( ! vis[v] ) vis[v] = 1, q.push( v );
            }
        }
    }
    return ~ lst[t];
}

int MCMF() {
    int cost = 0;
    while( SPFA() ) {
        int flow = inf;
        for( int i = lst[t];~ i;i = lst[E[i ^ 1].to] )
            flow = min( flow, E[i].flow );
        for( int i = lst[t];~ i;i = lst[E[i ^ 1].to] ) {
            E[i].flow -= flow;
            E[i ^ 1].flow += flow;
            cost += E[i].cost * flow;
        }
    }
    return cost;
}

signed main() {
    scanf( "%lld", &n );
    for( int i = 1;i <= n;i ++ )
        for( int j = 1;j <= n;j ++ )
            scanf( "%lld", &c[i][j] );
    s = 0, t = n << 1 | 1;
    cnt = -1, memset( head, -1, sizeof( head ) );
    for( int i = 1;i <= n;i ++ ) {
        addedge( s, i, 1, 0 );
        addedge( i + n, t, 1, 0 );
    }
    for( int i = 1;i <= n;i ++ )
        for( int j = 1;j <= n;j ++ )
            addedge( i, j + n, inf, c[i][j] );
    printf( "%lld\n", MCMF() );
    cnt = -1, memset( head, -1, sizeof( head ) );
    for( int i = 1;i <= n;i ++ ) {
        addedge( s, i, 1, 0 );
        addedge( i + n, t, 1, 0 );
    }
    for( int i = 1;i <= n;i ++ )
        for( int j = 1;j <= n;j ++ )
            addedge( i, j + n, inf, -c[i][j] );
    printf( "%lld\n", - MCMF() );
    return 0;
}

运输问题

luogu-P4015

建图大致同上一题,只不过这次的流量限制不再为 1 1 1。仓库和货物的各自限制不同。

#include <bits/stdc++.h>
using namespace std;
#define maxn 205
#define maxm 30000
#define int long long
#define inf 0x3f3f3f3f
struct node { int to, nxt, flow, cost; }E[maxm];
queue < int > q;
int m, n, s, t, cnt;
bool vis[maxn];
int head[maxn], lst[maxn], dis[maxn];
int a[maxn], b[maxn], c[maxn][maxn];

void addedge( int u, int v, int flow, int cost ) {
    E[++ cnt] = { v, head[u], flow, cost }; head[u] = cnt;
    E[++ cnt] = { u, head[v], 0, -cost }; head[v] = cnt;
}

bool SPFA() {
    memset( dis, 0x3f, sizeof( dis ) );
    memset( lst, -1, sizeof( lst ) );
    dis[s] = 0; q.push( s );
    while( ! q.empty() ) {
        int u = q.front(); q.pop(); vis[u] = 0;
        for( int i = head[u];~ i;i = E[i].nxt ) {
            int v = E[i].to;
            if( dis[v] > dis[u] + E[i].cost and E[i].flow ) {
                dis[v] = dis[u] + E[i].cost; lst[v] = i;
                if( ! vis[v] ) vis[v] = 1, q.push( v );
            }
        }
    }
    return ~ lst[t];
}

int MCMF() {
    int ans = 0;
    while( SPFA() ) {
        int flow = inf;
        for( int i = lst[t];~ i;i = lst[E[i ^ 1].to] )
            flow = min( flow, E[i].flow );
        for( int i = lst[t];~ i;i = lst[E[i ^ 1].to] ) {
            E[i].flow -= flow;
            E[i ^ 1].flow += flow;
            ans += flow * E[i].cost;
        }
    }
    return ans;
}

signed main() {
    scanf( "%lld %lld", &m, &n );
    s = 0, t = n + m + 1;
    for( int i = 1;i <= m;i ++ ) scanf( "%lld", &a[i] );
    for( int i = 1;i <= n;i ++ ) scanf( "%lld", &b[i] );
    for( int i = 1;i <= m;i ++ )
        for( int j = 1;j <= n;j ++ )
            scanf( "%lld", &c[i][j] );
    cnt = -1, memset( head, -1, sizeof( head ) );
    for( int i = 1;i <= m;i ++ ) addedge( s, i, a[i], 0 );
    for( int i = 1;i <= n;i ++ ) addedge( i + m, t, b[i], 0 );
    for( int i = 1;i <= m;i ++ )
        for( int j = 1;j <= n;j ++ )
            addedge( i, j + m, inf, c[i][j] );
    printf( "%lld\n", MCMF() );
    cnt = -1, memset( head, -1, sizeof( head ) );
    for( int i = 1;i <= m;i ++ ) addedge( s, i, a[i], 0 );
    for( int i = 1;i <= n;i ++ ) addedge( i + m, t, b[i], 0 );
    for( int i = 1;i <= m;i ++ )
        for( int j = 1;j <= n;j ++ )
            addedge( i, j + m, inf, -c[i][j] );
    printf( "%lld\n", -MCMF() );
    return 0;
}
  • 小结Ⅰ。

数字梯形问题

luogu-P4013

这道题就能体现小结的所求问题不同建边方式不同。你可以检查一下自己是否真的都明白了?

路径不交,即限制每个点只能走一次。

路径仅在结点处相交,即限制每条边只能走一次。

路径可交,即每条边无限制。

#include <bits/stdc++.h>
using namespace std;
#define maxn 2000
#define maxm 50000
#define int long long
#define inf 0x3f3f3f3f
struct node { int to, nxt, flow, cost; }E[maxm];
int lst[maxn], dis[maxn], head[maxn], a[maxn];
int id[maxn][maxn];
bool vis[maxn];
int n, m, cnt, s, t;
queue < int > q;

void addedge( int u, int v, int w, int c ) {
    E[++ cnt] = { v, head[u], w, c }, head[u] = cnt;
    E[++ cnt] = { u, head[v], 0, -c }, head[v] = cnt;
}

bool SPFA() {
    memset( lst, -1, sizeof( lst ) );
    memset( dis, 0x3f, sizeof( dis ) );
    dis[s] = 0; q.push( s );
    while( ! q.empty() ) {
        int u = q.front(); q.pop(); vis[u] = 0;
        for( int i = head[u];~ i;i = E[i].nxt ) {
            int v = E[i].to;
            if( dis[v] > dis[u] + E[i].cost and E[i].flow ) {
                dis[v] = dis[u] + E[i].cost; lst[v] = i;
                if( ! vis[v] ) vis[v] = 1, q.push( v );
            }
        }
    }
    return ~ lst[t];
}

int MCMF() {
    int ans = 0;
    while( SPFA() ) {
        int flow = inf;
        for( int i = lst[t];~ i;i = lst[E[i ^ 1].to] )
            flow = min( flow, E[i].flow );
        for( int i = lst[t];~ i;i = lst[E[i ^ 1].to] ) {
            E[i].flow -= flow;
            E[i ^ 1].flow += flow;
            ans += E[i].cost * flow;
        }
    }
    return ans;
}

signed main() {
    scanf( "%lld %lld", &m, &n );
    int num = 0;
    for( int i = 1;i <= n;i ++ )
        for( int j = 1;j < m + i;j ++ )
            id[i][j] = ++ num, scanf( "%lld", &a[num] );
    //case 1
    memset( head, -1, sizeof( head ) );
    s = 0, t = num << 1 | 1, cnt = -1;
    for( int i = 1;i <= m;i ++ ) addedge( s, id[1][i], 1, 0 );
    for( int i = 1;i < n;i ++ )
        for( int j = 1;j < m + i;j ++ ) {
            addedge( id[i][j], id[i][j] + num, 1, -a[id[i][j]] );
            addedge( id[i][j] + num, id[i + 1][j], inf, 0 );
            addedge( id[i][j] + num, id[i + 1][j + 1], inf, 0 );
        }
    for( int i = 1;i < m + n;i ++ ) addedge( id[n][i], t, 1, -a[id[n][i]] );
    printf( "%lld\n", -MCMF() );
    //case 2
    memset( head, -1, sizeof( head ) );
    s = 0, t = num + 1, cnt = -1;
    for( int i = 1;i <= m;i ++ ) addedge( s, id[1][i], 1, 0 );
    for( int i = 1;i < n;i ++ )
        for( int j = 1;j < m + i;j ++ ) {
            addedge( id[i][j], id[i + 1][j], 1, -a[id[i][j]] );
            addedge( id[i][j], id[i + 1][j + 1], 1, -a[id[i][j]] );
        }
    for( int i = 1;i < m + n;i ++ )
        addedge( id[n][i], t, inf, -a[id[n][i]] );
    printf( "%lld\n", -MCMF() );
    //case 3
    memset( head, -1, sizeof( head ) );
    s = 0, t = num + 1, cnt = -1;
    for( int i = 1;i <= m;i ++ ) addedge( s, id[1][i], 1, 0 );
    for( int i = 1;i < n;i ++ )
        for( int j = 1;j < m + i;j ++ ) {
            addedge( id[i][j], id[i + 1][j], inf, -a[id[i][j]] );
            addedge( id[i][j], id[i + 1][j + 1], inf, -a[id[i][j]] );
        }
    for( int i = 1;i < m + n;i ++ )
        addedge( id[n][i], t, inf, -a[id[n][i]] );
    printf( "%lld\n", -MCMF() );
    return 0;
}

最小路径覆盖问题

luogu-P2764

限制每个点只能用一次且必用。输出方案。

枚举左部点以及枚举单位与右部点的连边。可以通过反向边是否为 1 1 1 判断是否流了这条边。

注意:左部点还与源点有连边,所以需要判断这条边另一端不是源点。

并将右部点对应原图上的点入度 + 1 +1 +1,从原图上入度为 0 0 0 的点开始搜索路径输出即可。

#include <bits/stdc++.h>
using namespace std;
#define maxn 500
#define maxm 20000
#define inf 0x3f3f3f3f
struct node { int to, nxt, flow; } E[maxm];
int dep[maxn], cur[maxn], head[maxn], vis[maxn], deg[maxn];
int n, m, cnt = -1, s, t;
queue < int > q;

void addedge( 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 ) );
    memcpy( cur, head, sizeof( head ) );
    dep[s] = 1; q.push( 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;
            if( ! dep[v] and E[i].flow ) {
                dep[v] = dep[u] + 1;
                q.push( v );
            }
        }
    }
    return dep[t];
}

int dfs( int u, int cap ) {
    if( u == t or ! cap ) return cap;
    int flow = 0;
    for( int i = cur[u];~ i;i = E[i].nxt ) {
        int v = E[i].to;
        if( dep[v] == dep[u] + 1 ) {
            int w = dfs( v, min( E[i].flow, cap ) );
            if( ! w ) continue;
            E[i ^ 1].flow += w;
            E[i].flow -= w;
            flow += w;
            cap -= w;
            if( ! cap ) break;
        }
    }
    return flow;
}

void dfs( int u ) {
    if( ! u ) return;
    printf( "%d ", u );
    dfs( vis[u] );
}

int main() {
    memset( head, -1, sizeof( head ) );
    scanf( "%d %d", &n, &m );
    s = 0, t = n << 1 | 1;
    for( int i = 1, u, v;i <= m;i ++ ) {
        scanf( "%d %d", &u, &v );
        addedge( u, v + n, inf );
    }
    for( int i = 1;i <= n;i ++ ) {
        addedge( s, i, 1 );
        addedge( i + n, t, 1 );
    }
    int ans = 0;
    while( bfs() ) ans += dfs( s, inf );
    for( int i = 1;i <= n;i ++ )
        for( int j = head[i];~ j;j = E[j].nxt )
            if( E[j].to > i and E[j ^ 1].flow ) {
                vis[i] = E[j].to - n;
                ++ deg[E[j].to - n];
            }
    for( int i = 1;i <= n;i ++ )
        if( ! deg[i] ) dfs( i ), puts("");
    printf( "%d\n", n - ans );
    return 0;
}

魔术球问题

luogu-P2765

所有的操作都是关于珠子的编号的。以每一个珠子为点,若满足条件(编号相加为平方数)就两两连边。

题目转化成图的问题就是 “对于给定的 n,计算不超过 n 条路径最多可以覆盖多少满足条件的节点”。

最小边覆盖 = = = 点总数 − - 最大匹配。

有这么个性质,于是再将此图进行拆点,转化成二分图的形式,每加一个点就在上面跑网络流并统计总匹配,如果发现 点总数 − - 最大匹配 > > > 最小边覆盖 那就退出。

我们每次重新跑网络流时,都是在跑残量网络,即我们每次所得的最大流都是增加的匹配数,所以是累加得到总的匹配数。

#include <bits/stdc++.h>
using namespace std;
#define maxn 10000
#define maxm 400000
#define inf 0x3f3f3f3f
struct node { int to, nxt, flow; }E[maxm];
int n, s, t, cnt = -1;
int dep[maxn], cur[maxn], head[maxn], p[maxn];
queue < int > q;

void addedge( 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 ) );
    memcpy( cur, head, sizeof( head ) );
    dep[s] = 1; q.push( 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;
            if( ! dep[v] and E[i].flow ) {
                dep[v] = dep[u] + 1;
                q.push( v );
            }
        }
    }
    return dep[t];
}

int dfs( int u, int cap ) {
    if( ! cap or u == t ) return cap;
    int flow = 0;
    for( int i = cur[u];~ i;i = E[i].nxt ) {
        int v = E[i].to; cur[u] = i;
        if( dep[v] == dep[u] + 1 ) {
            int w = dfs( v, min( cap, E[i].flow ) );
            if( ! w ) continue;
            E[i ^ 1].flow += w;
            E[i].flow -= w;
            flow += w;
            cap -= w;
            if( ! cap ) break;
        }
    }
    return flow;
}

int dinic() {
    int ans = 0;
    while( bfs() ) ans += dfs( s, inf );
    return ans;
}

void print( int u ) {
    for( int i = head[u];~ i;i = E[i].nxt ) {
        if( E[i].to == s or E[i].to == t or E[i].flow ) continue;
        else printf( "%d ", E[i].to >> 1 );
        print( ( E[i].to >> 1 ) << 1 );
    }
}

int main() {
    memset( head, -1, sizeof( head ) );
    scanf( "%d", &n );
    int tot = 0, x = 0; s = 0, t = 1;
    while( tot <= n ) {
        ++ x;
        addedge( s, x << 1, 1 );
        addedge( x << 1 | 1, t, 1 );
        for( int i = sqrt( x ) + 1;i * i < ( x << 1 );i ++ )
            addedge( ( i * i - x ) << 1, x << 1 | 1, 1 );
        if( ! dinic() ) p[++ tot] = x;
    }
    printf( "%d\n", -- x );
    for( int i = 1;i <= n;i ++ ) {
        printf( "%d ", p[i] );
        print( p[i] << 1 );
        puts("");
    }
    return 0;
}

圆桌问题

luogu-P3254

从同一个单位来的代表不在同一个餐桌就餐,等价于限制同样的单位和餐桌的配对只能有 1 1 1 次。

建图不再赘述,这里主要是要输出就餐的方案。

一条单位和餐桌连边被流就意味着该单位有一个人在这个餐桌就餐。

枚举单位(左部点)以及枚举单位与餐桌(右部点)的连边。可以通过反向边是否为 1 1 1 判断是否流了这条边。

注意:左部点还与源点有连边,所以需要判断这条边另一端不是源点。

#include <bits/stdc++.h>
using namespace std;
#define maxn 500
#define maxm 100000
#define inf 0x3f3f3f3f
struct node { int to, nxt, flow; }E[maxm];
int m, n, s, t, cnt = -1;
int dep[maxn], cur[maxn], head[maxn];
queue < int > q;

void addedge( 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 ) );
    memcpy( cur, head, sizeof( head ) );
    dep[s] = 1; q.push( 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;
            if( ! dep[v] and E[i].flow ) {
                dep[v] = dep[u] + 1;
                q.push( v );
            }
        }
    }
    return dep[t];
}

int dfs( int u, int cap ) {
    if( ! cap or u == t ) return cap;
    int flow = 0;
    for( int i = cur[u];~ i;i = E[i].nxt ) {
        int v = E[i].to; cur[u] = i;
        if( dep[v] == dep[u] + 1 ) {
            int w = dfs( v, min( cap, E[i].flow ) );
            if( ! w ) continue;
            E[i ^ 1].flow += w;
            E[i].flow -= w;
            flow += w;
            cap -= w;
            if( ! cap ) break;
        }
    }
    return flow;
}

int main() {
    memset( head, -1, sizeof( head ) );
    scanf( "%d %d", &m, &n );
    s = 0, t = n + m + 1; int ans = 0;
    for( int i = 1, x;i <= m;i ++ ) {
        scanf( "%d", &x );
        addedge( s, i, x );
        ans += x;
    }
    for( int i = 1, x;i <= n;i ++ ) {
        scanf( "%d", &x );
        addedge( i + m, t, x );
    }
    for( int i = 1;i <= m;i ++ ) 
        for( int j = 1;j <= n;j ++ )
            addedge( i, j + m, 1 );
    while( bfs() ) ans -= dfs( s, inf );
    if( ans ) return ! printf( "0\n" );
    else printf( "1\n" );
    for( int i = 1;i <= m;i ++ ) {
        for( int j = head[i];~ j;j = E[j].nxt )
            if( E[j].to > i and E[j ^ 1].flow ) printf( "%d ", E[j].to - m );
        puts("");
    }
    return 0;
}

试题库问题

luogu-P2763

完全同上。

#include <bits/stdc++.h>
using namespace std;
#define maxn 2000
#define maxm 50000
#define inf 0x3f3f3f3f  
int k, n, s, t, cnt = -1;
int dep[maxn], cur[maxn], head[maxn];
struct node { int to, nxt, flow; }E[maxm];
queue < int > q;

void addedge( 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() {
    memcpy( cur, head, sizeof( head ) );
    memset( dep, 0, sizeof( dep ) );
    dep[s] = 1; q.push( 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;
            if( ! dep[v] and E[i].flow ) {
                dep[v] = dep[u] + 1;
                q.push( v );
            }
        }
    }
    return dep[t];
}

int dfs( int u, int cap ) {
    if( u == t or ! cap ) return cap;
    int flow = 0;
    for( int i = cur[u];~ i;i = E[i].nxt ) {
        cur[u] = i; int v = E[i].to;
        if( dep[v] == dep[u] + 1 ) {
            int w = dfs( v, min( E[i].flow, cap ) );
            if( ! w ) continue;
            E[i ^ 1].flow += w;
            E[i].flow -= w;
            flow += w;
            cap -= w;
            if( ! cap ) break;
        }
    }
    return flow;
}

int main() {
    memset( head, -1, sizeof( head ) );
    scanf( "%d %d", &k, &n );
    s = 0, t = n + k + 1; int ans = 0;
    for( int i = 1;i <= n;i ++ ) addedge( s, i, 1 );
    for( int i = 1, x;i <= k;i ++ ) {
        scanf( "%d", &x );
        addedge( i + n, t, x );
        ans += x;
    }
    for( int i = 1, num;i <= n;i ++ ) {
        scanf( "%d", &num );
        for( int j = 1, x;j <= num;j ++ ) {
            scanf( "%d", &x );
            addedge( i, x + n, 1 );
        }
    }
    while( bfs() ) ans -= dfs( s, inf );
    if( ans ) return ! printf( "No Solution!\n" );
    for( int i = 1;i <= k;i ++ ) {
        printf( "%d:", i );
        for( int j = head[i + n];~ j;j = E[j].nxt )
            if( E[j].to < i + n and E[j].flow ) printf( " %d", E[j].to );
        puts("");
    }
    return 0;
}

深海机器人问题

luogu-P4012

将机器人抽象成流量,从源点向 ( 0 , 0 ) (0,0) (0,0) 的流量设置为机器人个数。

原图上的边在新图上全都存在。

一条边流过多少流量就意味着有多少个机器人走了这条边。

本题特殊的是,一条边只贡献一次边权,多个机器人多次走过也只计算一次。

换言之,重要的是第一次经过,这一次才会带来这条边权“花费”。

怎么在图上体现?—— “拆出来“。

即一条边连两次,一条流量为 1 1 1 带”花费“代表第一次经过,一条流量无穷不带花费代表第 n ( n > 1 ) n(n>1) n(n>1) 次经过。

跑最小费用流的时候就会先跑边权为负的,即先跑代表第一次的边。

#include <bits/stdc++.h>
using namespace std;
#define int long long
#define inf 0x3f3f3f3f
#define maxn 800
#define maxm 1000000
struct node { int to, nxt, flow, cost; }E[maxm];
int dis[maxn], lst[maxn], head[maxn];
bool vis[maxn];
int a, b, n, m, s, t, cnt = -1;
queue < int > q;

void addedge( int u, int v, int flow, int cost ) {
    E[++ cnt] = { v, head[u], flow, cost }; head[u] = cnt;
    E[++ cnt] = { u, head[v], 0, -cost }; head[v] = cnt;
}

bool SPFA() {
    memset( lst, -1, sizeof( lst ) );
    memset( dis, 0x3f, sizeof( dis ) );
    dis[s] = 0; q.push( s );
    while( ! q.empty() ) {
        int u = q.front(); q.pop(); vis[u] = 0;
        for( int i = head[u];~ i;i = E[i].nxt ) {
            int v = E[i].to;
            if( dis[v] > dis[u] + E[i].cost and E[i].flow ) {
                dis[v] = dis[u] + E[i].cost; lst[v] = i;
                if( ! vis[v] ) vis[v] = 1, q.push( v );
            }
        }
    }
    return ~ lst[t];
}

int MCMF() {
    int ans = 0;
    while( SPFA() ) {
        int flow = inf;
        for( int i = lst[t];~ i;i = lst[E[i ^ 1].to] )
            flow = min( flow, E[i].flow );
        for( int i = lst[t];~ i;i = lst[E[i ^ 1].to] ) {
            E[i ^ 1].flow += flow;
            E[i].flow -= flow;
            ans += E[i].cost;
        }
    }
    return ans;
}

int id( int x, int y ) { return ( x - 1 ) * m + y; }

signed main() {
    memset( head, -1, sizeof( head ) );
    scanf( "%lld %lld %lld %lld", &a, &b, &n, &m );
    n ++, m ++;
    s = 0, t = n * m + 1;
    int num = t;
    for( int i = 1;i <= n;i ++ )
        for( int j = 1, x;j < m;j ++ ) {
            scanf( "%lld", &x );
            addedge( id( i, j ), id( i, j + 1 ), 1, -x );
            addedge( id( i, j ), ++ num, inf, 0 );
            addedge( num, id( i, j + 1 ), inf, 0 );
        }
    for( int j = 1;j <= m;j ++ )
        for( int i = 1, x;i < n;i ++ ) {
            scanf( "%lld", &x );
            addedge( id( i, j ), id( i + 1, j ), 1, -x );
            addedge( id( i, j ), ++ num, inf, 0 );
            addedge( num, id( i + 1, j ), inf, 0 );
        }
    for( int i = 1, k, x, y;i <= a;i ++ ) {
        scanf( "%lld %lld %lld", &k, &x, &y );
        x ++, y ++;
        addedge( s, id( x, y ), k, 0 );
    }
    for( int i = 1, k, x, y;i <= b;i ++ ) {
        scanf( "%lld %lld %lld", &k, &x, &y );
        x ++, y ++;
        addedge( id( x, y ), t, k, 0 );
    }
    printf( "%lld\n", -MCMF() );
    return 0;
}

航空路线问题

luogu-P2770

显然不可能让网络流跑一个环流,会死循环。

所以把这个回来也变成过去,限制每个点只能经过一次,起点和终点可以经过两次。

限制的是点不是关系,拆点跑最大流,输出方案。

#include <bits/stdc++.h>
using namespace std;
#define maxn 205
#define maxm 100000
#define inf 0x3f3f3f3f
struct node { int to, nxt, flow, cost; }E[maxm];
int dis[maxn], lst[maxn], head[maxn];
bool vis[maxn];
queue < int > q;
map < string, int > id;
map < int, string > ch;
int n, m, cnt = -1, s, t;

void addedge( int u, int v, int flow, int cost ) {
    E[++ cnt] = { v, head[u], flow, cost }; head[u] = cnt;
    E[++ cnt] = { u, head[v], 0, -cost }; head[v] = cnt;
}

bool SPFA() {
    memset( dis, 0x3f, sizeof( dis ) );
    memset( lst, -1, sizeof( lst ) );
    dis[s] = 0; q.push( s );
    while( ! q.empty() ) {
        int u = q.front(); q.pop(); vis[u] = 0;
        for( int i = head[u];~ i;i = E[i].nxt ) {
            int v = E[i].to;
            if( dis[v] > dis[u] + E[i].cost and E[i].flow ) {
                dis[v] = dis[u] + E[i].cost; lst[v] = i;
                if( ! vis[v] ) vis[v] = 1, q.push( v );
            }
        }
    }
    return ~ lst[t];
}

int MCMF() {
    int ans = 0;
    while( SPFA() ) {
        int flow = inf;
        for( int i = lst[t];~ i;i = lst[E[i ^ 1].to] )
            flow = min( flow, E[i].flow );
        for( int i = lst[t];~ i;i = lst[E[i ^ 1].to] )
            E[i].flow -= flow, E[i ^ 1].flow += flow;
        ans += flow;
    }
    return ans;
}

vector < int > ret[2];
bool flag;
void dfs( int u, int w ) {
    for( int i = head[u];~ i and ! flag;i = E[i].nxt ) {
        if( E[i ^ 1].flow ) {
            E[i ^ 1].flow --;
            if( E[i].to == u + n ) ret[w].push_back( u ), dfs( E[i].to, w ), flag = 1;
            else if( u > n and E[i].to ^ u - n and E[i].to ^ s ) dfs( E[i].to, w ), flag = 1;
        }
    }
}

int main() {
    memset( head, -1, sizeof( head ) );
    scanf( "%d %d", &n, &m );
    string name1, name2; s = n << 1 | 1, t = s + 1;
    for( int i = 1;i <= n;i ++ ) {
        cin >> name1;
        ch[id[name1] = i] = name1;
        if( i ^ 1 and i ^ n ) addedge( i, i + n, 1, -1 );
        else addedge( i, i + n, 2, -1 );
    }
    for( int i = 1;i <= m;i ++ ) {
        cin >> name1 >> name2;
        int x = id[name1], y = id[name2];
        if( x > y ) swap( x, y );
        if( x == 1 and y == n ) addedge( x + n, y, 2, 0 );
        else addedge( x + n, y, 1, 0 );
    }
    addedge( s, 1, 2, 0 );
    addedge( n << 1, t, 2, 0 );
    if( MCMF() ^ 2 ) return ! printf( "No Solution!\n" );
    flag = 0;
    dfs( s, 0 );
    flag = 0;
    dfs( s, 1 );
    printf( "%d\n", ret[0].size() + ret[1].size() - 2 );
    for( int i = 0;i < ret[0].size();i ++ ) cout << ch[ret[0][i]] << endl;
    for( int i = ret[1].size() - 2;~ i;i -- ) cout << ch[ret[1][i]] << endl;
    return 0;
}

火星探险问题

luogu-P3356

这道题和《深海机器人》是一样的,只不过这道题的权值是点权而非边权,且不是每个点都有。

所以需要拆点。 [ 1 , P ∗ Q ] [1,P*Q] [1,PQ] 是普通点, [ P ∗ Q + 1 , P ∗ Q ∗ 2 ] [P*Q+1,P*Q*2] [PQ+1,PQ2] 是有石块点。这里同样的石块点只能贡献一次,连接石块点的边流量限制为 1 1 1

这里输出方案,因为车数量少图小,直接最后 dfs \text{dfs} dfs 走被流量流过的边,用 s e t set set 记录每个位置到达的车编号。

#include <bits/stdc++.h>
using namespace std;
#define maxn 5000
#define maxm 1000000
#define inf 0x3f3f3f3f
struct node { int to, nxt, flow, cost, dir; }E[maxm];
int ch[maxn][maxn];
int lst[maxn], dis[maxn], head[maxn];
bool vis[maxn];
int n, P, Q, S, T, cnt = -1;
queue < int > q;

void addedge( int u, int v, int flow, int cost ) {
    E[++ cnt] = { v, head[u], flow, cost, 1 }; head[u] = cnt;
    E[++ cnt] = { u, head[v], 0, -cost, 0 }; head[v] = cnt;
}

bool SPFA() {
    memset( dis, 0x3f, sizeof( dis ) );
    memset( lst, -1, sizeof( lst ) );
    dis[S] = 0; q.push( S );
    while( ! q.empty() ) {
        int u = q.front(); q.pop(); vis[u] = 0;
        for( int i = head[u];~ i;i = E[i].nxt ) {
            int v = E[i].to;
            if( dis[v] > dis[u] + E[i].cost and E[i].flow ) {
                dis[v] = dis[u] + E[i].cost; lst[v] = i;
                if( ! vis[v] ) vis[v] = 1, q.push( v );
            }
        }
    }
    return ~ lst[T];
}

void MCMF() {
    while( SPFA() ) {
        int flow = inf;
        for( int i = lst[T];~ i;i = lst[E[i ^ 1].to] ) 
            flow = min( flow, E[i].flow );
        for( int i = lst[T];~ i;i = lst[E[i ^ 1].to] )
            E[i].flow -= flow, E[i ^ 1].flow += flow;
    }
}

int id( int x, int y ) { return ( x - 1 ) * Q + y; }

bool print( int x, int y, int i ) {
    if( x < 1 or x > P * Q or y < 1 or y > P * Q ) return 0;
    if( y == x + 1 ) printf( "%d 1\n", i );
    else printf( "%d 0\n", i );
    return 1;
}

set < int > s[maxn];

void dfs( int x, int lst ) {
    for( int i = head[x];~ i;i = E[i].nxt )
        if( E[i ^ 1].flow and E[i].dir ) {
            bool flag = 1;
            while( E[i ^ 1].flow and s[x].size() ) {
                E[i ^ 1].flow --;
                flag = print( ( 1 <= x and x <= P * Q ) ? x : lst, E[i].to, *s[x].begin() );
                s[E[i].to].insert( *s[x].begin() );
                s[x].erase( s[x].begin() );
            }
            dfs( E[i].to, flag ? lst : x );
        }
}

int main() {
    memset( head, -1, sizeof( head ) );
    scanf( "%d %d %d", &n, &Q, &P );
    for( int i = 1;i <= P;i ++ )
        for( int j = 1;j <= Q;j ++ )
            scanf( "%d", &ch[i][j] );
    addedge( S, 1, n, 0 );
    #define ID id( i, j )
    for( int i = 1;i <= P;i ++ )
        for( int j = 1;j <= Q;j ++ ) {
            if( j ^ Q and ch[i][j + 1] ^ 1 )
                addedge( ID, id( i, j + 1 ), inf, 0 );
            if( i ^ P and ch[i + 1][j] ^ 1 )
                addedge( ID, id( i + 1, j ), inf, 0 );
        }
    for( int i = 1;i <= P;i ++ )
        for( int j = 1;j <= Q;j ++ ) {
            if( j ^ Q and ch[i][j + 1] == 2 )
                addedge( ID, id( i, j + 1 ) + P * Q, inf, 0 );
            if( i ^ P and ch[i + 1][j] == 2 )
                addedge( ID, id( i + 1, j ) + P * Q, inf, 0 );
        }
    for( int i = 1;i <= P;i ++ )
        for( int j = 1;j <= Q;j ++ )
            if( ch[i][j] == 2 ) addedge( id( i, j ) + P * Q, id( i, j ), 1, -1 );
    T = P * Q << 1 | 1;
    addedge( P * Q, T, inf, 0 );
    MCMF();
    for( int i = 1;i <= n;i ++ ) s[1].insert( i );
    dfs( 1, 0 );
    return 0;
}
  • 小结Ⅱ。

太空飞行计划问题

luogu-P2762

这里的限制不能用小结Ⅰ的方法理解了。

因为这里的限制是一堆绑定,少了其中一个都不行。

我们就用最小割的角度来看。

还是实验左部点,元件右部点,与源点汇点的连边上带花费。

如果一条边被流,看作是断掉了这条边。

最后跑最大流,割裂源点和汇点不再在同一个连通块。

这其实是最大权闭合子图,我以前写过详细的内容。

这里主要是有新的方案输出方法。

我们考虑怎么判断的流不动了?—— bfs \text{bfs} bfs 无法走到汇点。

这其实是通过网络流 bfs \text{bfs} bfs 的分层图来判断的。

在最后我们只需要看这些元件是否在分层图中(被标号了)就知道其是否与源点连通。

#include <bits/stdc++.h>
using namespace std;
#define maxn 105
#define maxm 30000
#define int long long
#define inf 0x3f3f3f3f
int n, m, cnt = -1, s, t;
struct node { int to, nxt, flow; }E[maxm];
int head[maxn], dep[maxn], cur[maxn];
bool vis[maxn];
queue < int > q;
vector < int > G[maxn];

void addedge( 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 ) );
    memcpy( cur, head, sizeof( head ) );
    dep[s] = 1; q.push( 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;
            if( ! dep[v] and E[i].flow ) {
                dep[v] = dep[u] + 1;
                q.push( v );
            }
        }
    }
    return dep[t];
}

int dfs( int u, int cap ) {
    if( u == t or ! cap ) return cap;
    int flow = 0;
    for( int i = cur[u];~ i;i = E[i].nxt ) {
        int v = E[i].to; cur[u] = i;
        if( dep[v] == dep[u] + 1 ) {
            int w = dfs( v, min( cap, E[i].flow ) );
            if( ! w ) continue;
            E[i ^ 1].flow += w;
            E[i].flow -= w;
            flow += w;
            cap -= w;
            if( ! cap ) break;
        }
    }
    return flow;
}

signed main() {
    memset( head, -1, sizeof( head ) );
    scanf( "%lld %lld", &m, &n );
    s = 0, t = n + m + 1; int ans = 0;
    for( int i = 1, x;i <= m;i ++ ) {
        scanf( "%lld", &x );
        addedge( s, i, x );
        ans += x;
        char tools[10000];
        memset( tools, 0, sizeof( tools ) );
        cin.getline( tools, 10000 );
        int ulen = 0, tool;
        while( sscanf( tools + ulen, "%lld" , &tool ) == 1 ) {
            G[i].push_back( tool );
            addedge( i, tool + m, inf );
            if( tool == 0 ) ulen ++;
            else while( tool ) tool /= 10, ulen ++;
            ulen ++;
        }
    }
    for( int i = 1, x;i <= n;i ++ ) {
        scanf( "%lld", &x );
        addedge( i + m, t, x );
    }
    while( bfs() ) ans -= dfs( s, inf );
    for( int i = 1;i <= m;i ++ )
        if( dep[i] ) printf( "%lld ", i );
    puts("");
    for( int i = 1;i <= n;i ++ ) if( dep[i + m] ) printf( "%lld ", i );
    puts("");
    printf( "%lld\n", ans );
    return 0;
}

方格取数问题

luogu-P2774

将每个格子拆成选 i i i 和不选 i + n i+n i+n 两个点。

u , v u,v u,v 两点代表的格子有共同边,连边无穷 ( u , v + n ) , ( v , u + n ) (u,v+n),(v,u+n) (u,v+n),(v,u+n),选点和源点连边,不选点和汇点连边。

从最小割角度理解跑费用流。

最后看和 S S S 在同一个集合的点的值。

#include <bits/stdc++.h>
using namespace std;
#define maxn 20005
#define maxm 50000
#define int long long
#define inf 0x3f3f3f3f
struct node { int to, nxt, flow; }E[maxm];
int dep[maxn], cur[maxn], a[maxn], head[maxn];
bool vis[maxn];
queue < int > q;
int n, m, s, t, cnt = -1;

void addedge( 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 ) );
    memcpy( cur, head, sizeof( head ) );
    dep[s] = 1; q.push( 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;
            if( ! dep[v] and E[i].flow ) {
                dep[v] = dep[u] + 1;
                q.push( v );
            }
        }
    }
    return dep[t];
}

int dfs( int u, int cap ) {
    if( ! cap or u == t ) return cap;
    int flow = 0;
    for( int i = cur[u];~ i;i = E[i].nxt ) {
        int v = E[i].to; cur[u] = i;
        if( dep[v] == dep[u] + 1 ) {
            int w = dfs( v, min( cap, E[i].flow ) );
            if( ! w ) continue;
            E[i ^ 1].flow += w;
            E[i].flow -= w;
            flow += w;
            cap -= w;
            if( ! cap ) break;
        }
    }
    return flow;
}

int id( int x, int y ) { return ( x - 1 ) * n + y; }

signed main() {
    memset( head, -1, sizeof( head ) );
    scanf( "%lld %lld", &m, &n );
    s = 0, t = n * m << 1 | 1;
    for( int i = 1;i <= m;i ++ )
        for( int j = 1;j <= n;j ++ )
            scanf( "%lld", &a[id( i, j )] );
    for( int i = 1;i <= m;i ++ )
        for( int j = 1;j <= n;j ++ ) {
            addedge( s, id( i, j ), a[id( i, j )] );
            addedge( id( i, j ) + n * m, t, a[id( i, j )] );
        }
    for( int i = 1;i <= m;i ++ )
        for( int j = 1;j <= n;j ++ ) {
            if( i ^ 1 ) addedge( id( i, j ), id( i - 1, j ) + n * m, inf );
            if( j ^ 1 ) addedge( id( i, j ), id( i, j - 1 ) + n * m, inf );
            if( i ^ m ) addedge( id( i, j ), id( i + 1, j ) + n * m, inf );
            if( j ^ n ) addedge( id( i, j ), id( i, j + 1 ) + n * m, inf );
        }
    while( bfs() ) dfs( s, inf );
    int ans = 0;
    for( int i = 1;i <= m;i ++ )
        for( int j = 1;j <= n;j ++ )
            if( dep[id( i, j )] ) ans += a[id( i, j )];
    printf( "%lld\n", ans );
    return 0;
}

骑士共存问题

选了一个点意味着一些点的不选择。

在《太空飞行计划问题》中选了一个点就要选择一些点,与这道题恰恰相反,当时我们是最小割入手。

在《方格取数问题》中,与本题是同类题,我们是从拆点最小割入手。

这道题我们当然可以同上一题的做法,但这道题的特殊性质提供了一种新方法。

一个点横纵坐标和和其攻击点的横纵坐标和奇偶性一定不同。

将这个棋盘变成二分图,按 x + y x+y x+y 奇偶分类。

选了一个点意味着不选一些点,我反而将这种关系体现成边。

这些边一定是连接左右部点的边。

假设奇为左部点,偶为右部点,在新图上跑最大匹配。

先假设所有点都选择,此时一个匹配意味着什么?意味着有一对关系冲突。

那么我只需要删掉其中一个点即可。

所以总点数减去最大匹配恰恰就是可选的最大点数!

#include <bits/stdc++.h>
using namespace std;
#define maxn 40005
#define maxm 500000
#define inf 0x3f3f3f3f
struct node { int to, nxt, flow; }E[maxm];
int n, m, s, t, cnt = -1;
int dep[maxn], cur[maxn], head[maxn];
bool vis[maxn];
queue < int > q;

bool bfs() {
    memset( dep, 0, sizeof( dep ) );
    memcpy( cur, head, sizeof( head ) );
    dep[s] = 1; q.push( 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;
            if( ! dep[v] and E[i].flow ) {
                dep[v] = dep[u] + 1;
                q.push( v );
            }
        }
    }
    return dep[t];
}

int dfs( int u, int cap ) {
    if( ! cap or u == t ) return cap;
    int flow = 0;
    for( int i = cur[u];~ i;i = E[i].nxt ) {
        int v = E[i].to; cur[u] = i;
        if( dep[v] == dep[u] + 1 ) {
            int w = dfs( v, min( cap, E[i].flow ) );
            if( ! w ) continue;
            E[i ^ 1].flow += w;
            E[i].flow -= w;
            flow += w;
            cap -= w;
            if( ! cap ) break;
        }
    }
    return flow;
}

void addedge( int u, int v, int w ) {
    if( vis[u] or vis[v] ) return;
    E[++ cnt] = { v, head[u], w }, head[u] = cnt;
    E[++ cnt] = { u, head[v], 0 }, head[v] = cnt;
}

int id( int x, int y ) { return ( x - 1 ) * n + y; }

int main() {
    memset( head, -1, sizeof( head ) );
    scanf( "%d %d", &n, &m );
    for( int i = 1, x, y;i <= m;i ++ ) {
        scanf( "%d %d", &x, &y );
        vis[id( x, y )] = 1;
    }
    s = 0, t = n * n + 1;
    for( int i = 1;i <= n;i ++ )
        for( int j = 1;j <= n;j ++ )
            if( ( i + j ) & 1 ) {
                addedge( s, id( i, j ), 1 );
                if( i > 2 and j > 1 ) addedge( id( i, j ), id( i - 2, j - 1 ), inf );
                if( i > 2 and j < n ) addedge( id( i, j ), id( i - 2, j + 1 ), inf );
                if( i > 1 and j > 2 ) addedge( id( i, j ), id( i - 1, j - 2 ), inf );
                if( i > 1 and j < n - 1 ) addedge( id( i, j ), id( i - 1, j + 2 ), inf );
                if( i < n and j > 2 ) addedge( id( i, j ), id( i + 1, j - 2 ), inf );
                if( i < n and j < n - 1 ) addedge( id( i, j ), id( i + 1, j + 2 ), inf );
                if( i < n - 1 and j > 1 ) addedge( id( i, j ), id( i + 2, j - 1 ), inf );
                if( i < n - 1 and j < n ) addedge( id( i, j ), id( i + 2, j + 1 ), inf );
            }
            else addedge( id( i, j ), t, 1 );
    int ans = n * n - m;
    while( bfs() ) ans -= dfs( s, inf );
    printf( "%d\n", ans );
    return 0;
}
  • 小结Ⅰ,Ⅱ补充。

最长不下降子序列问题

luogu-P2766

d p dp dp 求出最长不下降子序列长度。

每个点只能用一次的情况下有多少个最长不下降子序列。

限制点,拆点加边。而且求的是最长。

我们考虑 d p dp dp 转移式子,只有 d p j + 1 = d p i j < i dp_{j}+1=dp_i\quad j<i dpj+1=dpij<i 才意味着 j j j 可能和 i i i 在同一个最长不下降子序列内。

按照这种条件进行连边。

这有点“分层”的意味了:

d p i dp_i dpi 相同的为一层,每条边都是相邻两层间的边,且是上一层指向下一层。

符合要求的答案一定在最后一层,开头从第一层开始。

我们在第一层上方单建一层只放源点,最后一层下方单建一层只放汇点。

(这只是脑子里形成的分层哦~代码里还是感觉是一坨)

分层图就是本层内是不存在边的,边都不是一层中的某点连向另一层的某点。

由《魔术球问题》知学得了在残余网络上加边继续判解存在性问题。

这里不用向《数字梯形问题》每次都重建(那只是刚开始为了方便理解)

我们直接在第二个子问题跑完后,另给 1 , n 1,n 1,n 号点加入无限制的边继续跑。

#include <bits/stdc++.h>
using namespace std;
#define maxn 1005
#define maxm 300000
#define inf 0x3f3f3f3f
struct node { int to, nxt, flow; }E[maxm];
int head[maxn], cur[maxn], dep[maxn];
bool vis[maxn];
queue < int > q;
int n, s, t, cnt, S, T;
int x[maxn], dp[maxn];

void addedge( 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 ) );
    memcpy( cur, head, sizeof( head ) );
    dep[s] = 1; q.push( 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;
            if( ! dep[v] and E[i].flow ) {
                dep[v] = dep[u] + 1;
                q.push( v );
            }
        }
    }
    return dep[t];
}

int dfs( int u, int cap ) {
    if( u == t or ! cap ) return cap;
    int flow = 0;
    for( int i = cur[u];~ i;i = E[i].nxt ) {
        cur[u] = i; int v = E[i].to;
        if( dep[v] == dep[u] + 1 ) {
            int w = dfs( v, min( E[i].flow, cap ) );
            if( ! w ) continue;
            E[i ^ 1].flow += w;
            E[i].flow -= w;
            flow += w;
            cap -= w;
            if( ! cap ) break;
        }
    }
    return flow;
}

int dinic() {
    int ans = 0;
    while( bfs() ) ans += dfs( s, inf );
    return ans;
}

int main() {
    scanf( "%d", &n );
    for( int i = 1;i <= n;i ++ ) scanf( "%d", &x[i] );
    if( n == 1 ) return ! printf( "1\n1\n1\n" );
    int ans = 0;
    for( int i = 1;i <= n;i ++ ) {
        dp[i] = 1;
        for( int j = 1;j < i;j ++ )
            if( x[j] <= x[i] ) dp[i] = max( dp[i], dp[j] + 1 );
        ans = max( ans, dp[i] );
    }
    printf( "%d\n", ans );
    s = 0, t = n << 1 | 1, cnt = -1;
    memset( head, -1, sizeof( head ) );
    for( int i = 1;i <= n;i ++ ) {
        addedge( i, i + n, 1 );
        if( dp[i] == 1 ) addedge( s, i, 1 );
        if( dp[i] == ans ) addedge( i + n, t, 1 );
        for( int j = i + 1;j <= n;j ++ )
            if( dp[i] + 1 == dp[j] and x[i] <= x[j] ) addedge( i + n, j, 1 );
    }
    int ret = dinic();
    printf( "%d\n", ret );
    addedge( s, 1, inf ), addedge( 1, n + 1, inf );
    if( dp[n] == ans ) addedge( n, n << 1, inf ), addedge( n << 1, t, inf );
    printf( "%d\n", ret + dinic() );
    return 0;
}

孤岛营救问题

luogu-P4011

分层。设计 2 p 2^p 2p 层,每层表示身上所有钥匙的状态,第 i i i 层二进制 j j j 1 1 1 表示当前状态有 j j j 类钥匙。

每层根据钥匙的拥有不同能到达的状态层(这个层可就不一定是相邻了)也不同,能到达的状态层中的位置也不同。

#include <bits/stdc++.h>
using namespace std;
#define maxn 200000
#define maxm 2000000
#define inf 0x3f3f3f3f
struct node { int to, nxt, flow, cost; }E[maxm];
int head[maxn], lst[maxn], dis[maxn];
bool vis[maxn];
queue < int > q;
int N, M, K, P, S, s, t, cnt = -1;

void addedge( int u, int v, int w, int c ) {
    E[++ cnt] = { v, head[u], w, c }, head[u] = cnt;
    E[++ cnt] = { u, head[v], 0,-c }, head[v] = cnt;
}

bool SPFA() {
    memset( dis, 0x3f, sizeof( dis ) );
    memset( lst, -1, sizeof( lst ) );
    dis[s] = 0; q.push( s );
    while( ! q.empty() ) {
        int u = q.front(); q.pop(); vis[u] = 0;
        for( int i = head[u];~ i;i = E[i].nxt ) {
            int v = E[i].to;
            if( dis[v] > dis[u] + E[i].cost and E[i].flow ) {
                dis[v] = dis[u] + E[i].cost; lst[v] = i;
                if( ! vis[v] ) vis[v] = 1, q.push( v );
            }
        }
    }
    return ~ lst[t];
}

void MCMF( int &maxflow, int &mincost ) {
    maxflow = mincost = 0;
    while( SPFA() ) {
        int flow = inf;
        for( int i = lst[t];~ i;i = lst[E[i ^ 1].to] )
            flow = min( flow, E[i].flow );
        for( int i = lst[t];~ i;i = lst[E[i ^ 1].to] ) {
            E[i].flow -= flow;
            E[i ^ 1].flow += flow;
            mincost += flow * E[i].cost;
        }
        maxflow += flow;
    }
}

pair < int, int > key[150][150];
int id( int x, int y ) { return ( x - 1 ) * M + y; }
bool flag[150]; 
int tot[150];
int g[150][150];

void build() {
    s = 0, t = (1 << P) * N * M + 1; flag[0] = 1;
    for( int sta = 0;sta < (1 << P);sta ++ ) {
        for( int i = 1;i <= P;i ++ )
            if( (1 << i - 1) & sta ) flag[i] = 1;
            else flag[i] = 0;
        for( int i = 1;i <= N;i ++ )
            for( int j = 1;j <= M;j ++ ) {
                if( j < M and ~ g[id(i, j)][id(i, j + 1)] and flag[g[id(i, j)][id(i, j + 1)]] ) {
                    addedge( sta * N * M + id(i, j), N * M * sta + id(i, j + 1), inf, 1 );
                    addedge( sta * N * M + id(i, j + 1), N * M * sta + id(i, j), inf, 1 );
                }
                if( i < N and ~ g[id(i, j)][id(i + 1, j)] and flag[g[id(i + 1, j)][id(i, j)]] ) {
                    addedge( sta * N * M + id(i, j), N * M * sta + id(i + 1, j), inf, 1 );
                    addedge( sta * N * M + id(i + 1, j), N * M * sta + id(i, j), inf, 1 );
                }
                if( i == N and j == M ) addedge( sta * N * M + id(i, j), t, inf, 0 );
            }
        for( int i = 1;i <= P;i ++ )
            if( ! flag[i] ) {
                int nxt = sta + (1 << i - 1);
                for( int j = 1;j <= tot[i];j ++ ) {
                    int x = id(key[i][j].first, key[i][j].second);
                    addedge( N * M * sta + x, N * M * nxt + x, inf, 0 );
                }
            }
    }
    addedge( s, 1, 1, 0 );
}

int main() {
    memset( head, -1, sizeof( head ) );
    scanf( "%d %d %d %d", &N, &M, &P, &K );
    for( int i = 1, x, y, id1, id2;i <= K;i ++ ) {
        scanf( "%d %d", &x, &y );
        id1 = id( x, y );
        scanf( "%d %d", &x, &y );
        id2 = id( x, y );
        scanf( "%d", &x );
        if( ! x ) x = -1;
        g[id1][id2] = g[id2][id1] = x;
    }
    scanf( "%d", &S );
    for( int i = 1, x, y, p;i <= S;i ++ ) {
        scanf( "%d %d %d", &x, &y, &p );
        key[p][++ tot[p]] = make_pair( x, y );
    }
    build();
    int flow, cost;
    MCMF( flow, cost );
    if( flow ^ 1 ) puts("-1");
    else printf( "%d\n", cost );
    return 0;
}

汽车加油行驶问题

luogu-P4009

按油量分层,第 i i i 层表示油量剩余 i i i。翻译题目中的边即可。

0 0 0 层一定要加油回到油满层。

#include <bits/stdc++.h>
using namespace std;
#define inf 0x3f3f3f3f
#define int long long
#define maxn 300000
#define maxm 4000000
struct node { int to, nxt, flow, cost; }E[maxm];
int dis[maxn], lst[maxn], head[maxn];
bool vis[maxn];
int s, t, cnt = -1;
queue < int > q;

void addedge( int u, int v, int w, int c ) {
    E[++ cnt] = { v, head[u], w, c }, head[u] = cnt;
    E[++ cnt] = { u, head[v], 0,-c }, head[v] = cnt;
}

bool SPFA() { 
    memset( lst, -1, sizeof( lst ) );
    memset( dis, 0x3f, sizeof( dis ) );
    dis[s] = 0; q.push( s );
    while( ! q.empty() ) {
        int u = q.front(); q.pop(); vis[u] = 0;
        for( int i = head[u];~ i;i = E[i].nxt ) {
            int v = E[i].to;
            if( dis[v] > dis[u] + E[i].cost and E[i].flow ) {
                dis[v] = dis[u] + E[i].cost; lst[v] = i;
                if( ! vis[v] ) vis[v] = 1, q.push( v );
            }
        }
    }
    return ~lst[t];
}

int MCMF() {
    int ans = 0;
    while( SPFA() ) {
        int flow = inf;
        for( int i = lst[t];~ i;i = lst[E[i ^ 1].to] )
            flow = min( flow, E[i].flow );
        for( int i = lst[t];~ i;i = lst[E[i ^ 1].to] ) {
            E[i].flow -= flow;
            E[i ^ 1].flow += flow;
            ans += E[i].cost * flow;
        }
    }
    return ans;
}

int fuel[105][105];
int N, K, A, B, C;

int id( int x, int y, int k ) { return k * N * N + ( x - 1 ) * N + y; }

void build() {
    memset( head, -1, sizeof( head ) );
    s = 0, t = id(N, N, K) + 1;
    addedge(s, id(1, 1, K), 1, 0 );
    for( int k = 0;k <= K;k ++ ) {//第k层表示剩余油量为k
        for( int i = 1;i <= N;i ++ ) {
            for( int j = 1;j <= N;j ++ ) {
                if( i == N and j == N ) addedge( id(i, j, k), t, inf, 0 );
                if( fuel[i][j] and k ^ K ) {
                    addedge( id(i, j, k), id(i, j, K), inf, A );
                    continue;
                }
                if( k ) {
                    if( i ^ 1 ) addedge( id(i, j, k), id(i - 1, j, k - 1), inf, B );
                    if( j ^ 1 ) addedge( id(i, j, k), id(i, j - 1, k - 1), inf, B );
                    if( i ^ N ) addedge( id(i, j, k), id(i + 1, j, k - 1), inf, 0 );
                    if( j ^ N ) addedge( id(i, j, k), id(i, j + 1, k - 1), inf, 0 );
                }
                else addedge( id(i, j, k), id(i, j, K), inf, A + C );
            }
        }
    }
}

signed main() {
    scanf( "%lld %lld %lld %lld %lld", &N, &K, &A, &B, &C );
    for( int i = 1;i <= N;i ++ )
        for( int j = 1;j <= N;j ++ )
            scanf( "%lld", &fuel[i][j] );
    build();
    printf( "%lld\n", MCMF() );
    return 0;
}

[CTSC1999]家园 / 星际转移问题

luogu-P2754

先用并查集判断地球(源点)和月球(汇点)是否联通。

按时间分层,枚举答案,同时建层(不再是一开始就建完了)船的停留站点是个循环。

每一次都跑网络流统计流到汇点的流量,当达到总人数时证明全部运送完了。

#include <bits/stdc++.h>
using namespace std;
#define maxn 1000000
#define inf 0x3f3f3f3f
struct node { int to, nxt, flow; }E[maxn];
int dep[maxn], cur[maxn], head[maxn];
int h[200], r[200], g[200][200];
int cnt = -1, s, t, n, m, k;
queue < int > q;

void addedge( 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 ) );
    memcpy( cur, head, sizeof( head ) );
    dep[s] = 1, q.push( 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;
            if( ! dep[v] and E[i].flow ) {
                dep[v] = dep[u] + 1;
                q.push( v );
            }
        }
    }
    return dep[t];
}

int dfs( int u, int cap ) {
    if( ! cap or u == t ) return cap;
    int flow = 0;
    for( int i = cur[u];~ i;i = E[i].nxt ) {
        int v = E[i].to; cur[u] = i;
        if( dep[v] == dep[u] + 1 ) {
            int w = dfs( v, min( cap, E[i].flow ) );
            if( ! w ) continue;
            E[i ^ 1].flow += w;
            E[i].flow -= w;
            flow += w;
            cap -= w;
            if( ! cap ) break;
        }
    }
    return flow;
}

int dinic() {
    int ans = 0;
    while( bfs() ) ans += dfs( s, inf );
    return ans;
}

namespace DSU {
    int f[maxn];
    void init() { for( int i = 1;i <= n + 2;i ++ ) f[i] = i; }
    int find( int x ) { return f[x] == x ? x : f[x] = find( f[x] ); }
    void merge( int x, int y ) { x = find( x ), y = find( y ); if( x ^ y ) f[y] = x; }
}

int main() {
    memset( head, -1, sizeof( head ) );
    scanf( "%d %d %d", &n, &m, &k );
    DSU :: init();
    for( int i = 1;i <= m;i ++ ) {
        scanf( "%d %d", &h[i], &r[i] );
        for( int j = 0;j < r[i];j ++ ) {
            scanf( "%d", &g[i][j] );
            if( g[i][j] == 0 ) g[i][j] = n + 1;
            if( g[i][j] == -1 ) g[i][j] = n + 2;
            if( j ) DSU :: merge( g[i][j - 1], g[i][j] );
        }
    }
    if( DSU :: find( n + 1 ) ^ DSU :: find( n + 2 ) ) return ! printf( "0\n" );
    s = 0, t = maxn - 1; int flow = 0;
    for( int ans = 1;;ans ++ ) {
        addedge( s, (ans - 1) * (n + 1) + n + 1, inf ); //第ans秒的地球编号
        for( int i = 1;i <= m;i ++ ) { //往下一层建边
            int x = (ans - 1) % r[i], y = ans % r[i];
            if( g[i][x] == n + 2 ) x = t;
            else x = (ans - 1) * (n + 1) + g[i][x];
            if( g[i][y] == n + 2 ) y = t;
            else y = ans * (n + 1) + g[i][y];
            addedge( x, y, h[i] );
        }
        flow += dinic();
        if( flow >= k ) return ! printf( "%d\n", ans );
        for( int i = 1;i <= n + 1;i ++ )
            addedge( (ans - 1) * (n + 1) + i, ans * (n + 1) + i, inf );
    }
    return 0;
}

小结Ⅲ。

餐巾计划问题

luogu-P1251

按天分层建图。

将每天拆成起始点和结束点。

  • 送到快洗部属于结束点操作,连向 i + m i+m i+m 天后的起始点,带费用。
  • 送到慢洗部属于结束点操作,连向 i + n i+n i+n 天后的起始点,带费用。
  • 延期送洗属于结束点操作,连向 i + 1 i+1 i+1 天的结束点,不带费用。
  • 购买新的餐巾的操作也应是连向每天起始点的边,目前没有确定从哪连,但带费用。

网络流的建图一定有顺序的,建的边一定是沿源点流向汇点方向,才能流通且不环流。

发现上面的建边都是从一天的结束点到后面天的起始点/结束点,且每天起始点没有往后面的天连点。

所以应当是源点向结束点连边,起始点向汇点连边。新毛巾就是源点向起始点连边。

#include <bits/stdc++.h>
using namespace std;
#define inf 0x7f7f7f7f
#define int long long
#define maxn 600000
struct node { int to, nxt, flow, cost; }E[maxn];
int head[maxn], dis[maxn], lst[maxn];
bool vis[maxn];
queue < int > q;
int cnt = -1, s, t;

void addedge( int u, int v, int flow, int cost ) {
	E[++ cnt] = { v, head[u], flow, cost }, head[u] = cnt;
	E[++ cnt] = { u, head[v], 0, -cost }, head[v] = cnt;
}

bool SPFA() {
	memset( dis, 0x7f, sizeof( dis ) );
	memset( lst, -1, sizeof( lst ) );
	dis[s] = 0; q.push( s );
	while( ! q.empty() ) {
		int u = q.front(); q.pop(); vis[u] = 0;
		for( int i = head[u];~ i;i = E[i].nxt ) {
			int v = E[i].to;
			if( dis[v] > dis[u] + E[i].cost and E[i].flow ) {
				dis[v] = dis[u] + E[i].cost; lst[v] = i;
				if( ! vis[v] ) vis[v] = 1, q.push( v );
			}
		}
	}
	return ~ lst[t];
}

int MCMF() {
	int cost = 0;
	while( SPFA() ) {
		int flow = inf;
		for( int i = lst[t];~ i;i = lst[E[i ^ 1].to] )
			flow = min( flow, E[i].flow );
		for( int i = lst[t];~ i;i = lst[E[i ^ 1].to] ) {
			E[i ^ 1].flow += flow;
			E[i].flow -= flow;
			cost += flow * E[i].cost;
		}
	}
	return cost;
}

signed main() {
	memset( head, -1, sizeof( head ) );
	int n; scanf( "%lld", &n );
	s = 0, t = n << 1 | 1;
	//[1,n]表示晚上 [n+1,2n]表示白天
	for( int i = 1, x;i <= n;i ++ ) {
		scanf( "%lld", &x );
		addedge( s, i, x, 0 ); //第i天晚上得到x张脏餐巾
		addedge( i + n, t, x, 0 ); //第i天早上有x张干净餐巾可用
	}
	int P, M, F, N, S;
	scanf( "%lld %lld %lld %lld %lld", &P, &M, &F, &N, &S );
	for( int i = 1;i <= n;i ++ ) {
		if( i + 1 <= n ) addedge( i, i + 1, inf, 0 ); //第i天晚上的脏餐巾可以留到i+1天晚上
		if( i + M <= n ) addedge( i, i + M + n, inf, F ); //送去快洗部 在第i+M天早上得到干净餐巾
		if( i + N <= n ) addedge( i, i + N + n, inf, S ); //送去慢洗部 在第i+N天早上得到干净餐巾
		addedge( s, i + n, inf, P ); //直接在第i天早上购买新餐巾
	}
	printf( "%lld\n", MCMF() );
	return 0;
}
  • 小结Ⅲ。

最长k可重区间集问题

luogu-P3358

每个点被覆盖当且仅当线段的起点或者终点被覆盖,那么可以离散成点,因为一条线段的中间的点是不必要的。

对于每个点,其最多被覆盖 k k k 次,也就是说一条线段能覆盖好多点,但是一个点最多被覆盖 k k k 次。

如果我们将每条线段连向源点表示使用当前这条线段,而这条线段需要连向自己覆盖的那些点,这些点连向汇点,容量为 k k k 表示自己最多被覆盖 k k k 次。

我们让最终被覆盖的点通过汇点的流量限制保证这个条件,但是这并不正确。

因为这是费用流,这条线段的花费肯定是其长度,而流量为 1 1 1,如果流量 > 1 >1 >1 的话费用将会被统计的不正确,但是为 1 1 1 的话一条线段的流必然流向某个点,但是我们想让这一条流流过所有的点!怎么办?

可以发现这变成了一个一流对多流的问题,问题自然也被转换成了如何使用一条流流过若干个点,发现想要这么做必然这些点得横着连到一块然后使用一条流流过他们,所以就有本题的基本模型了:把点串联起来。

我们从源点放出 k k k 流,所以每个相邻两点之间连上容量为 k k k 费用为 0 0 0 的边 然后线段连接 l l l r r r(离散化),费用为其长度,跑费用流即可。

#include <bits/stdc++.h>
using namespace std;
#define maxn 1005
#define maxm 20000
#define inf 0x3f3f3f3f
struct node { int to, nxt, flow, cost; }E[maxm];
int dis[maxn], lst[maxn], head[maxn], l[maxn], r[maxn], len[maxn], x[maxn];
bool vis[maxn];
queue < int > q;
int n, k, s, t, cnt = -1;

void addedge( int u, int v, int w, int c ) {
    E[++ cnt] = { v, head[u], w, c }; head[u] = cnt;
    E[++ cnt] = { u, head[v], 0, -c }; head[v] = cnt;
}

bool SPFA() {
    memset( lst, -1, sizeof( lst ) );
    memset( dis, 0x3f, sizeof( dis ) );
    dis[s] = 0; q.push( s );
    while( ! q.empty() ) {
        int u = q.front(); q.pop(); vis[u] = 0;
        for( int i = head[u];~ i;i = E[i].nxt ) {
            int v = E[i].to;
            if( dis[v] > dis[u] + E[i].cost and E[i].flow ) {
                dis[v] = dis[u] + E[i].cost; lst[v] = i;
                if( ! vis[v] ) vis[v] = 1, q.push( v );
            }
        }
    }
    return ~ lst[t];
}

int MCMF() {
    int ans = 0;
    while( SPFA() ) {
        int flow = inf;
        for( int i = lst[t];~ i;i = lst[E[i ^ 1].to] )
            flow = min( flow, E[i].flow );
        for( int i = lst[t];~ i;i = lst[E[i ^ 1].to] )
            E[i ^ 1].flow += flow, E[i].flow -= flow, ans += flow * E[i].cost;
    }
    return ans;
}

int main() {
    memset( head, -1, sizeof( head ) );
    scanf( "%d %d", &n, &k );
    for( int i = 1;i <= n;i ++ ) {
        scanf( "%d %d", &l[i], &r[i] );
        x[i] = l[i], x[i + n] = r[i];
        len[i] = r[i] - l[i];
    }
    sort( x + 1, x + ( n << 1 | 1 ) );
    int m = unique( x + 1, x + ( n << 1 | 1 ) ) - x - 1;
    for( int i = 1;i <= n;i ++ ) {
        l[i] = lower_bound( x + 1, x + m + 1, l[i] ) - x;
        r[i] = lower_bound( x + 1, x + m + 1, r[i] ) - x;
    }
    s = 0, t = m + 1;
    for( int i = 1;i <= m;i ++ ) {
        if( i == 1 ) addedge( s, i, k, 0 );
        else if( i == m ) addedge( i, t, k, 0 ), addedge( i - 1, i, k, 0 );
        else addedge( i - 1, i, k, 0 );
    }
    for( int i = 1;i <= n;i ++ ) addedge( l[i], r[i], 1, -len[i] );
    printf( "%d\n", -MCMF() );
    return 0;
}

最长k可重线段集问题

如果 y y y 各不相同,完全可以拍成 x x x 轴上的问题,就转化到上一题了。

那我们直接换种表示方法在 x x x 轴上表示一个线段?

在数轴上,比较简单的方式就是扩域。

每个线段 i i i 的左右端点 ( l i , r i ) (l_i,r_i) (li,ri) 变换成 ( 2 × l i , 2 × r i ) (2\times l_i,2\times r_i) (2×li,2×ri),这样的话就相当于每个下标多了一个空间。

那么对于一个左右端点相同的区间 ( x , x ) (x,x) (x,x),就可以连边成 ( 2 x , 2 x + 1 ) (2x,2x + 1) (2x,2x+1)

但这样的话,原本左右端点不用的区间也要改。

由于那些相同的区间右端点加了 1 1 1,所以如果存在这样两个线段 ( p , p ) , ( p , q ) (p,p),(p,q) (p,p),(p,q),那么原本不交的两个区间,在扩域之后变成了相交的 ( 2 p , 2 p + 1 ) , ( 2 p , 2 q ) (2p,2p+1),(2p,2q) (2p,2p+1),(2p,2q)

处理方式很简单,对于一个 p ≠ q p\not=q p=q 的区间 ( p , q ) (p,q) (p,q),连边 ( 2 p + 1 , 2 q ) (2p+1,2q) (2p+1,2q) 即可。

对于原本存在的两个均不垂直 x x x 轴的线段,他们如果相交,那么交的那一端, r 1 − l 2 ≥ 1 r_1-l_2\ge 1 r1l21;如果不交,那么有 l 2 − r 1 ≥ 1 l_2-r_1\ge 1 l2r11

扩域之后就变成了 ≥ 2 \ge 2 2

所以如果只是左端点增加 1 1 1 ,根本不影响判定。

#include <bits/stdc++.h>
using namespace std;
#define maxn 1005
#define maxm 20000
#define inf 0x3f3f3f3f
struct node { int to, nxt, flow, cost; }E[maxm];
int dis[maxn], lst[maxn], head[maxn], l[maxn], r[maxn], len[maxn], x[maxn];
bool vis[maxn];
queue < int > q;
int n, k, s, t, cnt = -1;

void addedge( int u, int v, int w, int c ) {
    E[++ cnt] = { v, head[u], w, c }; head[u] = cnt;
    E[++ cnt] = { u, head[v], 0, -c }; head[v] = cnt;
}

bool SPFA() {
    memset( lst, -1, sizeof( lst ) );
    memset( dis, 0x3f, sizeof( dis ) );
    dis[s] = 0; q.push( s );
    while( ! q.empty() ) {
        int u = q.front(); q.pop(); vis[u] = 0;
        for( int i = head[u];~ i;i = E[i].nxt ) {
            int v = E[i].to;
            if( dis[v] > dis[u] + E[i].cost and E[i].flow ) {
                dis[v] = dis[u] + E[i].cost; lst[v] = i;
                if( ! vis[v] ) vis[v] = 1, q.push( v );
            }
        }
    }
    return ~ lst[t];
}

int MCMF() {
    int ans = 0;
    while( SPFA() ) {
        int flow = inf;
        for( int i = lst[t];~ i;i = lst[E[i ^ 1].to] )
            flow = min( flow, E[i].flow );
        for( int i = lst[t];~ i;i = lst[E[i ^ 1].to] )
            E[i ^ 1].flow += flow, E[i].flow -= flow, ans += flow * E[i].cost;
    }
    return ans;
}

int main() {
    memset( head, -1, sizeof( head ) );
    scanf( "%d %d", &n, &k );
    for( int i = 1;i <= n;i ++ ) {
        scanf( "%d %d", &l[i], &r[i] );
        x[i] = l[i], x[i + n] = r[i];
        len[i] = r[i] - l[i];
    }
    sort( x + 1, x + ( n << 1 | 1 ) );
    int m = unique( x + 1, x + ( n << 1 | 1 ) ) - x - 1;
    for( int i = 1;i <= n;i ++ ) {
        l[i] = lower_bound( x + 1, x + m + 1, l[i] ) - x;
        r[i] = lower_bound( x + 1, x + m + 1, r[i] ) - x;
    }
    s = 0, t = m + 1;
    for( int i = 1;i <= m;i ++ ) {
        if( i == 1 ) addedge( s, i, k, 0 );
        else if( i == m ) addedge( i, t, k, 0 ), addedge( i - 1, i, k, 0 );
        else addedge( i - 1, i, k, 0 );
    }
    for( int i = 1;i <= n;i ++ ) addedge( l[i], r[i], 1, -len[i] );
    printf( "%d\n", -MCMF() );
    return 0;
}

负载平衡问题

luogu-P4016

就是个结论题,非要包装成网络流??那我们就已知答案反推网络流写法?

a i − a v e a_i-ave aiave 分成正负两部分,源点流给负的表示“需要”,正的流给汇点表示“传递”。

编号相邻点之间连边即可。

结论为什么正确不是本篇博客讨论重点,略过。

#include <bits/stdc++.h>
using namespace std;
#define maxn 105
#define maxm 5005
#define inf 0x3f3f3f3f
int n, s, t, cnt = -1;
int a[maxn], dis[maxn], lst[maxn], head[maxn];
bool vis[maxn];
struct node { int to, nxt, flow, cost; }E[maxm];
queue < int > q;

void addedge( int u, int v, int w, int c ) {
    E[++ cnt] = { v, head[u], w, c }, head[u] = cnt;
    E[++ cnt] = { u, head[v], 0, -c }, head[v] = cnt;
}

bool SPFA() {
    memset( dis, 0x3f, sizeof( dis ) );
    memset( lst, -1, sizeof( lst ) );
    dis[s] = 0; q.push( s );
    while( ! q.empty() ) {
        int u = q.front(); q.pop(); vis[u] = 0;
        for( int i = head[u];~ i;i = E[i].nxt ) {
            int v = E[i].to;
            if( dis[v] > dis[u] + E[i].cost and E[i].flow ) {
                dis[v] = dis[u] + E[i].cost; lst[v] = i;
                if( ! vis[v] ) vis[v] = 1, q.push( v );
            }
        }
    }
    return ~ lst[t];
}

void MCMF() {
    int ans = 0;
    while( SPFA() ) {
        int flow = inf;
        for( int i = lst[t];~ i;i = lst[E[i ^ 1].to] )
            flow = min( flow, E[i].flow );
        for( int i = lst[t];~ i;i = lst[E[i ^ 1].to] ) {
            E[i ^ 1].flow += flow;
            E[i].flow -= flow;
            ans += flow * E[i].cost;
        }
    }
    printf( "%d\n", ans );
}

int main() {
    memset( head, -1, sizeof( head ) );
    scanf( "%d", &n );
    int ave = 0;
    for( int i = 1;i <= n;i ++ )
        scanf( "%d", &a[i] ), ave += a[i];
    ave /= n;
    s = 0, t = n + 1;
    for( int i = 1;i <= n;i ++ ) {
        if( a[i] > ave ) addedge( i, t, a[i] - ave, 0 );
        if( a[i] < ave ) addedge( s, i, ave - a[i], 0 );
        addedge( i, i == n ? 1 : i + 1, inf, 1 );
        addedge( i, i == 1 ? n : i - 1, inf, 1 );
    }
    MCMF();
    return 0;
}

软件补丁问题

luogu-P2761

就是个最短路问题不懂为啥是在二十四题内(也许是因为可以硬套成一个流量为 1 1 1 的最小费用流)。

#include <bits/stdc++.h>
using namespace std;
#define inf 0x3f3f3f3f
int dis[4000000], vis[4000000];
int b1[1000], b2[1000], f1[1000], f2[1000], w[1000];
char a[1000], b[1000];
int n, m;
queue < int > q;

void SPFA() {
    int s = (1 << n) - 1, t = 0;
    memset( dis, 0x3f, sizeof( dis ) );
    dis[s] = 0; q.push( s );
    while( ! q.empty() ) {
        int u = q.front(); q.pop(); vis[u] = 0;
        for( int i = 1;i <= m;i ++ )
            if( (b1[i] & u) == b1[i] and !(b2[i] & u) ) {
                int v = ((u | f1[i]) ^ f1[i]) | f2[i];
                if( dis[v] > dis[u] + w[i] ) {
                    dis[v] = dis[u] + w[i];
                    if( ! vis[v] ) vis[v] = 1, q.push( v );
                }
            }
    }
    if( dis[t] >= inf ) printf( "0\n" );
    else printf( "%d\n", dis[t] );
}

int main() {
    scanf( "%d %d", &n, &m );
    for( int i = 1;i <= m;i ++ ) {
        scanf( "%d %s %s", &w[i], a, b );
        for( int j = 0;j < n;j ++ ) {
            switch( a[j] ) {
                case '+' : b1[i] |= (1 << j); break;
                case '-' : b2[i] |= (1 << j); break;
                case '0' : break;
            }
            switch( b[j] ) {
                case '-' : f1[i] |= (1 << j); break;
                case '+' : f2[i] |= (1 << j); break;
                case '0' : break;
            }
        }
    }
    SPFA();
    return 0;
}

总结

  • 网络流是“单向”的,类比水流,一旦建图形成环就会无限流下去。

    所以有些题目的路程看似是一条回路,也得拆成两条单向路。

    网络流是“定向”的,从源点流向汇点方向。

  • 对于边无穷流量设计的理解:

    • 如果是从最大流角度,那么意味着这条边是可以随便走的,不被限制,也不限制其余边。这条边对应的信息可以无限次数地使用。
    • 如果是从最小割角度,那么意味着这条边连接的两个点的信息是被绑定在一起的,无法分开。一般在”这个东西选/不选“问题中出现。
  • 碰到不是“一个单位时间”的问题(例如多天,多个小时)往往考察分层图。

  • 如果一个“点”带了两个独立的属性(限制与花费)往往是考察费用流。一个成为边流量一个成为边花费。

    如果一个点只有花费属性,就变成流量还是跑最大流。

  • 点权题往往意味着拆点变边权。网络流是没有“点权”这一概念的。

小结Ⅰ

(最大流角度)

  • 每个点都有限制,限制将变成网络流上的“流量”。

    • 这个限制是只针对自己的,那么就会将且是与源/汇点的边上的。
    • 这个限制是针对“关系”的,就是两个点的连边上。
    • 其余为 i n f inf inf
  • 所求的花费和价值都变成网络流上的“花费”。

    • “花费”针对“关系”时就在两个点的连边上。
    • 针对是否选择该点时就在与源/汇点的边上。
    • 其余为 0 0 0
  • 当全体都无限制的时候,肯定会有大背景下的限制,比如求 m m m 种,只有 m m m 个人什么的。

    这个时候就是源点向唯一起点的边流量限制成 m m m

    如果起点不唯一,那么还得新建一个超级起点,源点向超级起点限制,超级起点向所有起点建边无穷。

    这是为了统一起点,至于为什么?解释与《飞行员配对方案问题》中所述问题同理。

小结Ⅱ

网络流很常见的技巧——拆点。拆点往往有着一定的暗示:

  • 同一种“选择”(不同时刻经过同一个点,同一条边)但其中有一些会自带独立属性(前几次会有贡献之类的)

    这个时候,往往将选择拆成有贡献的选择和无贡献的选择。

    通过流量限制“选择”的次数。

    一般这种边都可以形象的表示: ∘ → ∘ \circ\rightarrow\circ 变成了 ∘ ↗ ∘ ↘ ∘ _\circ\nearrow^\circ\searrow_\circ 一定要记得连回来!

  • 状态不同(到达同一个位置,自身属性不同:钥匙油量等)

    会将一个点的每一个不同状态都拆成一个点。

网络流的方案输出问题。

  • 根据反向边流量来看。(流的角度)

    这条边流了,那么反向边一定有流量增加。

  • 根据 S , T S,T S,T 集合划分来看(割的角度)

    判断是否 bfs \text{bfs} bfs 有层编号,便可知道能否从 S S S 走到这个点。

  • 但有的时候根据反向边流量来看可能会出错。一般是把花费当成流量的题上。

小结Ⅰ、Ⅱ 补充

(最小割角度)

往往是出现价值/花费的时候,建图的边是从最小割方面理解。

通常对于一条流过的边,在最大流角度意味着“选择”,在最小割角度意味着“放弃”。

不管是流角度还是割角度,一条流通路上的流量一定是由路上边的最小流量决定的。

会发现输出方案有的题是选择割的角度有的是流的角度,真的每道题两种角度都适用吗?

对于绑定选择拆点的问题:

如果把点拆成选和不选分成二分图,选与 S S S 联通,不选与 T T T 联通。

最小割来理解,你会发现如果绑定是单向的,跑出来就打错特错了。

单向即,选 a, 必不选 b,但是选 b 没要求不能选 a。

双向即,选a,不能选 b;选 b,也不能选 a。

觉得有点怪是吧?其实是因为不能绑定互斥关系导致的。

你想一个点拆成选和不选,那么势必这两个拆点之间不会连边,对于一个限制关系将 u u u 选点和 v v v 不选点绑定在一起,而不绑定 v v v 选点和 u u u 不选点。那跑二分图最大完备匹配后就无法得知最大价值了。

如果我们从流的角度算所有的流通路上的值和,很有可能某个点选和不选在不同的流通路上,这就废了。

如果我们从割的角度看所有与源点联通的值和,那也完了,可能割掉的是某个点不选到汇点边(这条流量最小),本身这个点选和不选也不连通,代表选意义的点可能也在源点集合,那不还是将冲突的两个点都选了。

而如果这个绑定是双向的,那么对于对于一对关系 x , y x,y x,y ,以 x x x 选出发选择断 y y y 不选边,那么从 y y y 选出发也会选择断 y y y 选边而不断 x x x 不选边。

这就恰好避免了上述问题。这个时候就直接去找和源点联通的点求值和。

这也是《太空飞行计划》没拆点,而《方格取数》拆点也可做的原因。

所以拆成互斥状态点时一定要是双向绑定的。

这里就可以扯到入度和出度的匹配应用。

比如每次可以选择两个点将两个点度数 − 1 -1 1 且不同对操作花费不同,让最后将所有点度数成为 0 0 0

当你将点拆成左右部分时, x x x 左到 y y y 右流了一定也会 y y y 左到 x x x 右流。这个时候一定会让一个点度数就 − 2 -2 2

在网络流图上看似是两种走法但对应回去确实一种操作。

所以这个时候就是左右点和源点/汇点的边上流量限制是度数的一半,就与二倍递减相匹配了。

小结Ⅲ

关于分层图,题目的暗示往往非常明显。

一般涉及状态不同的拆点时就是在分层图上。

当操作有延迟效应无法一次性完成时(单位时间)也是分层图。

分层图要注意,是否有循环。

  • 链层

    像天数这种一直增加的连边通常在相邻两层,有些操作可能会一次性跨好多层。

    但层数总是不减的。

    “出口”往往是从最后一层的点出来。

  • 环层。

    像循环的,油量补充的,分层图是一个循环。

    一般这种时候“出口”每一层的出口点都是出口。

感觉说的很杂乱,因为这是作者自己刷题掉过的坑然后总结出来的。每个人形成的感觉是无法通过学习他人结果建立的,只能自己去试错。——作者

以下是不定期更新:

网络流经典模型

网格图的行列匹配(Upd2022-02-15)

例题:[ZJOI2007] 矩阵游戏

可以发现,如果一开始两个点不在同一行(列)那么无论怎么交换最后也不会在同一行(列)。

如果我们把一个 ( i , j ) = 1 (i,j)=1 (i,j)=1 的点看做是 i i i j j j 列的一条匹配边的话。

最后有解的是每一行每一列 ( 1 , 1 ) , ( 2 , 2 ) , . . . , ( n , n ) (1,1),(2,2),...,(n,n) (1,1),(2,2),...,(n,n) 都恰好匹配。

对于原图,如果 ( i , j ) = 1 (i,j)=1 (i,j)=1 就连一条行 i → i\rightarrow i j j j 的边。

所有列交换都可以用若干次行交换代替,反之亦然。因此我们不妨只考虑行交换。

当按上述规则建立起了一个二分图匹配的网络。

我们交换两行对应的就是交换两个行点对应的列点。

所以如果最后是完备匹配,那么一定存在方案将其交换成 i i i i i i 列的匹配。

明白这一点过后,这道题就是个普普通通的匹配问题跑最大流。

#include <bits/stdc++.h>
using namespace std;
#define maxn 1005
#define inf 0x3f3f3f3f
int T, n, s, t, cnt;
int head[maxn], dep[maxn], cur[maxn];
struct node { int to, nxt, flow; }E[maxn * maxn];
queue < int > q;

void addedge( 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 ) );
    memcpy( cur, head, sizeof( head ) );
    dep[s] = 1; q.push( 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; 
            if( ! dep[v] and E[i].flow ) {
                dep[v] = dep[u] + 1;
                q.push( v );
            }
        }
    }
    return dep[t];
}

int dfs( int u, int cap ) {
    if( u == t or ! cap ) return cap;
    int flow = 0;
    for( int i = cur[u];~ i;i = E[i].nxt ) {
        int v = E[i].to; cur[u] = i;
        if( dep[v] == dep[u] + 1 ) {
            int w = dfs( v, min( cap, E[i].flow ) );
            if( ! w ) continue;
            E[i ^ 1].flow += w;
            E[i].flow -= w;
            flow += w;
            cap -= w;
            if( ! cap ) break;
        }
    }
    return flow;
}

int dinic() {
    int ans = 0;
    while( bfs() ) ans += dfs( s, inf );
    return ans;
}

int main() {
    scanf( "%d", &T );
    while( T -- ) {
        scanf( "%d", &n );
        memset( head, -1, sizeof( head ) );
        cnt = -1, s = 0, t = n << 1 | 1;
        for( int i = 1;i <= n;i ++ ) {
            for( int j = 1, x;j <= n;j ++ ) {
                scanf( "%d", &x );
                if( x ) addedge( i, j + n, inf );
            }
            addedge( s, i, 1 );
            addedge( i + n, t, 1 );
        }
        if( dinic() == n ) puts("Yes");
        else puts("No");
    }
    return 0;
}

图的入度出度匹配(Upd2022-02-15)

例题:[TJOI2013]循环格

一个循环我们可以看成是一个欧拉回路。题目就是求改边的最小次数可以将一个一般图转化成若干条欧拉回路。

那么每个位置只会存在于一条欧拉回路里面。也就是说这个点的入度和出度是匹配的。

将每个点拆成入度和出度点,将四个方向都连边。本身就是这个方向的,这条边就不带花费。

否则带花费 1 1 1,流了这条边就相当于是进行了一次改边操作。

这样就转化成了费用流模型。

还有一道题也是考察的这个:一般图带权多重匹配

#include <bits/stdc++.h>
using namespace std;
#define maxn 500
#define inf 0x7f7f7f7f
struct node { int to, nxt, flow, cost; }E[maxn << 4];
int n, m, s, t, cnt = -1;
char dir[maxn];
int dis[maxn], lst[maxn], vis[maxn], head[maxn];
queue < int > q;

void addedge( int u, int v, int flow, int cost ) {
    E[++ cnt] = { v, head[u], flow, cost }, head[u] = cnt;
    E[++ cnt] = { u, head[v], 0, - cost }, head[v] = cnt;
}

bool SPFA() {
    memset( dis, 0x3f, sizeof( dis ) );
    memset( lst, -1, sizeof( lst ) );
    dis[s] = 0; q.push( s );
    while( ! q.empty() ) {
        int u = q.front(); q.pop(); vis[u] = 0;
        for( int i = head[u];~ i;i = E[i].nxt ) {
            int v = E[i].to;
            if( dis[v] > dis[u] + E[i].cost and E[i].flow ) {
                dis[v] = dis[u] + E[i].cost; lst[v] = i;
                if( ! vis[v] ) vis[v] = 1, q.push( v );
            }
        }
    }
    return ~ lst[t];
}

int MCMF() {
    int ans = 0;
    while( SPFA() ) {
        int flow = inf;
        for( int i = lst[t];~ i;i = lst[E[i ^ 1].to] )
            flow = min( flow, E[i].flow );
        for( int i = lst[t];~ i;i = lst[E[i ^ 1].to] ) {
            E[i].flow -= flow;
            E[i ^ 1].flow += flow;
            ans += flow * E[i].cost;
        }
    }
    return ans;
}

int id( int x, int y ) { 
    if( x < 1 ) x = n;
    if( x > n ) x = 1;
    if( y < 1 ) y = m;
    if( y > m ) y = 1;
    return (x - 1) * m + y; 
}

int main() {
    scanf( "%d %d", &n, &m );
    memset( head, -1, sizeof( head ) );
    s = 0, t = n * m << 1 | 1;
    for( int i = 1;i <= n;i ++ ) {
        scanf( "%s", dir + 1 );
        for( int j = 1;j <= m;j ++ ) {
            addedge( s, id(i, j), 1, 0 );
            addedge( id(i, j) + n * m, t, 1, 0 );
            addedge( id(i, j), id(i - 1, j) + n * m, inf, dir[j] != 'U' );
            addedge( id(i, j), id(i + 1, j) + n * m, inf, dir[j] != 'D' );
            addedge( id(i, j), id(i, j - 1) + n * m, inf, dir[j] != 'L' );
            addedge( id(i, j), id(i, j + 1) + n * m, inf, dir[j] != 'R' );
        }
    }
    printf( "%d\n", MCMF() );
    return 0;
}

连续区间建图问题

单纯地对二十四题中《最长k可重区间问题》的再阐释。👉 看第一种解法的详细阐释即可

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值