二分图

1. 二分图的定义

二分图,又称二部图,英文名叫 Bipartite graph。
二分图是什么?节点由两个集合组成,且两个集合内部没有边的图。
换言之,存在一种方案,将节点划分成满足以上性质的两个集合。
如图
这是图片
取自于oi-wiki


2. 二分图的判定

观察二分图的定义可以发现

  • 一个图中每一条边都连接着不同颜色的两个点
  • 一个图是二分图当且仅当图中不存在奇环

因为在一个环中只有偶数时绕一圈才会回到相同的颜色.

2.1 染色法

我们利用染色法进行二分图的判定

  1. 首先将所有节点初始化为未染色
  2. 从未染色点u开始染色并向结点v遍历(这里用11表示两个颜色)
  3. 如果结点v未染色则重复操作2,否则判定uv的颜色是否相同
    • 如果两结点颜色相同则改图不是二分图

时间复杂度 O(n+m) 模板:

bool dfs(int x,int col){
	c[x] = col;
	for(int i = hd[x];i;i = e[i].nx){
		int y = e[i].ver;
		if(!c[y] && !dfs(y,-col))return 0;
		else if(c[y] == col)return 0;
	}
	return 1;
}

2.2 例题

P1525 [NOIP2010 提高组] 关押罪犯
题意:给定一个图,求把这个图分为两个点集,找出每个点集内最大边权最小的分法,求最小的最大边权
首先可以想到求 '最小的最大' 可以二分;
转化为是否存在一个分法,使得两个点集内最大边权是否小于等于mid
我们可以把大于mid的边权重新建图,进行二分图的判定,如果重建图是二分图则符合答案,更新即可

点击查看代码
#include <bits/stdc++.h> 
using namespace std;
const int N = 2e4+10,M = 2e5+10;
struct made{
	int ver,nx;
}e[M];
int hd[N],tot,c[N];
void add(int x,int y){
	tot++;
	e[tot].nx = hd[x],e[tot].ver = y,hd[x] = tot; 
} 
void first(){
	tot = 0;
	memset(hd,0,sizeof(hd));
	memset(e,0,sizeof(e));
	memset(c,0,sizeof(c));
}
int n,m;
bool f;
struct node{
	int x,y,z;
}a[M];
bool dfs(int x,int col){
	c[x] = col;
	for(int i = hd[x];i;i = e[i].nx){
		int y = e[i].ver;
		if(!c[y] && !dfs(y,-col))return 0;
		else if(c[y] == col)return 0;
	}
	return 1; 
}
bool check(int mid){
	first();
	for(int i = 1;i <= m;i++)
		if(a[i].z > mid){
		    add(a[i].x,a[i].y);
		    add(a[i].y,a[i].x);
		}
	f = 1;
	for(int i = 1;i <= n;i++)
		if(!c[i])f &= dfs(i,1);//该图不一定是连通图 
	return f;
}
int main(){
	scanf("%d%d",&n,&m);
	for(int i = 1;i <= m;i++)scanf("%d%d%d",&a[i].x,&a[i].y,&a[i].z);
    int l = 0,r = 1e9;
    while(l < r){
    	int mid = l + r >> 1;
    	if(check(mid))r = mid;
    	else l = mid + 1;
    }
    printf("%d\n",l);
	
	return 0;
	
}
	

3. 二分图的匹配

3.1 二分图最大匹配

  • 图的匹配: 无向图中一个边集中,任意两条边都没有公共端点,则称这组边的集合为该图的一组匹配,对于一组匹配 S
    • 匹配变/非匹配边:属于 S 的边被称为匹配边,不属于 S 的边被称为非匹配边
    • 匹配点/非匹配点:匹配边的端点为匹配点,其他节点为非匹配点
    • 增广路:如果在二分图中存在一条连接两个非匹配点的路径 path,使得非匹配边匹配边path 交替出现,则 path 是匹配 S增广路,也称交错路
  • 二分图的最大匹配:包含边数最多的一组匹配被称为二分图的最大匹配

性质:二分图的一组匹配 S 是最大匹配,当且仅当图中不存在S的增广路

因为如果存在增广路,那么我们把增广路上的所有边的状态全部取反,那么得到的新的边集 S 仍然是一组匹配,且边数增加了1

取自于《算法竞赛进阶指南》

匈牙利算法(增广路算法)

