您的位置:首页 > 其它

二分图最大匹配,最小路径覆盖,最小点覆盖,最大独立集,最小边覆盖与建图方法

2015-07-02 01:10 323 查看
转载请注明出处(别管写的好坏,码字也不容易):[b]http://blog.csdn.net/hitwhacmer1

[/b]

前言:

有自己写的,有摘的别人的,前面是摘的,也是无心整理,出错是难免的,反正我都不会证明,智人见智,别被我误导了。

§1图论点、边集和二分图的相关概念和性质

点覆盖、最小点覆盖

点覆盖集即一个点集,使得所有边至少有一个端点在集合里。或者说是“点” 覆盖了所有“边”。。极小点覆盖(minimal vertex covering):本身为点覆盖,其真子集都不是。最小点覆盖(minimum vertex covering):点最少的点覆盖。点覆盖数(vertex covering number):最小点覆盖的点数。

边覆盖、极小边覆盖

边覆盖集即一个边集,使得所有点都与集合里的边邻接。或者说是“边” 覆盖了所有“点”。极小边覆盖(minimal
edge covering):本身是边覆盖,其真子集都不是。最小边覆盖(minimum edge covering):边最少的边覆盖。边覆盖数(edge covering number):最小边覆盖的边数。

独立集、极大独立集

独立集即一个点集,集合中任两个结点不相邻,则称V为独立集。或者说是导出的子图是零图(没有边)的点集。极大独立集(maximal independent set):本身为独立集,再加入任何点都不是。最大独立集(maximum independent set):点最多的独立集。独立数(independent number):最大独立集的点。



团即一个点集,集合中任两个结点相邻。或者说是导出的子图是完全图的点集。极大团(maximal clique):本身为团,再加入任何点都不是。最大团(maximum clique):点最多的团。团数(clique number):最大团的点数。

边独立集、极大边独立集

边独立集即一个边集,满足边集中的任两边不邻接。极大边独立集(maximal edge independent set):本身为边独立集,再加入任何边都不是。最大边独立集(maximum edge independent set):边最多的边独立集。边独立数(edge independent number):最大边独立集的边数。

边独立集又称匹配(matching),相应的有极大匹配(maximal matching),最大匹配(maximum matching),匹配数(matching number)。

支配集、极小支配集

支配集即一个点集,使得所有其他点至少有一个相邻点在集合里。或者说是一部分的“点”支配了所有“点”。极小支配集(minimal dominating set):本身为支配集,其真子集都不是。最小支配集(minimum dominating set):点最少的支配集。支配数(dominating number):最小支配集的点数。

边支配集、极小边支配集

边支配集即一个边集,使得所有边至少有一条邻接边在集合里。或者说是一部分的“边”支配了所有“边”。极小边支配集(minimal edge dominating set):本身是边支配集,其真子集都不是。最小边支配集(minimum edge dominating set):边最少的边支配集。边支配数(edge dominating number):最小边支配集的边数。

最小路径覆盖

最小路径覆盖(path covering):是“路径” 覆盖“点”,即用尽量少的不相交简单路径覆盖有向无环图G的所有顶点,即每个顶点严格属于一条路径。路径的长度可能为0(单个点)。

最小路径覆盖数=G的点数-最小路径覆盖中的边数。应该使得最小路径覆盖中的边数尽量多,但是又不能让两条边在同一个顶点相交。拆点:将每一个顶点i拆成两个顶点Xi和Yi。然后根据原图中边的信息,从X部往Y部引边。所有边的方向都是由X部到Y部。因此,所转化出的二分图的最大匹配数则是原图G中最小路径覆盖上的边数。因此由最小路径覆盖数=原图G的顶点数-二分图的最大匹配数便可以得解。

匹配

匹配(matching)是一个边集,满足边集中的边两两不邻接。匹配又称边独立集(edge independent set)。

在匹配中的点称为匹配点(matched vertex)或饱和点;反之,称为未匹配点(unmatched
vertex)或未饱和点。

交错轨(alternating path)是图的一条简单路径,满足任意相邻的两条边,一条在匹配内,一条不在匹配内。

增广轨(augmenting path):是一个始点与终点都为未匹配点的交错轨。

最大匹配(maximum matching)是具有最多边的匹配。

匹配数(matching number)是最大匹配的大小。

完美匹配(perfect matching)是匹配了所有点的匹配。

完备匹配(complete matching)是匹配了二分图较小集合(二分图X,Y中小的那个)的所有点的匹配。

