hihoCoder 二分图系列(模板)

#1121 : 二分图一•二分图判定

时间限制: 10000ms
单点时限: 1000ms
内存限制: 256MB
描述

大家好,我是小Hi和小Ho的小伙伴Nettle,从这个星期开始由我来完成我们的Weekly。

新年回家,又到了一年一度大龄剩男剩女的相亲时间。Nettle去姑姑家玩的时候看到了一张姑姑写的相亲情况表,上面都是姑姑介绍相亲的剩男剩女们。每行有2个名字,表示这两个人有一场相亲。由于姑姑年龄比较大了记性不是太好,加上相亲的人很多,所以姑姑一时也想不起来其中有些人的性别。因此她拜托我检查一下相亲表里面有没有错误的记录,即是否把两个同性安排了相亲。

OK,让我们愉快的暴力搜索吧!

才怪咧。

对于拿到的相亲情况表,我们不妨将其转化成一个图。将每一个人作为一个点(编号1..N),若两个人之间有一场相亲,则在对应的点之间连接一条无向边。(如下图)

因为相亲总是在男女之间进行的,所以每一条边的两边对应的人总是不同性别。假设表示男性的节点染成白色,女性的节点染色黑色。对于得到的无向图来说,即每一条边的两端一定是一白一黑。如果存在一条边两端同为白色或者黑色,则表示这一条边所表示的记录有误。

由于我们并不知道每个人的性别,我们的问题就转化为判定是否存在一个合理的染色方案,使得我们所建立的无向图满足每一条边两端的顶点颜色都不相同

那么,我们不妨将所有的点初始为未染色的状态。随机选择一个点,将其染成白色。再以它为起点,将所有相邻的点染成黑色。再以这些黑色的点为起点,将所有与其相邻未染色的点染成白色。不断重复直到整个图都染色完成。(如下图)

在染色的过程中,我们应该怎样发现错误的记录呢?相信你一定发现了吧。对于一个已经染色的点,如果存在一个与它相邻的已染色点和它的颜色相同,那么就一定存在一条错误的记录。(如上图的4,5节点)

到此我们就得到了整个图的算法:

  1. 选取一个未染色的点u进行染色
  2. 遍历u的相邻节点v:若v未染色,则染色成与u不同的颜色,并对v重复第2步;若v已经染色,如果 u和v颜色相同,判定不可行退出遍历。
  3. 若所有节点均已染色,则判定可行。

接下来就动手写写吧!

输入

第1行:1个正整数T(1≤T≤10)

接下来T组数据,每组数据按照以下格式给出:

第1行:2个正整数N,M(1≤N≤10,000,1≤M≤40,000)

第2..M+1行:每行两个整数u,v表示u和v之间有一条边

输出

第1..T行:第i行表示第i组数据是否有误。如果是正确的数据输出”Correct”,否则输出”Wrong”


样例输入

2
5 5
1 2
1 3
3 4
5 2
1 5
5 5
1 2
1 3
3 4
5 2
3 5
样例输出

Wrong
Correct

题目链接:http://hihocoder.com/problemset/problem/1121

代码清单:

#include<map>
#include<queue>
#include<stack>
#include<cmath>
#include<cstdio>
#include<string>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
typedef long long ll;

const int maxn = 10000 + 5;
const int maxv = 40000 + 5;

int T;
int N,M,a,b;
vector<int>graph[maxn];
int mark[maxn];
bool ok;

void init(){
    for(int i=0;i<maxn;i++)
        graph[i].clear();
    memset(mark,-1,sizeof(mark));
    ok=true;
}

void input(){
    scanf("%d%d",&N,&M);
    for(int i=0;i<M;i++){
        scanf("%d%d",&a,&b);
        graph[a].push_back(b);
        graph[b].push_back(a);
    }
}

void dfs(int u){
    for(int i=0;i<graph[u].size();i++){
        int v=graph[u][i];
        if(mark[v]==-1){
            mark[v]=mark[u]^1;
            dfs(v);
            if(!ok) return ;
        }
        else{
            if(mark[v]==mark[u]){
                ok=false;
                return ;
            }
        }
    }
}

void solve(){
    for(int i=1;i<=N;i++){
        if(mark[i]==-1) dfs(i);
        if(!ok) break;
    }
    if(ok) printf("Correct\n");
    else printf("Wrong\n");
}



