UVA 11419 SAM I AM(最大二分匹配&最小点覆盖:König定理)

时间:2021-10-21 06:12:45

题意:在方格图上打小怪,每次可以清除一整行或一整列的小怪,问最少的步数是多少,又应该在哪些位置操作(对输出顺序没有要求)。

分析:最小覆盖问题

  这是一种在方格图上建立的模型:令S集表示“行”,T集表示“列”,那么小怪站的位置w(i,j),就是二分图上的边。如此建图,那么每次清除,就是把与某个点相连的边全部清除,问最少选择多少个点。(这也是最小点覆盖的概念:选择尽量少的点,使得每条边至少有一个端点被选中)

  这里有一个König定理:最大二分匹配数==最小覆盖点数。

  既然是求最小点覆盖,那么自然是选那些所连边数多的点,不过貌似不好安排啊?

  先从简单问题开始讨论:找到必然要选的点。对于一棵树,为了覆盖到叶子节点所在的边,必须选择该条边上的两个端点之一。不用问,按照最小覆盖的原则,必然是要选非叶子节点了。把所有与选择的点相连的边都处理掉,就相当于是这棵树的最下面一层被砍掉了(叶子的爷爷成为了新的叶子)。那么对于剩下的树,仍然可以这样操作,最终得到了最小点覆盖。幸运的是,树属于二分图,而二分图虽然不一定是树(存在偶环),但可以构建出匈牙利树(交错树),是可以借鉴的。

  聪明的地球人提出了如下算法:

  对于一个二分图,求出其最大匹配。然后取左侧(S侧)所有的未匹配点,按照增广路算法,寻找交错路径(路径上的点都是匹配点),标记掉路径上的所有点(不存在重复标记)。那么左侧(S侧)所有的未标记点,与右侧(T侧)所有的标记点,就是实现最小点覆盖的点。

UVA 11419 SAM I AM(最大二分匹配&最小点覆盖:König定理)

  1、为什么这些点可以覆盖所有边?

  如果还有没有覆盖的边,那么一定是一些左端点已标记,右端点未标记的边。而通过我们算法中的构造,不存在这样的边。为什么呢?回忆一下增广路算法,我们的起点选择的是S侧的未匹配点,之后选择一条边走到T侧(该点必然是匹配点);因为要走交错路径,就沿着匹配边回到了S侧...可以想象成每次只是从S侧走到T侧,然后沿匹配边回到S侧。所以不存在S端已标记,T端未标记的线段。所以所有边都被覆盖了。

  2、为什么这些点的在数量上等于最大二分匹配的边数?

  因为每条匹配边上恰好选择一个点。为什么是这样呢?注意,最小点覆盖的点集包括:S侧的未标记点,和T侧的标记点。有增广路算法可知(同上一个问题相似),若T侧的点被标记,那么同一条匹配边上的S侧的点必然也被标记——点集中不包含同一条匹配边上的两个点。S侧的未标记点,实际上是除去未匹配点,以及跟随T侧点而被标记的点后,剩余的(即选择了那些T侧未被标记的匹配边)。所以两侧的点把所有的匹配边都包含了。所以最大二分匹配数==最小覆盖点数。

  3、为什么这些点就是最小值?

  这个问题最简单:及使图上只有n条匹配边,我们都要用n个点才能覆盖。若再少,至少漏掉一条匹配边。所以是最小值。

(注:难得自己动手改了张图...圆圈是覆盖点,方格是标记点,箭头是交错路径,蓝线是匹配边。)

可以看看这个链接,我认为上面的解释更通俗一些。

http://www.matrix67.com/blog/?s=%E6%9C%80%E5%B0%8F%E8%A6%86%E7%9B%96%E7%82%B9

UVA 11419 SAM I AM(最大二分匹配&最小点覆盖:König定理)UVA 11419 SAM I AM(最大二分匹配&最小点覆盖:König定理)
  1 #include<cstdio>
  2 #include<cstring>
  3 #include<vector>
  4 #include<algorithm>
  5 #define clr(a,m) memset(a,m,sizeof(a))
  6 #define rep(i,a,b) for(int i=a;i<=b;i++)
  7 using namespace std;
  8 
  9 const int MAXN=1111;
 10 
 11 int n,m,cnt,ans;
 12 
 13 vector<int>G[MAXN];
 14 vector<int>row,col;
 15 
 16 int left[MAXN],right[MAXN],S[MAXN],T[MAXN];
 17 
 18 void init()
 19 {
 20     rep(i,1,n)
 21         G[i].clear();
 22 }
 23 
 24 void read()
 25 {
 26     int x,y;
 27     init();
 28     rep(i,1,cnt){
 29         scanf("%d%d",&x,&y);
 30         G[x].push_back(y);
 31     }
 32 }
 33 
 34 bool match(int u)
 35 {
 36     S[u]=true;
 37     int sz=G[u].size();
 38     rep(i,0,sz-1){
 39         int v=G[u][i];
 40         if(!T[v]){
 41             T[v]=true;
 42             if(!left[v]||match(left[v])){
 43                 left[v]=u;
 44                 right[u]=v;
 45                 return true;
 46             }
 47         }
 48     }
 49     return false;
 50 }
 51 
 52 void AP()
 53 {
 54     rep(i,1,m)left[i]=0;//n和m写反了
 55     rep(i,1,n)right[i]=0;
 56 
 57     ans=0;
 58     rep(i,1,n){
 59         rep(j,1,n)S[j]=0;
 60         rep(j,1,m)T[j]=0;
 61         if(match(i))
 62             ans++;
 63     }
 64 }
 65 
 66 void mincover()
 67 {
 68     rep(i,1,n)S[i]=0;//重新标记增广路
 69     rep(i,1,m)T[i]=0;
 70 
 71     rep(i,1,n)//选择S侧的未标记点
 72         if(!right[i])
 73             match(i);
 74 
 75     row.clear();
 76     col.clear();
 77     rep(i,1,n)
 78         if(!S[i])
 79             row.push_back(i);
 80     rep(i,1,m)
 81         if(T[i])
 82             col.push_back(i);
 83 }
 84 
 85 void print()
 86 {
 87     printf("%d",ans);
 88 int sz=col.size();
 89     rep(i,0,sz-1)
 90         printf(" c%d",col[i]);
 91      sz=row.size();
 92     rep(i,0,sz-1)
 93         printf(" r%d",row[i]);
 94 
 95 
 96 
 97     printf("\n");
 98 }
 99 
100 int main()
101 {
102     while(~scanf("%d%d%d",&n,&m,&cnt))
103     {
104         if(!n&&!m&&!cnt)
105             return 0;
106         read();
107         AP();
108         mincover();
109         print();
110     }
111     return 0;
112 }
View Code