二分图与匹配算法

本文详细介绍了二分图的概念及其性质,包括如何判断一个图是否为二分图、最大匹配的定义以及增广路径的重要性。重点阐述了染色法和匈牙利算法在求解二分图最大匹配中的应用,并通过König定理建立了最大匹配与最小点覆盖之间的关系。此外,还提及了二分图在最优匹配问题中的应用,如KM算法。最后,讨论了多重匹配和最大流问题在信息技术领域的相关性。
摘要由CSDN通过智能技术生成

二分图

  • 至少由两个顶点且没有奇数环

在一个无向图中,定义一条边覆盖的点为这条边的两个端点

找到一个边集S包含最多的边,使得这个边集覆盖的所有顶点中的每个顶点只被一条边覆盖。S的大小叫做图的最大匹配

  • 增广路

路径的起点和终点都是还没有匹配的点,并且路径经过的连线是一条没被匹配,一条已经匹配过,再下一条又没匹配这样交替地出现。找到这样的路径后,显然路径里没被匹配的连线比已经匹配了的连线多一条,把所有匹配的连线去掉匹配关系,把没有匹配的连线变成匹配的,这样匹配数就比原来多1

在这里插入图片描述


二分图算法

二分图{//一个图是二分图当且仅当图中不含奇数环
	1.染色法{ O(n+m)
		1.建邻接表
		2.for(1~n)
			if(i未染色)
				dfs(i)
		需要标记每个点是否被染色
	}
	关键的bool dfs:
		1.标记颜色
		2.遍历此点所有连接的点,如果没有被染色,则染色并dfs,若返回false 则返回false(这里的迭代十分关键,看代码)
		3.否则如果染过颜色,则判断颜色是都矛盾
		4.都没问题返回true
代码:
bool dfs(int x,int k){
    color[x]=k;
    for(int i=h[x];i!=-1;i=ne[i]){
        int j=e[i];
        if(!color[j]){
            if(!dfs(j,3-k)) return false;
        }
        else if(color[j]==k) return false;
    }
    return true;
}
//之后的main函数内:
for(int i=1;i<=n;i++){
    if(!color[i]){
        if(!dfs(i,1)){
            flag=false;
            break;
        }
    }
}
if(!flag) puts("No");
else puts("Yes");

匈牙利算法 O(nm)

在使用匈牙利算法之前一定要证明是二分图,即可以二染色!
遍历二分图要找的点每一个对应点
如果此点不在配对成功的集合中{
	将此点加入配对成功的集合
	如果此映射点没有被配对或者将其配对的点有其他可配对的点,则返回配对成功(对应函数外侧更新答案)
}
代码:
bool find(int x){
    for(int i=h[x];i!=-1;i=ne[i]){
        int j=e[i];
        if(st[j]) continue;
        st[j]=true;
        if(match[j]==0||find(match[j])){
            match[j]=x;
            return true;
        }
    }
    return false;
}

int main()
{
    for(int i=1;i<=n;i++)
    {
        memset(st,0,sizeof st);
        dfs(i);
    }
}

1.二分图不存在奇数环,染色法不存在矛盾问题
2.匈牙利算法,匹配,最大匹配,匹配点,增广路经
匹配:即没有公共点的边
最大匹配:边数最多的匹配
匹配点:在匹配中的点
增广路径:从一个非匹配点走,依次走非匹配边与匹配边,直到通过非匹配边走到一个非匹配点

König定理(柯尼定理):

结论:
最 大 匹 配 ∼ 不 存 在 增 广 路 径 最大匹配 \sim 不存在增广路径 广

3.最小点覆盖,最大独立集,最小路径点覆盖,最小路径重复点覆盖

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

最小点覆盖:从一个图中选出最少的点使得使得每一条边至少有一个端点被选出来
证明:最小点覆盖数=最大匹配数      
[1]最小点覆盖数>=最大匹配数:
	每一条边都选出一个点即可
[2]最小覆盖数与最大匹配数之间可取等号:
将一个图分成两半,构造:
	从左边每个非匹配点做一遍匈牙利算法,并标记所有经过的点。最终选出左边未被标记的点和右边被标记的点

上述构造满足三个性质:
    1)左边所有未被标记的点都是匹配点
    2)右边所有被标记的点都是匹配点
    3)对于匹配边,要么左右全被标记,要么全不被标记

因此选出来的点恰好可以满足最小覆盖,因为每个匹配边有且只有一个点被选
  • 最大独立集:从一个图中选出最多的点使得选出的点之间无边<=>去掉最少的点将所有边都破坏掉

  • 最大团:选出最大的点使得任意两点之间都有边

  • 最小路径点覆盖:在DAG中用最少的互不相交的路径(从起点连连连到终点)将所有点覆盖,拆点将每个点分成出点和入点,使得原图变成一个二分图
  • 最小路径重复点覆盖:
    • [1]求传递闭包(如果一个点间接连向另外一个点的话就直接加一条边)得到G’
    • [2]在G’上求最小路径覆盖
