匹配问题

匹配问题

1. 算法分析

匹配问题可以转换为二分匹配来处理,匈牙利算法求解;选择问题中有部分可以转换为点覆盖集问题,选择a或者b,那么a<->b,然后每条边只能选择其中一个。

1.1 几个重要概念

1.交替路:从一个未匹配点出发,依次经过非匹配边、匹配边、非匹配边…形成的路径叫交替路
2.增广路:从一个未匹配的点出发,走交替路,如果途径另一个未匹配点,则这条交替路称为增广路

1.2 二分图判定

一张图是二分图当且仅当图中没有奇数环,二分图必然不含有奇数环

1.3 二分图点覆盖、独立集和最小路径点覆盖

最大匹配数=最小点覆盖=总点数-最大独立集=总点数-最小路径点覆盖

1.3.1 二分图的点覆盖

最小点覆盖: 求一个最小的点集S,使得图中任意一条边都有至少一个端点属于S。
定理: 最小覆盖集中点数目等于二分图的最大匹配包含的边数

1.3.2 二分图的独立集

定义: 一个独立集内的点没有连边
定理: 一个二分图的最大独立集内点个数等于总个数n-最大匹配个数
最大团: 一个点集的所有点间都有边相连,一张图G的最大独立集的补集就是最大团

1.3.3 DAG的最小路径点覆盖

DAG的最小路径点覆盖是描述: 给定一张有向无环图,要求用尽量少的不相交的简单路径,覆盖有向无环图的所有顶点(也就是每个顶点恰好被覆盖一次)。
拆点: 把原图中的每个点拆分成两个点,比如i点拆成i点和i’点,把原来的连边和新点连接起来,比如说原图i->j,那么拆点后,把i和j’连边(i和j不用连边),即i->j’,这样的方式组成一张新图

Screenshot_20201224_133504.jpg

定理: 最小路径点覆盖的数目等于原先所有的点数n-新图最大二分匹配的数目

优化: i->j’的边可以直接建为i->j。由于这里新建完的图是二分图,左部图是<=n的点,右部图是>=n的点,那么本来i->j’的边可以直接建为i->j,也就是说把左部图里的点j放在右部图内,然后对于左右部图都做二分匹配,本来这样计算答案要除以2,但是左部图和右部图规模都减小了1/2,因此抵消了。

1.3.4 DAG的最小路径可重复点覆盖

1.利用传递闭包把DAG变成一张没有重复点的图
2.套用上述求DAG的最小路径覆盖的算法

1.3.5 有向环覆盖问题

问题描述: 给你一个N个顶点M条边的带权有向图,要你把该图分成一个或多个不相交的有向环(点和边都不相交)。且所有定点都被有向环覆盖。问你该有向环所有权值的总和最小是多少?

结论: 原图有向环最大权值覆盖=新图最优匹配。把任意一个顶点i都分成两个,即i和i’. 如果原图存在i->j的边,那么二分图有i->j’的边.然后跑最优匹配。

拓展: 如果,改为无向图,问你无向环最大权值覆盖?答案也是一样的。只是在建图的时候把有向改为无向即可。

进一步结论:

① 如果原图能由多个不相交的有向环覆盖,那么二分图必然存在完备匹配。他们互为充要条件,也就是说如果二分图存在完备匹配,那么原图必定能由几个不想交的有向环覆盖。因此如果只需要判定十分存在有向环覆盖,只需要判断跑最大匹配判断即可。

② 如果原图存在权值最大的有向环覆盖,那么二分图的最优匹配一定就是这个值。即权值最大的有向环覆盖在数值上等于改图的最优匹配值。

1.4 各类二分匹配问题

1.4.1 最大匹配

在一张无权的二分图中,一个图所有匹配中,所含匹配边数最多的匹配,称为这个图的最大匹配。找最大匹配数可以使用匈牙利算法实现 O ( n 2 ) O(n^2) O(n2),也可以使用最大流算法(dinic)解决 O ( n m 2 ) O(nm^2) O(nm2)

1.4.2 完美(备)匹配

在一张无权的二分图中,如果一个图的某个匹配中,所有的顶点都是匹配点,那么它就是一个完美匹配。完美匹配一定是最大匹配,但并非每个图都存在完美匹配。

1.4.3 最大权匹配/最优匹配

最优匹配就是指在带权边的二分图中,求一个匹配使得匹配边上的权值和最大。如果匹配是完美匹配,那么这个问题可以使用km算法来实现,它的算法复杂度是 O ( n 3 ) O(n^3) O(n3)。如果匹配不一定是完美匹配,那么将其转化为最小费用最大流来做,它的复杂度是 O ( n 2 m ) O(n^2m) O(n2m)

