二分图染色

二分图

image

bool dfs(int u, int c) {
	if (color[u] == c) 
		return true;
	else if (color[u] == 3 - c) 
		return false;
	color[u] = c;
	for (int v : graph[u])
		if (!dfs(v, 3 - c)) return false;
	return true;
}

image


选择题:二分图是指能将顶点划分成两个部分,每一部分内的顶点间没有边相连的简单无向图。那么,24 个顶点的二分图至多有多少条边?

  • A. 144
  • B. 100
  • C. 48
  • D. 122
答案

A

有 24 个顶点,设这两个顶点集合 U 和 V 的大小分别为 m 和 k。根据题意,顶点总数为 24,所以 m + k = 24。在一个大小为 m 和 k 的完全二分图中,总边数为 m × k。现在问题转化为在 m + k = 24 的约束下,求 m × k 的最大值。

这是一个经典数学问题:当两个正数的和固定时,它们的乘积在它们相等时达到最大值。因此,最大边数 = 12 × 12 = 144。


选择题:以下连通无向图中,哪个一定可以用不超过两种颜色进行染色?

  • A. 完全三叉树
  • B. 平面图
  • C. 边双连通图
  • D. 欧拉图
答案

A。一个图可以用不超过两种颜色进行染色(使得任意一条边的两个端点颜色都不同),这个性质被称为二染色

一个图是可二染色的,当且仅当它是一个二分图

而一个图是二分图的充要条件是:图中不包含任何长度为奇数的圈

所以,这道题本质上是在问:哪种图一定不包含奇数长度的圈

树的定义是一个无圈的连通图,既然树中没有任何圈,那么它自然也就不可能包含长度为奇数的圈。因此,任何树(包括完全三叉树)都是二分图,一定可以用两种颜色进行染色。

一个简单的三角形(\(K_3\),3 个顶点,3 条边),同时是平面图、边双连通图、欧拉图。但它本身就是一个长度为 3 的奇数圈,需要 3 种颜色来染色。


习题:P1330 封锁阳光大学

解题思路

按照题目要求,每一条边所连接的点中,至少要有一个被选中,但又不能同时选中。因此可以转化为二分图染色问题。答案取两种颜色数量较少的那个。

参考代码
#include <cstdio>
#include <vector>
#include <algorithm>
using namespace std;
const int N = 10005;
vector<int> graph[N];
int color[N], cnt[3];
bool dfs(int u, int c) {
    if (color[u] == c) return true;
    else if (color[u] == 3 - c) return false;
    color[u] = c; cnt[c]++;
    for (int v : graph[u])
        if (!dfs(v, 3 - c)) return false;
    return true;
}
int main()
{
    int n, m; scanf("%d%d", &n, &m);
    while (m--) {
        int u, v; scanf("%d%d", &u, &v);
        graph[u].push_back(v); graph[v].push_back(u);
    }
    bool ok = true;
    int ans = 0; 
    for (int i = 1; i <= n; i++) 
        if (color[i] == 0) {
            cnt[1] = cnt[2] = 0;
            ok &= dfs(i, 1);
            if (!ok) break;
            ans += min(cnt[1], cnt[2]);
        }
    if (!ok) printf("Impossible\n");
    else printf("%d\n", ans);
    return 0;
}

习题:CF862B Mahmoud and Ehab and the bipartiteness

解题思路

若二分图中两种点的集合的大小分别为 \(|S_1|\)\(|S_2|\),则根据二分图的定义,两种集合间的点都是可以连边的,因此最多 \(|S_1| \times |S_2|\) 条边。所以还可以添加 \(|S_1| \times |S_2| - (n - 1)\) 条边。

参考代码
#include <cstdio>
#include <vector>
using namespace std;
const int N = 100005;
vector<int> graph[N];
int cnt[3], color[N];
void dfs(int u, int c) {
    color[u] = c; cnt[c]++;
    for (int v : graph[u])
        if (color[v] == 0) dfs(v, 3 - c); 
}
int main()
{
    int n; scanf("%d", &n);
    for (int i = 1; i < n; i++) {
        int u, v; scanf("%d%d", &u, &v);
        graph[u].push_back(v); graph[v].push_back(u);
    }
    dfs(1, 1);
    printf("%lld\n", 1ll * cnt[1] * cnt[2] - (n - 1));
    return 0;
}

