二分图

二分图

1. 二分图原理

原理

1. 二分图定义

  • 二分图又称为二部图,如果一个点集可以被分成两个部分,所有的边都在这两个部分之间,而每个集合内部没有边,则称这个图是一个二分图。如下图:

在这里插入图片描述

  • 显然(哈哈),如果一个图是二分图,则其去掉一些边之后还是二分图。

  • 下面证明一个重要的定理:一个图是二分图    ⟺    \iff 图中不存在奇数环    ⟺    \iff 染色过程中不存在矛盾

    • 首先证明:图中不存在奇数环    ⟺    \iff 染色过程中不存在矛盾

    首先证明充分性:图中不存在奇数环 ⇒ \Rightarrow 染色过程中不存在矛盾。可以证明其逆否命题成立,即染色过程中存在矛盾,则图中存在奇数环,因为染色法存在矛盾,说明一定存在相邻的两个点颜色相同,(白、黑、白、黑、…、白)因此一定存在奇数环;

    再证明必要性:图中不存在奇数环 ⇐ \Leftarrow 染色过程中不存在矛盾。可以使用反证法,染色法不存在矛盾,但存在奇数环,对于某个环而言,如果是奇数环的话,根据染色过程,一定有(白、黑、白、黑、…、白),最后一个一定是白,否则不能构成奇数环,此时发现染色法矛盾了,因为白色的点和白色的点相邻了。

    • 然后证明:一个图是二分图和上面两个条件等价

    首先证明必要性:一个图是二分图 ⇐ \Leftarrow 染色过程中不存在矛盾。染色法不存在矛盾,则可以将白点和黑点分成两个部分,此时可以构造出一个二分图,成立。

    再证明充分性:一个图是二分图 ⇒ \Rightarrow 图中不存在奇数环。反证法,假设最后推出图中存在奇数环,则最终图中一定存在两个相邻的点颜色相同,矛盾,因此结论成立。

2. 染色法判断二分图

  • 根据上面三个结论的等价性,我们可以使用染色法判定一个图是不是二分图。
  • 染色法:如果一个点被染成白色,则这个点相邻的点都应该被染成黑色,反之也是如此。根据染色法的过程,如果一个连通块中的某个点颜色确定了,则整个连通块每个点的颜色也就确定了。
  • 如果在染色过程中没有出现矛盾,则是二分图;否则不是二分图。
  • 代码实现中可以使用1、2分别表示两种颜色,则如果当前点颜色为c,其相邻的点的颜色应该为3-c。

代码模板

  • AcWing 860. 染色法判定二分图

    二分图_第1张图片

  • C++

#include 
#include 

using namespace std;

const int N = 100010, M = 200010;

int n, m;
int h[N], e[M], ne[M], idx;
int color[N];  // 0代表当前节点还未染色

void add(int a, int b) {
    e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}

// 当前给u染成颜色c, 返回是否能染色成功
bool dfs(int u, int c) {
    
    color[u] = c;
    
    for (int i = h[u]; ~i; i = ne[i]) {
        int j = e[i];
        if (!color[j]) {
            if (!dfs(j, 3 - c)) return false;
        } else if (color[j] == c) return false;
    }
    
    return true;
}

int main() {
    
    scanf("%d%d", &n, &m);
    
    memset(h, -1, sizeof h);
    
    while (m--) {
        int a, b;
        scanf("%d%d", &a, &b);
        add(a, b), add(b, a);
    }
    
    bool success = true;  // 是否染色成功
    for (int i = 1; i <= n; i++)
        if (!color[i]) {
            if (!dfs(i, 1)) {
                success = false;
                break;
            }
        }
    
    if (success) puts("Yes");
    else puts("No");
    
    return 0;
}