又称增广路算法,用于计算二分图的最大匹配,过程为

  1. 首先将所有边定为非匹配边
  2. 寻找增广路 path,把路径上所有边状态取反,得到一个更大的S
  3. 重复第2步,直至图中不存在增广路

匈牙利算法依次尝试每一个左部结点 x 寻找一个匹配的右部结点 y。使得 y 能与 x 匹配,需要满足以下两个条件之一:

  1. y本身就是非匹配边,无向边 (x,y) 自己构成一条长度为 1 的增广路
  2. y已经与左部点 x 匹配,但从 x 出发可以找到另一个右部点 y 与之匹配。此时 xyxy 为一条增广路

每一个左部结点最多遍历二分图一次,时间复杂度为 O(nm)。模板:

bool dfs(int x){
	for(int i = hd[x];i;i = e[i].nx){
		int y = e[i].ver;
		if(v[y])continue;
		v[y] = 1;
		if(!match[y] || dfs(match[y])){
			match[y] = x;
			return 1;
		} 
	}
    return 0;
}
//main函数
for(int i = 1;i <= n;i++){
	memset(v,0,sizeof(v));
	if(dfs(i))ans++;
}

3.2 二分图的多重匹配

给定一个包含 N 个左部结点和 M 个右部结点的二分图,从中选出尽量多的边,使第 i(1in) 个左部结点至多与 kl(i) 条圈出的边相连,第 j(1in) 个右部结点至多与 kr(j) 条选出的边相连。

多重匹配一般有这三种解决方案:

  1. 拆点。把第 i 个左部结点拆为 kl(i) 个不同的左部结点,右部结点同理,然后跑二分图的最大匹配即可。
  2. 当右部点 kr=1 均成立,则我们可以每个左部节点跑 kl(i) 次dfs即可。
  3. 网络流。

3.3 二分图带权匹配

完备匹配:一个二分图,其左右部点数相同均为 N 个结点,如果该二分图的最大匹配包含 N 条匹配边,则称该二分图具有完备匹配
二分图带权匹配:给定一个二分图,二分图中的每条边都有一个权值,求出该二分匹配的一组最大匹配,使得匹配边的权值之和最大(前提是先保证匹配数最大,其次保证边权和最大)

3.3.1 KM算法

一些定义:

  • 交错树:在匈牙利算法中如果某个左部结点匹配失败,则称该节点 dfs 经过的路径构成一颗树,因为该树中非匹配边与匹配边交错更替,所以称作交错树
  • 顶标:在二分图中给左部节点和右部节点都给出一个整数值,满足任意一组边 (i,j)a(i)+b(j)w(i,j) 都成立,a(i)b(j) 称为节点的顶标
  • 相等子图:二分图中所有节点和满足 a(i)+b(j)=w(i,j) 的边构成的子图,称为二分图的相等子图

定理:若在相等子图中存在完备匹配,则这个完备匹配就是二分图的带权最大匹配。

3.4 例题

I 372. 棋盘覆盖

题意:在一个 nn 列的棋盘上,有些格子不能放,求最多能放置几块 21 的骨牌,
n 较小时可以用状压dp做,构造二分图匹配模型则更加高效

二分图匹配模型:

  1. 结点可以分为独立的两个集合,每个集合内部有 0 条边
  2. 每个结点只能与一条匹配边相连

在本题中,任意两块骨牌都不重叠,即每个结点最多只被一个骨牌覆盖,而骨牌大小为 12,只能覆盖相邻的两个格子,于是我们可以把没有被禁止的格子作为结点,与相邻的格子连边。我们再把格子进行染色(行列号加起来为偶数的染为白色奇数染为黑色),则本图为二分图,可以把白色结点当做左部结点,黑色节点当做右部结点。
为使骨牌不重叠的情况下尽量多放,即求该二分图的最大匹配

时间复杂度 O(n2m2)

小优化:我们可以发现从右部点到左部点的连边是无用的,所以只需建左部点向右部点的边就可以了(减少内存)