习题:CF27D Ring Road 2

解题思路

两条新路是否相交,取决于它们的位置关系。

  1. 一条在环内,一条在环外:永远不会相交。
  2. 两条都在环内(或都在环外):当且仅当它们在环上的四个端点相互交错时,它们才会相交。

例如,有两条路,分别是连接城市 \((a,b)\)\((c,d)\)。将城市按环的顺序编号,如果端点的顺序是 \(a,c,b,d\),那么这两条路就是交错的。

这个观察是解决问题的关键,如果两条路是交错的,那么它们不能同时在环内,也不能同时在环外。它们必须一个在内,一个在外,这就构成了一个“二元约束”。

这个问题可以抽象成一个图论的染色问题:

  • \(m\) 条新路看作 \(m\) 个节点。
  • 如果两条路(例如路 \(i\) 和路 \(j\))是交错的,就在节点 \(i\) 和节点 \(j\) 之间连一条边,这条边表示“\(i\)\(j\) 不能是同一种类型(颜色)”。
  • 需要给每个节点分配两种颜色之一(例如,颜色 1 代表“内部”,颜色 2 代表“外部”),并满足所有的约束,即相邻节点颜色必须不同。

这就是经典的二分图判定问题,一个图是二分图,当且仅当它不包含任何奇数长度的环。如果“冲突图”是二分图,那么就存在一种合法的染色方案;否则,就不存在。

参考代码
#include <cstdio>
#include <vector>
#include <algorithm>
using std::vector;
using std::swap;

const int N = 105;
int a[N], b[N]; // a[i], b[i] 存储第 i 条边的两个端点
int color[N];   // color[i] 存储第 i 条边的颜色 (1 或 2),0 表示未染色
vector<int> g[N]; // 邻接表,用于构建冲突图

// 判断两条边 i 和 j 是否交叉
// 交叉的条件是,两条边的端点在环上是交错的
bool cross(int i, int j) {
    // 确保 a[k] < b[k]
    // 交叉情况1: a[i] < a[j] < b[i] < b[j]
    if (a[i] < a[j] && a[j] < b[i] && b[i] < b[j]) return true;
    // 交叉情况2: a[j] < a[i] < b[j] < b[i]
    if (a[j] < a[i] && a[i] < b[j] && b[j] < b[i]) return true;
    return false;
}

// DFS 用于进行二分图判定和染色
// u: 当前要染色的节点(边)
// c: 要给 u 染的颜色 (1 或 2)
bool dfs(int u, int c) {
    // 如果已经染过色
    if (color[u]) {
        // 如果颜色与期望的 c 相同,说明没有冲突,返回 true
        // 如果颜色与期望的 c 不同,说明存在奇环,不是二分图,返回 false
        return color[u] == c;
    }
    // 进行染色
    color[u] = c;
    // 遍历 u 的所有邻居(与 u 冲突的边)
    for (int v : g[u]) {
        // 递归地给邻居染上相反的颜色 (3-c)
        if (!dfs(v, 3 - c)) return false; // 如果子过程出现冲突,则立即返回 false
    }
    return true; // 整棵 DFS 树都成功染色
}

int main()
{
    int n, m; scanf("%d%d", &n, &m);
    for (int i = 1; i <= m; i++) {
        scanf("%d%d", &a[i], &b[i]);
        // 保证 a[i] < b[i],方便 cross 函数判断
        if (a[i] > b[i]) swap(a[i], b[i]);
    }

    // 1. 构建冲突图
    // 遍历每一对边,如果它们交叉,就在它们之间连一条边
    for (int i = 1; i <= m; i++)
        for (int j = i + 1; j <= m; j++) {
            if (cross(i, j)) {
                g[i].push_back(j);
                g[j].push_back(i);
            }
        }

    bool ok = true;
    // 2. 对冲突图进行二分图染色
    // 遍历所有节点(边),如果还未染色,就从它开始进行新一轮的 DFS 染色
    for (int i = 1; i <= m; i++) 
        if (!color[i]) {
            // 如果 dfs(i, 1) 返回 false,说明图不是二分图,无解
            if (!dfs(i, 1)) {
                ok = false;
                break;
            }
        }

    // 3. 输出结果
    if (ok) {
        // 如果是二分图,则有解。根据颜色输出 'i' 或 'o'
        for (int i = 1; i <= m; i++) {
            // " io" 是一个技巧,color[i]为1时访问索引1('i'),为2时访问索引2('o')
            printf("%c", " io"[color[i]]);
        }
        printf("\n");
    } else {
        printf("Impossible\n");
    }
    return 0;
}