3. 匈牙利算法

  • 一些概念:
    • 匹配:是指中的一组边的集合,每两条边之间没有公共点。
    • 最大匹配:边数最多的匹配。
    • 匹配点:指在匹配当中的点。
    • 非匹配点:指不在匹配当中的点。
    • 增广路径:指一条路径,这条路径起始位置都是第一个集合中的非匹配点,中间经过(非匹配边,匹配边,非匹配边,…,非匹配边),到达右边一个非匹配点。
  • 最大匹配    ⟺    \iff 不存在增广路径,证明略。

  • 匈牙利算法是针对二分图来说的,它是求二分图的一个最大匹配。
  • 一个形象的例子就是男女恋爱关系,二分图两个集合,一个集合代表男生,一个集合代表女生,两个集合之间的边代表男女生之间存在好感,问最多确定多少对恋爱关系(男女生都不能脚踏多只船,即不能有选出的两条边有公共节点)。这里使用这个例子阐述匈牙利算法的过程:

二分图_第2张图片

(1)我们可以随便考察两个集合中的某个集合,比如我们考察男生这个集合,按照编号递增的顺序给每个男生找女朋友;

(2)考察男1号,因为女6号还没有确定恋爱关系,所以可以与女6号成功牵手,连上红线;

(3)接着考察男2号,因为女5号还没有确定恋爱关系,所以可以与女5号成功牵手,连上红线;此时确定的恋爱关系如下图:

二分图_第3张图片

(4)接着考察男3号,但发现女6号已经和男1号确定恋爱关系了,此时就是匈牙利算法的关键了,此时女6号就会找到她现在的男友(1号),看看他能不能换个女友,如果不能换的话,男3号接着看下一个他喜欢的女生,但是结果发现男1号可以换个女友,此时1、6分手,1号和8号确定恋爱关系,然后3、6号成为情侣,如下图(绿色代表分手了):

二分图_第4张图片

(5)考察男4号,因为女7号还没有确定恋爱关系,所以可以与女7号成功牵手,连上红线;如下图:

二分图_第5张图片

最终有四条红线,代表有四对情侣,因此最大匹配是4。

下面这个网友的评论很精彩:

二分图_第6张图片

  • 匈牙利算法的时间复杂度最坏情况下是 O ( n × m ) O(n \times m) O(n×m)的。

  • AcWing 861. 二分图的最大匹配

    二分图_第7张图片

  • C++

#include 
#include 

using namespace std;

const int N = 510, M = 100010;

int n1, n2, m;  // n1表示第一个集合中的点数,n2表示第二个集合中的点数
int h[N], e[M], ne[M], idx;  // 邻接表存储所有边,匈牙利算法中只会用到从第一个集合指向第二个集合的边,所以这里只用存一个方向的边
int match[N];  // 存储第二个集合中的每个点 当前 匹配的第一个集合中的点是哪个
bool st[N];  // 表示第二个集合中的每个点是否已经被遍历过

void add(int a, int b) {
    e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}

bool find(int x) {
    // 遍历自己喜欢的女孩
    for (int i = h[x]; i != -1; i = ne[i]) {
        int j = e[i];
        if (!st[j]) {  // 如果在这一轮模拟匹配中,这个女孩尚未被预定
            st[j] = true;  // 那x就预定这个女孩了
            // 如果女孩j没有男朋友,或者她原来的男朋友能够预定其它喜欢的女孩。配对成功
            if (match[j] == 0 || find(match[j])) {
                match[j] = x;
                return true;
            }
        }
    }
    // 自己中意的全部都被预定了。配对失败。
    return false;
}

int main() {

    scanf("%d%d%d", &n1, &n2, &m);

    memset(h, -1, sizeof h);
    while (m--) {
        int a, b;
        scanf("%d%d", &a, &b);
        add(a, b);
    }

    int res = 0;
    for (int i = 1; i <= n1; i++) {
        // 每轮模拟都要初始化,因为新来的男嘉宾可以随意选择女孩
        memset(st, false, sizeof st);
        if (find(i)) res++;
    }

    printf("%d\n", res);

    return 0;
}

4. 最小点覆盖

  • 这个概念针对任意的无向图都是成立的。是指我们从图中选出最少的点集,使得所有的边中的两个端点至少有一个端点在该点集中。
  • 在二分图中,最小点覆盖==最大匹配数。假设在二分图中最小点覆盖为n,最大匹配数为m,下面证明 n ≥ m n \ge m nm且等号可以成立。

