先给出二分图的定义:
一张图 G 是二分图当且仅当 G 的点集 V 可以分为两个点集 V0,V1,满足 V0∪V1=V,V0∩V1=∅,且对于 G 的每条边 e,其两个端点分别属于不同的点集。
简单来说,一张图是二分图,当且仅当它的点可以被分成两部分,而这张图上的所有边的两个端点,都属于两个不同的部分。
那么如何判断一张图是不是二分图呢?注意到每一条边的两个顶点都属于两个不同的集合,所以我们可以从任意一个点开始,将与其直接相连的点染成相反的颜色,用来代表两个不同的集合,并对其它点重复此过程,如果一张图染下来没有出现颜色矛盾,这就是一张二分图了。
bool dfs(ll u,ll co)
{
color[u]=co;
for(int i=head[u];i!=-1;i=edge[i].next)
{
ll y=edge[i].t;
if(color[y]==co) return 0;
if(color[y]==-co) return 1;
if(color[y]==0&&!dfs(y,-co)) return 0;
}
return 1;
}
接下来讲一讲最大二分图匹配(主要是刚发现dinic也可以解决这个问题。。。)
给出板子题
首先是最正宗的方法,匈牙利匹配。
大概的思路就是:(绿与被绿)
处理左部图,看左边最多能有多少个点能匹配,那么那个就是答案。
遍历左部图,用一个时间戳来标记右部图中每一个点被标记的对应时间。每次从左边找一个点S,并从右边找一个与其相连的点S'。如果右边的这个点还没有被匹配。。。啥也别说,拉走!
如果不巧这个点S'已经有伴了,我们就可以选择绿了它的伴侣让它的伴侣A成人之美。如果它的伴侣A还能找到别的匹配,那大家皆大欢喜(也就是一个协商的过程),但是如果A没别人可以选了,那S也只能找下家,并重复上述操作知道找到匹配。如果找不到,那就找不到吧。。。
思路是这样,至于证明。。。指路别的博客)
贴一个代码:(还算简单,因为上文中协商的过程可以用dfs来实现)
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll N=101000;
#define endl '\n'
const ll inf=1<<30;
struct ty{
ll t,next;
}edge[N<<1];
ll head[N];
ll cnt=0;
void add(ll a,ll b)
{
edge[++cnt].t=b;
edge[cnt].next=head[a];
head[a]=cnt;
}
ll n,m,e;
ll a,b;
ll ti=0;//时间戳
ll ans=0;
ll df[N];//被匹配的时间
ll match[N];//匹配对象
bool dfs(ll u,ll tim)
{
for(int i=head[u];i!=-1;i=edge[i].next)
{
ll y=edge[i].t;
if(df[y]==tim) continue;
df[y]=tim;
if(!match[y]/*还没配对,直接拉走*/||dfs(match[y],tim))
{
match[y]=u;
return 1;
}
}
return 0;
}
int main()
{
cin>>n>>m>>e;
memset(head,-1,sizeof head);
while(e--)
{
cin>>a>>b;
add(a,b);
}
for(int i=1;i<=n;++i)
{
if(dfs(i,++ti)) ans++;
}
cout<<ans<<endl;
return 0;
}
然后是一个网络流的做法。对于每一条边,可以将其想象成一个管道,那么两个点相连,就是可以通过大小为1的流量。将左部图与一个超级源点相连(出水口),将右部图与一个超级聚点相连(进水口),那么就可以转化为最大流问题了。
那就是一个快乐的套板子了!
不过EK可能比较慢(好像比匈牙利还慢),dinic就很好
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll N=101000;
#define endl '\n'
const ll inf=1<<30;
struct ty{
ll t,l,next;
}edge[N<<1];
ll cnt=1;//后面要让第i条边^1后能得到它的反向边(与它相邻)
ll head[N];
inline ll read(){
int x=0;
char c=getchar();
while(c>'9'||c<'0')c=getchar();
while(c>='0'&&c<='9')x=x*10+c-'0',c=getchar();
return x;
}
void add(ll a,ll b,ll c)
{
edge[++cnt].t=b;
edge[cnt].l=c;
edge[cnt].next=head[a];
head[a]=cnt;
}
ll inque[N];
ll dep[N];//用于分层
ll cur[N];//记录弧
ll vis;//这次是否能到达终点
ll ans=0;
struct Pre
{
ll v;//前一个节点
ll edge;//前一条边
}pre[N];
ll n,m,s,t,e;
ll a,b,c;
bool bfs()//用于分层
{
// memset(inque,0,sizeof inque);
// memset(dep,0x3f,sizeof dep);
for(int i=0;i<=t;++i)
{
cur[i]=head[i];
dep[i]=0x3f3f3f3f3f3f3f3f;
inque[i]=0;
}
dep[s]=0;//起点
queue<ll> q;
q.push(s);
while(!q.empty())
{
ll ty=q.front();
q.pop();
inque[ty]=0;//
for(int i=head[ty];i!=-1;i=edge[i].next)
{
ll y=edge[i].t;
if(dep[y]<=dep[ty]+1) continue;//无需更新
if(edge[i].l==0) continue;
dep[y]=dep[ty]+1;
if(inque[y]) continue;//看是否在队里
inque[y]++;
q.push(y);
}
}
if(dep[t]!=0x3f3f3f3f3f3f3f3f) return 1;
return 0;
}
ll dfs(ll u,ll flow)//节点,当前最小流量
{
ll rl=0;
if(u==t)
{
vis=1;
ans+=flow;
return flow;
}
ll used=0;//该点用过的流量
for(int i=cur[u]/*是cur!这里不是head(当前弧优化)*/;i!=-1;i=edge[i].next)
{
cur[u]=i;//修改当前弧
ll y=edge[i].t;
if(dep[y]==dep[u]+1&&edge[i].l)
{
rl=dfs(y,min(flow-used,edge[i].l));//减去后的费用拿去比较
if(rl==0) continue;
used+=rl;
edge[i].l-=rl;
edge[i^1].l+=rl;
if(used==flow) break;//满了
//return rl;
}
}
return used;
}
ll dic()
{
while(bfs())
{
vis=1;
while(vis)
{
vis=0;
dfs(s,inf);
}
}
return ans;
}
int main()
{
memset(head,-1,sizeof head);
n=read();m=read();e=read();
s=0;t=n+m+1;
for(int i=1;i<=n;++i)
{
add(0,i,1);add(i,0,0);
}
for(int i=n+1;i<=t-1;++i)
{
add(i,t,1);add(t,i,0);
}
for(int i=1;i<=e;++i)
{
a=read();b=read();
add(a,b+n,1);
add(b+n,a,0);//添加反向边
}
cout<<dic()<<endl;
return 0;
}