习题:P6185 [NOI Online #1 提高组] 序列

解题思路

把每个位置看成一个点。

对于 \(2\) 操作,如果两个位置连通则意味着可以使一个位置 \(+1\) 而另一个位置 \(-1\),这样一来对于整个连通块,可以使其在总和不变的情况下任意加减,因此可以用并查集将一个连通块看成一个点。

对于 \(1\) 操作连边,如果形成的图是二分图,则可以保证其两个集合内的总和之差保持不变的情况下任意加减。如果形成的图不是二分图,则可以保证整个连通块总和奇偶性保持不变的情况下任意加减。

参考代码
#include <cstdio>
#include <vector>
#include <cmath>
using namespace std;
typedef long long LL;
const int N = 100005;
int a[N], b[N], color[N], fa[N];
LL sum[3], delta[N];
struct Operation {
    int t, u, v;
};  
Operation op[N];
vector<int> graph[N];
int query(int x) {
    return fa[x] == x ? x : fa[x] = query(fa[x]);
}
void merge(int x, int y) {
    int qx = query(x), qy = query(y);
    if (qx != qy) {
        fa[qx] = qy;
        delta[qy] += delta[qx];
    }
}
bool dfs(int u, int c) {
    if (color[u] == c) return true;
    else if (color[u] == 3 - c) return false;
    color[u] = c; sum[c] += delta[u];
    bool ret = true;
    for (int v : graph[u]) {
        if (!dfs(v, 3 - c)) {
            ret = false; // 注意这里不管是否构成二分图都要把染色流程走完
        }
    }
    return ret;
}
int main()
{
    int t; scanf("%d", &t);
    while (t--) {
        int n, m; scanf("%d%d", &n, &m);
        for (int i = 1; i <= n; i++) {
            scanf("%d", &a[i]);
            fa[i] = i; color[i] = 0;
            graph[i].clear();
        }
        for (int i = 1; i <= n; i++) {
            scanf("%d", &b[i]);
            delta[i] = b[i] - a[i];
        }
        for (int i = 1; i <= m; i++) {
            scanf("%d%d%d", &op[i].t, &op[i].u, &op[i].v);
            if (op[i].t == 2) {
                merge(op[i].u, op[i].v);
            }
        }
        for (int i = 1; i <= m; i++) {
            if (op[i].t == 1) {
                int qu = query(op[i].u), qv = query(op[i].v);
                graph[qu].push_back(qv); graph[qv].push_back(qu);
            }
        }
        bool ans = true;
        for (int i = 1; i <= n; i++) {
            if (color[i] == 0 && query(i) == i) {
                sum[1] = sum[2] = 0;
                bool ok = dfs(i, 1);
                if (ok && sum[1] != sum[2]) {
                    ans = false; break;
                }
                // 注意坑点:C++中的负数取余
                if (!ok && (abs(sum[1]) + abs(sum[2])) % 2 == 1) {
                    ans = false; break;
                }
            }
        }
        printf("%s\n", ans ? "YES" : "NO");
    }
    return 0;
}

习题:P1155 [NOIP2008 提高组] 双栈排序

解题思路

首先考虑只有一个栈的情况。用一个栈去模拟可以得到部分分(靠一个栈可以搞定的数据点以及结果为 \(0\) 的数据点)。

\(2 3 1\) 这个样例需要两个栈才能完成,实际上可以概括为对于 \(i < j < k\) 三个位置存在 \(a_k < a_i < a_j\),此时 \(a_i\)\(a_j\) 无法共存在同一个栈中。因为 \(a_k\) 需要在 \(a_i\)\(a_j\) 之前出栈,但 \(a_i\) 又需要再 \(a_j\) 之前出栈,产生了矛盾。

因此可以预处理每个数之后最小的数,这样就可以在 \(O(n^2)\) 的时间复杂度下完成每一对 \((i,j)\) 能否共存在一个栈中的判断。

对于不能共存的两个数,实际上就需要尝试交给两个栈分别处理,则问题被转化为二分图染色问题。对于每一对不能共存的 \(i\)\(j\) 连边,如果不存在合法的染色方案,则说明无解。

接下来考虑如何保证字典序最小:

染色时让编号小的数尽量放入第一个栈,最终每个点的染色方案代表其交给哪一个栈来处理。用两个栈模拟操作时,注意如果第二个栈的栈顶是下一个排完序的数时,不一定马上就将其出栈(d 操作),此时有一部分 a 操作可以先引入进来(字典序更小),而这时可行的 a 操作需要保证新入栈的数属于第一个栈并且小于第一个栈的栈顶(否则说明它至少要等当前的栈顶出栈之后才能入栈)。

参考代码
#include <cstdio>
#include <stack>
#include <vector>
#include <queue>
using namespace std;
const int N = 1005;
int n, a[N], color[N], ans[N * 2], suf[N], g[N][N];
bool dfs(int u, int c) {
    if (color[u] == c) 
        return true;
    else if (color[u] == 3 - c) 
        return false;
    color[u] = c;
    for (int i = 1; i <= n; i++) 
        if (g[u][i] && !dfs(i, 3 - c)) return false;
    return true;
}
int main()
{
    scanf("%d", &n);
    for (int i = 1; i <= n; i++) {
        scanf("%d", &a[i]);
    }
    // suf[i]记录a[i]后最小的数
    int minnum = n;
    for (int i = n - 1; i >= 1; i--) {
        suf[i] = minnum;
        if (a[i] < a[minnum]) minnum = i;
    }
    for (int i = 1; i < n; i++)
        for (int j = i + 1; j < n; j++) {
            // 寻找是否存在i<j<k而a[k]<a[i]<a[j]
            if (a[i] < a[j] && a[suf[j]] < a[i]) {
                g[i][j] = g[j][i] = 1;
            }
        }
    bool ok = true;
    for (int i = 1; i <= n; i++)
        if (color[i] == 0) {
            ok = dfs(i, 1);
            if (!ok) break;
        }
    if (!ok) printf("0\n");
    else {
        int cur = 1, i = 1, idx = 0;
        stack<int> s1, s2;
        while (cur <= n || i <= n) {
            if (!s1.empty() && s1.top() == cur) {
                ans[++idx] = 1;
                cur++;
                s1.pop();
            } else if (!s2.empty() && s2.top() == cur) {
                // s2出栈是d操作,但此时如果有合适的a操作可做则做a操作
                if (i <= n && color[i] == 1 && (s1.empty() || s1.top() > a[i])) {
                    ans[++idx] = 0;
                    s1.push(a[i]); i++;
                } else {
                    ans[++idx] = 3;
                    cur++; 
                    s2.pop();
                }
            } else if (i <= n && color[i] == 1) {
                ans[++idx] = 0;
                s1.push(a[i]); i++;
            } else if (i <= n && color[i] == 2) {
                ans[++idx] = 2;
                s2.push(a[i]); i++;
            }
        }
        for (int i = 1; i <= idx; i++)
            printf("%c%c", 'a' + ans[i], i == idx ? '\n' : ' ');
    }
    return 0;
}

习题:P3209 [HNOI2010] 平面图判定

解题思路

判定任意图是否为平面图是一个复杂问题,但本题给出了一个极其重要的条件:图存在哈密顿回路。

可以利用这个哈密顿回路来简化问题,想象一下,把这个哈密顿回路画成一个大圆圈,图中的 \(N\) 个顶点就顺次分布在这个圆圈上。这样一来,图中的所有边可以被分为两类:

  1. 哈密顿回路自身的边:这些边构成了圆圈的边界,它们之间不会交叉。
  2. 不属于哈密顿回路的边:这些边连接圆圈上不相邻的顶点,相当于圆的“弦”。

现在,整个图能否平面化,就取决于这些“弦”能否在不交叉的情况下画出来。对于每一条弦,都有两种画法:画在圆圈内部,或者画在圆圈外部。

两条弦在什么情况下会交叉呢?

  • 如果一条画在内部,一条画在外部,它们永远不会交叉。
  • 如果两条都画在内部(或都画在外部),只有当它们在圆圈上的四个端点相互交错时,它们才会交叉。

这个约束条件是解决问题的核心:如果两条弦是交错的,那么它们必须被画在不同的区域(一个在内,一个在外)。

这能让我们联想到二分图染色模型,可以构建一个“冲突图”:

  • 冲突图的每个节点代表原图中的一条
  • 如果两条弦 A 和 B 在哈密顿回路上是交错的,就在冲突图中对应的节点 A 和 B 之间连接一条边。
  • 这条边意味着“弦 A 和弦 B 不能在同一区域”。

现在问题转化为:能否给冲突图中的每个节点染上两种颜色(比如“内”和“外”)之一,使得任意一条边的两个端点颜色都不同?这正是二分图判定问题。

此外,根据欧拉公式,一个顶点数 \(N \ge 3\) 的简单平面图,其边数 \(M\) 必须满足 \(M \le 3N-6\)。这个定理可以作为一个强大的剪枝,快速排除掉边过多的非平面图。

参考代码
#include <cstdio>
#include <algorithm>
#include <vector>
using std::swap;
using std::vector;

const int N = 605;   // 适用于 m 和 n 的大小
const int M = 10005;

int n, m;
int from[M], to[M]; // 存储 m 条边的端点
int id[N];        // id[v] = i 表示顶点 v 在哈密顿回路上的位置是 i
int color[M];     // color[i] 存储第 i 条弦的颜色 (1 或 2)
vector<int> g[M]; // 冲突图的邻接表,节点是图的弦

// 判断两条弦 (l1,r1) 和 (l2,r2) 是否交叉
// 这里的 l,r 是顶点在哈密顿回路上的位置
bool cross(int l1, int r1, int l2, int r2) {
    if (l1 < l2 && l2 < r1 && r1 < r2) return true;
    if (l2 < l1 && l1 < r2 && r2 < r1) return true;
    return false;
}

// DFS 用于二分图判定和染色
bool dfs(int u, int c) {
    if (color[u]) { // 如果已染色
        return color[u] == c; // 检查颜色是否冲突
    }
    color[u] = c; // 染色
    for (int v : g[u]) { // 遍历所有冲突的弦
        if (!dfs(v, 3 - c)) return false; // 给冲突的弦染上相反的颜色
    }
    return true;
}

void solve() {
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= m; i++) {
        scanf("%d%d", &from[i], &to[i]);
    }
    // 建立顶点到哈密顿回路位置的映射
    for (int i = 1; i <= n; i++) {
        int v; scanf("%d", &v);
        id[v] = i;
    }

    // 平面图的必要条件:对于 n>=3 的简单图,m <= 3n - 6
    // 这是一个快速的剪枝,可以排除大部分非平面图
    if (m > 3 * n - 6) {
        printf("NO\n");
        return;
    }

    // 清理上一组数据的冲突图和颜色
    for (int i = 1; i <= m; i++)  {
        color[i] = 0;
        g[i].clear();
    }

    // 构建冲突图
    for (int i = 1; i <= m; i++) {
        int l1 = id[from[i]], r1 = id[to[i]];
        if (l1 > r1) swap(l1, r1);
        // 如果这条边是哈密顿回路的一部分,则它不是“弦”,不参与冲突判断
        if (r1 == l1 + 1 || (l1 == 1 && r1 == n)) continue;

        for (int j = i + 1; j <= m; j++) {
            int l2 = id[from[j]], r2 = id[to[j]];
            if (l2 > r2) swap(l2, r2);
            if (r2 == l2 + 1 || (l2 == 1 && r2 == n)) continue;

            // 如果两条弦交叉,则在冲突图中连边
            if (cross(l1, r1, l2, r2)) {
                g[i].push_back(j);
                g[j].push_back(i);
            }
        }
    }

    // 对冲突图进行二分图染色
    bool ok = true;
    for (int i = 1; i <= m; i++) {
        if (!color[i]) { // 如果第 i 条弦还未染色
            if (!dfs(i, 1)) { // 尝试染色,如果失败则不是二分图
                ok = false;
                break;
            }
        }
    }

    printf("%s\n", ok ? "YES" : "NO");
}

int main()
{
    int t; scanf("%d", &t);
    while (t--) solve();
    return 0;
}
posted @ 2024-05-04 11:20  RonChen  阅读(126)  评论(0)    收藏  举报