(1)首先证明 n ≥ m n \ge m nm。因为对于最大匹配来说,每两条边之间没有公共点,要想把所有用点覆盖所有的边,则必须每条边都选一个端点,所以 n ≥ m n \ge m nm

(2)证明等号可以成立。这里使用构造性证明。

​ (2.1)求二分图的最大匹配;

​ (2.2)从左部每个非匹配点出发,做一遍增广(一定不会成功,成功的话就可以换成增广了),标记所有经过的点(左右都需要标记)

做完上面的步骤之后,就可以构造出一个最小覆盖的方案,方案中包含的点=左部所有未被标记的点+右部所有被标记的点。如下图:

二分图_第8张图片

因为:方案中包含的点 = 左部所有未被标记的点 + 右部所有被标记的点。又左部所有未被标记的点一定是匹配点(逆否命题),右部所有被标记的点一定是匹配点(逆否命题),因为又有性质③,对于每条匹配边,我们必然只选其中的一个点,被标记的选择右边的点,未被标记的选择左边的点,所以n可以取到m。

根据在左部右部,以及匹配不匹配的情况,我们可以构造出四种边:

① 左边匹配,右边匹配;②左边非匹配,右边匹配;③ 左边匹配,右边非匹配;④ 左边非匹配,右边非匹配;

对于第①种情况我们已经讨论过;对于第④种情况不可能出现,否则的话我们求的不是最大匹配。

对于第②种情况,左边非匹配的点一定是被标记的点,因为走的是增广路径,走到右部的点一定是被标记的,这样的边一定被选;

对于第③种情况,这种情况不可能存在,右边非匹配一定没有被标记,因此左部的点不可能是匹配点;

  • 综上所述,我们证明了 n ≥ m n \ge m nm且可以取到m,将所有的边覆盖住,因此 n = = m n == m n==m

5. 最大独立集

  • 这个概念针对任意的无向图都是成立的。是指我们从图中选出最多的点集,使得选出的点之间都没有边。
  • 另外还有一个与最大独立集相对的概念最大团,是指我们从图中选出最多的点集,使得选出的点之间都有边。
  • 原图的最大独立集就是补图的最大团,原图的最大团就是补图的最大独立集。我们只需要掌握其中一个,另一个就自然掌握了。(所谓补图是指边互补,你有的边我没有,我有的边你没有)。
  • 在二分图中,最大独立集t == 总点数n - 最大匹配数m。(这里的n是指两个集合中 点个数之和)证明如下:
  • 最大独立集    ⟺    \iff 去掉最少的点,将所有边都破坏掉,剩余的点就是最大独立集    ⟺    \iff 找最小点覆盖    ⟺    \iff 找最大匹配。
  • 另外提一点,最大独立集问题在一般的图上是一个NPC问题,只有在二分图上才能使用匈牙利算法。

6. 最小路径点覆盖

  • 这个概念是针对DAG(有向无环图)的。最小路径点覆盖又被称为最小路径覆盖,是指在一个DAG中,我们最少需要使用多少条互不相交(点和边都不重复)的路径将所有点都覆盖住。
  • 这里使用到的技巧是拆点,假设原图中有n个点,将每个点都复制一份放在右边(此时新图中有2n个点),假设点 i i i复制后的点为点 i ′ i' i,我们将边 ( i , j ) (i, j) (i,j)变为新图中的 ( i , j ′ ) (i, j') (i,j)。新图必然是一个二分图,在这个二分图中求最大匹配数m,则最终有:原图的最小路径点覆盖t == 原图总点数n - 新图的最大匹配数m。下面证明这个结论:

二分图_第9张图片

  • 考虑原图中的任意一条路径转化到新图中是啥样的:

(1)每条路径转化到新图中一定对应新图的一个匹配,即每个点只会在一条边中。反之也成立。

(2)我们可以看一下原图中每条路径的终点,对应到新图中的出点是没有出边的,即左部的非匹配点,例如上图中的点3。同理左部的每个非匹配点一定也对应着原图中的路径。即使孤立点也可以看成一个终点,符合要求。

