猫史档案馆


【C++基础教程】蒟蒻也能懂算法:二分图从入门到提高

用户:爵士OIer爵士OIer查看:265 回复:16 评论:3 创建时间:2020-05-05T17:52:15


【C++基础教程】蒟蒻也能懂算法:二分图从入门到提高

对于n皇后问题和覆盖问题,最基础的算法是无优化DFS(深度优先搜索)和状态压缩动态规划。这两种都是时间复杂度比较高的算法。还有什么其他方法呢?

 

二分图

 

center_image

如图,一个图的点集可以划分为两个不相交的子集,每一个子集中的点和该子集中的其他点没有边相连的图就叫二分图。比如上面有两个点集u,v符合上述条件,则它是一个二分图。

 

 

二分图的判定

 

 · 定理:一个无向图是二分图,当且仅当图中不存在奇环(长度为奇数的环)

 · 根据该定理,可用染色法判定二分图:尝试用黑、白两种颜色标记图中的节点,当一个节点被标记后,它的所有邻接点应被标记为与它相反的颜色。若发生冲突,说明图中存在奇环。二分图染色一般基于DFS实现,时间复杂度为O(N+M)。

 · 判定流程伪代码如下:

void dfs(int x,int color){  
    v[x]=color; //对点x染色  
    //遍历x相连的无向边(x,y)  
        if (v[y]==0) //未染色  
            dfs(y,3-color); //黑1,白2,3相减取反色  
        else if (v[y]==color)  
            //该无向图不是二分图,算法结束  
    //是无向图,算法结束  
}  
//在主函数中  
    for (int i=1;i<=n;i++)  
        if (v[i]==0) dfs(i,1);  

 

 

二分图匹配

 

 · 【任意两条边都没有公共端点】的边的集合称为图的匹配。

 · 最大匹配(maximal matching problem)就是包含边的个数最多的匹配。center_image

上面是一种匹配方式,但是不一定是最大匹配。

二分图匹配的用途很多,如自来水管、车站打车等。

 

 

增广路

 

 · 对于任意一组匹配M(M是一个边集),属于M的边被称为“匹配边”,不属于M的边被称为“非匹配边”。

 · 匹配边的端点被称为M的“饱和点”,其它节点被称为“非饱和点”

 · p是G的一条通路,如果p中的边为属于M中的匹配边与不属于M的非匹配边交替出现,则称p是一条M-交错路。

 · p是一条M-交错路,如果p的起点和终点都是非M-饱和点,则称p为M-增广路。

center_imagecenter_image

 

如上两图,左图是一种匹配。右图是一种更大的匹配。

 · 增广路显然具有以下性质

      1.长度len是奇数

      2.路径上第1,3,5,……,len条边是非匹配边,第2,4,6,……,len-1条边是匹配边。

 · 如果将增广路p上所有边的状态取反,即原来的边变为非匹配边,原来的非匹配边变成匹配边。那么新得到的边集M’仍然是一组匹配,并且匹配边数增加了1。由此可得推论:

 · 二分图的一组匹配M是最大匹配,当且仅当图中不存在M的增广路。

center_image

 

 

 

匈牙利算法(增广路算法)

 

 

 · 匈牙利算法,又称增广路算法,用于计算二分图最大匹配。其主要过程为:

       1.设M为空集,即所有边都是非匹配边。

       2.寻找M的增广路p,把路径上的所有边的匹配状态取反,得到一个更大的匹配M’。

       3.重复第2步,直至图中不存在M的增广路。

 

 · 该算法的关键在于如何找到一条增广路:依次尝试给每一个左部节点x寻找一个匹配的右部节点y。右部点y能与左部点x匹配:需要满足以下两个条件之一:

       1.y本身就是非匹配点

          此时无向边(x,y)本身就是非匹配边,自己构成一条长度为1的增广路。

       2.y已经与左部点x’匹配,但从x’出发能找到另一个右部点y’与之匹配。

          递归进入该左部点,为其寻找匹配的右部点,此时路径x~y~x’~y’为一条增广路。

 · 在实际的程序实现中,可采用DFS的框架,递归地从x出发寻找增广路。若找到,则在深搜回溯时,正好把路径上的匹配状态取反。另外可以用全局bool数组标记节点的访问情况,避免重复搜索。

 

