2024.12.9 二分图 和 网络流 讲课笔记

二分图的最大匹配

模板题:P3386 【模板】二分图最大匹配

匈牙利算法

每次找到一条增广路

模板:

vector<int> ve[5010];
//ve[i]:从左部点 i 出发直接连边的右部点编号
int vtm[510], mch[510];

//main
int rs = 0;
function<bool(int, int)> find = [&](int x, int tm)->bool {//x is left point
    if (vtm[x] == tm)return 0;vtm[x] = tm;
    for (int t : ve[x])if (!mch[t] || find(mch[t], tm))return mch[t] = x, 1;//enum right point and try match
    return 0;
};
for (int i = 1; i <= n; ++i)if (find(i, i))++rs;
cout << rs << endl;
//参考:https://www.luogu.com.cn/article/nez5vwet

时间复杂度 \(O(ne+m)\),其中 \(n\) 为左部点数,\(m\) 为右部点数,\(e\) 为边数,因此左部点更多时可以交换两侧的点以减小复杂度

网络流解法

源点向每个左部点连边,每个右部点向汇点连边,每个左部点向有边直接相连的右部点连边,边的容量都是 \(1\)

则原二分图的最大匹配为新网络的最大流,具体方案为最大流经过的原图的边

代码(\(Dinic\)

时间复杂度 \(O(m\sqrt n)\)

DAG 最小路径点覆盖

选出最少的路径,覆盖 \(DAG\) 所有点

将每个点 \(i\) 拆为 \(i\)\(i+n\) 两个点,\(1\sim n\) 为左部点,\(n+1\sim 2n\) 为右部点

对于原图的边 \(u\to v\),在新二分图上连 \(u\to v+n\)(得到的图称为原图的拆点二分图)

可证 \(DAG\) 最小路径点覆盖等于 \(n\) 减去其拆点二分图的最大匹配

DAG 最小路径可重点覆盖

在原图中直接或间接联通的有序点对 \((x,y)\)\(x\) 连通到 \(y\),可由 \(Floyed\) 传递闭包求出),在新图上连 \(x\to y\),新图的最小路径点覆盖为原图的最小路径可重点覆盖

二分图最小点覆盖

选择原图顶点的一个子集,满足对于任意一条边,其两端点中至少一个在被选择的点集中,则最小的合法子集就是原图最小点覆盖