最小路径点覆盖    ⟺    \iff 让左部的非匹配点最少    ⟺    \iff 让左部的匹配点最多    ⟺    \iff 找最大匹配。

  • 综上所述,有:原图的最小路径点覆盖t == 原图总点数n - 新图的最大匹配数m

7. 最小路径重复点覆盖

  • 最小路径重复点覆盖:也是针对DAG来说的;将最小路径点覆盖中路径不能相交的条件去掉即可,即我们最少用多少条路径(点和边都可以重复)可以将所有的点覆盖住。
  • 求最小路径重复点覆盖的步骤如下:

(1)求原图的传递闭包得到新图;

(2)则原图的最小路径重复点覆盖    ⟺    \iff 新图的最小路径点覆盖。

  • 下面对上述等价性进行证明:

    ① 充分性:依次考虑原图的每条符合条件的路径,当我们考察到第i条路径时,如果路径上的点和前i-1条边上的点重复,则直接跳过即可,新图中加了很多边,可以跳过。另外第i条路径上的点不可能全部和前i-1条边上的点重复,否则的话第i条路径就没有存在的必要了。

    ② 必要性:将新图中间接转移过去的边展开成原来的边即可得到原图中的路径。

2. AcWing上的二分图题目

AcWing 257. 关押罪犯

问题描述

  • 问题链接:AcWing 257. 关押罪犯

    二分图_第10张图片

分析

  • 这一题相当于让我们求解:将图中的点分为两个集合,使得每个集合中的边权的最大值最小。
  • 这一题可以使用二分来求解,二分的区间是 [ 0 , 1 0 9 ] [0, 10^9] [0,109],为什么可以使用二分来求解呢?因为答案具有二段性。
  • 性质是:把边权值大于mid的边都保留,小于mid的边都删除,然后判断这个图是不是二分图。
  • 对于答案右边的点,因为我们取得阈值更大了,保留的边会更少,答案都满足条件,因此答案右边的点都满足这个性质;答案左边的点都不满足这个性质,因为答案是所有符合条件中最小的一个。

代码

  • C++
#include 
#include 

using namespace std;

const int N = 20010, M = 200010;

int n, m;
int h[N], e[M], w[M], ne[M], idx;
int color[N];

void add(int a, int b, int c) {
    e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}

bool dfs(int u, int c, int mid) {
    
    color[u] = c;
    
    for (int i = h[u]; ~i; i = ne[i]) {
        int j = e[i];
        if (w[i] <= mid) continue;
        if (!color[j]) {
            if (!dfs(j, 3 - c, mid)) return false;
        } else if (color[j] == c) return false;
    }
    
    return true;
}

bool check(int mid) {
    
    memset(color, 0, sizeof color);
    for (int i = 1; i <= n; i++)
        if (!color[i] && !dfs(i, 1, mid))
            return false;
    return true;
}

int main() {
    
    scanf("%d%d", &n, &m);
    
    memset(h, -1, sizeof h);
    
    while (m--) {
        int a, b, c;
        scanf("%d%d%d", &a, &b, &c);
        add(a, b, c), add(b, a, c);
    }
    
    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", r);
    
    return 0;
}

AcWing 372. 棋盘覆盖

问题描述

  • 问题链接:AcWing 372. 棋盘覆盖

    二分图_第11张图片

分析

  • 我们将可以放置骨牌的格子看成点,可以放置骨牌的两个格子之间连一条边。则问题就转化为了:最多取多少条边,所有选出的边无公共点。相当于让我们求最大匹配。我们还要判断这个图是不是二分图。
  • 因为:一个图是二分图    ⟺    \iff 这个图可以二染色。我们需要判断棋盘是否可以二染色即可,对于棋盘,存在一种经典的二染色方式,如下图:

二分图_第12张图片

  • 由上面分析可知,任意的棋盘都满足二染色,对应的图是二分图,所以此时就可以使用匈牙利算法求解了。
  • 那么如何判断每个格子的颜色呢?我们可以给每个格子一个编号,等于横纵坐标之和,和为偶数的是一种颜色,和为奇数的是另一种颜色。我们枚举偶点或者奇点都可以。