增广轨定理:一个匹配是最大匹配当且仅当没有增广轨。

所有匹配算法都是基于增广轨定理:一个匹配是最大匹配当且仅当没有增广轨。这个定理适用于任意图。

二分图的性质

二分图中,点覆盖数是匹配数。

(1) 二分图的最大匹配数等于最小覆盖数,即求最少的点使得每条边都至少和其中的一个点相关联,很显然直接取最大匹配的一段节点即可。

(2) 二分图的独立数等于顶点数减去最大匹配数,很显然的把最大匹配两端的点都从顶点集中去掉这个时候剩余的点是独立集,这是|V|-2*|M|,同时必然可以从每条匹配边的两端取一个点加入独立集并且保持其独立集性质。

(3) DAG的最小路径覆盖,将每个点拆点后作最大匹配,结果为n-m,求具体路径的时候顺着匹配边走就可以,匹配边i→j',j→k',k→l'....构成一条有向路径。

(4)最大匹配数=左边匹配点+右边未匹配点。因为在最大匹配集中的任意一条边,如果他的左边没标记,右边被标记了,那么我们就可找到一条新的增广路,所以每一条边都至少被一个点覆盖。

(5)最小边覆盖=图中点的个数-最大匹配数=最大独立集。

二分图的判定

二分图是这样一个图: 有两顶点集且图中每条边的的两个顶点分别位于两个顶点集中,每个顶点集中没有边直接相连接!

 无向图G为二分图的充分必要条件是,G至少有两个顶点,且其所有回路的长度均为偶数。

 判断二分图的常见方法是染色法: 开始对任意一未染色的顶点染色,之后判断其相邻的顶点中,若未染色则将其染上和相邻顶点不同的颜色, 若已经染色且颜色和相邻顶点的颜色相同则说明不是二分图,若颜色不同则继续判断,bfs和dfs可以搞定!

易知:任何无回路的的图均是二分图。

§2 另外一些姿势

定理1:最大独立集S 与 最小覆盖集T 互补

对于无向图:
最小点覆盖+最大独立集=顶点个数
最大团=补图的最大独立子集

关系1:给定图G = (V,E)无孤立点,则G的极大点独立集都是G的极小支配集。
关系2:G的点覆盖数 a与点独立集数 b满足: a + b = n。
关系3:G的边覆盖数 a与边独立集数 b满足: a + b = n。(边独立集数即匹配数)
关系3:给定图G = (V,E)无孤立点,|V | = n。M是G的匹配,W是G的边覆盖,则|M|≤|W|,等号成立时M是G的完美匹配而W是G的最小边覆盖。

对于二部图:
最小点覆盖数 = 最大匹配数

最小路径覆盖 = 顶点数 – 最大(二分)匹配数
最小边覆盖与最小路径覆盖的区别:

边覆盖集:通俗地讲,所谓边覆盖集,就是G中所有的顶点都是E*中某条边的邻接顶点(边覆盖顶点),一条边只能覆盖2个顶点。
注意:在无向图中存在用尽量少的边去“覆盖”住所有的顶点,所以边覆盖集有极小与最小的区别。
极小边覆盖:若边覆盖E*中的任何真子集都不是边覆盖集,则称E*是极小边覆盖集。
最小边覆盖:边数最小的边覆盖称为最小边覆盖,通俗地讲,就是极小边覆盖中的最小的一个集合。
最小边覆盖在二分图中的应用:
最小边覆盖 = 最大独立集 = n - 最大匹配,这个是二分图上的一个性质。
最小路径覆盖最小边覆盖不同不要求给的图是二分图,而是要求是PXP的有向图,不能有环,然后根据原图构造二分图,构造方法是将点一分为二,如,i分为i1和i2然后如果i和j有边,那么就在i1和j2之间连一条边。由此构成二分图
然后最小路径覆盖是n-m,n为原图的点的个数,m为新造二分图的最大匹配。