1.4.4 多重匹配

多重匹配就是解决一连多的问题.

1.4.4.1 多重最大匹配

在一张无权二分图中,找到一对多的最大匹配。使用网络流算法解决:在原图上建立源点S和汇点T,S向每个X方点连一条容量为该X方点L值的边,每个Y方点向T连一条容量为该Y方点L值的边,原来二分图中各边在新的网络中仍存在,容量为1(若该边可以使用多次则容量大于1),求该网络的最大流,就是该二分图多重最大匹配的值。也可以使用匈牙利算法解决,只需要一个点维护多个匹配点即可。

1.4.4.2 多重最优匹配

在一张带权二分图中,找到一对多的最大权匹配。使用网络流算法解决。在原图上建立源点S和汇点T,S向每个X方点连一条容量为该X方点L值、费用为0的边,每个Y方点向T连一条容量为该Y方点L值、费用为0的边,原来二分图中各边在新的网络中仍存在,容量为1(若该边可以使用多次则容量大于1),费用为该边的权值。求该网络的最大费用最大流,就是该二分图多重最优匹配的值。

1.5 一般图匹配

带花树算法 O ( n 3 ) O(n^3) O(n3)

2. 模板

2.1 染色法判断是否为二分图

#include <bits/stdc++.h>

using namespace std;

int const N = 1e5 + 10;
int e[N * 2], ne[N * 2], h[N], idx, color[N];  // color记录每个点颜色
int n, m;

// 建立邻接表
void add(int a, int b) {
   
    e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}

// 判断是否u号点可以打上c的颜色
int dfs(int u, int c) {
   
    color[u] = c;  // 给u号点打上c的颜色
    for (int i = h[u]; i != -1; i = ne[i]) {
     // 遍历所有与i号点相连的点
        int j = e[i];  // 看j号点
        if (!color[j]) {
     // 如果j号点没有染色
            if (!dfs(j, 3 - c)) return 0;  // 如果j号点染色3-c过程中失败
        }
        if (color[j] == c) return 0;  // 如果j号点也染色c颜色
    }
    return 1;  // 如果u号点的所有邻点都没有染色失败
}

int main() {
   
    memset(h, -1, sizeof h);  // 初始化h
    cin >> n >> m;  // 输入顶点数和边数
    for (int i = 0; i < m ; ++i) {
     // 读入边信息
        int a, b;
        scanf("%d %d", &a, &b);
        add(a, b), add(b, a);  // 无向边
    }
    int flg = 1;  // flg记录染色是否成功,成功为1,失败为0
    for (int i = 1; i <= n; ++i) {
     // 从1号点开始枚举
        if (!color[i]) {
     // 如果i号点为染色
            if (!dfs(i, 1)) {
     // 如果i号点染色失败
                flg = 0;  // flg 打上失败标记
                break;
            }
        }
    }
    if (flg) cout << "Yes\n";
    else cout << "No\n";
    return 0;
}

2.2 二分图最大匹配

2.2.1 匈牙利算法(O(VE))
#include <bits/stdc++.h>

using namespace std;

int const N = 110, M = 1010;
int e[M], ne[M], idx, h[N], match[N], st[N];  // match[j] = x: 右半部分的j匹配左半部分的x,st[j] = 1:右半部分的j匹配到人了
int n, m, k;

// 建立邻接表
void add(int a, int b) {
   
    e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}

/* 邻接矩阵写法
bool find(int x) {
    for (int i = 1; i <= n; ++i) {
        if (l[x][i]) continue;  // 没有边
        if (!st[i]) {
            st[i] = 1;
            if (!match[i] || find(match[i])) {
                match[i] = x;
                return true;
            }
        }
    }
    return false;
}
*/

// 找左半部分的x是否能够在右半部分找到匹配的对象
bool find(int x) {
   
    for (int i = h[x]; i != -1; i = ne[i]) {
    // 遍历所有与x之间相连的点
        int j = e[i];  // 点为j
        if (!st[j]) {
     // 如果j没有匹配过
            st[j] = 1;  // 记录j匹配过
            if (!match[j] || find(match[j])) {
     // 如果右半部分的j没有匹配到左半部分的人或者j匹配到的可以去匹配其他右半部分的人
                match[j] = x;  // 记录右半部分的j和左半部分的x匹配
                return true;  // 找到x的匹配对象
            }
        }
    }
    return false;  // 全部都遍历仍然没有成功,返回false
}

int hungary() {
   
    int res = 0;  // 记录结果数目
    for (int i = 1; i <= n; ++i) {
    // 从左半部分向右半部分匹配  
        memset(st, 0, sizeof st);  // 每次匹配时对右半部分的女生情况匹配情况
        if (find(i)) res++;  // 如果能够找到,res加一
    }
    return res;
}