代码

  • C++
#include 
#include 

#define x first
#define y second

using namespace std;

typedef pair<int, int> PII;

const int N = 110;

int n, m;  // 边长、不能放置的点的数量
bool g[N][N];  // 为true代表有障碍物
PII match[N][N];  // 存储第二个集合中的每个点 当前 匹配的第一个集合中的点是哪个
bool st[N][N];  // 表示第二个集合中的每个点是否已经被遍历过

int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1, 0, -1};

bool find(int x, int y) {
    
    for (int i = 0; i < 4; i++) {
        int a = x + dx[i], b = y + dy[i];
        
        if (a < 1 || a > n || b < 1 || b > n) continue;
        if (st[a][b] || g[a][b]) continue;
        st[a][b] = true;
        
        PII t = match[a][b];
        if (t.x == 0 || find(t.x, t.y)) {
            match[a][b] = {x, y};
            return true;
        }
    }
    return false;
}

int main() {
    
    cin >> n >> m;
    while (m--) {
        int a, b;
        cin >> a >> b;
        g[a][b] = true;
    }
    
    
    int res = 0;
    for (int i = 1; i <= n; i++)
        for (int j = 1; j <= n; j++) 
            if ((i + j) % 2 && !g[i][j]) {
            memset(st, 0, sizeof st);
            if (find(i, j)) res++;
        }
    
    cout << res << endl;
    
    return 0;
}

AcWing 376. 机器任务

问题描述

  • 问题链接:AcWing 376. 机器任务

    二分图_第13张图片

分析

  • 一个任务可以在A、B两台机器中的任意一个被完成。刚开始A、B都处于模式0,因此需要切换到0的任务不需要重启就可以完成,可以不用考虑(因为不会影响重启次数)。
  • 我们的问题可以转换成:需要从A、B的N+M-2个模式当中最少选择多少个模式可以把每个任务完成,如果将 a [ i ] 、 b [ i ] a[i]、b[i] a[i]b[i]之间连一条无向边,则把每个任务完成相当于在这条无向边上选择一个点。因此为题最终变为了:我们最少需要选择多少点可以把所有的边全部覆盖住。这是一个最小点覆盖的问题。
  • 又因为这是一个二分图,所以最小点覆盖n==最大匹配数m,我们求一遍最大匹配即可。

代码

  • C++
#include 
#include 

using namespace std;

const int N = 110;

int n, m, k;
bool g[N][N];  // 表示两点之间是否有任务
int match[N];
bool st[N];

bool find(int x) {
    
    for (int i = 1; i < m; i++)
        if (!st[i] && g[x][i]) {
            st[i] = true;
            if (match[i] == -1 || find(match[i])) {
                match[i] = x;
                return true;
            }
        }
    return false;
}

int main() {
    
    while (cin >> n, n) {
        
        cin >> m >> k;
        
        memset(g, 0, sizeof g);
        memset(match, -1, sizeof match);
        
        while (k--) {
            int t, a, b;
            cin >> t >> a >> b;
            if (!a || !b) continue;
            g[a][b] = true;  // 只需要记录从第一个集合到第二个集合的边即可
        }
        
        int res = 0;
        for (int i = 1; i < n; i++) {
            memset(st, 0, sizeof st);
            if (find(i)) res++;
        }
        
        cout << res << endl;
    }
    
    return 0;
}

AcWing 378. 骑士放置

问题描述

  • 问题链接:AcWing 378. 骑士放置

    二分图_第14张图片

分析

  • 将格子看成图中的点,如果两个马是可以相互攻击到的,则在这两个格子之间连一条边。则剩下的问题就变成了:我们可以最多可以选择多少点,使得这些点之间没有边。即最大独立集问题
  • 下面验证建立的图是二分图。如下图,可以看到马向八个方向跳的格子都是和其所在的格子沿着是不同的(这是因为偏移量一定是一个计数,一个偶数):

二分图_第15张图片

  • 根据分析,因此最终的结果为 n × m − T − n \times m - T - n×mT最大匹配数量。

代码

  • C++