点击查看代码
#include <bits/stdc++.h> 
using namespace std;
const int N = 1e4+10,M = 2e5+10;
//构造二分图匹配 
int n,m,E;
struct made{
    int ver,nx;
}e[M<<1];
int hd[N],tot,cnt,ans;
int match[N];
bool v[N],f[N];
int id(int x,int y){
    return (x-1)*n+y;
}
void add(int x,int y){
    tot++;
    e[tot].nx = hd[x],e[tot].ver = y,hd[x] = tot; 
}
bool dfs(int x){
    for(int i = hd[x];i;i = e[i].nx){
        int y = e[i].ver;
        if(v[y])continue;
        v[y] = 1;
        if(!match[y] || dfs(match[y])){
            match[y] = x;
            return 1;
        }
    }
    return 0;
}//
int main(){
	scanf("%d%d",&n,&m);
	for(int i = 1;i <= m;i++){
	    int x,y;
	    scanf("%d%d",&x,&y);
	    f[id(x,y)] = 1;
    }
    for(int i = 1;i <= n;i++){
        for(int j = 1;j <= n;j++){
            if(f[id(i,j)])continue;//被禁不能连边 
            if(i > 1 && !f[id(i-1,j)])add(id(i,j),id(i-1,j));
            if(i < n && !f[id(i+1,j)])add(id(i,j),id(i+1,j));
            if(j > 1 && !f[id(i,j-1)])add(id(i,j),id(i,j-1));
            if(j < n && !f[id(i,j+1)])add(id(i,j),id(i,j+1));
        }
    }
    for(int i = 1;i <= n;i++)
        for(int j = (i&1)?1:2;j <= n;j += 2){
            memset(v,0,sizeof(v));
            if(dfs(id(i,j)))ans++;
        }//找左部结点即白色结点 
    printf("%d\n",ans);
	
	return 0;
	
}
	

II 373. 車的放置

题意:在一个 nm 列的棋盘上,有些格子不能放棋子,求最多可以放几个車。
懂了上一题这题直接秒
同上一题,我们可以发现在任意一行或一列中只能存在一个棋子,所以我们可以把行数定义为二分图中的左部点,把列数定义为右部点,且满足左部点与右部点内部不存在边(不可能有一个点同时在第 i 列与第 j 列),符合二分图匹配模型,那么我们把没有被禁的点 (i,j) 把左部点 i 连到右部点 j 上,跑二分图最大匹配就可以了。

时间复杂度 O(n3)

点击查看代码
#include <bits/stdc++.h> 
using namespace std;
const int N = 210,M = 5e4+10;
//二分图匹配模型
int n,m,q;
struct made{
    int ver,nx;
}e[M<<1];
int hd[N],tot,ans;
int match[N];
bool v[N],f[N][N];
void add(int x,int y){
    tot++;
    e[tot].nx = hd[x],e[tot].ver = y,hd[x] = tot; 
}
bool dfs(int x){
    for(int i = hd[x];i;i = e[i].nx){
        int y = e[i].ver;
        if(v[y])continue;
        v[y] = 1;
        if(!match[y] || dfs(match[y])){
            match[y] = x;
            return 1;
        }
    }
    return 0;
}// 
int main(){
	scanf("%d%d%d",&n,&m,&q);
	for(int i = 1;i <= q;i++){
	    int x,y;
	    scanf("%d%d",&x,&y);
	    f[x][y] = 1;
    }
    for(int i = 1;i <= n;i++)
        for(int j = 1;j <= m;j++)
            if(!f[i][j])add(i,j);//
    for(int i = 1;i <= n;i++){
        memset(v,0,sizeof(v));
        if(dfs(i))ans++;
    }
    printf("%d\n",ans);
	
	return 0;
	
}
	

III P1402 酒店之王

