二分图最大匹配+最小点覆盖

本文探讨了二分图的最大匹配问题,包括定义、性质和实现算法,以及如何利用这些概念解决实际问题,如选课和守卫城镇。同时,介绍了最小点覆盖的概念和求解策略。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

二分图最大匹配

二分图基本概念:一个无向图 G=<V, E> ,如果存在两个集合
X 、 Y ,使得 X∪Y=V , X∩Y=Φ ,并且 每一条边 e={x , y} 有
x∈X , y∈Y ,则称 G 为一个二分图 (bipartite graph) 。常用 <X,
E, Y> 来表示一个二分图。若对 X 中任一 x 及 Y 中任一 y 恰有一
边 e∈E ,使 e = {x, y}, 则称 G 为完全二分图 (complete
bipartite graph) 。当 |X| = m , |Y| = n 时,完全二分图 G 记为
Km,n 。

匹配:设 G=<V, E> 为二分图,如果 M⊆E ,并且 M 中没有任何两边
有公共端点。
最大匹配 :G 的所有匹配中边数最多的匹配称为最大匹配。最大匹配总
是存在但未必唯一。
完全匹配 : 若 X(Y) 中所有的顶点都是匹配 M 中的端点。则称 M 为
X(Y) 完全匹配。若 M 既 是 X- 完全匹配又是 Y- 完全匹配,则称 M 为
G 的完全匹配。完全匹配未必存在。

M 中边的端点称为 M- 顶点,其它顶点称为非 M- 顶点。
增广路径 : 除了起点和终点两个顶点为非 M- 顶点,其他路径
上所有的点都是 M- 顶 点。而且它的边为匹配边、非匹配边交
替出现。

增广路径的性质。

  1. 有奇数条边。
  2. 起点在二分图的左半边,终点在右半边。
  3. 路径上的点一定是一个在左半边,一个在右半边,交替出现。
  4. 整条路径上没有重复的点。
  5. 起点和终点都是目前还没有配对的点,而其它所有点都是已经配好对的。
  6. 路径上的所有第奇数条边都不在原匹配中,所有第偶数条边都出现在原匹
    配中。
  7. 最后,也是最重要的一条,把增广路径上的所有第奇数条边加入到原匹配中去,并把增 广路径中的所有第偶数条边从原匹配中删除(这个操作称为增广路径的取反),则新的 匹配数就比原匹配数增加了 1 个。
#include<bits/stdc++.h>
using namespace std;
const int maxn=1000+5;

int Map[maxn][maxn];
int linker[maxn];

bool used[maxn];
int n,p;  //n左半边的节点数,p右半边的节点数
bool dfs(int u){
    for(int v=1;v<=p;v++){
        if(Map[u][v]&&!used[v]){
           used[v]=true;
            if(linker[v]==-1||dfs(linker[v])){
               linker[v]=u;
               return true;
           }
        }
    }
    return false;
}

int solve(){
   int res=0;
   memset(linker,-1,sizeof(linker));
   for(int u=1;u<=n;u++){
         memset(used,false,sizeof(used));
        if(dfs(u)) res++;
   }
   return res;
}

问题 C: 选课
题目描述
一共有N个学生跟P门课程一个学生可以任意选一 门或多门课,你需写一个程序,判断给定的输入是否可以满足以下条件。
1.每个学生选的都是不同的课(即不能有两个学生选同一门课)
2.即P门课都被成功选过
注意:是课程匹配的学生,学生没课上没事。

输入
第一行一个T代表T(T ≤ 10)组数据
对于每组测试样例,先输入两个整数P, N(P课程数 N学生数)。
接着p行:第i行代表第i门课程,首先一个数字k代表对课程i感兴趣的学生的个数,接下来是k个对这门课感兴趣的同学的编号。0 < P ≤ 500 0 < n ≤ 500。最大边数不超过1000.

输出
若能满足上述要求输出YES否则输出NO。
样例输入
2
3 3
3 1 2 3
2 1 2
1 1
3 3
2 1 3
2 1 3
1 1
样例输出
YES
NO

二分图最大匹配
#include<bits/stdc++.h>
using namespace std;
const int maxn=1000+5;

int Map[maxn][maxn];
int linker[maxn];

bool used[maxn];
int n,p;
bool dfs(int u){
    for(int v=1;v<=p;v++){
        if(Map[u][v]&&!used[v]){
           used[v]=true;
            if(linker[v]==-1||dfs(linker[v])){
               linker[v]=u;
               return true;
           }
        }
    }
    return false;
}

int solve(){
   int res=0;
   memset(linker,-1,sizeof(linker));
   for(int u=1;u<=n;u++){
         memset(used,false,sizeof(used));
        if(dfs(u)) res++;
   }
   return res;
}
int main(){
  int t;
  cin>>t;
  while(t--){
      memset(Map,0,sizeof(Map));
      int k,num;
      cin>>p>>n;
      for(int i=1;i<=p;i++){
          cin>>k;
          for(int j=1;j<=k;j++){
             cin>>num;
             Map[num][i]=1;
          }
      }
      if(solve()==p)
         cout<<"YES"<<endl;
      else
        cout<<"NO"<<endl;
  }
}

最小点覆盖