#include 
#include 

#define x first
#define y second

using namespace std;

typedef pair<int, int> PII;

const int N = 110;

int n, m, k;
bool g[N][N];  // 代表是否有障碍物
PII match[N][N];
bool st[N][N];

int dx[8] = {-2, -1, 1, 2, 2, 1, -1, -2};
int dy[8] = {1, 2, 2, 1, -1, -2, -2, -1};

bool find(int x, int y) {
    
    for (int i = 0; i < 8; i++) {
        
        int a = x + dx[i], b = y + dy[i];
        if (a < 1 || a > n || b < 1 || b > m) continue;
        if (g[a][b] || st[a][b]) continue;
        
        st[a][b] = true;
        
        PII t = match[a][b];
        if (t.x == 0 || find(t.x, t.y)) {
            match[a][b] = {x, y};
            return true;
        }
    }
    return false;
}

int main() {
    
    cin >> n >> m >> k;
    for (int i = 0; i < k; i++) {
        int x, y;
        cin >> x >> y;
        g[x][y] = true;
    }
    
    int res = 0;
    for (int i = 1; i <= n; i++)
        for (int j = 1; j <= m; j++) {
            if (g[i][j] || (i + j) % 2) continue;
            memset(st, 0, sizeof st);
            if (find(i, j)) res++;
        }
    
    cout << n * m - k - res << endl;
    
    return 0;
}

AcWing 379. 捉迷藏

问题描述

  • 问题链接:AcWing 379. 捉迷藏

    二分图_第16张图片

分析

  • 分析题目可知,只要两个点存在一条路径,两者就是可以相互看到的(相当于人的视线可以拐弯的)。
  • 该题相当于问:给定我们一个有向无环图,让我们选择尽可能多的点,使得这些点任意两点之间都不能相互到达。
  • 首先说答案:答案 = 最小路径重复点覆盖的数量cnt。下面给出证明:

(1)首先一定有 K ≤ c n t K \le cnt Kcnt,否则一条路径上会选出两个点,会相互看见。接着证明K可以取到cnt。

(2)我们找出这cnt条路径的终点,这些终点肯定两两都不相同,否则的话某条路径上可以删除重复的终点,仍然符合要求。这些记为集合V,然后我们将这些终点所有能反向到达的点的集合记为 n e x t ( V ) next(V) next(V)

​ (2.1)如果有 V ⋂ n e x t ( V ) = ∅ V \bigcap next(V) = \emptyset Vnext(V)=,意味着我们从E出发是不可能到达V内部的点的,此时选择E中的cnt个点是符合要求的。

​ (2.2)如果有 V ⋂ n e x t ( V ) ≠ ∅ V \bigcap next(V) \neq \emptyset Vnext(V)=,则我们可以从V中选择出某一个终点 v i v_i vi,我们让 v i v_i vi沿着边反向向前走(向前走到的点仍然记为 v i v_i vi),直到走到满足条件 v i ∉ n e x t ( V − { v i } ) v_i \not \in next(V -\{v_i\}) vinext(V{vi}),一定是可以找到这样点 v i v_i vi的,否则这条路径就是多余的。如果仍然不为空,继续选择另一个终点进行这样的操作,直到交集为空集为止。

  • 根据上面的讲解可知:原图的最小路径重复点覆盖t == 原图总点数n - 原图对应闭包图构造的二分图的最大匹配数m

代码

  • C++
#include 
#include 

using namespace std;

const int N = 210, M = 30010;

int n, m;
bool d[N][N];  // 邻接矩阵
int match[N];
bool st[N];

bool find(int x) {
    
    for (int i = 1; i <= n; i++)
        if (d[x][i] && !st[i]) {
            st[i] = true;
            
            if (match[i] == 0 || find(match[i])) {
                match[i] = x;
                return true;
            }
        }
    return false;
}