int main(){
    scanf("%d",&T);
    while(T--){
        init();
        input();
        solve();
    }return 0;
}


#1122 : 二分图二•二分图最大匹配之匈牙利算法

时间限制: 10000ms
单点时限: 1000ms
内存限制: 256MB

描述

上一回我们已经将所有有问题的相亲情况表剔除了,那么接下来要做的就是安排相亲了。因为过年时间并不是很长,所以姑姑希望能够尽可能在一天安排比较多的相亲。由于一个人同一天只能和一个人相亲,所以要从当前的相亲情况表里选择尽可能多的组合,且每个人不会出现两次。不知道有没有什么好办法,对于当前给定的相亲情况表,能够算出最多能同时安排多少组相亲呢?

同样的,我们先将给定的情况表转换成图G=(V,E)。在上一回中我们已经知道这个图可以被染成黑白两色。不妨将所有表示女性的节点记为点集A,表示男性的节点记为点集B。则有A∪B=V。由问题可知所有边e的两个端点分别属于AB两个集合。则可以表示成如下的图:

同样的,我们将所有的边分为两个集合。集合S和集合M,同样有S∪M=E。边集S表示在这一轮相亲会中将要进行的相亲,边集M表示在不在这一次进行。对于任意边(u,v) ∈ S,我们称u和v为一组匹配,它们之间相互匹配。在图G,我们将边集S用实线表示,边集M用虚线表示。得到下图:

则原问题转化为,最多能选择多少条边到集合S,使得S集合中任何两条边不相邻(即有共同的顶点)。显然的,|S|<=Min{|A|, |B|}。

那么能不能找到一个算法,使得能够很容易计算出尽可能多的边能够放入集合S?我们不妨来看一个例子:

对于已经匹配的点我们先不考虑,我们从未匹配的点来做。这里我们选择A集合中尚未匹配的点(A3和A4)考虑:

对于A3点,我们可以发现A3与B4右边相连,且都未匹配。则直接将(A3,B4)边加入集合S即可。

对于A4点,我们发现和A4相连的B3,B4点都已经匹配了。但是再观察可以发现,如果我们将A2和B2相连,则可以将B3点空出来。那么就可以同时将(A2,B2),(A4,B3)相连。将原来的一个匹配变成了两个匹配。

让我们来仔细看看这一步:我们将这次变换中相关联的边标记出来,如下图所示紫色的3条边(A2,B2),(A2,B3),(A4,B3)。

这三条边构成了一条路径,可以发现这条路径有个非常特殊的性质。虚线和实线相互交错,并且起点和终点都是尚未匹配的点,且属于两个不同的集合。我们称这样的路径为交错路径。

再进一步分析,对于任意一条交错路径,虚线的数量一定比实线的数量多1。我们将虚线和实线交换一下,就变成了下面的图:

在原来1个匹配的基础上,我们得到了2个新的匹配,S集合边的数量也增加了1。并且原来在已经匹配的点仍然是已经匹配的状态。

再回头看看A3点匹配时的情况:对于(A3,B4)这一条路径,同样满足了交错路径的性质。

至此我们得到了一个找新匹配的有效算法:

选取一个未匹配的点,查找是否存在一条以它为起点的交错路径。若存在,将该交错路径的边虚实交换。否则在当前的情况下,该点找不到可以匹配的点。

又有对于已经匹配的点,该算法并不会改变一个点的匹配状态。所以当我们对所有未匹配的点都计算过后,仍然没有交错路径,则不可能找到更多的匹配。此时S集合中的边数即为最大边数,我们称为最大匹配数。

那么我们再一次梳理整个算法:

1. 依次枚举每一个点i;
2. 若点i尚未匹配,则以此点为起点查询一次交错路径。

最后即可得到最大匹配数。

在这个基础上仍然有两个可以优化的地方:

1.对于点的枚举:当我们枚举了所有A中的点后,无需再枚举B中的点,就已经得到了最大匹配。
2.在查询交错路径的过程中,有可能出现Ai与Bj直接相连,其中Bj为已经匹配的点,且Bj之后找不到交错路径。之后又通过Ai查找到了一条交错路径{Ai,Bx,Ay,…,Az,Bj}延伸到Bj。由于之前已经计算过Bj没有交错路径,若此时再计算一次就有了额外的冗余。所以我们需要枚举每个Ai时记录B集合中的点是否已经查询过,起点不同时需要清空记录。