要使用上面的最小边覆盖的公式的前提是你已经将点集分成了左右两个点集,如果只是一个无向图的话那么就可以按照下面的做法来做:
无向图G(V,E)边覆盖的求解步骤:
1.将无向图拆点,即若在无向图中存在节点i,则将节点i拆为i1,i2分别位于二分图的X部和Y部.若存在边ij,则连接二分图的i1j2,i2j1。
2.原无向图中的节点数为|V|所以在构造的二分图有2*|V|个节点。在二分图中存在公式:
2*|V| = 2*二分图的最大匹配数 + 二分图中未匹配的点。其中二分图的最大匹配数+二分图中未匹配的点即覆盖了二分图中所有的点,相对于原无向图,相当于覆盖了每个点两次,即原边覆盖的最小 值转化为二分图的最大匹配数+二分图中未匹配的点的最小值。又有公式2*|V| = 2*二分图的最大匹配数 + 二分图中未匹配的点,可得:
二分图的最大匹配数+二分图中未匹配的点 = 2*|V| - 二分图的最大匹配数,又此结果为覆盖了原图所有顶点两次,所以结果应该除以2.

所以无向图的最小边覆盖 = |V| - 二分图的最大匹配数/2.

§3匈牙利算法的模板(至于这个算法原理就不讲了,自己太弱了)
const int maxm=10000;
const int maxn=2005;
struct node{
    int v,next;
};
node edge[maxm];
int head[maxn];
int cnt;
int vis[maxn];
int link[maxn];
int ans;
int n,m,k;
void add(int u,int v){
    edge[cnt].v=v;
    edge[cnt].next=head[u];
    head[u]=cnt++;
}
bool dfs(int u){
    int v;
    for(int i=head[u];i!=-1;i=edge[i].next){
            int v=edge[i].v;
            if(!vis[v]){
                vis[v]=1;
                if(link[v]==-1||dfs(link[v])){
                    link[v]=u;
                    return true;
                }
            }
        }
    return false;
}
void work(){
    for(int i=0;i<n;i++){
        memset(vis,0,sizeof(vis));
        if(dfs(i)){
            ans++;
        }
    }
    return;
}

§4我个人的理解(完全是个人 的拙见,仅供参考,说错了大神们勿喷)
我感觉二分图匹配关键是在于建模应用上,知道了如何建立正确的模型才是重点,下一步进行学习的网络流也是如此。
首先是对匈牙利模板的一点理解,我个人感觉,其实它只用到了左边集合到右边集合的边,而没有用到右边集合到左边集合的边,因为右边集合到左边集合的边是由link标记所替代,我不知道这样理解是不是正确。但从建模的方法上来说,这个理解应该是可行的。就是说如果你一开始知道一个大的集合是可以被分成左右两个集合的,那么你加边的时候只会加a-》b的边,而不用加b-》a的边,操作上是这样的。但是如果你并不清楚这个集合里那些点是左集合的点,哪些是有集合的点,那么你就要将这些点拆点,然后对于例如1-2的关系,你就要在左边连一条1-》2'的边,还要连一条2-》1’的边,然后最大匹配数就要除以2,具体的建图方法见poj3020,我的理解都是从这个题目上得到的,然后二分图是可以用网络流做的,这个就不多说了。
§5几种常见建模(大白上的几种与做题遇见的几种)

1.uva
11419 SAM I AM 类似的有poj3041
题意是给你一个nxm的棋盘,然后棋盘上有一些目标,你可以竖着打一列或者横着打一行,问你最少需要多少子弹才能把所有目标打完。
建模方法:将行与列当成左集合与右集合,目标就相当于连接左右集合的边,就是说目标坐标是(x,y)那么它把x行与y列连起来。然后求最小点覆盖就行了。

2.poj3020 Antenna Placement 类似的还有poj2446

这个题是最小边覆盖,千万别看网上的博客说是最小路径覆盖就信了,我就是因为想不通为啥是最小路径覆盖才研究了一天,也是我整理这个文章的初衷。
题意我懒得打了,自己去看原题吧。
思路有两种,
一是直接建成二分图,这个需要特别的技巧,即按奇偶建图。
因为是一个边覆盖两个点,那么这两个点的坐标和(x1+y1)和(x2+y2)必然一个奇数一个偶数,然后分别给图中的奇数偶数点依次从1开始标号,相邻的按其标号建图,建出来的就是二分图,且利用最小边覆盖 =
最大独立集 = n - 最大匹配做即可。
第二个就是上面提到的无向图边覆盖的做法,如果没想到第一种建图的方法时,就用笨方法,把点集扩大二倍,弄成一个无向图,而不是已分好左右集合的二分图,具体见上面的方法。
法一代码:
#include<iostream>
#include<cstdio>
#include<vector>
using namespace std;
vector<int>map[400];//按奇偶性建二分图 
int t,n,m;
int match[400],fy[400],ln,rn; 
int map1[41][11];//原始图 
int dirt[4][2]={0,1,0,-1,1,0,-1,0};