4.最优匹配 KM算法 [最小费用流也行]

最优匹配时建立在完美匹配的基础上的,如果不存在完美匹配,那么算法失效(完美匹配即所有人都能匹配)。如果不存在完美匹配可以人为加上边权为0的点边,从而得到最优匹配

  • KM(Kuhn-Munkres)算法

思路:

给每个点一个顶标,只有两个端点的顶标加起来等于边的权重的时候,我们才认为这是有效边

L[x]+R[y]==w[x][y]

一开始左集合(待配对集合)中每个点顶标为他连出的权值最大的边权,右边点集顶标全部为0.

当匹配失败的时候,遍历之前左集合本次尝试撇配遍历过的点,在连向的右集合中找一个差值与顶标和差值较小的边,不断更新delta=min{w[i]-l[x]-r[y]},左侧所有遍历过的点-delta右侧所有遍历过的点+delta

#include<iostream>//二分图最大权匹配 
#include<cstdio>
#include<queue>
#include<algorithm>
#include<cstring>
#define N 505
#define M 250005
#define INF 9990365505
#define ll long long
using namespace std;

int n,m,x,y,z,tot,tim;
int visx[N],visy[N];//visx[i]表示左侧第i个节点第几轮访问 
int matchx[N],matchy[N];//matchy[i]表示左边第i个节点与右边第matchy[i]个节点匹配 
ll ex[N],ey[N],slack[N];//ex表示左边节点值,ey表示右边节点的值,slack用于维护最小的的点权之和减去边权 
int e[M],ne[M],h[M],w[M],idx=0,pre[N];//记录x的先驱节点 

void add(int x,int y,int z){//链式前向星存边 
	e[idx]=y,w[idx]=z;
	ne[idx]=h[x],h[x]=idx++;
} 

void modify(int cur){//修改之前的 匹配方式 
    for (int last,x=cur;x;x=last){
    	last=matchx[pre[x]]; 
		matchx[pre[x]]=x;
		matchy[x]=pre[x];
	}
}
void bfs(int cur){
    for(int i=1;i<=n;i++)
        slack[i]=INF,pre[i]=0;//初始化 
    queue<int>q;
    q.push(cur);
    ++tim;
    while(1){
        while (!q.empty()){//bfs交错树 
            int u=q.front();
            q.pop();
            visx[u]=tim;//u是第tim轮被访问的 
            for (int i=h[u];~i;i=ne[i]){
            	
                int v=e[i],cost=w[i];//访问u的相邻节点 
                
                if(visy[v]==tim)
                    continue;//本轮已经被访问过的不需要再次被访问 
                    
                ll mincost=ex[u]+ey[v]-cost;//记录 
                
                if (mincost<slack[v]){//维护最小点权之和减边权 
                    slack[v]=mincost;
                    pre[v]=u;//v的先驱节点记为u   
                    if (!mincost){//mincost==0 则连边 
                        visy[v]=tim;//这一轮也访问到了v 
                        if (!matchy[v]){//左侧第v个节点没有与右侧节点匹配 
                            modify(v);//修改之前的匹配方式,并终止 
                            return;
                        }
                        else q.push(matchy[v]);//否则入队 
                    }
                }
            }
        }
        ll mincost=INF;
        for(int i=1;i<=n;++i){ 
            if(visy[i]!=tim){ //本轮没有被访问过 
                mincost=min(mincost,slack[i]);//在交错树的边中寻找顶标和与边权之差最小的边
            }
		} 
        for(int i=1;i<=n;++i){
            if (visx[i]==tim)//左侧节点减去这个值 
                ex[i]-=mincost;
            if (visy[i]==tim)
                ey[i]+=mincost;//右侧节点加这个值 
			else
                slack[i]-=mincost;//维护更新 
        }
        for(int i=1;i<=n;++i){ 
            if( visy[i]!=tim && !slack[i]){//发生冲突并解决冲突之后,继续匹配 
                visy[i]=tim;//标记为本轮访问的 
                if (!matchy[i]){//没有匹配过 
                    modify(i);//修改 
                    return;//返回 
                }
                else q.push(matchy[i]);//否则入队 
            }
        }
    }
}
void KM(){
    for(int i=1;i<=n;++i)
        bfs(i);//对每个点做一遍bfs 
    //做完之后的匹配值就是左右两端节点数的总和 
    ll ans=0;//
    for (int i=1;i<=n;++i)
        ans+=ex[i]+ey[i];
    printf("%lld\n",ans);
    for (int i=1;i<=n;++i)
        printf("%d ",matchy[i]);
    printf("\n");
}
int main(){
	memset(h,-1,sizeof h);
    scanf("%d%d",&n,&m);
    for (int i=1;i<=m;++i){//读入数据 
        scanf("%d%d%d",&x,&y,&z);
        add(x,y,z);
        ex[x]=max(ex[x],(ll)z);
    }
    KM();//km算法求解 
    return 0;
}
5.多重匹配(多夫多妻)[最大流]
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值