伪代码:

Function FindPath(u)
    For v∈u的相邻节点
       标记v已经查询过
       If v未匹配 or FindPath(v的匹配的点) Then
            更改u的匹配为v
            Return Ture
        End If
    End For
Return False

For i ∈ V
    清空标记
   FindPath(i)
End If


输入

第1行:2个正整数,N,M(N表示点数 2≤N≤1,000,M表示边数1≤M≤5,000)
第2..M+1行:每行两个整数u,v,表示一条无向边(u,v)

输出

第1行:1个整数,表示最大匹配数

样例输入

5 4
3 2
1 3
5 4
1 5
样例输出

2
题目链接: http://hihocoder.com/problemset/problem/1122

代码清单:

#include<map>
#include<queue>
#include<stack>
#include<cmath>
#include<cstdio>
#include<string>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
typedef long long ll;

const int maxn = 1000 + 5;
const int maxv = 5000 + 5;

int N,M,a,b;
vector<int>graph[maxn];
bool vis[maxn];
int mark[maxn];

void init(){
    for(int i=0;i<maxn;i++)
        graph[i].clear();
    memset(mark,-1,sizeof(mark));
}

void input(){
    scanf("%d%d",&N,&M);
    for(int i=0;i<M;i++){
        scanf("%d%d",&a,&b);
        graph[a].push_back(b);
        graph[b].push_back(a);
    }
}

bool dfs(int u){
    for(int i=0;i<graph[u].size();i++){
        int v=graph[u][i];
        if(vis[v]) continue;
        vis[v]=true;
        if(mark[v]==-1 || dfs(mark[v])){
            mark[v]=u;
            return true;
        }
    }return false;
}

void solve(){
    int ans=0;
    for(int i=1;i<=N;i++){
        memset(vis,false,sizeof(vis));
        if(dfs(i)) ans++;
    }
    printf("%d\n",ans/2);
}

int main(){
    init();
    input();
    solve();
    return 0;
}

/* 第二种写法 */
/*
#include<map>
#include<queue>
#include<stack>
#include<cmath>
#include<cstdio>
#include<string>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
typedef long long ll;

const int maxn = 1000 + 5;
const int maxv = 5000 + 5;

int N,M,a,b;
vector<int>graph[maxn];
bool vis[maxn];
int mark[maxn];

void init(){
    for(int i=0;i<maxn;i++)
        graph[i].clear();
    memset(mark,-1,sizeof(mark));
}

void input(){
    scanf("%d%d",&N,&M);
    for(int i=0;i<M;i++){
        scanf("%d%d",&a,&b);
        graph[a].push_back(b);
        graph[b].push_back(a);
    }
}

bool dfs(int u){
    for(int i=0;i<graph[u].size();i++){
        int v=graph[u][i];
        if(vis[v]) continue;
        vis[v]=true;
        if(mark[v]==-1 || dfs(mark[v])){
            mark[v]=u;
            mark[u]=v;
            return true;
        }
    }return false;
}

void solve(){
    int ans=0;
    for(int i=1;i<=N;i++){
        if(mark[i]==-1){
            memset(vis,false,sizeof(vis));
            if(dfs(i)) ans++;
        }
    }
    printf("%d\n",ans);
}

int main(){
    init();
    input();
    solve();
    return 0;
}

*/

#1127 : 二分图三·二分图最小点覆盖和最大独立集

时间限制: 10000ms
单点时限: 1000ms
内存限制: 256MB
描述

在上次安排完相亲之后又过了挺长时间,大家好像都差不多见过面了。不过相亲这个事不是说那么容易的,所以Nettle的姑姑打算收集一下之前的情况并再安排一次相亲。所以现在摆在Nettle面前的有2个问题:

1.姑姑想要了解之前所有相亲的情况。对于任一个一次相亲,只要跟参与相亲的两人交流就可以得到这次相亲的情况。如果一个人参加了多次相亲,那么跟他交流就可以知道这几次相亲的情况。那么问题来了,挖掘技术到底哪家强姑姑最少需要跟多少人进行交流可以了解到所有相亲的情况。

2.因为春节快要结束了,姑姑打算给这些人再安排一次集体相亲。集体相亲也就是所有人在一起相亲,不再安排一对一对的进行相亲。但是姑姑有个条件,要求所有参与相亲的人之前都没有见过。也就是说在之前的每一次相亲中的两人不会被同时邀请来参加这次集体相亲。那么问题又来了,姑姑最多可以让多少人参与这个集体相亲。