int path(int u)
{
 for(int i=0,j;i<map[u].size();i++)
 {
  j=map[u][i];
  if(!fy[j])
  {
   fy[j]=1;
   if(match[j]==-1||path(match[j]))
   {
    match[j]=u;
    return 1;
   }
  }
 }
 return 0;
}

int main()
{
 int i,j,x,y,ans;
 char tmp;
 scanf("%d",&t);
 while(t--)
 {
  scanf("%d%d",&n,&m);
  ln=rn=0;
  for(i=1;i<=n;i++)
  {
   getchar();
   for(j=1;j<=m;j++)
   {
    scanf("%c",&tmp);
    if(tmp=='*')
    {
     if((i+j)&1)//奇点 
      map1[i][j]=++ln;
     else//偶点 
      map1[i][j]=++rn;
    }
    else
     map1[i][j]=0;
   }
  }
  for(i=1;i<=ln;i++)
   map[i].clear();
  for(i=1;i<=rn;i++)
   match[i]=-1;
   
  //建二分图 
  for(i=1;i<=n;i++)
   for(j=1;j<=m;j++)
   {
    if(!map1[i][j]||!((i+j)&1))
     continue;
    for(int k=0;k<4;k++)
    {
     x=i+dirt[k][0];
     y=j+dirt[k][1];
     if(x<=0||x>n||y<=0||y>m||!map1[x][y])
      continue;
     map[map1[i][j]].push_back(map1[x][y]);
    }
   }
   
  //求最大匹配 
  ans=0;
  for(i=1;i<=ln;i++)
  {
   for(j=1;j<=rn;j++)
    fy[j]=0;
   if(path(i))
     ans++;
  }
  printf("%d\n",ln+rn-ans);
 }
 return 0;
}
法二代码:
#include<iostream>
using namespace std;

int ipmap[41][11];   //标记存在城市'*'的位置,并依次记录城市的编号
int ip;     //城市编号(最终是城市数量)
int V1,V2;  //二分图的两个顶点集
int M;      //最大二分匹配

bool city[401][401];   //标记两个城市之间是否能连通
                      //通过“拆点”操作,把每一个城市拆分为2个,分别属于所构造的二分图的两个点集
bool vist[401];
int link[401];

int dire_r[4]={-1,1,0,0};
int dire_c[4]={0,0,-1,1};   //分别对应四个方位 上 下 左 右

/*Hungary Algorithm*/

bool dfs(int x)
{
 for(int y=1;y<=V2;y++)
  if(city[x][y] && !vist[y])
  {
   vist[y]=true;
   if(link[y]==0 || dfs(link[y]))
   {
    link[y]=x;
    return true;
   }
  }
 return false;
}

void search(void)
{
 for(int x=1;x<=V1;x++)
 {
  memset(vist,false,sizeof(vist));
  if(dfs(x))
   M++;
 }
 return;
}

int main(void)
{
 int test,h,w;
 cin>>test;
 while(test--)
 {
  /*Initial*/

  memset(ipmap,0,sizeof(ipmap));
  memset(city,false,sizeof(city));
  memset(link,0,sizeof(link));
  ip=0;
  M=0;

  /*Read in the maps*/

  cin>>h>>w;

  int i,j;
  char temp;
  for(i=1;i<=h;i++)
   for(j=1;j<=w;j++)
   {
    cin>>temp;
    if(temp=='*')
     ipmap[i][j]=++ip;
   }

  /*Structure the Bipartite Graphs*/

  for(i=1;i<=h;i++)
   for(j=1;j<=w;j++)
    if(ipmap[i][j])
     for(int k=0;k<4;k++)
     {
      int x=i+dire_r[k];
      int y=j+dire_c[k];
      if(ipmap[x][y])
       city[ ipmap[i][j] ][ ipmap[x][y] ]=true;      //"拆点"操作是"顺便"被完成的
     }                                                    //二分图构造完毕后,之后的问题就和POJ3041一样处理了

  V1=V2=ip;

  /*增广轨搜索*/

  search();

  /*Output*/

  cout<<ip-M/2<<endl;   //无向二分图:最小路径覆盖数 = "拆点"前原图的顶点数 - 最大匹配数/2
 }
 return 0;
}