下面贴出具体流程,方便理解。

center_imagecenter_image  center_imagecenter_imagecenter_imagecenter_imagecenter_imagecenter_image

图一到图五很好理解,不讲;

可以看到,在图六中出现了两条红色的边。搜索到第四个白色点时,出现了下面这条红色的边,占用第三个黑色点。因此,第二个白色点重新建边,搜索与第二个白色点相邻的黑色点,搜索到了第一个黑色点,出现上面这条红色的边,发现这个黑色点如果被占用,那么第一个白色点将会无法建边,回溯;

到图七,第二个白色点搜索与没有被占用且与其联通的第四个黑色点,建边(图七中新出现的红色边),形成增广路。

至此,DFS过程结束。形成图八那样的一种从第一个白色点出发的最大匹配。

 

 · 匈牙利算法的正确性基于贪心策略,它的一个重要特点是:当一个节点称为匹配点后,至多因为找到增广路而更换匹配对象,但是绝不会再变回非匹配点。

 · 对于每个左部节点,寻找增广路最多遍历整张二分图一次。因此,该算法的时间复杂度为O(NM)。

 

Code:

bool dfs(int x){  
    int i,y;  
    for(i=head[x]; i>0; i=next[i]) //考虑与x相连的每个点y=ver[i]  
        if(!visit[y=ver[i]]){//没有被搜索过    
            visit[y]=1;  
            if(!match[y] || dfs(match[y])){
		   match[y]=x; return true;
	     }  
        }  
    return false;  
}

//下面是主函数核心部分
for(int i=1;i<=n;i++){  
    memset(visit,0,sizeof(visit)); //清空访问标记  
    if(dfs(i)) ans++; //从左部点i出发寻找增广路  
}  

 

 

二分图匹配的模型

 

 · 二分图匹配的模型有两个要素

      1.节点能分成两个独立的集合,每个集合内部有0条边。

      2.每个节点只能与1条匹配边相连。

 

 

 

例题:匈牙利算法-棋盘上的问题

 

还记得开篇的n皇后和状压DP吗?

 

 

棋盘覆盖

 

问题描述

给定一个N行N列的棋盘,已知某些格子禁止放置。

求最多能往棋盘上放多少块的长度为2、宽度为1的骨牌,骨牌的边界与格线重合(骨牌占用两个格子),并且任意两张骨牌都不重叠。

 

输入格式

第一行包含两个整数N和t,其中t为禁止放置的格子的数量。

接下来t行每行包含两个整数x和y,表示位于第x行第y列的格子禁止放置,行列数从1开始。

 

输出格式

输出一个整数,表示结果。

 

数据范围

1≤N≤100

 

输入样例

8 0

输出样例

32

 

解题思路:

相信大家一看到就以为是状压DP吧(雾

我们怎么用二分图的匈牙利算法?看下:

棋盘黑白染色

相邻格子连边

1*2骨牌对应一条匹配边

这个用特殊字体区分的就是简单思路。

#include<bits/stdc++.h>
#define N 105
#define M 1000005
using namespace std;
struct E{int next,to;}e[M];
int n,m,num,ans;
int h[N*N],mat[N*N];
int dx[5]={0,-1,1,0,0};
int dy[5]={0,0,0,-1,1};
bool vis[N*N];
bool tag[N][N];
void add(int u,int v){
	e[++num].next=h[u];
	e[num].to=v;
	h[u]=num;
}
int cal(int x,int y){return(x-1)*n+y;}
bool dfs(int x){
	for(int i=h[x];i!=0;i=e[i].next)
		if(!vis[e[i].to]){
			vis[e[i].to]=1;
			if(!mat[e[i].to]||dfs(mat[e[i].to])){
				mat[e[i].to]=x;
				return 1;
			}
		}
	return 0;
}
int main(){
	cin>>n>>m;
	for(int i=1;i<=m;i++){
		int x,y;
		cin>>x>>y;
		tag[x][y]=1;
	}
	for(int i=1;i<=n;i++)
		for(int j=1;j<=n;j++)
			for(int k=1;k<=4;k++){
				int x=i,y=j;
				int xx=x+dx[k],yy=y+dy[k];
				if(xx<1||xx>n||yy<1||yy>n)continue;
				if(tag[x][y]||tag[xx][yy])continue;
				add(cal(x,y),cal(xx,yy));
				add(cal(xx,yy),cal(x,y));
			}
	for(int i=1;i<=n;i++)
		for(int j=1;j<=n;j++)
			if((i+j)%2){
				memset(vis,0,sizeof(vis));
				if(dfs(cal(i,j)))ans++;
			}
	cout<<ans;
	return 0;
}

 

 

車的放置

 

题目描述

给定一个N行M列的棋盘,已知某些格子禁止放置。

问棋盘上最多能放多少个不能互相攻击的車。

車放在格子里,攻击范围与中国象棋的“車”一致。

 

输入格式

第一行包含三个整数N,M,T,其中T表示禁止放置的格子的数量。

接下来T行每行包含两个整数x和y,表示位于第x行第y列的格子禁止放置,行列数从1开始。

 

输出格式

输出一个整数,表示结果。

 

数据范围

1≤N,M≤200

 

输入样例

8 8 0

输出样例

8

解题思路:

#include <bits/stdc++.h>
#define mem(a,b) memset(a,b,sizeof a)
using namespace std;
int n,m,t;
const int N=100000;
int head[N],nex[N],to[N],cnt;
bool g[220][220];
int match[N];
bool vis[N];
void init(){
	mem(head,-1);
	mem(nex,-1);
	mem(to,-1);
	cnt=0;
	mem(match,-1);
	mem(vis,false);
	mem(g,true);
}
void add(int a,int b){
	++cnt;
	nex[cnt]=head[a];
	head[a]=cnt;
	to[cnt]=b;
}
bool dfs(int p){
	for (int i=head[p];i!=-1;i=nex[i]){
		int y=to[i];
		if(vis[y])continue;
		vis[y]=true;
		if (match[y]==-1||dfs(match[y])){
			match[y]=p;
			return true;
		}
	}
	return false;
}
int main(){
	init();
	cin >>n>>m>>t;
	while(t--){
		int x,y;
		cin>>x>>y;
		g[x][y]=false;
	}
	for (int i=1;i<=n;i++){
		for (int j=1;j<=m;j++){
			if (g[i][j]){
				add(i,j+n);
				add(j+n,i);
			}
		}
	}
	int res=0;
	for (int i=1;i<=n;i++){
		mem(vis,false);
		if(dfs(i))res++;
	}
	cout<<res<<"\n";
	return 0;
}

 

 

【上机练习】例题:Place the Robots

 

问题描述

有一个N*M(N,M<=50)的棋盘,棋盘的每一格是三种类型之一:空地、喵地、墙。机器人只能放在空地上。在同一行或同一列的两个机器人,若它们之间没有墙,则它们可以互相攻击。问给定的棋盘,最多可以放置多少个机器人,使它们不能互相攻击。

center_image

把每一行、每一列被墙隔开,并且包含空地的连续区域称作“块”。

显然,在一个块之中,最多只能放一个机器人。把这些块编上号。

把每个横向块看作X部的点,竖向块看作Y部的点,若两个块有公共的空地,则在它们之间连边。

由于每条边表示一个空地,有冲突的空地之间必有公共顶点,所以问题转化为二部图的最大匹配问题。

 

center_image

 

 

现在,我们已经基本掌握了二分图的基础算法。让我们接着学习吧!

 

完备匹配、多重匹配

 

给定一张二分图,其左部,右部节点数相同,均为N个节点。如果该二分图的最大匹配包含N条匹配边,则称该二分图具有完备匹配。

给定一张包含N个左部节点、M个右部节点的二分图。从中选出尽量多的边。使第i(1≤i≤N)个左部节点至多与kl_i条选出的边相连,第j(1≤j≤M)个右部节点至多与kr_ j条选出的边相连。该问题被称为二分图的多重匹配。

当kl_i=kr_j=1时,二分图的多重匹配即简化为最大匹配。

 

 

多重匹配的解决方案

 

1.拆点:把第i个左部节点拆成kl_i个不同的左部节点,第j个右部节点拆成kr_j个右部节点。对于原图中的每条边(i,j),在i拆成的所有节点与j拆成的所有节点之间连边。然后求解二分图最大匹配。

2.网络流(想展开讲是不可能的)

 

 

例题:导弹防御塔

 

问题描述

Freda Shi 的城堡遭受了M个入侵者的攻击!     

Freda 控制着N座导弹防御塔,每座塔都有足够数量的导弹,但是每次只能发射一枚。每座塔每次发射前需要预热T1秒,发射后需要冷却T2秒。

给定塔和入侵者的坐标。导弹从塔发出直到击中目标所需的飞行时间=塔和入侵者之间的距离。

由于小伙伴 Rainbow Li 就要来拜访她的城堡,Freda 想用最少的时间击退所有的入侵者,请你告诉她一种攻击方案。

 

输入格式

第一行五个正整数N,M,T1,T2,V。

接下来 M 行每行两个整数,代表入侵者的坐标。

接下来 N 行每行两个整数,代表防御塔的坐标。

 

输出格式

输出一个实数,表示最少需要多少分钟才能击中所有的入侵者,四舍五入保留六位小数。

 

数据范围

1≤N,M≤50,坐标绝对值不超过10000,T1,T2,V不超过2000。

 

输入样例:

3 3 30 20 1 0 0 0 50 50 0 50 50 0 1000 1000 0


输出样例:
91.500000


解题思路:

 

 

Code:

#include<bits/stdc++.h>
using namespace std;
const int maxn=50+10;
struct Node
{
    int x,y;
    Node() {}
    Node(int a,int b):x(a),y(b) {}
} def[maxn],ta[maxn];
int vis[maxn*maxn],pre[maxn*maxn];
double times[maxn][maxn];
double t1,t2,V;
vector<int >G[maxn];
int n,m;
bool DFS(int u)
{
    for(int i=0; i<G[u].size(); i++)
    {
        int v=G[u][i];
        if(vis[v])
            continue;
        vis[v]=1;
        if(pre[v]==0||DFS(pre[v]))
        {
            pre[v]=u;
            return true;
        }
    }
    return false;
}
bool judge(double mid)
{
    for(int i=1; i<=m; i++)
        G[i].clear();
    for(int i=1; i<=m; i++)
        for(int j=1; j<=n; j++)
        {
            double now=0;
            for(int k=0; k<=m; k++)
            {
                now=k*(t1+t2);
                if(mid-(now+t1+times[i][j])>1e-7)
                {
                    G[i].push_back(j*(m+1)+k-m-1);
                }
                else
                    break;
            }
        }
    memset(pre,0,sizeof(pre));
    for(int i=1; i<=m; i++)
    {
        memset(vis,0,sizeof(vis));
        if(!DFS(i))
            return false;
    }
    return true;
}
int main()
{
    cin>>n>>m>>t1>>t2>>V;
    t1/=60.0;
    for(int i=1; i<=m; i++)
    {
        int x,y;
        scanf("%d%d",&x,&y);
        ta[i]=Node(x,y);
    }
    for(int i=1; i<=n; i++)
    {
        int x,y;
        scanf("%d%d",&x,&y);
        def[i]=Node(x,y);
    }
    for(int i=1; i<=m; i++)
        for(int j=1; j<=n; j++)
            times[i][j]=sqrt((ta[i].x-def[j].x)*(ta[i].x-def[j].x)+(ta[i].y-def[j].y)*(ta[i].y-def[j].y))/V;
    double l=0,r=1000000,ans=r;
    while(r-l>1e-9)
    {
        double mid=(l+r)/2.0;
        if(judge(mid))
            ans=min(ans,mid),r=mid;
        else
            l=mid;
    }
    printf("%.6lf\n",ans);
}

 

 

 

二分图最小点覆盖(Minimum Vertex Cover)

 

center_image

 

 

 

König定理

二分图最小点覆盖包含的点数等于二分图最大匹配包含的边数

 

证明:

首先因为最大匹配是原二分图边集的一个子集,并且所有边都不相交,所以至少需要从每条匹配中选出一个端点。因此最小点覆盖包含的点数不可能小于最大匹配包含的边数。如果能对任意二分图构造出一组点覆盖,其包含的点数等于最大匹配包含的边数,定理即得证。

构造方法

       1.求出最大匹配

       2.从右部每个未匹配点出发寻找增广路(一定失败),标记访问过的节点。

       3.取左部标记点,右部未标记点,构成一组最小覆盖。

 

经过上述构造方法后,

       右部未匹配点一定是标记点——因为它们是出发点

       左部未匹配点一定是未标记点——如果被标记(访问)则找到了增广路,矛盾

       一对匹配点都被标记或者都未标记——因为左部匹配点只能通过右部到达

取左部标记点、右部未标记点,恰好使得每对匹配点被取走一个

数值上的相等性得证。

 

       匹配边一定被覆盖——每对匹配点取走一个

       不存在连接两个未匹配点的边——否则出现仅包含1条边的增广路,矛盾

       连接左部匹配点和右部未匹配点的边——后者是出发点,前者一定被标记

       连接左部未匹配点和右部匹配点的边——后者未被标记,否则存在增广路

所有边都被覆盖,合法性得证。

 

 

例题:Muddy Fields

 

问题描述

在一块N*M的矩形地面上,有一些格子是泥泞的。

用一些宽为1的木板把泥地盖住,并且不能盖住好地,木板可以重叠。

问最少需要多少木板? N,M<=50。

 

解题思路

不能盖住好地,那么宽为1的木板只能放在行、列泥泞块里。

每个泥格子都要被盖住,选择一个块可以盖住一些泥格子。

行、列泥泞块对应左、右部中的点,泥格子对应边。

答案=二分图最小点覆盖。

 

Code:

#include<bits/stdc++.h>
#define N 1100
int G[N][N], vis[N], used[N];
char maps[N][N];
int m, n, x, y;
bool Find(int u){
    int i;
    for(i = 1 ; i <= y ; i++){
        if(!vis[i] && G[u][i]){
            vis[i] = 1;
            if(!used[i] || Find(used[i])){
                used[i] = u;
                return true;
            }
        }
    }
    return false;
}
void Build()//构图
{
    int i, j, a[N][N] , b[N][N];
    x = y = 0;
    memset(a, 0, sizeof(a));
    memset(b, 0, sizeof(b));
    for(i = 1 ; i <= m ; i++)
    {
        for(j = 1 ; j <= n ; j++)
        {
            if(maps[i][j] == '*')
            {
                if(maps[i][j - 1] == '*')
                    a[i][j] = a[i][j - 1];
                else
                    a[i][j] = ++x;
            }
        }
    }//木板横着放
    for(i = 1 ; i <= m ; i++)
    {
        for(j = 1 ; j <= n ; j++)
        {
            if(maps[i][j] == '*')
            {
                if(maps[i - 1][j] == '*')
                    b[i][j] = b[i - 1][j];
                else
                    b[i][j] = ++y;
                G[a[i][j]][b[i][j]] = 1;
            }
        }
    }//木板竖着放
}
int main()
{
    int i, j, ans;
    while(~scanf("%d%d", &m, &n))
    {
        ans = 0;
        memset(G, 0, sizeof(G));
        for(i = 1 ; i <= m ; i++)
           {
               getchar();
               for(j = 1 ; j <= n ; j++)
               {
                   scanf("%c", &maps[i][j]);
               }
           }
        Build();
        memset(used, 0, sizeof(used));
        for(i = 1 ; i <= x ; i++)//X集合中的点与Y集合中的点找最大匹配
        {
            memset(vis, 0, sizeof(vis));
            if(Find(i))
                ans++;
        }
        printf("%d\n", ans);
    }
    return 0;
}

 

 

 

二分图最大独立集(Maximum Independent Set)

 

任意两点在图中都没有边相连的点集称为图的独立集。

 

定理:二分图最大独立集 = 图的点数 – 二分图最大匹配

 

证明:选出最多的点构成独立集

       在图中去掉最少的点,使剩下的点之间没有边。

       用最少的点覆盖所有的边,去掉的是最小覆盖。

 

 

例题:骑士放置

 

问题描述

给定一个N*M的棋盘,有一些格子不能放棋子。问棋盘上最多能放多少个不能互相攻击的骑士(马)。

 

解题思路:

对棋盘黑白染色,黑、白色的点分别属于左、右部。

可以攻击到的两个格子之间连边。

马沿日字形攻击,所以肯定是在不同颜色的点之间连边。

不能互相攻击,就是寻找一个最大独立集。

 

Code:

#include<bits/stdc++.h>
#define rg register
#define il inline
#define co const
template<class T>il T read(){
    rg T data=0,w=1;rg char ch=getchar();
    for(;!isdigit(ch);ch=getchar())if(ch=='-') w=-w;
    for(;isdigit(ch);ch=getchar()) data=data*10+ch-'0';
    return data*w;
}
template<class T>il T read(rg T&x) {return x=read<T>();}
typedef long long ll;
using namespace std;

co int N=101;
int n,m,t,ans,fx[N][N],fy[N][N];
bool a[N][N],v[N][N];
co int dx[8]={-2,-2,-1,-1,1,1,2,2};
co int dy[8]={-1,1,-2,2,-2,2,-1,1};
bool dfs(int x,int y){
	for(int i=0;i<8;++i){
		int nx=x+dx[i],ny=y+dy[i];
		if(nx<1||nx>n||ny<1||ny>m||a[nx][ny]||v[nx][ny]) continue;
		v[nx][ny]=1;
		if(!fx[nx][ny]||dfs(fx[nx][ny],fy[nx][ny])){
			fx[nx][ny]=x,fy[nx][ny]=y;
			return 1;
		}
	}
	return 0;
}
int main(){
	read(n),read(m),read(t);
	for(int i=1;i<=t;++i) a[read<int>()][read<int>()]=1;
	for(int i=1;i<=n;++i)for(int j=1;j<=m;++j){
		if(i+j&1||a[i][j]) continue;
		memset(v,0,sizeof v);
		ans+=dfs(i,j);
	}
	printf("%d\n",n*m-t-ans);
	return 0;
}

 

 

 

有向无环图的最小路径点覆盖

 

 

最小路径覆盖:用尽量少的不相交简单路径覆盖有向无环图的所有顶点。(即每个顶点恰好被覆盖一次)

 

把原图中的每个点拆成二分图中左、右两个点;

对于每条有向边(u,v),从u的左部点(成为出点)向v的右部点(称为入点)连一条有向边。

 

最小路径覆盖数 = 原有向图节点数 – 新二分图最大匹配数。

 

 

 · 不相交的最小路径覆盖->每个点的入度、出度均不超过1

 · 拆点转化->二分图每个点连接的边不超过1条->一个匹配

 

 · 有向图路径上的每条边 对应 二分图匹配中的一条匹配边

 · 有向图路径上每条边的出点 对应 二分图匹配边的左部点

 · 每条路径的最后一个点没有对应

 

 · 最小路径覆盖的路径数 = 最少未匹配的点数 = 原图节点数 – 二分图最大匹配数。

 

 

【上机练习】例题:Air Raid

 

问题描述

N个城市M条道路形成有向无环图。

现在要求派一些伞兵空降在某些城市,然后这些伞兵可以沿着道路访问到其他城市,但是不能有两个或两个以上伞兵访问同一个城市。

问最少需要多少个伞兵。N<=120。

 

代码不上了。

 


二分图带权最大匹配(最优匹配)

 

给定一张二分图,每条边都带有一个权值。求出该二分图的一组最大匹配,使得匹配边的权值总和最大。该问题称为二分图的带权最大匹配,也称最优匹配。

 

 · 二分图带权最大匹配的前提是匹配数最大,然后在最大化匹配边的权值总和。

 · 两种解法:KM算法与费用流。

 · KM算法只能在满足“带权最大匹配一定是完备匹配”的图中正确求解。

 · 不满足时,用最大费用最大流求解。

 

KM算法

 

交错树:

在匈牙利算法中,如果从某个左部节点出发寻找匹配失败,那么在DFS的过程中,所有访问过的节点,以及为了访问这些节点而经过的边,共同构成一棵树。这课树的根是一个左部节点,所有叶子节点也都是左部节点(因为最终匹配失败了),并且树上第1,3,5,…层的边都是非匹配边,第2,4,6,…层的边都是匹配边。这棵树称为交错树。

顶标:

在二分图中,给第i(1≤i≤N)个左部节点一个整数值A_i,给第j(1≤j≤N)个右部节点一个整数值B_j。同时,必须满足∀i,j,A_i+B_j≥w(i,j),其中w(i,j)表示第i个左部点和第j个右部点之间的边权(没有边时设为负无穷)。这些整数值A_i, B_j称为节点的顶标。

 

 · 相等子图

          二分图中所有满足节点和A_i+B_j=w(i,j)的边构成的子图,称为二分图的相等子图。

 · 若相等子图中存在完备匹配,则这个完备匹配就是二分图的带权最大匹配。

 

 · 证明:

         在相等子图中,完备匹配的边权之和等于∑2_(i=1)^N▒〖(A_i+B_j)〗,即所有顶标之和。

         因为顶标满足∀i,j,A_i+B_j≥w(i,j),所以在整个二分图中,任何一组匹配的边权之和都不可能大于所有顶标之和。

 

KM算法的基本思想就是。先在满足∀i,j,A_i+B_j≥w(i,j)的前提下,给每个节点随意赋值一个顶标,然后采取适当的策略不断扩大相等子图的规模,直至相等子图存在完备匹配。可以赋值A_i=max┬(1≤j≤N)⁡〖{w(i,j)},B_j=0〗

对于一个相等子图,用匈牙利算法求它的最大匹配。若最大匹配不完备,则一定有一个左部节点匹配失败。该节点匹配失败的那次DFS形成了一棵交错树T。

1.除了根节点以外, T中其它的左部点都是从右部点沿着匹配边访问到的,即在程序中调用了dfs(match[y]),其中y是一个右部节点。

2.T中所有的右部点都是从左部点沿着非匹配边访问到的。

在寻找到增广路以前,不会改变已有的匹配,所以一个右部点沿着匹配边能访问到的左部点是固定的。为了让匹配数增加,只能从第2条结论入手,考虑怎样能让左部点沿着非匹配边访问到更多的右部点。

 

把交错树T中所有的左部节点顶标A_i (i∈T)减小一个整数值∆,把T中所有右部节点顶标B_j (j∈T)增大一个整数值∆:

  1.右部点j沿着匹配边,递归访问到i=match[j]:

  对于一条匹配边,显然要么i,j∈T(被访问到),要么i,j∉T(没被访问到),故A_i+B_j不变,匹配边仍然属于相等子图。

  2.左部点i沿着非匹配边,访问右部点j,尝试与之匹配:

  因为左部点的访问是被右部点沿着匹配边递归。只需考虑i∈T。

      (1) 若i,j∈T, A_i+B_j不变,即以前能从点i访问点j, 现在仍可以。

      (2) 若i∈T,j∉T,则A_i+B_j变小,即以前点i访问不到的点j,现在有可能访问到。

 

为了保证顶标符合前提条件∀i,j,A_i+B_j≥w(i,j),可在所有的i∈T,j∉T的边(i,j)中,找出最小的A_i+B_j-w(i,j),作为∆。只要原图存在完备匹配,这样的边一定存在,可以保证至少有一条新的边加入相等子图,使交错树中至少一个左部点能访问到的右部点增多。

不断重复以上过程,直到每一个左部点都匹配成功,就得到了相等子图的完备匹配。即原图的带权最大匹配。

 

Code:

const int N=105;  
int w[N][N]; //边权  
int la[N],lb[N];//左右顶点的坐标  
bool va[N],vb[N];//访问标记,是否在交错树中  
int match[N];//右部点匹配了哪一个左部点  
int n,delta;  
  
bool dfs(int x){  
    va[x]=1; //访问标记,x在交错树中  
    for (int y=1;y<=n;y++)  
        if (!vb[y])  
            if (la[x]+lb[y]-w[x][y]==0){//相等子图  
                vb[y]=1;//访问标记,y在交错树中  
                if (!match[y]||dfs(match[y])){  
                    match[y]=x;  
                    return true;  
                }  
            }  
            else delta = min(delta,la[x]+la[y]-w[x][y]);  
    return false;  
}  
int KM(){  
    for (int i=1;i<=n;i++){  
        la[i]=-(1<<30); //-inf  
        lb[i]=0;  
        for (int j=1;j<=n;j++)  
            la[i]=max(la[i],w[i][j]);  
    }  
    for (int i=1;i<=n;i++)  
        while (true){//直到左部点找到匹配  
            memset(va,0,sizeof(va));  
            memset(vb,0,sizeof(vb));  
            delta = 1<<30; //inf  
            if (dfs(i)) break;  
            for (int j=1;j<=n;j++){  
                if (va[j]) la[j]-=delta;  
                if (vb[j]) lb[j]+=delta;  
            }  
        }  
    int ans = 0;  
    for (int i=1;i<=n;i++) ans+=w[match[i]][i];  
    return ans;  
 }  

 

 

【上机练习】例题:Ants

 

问题描述

平面上共有2*N个点,N个是白点,N个是黑点。对于每个白点,找到一个黑点,把二者用线段连起来,要求最后所有线段都不相交。

 

解题思路

假定有白点1(a1,b1), 2(a2,b2), 黑点3(a3,b3),4(a4,b4); 如果1(a1,b1)与3(a3,b3)相连,2(a2,b2)与4(a4,b4)相连,如果他们两条边相交,那么dis(1,3)+dis(2,4) 一定大于dis(1,4)+dis(2,3)这两条没有相交的边长和。所以应该连接1-4,2-3; 如果存在满足题目要求的解,那么求一个最小权完美匹配,就可以保证所有匹配的边不相交。

 

 

KM算法的几种转化

 

1.二分图最佳匹配=最小点权覆盖(对偶问题)

2.KM算法是求最大权完备匹配,如果要求最小权完备匹配怎么办?方法很简单,只需将所有的边权值取其相反数,求最大权完备匹配,匹配的值再取相反数即可。

3.KM算法的运行要求是必须存在一个完备匹配,如果求一个最大权匹配(不一定完备)该如何办?依然很简单,把不存在的边权值赋为0。

4.KM算法求得的最大权匹配是边权值和最大,如果我想要边权之积最大,又怎样转化?还是不难办到,每条边权取自然对数,然后求最大和权匹配,求得的结果a再算出ea就是最大积匹配。至于精度问题则没有更好的办法了。

 

 

二分图的教程就到此为止了。大家可以回顾一下,欢迎在评论区讨论!

 

 

 


回复

上一页第 1 页 / 共 1 页下一页
爵士OIer爵士OIer

dd

点赞0


评论


爵士OIer爵士OIer

dd

点赞0


评论


爵士OIer爵士OIer

dd

点赞0


评论


爵士OIer爵士OIer

dd

点赞0


评论


爵士OIer爵士OIer

dd

点赞0


评论


CXS袅娜迎风CXS袅娜迎风

(⊙o⊙)!

点赞0


评论

dd

dd


爵士OIer爵士OIer

dd

大家一起顶()

点赞0


评论


唧唧的猫唧唧的猫

喵

点赞0


评论


爵士OIer爵士OIer

tql

点赞0


评论

编程猫教程的最高境界


爵士OIer爵士OIer

dd

点赞0


评论


爵士OIer爵士OIer

喵

点赞0


评论


爵士OIer爵士OIer

dd

点赞0


评论


AlcalaAlcala

dd

点赞0


评论


去世的细胞去世的细胞

从入门到入坟

点赞1


评论


L54321L54321

66666写得好详细,期待看到你更多的干货

点赞0


评论


爵士OIer爵士OIer

喵

点赞0


评论