int main() {
    
    scanf("%d%d", &n, &m);
    while (m--) {
        int a, b;
        scanf("%d%d", &a, &b);
        d[a][b] = true;
    }
    
    // 传递闭包
    for (int k = 1; k <= n; k++)
        for (int i = 1; i <= n; i++)
            for (int j = 1; j <= n; j++)
                d[i][j] |= d[i][k] & d[k][j];
    
    int res = 0;
    for (int i = 1; i <= n; i++) {
        memset(st, 0, sizeof st);
        if (find(i)) res++;
    }
    
    printf("%d\n", n - res);
    
    return 0;
}

3. 力扣上二分图题目

Leetcode 0785 判断二分图

问题描述

  • 问题链接:Leetcode 0785 判断二分图

    二分图_第17张图片

分析

  • 染色法求二分图,模板题。

代码

  • C++
/**
 * 执行用时:32 ms, 在所有 C++ 提交中击败了40.35%的用户
 * 内存消耗:14.2 MB, 在所有 C++ 提交中击败了5.03%的用户
 */
class Solution {
public:
    vector<int> color;
    vector<vector<int>> g;

    bool isBipartite(vector<vector<int>> &graph) {

        g = graph;
        int n = g.size();
        color.resize(n, 0);
        for (int i = 0; i < n; i++)
            if (!color[i])
                if (!dfs(i, 1))
                    return false;
        return true;
    }

    bool dfs(int u, int c) {
        color[u] = c;
        for (auto w : g[u]) {
            if (!color[w]) {
                if (!dfs(w, 3 - c)) return false;
            } else if (color[w] == c) return false;
        }
        return true;
    }
};
  • Java
/**
 * Date: 2021/4/13 8:59
 * 执行用时:0 ms, 在所有 Java 提交中击败了100.00%的用户
 * 内存消耗:38.9 MB, 在所有 Java 提交中击败了65.81%的用户
 */
public class Solution {
    
    int[] color;  // 0代表当前节点还未染色, 1、2分别表示两种颜色
    int[][] g;

    public boolean isBipartite(int[][] graph) {

        g = graph;
        int n = g.length;  // 图中顶点数
        color = new int[n];
        for (int i = 0; i < n; i++)
            if (color[i] == 0)
                if (!dfs(i, 1))
                    return false;
        return true;
    }

    private boolean dfs(int u, int c) {
        color[u] = c;
        for (int w : g[u]) {
            if (color[w] == 0) {
                if (!dfs(w, 3 - c)) return false;
            } else if (color[w] == c) return false;
        }
        return true;
    }
}

Leetcode LCP 04 覆盖

问题描述

  • 问题链接:Leetcode LCP 04 覆盖

    二分图_第18张图片

分析

  • 和AcWing 372. 棋盘覆盖一样,可以参考该题的分析。

代码

  • C++
/**
 * 执行用时:0 ms, 在所有 C++ 提交中击败了100.00%的用户
 * 内存消耗:8.2 MB, 在所有 C++ 提交中击败了80.81%的用户
 */
class Solution {
public:
    typedef pair<int, int> PII;

    int n, m;
    vector<vector<int>> g;
    PII match[10][10];
    bool st[10][10];
    int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1, 0, -1};

    bool find(int x, int y) {

        for (int i = 0; i < 4; i++) {
            int a = x + dx[i], b = y + dy[i];
            if (a < 0 || a >= n || b < 0 || b >= m) continue;
            if (st[a][b] || g[a][b]) continue;

            st[a][b] = true;
            PII t = match[a][b];
            if (t.first == -1 || find(t.first, t.second)) {
                match[a][b] = {x, y};
                return true;
            }
        }
        return false;
    }

    int domino(int _n, int _m, vector<vector<int>> &broken) {

        n = _n, m = _m;
        g = vector<vector<int>>(n, vector<int>(m, 0));
        for (int i = 0; i < broken.size(); i++) {
            int a = broken[i][0], b = broken[i][1];
            g[a][b] = 1;
        }

        memset(match, -1, sizeof match);

        int res = 0;
        for (int i = 0; i < n; i++)
            for (int j = 0; j < m; j++)
                if ((i + j) % 2 && !g[i][j]) {
                    memset(st, 0, sizeof st);
                    if (find(i, j)) res++;
                }
        return res;
    }
};

你可能感兴趣的:(算法,图论)