int main() {
   
    while (scanf("%d %d %d", &n, &m, &k) != EOF && n != 0) {
   
        memset(h, -1, sizeof h);  // 初始化h
        memset(e, 0, sizeof e);
        memset(ne, 0, sizeof ne);
        memset(match, 0, sizeof match);
        idx = 0;
        for (int i = 0; i < k; ++i) {
     // 读入边信息
            int a, b, c;
            scanf("%d %d %d", &c, &a, &b);
            if (!a || !b) continue;  // 起始点是0,不用覆盖
            add(a, b);  // 只需要add一次即可,因为二分匹配时只需要从左半部分向右半部分匹配就行
        }

        cout << hungary() << endl;
    }
    
    return 0;
}
2.2.2 hopcroft-karp算法(O(sqrt(V) * E ))
#include <bits/stdc++.h>

using namespace std;

typedef long long LL;
typedef pair<int, int> PII;
int const MAXN = 3e3 + 10, MAXM = MAXN * MAXN, INF = 1e9 + 10;
int n, m, T, t, kase = 1;
int idx, h[MAXN], ne[MAXM], e[MAXM], xline[MAXN], yline[MAXN], dx[MAXN], dy[MAXN], st[MAXN], dis;

void add(int a, int b) {
   
    e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}

struct Node {
   
    int x, y, v;
}node[MAXN];

int check(int a, int x, int y) {
   
    double dis = sqrt((node[a].x - x) * (node[a].x - x) + (node[a].y - y) * (node[a].y - y));
    return dis/(double)node[a].v <= t;
}

int bfs() {
   
    queue<int> q;
    dis = INF;  // dis是全局变量
    memset(dx, -1, sizeof(dx));  // dx和dy每次bfs都需要初始化
    memset(dy, -1, sizeof(dy));
    for (int i = 1; i <= m; i++)   // 左部点
        if (xline[i] == -1) {
   
            q.push(i);
            dx[i] = 0;
        }
    while (!q.empty()) {
   
        int t = q.front();
        q.pop();
        if (dx[t] > dis) break;
        for (int i = h[t]; ~i; i = ne[i]) {
   
            int j = e[i];
            if (dy[j] == -1) {
   
                dy[j] = dx[t] + 1;
                if (yline[j] == -1) dis = dy[j];
                else {
   
                    dx[yline[j]] = dy[j] + 1;
                    q.push(yline[j]);
                }
            }
        }
    }
    return dis != INF;
}

int find(int t) {
   
    for (int i = h[t]; ~i; i = ne[i]) {
   
        int j = e[i];
        if (!st[j] && dy[j] == dx[t] + 1) {
   
            st[j] = 1;
            if (yline[j] != -1 && dy[j] == dis) continue;
            if (yline[j] == -1 || find(yline[j])) {
   
                yline[j] = t, xline[t] = j;
                return 1;
            }
        }
    }
    return 0;
}

int Maxmatch() {
     //最大匹配
    int res = 0;
    while (bfs()) {
   
        memset(st, 0, sizeof(st));
        for (int i = 1; i <= m; i++)  // 遍历左部图每个点
            if (xline[i] == -1 && find(i)) res++;
    }
    return res;
}

int main() {
   
    cin >> T;
    while(T--) {
   
        memset(xline, -1, sizeof(xline));  // 初始化左边标记
        memset(yline, -1, sizeof(yline));  // 初始化右边标记
        memset(h, -1, sizeof h);
        scanf("%d", &t);
        scanf("%d", &m);  // 左部m个点,右部n个点
        idx = 0;
        for (int i = 1; i <= m; ++i) scanf("%d%d%d", &node[i].x, &node[i].y, &node[i].v);
        scanf("%d", &n);
        for (int i = 1; i <= n; ++i) {
   
            int x, y;
            scanf("%d%d", &x, &y);
            for (int j = 1; j <= m; ++j) {
   
                if (check(j, x, y)) add(j, i);  // 如果能够建边
            }
        }
        printf("Scenario #%d:\n", kase++);
        cout << Maxmatch() << endl << endl;
    }   
    return 0;
}

2.3 二分图最优匹配(必须是完美匹配)

// 本板子是求最大权的最优匹配,如果是求最小权值的最优匹配,那么只需要将所有的边权取负值,然后再次跑km算法,最后答案取反即可。
#include <bits/stdc++.h>

using namespace std;

typedef long long LL;
LL const INF = 0x3f3f3f3f3f3f3f3f;
int const MAXN = 500;