对于图 G=(V,E) 来说,最小点覆盖指的是从 V 中取尽
量少的点组成一个集合,使得 E 中所有的边都与取出来的点相连。
也就是说,设 V‘ 是图 G 的一个顶点覆盖,则对于图中的任意一
条边 (u,v) ,要么 u 属于集合 V’ ,要么 v 属于集合 V‘ 。在
V‘ 中除去任何元素后 V’ 不在是顶点覆盖,则 V‘ 是极小顶点覆
盖。称 G 的所有顶点覆盖中顶点个数最少的覆盖为最小点覆盖。

贪心策略:同样需要按照反方向的深度优先遍历序列来进行贪心 .
每检查一个结点 , 如果当前点和当前点的父节点都不属于顶点覆盖
集合 , 则将父节点加入到顶点覆盖集合 , 并标记当前节点和其父节
点都被覆盖 . 注意此贪心策略不适用于根节点 , 所以要把根节点排
除在外 .

#include<bits/stdc++.h>
using namespace std;
const int maxn=1000+5;

struct edge{
   int to,next;
}edge[maxn*maxn];
int head[maxn];
int cnt;

int vis[maxn];
int pre[maxn];
int newpos[maxn];
int now;
int n;
bool s[maxn],Set[maxn]; //s标记被覆盖,set最小覆盖集合
void init(){
   memset(head,-1,sizeof(head));
   memset(vis,0,sizeof(vis));
   memset(pre,-1,sizeof(pre));
   memset(newpos,0,sizeof(newpos));
   memset(s,false,sizeof(s));
   memset(Set,false,sizeof(false));
   cnt=1;
   now=0;
}

void addedge(int u,int v){
   edge[cnt].to=v;
   edge[cnt].next=head[u];
   head[u]=cnt++;
}

void DFS(int u){
 //cout<<"dfs1"<<endl;
    vis[u]=1;
    newpos[now++]=u;
    for(int i=head[u];~i;i=edge[i].next){
         int v=edge[i].to;
         if(!vis[v]){
            pre[v]=u;
            DFS(v);
         }
    }
}

int MVC(){
  int ans=0;
  for(int i=n-1;i>=1;i--){  //排除根节点
    int u=newpos[i];
    if(!s[u]&&!s[pre[u]]){
        Set[pre[u]]=true;
        s[u]=true;
        s[pre[u]]=true;
        ans++;
    }
  }
  return ans;
}

int solve(){
    DFS(0);
    return MVC();
}

问题 B: 守卫城镇
题目描述
边边喜欢玩电脑游戏,特别是战略游戏,但有时他无法找到解决方案,速度不够快,那么他很伤心。现在,他有以下的问题。他必须捍卫一个中世纪的城市,城市间的所有道路刚好形成了一颗以城市为结点以城市间的路为边的树。为了节省士兵的个数且使他们可以观察到所有的路(如果两个城镇间有路则士兵可以观测到),他打算为尽可能少的城镇中派遣观察士兵(每个城镇一位士兵),使得这些士兵可以观测到所有路的情况,问你最少需要多少名士兵。你能帮助他吗?
输入
输入包含多组测试样例。
对于每组测试样例,输入第一行包含一个数字n(n ≤ 1000)表示结点的个数。
接下来n行,对于每行,第一个数字表示结点u,第二个数字表示结点u的领边条数m,接下来m个数字表示与结点u相连结点的编号。保证边数小于10000。

输出
输出一个数字表示最小士兵数目。
样例输入
4
0 1 1
1 2 2 3
2 0
3 0
5
3 3 1 4 2
1 1 0
2 0
0 0
4 0
样例输出
1
2

最小点覆盖
#include<bits/stdc++.h>
using namespace std;
const int maxn=1000+5;

struct edge{
   int to,next;
}edge[maxn*maxn];
int head[maxn];
int cnt;

int vis[maxn];
int pre[maxn];
int newpos[maxn];
int now;
int n;
bool s[maxn],Set[maxn]; //s标记被覆盖,set最小覆盖集合
void init(){
   memset(head,-1,sizeof(head));
   memset(vis,0,sizeof(vis));
   memset(pre,-1,sizeof(pre));
   memset(newpos,0,sizeof(newpos));
   memset(s,false,sizeof(s));
   memset(Set,false,sizeof(false));
   cnt=1;
   now=0;
}

void addedge(int u,int v){
   edge[cnt].to=v;
   edge[cnt].next=head[u];
   head[u]=cnt++;
}

void DFS(int u){
 //cout<<"dfs1"<<endl;
    vis[u]=1;
    newpos[now++]=u;
    for(int i=head[u];~i;i=edge[i].next){
         int v=edge[i].to;
         if(!vis[v]){
            pre[v]=u;
            DFS(v);
         }
    }
}

int MVC(){
  int ans=0;
  for(int i=n-1;i>=1;i--){  //排除根节点
    int u=newpos[i];
    if(!s[u]&&!s[pre[u]]){
        Set[pre[u]]=true;
        s[u]=true;
        s[pre[u]]=true;
        ans++;
    }
  }
  return ans;
}

int solve(){
    DFS(0);
    return MVC();
}

int main(){
    while(cin>>n){
      init();
        for(int i=1;i<=n;i++){
            int u,m,v;
            cin>>u>>m;
            for(int j=1;j<=m;j++){
               cin>>v;
               addedge(u,v);
               addedge(v,u);
            }
        }
        cout<<solve()<<endl;
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值