(问题1解答):最小点覆盖

同样的转化为图G=(V,E),则问题转化为:
在图G中选取尽可能少的点,使得图中每一条边至少有一个端点被选中。
这个问题在二分图问题中被称为最小点覆盖问题。即用最少的点去覆盖所有的边。
结论:由König定理可知最小点覆盖的点数 = 二分图最大匹配

证明:请戳这里

(问题2解答):最大独立集

依旧转化为图G=(V,E),则问题转化为:
在图G中选取尽可能多的点,使得任意两个点之间没有连边。
这个问题在二分图问题中被称为最大独立集问题。
结论:最大独立集的点数 = 总点数 - 二分图最大匹配

证明:

假设最大独立集的点数为|U|,二分图最大匹配的匹配数为|M|,最大匹配中所有顶点集合为EM

先证明 |U|≤|V|-|M|

M中任意一条边的两个端点是连接的,所有对于M中的边必有一个端点不在|U|集合中,所以|M|≤|V|-|U|

再证明|U|≥|V|-|M|

首先我们知道一定有|U|≥|V|-|EM|,即将最大匹配的点删除之后,剩下的点一定都不相连。
接下来我们考虑能否将M集合中的一个端点放入U中:
假设(x,y)属于M,存在(a,x),(b,y),且a,b都在U中,则会出现两种情况:


输入

第1行:2个正整数,N,M(N表示点数 2≤N≤1,000,M表示边数1≤M≤5,000)
第2..M+1行:每行两个整数u,v,表示一条无向边(u,v)

输出

第1行:1个整数,表示最小点覆盖数
第2行:1个整数,表示最大独立集数

样例输入

5 4
3 2
1 3
5 4
1 5
样例输出

2
3
题目链接: http://hihocoder.com/problemset/problem/1127

代码清单:

#include<map>
#include<queue>
#include<stack>
#include<cstdio>
#include<string>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
typedef long long ll;

const int maxn = 1000 + 5;
const int maxv = 5000 + 5;

int N,M,a,b;
bool vis[maxn];
int match[maxn];
vector<int>graph[maxn];

void input(){
    scanf("%d%d",&N,&M);
    for(int i=0;i<M;i++){
        scanf("%d%d",&a,&b);
        graph[a].push_back(b);
        graph[b].push_back(a);
    }
}

bool dfs(int u){
    for(int i=0;i<graph[u].size();i++){
        int v=graph[u][i];
        if(vis[v]) continue;
        vis[v]=true;
        if(match[v]==-1 || dfs(match[v])){
            match[v]=u;
            match[u]=v;
            return true;
        }
    }return false;
}

void solve(){
    int ans=0;
    memset(match,-1,sizeof(match));
    for(int i=1;i<=N;i++){
        if(match[i]==-1){
            memset(vis,false,sizeof(vis));
            if(dfs(i)) ans++;
        }
    }
    printf("%d\n%d\n",ans,N-ans);
}

int main(){
    input();
    solve();
    return 0;
}

/* 第二种写法 */
/*

#include<map>
#include<queue>
#include<stack>
#include<cstdio>
#include<string>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
typedef long long ll;

const int maxn = 1000 + 5;
const int maxv = 5000 + 5;

int N,M,a,b;
bool vis[maxn];
int match[maxn];
vector<int>graph[maxn];

void input(){
    scanf("%d%d",&N,&M);
    for(int i=0;i<M;i++){
        scanf("%d%d",&a,&b);
        graph[a].push_back(b);
        graph[b].push_back(a);
    }
}

bool dfs(int u){
    for(int i=0;i<graph[u].size();i++){
        int v=graph[u][i];
        if(vis[v]) continue;
        vis[v]=true;
        if(match[v]==-1 || dfs(match[v])){
            match[v]=u;
            return true;
        }
    }return false;
}

void solve(){
    int ans=0;
    memset(match,-1,sizeof(match));
    for(int i=1;i<=N;i++){
        memset(vis,false,sizeof(vis));
        if(dfs(i)) ans++;
    }
    printf("%d\n%d\n",ans/2,N-ans/2);
}

int main(){
    input();
    solve();
    return 0;
}

*/


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值