int n;
LL g[MAXN][MAXN], hl[MAXN], hr[MAXN], slk[MAXN];
int fl[MAXN], fr[MAXN], pre[MAXN], vl[MAXN], vr[MAXN], q[MAXN], ql, qr;
LL a[MAXN], b[MAXN], p[MAXN], c[MAXN];

inline int check(int i) {
   
    vl[i] = 1;
    if (fl[i] != -1) {
   
        q[qr++] = fl[i];
        vr[fl[i]] = 1;
        return 1;
    }
    while (i != -1) {
   
        fl[i] = pre[i];
        swap(i, fr[fl[i]]);
    }
    return 0;
}

void bfs(int s) {
   
    for (int i = 1; i <= n; i++) vl[i] = vr[i] = 0, slk[i] = INF;
    for (vr[q[ql = 0] = s] = qr = 1;;) {
   
        for (LL d; ql < qr;) {
   
            for (int i = 1, j = q[ql++]; i <= n; i++) {
   
                if (!vl[i] && slk[i] >= (d = hl[i] + hr[j] - g[i][j])) {
   
                    pre[i] = j;
                    if (d)
                        slk[i] = d;
                    else if (!check(i))
                        return;
                }
            }
        }
        LL d = INF;
        for (int i = 1; i <= n; i++) {
   
            if (!vl[i] && d > slk[i]) d = slk[i];
        }
        for (int i = 1; i <= n; i++) {
   
            if (vl[i])
                hl[i] += d;
            else
                slk[i] -= d;
            if (vr[i]) hr[i] -= d;
        }
        for (int i = 1; i <= n; i++)
            if (!vl[i] && !slk[i] && !check(i)) return;
    }
}

LL KM() {
   
    for (int i = 1; i <= n; i++) fl[i] = fr[i] = -1, hr[i] = 0;  // 初始化标杆:左标杆fl,右标杆fr
    for (int i = 1; i <= n; i++) hl[i] = *max_element(g[i] + 1, g[i] + n + 1);  // 一开始左标杆的值
    for (int j = 1; j <= n; j++) bfs(j);  // O(n^3)的bfs解法
    LL re = 0;
    for (int i = 1; i <= n; i++)
        if (g[i][fl[i]])
            re += g[i][fl[i]];
        else
            fl[i] = 0;
    return re;
}

int main() {
   
    while (scanf("%d", &n) != EOF) {
   
/* 如果是求最小权值的最优匹配,这里建图要这样初始化
        for (int i = 1; i <= n; ++i)
            for (int j = 1; j <= n; ++j) 
                g[i][j] = -INF;
*/
        for (int i = 1; i <= n; ++i)  // 读入任意两个点的权值关系
            for (int j = 1; j <= n; ++j) scanf("%lld", &g[i][j]);
        // 如果是求最小权值的最优匹配,那么这里g[i][j] = -g[i][j]
        LL ans = KM();
        printf("%lld\n", ans);  // 如果是求最小权值的最优匹配,这里ans = -ans
    }
    return 0;
}

2.4 二分图多重匹配

#include <iostream>
#include <cstring>
#include <cstdio>
#include <algorithm>

using namespace std;

const int MAXN = 1010;
int st[MAXN], g[MAXN][MAXN], n, m;

struct NODE {
   
    int cnt;
    int matchs[MAXN];
} match[MAXN];

bool dfs_solve(int x, int limit) {
   
    for (int i = 1; i <= m; ++i) {
     // 枚举右部图的每个点
        if (!st[i] && g[x][i]) {
   
            st[i] = 1;
            if (match[i].cnt < limit) {
     // 如果当前点的匹配数目小于limit,那么直接匹配。如果每个点能够匹配的数目不同,那么这里使用num[i](替代limit)表示右部图第i个点能够匹配的点数目
                match[i].matchs[++match[i].cnt] = x;
                return 1;
            }
            for (int j = 1; j <= match[i].cnt; j++) {
     // 如果当前点的匹配数目等于limit
                if (dfs_solve(match[i].matchs[j], limit)) {
     // 那么让当前所匹配的点去匹配其他点
                    match[i].matchs[j] = x;
                    return 1;
                }
            }
        }
    }
    return 0;
}

int hungary(int limit) {
     // 多重匹配的最大匹配
    int res = 0;
    memset(match, 0, sizeof(match));
    for (int i = 1; i <= n; ++i) {
   
        memset(st, 0, sizeof(st));
        if (dfs_solve(i, limit)) res++;  // 这里还可以优化,改为if (!dfs_solve(i, l, r)) return 0;意思为如果当前这个点没有找到匹配点,那么则不可能找到使得左部图每个点都匹配。
    }
    return res;  // 如果40行采用优化写法,这个改为:return 1;
}

int main() {
   
    while (scanf("%d%d", &n, &m) != EOF) {
     
  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值