题意:给定一个三分图(雾,一个人有喜欢与不喜欢的菜品,以及喜欢与不喜欢的房间,只有两个条件都满足该客人才会留下,求最大客人数。
我们可以从客人向菜品与房间分别跑一次最大匹配,只有都匹配到了则 ans++ 否则还原回原来情况(因为有可能一个满足而另一个不满足,需要还原)。

点击查看代码
#include <bits/stdc++.h> 
using namespace std;
//三分图最大匹配(雾
const int N = 210,M = 5e4+10;
int n,m,k,ans;
struct made{
    int ver,nx;
}e1[M],e2[M];
int hd1[N],hd2[N],tot1,tot2;
int m1[N],m2[N],l1[N],l2[N];
bool v1[N],v2[N];
void add1(int x,int y){
    tot1++;
    e1[tot1].nx = hd1[x],e1[tot1].ver = y,hd1[x] = tot1;
}
void add2(int x,int y){
    tot2++;
    e2[tot2].nx = hd2[x],e2[tot2].ver = y,hd2[x] = tot2;
}
bool dfs1(int x){
    for(int i = hd1[x];i;i = e1[i].nx){
        int y = e1[i].ver;
        if(v1[y])continue;
        v1[y] = 1;
        if(!m1[y] || dfs1(m1[y])){
            m1[y] = x;
            return 1;
        }
    }
    return 0;
}
bool dfs2(int x){
    for(int i = hd2[x];i;i = e2[i].nx){
        int y = e2[i].ver;
        if(v2[y])continue;
        v2[y] = 1;
        if(!m2[y] || dfs2(m2[y])){
            m2[y] = x;
            return 1;
        }
    }
    return 0;
}
int main(){
    scanf("%d%d%d",&n,&m,&k);
    for(int i = 1;i <= n;i++)
	    for(int j = 1;j <= m;j++){
	        int x;scanf("%d",&x);
	        if(x)add1(i,j);
        }
	for(int i = 1;i <= n;i++)
	    for(int j = 1;j <= k;j++){
	        int x;scanf("%d",&x);
	        if(x)add2(i,j);
        }
    for(int i = 1;i <= n;i++){
        memset(v1,0,sizeof(v1));
        memset(v2,0,sizeof(v2));
        memcpy(l1,m1,sizeof(m1));
        memcpy(l2,m2,sizeof(m2));
        if(dfs1(i) && dfs2(i))ans++;
        else{//不符
            memcpy(m1,l1,sizeof(l1));
            memcpy(m2,l2,sizeof(l2));//还原
        }
    }
    printf("%d\n",ans);
	
    return 0;
	
}
	

IV P2055 [ZJOI2009] 假期的宿舍

本题非常好,建议先自己做。
题意:有一些人需要找一些座位,一些人原来就有座位,一些人是新来的,也有一些人(有座位的)要离开,这些人中有一些关系,每个人只能坐在和自己有直接关系(包括自己)的座位上,判断留下来的人是否都能找到座位。
首先我们找出座位,以及有座位但是要离开的人 now,则需要座位的人数就为 nnow,我们把人作为左部节点,座位为右部节点,如果 (i,j) 有关系且 j 有座位,且 i 留下,在 ij 连一条边,跑最大匹配,如果最大匹配数为 nnow 则都能找到座位。

点击查看代码
#include <bits/stdc++.h> 
using namespace std;
const int N = 110,M = 2e4+10;
int t,n,now,ans;
struct made{
    int ver,nx;
}e[M];
int hd[N],tot;
int match[N],a[N],b[N];
bool v[N];
void add(int x,int y){
    tot++;
    e[tot].nx = hd[x],e[tot].ver = y,hd[x] = tot;
}
bool dfs(int x){
    for(int i = hd[x];i;i = e[i].nx){
        int y = e[i].ver;
        if(v[y])continue;
        v[y] = 1;
        if(!match[y] || dfs(match[y])){
            match[y] = x;
            return 1;
        }
    }
    return 0;
}
int main(){
    scanf("%d",&t);
    while(t--){
        tot = ans = now = 0;
        memset(match,0,sizeof(match));
        memset(hd,0,sizeof(hd));
        scanf("%d",&n);
        for(int i = 1;i <= n;i++)scanf("%d",&a[i]);
        for(int i = 1;i <= n;i++){
            scanf("%d",&b[i]);
            if(a[i] && b[i])now++;
        }
        for(int i = 1;i <= n;i++){
            if(a[i] && !b[i])add(i,i);//自己有座位
            for(int j = 1;j <= n;j++){
                int x;scanf("%d",&x);
                if(a[i] && b[i])continue;//i离开了
                if(x && a[j])add(i,j);
            }
        }
        for(int i = 1;i <= n;i++){
            memset(v,0,sizeof(v));
            if(dfs(i))ans++;
        } 
        if(ans == n - now)printf("^_^\n");
        else printf("T_T\n");
    }
	
	return 0;
	
}

V 374. 导弹防御塔

题意:有一些防御塔可以发出导弹攻击目标,防御塔需要 T1 时间发出导弹且需要 T2 时间冷却导弹,导弹的飞行速度是 V,存在一些目标,求最小需要多长时间才可以吧所有目标摧毁(每个防御塔或者目标都在直角坐标系中 t=dis/V)。
首先我们可以发现如果时间较短可以完全摧毁,则时间更长一定可以,显然答案具有单调性,可以二分,问题转化为是否能在 mid 时间内完全摧毁目标。
我们可以先算出 mid 时间内至多发出几枚导弹,每个导弹只能摧毁一个目标,可以发现这是一个多重匹配问题,我们可以拆点,把 M 个目标当做左部结点,把一个防御塔拆为 K 个导弹,进行二分图最大匹配,继续二分即可。

时间复杂度 O(n4log(T))

点击查看代码
#include <bits/stdc++.h> 
using namespace std;
const int N = 60,M = 3e5+10;
const double eps = 1e-8;
//二分图多重匹配 
int n,m;
double t1,t2,V;
struct made{
    int ver,nx;
}e[M];
int hd[N],tot;
int match[M];
bool v[M];
struct node{
    int x,y;
}a[N],b[N]; 
void add(int x,int y){
    tot++;
    e[tot].ver = y,e[tot].nx = hd[x],hd[x] = tot;
}
double distan(int x,int y){
    return (double)sqrt((a[x].x-b[y].x)*(a[x].x-b[y].x)+(a[x].y-b[y].y)*(a[x].y-b[y].y));
}
bool dfs(int x){
    for(int i = hd[x];i;i = e[i].nx){
        int y = e[i].ver;
        if(v[y])continue;
        v[y] = 1;
        if(!match[y] || dfs(match[y])){
            match[y] = x;
            return 1;
        }
    }
    return 0;
}//模板 
void build(double mid){
    int cnt = 0;
    for(int i = 1;i <= n;i++){
        double t = t1;
        for(int k = 1;k <= m;k++){
            if(t > mid)break;
            cnt++;
            for(int j = 1;j <= m;j++){
                double dis = distan(i,j);
                if(dis / V + t <= mid)add(j,cnt);
            }
            t += t1 + t2;
        }
    }//建二分图 
}
bool check(double mid){
    tot = 0;
    memset(hd,0,sizeof(hd));
    memset(match,0,sizeof(match));
    build(mid);
    for(int i = 1;i <= m;i++){
        memset(v,0,sizeof(v));
        if(!dfs(i))return 0;// 
    }
    return 1;
}
int main(){
	scanf("%d%d%lf%lf%lf",&n,&m,&t1,&t2,&V);
	t1 /= 60;
	for(int i = 1;i <= m;i++)scanf("%d%d",&b[i].x,&b[i].y);
	for(int i = 1;i <= n;i++)scanf("%d%d",&a[i].x,&a[i].y);
	double l = t1,r = 1e6;
	while(l + eps < r){
	    double mid = (l + r) / 2;
	    if(check(mid))r = mid;
	    else l = mid; 
    }
    printf("%.6lf\n",l);
	
	return 0;
	
}
	

VI 375. 蚂蚁

题意:在直角坐标系中有 2N 个点,有 N 个白点与 N 个黑点,把二者用线连起来,要求最后所有线段都不相交,求一种方案。
首先我们观察两个点相交与不相交时的性质:

根据三角形两边之和一定大于第三边,我们可以发现把相交的线段变为不相交的线段使得线段总和变短了,这相当于求二分图的最小匹配,又因为该图是一个稠密图,且存在完备匹配,所以用KM算法更好,我们把边权取反,求最大匹配就行了。

时间复杂度 O(n3)O(n4)

注:注意一下浮点数。

点击查看代码
#include <bits/stdc++.h> 
using namespace std;
const int N = 110,M = 2e4+10;
const double eps = 1e-8;
//二分图带权最大匹配
int n;
struct node{
    int x,y;
}a[N],b[N];
int hd[N],tot;
int match[N];
double la[N],lb[N],w[N][N],upd[N],d;
bool va[N],vb[N];
double distan(int x,int y){
    return sqrt((double)(a[x].x-b[y].x)*(a[x].x-b[y].x)+(a[x].y-b[y].y)*(a[x].y-b[y].y));
}
bool dfs(int x){
    va[x] = 1;//
    for(int y = 1;y <= n;y++){
        if(!vb[y]){
            if(fabs(la[x] + lb[y] - w[x][y]) < eps){//相等子图 
                vb[y] = 1;//
                if(!match[y] || dfs(match[y])){
                    match[y] = x;
                    return 1;
                }
            }
            else upd[y] = min(upd[y],la[x] + lb[y] - w[x][y]);
        }
    }
    return 0;
}//匈牙利算法 
int main(){
	scanf("%d",&n);
	for(int i = 1;i <= n;i++)scanf("%d%d",&b[i].x,&b[i].y);
	for(int i = 1;i <= n;i++)scanf("%d%d",&a[i].x,&a[i].y);
    //KM 
	for(int i = 1;i <= n;i++){
	    la[i] = INT_MIN;
	    lb[i] = 0;
	    for(int j = 1;j <= n;j++){
	        w[i][j] = -distan(i,j);//将最小匹配改为最大匹配 
	        la[i] = max(la[i],w[i][j]);
        }
    }
	for(int i = 1;i <= n;i++){
	    while(1){
	        memset(va,0,sizeof(va));
	        memset(vb,0,sizeof(vb));
	        d = INT_MAX;
	        for(int j = 1;j <= n;j++)upd[j] = INT_MAX;
	        if(dfs(i))break;
	        for(int j = 1;j <= n;j++)
                if(!vb[j])d = min(d,upd[j]);
	        for(int j = 1;j <= n;j++){
	            if(va[j])la[j] -= d;
	            if(vb[j])lb[j] += d;// 
            }
        }
    }
    //
    for(int i = 1;i <= n;i++)printf("%d\n",match[i]);
	
	return 0;
	
}
	

VII P6577 【模板】二分图最大权完美匹配

4. 二分图的覆盖

4.1 二分图最小点覆盖

  • 给定一个二分图,求出一个最小的点集 S,使得图中任意一条边都有至少一个端点属于 S,称为二分图的最小点覆盖

König 定理:二分图最小点覆盖包含的点数等于二分图最大匹配包含的边数。

证明 我们可以构造一组点覆盖,使其包含的点数等于最大匹配包含的边数。 构造如下:
  1. 先求出最大匹配。
  2. 从每个左部非匹配点出发,再 dfs 一遍并记录所访问过的点。
  3. 左部未被标记的点,右部被标记的点,就可以得到二分图最小点覆盖。

我们发现:

  1. 左部非匹配节点一定被标记————因为是出发点。
  2. 右部非匹配节点一定未被标记————不然有增广路。
  3. 一对匹配点一定同时被标记或未被标记————因为只要到右部匹配点一定连到左部匹配点。

所以我们取了左部未被标记的节点,右部被标记的节点,可以发现选出的点数一定与匹配边一一对应。
再来看是否符合所有边都被覆盖:

  1. 匹配边一定被覆盖。
  2. 非匹配边中两端如果左端为非匹配点,那么右部节点一定被标记,会被覆盖。
  3. 非匹配边中两端如果右端为非匹配点,那么左部结点一定未被标记,不然就有增广路,也会被覆盖。

得证。

4.2 例题

I 376. 机器任务

题意:有两个机器 AB 以及 n 个任务,每台机器都有 m 种模式,每个任务在 A 机器上完成需要为 a(i) 模式,B 同理,求最少转换机器模式的次数(机器刚开始为模式0)。
二分图最小覆盖模型: 每条边有 2 个端点,二者至少选一个。

本题中每个任务至少选 AB 一个,我们把 A 的模式作为左部节点,B 的模式作为右部节点,每个任务连边 a(i)b(i),求最小覆盖即可。

时间复杂度 O(nm)

点击查看代码
#include <bits/stdc++.h> 
using namespace std;
//二分图最小点覆盖
const int N = 110,M = 2e4+10;
int n,m,k;
struct made{
    int ver,nx;
}e[M];
int hd[N],tot,ans;
int match[N];
bool v[N];
void add(int x,int y){
    tot++;
    e[tot].nx = hd[x],e[tot].ver = y,hd[x] = tot;
}
bool dfs(int x){
    for(int i = hd[x];i;i = e[i].nx){
        int y = e[i].ver;
        if(v[y])continue;
        v[y] = 1;
        if(!match[y] || dfs(match[y])){
            match[y] = x;
            return 1;
        }
    }
    return 0;
}
int main(){
	scanf("%d%d%d",&n,&m,&k);
	for(int i = 1;i <= k;i++){
	    int op,x,y;
	    scanf("%d%d%d",&op,&x,&y);
	    if(!x || !y)continue;//最开始为模式0,关于0的模式都不需要改变
	    x++,y++;
	    add(x,y);
    }
	for(int i = 1;i <= n;i++){
	    memset(v,0,sizeof(v));
	    if(dfs(i))ans++;
    }
    printf("%d\n",ans);
	
	return 0;
	
}
posted @   oXUo  阅读(218)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!
网站统计
点击右上角即可分享
微信分享提示