[CF19E]Fairy 题解

[CF19E]Fairy 题解

给出一张无向图,求删除这边后此图变成二分图的所有边。

思路

首先考虑二分图的真谛是什么,可以发现,如果一个图里面没有奇环,那么他就是一个二分图,实际上,这是充分必要的。

接着结合 DFS 树思考,可以发现:

  1. 对于树上的所有回边,他能产生贡献,当且仅当这棵树里只有一个奇环。

  2. 对于一条树边,他能产生贡献的一个必要条件是这棵树里面的所有奇环都经过它,但是这不是充分的。要想让他是充分的,还需要加上一个必要条件:没有偶环经过。

证明:

所有奇环经过且没有偶环经过的可以构成二分图。

考虑当前边连接的上半部分和下半部分,所有的奇环都跨越两部分,因此所有的奇环边两端都是同色的,删除这条边之后把下半部分的所有点颜色取反,那么原本合法的边依旧合法,不合法的奇环边也合法。

边删掉之后可以构成二分图必须要所有奇环经过且没有偶环经过。

必须要经过所有奇环显然,假设有偶环经过且构成二分图。有一个偶环和一个奇环同时经过当前边,那么这个边删掉之后偶环变成奇环,奇环变成偶环,它们连起来形成了一个奇环,矛盾,假设不成立。

既然如此,那么只需要判断一条边是否被所有奇环经过,且没有被任何偶环经过,这个显然可以用树上差分轻松解决,不过另一种巧妙的方法是类比 Tarjan 算法中的 low 函数。

\(cnt_u\) 表示经过 \(u\) 到父亲这条边中的奇环个数,转移就是儿子的函数值加上从 \(u\) 开始的,减去到 \(u\) 结束的。

偶环可以类似转移。

再次梳理算法步骤:

  1. 判断是否有至少两个奇环在不同的连通分量,如果是则无解
  2. 判断是否没有奇环,如果是直接输出所有边
  3. 对于拥有所有奇环的那个连通分量,计算每条边被覆盖的次数,对于回边,判断它是否是唯一的一条奇环回边;对于树边,判断它是否所有奇环经过且没有偶环经过。

代码实现有一些难,不过时间复杂度为极为优秀的线性 \(O(n + m)\)

// Problem: Fairy
// Contest: Luogu
// Author: Moyou
// Copyright (c) 2023 Moyou All rights reserved.
// Date: 2023-09-20 00:16:38

#include <algorithm>
#include <cstring>
#include <iostream>
#include <queue>
using namespace std;
const int N = 1e4 + 10, M = 2e4 + 10;
int n, m;
int h[N], ne[M], e[M], idx, cnt0[N], cnt1[N], dep[N];
int back[M], from[M];
void add(int a, int b) {
	e[idx] = b, from[idx] = a, ne[idx] = h[a], h[a] = idx++; 
}
int tot[2][N], ance[N], only;
void dfs(int u, int lst, int fa, int anc) {
    dep[u] = dep[fa] + 1;
    ance[u] = anc;
    for(int i = h[u]; ~i; i = ne[i]) {
        int v = e[i];
        if(!dep[v]) back[i] = 2, dfs(v, i, u, anc), cnt0[u] += cnt0[v], cnt1[u] += cnt1[v]; // back i = 2 树边
        else if(i != (lst ^ 1) && dep[v] < dep[u]) {
            back[i] = 1; // 回边
            if(dep[v] + dep[u] & 1) tot[1][anc] ++, cnt1[u] ++, cnt1[v] --; // 偶环
            else only = i, cnt0[u] ++, cnt0[v] --, tot[0][anc] ++; // 奇环
        }
    }
}
int root[N], cnt;
vector<int> ans;
signed main() {
    ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    cin >> n >> m;
    memset(h, -1, sizeof h);
    for(int i = 1, a, b; i <= m; i ++) {
        cin >> a >> b;
        add(a, b), add(b, a);
    }
    for(int i = 1; i <= n; i ++)
        if(!dep[i]) dfs(i, -1, i, i), root[++ cnt] = i;
    int flg = 0;
    for(int i = 1; i <= cnt; i ++) {
        if(flg && tot[0][root[i]]) return cout << 0 << '\n', 0;
        if(tot[0][root[i]]) flg = root[i];
    } // flg => 那个特殊连通分量的根
    if(flg == 0) {
        cout << m << '\n';
        for(int i = 1; i <= m; i ++) cout << i << ' ';
        return 0;
    }
    for(int i = 0; i < idx; i ++) {
        if(ance[e[i]] == flg) { // 找所有那个连通分量里的边
            int u = e[i];
            if(tot[0][flg] == 1 && i == only) ans.push_back(i / 2 + 1); // 回边
            else if(back[i] == 2 && cnt0[u] == tot[0][flg] && (cnt1[u] == 0)) ans.push_back(i / 2 + 1); // 树边
        }
    }
    sort(ans.begin(), ans.end());
    cout << ans.size() << '\n';
    for(auto x : ans) cout << x << ' ';
    return 0;
}

posted @ 2023-09-21 01:03  MoyouSayuki  阅读(33)  评论(0编辑  收藏  举报
:name :name