根据 \(K\"onig\) 定理,二分图 最小点覆盖大小 等于其 最大匹配

构造:从每一个非匹配点出发,只能正向穿过非匹配边(包含匹配边的反向边),标记所有可以到达的点,则左部点中没有标记的点右部点中标记的点是原图的一组最小点覆盖

二分图最大独立集

二分图最大独立集大小 等于 总点数 减去 最小点覆盖

构造:最小点覆盖的补集即为一种最大独立集

二分图最小边覆盖

选择原图边的一个子集,满足所有顶点都存在至少一条邻边为被选择的边,则最小的合法子集就是原图最小边覆盖

二分图最小边覆盖 等于 总点数 减去 最大匹配

构造:取所有匹配边,并对于每个非匹配点任选一条邻边,则取出的边为原图的一组最小边覆盖

二分图最大权完美匹配

求原图的一组完美匹配,满足匹配边边权之和最大

网络流解法

类似最大匹配的方式建网络后,跑最大费用最大流即可,时间复杂度 \(O(nmf)=O(n^4)\)

KM 算法

模板题:P6577 【模板】二分图最大权完美匹配

左部点和右部点都是 \(n\) 个,\(w(i,j)\) 为左部点 \(i\) 和右部点 \(j\) 之间的边权,\(mtc_i\) 表示右部点 \(i\) 匹配的左部点编号(若没匹配则为 \(-1\)),\(vsl\) 为左部点访问标记,\(vsr\) 为右部点访问标记,\(exl\) 为左部点顶标,\(exr\) 为右部点顶标,\(slk_i\) 为右部点 \(i\) 的松弛量,即 \(\min_j(exl_j+exr_i-w(j,i))\)

定义相等子图为 所有点 和 满足 \(exl_u+exr_v=w(u,v)\) 的边 \((u,v)\) 构成的子图

初始 \(exl_i=\max_j w(i,j)\)\(exr_i=0\)\(mtc_i=-1\)

\(1\)\(n\) 依次考虑每个右部点,尝试将其加入匹配,设当前处理点为 \(i\)

\(slk_x=\infty\),然后重复以下过程:

  • 沿相等子图中的边跑匈牙利算法,尝试从 \(i\) 开始找增广路,标记到达的 \(exr\)\(exl\),并在该过程中计算出 \(slk\)(根据定义)
  • 若成功增广则跳出
  • 否则令 \(d\)\(\min_{u\mid exr_u=0} slk_u\)
  • 对于每个 \(j\)
    • \(vsl_j=1\),则 \(exl_j\gets exl_j-d\)
    • \(vsr_j=1\),则 \(exr_j\gets exr_j-d\),否则 \(slk_j\gets slk_j-d\)

这样实现是 \(O(n^4)\) 的,考虑优化

瓶颈在于匈牙利部分,若将匈牙利换为 \(bfs\) 并在搜索过程中一同松弛,则可以做到 \(O(n^3)\)

模板:

#define int long long
constexpr int inf = 1e15;
namespace KM_algo {
    pair<int, vector<int> > max_match_n4(int n, vector<vector<int> > w){//ve_i: left_{ve_i} match right_i   O(n^4)
        vector<int> mtc(n + 1, -1);//right match left
        vector<int> exl(n + 1), exr(n + 1, 0), slk(n + 1);
        for (int i = 1; i <= n; ++i)exl[i] = *max_element(w[i].begin() + 1, w[i].end());
        for (int i = 1; i <= n; ++i){
            fill_n(slk.begin() + 1, n, inf);
            while (1){
                vector<bool> vsl(n + 1), vsr(n + 1);
                function<int(int)> dfs = [&](int Lf){
                    for (int Rt = vsl[Lf] = 1, gap; Rt <= n; ++Rt)if (!vsr[Rt]){
                        if (gap = exl[Lf] + exr[Rt] - w[Lf][Rt])slk[Rt] = min(slk[Rt], gap);
                        else {vsr[Rt] = 1;if (!~mtc[Rt] || dfs(mtc[Rt])){mtc[Rt] = Lf;return 1;}}/*Hungary algo*/
                    }
                    return 0;
                };
                if (dfs(i))break;int d = inf;for (int j = 1; j <= n; ++j)if (!vsr[j])d = min(d, slk[j]);
                for (int j = 1; j <= n; ++j){if (vsl[j])exl[j] -= d;if (vsr[j])exr[j] += d;else slk[j] -= d;}
            }
        }
        int ret = 0;for (int i = 1; i <= n; ++i)ret += w[mtc[i]][i]; return make_pair(ret, mtc);
    }
    pair<int, vector<int> > max_match(int n, vector<vector<int> > w){//ve_i: left_{ve_i} match right_i   O(n^3)
        vector<int> mtc(n + 1, -1);//right match left
        vector<int> exl(n + 1, 0), exr(n + 1, 0);
        for (int u = 1; u <= n; ++u){
            vector<int> slc(n + 1, inf);
            vector<int> pr(n + 1, 0);
            vector<bool> vsr(n + 1);
            int cur = 0, pcur = 0;
            mtc[cur] = u;
            while (~mtc[cur]){
                auto tmn = [](int &x, int y){return x > y? x = y, 1 : 0;};
                int Lf = mtc[cur], dlt = inf; vsr[cur] = 1;
                for (int Rt = 1; Rt <= n; ++Rt)if (!vsr[Rt]){
                    if (tmn(slc[Rt], exl[Lf] + exr[Rt] - w[Lf][Rt]))pr[Rt] = cur;
                    if (tmn(dlt, slc[Rt]))pcur = Rt;
                }
                for (int Rt = 0; Rt <= n; ++Rt){
                    if (vsr[Rt])exl[mtc[Rt]] -= dlt, exr[Rt] += dlt;
                    else slc[Rt] -= dlt;
                }
                cur = pcur;
            }
            while (cur)mtc[cur] = mtc[pr[cur]], cur = pr[cur];
        }
        int ret = mtc[0] = 0;for (int i = 1; i <= n; ++i)ret += w[mtc[i]][i];return make_pair(ret, mtc);
    }
}

若要求最大权匹配,则将两边点数补成相同后用边权为 \(0\) 的边补足即可

若要求特判不存在完美匹配的情况,则补的边边权设为 \(-\infty\),若答案为极小值,则无完美匹配

Hall 定理

一张二分图存在完美匹配,当且仅当对于左部点的任意非空子集,其中的点连向的不同右部点的数量不小于该子集大小

例:P3488 [POI2009] LYZ-Ice Skates

初始 \(1\sim n\) 各有 \(k\) 个,编号为 \(t\) 的元素可以匹配任意 \([t,t+d]\) 范围内的数,\(m\) 次修改每次新增 \(x\)(可为负)个编号为 \(r\) 的元素,每次修改后判断是否每个元素都可以匹配一个数字,\(n\le2\times10^5,m\le5\times10^5,r\le n-d,k,x\le10^9\)

\(x_i\) 为当前编号为 \(i\) 的元素数量,则全能匹配当且仅当 \(\forall S\subseteq([1,n-d]\cap \mathbb Z)\),有 \(\sum_{i=1}^n [\exists u\in S,i\in [u,u+d]]k\ge \sum_{u\in S}x_u\)

\(S\) 中元素从大到小为 \(a_{1\sim t}\)

则若存在 \(a_i+d<a_{i+1}\),则 \(S=\{a_{1\sim i}\}\)\(S=\{a_{i+1\sim t}\}\) 都合法显然不比 \(S=\{a\}\) 合法更弱

若存在 \(a_i+1<a_{i+1}\le a_i+d\),则 \(S=\{a\}\cup((a_i,a_{i+1})\cap \mathbb Z)\) 合法显然不比 \(S=\{a\}\) 合法更弱

因此,\(\forall S\subseteq([1,n-d]\cap \mathbb Z)\),一定存在若干个 \([l_i,r_i]\),满足所有 \(S_i=[l_i,r_i]\cap\mathbb Z\) 都符合要求,不弱于 \(S\) 符合要求

显然这样的 \(S_i\) 本身也是需要满足要求的

因此全能匹配当且仅当 \(\forall 1\le l\le r\le n-d\),有 \(k(r-l+1+d)\ge \sum_{i=l}^r x_i\)

\(x'_i=x_i-k\),则条件变为 \(\forall 1\le l\le r\le n-d\)\(kd\ge \sum_{i=l}^r x'_i\)

\(x'\) 序列的最大子段和不超过 \(kd\)

因此线段树维护最大子段和即可

时间复杂度 \(O(n\log n)\)

代码

最大流

几种算法存网络的方式相同

#define int long long
constexpr int inf = 1e15;
struct Eg{int v, f;} e[100010];int tot = 1;
vector<int> ve[100010];
void adde(int u, int v, int f = 1){
    e[++tot] = {v, f};ve[u].emplace_back(tot);
    e[++tot] = {u, 0};ve[v].emplace_back(tot);
}
int N, S, T; void init(int n, int s, int t){N = n, S = s, T = t;}

EK 算法

全称为 \(Edmonds−Karp\) 增广路算法

时间复杂度为 \(O(nm^2)\),但一般足够处理 \(n\)\(10^3\sim 10^4\) 规模的网络

模板:

namespace EK_algo{
    #define int long long
    constexpr int inf = 1e15;
    struct Eg{int v, f;} e[100010];int tot = 1;vector<int> ve[100010];
    void adde(int u, int v, int f = 1){
        e[++tot] = {v, f};ve[u].emplace_back(tot);
        e[++tot] = {u, 0};ve[v].emplace_back(tot);
    }
    int N, S, T; void init(int n, int s, int t){N = n, S = s, T = t;}
    int pr[100010], ds[100010];bool vs[100010];int Q[100010], HD, TL;
    bool bfs(){
        fill_n(vs + 1, N, 0);fill_n(ds + 1, N, 0);fill_n(pr + 1, N, 0);vs[Q[HD = TL = 1] = S] = 1;ds[S] = inf;
        while (HD <= TL){int u = Q[HD++];for (int i : ve[u]){
            int v = e[i].v;if (e[i].f && !vs[v])vs[v] = 1, ds[v] = min(ds[u], e[pr[Q[++TL] = v] = i].f);}} return ds[T];
    }
    int max_flow()
    {int ret = 0;while (bfs()){ret += ds[T];for (int u = T; u ^ S; u = e[pr[u] ^ 1].v)e[pr[u]].f -= ds[T], e[pr[u] ^ 1].f += ds[T];}return ret;}
}

Dinic 算法

本质为 \(EK\) 算法的优化

时间复杂度 \(O(n^2m)\)(使用了当前弧优化),一般可以处理 \(10^4\sim 10^5\) 级别的网络

若网络为单位容量(即每条边的容量为 \(1\))的,则可以 证明 其时间复杂度为 \(O(m\min(m^{\frac12},n^{\frac23}))\)

若网络为单位容量的,且除了源汇以外每个点出度等于 \(1\) 或入度等于 \(1\),则可以证明时间复杂度为 \(O(m\sqrt n)\)(这也是网络流求二分图匹配的时间复杂度来源)

模板:

namespace Dinic_algo{
    #define int long long
    constexpr int inf = 1e15;
    struct Eg{int v, f;} e[100010];int tot = 1;vector<int> ve[100010];
    void adde(int u, int v, int f = 1){
        e[++tot] = {v, f};ve[u].emplace_back(tot);
        e[++tot] = {u, 0};ve[v].emplace_back(tot);
    }
    int N, S, T; void init(int n, int s, int t){N = n, S = s, T = t;}
    int d[100010], Q[100010], HD, TL, nw[100010];
    int bfs(){
        fill_n(nw + 1, N, 0);fill_n(d + 1, N, 0);Q[HD = TL = 1] = S;d[S] = 1;
        while (HD <= TL){int u = Q[HD++];for (int i : ve[u])if (e[i].f && !d[e[i].v])d[Q[++TL] = e[i].v] = d[u] + 1;}return d[T];
    }
    int dfs(int k, int mnf){
        if (k == T)return mnf;int ret = 0;
        for (auto it = ve[k].begin() + nw[k]; it != ve[k].end() && mnf; ++it){nw[k] = it - ve[k].begin(); int i = *it, v = e[i].v;
            if (e[i].f && d[v] == d[k] + 1){int S = dfs(v, min(mnf, e[i].f));if (!S)d[v] = inf;e[i].f -= S, ret += S, e[i ^ 1].f += S, mnf -= S;}}
        return ret;
    }
    int max_flow(){int ret = 0;while (bfs())ret += dfs(S, inf);return ret;}
}

最小割

根据最大流最小割定理,网络的最小割等于最大流

若要求出具体方案,则在残量网络上从 \(S\) 开始,只走未满流边,标记所有可达点,所有由可达点连向不可达点的满流边的集合即为一种最小割(要忽略自己建的反向边)

若要最小化割边数量,则将残量网络上满流边容量设为 \(1\),非满流边权设为 \(\infty\),新网络的最小割即为原网络最小割边数量

费用流

一般为最小费用最大流

模板题:P3381 【模板】最小费用最大流

在建反向边时将反向边的费用设为正向边的相反数,并将 \(EK\) 算法或 \(Dinic\) 算法的 \(bfs\) 改为 \(spfa\)(边权为费用,即找到单位容量费用最小的路径)即可,一般选用 \(EK\),因为实现更加简单

时间复杂度 \(O(nmf)\),其中 \(f\) 为最大流,可以构造特殊网络卡到 \(O(n^32^{n/2})\),但在随机数据下效率很高

模板:

namespace min_cost{
    #define int long long
    constexpr int inf = 1e15;int n, s, t;void init(int N, int S, int T){n = N, s = S, t = T;}
    struct Eg{int to, fl, cst;} e[50010 * 2]; int tot = 1;vector<int> ve[5010];
    void adde(int u, int v, int w, int c){e[++tot] = {v, w, c};ve[u].emplace_back(tot);e[++tot] = {u, 0, -c};ve[v].emplace_back(tot);}
    int ds[5010], mnf[5010], pre[5010];int Q[5000010], HD, TL;bool inq[5010];
    int spfa(){
        fill(ds + 1, ds + n + 1, inf);ds[s] = 0, mnf[s] = inf;Q[HD = TL = 1] = s; inq[s] = 1;
        while (HD <= TL){int u = Q[HD++];inq[u] = 0;for (int i : ve[u]){int v = e[i].to;if (e[i].fl && ds[v] > ds[u] + e[i].cst){
            ds[v] = ds[u] + e[i].cst;mnf[v] = min(mnf[u], e[i].fl);pre[v] = i; if (!inq[v])inq[v] = 1, Q[++TL] = v;}}}
        return ds[t] == inf? 0 : ds[t];
    }
    pair<int, int> flow()/*min cost, max flow*/{int mnc = 0, mxf = 0;while (spfa()){mxf += mnf[t], mnc += mnf[t] * ds[t];
        for (int k = t; k; k = e[pre[k] ^ 1].to)e[pre[k]].fl -= mnf[t], e[pre[k] ^ 1].fl += mnf[t];}return make_pair(mnc, mxf);}
}

若要求最大费用最大流,在求最小费用最大流时费用取反即可

模板:

namespace max_cost{
    #define int long long
    constexpr int inf = 1e15;int n, s, t;void init(int N, int S, int T){n = N, s = S, t = T;}
    struct Eg{int to, fl, cst;} e[50010 * 2]; int tot = 1;vector<int> ve[5010];
    void adde(int u, int v, int w, int c){e[++tot] = {v, w, c};ve[u].emplace_back(tot);e[++tot] = {u, 0, -c};ve[v].emplace_back(tot);}
    int ds[5010], mnf[5010], pre[5010];int Q[5000010], HD, TL;bool inq[5010];
    int spfa(){
        fill(ds + 1, ds + n + 1, inf);ds[s] = 0, mnf[s] = inf;Q[HD = TL = 1] = s; inq[s] = 1;
        while (HD <= TL){int u = Q[HD++];inq[u] = 0;for (int i : ve[u]){int v = e[i].to;if (e[i].fl && ds[v] > ds[u] - e[i].cst){
            ds[v] = ds[u] - e[i].cst;mnf[v] = min(mnf[u], e[i].fl);pre[v] = i;if (!inq[v])inq[v] = 1, Q[++TL] = v;}}} ds[t] = -ds[t]; return ds[t];
    }
    pair<int, int> flow()/*min cost, max flow*/{int mnc = 0, mxf = 0;while (spfa() ^ -inf){mxf += mnf[t], mnc += mnf[t] * ds[t];
        for (int k = t; k; k = e[pre[k] ^ 1].to)e[pre[k]].fl -= mnf[t], e[pre[k] ^ 1].fl += mnf[t];}return make_pair(mnc, mxf);}
}

上下界网络流

每条边的容量有上下界,可能存在无解情况

无源汇上下界可行流

对于原网络上 \(u\)\(v\) 上界为 \(c\) ,下界为 \(b\) 的边,新网络上建立 \(u\)\(v\) 容量 \(c-b\) 的边,\(u\)\(T'\) 容量 \(b\) 的边,\(S'\)\(v\) 容量 \(b\) 的边,其中 \(S'\)\(T'\) 为新增的源汇点,新网络上跑最大流,若起点的所有出边都满流,则存在可行流

模板:

namespace Dinic_algo {
    //...
}
namespace nst_bound_flow{//无源汇上下界可行流
    int S, T; int sm;
    void init(int n){Dinic_algo::init(n + 2, S = n + 1, T = n + 2);sm = 0;}
    void adde(int u, int v, int b, int c){Dinic_algo::adde(u, v, c - b);if (b){Dinic_algo::adde(u, T, b);Dinic_algo::adde(S, v, b);sm += b;}}
    bool hsflr(){int F = Dinic_algo::max_flow();return F == sm;}
}

有源汇上下界可行流

按无源汇上下界可行流连边后,从 \(T\)\(S\)(不是无源汇上下界可行流中新建的源汇点)连容量 \(\infty\) 的边即可

这组可行流等于 \(T\)\(S\) 边的流量

模板:

namespace nst_bound_flow{
    //...
}
namespace st_bound_flow{//有源汇上下界可行流
    void init(int n, int S, int T){nst_bound_flow::init(n);nst_bound_flow::adde(T, S, 0, inf);}
    void adde(int u, int v, int b, int c){nst_bound_flow::adde(u, v, b, c);}
    bool hsflr(){return nst_bound_flow::hsflr();}
}

有源汇上下界最大流

模板题:Shoot the Bullet|东方文花帖|【模板】有源汇上下界最大流

先求出一组可行流,然后在残量网络上(删去新增的边)从 \(S\)\(T\)(不是无源汇上下界可行流中新建的源汇点)跑最大流,原网络的有源汇上下界最大流即为两次的流量之和

模板:

namespace st_bound_flow{
    //...
}
namespace st_bound_max_flow{//有源汇上下界最大流
    int s, t;
    void init(int n, int S, int T){s = S, t = T;st_bound_flow::init(n, s, t);}
    void adde(int u, int v, int b, int c){st_bound_flow::adde(u, v, b, c);}
    int max_flow(){
        if (!st_bound_flow::hsflr())return -1;
        int fl3 = Dinic_algo::Eds[3].f;//not nst_bound_flow::tmprs
        Dinic_algo::S = s;Dinic_algo::T = t;
        Dinic_algo::Eds[2].f = Dinic_algo::Eds[3].f = 0;//del s->t
        return fl3 + Dinic_algo::max_flow();
    }
}

有源汇上下界最小流

先求出一组可行流,然后在残量网络上(删去新增的边)从 \(T\)\(S\)(不是无源汇上下界可行流中新建的源汇点)跑最大流,原网络的有源汇上下界最小流即为两次的流量之差

模板:

namespace st_bound_flow{
    //...
}
namespace st_bound_min_flow{//有源汇上下界最小流
    int s, t;
    void init(int n, int S, int T){s = S, t = T;st_bound_flow::init(n, s, t);}
    void adde(int u, int v, int b, int c){st_bound_flow::adde(u, v, b, c);}
    int min_flow(){
        if (!st_bound_flow::hsflr())return -1;
        int fl3 = Dinic_algo::Eds[3].f;//not nst_bound_flow::tmprs
        Dinic_algo::S = t;Dinic_algo::T = s;
        Dinic_algo::Eds[2].f = Dinic_algo::Eds[3].f = 0;//del s->t
        return fl3 - Dinic_algo::max_flow();
    }
}

有源汇上下界 最小费用/最大费用 可行流/最大流

对有源汇上下界可行流/最大流进行修改,修改方式类似最大流到费用流,注意此时新建的边费用全为 \(0\)

模板:

#define int long long
int inf = 1e16;
namespace EK_algo{
    struct edge{int v, f, Cs;} Eds[100010];vector<int> ed[10010];
    int N, M, S, T;void init(int n, int m, int s, int t){N = n, M = m, S = s, T = t;}
    void adde(int u, int v, int w, int Cs)
    {static int tot = 1;Eds[++tot] = {v, w, Cs};ed[u].emplace_back(tot);Eds[++tot] = {u, 0, -Cs};ed[v].emplace_back(tot);}
    tuple<int, int, vector<int> > _spfa(){
        vector<int> ds(N + 1, inf), pr(N + 1), mnf(N + 1);queue<int> Q;vector<bool> inq(N + 1);Q.emplace(S);inq[S] = 1;mnf[S] = inf;ds[S] = 0;
        while (Q.size()){int t = Q.front();Q.pop();inq[t] = 0;for (int i : ed[t]){auto &[v, f, C] = Eds[i];
            if (f && ds[v] > ds[t] + C){ds[v] = ds[t] + C;pr[v] = i;mnf[v] = min(mnf[t], f);if (!inq[v])inq[v] = 1, Q.emplace(v);}}}
        return make_tuple(mnf[T], ds[T], pr);
    }
    void _upd(const vector<int> &pr, int flw){int nw = T;while (nw != S){int P = pr[nw];Eds[P].f -= flw;Eds[P ^ 1].f += flw;nw = Eds[P ^ 1].v;}}
    int max_flow(){int ans = 0;vector<int> pr;int flw, ds;while (tie(flw, ds, pr) = _spfa(), flw)_upd(pr, flw), ans += flw * ds;return ans;}
}
namespace nst_bound_flow{//无源汇最小费用上下界可行流
    int S, T;using EK_algo::max_flow;void init(int n){EK_algo::init(n + 2, 0, S = n + 1, T = n + 2);}
    void adde(int u, int v, int b, int c, int cst){EK_algo::adde(u, v, c - b, cst);EK_algo::adde(u, T, b, 0);EK_algo::adde(S, v, b, 0);}
}
namespace st_bound_flow
{using namespace nst_bound_flow;void init(int n, int S, int T){nst_bound_flow::init(n);adde(T, S, 0, inf, 0);}}//有源汇最小费用上下界可行流

存在负圈的费用流

模板题:P7173 【模板】有负圈的费用流

处理方式类似上下界网络流,将所有负权边强制满流,然后两次费用流

模板:

#define int long long
constexpr int inf = 1e15;
namespace min_cost{
    int n, s, t, tot;struct Eg{int to, fl, cst;} e[50010 * 2];vector<int> ve[5010];void init(int N, int S, int T){n = N, s = S, t = T;tot = 1;}
    void adde(int u, int v, int w, int c){e[++tot] = {v, w, c};ve[u].emplace_back(tot);e[++tot] = {u, 0, -c};ve[v].emplace_back(tot);}
    int ds[5010], mnf[5010], pre[5010];int Q[5000010], HD, TL;bool inq[5010];
    bool spfa(){fill_n(ds + 1, n, inf);ds[s] = 0, mnf[s] = inf;Q[HD = TL = 1] = s;inq[s] = 1;
        while (HD <= TL){int u = Q[HD++];inq[u] = 0;for (int i : ve[u]){int v = e[i].to;if (e[i].fl && ds[v] > ds[u] + e[i].cst){
            ds[v] = ds[u] + e[i].cst;mnf[v] = min(mnf[u], e[i].fl);pre[v] = i;if (!inq[v])inq[v] = 1, Q[++TL] = v;}}}return ds[t] ^ inf;}
    pair<int, int> flow()/*min cost, max flow*/{int mnc = 0, mxf = 0;while (spfa()){mxf += mnf[t], mnc += mnf[t] * ds[t];
        for (int k = t; k ^ s; k = e[pre[k] ^ 1].to)e[pre[k]].fl -= mnf[t], e[pre[k] ^ 1].fl += mnf[t];}return make_pair(mnc, mxf);}
}
namespace mn_min_cost {//有负圈的费用流
    int M_S, M_T; int offs; int S, T;
    void init(int n, int s, int t){min_cost::init(n + 2, M_S = n + 1, M_T = n + 2);S = s, T = t;min_cost::adde(T, S, inf, 0);offs = 0;}
    void adde(int u, int v, int w, int c)
    {if (c >= 0)min_cost::adde(u, v, w, c);else {offs += w * c;min_cost::adde(M_S, v, w, 0);min_cost::adde(u, M_T, w, 0);min_cost::adde(v, u, w, -c);}}
    pair<int, int> flow()
    {int Csts = offs;auto [Cst, Flw] = min_cost::flow();Csts += Cst;min_cost::s = S, min_cost::t = T;tie(Cst, Flw) = min_cost::flow();return make_pair(Csts + Cst, Flw);}
}//参考:https://www.luogu.com.cn/article/2jl99kzp

最大权闭合子图

定义带点权有向图的闭合子图为原图点集的一个子集,满足该点集内点的所有出边指向的点都在点集内

定义闭合子图的权值为子图中的点的点权之和

可以网络流求解

源点向正权点连边,容量为点权;负权点向汇点连边,容量为点权绝对值;\(0\) 权点任意;原图的边容量都为 \(\infty\)

可证正权点点权之和减去最小割即为最大权闭合子图权值(割开连接 \(S\) 的相当于舍弃正权点,割开连接 \(T\) 的相当于加入负权点)

最大权闭合子图为从源点开始残量网络上能到达的所有点

建模

例 1:[ABC285G] Tatami

给定 \(n\times m\) 的矩阵,每个位置为 \(1/2/?\),要用 \(1\times 1\)\(1\times 2\)(可以选择)的单元覆盖整个矩形,满足 \(1\) 位置只能被 \(1\times 1\) 的覆盖,\(2\) 位置只能被 \(1\times 2\) 的覆盖,\(?\)位置任意,判断是否存在合法方案,\(n,m\le300\)

相当于用 \(1\times 2\) 的单元覆盖所有 \(2\) 位置,且不能覆盖 \(1\) 位置

将矩阵黑白染色,分为两部分,相邻的非 \(1\) 位置之间连边,则转化为判断二分图是否存在一种匹配,满足所有的 \(2\) 都被匹配了

无法直接求,但是若将 \(?\) 位置之间两两连边,并将左右点数补成相同(补的点看做 \(?\)),则转化为判断是否存在完美匹配

但是直接两两连边边数为 \(O((nm)^2)\)

可以在中间增加一个点,左右都和其连边

总时间复杂度 \(O(nm\sqrt{nm})\)

代码

例 2:CF708D Incorrect Flow

给定一张 \(n\)\(m\) 边的有源汇网络,每次可以花 \(1\) 的代价使某条边的流量 \(f\) 或容量 \(c\) 加减一,求要使其合法,最少需要花费的代价,\(n,m\le100,c,f\le10^6\)

对于原图的边 \((u,v,c,f)\),首先需要 \(u\)\(v\) 连容量上下界都是 \(f\),费用为 \(0\) 的边,然后考虑连修改的边:

  • \(f\le c\),则
    • 可以花 \(1\) 的代价使流量减小,相当于从 \(v\)\(u\) 退流,且最多减小 \(c\) 次,因此 \(v\)\(u\) 连容量上界为 \(f\),费用为 \(1\) 的边
    • 可以花 \(1\) 的代价增大流量,最多增加 \(f-c\) 次,从 \(u\)\(v\) 连容量上界为 \(f-c\),费用为 \(1\) 的边
    • 流量等于 \(c\) 后,可以花 \(2\) 的代价使容量和流量都加一,因此 \(u\)\(v\) 连容量上界 \(\infty\),费用为 \(2\) 的边(显然最小费用最大流会优先选择费用小的边,因此不用考虑和前一种冲突)
  • \(f>c\),则至少需要花 \(f-c\) 的代价令 \(f=c\),因此\(f-c\) 先计入总答案
    • 此时的 \(f\)\([c,f]\) 的范围内,即在流量等于 \(f\) 的基础上,可以令流量免费减少不超过 \(f-c\),因此 \(v\)\(u\) 连容量为 \(f-c\),费用为 \(0\) 的边
    • 然后可以花 \(1\) 的代价继续减小,从 \(v\)\(u\) 连容量为 \(c\),费用为 \(1\) 的边(同样会优先选择上一种,不需要考虑冲突)
    • 或者花 \(2\) 的代价加一(显然最优解不会先减少再增加),\(u\)\(v\) 连容量为 \(\infty\),费用为 \(2\) 的边

然后跑有源汇上下界最小费用可行流即可

时间复杂度 \(\def\la{\langle}\def\ra{\rangle} O(\la N\ra \la M\ra \la F\ra)=O(n(n+m)mv)\),但基本卡不满

代码

例 3:[ABC239G] Builder Takahashi

求图的最小割(点),\(n\le 100\)

拆点,原本的点 \(i\) 拆为 \(i\)\(i+n\),其中 \(i\) 为入点,\(i+n\) 为出点,\(i\)\(i+n\) 连边,边权为删去点 \(i\) 的代价;原图的边 \(u-v\) 在新图上连 \(u-(v+n)\)\(v-(u+n)\),边权为无穷大;然后求新图上 \(n+1\)\(n\) 的最小割即可

时间复杂度 \(O(n^2m)\)

代码

例 4:[ABC274G] Security Camera 3

\(n\times m\) 的网格,一部分位置有障碍物,可以放置监控(一个位置可以放置多个),一个监控可以覆盖它正方向(四个方向之一)的所有位置(包括自己的位置,但不能穿越障碍物),求覆盖所有无障碍位置所需的最小监控数量,\(n,m\le300\)

对于每行和每列,分别求出所有极长连续无障碍子段。显然一个这样的子段最多放置一个监控

对于位置 \((i,j)\),显然它所在的行极长连续无障碍子段列极长连续无障碍子段中,至少一个需要放置监控

因此在两者之间连边,求出得到的二分图的最小边覆盖即可

时间复杂度 \(O(nm\sqrt {nm})\)

代码

例 5:[ABC317G] Rearranging

\(n\)\(m\) 列的矩阵(\(1\sim n\) 每个数恰好出现 \(m\) 次),可以重排每一行,使得最后得到的矩阵每一列都是 \(1\sim n\) 的排列,求是否存在方案并给出具体方案,\(n,m\le100\)

建立 \(n\) 个左部点分别表示每一行,建立 \(n\) 个右部点分别表示每种值,对于每个位置在它所在行对应的点和它的值对应的点之间连边

则相当于在得到的图上求 \(m\) 次完美匹配,且每次用的边不能重复

每次将匹配过的点删去并重置源点到左部点、右部点到汇点的边即可

时间复杂度 \(O(m\cdot mn\cdot \sqrt n)=O(m^2n^{3/2})\)

代码

例 6:[AGC034D] Manhattan Max Matching

\((RX_i,RY_i)\) 上有 \(RC_i\) 个红点,\((BX_i,BY_i)\) 上有 \(BC_i\) 个蓝点,其中 \(1\le i\le n\),满足 \(\sum RC_i=\sum BC_i\)。要将其配对,配对的价值为两个点的曼哈顿距离,求最大价值。\(n\le1000,RC_i,BC_i\le10,RX_i,RY_i,BX_i,BY_i\le10^9\)

\((x_1,y_1)\)\((x_2,y_2)\) 之间的曼哈顿距离为

\[\begin{aligned} &|x_1-x_2|+|y_1-y_2|\\ =&\max(x_1-x_2,x_2-x_1)+\max(y_1-y_2,y_2-y_1)\\ =&\max(x_1-x_2+y_1-y_2,x_1-x_2+y_2-y_1,x_2-x_1+y_1-y_2,x_2-x_1+y_2-y_1)\\ =&\max((x_1+y_1)+(-x_2-y_2),(x_1-y_1)+(-x_2+y_2),(-x_1+y_1)+(x_2-y_2),(-x_1-y_1)+(x_2+y_2))\\ \end{aligned}\]

于是红点作为左部点,蓝点作为右部点,中间建立四个虚点,左右分别和其连边(注意顺序),求多重最大权匹配即可(显然求最大权匹配时,会选四条边种最大的一个)

理论时间复杂度为 \(O(n^2f)\),但实际跑不满

代码

例 7:P6062 [USACO05JAN] Muddy Fields G

基本和 [ABC274G] Security Camera 3 相同

代码

例 8:CF1404E Bricks

类似 P6062 [USACO05JAN] Muddy Fields G,但是区域不能重叠,求最少数量,\(n,m\le200\)

定义分割线为分割两个向邻的可放置格子的线,定义选择分割线为最终方案中此分割线两侧分属不同区域,定义分割线相邻当且仅当两者有公共点

显然相邻的一对分割线中至少选择一个

相邻分割线中一定是一个横向,一个纵向

因此建立二分图,横向分割线对应左部点,纵向对应右部点,两线相邻则连边

由于最终区域数量要最少,因此选择的分割线最少,即不选的最多,而显然不选的分割线组成二分图的一个独立集,因此问题转化为求二分图的最大独立集

时间复杂度 \(O(nm\sqrt {nm})\)

代码

例 9:[AGC037D] Sorting a Grid

给定 \(n\times m\) 的矩阵 \(A\)(值为 \(1\sim n\times m\) 的排列),行内重排得到矩阵 \(B\)\(B\) 列内重排得到矩阵 \(C\)\(C\) 行内重排得到矩阵 \(D\),其中 \(D_{i,j}=(i-1)m+j\),求一组可能的 \((B,C)\)\(n,m\le100\)

左中右三部分点,左侧 \(n\) 个点表示 \(A\)\(B\) 的行,右侧 \(n\) 个点表示 \(C\)\(D\) 的行,中间 \(n\times m\) 个点表示 \(B\)\(C\) 的值

\(A_{i,j}=x\),则左侧第 \(i\) 个点向中间第 \(x\) 个点连边

\(D_{i,j}=y\),则中间第 \(y\) 个点向右侧第 \(i\) 个点连边

源点向左侧点连边,右侧点向汇点连边

所有边的容量都是 \(1\)

进行 \(m\) 次匹配,每次确定 \(B\)\(C\) 的一列(每次匹配后删去已经匹配的边,重置源汇的边),可证每次都为完美匹配

\(x\) 次匹配中 \(i-v-j\)(其中 \(i\) 为左侧点编号,\(j\) 为中间,\(k\) 为右侧)表示 \(A\) 的第 \(i\) 行有值 \(v\)\(B\)\(i\) 行第 \(x\) 个为 \(v\)\(C\)\(j\) 行第 \(x\) 个为 \(v\)\(D\)\(j\) 行有值 \(v\)

可证其正确性

时间复杂度 \(O(m\cdot nm\sqrt{nm}=nm^2\sqrt{nm})\)

代码

例 10:CF704D Captain America

给定平面上 \(n\) 个点,要给每个点染红色或黑色,\(m\) 条限制 \((1/2,l,x)\),表示直线 \(x=l/y=l\) 上两种颜色点数量之差不超过 \(x\),求一种染色方案,使黑点数量乘以 \(b\) 加红点数量乘以 \(r\) 最小,或判定无解,\(n,m\le10^5,x,y\le10^9\)

先对坐标离散化

假定 \(r\le b\)(若 \(r>b\) 交换即可,但输出方案时要注意)

若没有限制,则全染红最优

\(xct_i\) 表示 \(x=i\) 的点的数量,对于限制 \((1,l,x)\),相当于要求直线 \(x=l\) 上红点数量在 \(\left[\max\left(0,\left\lceil\dfrac{l-xct_i}2\right\rceil\right),\min\left(xct_i,\left\lfloor\dfrac{l+xct_i}2\right\rfloor\right)\right]\) 范围内(若某一条限制对应的区间为空则直接判无解)

\((2,l,x)\) 同理

问题转化为在给定点集中选出一个子集,使得其满足 \(m\) 条形如直线 \(x=l/y=l\) 上选出的数量在 \([L,R]\)的要求

对于每个 \(x\) 坐标建立一个左部点,每个 \(y\) 坐标建立右部点,源点向左部点连边的上下界为该左部点对应 \(x\) 坐标最紧的限制,右部点向汇点的边的上下界为该右部点对应 \(y\) 坐标最紧的限制

对于每个点,在其 \(x\) 坐标对应左部点和 \(y\) 坐标对应右部点之间连上下界为 \([0,1]\) 的边

则得到的图的上下界可行流即为一组解(若没有可行流则判无解)

由于要权值最小,因此红点最多,求上下界最大流即可

时间复杂度 \(O((n+m)\sqrt{n})\)

代码

例 11:CF1592F2 Alice and Recoloring 2

给定一个 \(n\times m\)\(0/1\) 矩阵 \(s\),每次用 \(1\) 的代价翻转包含 \((1,1)\) 的子矩阵,用 \(2\) 的代价翻转包含 \((n,m)\) 的子矩阵,用 \(3\) 的代价翻转包含 \((n,1)\) 的子矩阵,用 \(4\) 的代价翻转包含 \((1,m)\) 的子矩阵,求使原矩阵全 \(0\) 的最小代价,\(n,m\le500\)

一次 \(3\) 操作可以转化为两次 \(1\) 操作,且代价更小,因此最优解一定没有 \(3\) 操作;同理也没有 \(4\) 操作

因此转化只有为翻转左上角和翻转右下角两种操作

\(a_{i,j}=s_{i,j}\oplus s_{i+1,j}\oplus s_{i,j+1}\oplus s_{i+1,j+1}\)(假定 \(s\) 给定范围外全为 \(0\)

\(1\) 操作相当于 \(a\) 单点翻转,\(2\) 操作相当于同时翻转 \(a_{i,j},a_{n,j},a_{i,m},a_{n,m}\),其中 \(1\le i<n,1\le j<m\)

可证至少存在一种最优方案,其中对 \((i,j)\) 进行 \(2\) 操作的必要条件为 \(a_{i,j},a_{n,j},a_{i,m}\) 同时为 \(0\),且所有进行 \(2\) 操作的点的横纵坐标互不相同

建立二分图,左侧 \(n-1\) 个点表示 \(x\) 坐标,右侧 \(m-1\) 个点表示 \(y\) 坐标,若 \((i,j)\) 满足上述要求,则对应两点连边

显然 \(2\) 操作越多,剩下需要 \(1\) 操作处理的点越少,因此求图的最大匹配,设为 \(k\)

\(a_{n,m}\) 异或 \(k\bmod 2\),设 \(a\)\(1\) 的数量为 \(S\),则答案为 \(S-k\)

时间复杂度 \(O((n+m)\sqrt{nm})\)

代码

例 12:CF802C Heidi and Library (hard)

一个容量为 \(k\) 的容器,每次可以花 \(c_i\) 的代价放入一个 \(i\) 类物品,或取出其中一个物体,要求第 \(i\) 时刻必须有 \(a_i\) 号物体(同一时刻可以多次操作),求最小代价,\(1\le a_i\le n\le80\)

每个元素在容器中的时间为若干区间

先假设每个元素在第一次需要时放入,最后一次需要之后移除,然后考虑每时刻容量限制对答案的增量

即先选出整个区间,然后将整个区间划分为若干子区间,以减小容量

每时刻最多可以保留 \(k\) 个元素到下一时刻,因此 \(i\)\(i+1\) 连容量 \(k\) 费用 \(0\) 的边

设上一个与 \(i\) 时刻需要的元素相同的时刻为 \(pr_i\)(若不存在则为源点),则可以将 \(a_i\) 的整个区间在 \(pr_i\)\(i\) 之间划分开,需要花 \(c_{a_i}\) 的代价重新放入,为了避免 \(i\) 时刻放入再取出而不记容量,假设是在前一时刻放入的,\(pr_i\)\(i-1\)(为了避免出现 \(0\) 时刻,若 \(i=1\) 则不需要减 \(1\),因为此时一定是源点向其连边)连容量 \(1\) 费用 \(c_{a_i}\) 的边

对于每种元素 \(i\),设其最后出现时间为 \(t_i\)(若没出现则忽略),则要将 \(i\) 各个区间的贡献计入答案,\(t_i\) 向汇点连容量 \(1\) 费用 \(0\) 的边

总点数 \(n+2\),总边数 \(O(n)\)

时间复杂度 \(O(nmf)\)

代码

参考

输出方案:CF132E Bits of merry old England \(\quad\) 代码

例 13:P2045 方格取数加强版

\(n\times n\) 的矩阵 \(a\),选择 \(k\)\((1,1)\)\((n,n)\) 的路径,最大化路径经过的点的总权值(经过多次只算一次权值),\(n\le 50,k\le 10,0\le a_{i,j}\le1000\)

相当于一张网格图上选择不超过 \(k\) 条路径,最大化路径覆盖到的点的总权值和

\(n\times n\) 个格子分别建立入点和出点,格子 \((i,j)\) 对应的入点向出点连两条边,一条容量 \(1\) 费用 \(a_{i,j}\),表示可以选择 \(1\) 次,一条容量 \(k-1\) 费用 \(0\),表示剩下至多 \(k-1\) 次经过都没有收益

每个格子的出点向其下方和右侧相邻格子的入点连容量 \(k\) 费用 \(0\) 的边

源点为格子 \((1,1)\) 的入度,汇点为格子 \((n,n)\) 的出点

跑最大费用最大流即可,时间复杂度 \(O(NMF)=O(n^2\times n^2\times k)=O(n^4k)\)

代码

vjudge 练习

Bipartite Graph & Flow

题单

24.12.9 BpGr&Fl

网络流与线性规划 24 题

posted @ 2024-12-14 13:45  Hstry  阅读(57)  评论(0)    收藏  举报