3.LA 3126 出租车 最小路径覆盖
题意自己去看。这个题中的时间是一个序,也就是说,如果每个客人是一个节点,如果同一个出租车在接完客人u以后来的及接客人v,连u->v,这个图是DAG,且它的最小路径覆盖就是答案。
#include <stdio.h>  
#include <string.h>  
#include <algorithm>
#define abs(X) ((X) > 0 ? (X) : -(X))
#define REP(I, X) for(int I = 0; I < X; ++I)
#define clear(A, X, SIZE) memset(A, X, sizeof(A[0]) * (SIZE + 1))  
using namespace std;  
const int maxN = 2000;  
const int maxE = 1000000;  
struct Edge{  
        int v, n;  
}edge[maxE];  
struct Node{
        int s, e, x1, y1, x2, y2;
}a[maxN];
int adj[maxN], cntE, vis[maxN], link[maxN];  
int n;
void addedge(int u, int v){  
        edge[cntE].v = v; edge[cntE].n = adj[u]; adj[u] = cntE++;  
}  
int find(int u){  
        for(int i = adj[u]; ~i; i = edge[i].n) if(!vis[edge[i].v]){  
                int v = edge[i].v;  
                vis[v] = 1;  
                if(link[v] == -1 || find(link[v])){  
                        link[v] = u;  
                        return 1;
                }  
        }  
        return 0;  
}  
int match(){  
        int ans = 0;  
        clear(link, -1, n);  
        REP(i, n){  
                clear(vis, 0, n);  
                ans += find(i);  
        }  
        return ans;  
}  
void work(){  
        int h, m;  
        scanf("%d", &n);
        clear(adj, -1, n);  
        cntE = 0;  
        REP(i, n){  
                scanf("%d:%d%d%d%d%d", &h, &m, &a[i].x1, &a[i].y1, &a[i].x2, &a[i].y2);
                a[i].s = h * 60 + m;
                a[i].e = a[i].s + abs(a[i].x1 - a[i].x2) + abs(a[i].y1 - a[i].y2);
        }
        REP(i, n) REP(j, n){ 
                if(a[i].e + abs(a[i].x2 - a[j].x1) + abs(a[i].y2 - a[j].y1) < a[j].s) addedge(i, j);
        }
        printf("%d\n", n - match());  
}  
int main(){ 
        int t; 
        for(scanf("%d", &t); t; --t) work();
        return 0;  
}


4,HDU 2768 猫狗大战 类似的有LA3415
题意:说每个小孩子都会喜欢一个猫(或狗),讨厌一个狗(猫),管理员会移除一些猫或狗,然后如果留下的动物是这个小孩喜欢的,那么他就高兴,问最多的高兴孩子的个数。
同样有两种做法。
一:把喜欢猫的人当成左集合,喜欢狗的人当成右集合,如果喜欢猫的人的这只猫正好是喜欢狗的人讨厌的猫就连条边,说明这俩人是不能同时出现,是矛盾的,然后两个集合内部是不会矛盾的,没有问题,这样建图就是一个二分图,直接按公式做就可以了。
队友的代码:
#include<cstdio>
#include<cstring>
using namespace std;
int n,m,k;
int w[550][550];
int link[550],used[550];
int dfs(int u)
{
    int v;
    for(v=1;v<=k;v++)
    {
        if(w[u][v]&&!used[v])
        {
            used[v]=1;
            if(link[v]==-1||dfs(link[v]))
            {
                link[v]=u;
                return 1;
            }
        }
    }
    return 0;
}
int main()
{
    int i,j,u,v,ans;
    int c[550];
    char a[550][2][5];
    while(~scanf("%d%d%d",&n,&m,&k))
    {
        for(i=1;i<=k;i++)
        {
            scanf("%s",&a[i][0]);
            scanf("%s",&a[i][1]);
            if(a[i][0][0]=='C') c[i]=0;
            else c[i]=1;
        }
        memset(w,0,sizeof(w));
        for(i=1;i<=k;i++)
            for(j=i+1;j<=k;j++)
                if(!strcmp(a[i][0],a[j][1])||!strcmp(a[i][1],a[j][0]))
                    w[i][j]=w[j][i]=1;
        memset(link,-1,sizeof(link));
        for(u=1,ans=k;u<=k;u++)
        {
            memset(used,0,sizeof(used));
            if(c[u]&&dfs(u)) ans--;
        }
        printf("%d\n",ans);
    }
    return 0;
}
第二个是,不分左右集合,矛盾的两个人之间就连边,求没有连边的观众的数目,就是最大独立集了,但是这个是无向图的,不是分好左右集合的二分图,所以按照最小边覆盖的那种方法做就行了:最小边覆盖 = 最大独立集 = n - 最大匹配。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: