连通性相关

连通性相关

强连通分量

有向强连通图:任意两个点可以互相到达的有向图。

强连通分量(SCC):极大的强连通子图。

Tarjan 算法

维护一个栈存储搜索到的还未确定强连通分量的点,定义:

  • \(dfn_u\) :节点 \(u\) 被搜索到的次序。
  • \(low_u\)\(u\) 子树中能回溯到的最小的 \(dfn\)

对于 \(u\) 的出点 \(v\) ,分类讨论:

  • \(v\) 未被访问过:继续 dfs ,并用 \(low_v\) 更新 \(low_u\) ,这是因为 \(v\) 可以回溯到的栈中点 \(u\) 一定可以回溯到。

  • \(v\) 被访问过:

    • 已在栈中:根据 \(low\) 的定义,用 \(dfn_v\) 更新 \(low_u\)

    • 不在栈中:说明 \(v\) 已搜索完毕,其所在的连通分量已被处理,不用管它。

对于一个强连通分量,不难发现只有一个 \(u\) 满足 \(dfn_u = low_u\) ,其一定是这个强连通分量的根(dfs 树上的最浅点)。因此回溯过程中,若 \(dfn_u = low_u\) ,则新增一个强连通分量。

时间复杂度 \(O(n + m)\)

void Tarjan(int u) {
    dfn[u] = low[u] = ++dfstime, sta[++top] = u;

    for (int v : G.e[u]) {
        if (!dfn[v])
            Tarjan(v), low[u] = min(low[u], low[v]);
        else if (!leader[v])
            low[u] = min(low[u], dfn[v]);
    }

    if (low[u] == dfn[u]) {
        ++scc;

        while (sta[top] != u)
            leader[sta[top--]] = scc;
        
        leader[sta[top--]] = scc;
    }
}

Kosaraju 算法

由两次 dfs 组成:

  • 第一次 dfs:遍历所有点,并在回溯时入栈。
  • 第二次 dfs:在反图上依次从栈顶开始 dfs ,此时遍历到的点集就是一个 SCC。

时间复杂度 \(O(n + m)\) ,可以用 bitset 优化做到 \(O(\frac{n^2}{\omega})\)

void dfs1(int u) {
    vis[u] = true;
    
    for (int v : G.e[u])
        if (!vis[v])
            dfs1(v);
    
    sta[++top] = u;
}

void dfs2(int u) {
    leader[u] = scc;
    
    for (int v : rG.e[u])
        if (!leader[v])
            dfs2(v);
}

inline void kosaraju() {
    for (int i = 1; i <= n; ++i)
        if (!vis[i])
            dfs1(i);
   
   for (; top; --top)
       if (!leader[sta[top]])
           ++scc, dfs(sta[top]);
}

应用

CF1515G Phoenix and Odometers

给定一张带边权的有向图,\(q\) 次询问,每次给出 \(x, s, t\) ,判定是否存在一条经过 \(x\) 的回路满足长度与 \(-s\) 在模 \(t\) 意义下同余。

\(n, m, q \le 2 \times 10^5\)

首先不难发现每个 SCC 的答案是一致的,且不同 SCC 之间相互独立,故考虑对于每个 SCC 分开计算。

假设经过 \(u\) 有两个长度为 \(a\)\(b\) 的环,那么就相当于找两个非负整数 \(x\)\(y\),使得 \(ax + by = w\),其中 \(w\) 为题中的路径长,根据裴蜀定理得到上述方程成立当且仅当 \(\gcd(a, b) \mid w\)

考虑如何求出经过点 \(u\) 的所有环长度的 \(\gcd\) 。由于所有的非树边 \(u \to v\) 对答案的贡献都是 \(dis_u + w - dis_v\) ,于是搜索时顺便记录贡献即可。

#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int N = 2e5 + 7;

struct Graph {
    vector<pair<int, int> > e[N];
    
    inline void insert(int u, int v, int w) {
        e[u].emplace_back(v, w);
    }
} G;

ll dis[N], g[N];
int dfn[N], low[N], sta[N], leader[N];
bool vis[N];

int n, m, q, dfstime, top, scc;

void Tarjan(int u) {
    dfn[u] = low[u] = ++dfstime, sta[++top] = u;

    for (auto it : G.e[u]) {
        int v = it.first;

        if (!dfn[v])
            Tarjan(v), low[u] = min(low[u], low[v]);
        else if (!leader[v])
            low[u] = min(low[u], dfn[v]);
    }

    if (low[u] == dfn[u]) {
        ++scc;

        while (sta[top] != u)
            leader[sta[top--]] = scc;

        leader[sta[top--]] = scc;
    }
}

void dfs(int u, int id) {
    vis[u] = true;

    for (auto it : G.e[u]) {
        int v = it.first, w = it.second;

        if (leader[v] != id)
            continue;

        if (!vis[v])
            dis[v] = dis[u] + w, dfs(v, id);
        else
            g[id] = __gcd(g[id], abs(dis[u] - dis[v] + w));
    }
}

signed main() {
    scanf("%d%d", &n, &m);

    for (int i = 1; i <= m; ++i) {
        int u, v, w;
        scanf("%d%d%d", &u, &v, &w);
        G.insert(u, v, w);
    }

    for (int i = 1; i <= n; ++i)
        if (!dfn[i])
            Tarjan(i);

    for (int i = 1; i <= n; ++i)
        if (!vis[i])
            dfs(i, leader[i]);

    scanf("%d", &q);

    while (q--) {
        int x, s, t;
        scanf("%d%d%d", &x, &s, &t);
        puts((g[leader[x]] ? s % gcd(g[leader[x]], t) : s) ? "NO" : "YES");
    }

    return 0;
}

CF1361E James and the Chase

给定一张有向强连通图。一个点是好的当且仅当它到其它点有且只有一条简单路径。如果好的点至少有 \(20\%\) ,则输出所有好的点, 否则输出 \(-1\)

\(\sum n \le 10^5\)\(\sum m \le 2 \times 10^5\)

考虑如何判定 \(u\) 是好的,只要以 \(u\) 为根建出 dfs 树,若无横叉边或前向边则 \(u\) 即为好的。于是可以做到 \(O(n)\) 的判定。

考虑确定一个好点 \(u\) 后求出其余好点。

确定一个好点考虑随机化,随机选取一定数量的点进行上述算法流程,若均不满足条件则输出 \(-1\)

下面考虑在 \(u\) 为好点的情况下求出所有好点。以 \(u\) 为根建立 dfs 树,考虑某个 \(v\) 的子树,由于整个图的强连通性,\(v\) 的子树中有连向其祖先的返祖边。不难发现这样的边有且仅有一条,否则 \(v\) 有两条路径可以到 \(fa_v\) 或者根本无法到达 \(fa_v\) ,此时 \(v\) 就不是好点。

记录所有 \(v\) 子树内返祖到根的祖先的边的数量,若存在恰好一条返祖边就顺便记录其指向的点。假设 \(v\) 的子树这条返祖边指向了 \(w\),那么 \(v\) 是好点,当且仅当 \(w\) 是好点。

由此可以得到判定条件:一个点 \(v\) 是好点,当且仅当 \(v\) 的子树内有且仅有一条连向 \(v\) 的祖先的返祖边,并且这条边所连向的点是好点。

树上差分即可做到线性,注意最后求出好点后还需判断占比是否 \(\ge 20 \%\)

#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 7;

struct Graph {
    vector<int> e[N];

    inline void clear(int n) {
        for (int i = 1; i <= n; ++i)
            e[i].clear();
    }
    
    inline void insert(int u, int v) {
        e[u].emplace_back(v);
    }
} G, T;

int cnt[N], upto[N], dep[N];
bool vis[N], in[N], ans[N];

mt19937 myrand(time(0));
int n, m;

bool dfs1(int u) {
    vis[u] = in[u] = true;

    for (int v : G.e[u]) {
        if (vis[v] && !in[v])
            return false;
        else if (!vis[v]) {
            dep[v] = dep[u] + 1, T.insert(u, v);
            
            if (!dfs1(v))
                return false;
        } else {
            ++cnt[u], --cnt[v];

            if (dep[v] < dep[upto[u]])
                upto[u] = v;
        }
    }

    return in[u] = false, true;
}

inline bool check(int u) {
    memset(cnt + 1, 0, sizeof(int) * n);
    memset(upto + 1, 0, sizeof(int) * n);
    memset(vis + 1, false, sizeof(bool) * n);
    memset(in + 1, false, sizeof(bool) * n);
    T.clear(n), dep[0] = n + 1, dep[u] = 1;
    return dfs1(u);
}

inline void dfs2(int u) {
    for (int v : T.e[u]) {
        dfs2(v), cnt[u] += cnt[v];

        if (dep[upto[v]] < dep[upto[u]])
            upto[u] = upto[v];
    }
}

void dfs3(int u) {
    if(cnt[u] == 1 && dep[upto[u]] < dep[u] && ans[upto[u]])
        ans[u] = true;

    for (int v : T.e[u])
        dfs3(v);
}

signed main() {
    int T;
    scanf("%d", &T);

    while (T--) {
        scanf("%d%d", &n, &m), G.clear(n);

        for (int i = 1; i <= m; ++i) {
            int u, v;
            scanf("%d%d", &u, &v);
            G.insert(u, v);
        }

        vector<int> id(n);
        iota(id.begin(), id.end(), 1), shuffle(id.begin(), id.end(), myrand);
        int root = -1;

        for (int i = 0; i < min(n, 100); ++i)
            if (check(id[i])) {
                root = id[i];
                break;
            }

        if (root == -1) {
            puts("-1");
            continue;
        }

        memset(ans + 1, false, sizeof(bool) * n);
        dfs2(root), ans[root] = true, dfs3(root);

        if (count(ans + 1, ans + 1 + n, true) * 5 < n)
            puts("-1");
        else {
            for (int i = 1; i <= n; ++i)
                if (ans[i])
                    printf("%d ", i);

            puts("");
        }
    }

    return 0;
}

BZOJ5218 省队十连测 友好城市

给出一张有向图,\(q\) 次询问仅保留编号属于 \([l_i, r_i]\) 的边时有多少无序对城市满足可以两两到达。

\(n \le 150\)\(m \le 3 \times 10^5\)\(q \le 5 \times 10^4\)

注意到 \(n\) 很小,使用 Kosaraju 配合莫队即可,时间复杂度 \(O(\frac{q n^2}{\omega} + q \sqrt m)\)

#include <bits/stdc++.h>
using namespace std;
const int N = 1.5e2 + 7, M = 3e5 + 7, Q = 5e4 + 7;

struct Edge {
    int u, v;
} E[M];

struct Query {
    int l, r, *ans, bid;
    
    inline bool operator < (const Query &rhs) const {
        return bid == rhs.bid ? (bid & 1 ? r < rhs.r : r > rhs.r) : bid < rhs.bid;
    }
} qry[Q];

bitset<N> e1[N], e2[N];
bitset<N> vis;

int cnt1[N][N], cnt2[N][N], ans[Q], sta[N];

int n, m, q, block, top, scc;

inline void Add(int x) {
    int u = E[x].u, v = E[x].v;
    
    if (!cnt1[u][v])
        e1[u][v] = true;
    
    if (!cnt2[v][u])
        e2[v][u] = true;
    
    ++cnt1[u][v], ++cnt2[v][u];
}

inline void Del(int x) {
    int u = E[x].u, v = E[x].v;
    --cnt1[u][v], --cnt2[v][u];
    
    if (!cnt1[u][v])
        e1[u][v] = false;
    
    if (!cnt2[v][u])
        e2[v][u] = false;
}

void dfs1(int u) {
    vis.set(u);
    bitset<N> now = ~vis & e1[u];
    
    while (now.any())
        dfs1(now._Find_first()), now &= ~vis;
    
    sta[++top] = u;
}

int dfs2(int u) {
    vis.set(u);
    bitset<N> now = ~vis & e2[u];
    int siz = 1;
    
    while (now.any())
        siz += dfs2(now._Find_first()), now &= ~vis;

    return siz;
}

inline int kosaraju() {
    vis.reset(), scc = 0;
    int res = 0;
    
    for (int i = 1; i <= n; ++i)
        if (!vis.test(i))
            dfs1(i);
    
    vis.reset();
    
    for (; top; --top)
        if (!vis.test(sta[top])) {
            int siz = dfs2(sta[top]);
            res += siz * (siz - 1) / 2;
        }
    
    return res;
}

signed main() {
    scanf("%d%d%d", &n, &m, &q), block = sqrt(m);
    
    for (int i = 1; i <= m; ++i)
        scanf("%d%d", &E[i].u, &E[i].v);
    
    for (int i = 1; i <= q; ++i)
        scanf("%d%d", &qry[i].l, &qry[i].r), qry[i].bid = qry[i].l / block, qry[i].ans = ans + i;
    
    sort(qry + 1, qry + 1 + q);
    
    for (int i = 1, l = 1, r = 0; i <= q; ++i) {
        while (l > qry[i].l)
            Add(--l);
        
        while (r < qry[i].r)
            Add(++r);
        
        while (l < qry[i].l)
            Del(l++);
        
        while (r > qry[i].r)
            Del(r--);
        
        *qry[i].ans = kosaraju();
    }
    
    for (int i = 1; i <= q; ++i)
        printf("%d\n", ans[i]);
    
    return 0;
}

P5163 WD与地图

给定一张 \(n\) 个点 \(m\) 条边的有向图,每个点带点权。\(q\) 次操作,操作有:

  • 删除一条边。
  • 单点加点权。
  • 求某点所在 SCC 的点权前 \(k\) 大值和,若不足 \(k\) 个则取全部。

\(n \le 10^5\)\(m, q \le 2 \times 10^5\)

首先时光倒流将删边转化为加边,则需要动态维护 SCC,发现这比较困难。

考虑离线求出每条边合并 SCC 的时间,这样就可以直接线段树合并回答询问了。

考虑整体二分,假设目前已知 \([L, R]\) 内的边合并 SCC 的时间 \(\in [l, r]\) ,初始时每条边的时间 \(t\) 设为加入时间。记 \(mid = \lfloor \frac{l + r}{2} \rfloor\) ,拿出所有 \(t \le mid\) 的边跑一边 Tarjan,此时两端不在同一 SCC 的边均有 \(t > mid\) ,由此可以递归处理。

求出每条边合并 SCC 的时间问题与无向图的情况类似,线段树合并维护即可。

时间复杂度 \(O(q \log m + m \log q + q \log n)\)

#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int N = 1e5 + 7, M = 2e5 + 7;

struct Edge {
    int u, v, t;
} e[M], el[M], er[M];

struct Node {
    int op, x, y;
} nd[M];

struct Graph {
    vector<int> e[N];
    
    inline void insert(int u, int v) {
        e[u].emplace_back(v);
    }
} G;

struct DSU {
    int fa[N];
    
    inline void prework(int n) {
        iota(fa + 1, fa + n + 1, 1);
    }
    
    inline int find(int x) {
        while (x != fa[x])
            fa[x] = fa[fa[x]], x = fa[x];
    
        return x;
    }
    
    inline void merge(int x, int y) {
        fa[find(y)] = find(x);
    }
} dsu;

map<pair<int, int>, int> mp;
vector<int> vec;

ll ans[M];
int val[N], dfn[N], low[N], sta[N], leader[N];

int n, m, q, dfstime, top;

void Tarjan(int u) {
    dfn[u] = low[u] = ++dfstime, sta[++top] = u;

    for (int v : G.e[u]) {
        if (!dfn[v])
            Tarjan(v), low[u] = min(low[u], low[v]);
        else if (!leader[v])
            low[u] = min(low[u], dfn[v]);
    }

    if (low[u] == dfn[u]) {
        while (sta[top] != u)
            leader[sta[top--]] = u;
        
        leader[sta[top--]] = u;
    }
}

void solve(int l, int r, int L, int R) {
    if (L > R || l == r)
        return;

    int mid = (l + r) >> 1, tl = 0, tr = 0;

    for (int i = L; i <= R; ++i) {
        if (e[i].t <= mid)
            el[++tl] = e[i];
        else
            er[++tr] = e[i];
    }

    memcpy(e + L, el + 1, sizeof(Edge) * tl), memcpy(e + L + tl, er + 1, sizeof(Edge) * tr);

    for (int it : vec)
        dfn[it] = low[it] = leader[it] = 0, G.e[it].clear();

    dfstime = 0, vec.clear();

    for (int i = L; i < L + tl; ++i)
        vec.emplace_back(e[i].u), vec.emplace_back(e[i].v), G.insert(e[i].u, e[i].v);

    for (int it : vec)
        if (!dfn[it])
            Tarjan(it);

    for (int i = L + tl - 1; i >= L; --i)
        if (!leader[e[i].u] || !leader[e[i].v] || leader[e[i].u] != leader[e[i].v])
            e[i].t = mid + 1;

    tl = 0, tr = 0;

    for (int i = L; i <= R; ++i) {
        if (e[i].t <= mid)
            el[++tl] = e[i];
        else
            er[++tr] = e[i];
    }

    memcpy(e + L, el + 1, sizeof(Edge) * tl), memcpy(e + L + tl, er + 1, sizeof(Edge) * tr);

    for (int i = L + tl; i <= R; ++i) {
        if (leader[e[i].u])
            e[i].u = leader[e[i].u];

        if (leader[e[i].v])
            e[i].v = leader[e[i].v];
    }
    
    solve(l, mid, L, L + tl - 1), solve(mid + 1, r, L + tl, R);
}

namespace SMT {
const int S = 3e7 + 7;

ll s[S];
int rt[N], lc[S], rc[S], cnt[S];

int tot;

void update(int &x, int nl, int nr, int p, int k) {
    if (!x)
        x = ++tot;

    cnt[x] += k, s[x] += 1ll * p * k;

    if (nl == nr)
        return;

    int mid = (nl + nr) >> 1;

    if (p <= mid)
        update(lc[x], nl, mid, p, k);
    else
        update(rc[x], mid + 1, nr, p, k);
}

int merge(int a, int b, int l, int r) {
    if (!a || !b)
        return a | b;

    cnt[a] += cnt[b], s[a] += s[b];

    if (l == r)
        return a;
    
    int mid = (l + r) >> 1;
    lc[a] = merge(lc[a], lc[b], l, mid);
    rc[a] = merge(rc[a], rc[b], mid + 1, r);
    return a;
}

ll query(int x, int nl, int nr, int k) {
    if (!x)
        return 0;
    
    if (nl == nr)
        return 1ll * nl * min(cnt[x], k);

    int mid = (nl + nr) >> 1;
    return cnt[rc[x]] <= k ? s[rc[x]] + query(lc[x], nl, mid, k - cnt[rc[x]]) : query(rc[x], mid + 1, nr, k);
}
} // namespace SMT

signed main() {
    scanf("%d%d%d", &n, &m, &q);

    for (int i = 1; i <= n; ++i)
        scanf("%d", val + i);

    for (int i = 1; i <= m; ++i)
        scanf("%d%d", &e[i].u, &e[i].v), mp[make_pair(e[i].u, e[i].v)] = i;

    for (int i = q; i; --i) {
        scanf("%d%d%d", &nd[i].op, &nd[i].x, &nd[i].y);

        if (nd[i].op == 1)
            e[mp[make_pair(nd[i].x, nd[i].y)]].t = i;
        else if (nd[i].op == 2)
            val[nd[i].x] += nd[i].y;
    }

    solve(0, q, 1, m), dsu.prework(n);

    for (int i = 1; i <= n; ++i)
        SMT::update(SMT::rt[i], 1, 1e9, val[i], 1);

    for (int i = 0, j = 1; i <= q; ++i) {
        if (i) {
            int u = dsu.find(nd[i].x);

            if (nd[i].op == 2) {
                SMT::update(SMT::rt[u], 1, 1e9, val[nd[i].x], -1);
                SMT::update(SMT::rt[u], 1, 1e9, val[nd[i].x] -= nd[i].y, 1);
            } else if (nd[i].op == 3)
                ans[i] = SMT::query(SMT::rt[u], 1, 1e9, nd[i].y);
        }

        for (; j <= m && e[j].t == i; ++j) {
            int fx = dsu.find(e[j].u), fy = dsu.find(e[j].v);

            if (fx != fy)
                dsu.merge(fx, fy), SMT::rt[fx] = SMT::merge(SMT::rt[fx], SMT::rt[fy], 1, 1e9);
        }
    }

    for (int i = q; i; --i)
        if (nd[i].op == 3)
            printf("%lld\n", ans[i]);

    return 0;
}

[ARC092F] Two Faced Edges

给定一张有向图,对每条边判定将其反向后 SCC 的数量是否变化。

\(n \leq 10^3\)\(m \leq 2 \times 10^5\)

对于一条边 \(u \to v\) ,反向后 SCC 的数量变化当且仅当以下两个条件恰好满足其一:

  • 删去 \(u \to v\) 的边后 \(u\) 能到达 \(v\)
  • 删去 \(u \to v\) 的边后 \(v\) 能到达 \(u\)

后者删去与否没有区别,因此可以直接 dfs 判定。

对于前者,考虑把所有 \(u\) 相同的边一起处理,记 \(u\) 的出点为 \(v_{1 \sim k}\) 。考虑按 \(v_{1, 2, \cdots, k}\) 的顺序 dfs 一遍,记录每个点 \(w\) 是被哪个 \(v\) 搜到的,记为 \(p(w)\) 。然后再按 \(v_{k, k - 1, \cdots, 1}\) 的顺序 dfs 一遍,记录每个点 \(w\) 是被哪个 \(v\) 搜到的,记为 \(q(w)\) 。则对于一个 \(v_i\) ,若 \(p(v_i) = q(v_i)\) ,则说明 \(v_i\) 无法被其他的 \(v\) 走到,因此删去 \(u \to v_i\)\(u\) 无法到达 \(v\)

dfs 用 bitset 优化,时间复杂度 \(O(\frac{n^3}{\omega} + m)\)

#include <bits/stdc++.h>
using namespace std;
const int N = 1e3 + 7, M = 2e5 + 7;

struct Graph {
    vector<pair<int, int> > e[N];
    
    inline void insert(int u, int v, int w) {
        e[u].emplace_back(v, w);
    }
} G;

bitset<N> e[N], reach[N], vis;

int p[N], q[N];
bool ans[M];

int n, m;

void dfs1(int u) {
    vis.set(u);

    while ((e[u] & ~vis).any())
        dfs1((e[u] & ~vis)._Find_first());
}

void dfs2(int u, int *p, int r) {
    vis.set(u), p[u] = r;

    while ((e[u] & ~vis).any())
        dfs2((e[u] & ~vis)._Find_first(), p, r);
}

signed main() {
    scanf("%d%d", &n, &m);

    for (int i = 1; i <= m; ++i) {
        int u, v;
        scanf("%d%d", &u, &v);
        G.insert(u, v, i), e[u].set(v);
    }

    for (int i = 1; i <= n; ++i)
        vis.reset(), dfs1(i), reach[i] = vis;

    for (int u = 1; u <= n; ++u) {
        vis.reset(), vis.set(u);

        for (auto it : G.e[u])
            if (!vis.test(it.first))
                dfs2(it.first, p, it.first);

        reverse(G.e[u].begin(), G.e[u].end());
        vis.reset(), vis.set(u);

        for (auto it : G.e[u])
            if (!vis.test(it.first))
                dfs2(it.first, q, it.first);

        for (auto it : G.e[u])
            ans[it.second] = reach[it.first].test(u) ^ (p[it.first] != q[it.first]);
    }

    for (int i = 1; i <= m; ++i)
        puts(ans[i] ? "diff" : "same");

    return 0;
}

2-SAT

2-SAT 问题:给定一串布尔变量,每个变量只能为真或假。要求对这些变量进行赋值,满足布尔方程。

考虑建立图论模型:

  • 点的状态:将点 \(u\) 拆分成 \(u0,u1\) 两个点,分别表示 \(u\) 点为假、真。
  • 边的状态:若连的边为 \(u \to v\) ,就表示选择 \(u\) 就必须选 \(v\)
    • 注意连边不能漏连,每个条件的逆否命题也要连边。

由所构造的状态可知,对于图中的每一个强连通分量,如果选择了其中任意一个点,那就意味着这个强连通分量中的所有点都要选。显然 \(x0,x1\) 不可以同时选,由此可判断有无解。

构造方案时,对于每个点的两种状态,选择拓扑序大的,舍弃掉另一个,这样可以尽可能不冲突。如果要求字典序最小,就 dfs 枚举点 \(1 \to 2n\) ,贪心选取即可。

P4782 【模板】2-SAT 问题

#include <bits/stdc++.h>
using namespace std;
const int N = 2e6 + 7;

struct Graph {
    vector<int> e[N];
    
    inline void insert(int u, int v) {
        e[u].emplace_back(v);
    }
} G;

int dfn[N], low[N], sta[N], leader[N];

int n, m, dfstime, top, scc;

void Tarjan(int u) {
    dfn[u] = low[u] = ++dfstime, sta[++top] = u;

    for (int v : G.e[u]) {
        if (!dfn[v])
            Tarjan(v), low[u] = min(low[u], low[v]);
        else if (!leader[v])
            low[u] = min(low[u], dfn[v]);
    }

    if (low[u] == dfn[u]) {
        ++scc;

        while (sta[top] != u)
            leader[sta[top--]] = scc;

        leader[sta[top--]] = scc;
    }
}

signed main() {
    scanf("%d%d", &n, &m);

    for (int i = 1; i <= m; ++i) {
        int u, x, v, y;
        scanf("%d%d%d%d", &u, &x, &v, &y);
        G.insert(u + (x ^ 1) * n, v + y * n);
        G.insert(v + (y ^ 1) * n, u + x * n);
    }

    for (int i = 1; i <= n * 2; ++i)
        if (!dfn[i])
            Tarjan(i);

    for (int i = 1; i <= n; ++i)
        if (leader[i] == leader[i + n])
            return puts("IMPOSSIBLE"), 0;

    puts("POSSIBLE");

    for (int i = 1; i <= n; ++i)
        printf("%d ", leader[i + n] < leader[i]);

    return 0;
}

应用

P3825 [NOI2017] 游戏

给定一串由 ABCX 组成的序列,其中 X\(d\) 个。 ABC 分别表示该位置不能填的字母,X 表示三个字母均可以填。

给出 \(m\) 条限制,形如 \(i\) 处填 \(x\)\(j\) 处必须填 \(y\) ,构造一组合法填入 ABC 的方案。

\(n \le 5 \times 10^4\)\(m \le 10^5\)\(d \le 8\)

暴力枚举每个 \(x\) 地图不填 \(A\) 或不填 \(B\) 。因为不填 \(A\) 就可以填 \(B, C\) ,不填 \(B\) 就可以填 \(A, C\) ,这样就包含了 \(A, B, C\) 三种赛车。

时间复杂度降为 \(O((n+m) \times 2^d)\)

#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 7;

struct Graph {
    vector<int> e[N];

    inline void clear(int n) {
        for (int i = 1; i <= n; ++i)
            e[i].clear();
    }
    
    inline void insert(int u, int v) {
        e[u].emplace_back(v);
    }
} G;

struct Node {
    int x, y;
    char cx, cy;
} nd[N];

int dfn[N], low[N], leader[N], sta[N];
char str[N];

int n, d, m, dfstime, top, scc;

inline int trans(int x) {
    return x <= n ? x + n : x - n;
}

inline int getid(int x, char op) {
    if (str[x] == 'a')
        return op == 'b' ? x : x + n;
    else
        return op == 'a' ? x : x + n;
}

void Tarjan(int u) {
    dfn[u] = low[u] = ++dfstime, sta[++top] = u;

    for (int v : G.e[u]) {
        if (!dfn[v])
            Tarjan(v), low[u] = min(low[u], low[v]);
        else if (!leader[v])
            low[u] = min(low[u], dfn[v]);
    }

    if (dfn[u] == low[u]) {
        ++scc;

        while (sta[top] != u)
            leader[sta[top--]] = scc;
        
        leader[sta[top--]] = scc;
    }
}

inline bool solve() {
    memset(dfn + 1, 0, sizeof(int) * (n * 2));
    memset(low + 1, 0, sizeof(int) * (n * 2));
    memset(leader + 1, 0, sizeof(int) * (n * 2));
    G.clear(n * 2), dfstime = scc = 0;

    for (int i = 1; i <= m; ++i) {
        if (str[nd[i].x] == nd[i].cx)
            continue;

        int x = getid(nd[i].x, nd[i].cx), y = getid(nd[i].y, nd[i].cy);

        if (str[nd[i].y] == nd[i].cy)
            G.insert(x, trans(x));
        else
            G.insert(x, y), G.insert(trans(y), trans(x));
    }

    for (int i = 1; i <= n * 2; ++i)
        if (!dfn[i])
            Tarjan(i);

    for (int i = 1; i <= n; ++i)
        if (leader[i] == leader[i + n])
            return false;

    return true;
}

bool dfs(int pos) {
    if (pos > n)
        return solve();
    else if (str[pos] != 'x')
        return dfs(pos + 1);

    for (int i = 0; i < 2; ++i) {
        str[pos] = 'a' + i;

        if (dfs(pos + 1))
            return true;
    }

    return str[pos] = 'x', false;
}

signed main() {
    cin >> n >> d >> (str + 1) >> m;

    for (int i = 1; i <= m; ++i) {

        cin >> nd[i].x >> nd[i].cx >> nd[i].y >> nd[i].cy;
        nd[i].cx = tolower(nd[i].cx), nd[i].cy = tolower(nd[i].cy);
    }

    if (!dfs(1))
        return cout << -1, 0;

    for (int i = 1; i <= n; ++i) {
        if (str[i] == 'a')
            cout << (leader[i] < leader[i + n] ? 'B' : 'C');
        else if (str[i] == 'b')
            cout << (leader[i] < leader[i + n] ? 'A' : 'C');
        else
            cout << (leader[i] < leader[i + n] ? 'A' : 'B');
    }

    return 0;
}

[ARC161E] Not Dyed by Majority (Cubic Graph)

给出每个点的度数恰为 \(3\) 的无向图。一次操作为将每个点的颜色变为所有邻居颜色的众数。构造黑白颜色序列使得无论如何染色,恰好依次操作后都不可能变为该颜色序列。

\(\sum n \le 5 \times 10^4\) ,保证 \(n\) 为偶数

考虑如何判定一个颜色序列是否可作为操作后的颜色序列。

三元组的约束不好处理,考虑转化为二元组。设操作前的颜色序列为 \(a_i\)(不妨使 \(a_i = 0, 1\) 代表黑和白),操作后的颜色序列为 \(b_i\) 。对于点 \(i\)\(e_{i, j}\) 为与 \(i\) 相邻的三个点。

对每个 \(i\) 有两种情况:

  • \(b_i = 0\):则对 \(j \not = k\),若 \(a_{e_{i, j}} = 1\)\(a_{e_{i, k}} = 0\)
  • \(b_i=1\):则对 \(j\ne k\),若 \(a_{e_{i, j}} = 0\)\(a_{e_{i, k}} = 1\)

这是一个关于 \(a_i\),有 \(6n\) 个条件的 2-SAT 问题,可以 \(O(n)\) 解决,目标转化为找到一个方案使得 2-SAT 问题无解。

考虑 \(1\) 的邻域 \(x,y,z\),以及他们的邻域 \(\{ 1, x_0, x_1 \}, \{ 1, y_0, y_1 \}, \{1, z_0, z_1\}\) ,若 \(d_{x_0}=d_{x_1},d_{y_0}=d_{y_1},d_{z_0}=d_{z_1}\) ,那么无论 \(d_1\) 是什么,答案都不会更改。因此至少有 \(\frac 1{16}\) 的序列会互相重复,那么期望 \(O(1)\) 次随机后能得到一组解。

#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 7;

struct Graph {
    vector<int> e[N];

    inline void clear(int n) {
        for (int i = 1; i <= n; ++i)
            e[i].clear();
    }
    
    inline void insert(int u, int v) {
        e[u].emplace_back(v);
    }
} G, nG;

int col[N], dfn[N], low[N], sta[N], leader[N];

mt19937 myrand(time(0));
int n, dfstime, scc, top;

void Tarjan(int u) {
    dfn[u] = low[u] = ++dfstime, sta[++top] = u;

    for (int v : nG.e[u]) {
        if (!dfn[v]) {
            Tarjan(v);
            low[u] = min(low[u], low[v]);
        } else if (!leader[v])
            low[u] = min(low[u], dfn[v]);
    }

    if (low[u] == dfn[u]) {
        ++scc;

        while (sta[top] != u)
            leader[sta[top--]] = scc;

        leader[sta[top--]] = scc;
    }
}

signed main() {
    int T;
    scanf("%d", &T);

    while (T--) {
        scanf("%d", &n), G.clear(n);

        for (int i = 1; i <= n / 2 * 3; ++i) {
            int u, v;
            scanf("%d%d", &u, &v);
            G.insert(u, v), G.insert(v, u);
        }

        for (;;) {
            nG.clear(n * 2);

            for (int u = 1; u <= n; ++u) {
                col[u] = myrand() & 1;

                for (int v : G.e[u])
                    for (int w : G.e[u])
                        if (v != w) {
                            if (col[u])
                                nG.insert(v, w + n);
                            else
                                nG.insert(v + n, w);
                        }
            }

            memset(dfn + 1, 0, sizeof(int) * (n * 2));
            memset(low + 1, 0, sizeof(int) * (n * 2));
            memset(leader + 1, 0, sizeof(int) * (n * 2));
            dfstime = scc = 0;

            for (int i = 1; i <= n * 2; ++i)
                if (!dfn[i])
                    Tarjan(i);

            bool flag = false;

            for (int i = 1; i <= n; ++i)
                if (leader[i] == leader[i + n]) {
                    flag = true;
                    break;
                }

            if (flag)
                break;
        }

        for (int i = 1; i <= n; ++i)
            putchar(col[i] ? 'B' : 'W');

        puts("");
    }

    return 0;
}

QOJ4892. 序列

有一个长为 \(n\) 的序列 \(a_{1 \sim n}\) ,其中 \(a_i \in [1, 10^9]\)

给出 \(m\) 条信息,每条信息形如 \(a_x, a_y, a_z\) 的中位数为 \(k\)

构造一个合法的 \(a_{1 \sim n}\) 或报告无解。

\(n \le 10^5\)

\(S(x)\) 表示 \(x\) 的候选集合,每条信息都将 \(k\)\(k - 1\) 加入 \(x, y, z\) 的候选集合,则一定存在一组解满足每个值均为候选集合中的值,若候选集和为空则任意。

三元组的约束不好处理,考虑转化为二元组。

对于一组约束 \((x, y, z, k)\) ,若 \(a_i < x\)\(a_j, a_k \ge x\) ,剩余同理,而这些限制就足以表示原约束。

考虑建图,记 \((i, k) = [a_i \ge k]\) ,则连边除了每条信息的约束,对于同一个 \(i\) 的候选集和还需连一条链,表示若 \(k_1 \ge k_2\)\(a_i \ge k_1\)\(a_i \ge k_2\)

时间复杂度 \(O(n + m)\)

#include <bits/stdc++.h>
using namespace std;
const int N = 2e6 + 7;

struct Graph {
    vector<int> e[N];
    
    inline void insert(int u, int v) {
        e[u].emplace_back(v);
    }
} G;

struct Node {
    int a[3], k;
} nd[N];

vector<int> a[N];

int sum[N], dfn[N], low[N], sta[N], leader[N], ans[N];

int n, m, dfstime, top, scc;

void Tarjan(int u) {
    dfn[u] = low[u] = ++dfstime, sta[++top] = u;

    for (int v : G.e[u]) {
        if (!dfn[v])
            Tarjan(v), low[u] = min(low[u], low[v]);
        else if (!leader[v])
            low[u] = min(low[u], dfn[v]);
    }

    if (low[u] == dfn[u]) {
        ++scc;

        while (sta[top] != u)
            leader[sta[top--]] = scc;
        
        leader[sta[top--]] = scc;
    }
}

signed main() {
    scanf("%d%d", &n, &m);

    for (int i = 1; i <= m; ++i) {
        scanf("%d%d%d%d", nd[i].a + 0, nd[i].a + 1, nd[i].a + 2, &nd[i].k);

        for (int j = 0; j < 3; ++j)
            a[nd[i].a[j]].emplace_back(nd[i].k - 1), a[nd[i].a[j]].emplace_back(nd[i].k);
    }

    for (int i = 1; i <= n; ++i) {
        sort(a[i].begin(), a[i].end());
        a[i].erase(unique(a[i].begin(), a[i].end()), a[i].end());
        sum[i] = sum[i - 1] + a[i].size();
    }

    for (int i = 1; i <= m; ++i) {
        int b[3][2];

        for (int j = 0; j < 3; ++j) {
            int x = nd[i].a[j];
            b[j][0] = sum[x - 1] + lower_bound(a[x].begin(), a[x].end(), nd[i].k - 1) - a[x].begin() + 1;
            b[j][1] = sum[x - 1] + lower_bound(a[x].begin(), a[x].end(), nd[i].k) - a[x].begin() + 1;
        }

        for (int j = 0; j < 3; ++j)
            for (int k = 0; k < 3; ++k)
                if (j != k)
                    G.insert(b[j][0] + sum[n], b[k][0]), G.insert(b[j][1], b[k][1] + sum[n]);
    }

    for (int i = 1; i <= n; ++i)
        for (int j = 0; j + 1 < a[i].size(); ++j) {
            G.insert(sum[i - 1] + j + 2, sum[i - 1] + j + 1);
            G.insert(sum[i - 1] + j + 1 + sum[n], sum[i - 1] + j + 2 + sum[n]);
        }

    for (int i = 1; i <= sum[n] * 2; ++i)
        if (!dfn[i])
            Tarjan(i);

    for (int i = 1; i <= sum[n]; ++i)
        if (leader[i] == leader[i + sum[n]])
            return puts("NO"), 0;

    memset(ans + 1, -1, sizeof(int) * n);

    for (int i = 1; i <= n; ++i) {
        if (a[i].empty()) {
            ans[i] = 1;
            continue;
        }

        for (int j = 0; j < a[i].size(); ++j)
            if (leader[sum[i - 1] + j + 1] > leader[sum[i - 1] + j + 1 + sum[n]]) {
                ans[i] = a[i][j];
                break;
            }

        if (ans[i] == -1)
            ans[i] = a[i].back() + 1;

        if (ans[i] < 1 || ans[i] > 1e9)
            return puts("NO"), 0;
    }

    puts("YES");

    for (int i = 1; i <= n; ++i)
        printf("%d ", ans[i]);

    return 0;
}

滈葕

给定一张边权为 \(0\)\(1\) 的有向图,给每个点赋予 A/B/C/D 的一个字母,使得每条有向边边权为 \(1\)\((a_u, a_v) \in \{ (A, D), (A, B), (B, D), (B, A), (C, D), (C, A), (C, B) \}\) 的充要条件。

\(n \le 10^5\)\(m \le 5 \times 10^5\)

考虑令 \(A = 01, B = 10, C = 11, D = 11\) ,于是每一条 \(1\) 的边都对应了存在某一位 \(u > v\) 。考虑如何表示为 2-SAT 的限制:

  • \(w = 0\) :对于每一位 \(u\) 都不大于 \(v\)
    • \(u\) 的某一位是 \(1\) ,则 \(v\) 的相应位必须是 \(1\)
    • \(v\) 的某一位是 \(0\) ,则 \(u\) 的相应位必须是 \(0\)
  • \(w = 1\) :存在每一位 \(u\) 大于 \(v\)
    • \(u\) 的某一位是 \(0\) ,则 \(u\) 的另一位必须是 \(1\) ,且 \(v\) 的另一位必须是 \(0\)
    • \(v\) 的某一位是 \(1\) ,则 \(v\) 的另一位必须是 \(0\) ,且 \(u\) 的另一位必须是 \(1\)

时间复杂度 \(O(n)\)

#include <bits/stdc++.h>
using namespace std;
const int N = 4e5 + 7;

struct Graph {
    vector<int> e[N];
    
    inline void insert(int u, int v) {
        e[u].emplace_back(v);
    }
} G;

int dfn[N], low[N], sta[N], leader[N];

int n, m, dfstime, top, scc;

void Tarjan(int u) {
    dfn[u] = low[u] = ++dfstime, sta[++top] = u;

    for (int v : G.e[u]) {
        if (!dfn[v])
            Tarjan(v), low[u] = min(low[u], low[v]);
        else if (!leader[v])
            low[u] = min(low[u], dfn[v]);
    }

    if (low[u] == dfn[u]) {
        ++scc;

        while (sta[top] != u)
            leader[sta[top--]] = scc;
        
        leader[sta[top--]] = scc;
    }
}

signed main() {
    scanf("%d%d", &n, &m);
    // x, x + n * 2 : bit0 = 0 / 1
    // x + n, x + n * 3 : bit1 = 0 / 1

    for (int i = 1; i <= m; ++i) {
        int u, v, w;
        scanf("%d%d%d", &u, &v, &w);

        if (w) {
            G.insert(u, u + n * 3), G.insert(u, v + n);
            G.insert(u + n, u + n * 2), G.insert(u + n, v);
            G.insert(v + n * 2, v + n), G.insert(v + n * 2, u + n * 3);
            G.insert(v + n * 3, v), G.insert(v + n * 3, u + n * 2);
        } else {
            G.insert(u + n * 2, v + n * 2), G.insert(u + n * 3, v + n * 3);
            G.insert(v, u), G.insert(v + n, u + n);
        }
    }

    for (int i = 1; i <= n * 4; ++i)
        if (!dfn[i])
            Tarjan(i);

    for (int i = 1; i <= n * 2; ++i)
        if (leader[i] == leader[i + n * 2])
            return puts("NO"), 0;

    puts("YES");

    for (int i = 1; i <= n; ++i)
        putchar("DABC"[(leader[i + n * 2] < leader[i]) + (leader[i + n * 3] < leader[i + n]) * 2]);

    return 0;
}

[ARC069F] Flags

数轴上有 \(n\) 个球,第 \(i\) 个球可以选择放在 \(x_i\)\(y_i\) ,记球两两间的最小距离为 \(d\) ,求 \(\max d\)

\(n \le 10^4\)\(x_i, y_i \in [1, 10^9]\)

显然先二分答案,问题转化为 2-SAT,连边就是一个点向一个区间连边。

显然可以线段树优化建图做到 \(O(n \log n \log V)\) ,但是常数较大,并不优美。

考虑 Kosaraju,则需要在原图和反图上 dfs。用并查集维护每个点是否被访问,则容易找到区间内第一个没被访问的点,时间复杂度 \(O(n \alpha(n) \log V)\)

#include <bits/stdc++.h>
using namespace std;
const int N = 2e4 + 7;

struct DSU {
    int fa[N];
    
    inline void prework(int n) {
        iota(fa + 1, fa + n + 1, 1);
    }
    
    inline int find(int x) {
        while (x != fa[x])
            fa[x] = fa[fa[x]], x = fa[x];
    
        return x;
    }
    
    inline void merge(int x, int y) {
        fa[find(y)] = find(x);
    }
} dsu;

pair<int, int> p[N];

int rk[N], pre[N], id[N], leader[N];

int n;

inline bool check(int lim) {
    for (int i = 1, j = 1; i <= n * 2; ++i) {
        while (j <= n * 2 && p[j].first + lim <= p[i].first)
            ++j;

        pre[i] = j;
    }

    auto flip = [](int x) {
        return p[x].second <= n ? rk[p[x].second + n] : rk[p[x].second - n];
    };

    int col = -1, tot = 0;

    function<void(int)> dfs = [&](int x) {
        int u = (col == -1 ? x : flip(x));
        dsu.merge(flip(u) + 1, flip(u));

        for (int cur = dsu.find(pre[u]); cur <= n * 2 && p[cur].first < p[u].first + lim; cur = dsu.find(cur)) {
            if (cur == u)
                ++cur;
            else
                dfs(col == -1 ? flip(cur) : cur);
        }

        if (col == -1)
            id[++tot] = x;
        else
            leader[x] = col;
    };

    dsu.prework(n * 2 + 1);

    for (int i = 1; i <= n * 2; ++i)
        if (dsu.find(flip(i)) == flip(i))
            dfs(i);

    dsu.prework(n * 2 + 1);

    for (int i = tot; i; --i)
        if (dsu.find(id[i]) == id[i])
            dfs(col = id[i]);

    for (int i = 1; i <= n; ++i)
        if (leader[rk[i]] == leader[rk[i + n]])
            return false;

    return true;
}

signed main() {
    scanf("%d", &n);

    for (int i = 1; i <= n; ++i)
        scanf("%d%d", &p[i].first, &p[i + n].first), p[i].second = i, p[i + n].second = i + n;

    sort(p + 1, p + n * 2 + 1);

    for (int i = 1; i <= n * 2; ++i)
        rk[p[i].second] = i;

    int l = 0, r = 1e9, ans = 0;

    while (l <= r) {
        int mid = (l + r) >> 1;

        if (check(mid))
            ans = mid, l = mid + 1;
        else
            r = mid - 1;
    }

    printf("%d", ans);
    return 0;
}

割点

定义:在无向图中,删去后使得连通分量数增加的点称为割点,注意孤立点和孤立边的两个端点都不是割点。

对于非根点,若存在儿子 \(v\) 满足 \(low_v \ge dfn_u\) (即不能回到祖先),则该点为割点。对于根,若存在 \(\ge 2\) 个儿子则为割点。

P3388 【模板】割点(割顶)

void Tarjan(int u, int f) {
    dfn[u] = low[u] = ++dfstime;
    int sonsum = 0;

    for (int v : G.e[u]) {
        if (!dfn[v]) {
            ++sonsum, Tarjan(v, u);
            cut[u] |= (f && low[v] >= dfn[u]);
            low[u] = min(low[u], low[v]);
        } else
            low[u] = min(low[u], dfn[v]);
    }

    cut[u] |= (!f && sonsum >= 2);
}

割边(桥)

定义:在无向图中,删去后使得连通分量数增加的边称为割边(桥),注意孤立边是割边。

代码和割点差不多,只要改一处: \(low_v > dfn_u\) ,且无需特判根节点。

void Tarjan(int u, int f) {
    dfn[u] = low[u] = ++dfstime;

    for (int i = G.head[u]; i; i = G.e[i].nxt) {
        int v = G.e[i].v;

        if (!dfn[v]) {
            Tarjan(v, u), low[u] = min(low[u], low[v]);

            if (low[v] > dfn[u])
                G.e[i].cut = G.e[i ^ 1].cut = true;
        } else if (v != f)
            low[u] = min(low[u], dfn[v]);
    }
}

边双连通

P8436 【模板】边双连通分量

定义:

  • 边双连通图:不存在割边的无向连通图。
  • 边双连通分量:极大边双连通子图。
  • 边双连通:处在同一边双连通分量内的任意两点边双连通。

边双的一些性质:

  • 边双对点有传递性。
  • 每个点恰属于一个边双。
  • 对于边双内任意点 \(u\) ,存在经过 \(u\) 的回路。
  • 对于边双内任意边 \(e\) ,存在经过 \(e\) 的回路。
  • 对于边双内任意两点 \(u, v\) ,存在经过 \(u, v\) 的简单环,即 \(u, v\) 边双连通当且仅当 \(u, v\) 间无必经边。
  • 两点之间任意一条迹(不经过重复边的路径)上的所有割边,就是两点之间的所有必经边。
  • 边双可以给边定向后变成 SCC。
  • 边双缩点后形态为树或森林。

Tarjan 求解边双:先求出所有割边,遍历时不走割边即可求得边双。

也可以用栈维护 dfs 到的所有点,每次找到割边 \((fa,son)\) 就不断弹栈直到弹出 \(son\) ,则弹出的所有点是一个边双。

#include <bits/stdc++.h>
using namespace std;
const int N = 5e5 + 7, M = 4e6 + 7;

struct Graph {
    struct Edge {
        int nxt, v;
        bool cut;
    } e[M];
    
    int head[N];
    
    int tot = 1;
    
    inline void insert(int u, int v) {
        e[++tot] = (Edge){head[u], v, false}, head[u] = tot;
    }
} G;

vector<int> edcc[N];

int dfn[N], low[N];
bool vis[N];

int n, m, dfstime, tot;

void Tarjan(int u, int f) {
    dfn[u] = low[u] = ++dfstime;

    for (int i = G.head[u]; i; i = G.e[i].nxt) {
        int v = G.e[i].v;

        if (!dfn[v]) {
            Tarjan(v, u), low[u] = min(low[u], low[v]);

            if (low[v] > dfn[u])
                G.e[i].cut = G.e[i ^ 1].cut = true;
        } else if (v != f)
            low[u] = min(low[u], dfn[v]);
    }
}

void dfs(int u) {
    edcc[tot].emplace_back(u), vis[u] = true;

    for (int i = G.head[u]; i; i = G.e[i].nxt)
        if (!G.e[i].cut && !vis[G.e[i].v])
            dfs(G.e[i].v);
}

signed main() {
    scanf("%d%d", &n, &m);

    for (int i = 1; i <= m; ++i) {
        int u, v;
        scanf("%d%d", &u, &v);
        G.insert(u, v), G.insert(v, u);
    }

    for (int i = 1; i <= n; ++i)
        if (!dfn[i])
            Tarjan(i, 0);

    for (int i = 1; i <= n; ++i)
        if (!vis[i])
            ++tot, dfs(i);

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

    for (int i = 1; i <= tot; ++i) {
        printf("%d ", (int)edcc[i].size());

        for (int it : edcc[i])
            printf("%d ", it);

        puts("");
    }

    return 0;
}

P4652 [CEOI2017] One-Way Streets

给定一张 \(n\) 个点 \(m\) 条边的无向图,现在需要把边定向。

\(q\) 个限制条件,每个条件形如 \((x_i,y_i)\),表示在定向后的图中 \(x_i\) 能到达 \(y_i\)

判断每条边的方向是否能够唯一确定,并给出这些能够唯一确定的边的方向。

\(n, m, q \le 10^5\) ,保证有解

由于边双可以给边定向后变成 SCC,而将所有边反向后仍然是 SCC,因此一个边双内的边的方向都是无法确定的,而 \(x \to y\) 路径上的割边方向都是确定的。

将边双缩点,构建出一棵树,那么一个限制就是树上定向一条路径,不难树上差分解决。

#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 7;

struct Graph {
    struct Edge {
        int nxt, v;
        bool cut;
    } e[N << 1];
    
    int head[N];
    
    int tot = 1;
    
    inline void insert(int u, int v) {
        e[++tot] = (Edge) {head[u], v, false}, head[u] = tot;
    }
} G, T;

int dfn[N], low[N], leader[N], val[N], fa[N];
char ans[N];

int n, m, q, dfstime, edcc;

void Tarjan(int u, int f) {
    dfn[u] = low[u] = ++dfstime;

    for (int i = G.head[u]; i; i = G.e[i].nxt) {
        int v = G.e[i].v;

        if (!dfn[v]) {
            Tarjan(v, u), low[u] = min(low[u], low[v]);

            if (low[v] > dfn[u])
                G.e[i].cut = G.e[i ^ 1].cut = true;
        } else if (v != f)
            low[u] = min(low[u], dfn[v]);
    }
}

void dfs1(int u) {
    leader[u] = edcc;

    for (int i = G.head[u]; i; i = G.e[i].nxt) {
        int v = G.e[i].v;

        if (!G.e[i].cut && !leader[v])
            dfs1(v);
    }
}

void dfs2(int u, int f) {
    fa[u] = f;

    for (int i = T.head[u]; i; i = T.e[i].nxt) {
        int v = T.e[i].v;

        if (v != f)
            dfs2(v, u), val[u] += val[v];
    }
}

signed main() {
    scanf("%d%d", &n, &m);

    for (int i = 1; i <= m; ++i) {
        int u, v;
        scanf("%d%d", &u, &v);
        G.insert(u, v), G.insert(v, u);
    }

    for (int i = 1; i <= n; ++i)
        if (!dfn[i])
            Tarjan(i, 0);

    for (int i = 1; i <= n; ++i)
        if (!leader[i])
            ++edcc, dfs1(i);

    for (int u = 1; u <= n; ++u)
        for (int i = G.head[u]; i; i = G.e[i].nxt) {
            int v = G.e[i].v;

            if (leader[u] != leader[v])
                T.insert(leader[u], leader[v]);
        }

    scanf("%d", &q);

    while (q--) {
        int u, v;
        scanf("%d%d", &u, &v);
        ++val[leader[u]], --val[leader[v]];
    }

    for (int i = 1; i <= edcc; ++i)
        if (!fa[i])
            dfs2(i, 0);

    for (int u = 1; u <= n; ++u)
        for (int i = G.head[u]; i; i = G.e[i].nxt) {
            if (i & 1)
                continue;

            int v = G.e[i].v;

            if (leader[u] == leader[v])
                ans[i / 2] = 'B';
            else if (fa[leader[v]] == leader[u])
                ans[i / 2] = (val[leader[v]] ? (val[leader[v]] > 0 ? 'L' : 'R') : 'B');
            else
                ans[i / 2] = (val[leader[u]] ? (val[leader[u]] > 0 ? 'R' : 'L') : 'B');
        }

    ans[m + 1] = '\0', puts(ans + 1);
    return 0;
}

CF475E Strongly Connected City 2

给一个无向连通图,需要给所有边定向,最大化满足 \(a\) 能到达 \(b\) 的点对 \((a, b)\) 数量。

\(n \le 2000\)

由于边双可以赋方向后可以变成 SCC,于是考虑把先把边双缩点成树。

可以发现最优方案一定是确定根之后,一部分子树都是叶向边,剩下的子树都是父向边。

直接枚举根做 bitset 优化背包即可做到 \(O(n + m + \frac{n^2}{\omega})\)

#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const ll inf = 0x3f3f3f3f3f3f3f3f;
const int N = 2e3 + 7, M = 4e6 + 7;

struct Graph {
    struct Edge {
        int nxt, v;
        bool cut;
    } e[M];
    
    int head[N];
    
    int tot = 1;
    
    inline void insert(int u, int v) {
        e[++tot] = (Edge) {head[u], v, false}, head[u] = tot;
    }
} G, T;

int dfn[N], low[N], leader[N], a[N], siz[N];

int n, m, dfstime, edcc;

void Tarjan(int u, int f) {
    dfn[u] = low[u] = ++dfstime;

    for (int i = G.head[u]; i; i = G.e[i].nxt) {
        int v = G.e[i].v;

        if (!dfn[v]) {
            Tarjan(v, u), low[u] = min(low[u], low[v]);

            if (low[v] > dfn[u])
                G.e[i].cut = G.e[i ^ 1].cut = true;
        } else if (v != f)
            low[u] = min(low[u], dfn[v]);
    }
}

void dfs1(int u) {
    ++a[leader[u] = edcc];

    for (int i = G.head[u]; i; i = G.e[i].nxt)
        if (!leader[G.e[i].v] && !G.e[i].cut)
            dfs1(G.e[i].v);
}

void dfs2(int u, int fa) {
    siz[u] = a[u];

    for (int i = T.head[u]; i; i = T.e[i].nxt) {
        int v = T.e[i].v;

        if (v != fa)
            dfs2(v, u), siz[u] += siz[v];
    }
}

signed main() {
    scanf("%d%d", &n, &m);

    for (int i = 1; i <= m; ++i) {
        int u, v;
        scanf("%d%d", &u, &v);
        G.insert(u, v), G.insert(v, u);
    }

    for (int i = 1; i <= n; ++i)
        if (!dfn[i])
            Tarjan(i, 0);

    for (int i = 1; i <= n; ++i)
        if (!leader[i])
            ++edcc, dfs1(i);

    for (int u = 1; u <= n; ++u)
        for (int i = G.head[u]; i; i = G.e[i].nxt)
            if (G.e[i].cut)
                T.insert(leader[u], leader[G.e[i].v]);

    ll ans = 0;

    for (int u = 1; u <= edcc; ++u) {
        dfs2(u, 0);
        ll res = 0;

        for (int i = 1; i <= edcc; ++i)
            res += 1ll * siz[i] * a[i];

        bitset<N> f;
        f.set(0);

        for (int i = T.head[u]; i; i = T.e[i].nxt)
            f |= f << siz[T.e[i].v];

        for (int i = 0; i <= n - a[u]; ++i)
            if (f.test(i))
                ans = max(ans, res + 1ll * i * (n - a[u] - i));
    }

    printf("%lld", ans);
    return 0;
}

CF51F Caterpillar

定义一张无向连通无环图是毛毛虫,当且仅当图上存在一条路径 \(P\) ,满足所有点到与 \(P\) 的距离均 \(\leq 1\) ,且 \(P\) 不能有重边。

给出一张无向图,一次操作可以将两个点合并(出边也合并),求将其变成毛毛虫的最少操作次数。

\(n \le 2000\)\(m \le 10^5\)

由于毛毛虫无环,因此先对整张图按边双缩点转化为森林,则每个边双都要合并成一个点。

对于一棵树,最优的毛毛虫显然就是去掉叶子之后的直径,因为叶子可以变成一条链上挂的点,剩下的最长链显然就是直径。

时间复杂度 \(O(n + m)\)

#include <bits/stdc++.h>
using namespace std;
const int N = 2e3 + 7, M = 2e5 + 7;

struct Graph {
     struct Edge {
         int nxt, v;
         bool cut;
     } e[M];
     
     int head[N];
     
     int tot = 1;
     
     inline void insert(int u, int v) {
         e[++tot] = (Edge) {head[u], v, false}, head[u] = tot;
     }
} G, T;

int dfn[N], low[N], leader[N], len[N], son[N], deg[N];
bool vis[N], in[N];

int n, m, dfstime, edcc;

void Tarjan(int u, int f) {
    dfn[u] = low[u] = ++dfstime;

    for (int i = G.head[u]; i; i = G.e[i].nxt) {
        int v = G.e[i].v;

        if (!dfn[v]) {
            Tarjan(v, u), low[u] = min(low[u], low[v]);

            if (low[v] > dfn[u])
                G.e[i].cut = G.e[i ^ 1].cut = true;
        } else if (v != f)
            low[u] = min(low[u], dfn[v]);
    }
}

void dfs1(int u) {
    leader[u] = edcc;

    for (int i = G.head[u]; i; i = G.e[i].nxt)
        if (!leader[G.e[i].v] && !G.e[i].cut)
            dfs1(G.e[i].v);
}

int dfs2(int u, int fa) {
    vis[u] = true, len[u] = 1, son[u] = 0;
    int down = u;

    for (int i = T.head[u]; i; i = T.e[i].nxt) {
        int v = T.e[i].v;

        if (v == fa)
            continue;

        int x = dfs2(v, u);

        if (len[v] + 1 > len[u])
            down = x, son[u] = v, len[u] = len[v] + 1;
    }

    return down;
}

signed main() {
    scanf("%d%d", &n, &m);

    for (int i = 1; i <= m; ++i) {
        int u, v;
        scanf("%d%d", &u, &v);
        G.insert(u, v), G.insert(v, u);
    }

    for (int i = 1; i <= n; ++i)
        if (!dfn[i])
            Tarjan(i, 0);

    for (int i = 1; i <= n; ++i)
        if (!leader[i])
            ++edcc, dfs1(i);

    for (int u = 1; u <= n; ++u)
        for (int i = G.head[u]; i; i = G.e[i].nxt)
            if (G.e[i].cut)
                T.insert(leader[u], leader[G.e[i].v]), ++deg[leader[u]];

    int ans = count(deg + 1, deg + edcc + 1, 1), cnt = 0;

    for (int i = 1; i <= edcc; ++i)
        if (!vis[i]) {
            ++cnt;

            if (!deg[i]) {
                ++ans;
                continue;
            }

            int rt = dfs2(i, 0);
            dfs2(rt, 0), ans += len[rt] - 2;
        }

    printf("%d", n - ans + cnt - 1);
    return 0;
}

CF855G Harry Vs Voldemort

给出一棵树,\(q\) 次增加一条无向边的操作,每次加边后求满足 \(P(u \to w) \cap P(v \to w) = \emptyset\) 的三元组 \((u, v, w)\) 数量,其中 \(P\) 表示一条路径的边集。

\(n, q \le 10^5\)

考虑缩完边双的树,枚举 \(w\) 为根,则满足条件的 \((u, v, w)\) 必然满足 \(u\)\(v\) 不在 \(w\) 的同一子树内。确定根后一个 \(w\) 的贡献就是 \(n^2 - (n - s_w)^2 - \sum_{v \in son(w)} s_v^2\) ,其中 \(s_u\) 表示 \(u\) 的带权子树大小。

接下来考虑处理加边的情况,考虑用并查集维护边双,一个点指向每个边双深度最小的节点,每次暴力向上跳父亲合并边双,同时维护 \(s\) 和子树的 \(s^2\) 即可。

#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int N = 1e5 + 7;

struct Graph {
    vector<int> e[N];
    
    inline void insert(int u, int v) {
        e[u].emplace_back(v);
    }
} G;

ll s2[N];
int fa[N], dep[N], s1[N];

ll ans;
int n, q;

inline void calc(int x, int s, int op) {
    ans += 1ll * op * s * (s - 1) * (s - 2); // u, v \in edcc
    ans += 2ll * op * s * (s - 1) * (n - s); // u (or v) \not \in edcc
    ans += 1ll * op * s * (1ll * (n - s) * (n - s) - s2[x] - 1ll * (n - s1[x]) * (n - s1[x])); // u, v \not \in edcc
}

struct DSU {
    int fa[N], siz[N];
    
    inline void prework(int n) {
        iota(fa + 1, fa + n + 1, 1), fill(siz + 1, siz + n + 1, 1);
    }
    
    inline int find(int x) {
        while (x != fa[x])
            fa[x] = fa[fa[x]], x = fa[x];
    
        return x;
    }
    
    inline void merge(int x, int y) {
        x = find(x), y = find(y);
        calc(x, siz[x], -1), calc(y, siz[y], -1);
        siz[x] += siz[y], s2[x] += s2[y] - 1ll * s1[y] * s1[y], fa[y] = x;
        calc(x, siz[x], 1);
    }
} dsu;

void dfs(int u, int f) {
    fa[u] = f, dep[u] = dep[f] + 1, s1[u] = 1;

    for (int v : G.e[u])
        if (v != f)
            dfs(v, u), s1[u] += s1[v], s2[u] += 1ll * s1[v] * s1[v];

    calc(u, 1, 1);
}

signed main() {
    scanf("%d", &n);

    for (int i = 1; i < n; ++i) {
        int u, v;
        scanf("%d%d", &u, &v);
        G.insert(u, v), G.insert(v, u);
    }

    dfs(1, 0), dsu.prework(n);
    printf("%lld\n", ans);
    scanf("%d", &q);

    while (q--) {
        int x, y;
        scanf("%d%d", &x, &y);
        x = dsu.find(x), y = dsu.find(y);

        while (x != y) {
            if (dep[x] < dep[y])
                swap(x, y);

            dsu.merge(fa[x], x), x = dsu.find(x);
        }

        printf("%lld\n", ans);
    }

    return 0;
}

P8867 [NOIP2022] 建造军营

给出一张无向连通图,求有多少选出至少一个点组成点集 \(V' \subseteq V\) 、任意数量边组成边集 \(E' \subseteq E\) 的方案,满足对于任意 \(e \in E \setminus E'\) 满足删去 \(e\)\(V'\) 仍连通。

\(n \le 5 \times 10^5\)\(m \le 10^6\)

先将边双缩点成一棵树,则选出的点集在树上组成的极小连通块的树边必须选,剩下的边无所谓。

于是可以树形 DP,设 \(f_{u, 0/1}\) 表示 \(u\) 子树是否选点的方案数,则:

\[f_{u, 1} \gets (f_{u, 0} + f_{u, 1}) \times f_{v, 1} + 2 \times f_{u, 1} \times f_{v, 0} \\ f_{u, 0} \gets 2 \times f_{u, 0} \times f_{v, 0} \]

接下来考虑统计答案,每个点集连通块在 LCA(记为 \(u\))处统计,钦定 \(u\) 连父亲的边不能选(否则会在父亲再统计一次),则令 \(ans \gets f_{u, 1} \times 2^{m - s_e(u) - 1}\) ,其中 \(s_e(u)\) 表示 \(u\) 子树内的边数,对根特殊处理 \(ans \gets f_{r, 1}\) 即可。

#include <bits/stdc++.h>
using namespace std;
const int Mod = 1e9 + 7;
const int N = 5e5 + 7, M = 1e6 + 7;

struct Graph {
    struct Edge {
        int nxt, v;
        bool cut;
    } e[M << 1];
    
    int head[N];
    
    int tot = 1;
    
    inline void insert(int u, int v) {
        e[++tot] = (Edge){head[u], v, false}, head[u] = tot;
    }
} G, T;

int f[N][2], pw[M], dfn[N], low[N], leader[N], vsiz[N], esiz[N];

int n, m, dfstime, edcc, ans;

inline int add(int x, int y) {
    x += y;
    
    if (x >= Mod)
        x -= Mod;
    
    return x;
}

inline int dec(int x, int y) {
    x -= y;
    
    if (x < 0)
        x += Mod;
    
    return x;
}

void Tarjan(int u, int f) {
    dfn[u] = low[u] = ++dfstime;

    for (int i = G.head[u]; i; i = G.e[i].nxt) {
        int v = G.e[i].v;

        if (!dfn[v]) {
            Tarjan(v, u), low[u] = min(low[u], low[v]);

            if (low[v] > dfn[u])
                G.e[i].cut = G.e[i ^ 1].cut = true;
        } else if (v != f)
            low[u] = min(low[u], dfn[v]);
    }
}

void dfs1(int u) {
    ++vsiz[leader[u] = edcc];

    for (int i = G.head[u]; i; i = G.e[i].nxt)
        if (!G.e[i].cut && !leader[G.e[i].v])
            dfs1(G.e[i].v);
}

void dfs2(int u, int fa) {
    f[u][0] = pw[esiz[u]], f[u][1] = 1ll * dec(pw[vsiz[u]], 1) * pw[esiz[u]] % Mod;

    for (int i = T.head[u]; i; i = T.e[i].nxt) {
        int v = T.e[i].v;

        if (v == fa)
            continue;

        dfs2(v, u), esiz[u] += esiz[v] + 1;
        f[u][1] = add(1ll * add(f[u][0], f[u][1]) * f[v][1] % Mod, 2ll * f[u][1] * f[v][0] % Mod);
        f[u][0] = 2ll * f[u][0] * f[v][0] % Mod;
    }

    if (u == 1)
        ans = add(ans, f[u][1]);
    else
        ans = add(ans, 1ll * f[u][1] * pw[m - esiz[u] - 1] % Mod);
}

signed main() {
    scanf("%d%d", &n, &m);

    for (int i = 1; i <= m; ++i) {
        int u, v;
        scanf("%d%d", &u, &v);
        G.insert(u, v), G.insert(v, u);
    }

    Tarjan(1, 0);

    for (int i = 1; i <= n; ++i)
        if (!leader[i])
            ++edcc, dfs1(i);

    for (int u = 1; u <= n; ++u)
        for (int i = G.head[u]; i; i = G.e[i].nxt) {
            if (G.e[i].cut)
                T.insert(leader[u], leader[G.e[i].v]);
            else
                ++esiz[leader[u]];
        }

    for (int i = 1; i <= edcc; ++i)
        esiz[i] >>= 1;

    pw[0] = 1;

    for (int i = 1; i <= m; ++i)
        pw[i] = 2ll * pw[i - 1] % Mod;

    dfs2(1, 0);
    printf("%d", ans);
    return 0;
}

两条

给出一张无向图,\(q\) 次操作,操作有:

  • 1 k :删除第 \(k\) 条边。
  • 2 u v :查询 \(u, v\) 之间是否存在两条边不相交路径。

\(n \le 8 \times 10^5\)\(m, q \le 10^6\)

考虑时光倒流,则每次都是加入一条边。若能求出整个图的一棵生成树,则每次可以用并查集缩边双。

考虑求出一棵生成树,用并查集维护,先将始终存在的边加入,然后倒序枚举,若枚举到的边会合并两个连通块,则说明其为一条割边,删除或存在不影响答案,因此也将其加入。

#include <bits/stdc++.h>
using namespace std;
const int N = 8e5 + 7, M = 1e6 + 7;
 
struct Edge {
    int u, v;
} e[M];
 
struct Node {
    int op, x, y;
} nd[M];
 
struct Graph {
    vector<int> e[N];
    
    inline void insert(int u, int v) {
        e[u].emplace_back(v);
    }
} G;
 
struct DSU {
    int fa[N];
 
    inline void prework(int n) {
        iota(fa + 1, fa + 1 + n, 1);
    }
 
    inline int find(int x) {
        while (x != fa[x])
            fa[x] = fa[fa[x]], x = fa[x];
 
        return x;
    }
 
    inline void merge(int x, int y) {
        x = find(x), y = find(y);
 
        if (x != y)
            fa[y] = x;
    }
} dsu;
 
int fa[N], dep[N];
bool exist[M], ans[M];
 
int n, m, q, testid;
 
void dfs(int u, int f) {
    fa[u] = f, dep[u] = dep[f] + 1;
 
    for (int v : G.e[u])
        if (v != f)
            dfs(v, u);
}
 
inline void update(int x, int y) {
    x = dsu.find(x), y = dsu.find(y);

    while (x != y) {
        if (dep[x] < dep[y])
            swap(x, y);

        dsu.merge(fa[x], x), x = dsu.find(x);
    }
}
 
signed main() {
    scanf("%d%d%d%d", &n, &m, &q, &testid);
 
    for (int i = 1; i <= m; ++i)
        scanf("%d%d", &e[i].u, &e[i].v), exist[i] = true;
 
    for (int i = 1; i <= q; ++i) {
        scanf("%d", &nd[i].op);
 
        if (nd[i].op == 1)
            scanf("%d", &nd[i].x), exist[nd[i].x] = false;
        else
            scanf("%d%d", &nd[i].x, &nd[i].y);
    }
 
    dsu.prework(n);
 
    for (int i = 1; i <= m; ++i)
        if (exist[i]) {
            int fx = dsu.find(e[i].u), fy = dsu.find(e[i].v);
 
            if (fx != fy) {
                G.insert(e[i].u, e[i].v), G.insert(e[i].v, e[i].u);
                dsu.merge(fx, fy), exist[i] = false;
            }
        }
 
    for (int i = q; i; --i)
        if (nd[i].op == 1) {
            int fx = dsu.find(e[nd[i].x].u), fy = dsu.find(e[nd[i].x].v);
 
            if (fx != fy) {
                G.insert(e[nd[i].x].u, e[nd[i].x].v), G.insert(e[nd[i].x].v, e[nd[i].x].u);
                dsu.merge(fx, fy), nd[i].op = -1;
            }
        }
 
    for (int i = 1; i <= n; ++i)
        if (!dep[i])
            dfs(i, 0);
 
    dsu.prework(n);
 
    for (int i = 1; i <= m; ++i)
        if (exist[i])
            update(e[i].u, e[i].v);
 
    for (int i = q; i; --i) {
        if (nd[i].op == 1)
            update(e[nd[i].x].u, e[nd[i].x].v);
        else if (nd[i].op == 2)
            ans[i] = (dsu.find(nd[i].x) == dsu.find(nd[i].y));
    }
 
    for (int i = 1; i <= q; ++i)
        if (nd[i].op == 2)
            puts(ans[i] ? "YES" : "NO");
 
    return 0;
}

点双连通

P8435 【模板】点双连通分量

定义:

  • 点双连通图:不存在割点的无向连通图称为点双连通图。
  • 点双连通分量:极大点双连通子图。
  • 点双连通:处在同一点双连通分量内的任意两点点双连通。

点双的一些性质:

  • 点双对点没有传递性。
  • 每条边恰属于一个点双。
  • 一个点是割点当且仅当它属于多个点双。
  • 若两点双有交,那么交点一定是割点。
  • 由一条边直接相连的两个点点双连通。
  • 对于点双内的任意点 \(u\) ,存在经过 \(u\) 的简单环。
  • \(n \ge 3\) 时,在边中间插入点不影响点双连通性,因此钦定经过一个点和经过一条边是几乎等价的。
  • \(n \ge 3\) 的点双中任意点 \(u\) 与任意边 \(e\) ,存在经过 \(u, e\) 的简单环。
  • \(n \ge 3\) 的点双中任意不同两点 \(u, v\) 与任意边 \(e\) ,存在 \(u \rightsquigarrow e \rightsquigarrow v\) 的简单路径。
  • \(n \ge 3\) 的点双中任意不同三点 \(u, v, w\) ,存在 \(u \rightsquigarrow v \rightsquigarrow w\) 的简单路径。
  • 两点间任意一条路径上的所有割点,即为两点之间的所有必经点。

Tarjan 求解:每次判定 \(low_v \ge dfn_u\) 时,\(v\) 子树栈内的点与 \(u\) 共同构成一个点双,需要特判一下孤立点的情况。

#include <bits/stdc++.h>
using namespace std;
const int N = 5e5 + 7;

struct Graph {
    vector<int> e[N];
    
    inline void insert(int u, int v) {
        e[u].emplace_back(v);
    }
} G;

vector<int> vdcc[N];

int dfn[N], low[N], sta[N];

int n, m, dfstime, top, tot;

void Tarjan(int u, int f) {
    dfn[u] = low[u] = ++dfstime, sta[++top] = u;
    int sonsum = 0;

    for (int v : G.e[u]) {
        if (!dfn[v]) {
            ++sonsum, Tarjan(v, u), low[u] = min(low[u], low[v]);

            if (low[v] >= dfn[u]) {
                vdcc[++tot] = {u};

                while (sta[top] != v)
                    vdcc[tot].emplace_back(sta[top--]);

                vdcc[tot].emplace_back(sta[top--]);
            }
        } else if (v != f)
            low[u] = min(low[u], dfn[v]);
    }

    if (!f && !sonsum)
        vdcc[++tot] = {u};
}

signed main() {
    scanf("%d%d", &n, &m);

    for (int i = 1; i <= m; ++i) {
        int u, v;
        scanf("%d%d", &u, &v);
        G.insert(u, v), G.insert(v, u);
    }

    for (int i = 1; i <= n; ++i)
        if (!dfn[i])
            Tarjan(i, 0);

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

    for (int i = 1; i <= tot; ++i) {
        printf("%d ", (int)vdcc[i].size());

        for (int it : vdcc[i])
            printf("%d ", it);

        puts("");
    }

    return 0;
}

P8456 「SWTR-8」地地铁铁

给定边权为 \(0\)\(1\) 的无向连通图,求有多少点对之间存在同时经过 \(0\)\(1\) 的简单路径。

\(n \le 4 \times 10^5\)\(m \le 10^6\)

对于落在不同点双的点对,若路径上的点双包含 \(0\) 边和 \(1\) 边则合法,否则显然不合法。

对于落在相同点双的点对,如果点双内部边权相同,显然不合法,否则若存在唯二既有 \(0\) 出边又有 \(1\) 出边的点对,则该点对不合法;其他情况均合法,因为均可以走到同时存在 \(0\)\(1\) 的边的点处切换颜色。

#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int N = 4e5 + 7, M = 1e6 + 7;

struct DSU {
    int fa[N << 1], siz[N << 1];

    inline void prework(int n, int ext) {
        iota(fa + 1, fa + 1 + n + ext, 1);
        fill(siz + 1, siz + 1 + n, 1), fill(siz + 1 + n, siz + 1 + n + ext, 0);
    }

    inline int find(int x) {
        while (x != fa[x])
            fa[x] = fa[fa[x]], x = fa[x];

        return x;
    }

    inline void merge(int x, int y) {
        x = find(x), y = find(y);

        if (x == y)
            return;

        fa[y] = x, siz[x] += siz[y];
    }
} dsu1, dsu2;

struct Graph {
    struct Edge {
        int nxt, v, w;
    } e[M << 1];
    
    int head[N];
    
    int tot = 1;
    
    inline void insert(int u, int v, int w) {
        e[++tot] = (Edge) {head[u], v, w}, head[u] = tot;
    }
} G;

vector<int> vdcc[N];

int dfn[N], low[N], sta[N], esta[M], tag[N], tp[N];
bool in[M];

ll ans;
int testid, n, m, dfstime, top, etop, ext;

void Tarjan(int u) {
    dfn[u] = low[u] = ++dfstime, sta[++top] = u;

    for (int i = G.head[u]; i; i = G.e[i].nxt) {
        int v = G.e[i].v;

        if (!dfn[v]) {
            in[esta[++etop] = i >> 1] = true;
            Tarjan(v), low[u] = min(low[u], low[v]);
            
            if (low[v] >= dfn[u]) {
                vdcc[++ext].emplace_back(u);

                while (sta[top] != v)
                    vdcc[ext].emplace_back(sta[top--]);
                
                vdcc[ext].emplace_back(sta[top--]);
                vector<int> edg;

                while (esta[etop] != (i >> 1)) {
                    int x = esta[etop--];
                    in[x] = false, tp[ext] |= 1 << G.e[x << 1].w;
                    tag[G.e[x << 1].v] |= 1 << G.e[x << 1].w, tag[G.e[x << 1 | 1].v] |= 1 << G.e[x << 1].w;
                }

                int x = esta[etop--];
                in[x] = false, tp[ext] |= 1 << G.e[x << 1].w;
                tag[G.e[x << 1].v] |= 1 << G.e[x << 1].w, tag[G.e[x << 1 | 1].v] |= 1 << G.e[x << 1].w;
                int sum = 0;

                for (int x : vdcc[ext])
                    sum += (tag[x] == 3), tag[x] = 0;

                if (sum == 2)
                    --ans;
            }
        } else {
            low[u] = min(low[u], dfn[v]);

            if (dfn[v] < dfn[u] && !in[i >> 1])
                esta[++etop] = i >> 1;
        }
    }
}

signed main() {
    cin >> testid >> n >> m;

    for (int i = 1; i <= m; ++i) {
        int u, v;
        char w;
        cin >> u >> v >> w;
        G.insert(u, v, w == 'd'), G.insert(v, u, w == 'd');
    }

    ans = 1ll * n * (n - 1) / 2, Tarjan(1);
    dsu1.prework(n, ext), dsu2.prework(n, ext);

    for (int u = 1; u <= ext; ++u)
        for (int v : vdcc[u]) {
            if (tp[u] == 1)
                dsu1.merge(u + n, v);
            else if (tp[u] == 2)
                dsu2.merge(u + n, v);
        }

    for (int i = 1; i <= n + ext; ++i) {
        if (dsu1.find(i) == i)
            ans -= 1ll * dsu1.siz[i] * (dsu1.siz[i] - 1) / 2;

        if (dsu2.find(i) == i)
            ans -= 1ll * dsu2.siz[i] * (dsu2.siz[i] - 1) / 2;
    }

    printf("%lld", ans);
    return 0;
}

广义圆方树

在圆方树中,原来的每个点对应一个圆点,每一个点双对应一个方点。

对于每一个点双,它对应的方点向这个点双中的每个点连边。不难发现圆方树中每条边连接一个圆点和一个方点,每个点双形成一个菊花,多个菊花通过原图中的割点连接在一起。

圆方树的点数不超过 \(2n\) ,注意需要开两倍空间。

构建圆方树考虑 Tarjan 算法,不难发现对于一条边 \(u \to v\)\(u, v\) 在同一个点双中且 \(u\) 是该点双中深度最浅的节点当且仅当 \(low_v = dfn_u\) ,类似 Tarjan 求割点的方式构建圆方树即可。

void Tarjan(int u) {
    dfn[u] = low[u] = ++dfstime, sta[++top] = u;
    
    for (int v : G.e[u]) {
        if (!dfn[v]) {
            Tarjan(v), low[u] = min(low[u], low[v]);
            
            if (low[v] == dfn[u]) {
                T.insert(++ext, u), T.insert(u, ext);

                while (sta[top] != v)
                    T.insert(ext, sta[top]), T.insert(sta[top--], ext);

                T.insert(ext, sta[top]), T.insert(sta[top--], ext);
            }
        } else
            low[u] = min(low[u], dfn[v]);
    }
}

P4630 [APIO2018] 铁人两项

给定一张简单无向图,求有多少对三元组 \((s, c, f)\) 满足 \(s, c, f\) 互异且存在一条简单路径从 \(s\) 出发经过 \(c\) 到达 \(f\)

\(n \le 10^5\)

首先不难发现,若经过一个点双(至少两个点),则可以到达该点双中的任意点再走出来。

考虑构建圆方树,对于一对 \(s, f\) ,合法的 \(c\) 就是 \(s\)\(f\) 路径上所有圆点和方点所连的圆点,再去掉 \(s, f\)

令圆点权值为 \(-1\) (因为会被路径上相邻两个方点统计两次),方点权值为点双大小,则一对 \(s, f\) 的贡献就是树上路径权值和。不难树上换根 DP 处理,时间复杂度 \(O(n + m)\)

#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int N = 2e5 + 7;

struct Graph {
    vector<int> e[N];
    
    inline void insert(int u, int v) {
        e[u].emplace_back(v);
    }
} G, T;

int dfn[N], low[N], sta[N], siz[N];

ll ans;
int n, m, dfstime, top, ext;

int Tarjan(int u) {
    dfn[u] = low[u] = ++dfstime, sta[++top] = u;
    int siz = 1;
    
    for (int v : G.e[u]) {
        if (!dfn[v]) {
            siz += Tarjan(v), low[u] = min(low[u], low[v]);
            
            if (low[v] == dfn[u]) {
                T.insert(++ext, u), T.insert(u, ext);

                while (sta[top] != v)
                    T.insert(ext, sta[top]), T.insert(sta[top--], ext);

                T.insert(ext, sta[top]), T.insert(sta[top--], ext);
            }
        } else
            low[u] = min(low[u], dfn[v]);
    }

    return siz;
}

void dfs(int u, int f, int Siz) {
    siz[u] = (u <= n);
    ll sum = 0;

    for (int v : T.e[u])
        if (v != f)
            dfs(v, u, Siz), sum += 2ll * siz[u] * siz[v], siz[u] += siz[v];
    
    sum += 2ll * siz[u] * (Siz - siz[u]), ans += sum * (u <= n ? -1 : T.e[u].size());
}

signed main() {
    scanf("%d%d", &n, &m), ext = n;
    
    for (int i = 1; i <= m; ++i) {
        int u, v;
        scanf("%d%d", &u, &v);
        G.insert(u, v), G.insert(v, u);
    }
    
    for (int i = 1; i <= n; ++i)
        if (!dfn[i])
            dfs(i, 0, Tarjan(i));
    
    printf("%lld", ans);
    return 0;
}

CF487E Tourists

给定一张简单无向连通图,点有点权 \(w_i\) ,要求支持两种操作:

  • 修改一个点的点权。
  • 询问两点之间所有简单路径上点权的最小值。

\(n, m, q \le 10^5\)

发现一个点双对答案的贡献为点双里面的最小权值。构建圆方树,方点的权值为点双中的最小圆点权值。然后原图就变成了一棵树,询问时就可以直接树剖套线段树求路径最小值了。

一次修改一个圆点的点权,需要修改所有和它相邻的方点,这样很容易被卡到 \(O(n)\) 个修改。考虑令方点权值为儿子圆点的权值最小值,这样的话修改时只需要修改父亲方点。对于方点的维护,只需要对每个方点开一个 multiset 维护权值集合即可。

需要注意的是查询时若 LCA 是方点,则还需要查 LCA 的父亲圆点的权值。

时间复杂度 \(O(q \log^2 n)\)

#include <bits/stdc++.h>
using namespace std;
const int inf = 0x3f3f3f3f;
const int N = 2e5 + 7;

struct Graph {
    vector<int> e[N];
    
    inline void insert(int u, int v) {
        e[u].emplace_back(v);
    }
} G, T;

multiset<int> st[N];

int a[N], dfn[N], low[N], sta[N];

int n, m, q, ext, dfstime, top;

namespace TCD {
int fa[N], dep[N], siz[N], son[N], top[N], dfn[N], id[N];

int sum, dfstime;

namespace SMT {
int mn[N << 2];

inline int ls(int x) {
    return x << 1;
}

inline int rs(int x) {
    return x << 1 | 1;
}

void build(int x, int l, int r) {
    if (l == r) {
        mn[x] = a[id[l]];
        return;
    }

    int mid = (l + r) >> 1;
    build(ls(x), l, mid), build(rs(x), mid + 1, r);
    mn[x] = min(mn[ls(x)], mn[rs(x)]);
}

void update(int x, int nl, int nr, int pos, int k) {
    if (nl == nr) {
        mn[x] = k;
        return;
    }

    int mid = (nl + nr) >> 1;

    if (pos <= mid)
        update(ls(x), nl, mid, pos, k);
    else
        update(rs(x), mid + 1, nr, pos, k);

    mn[x] = min(mn[ls(x)], mn[rs(x)]);
}

int query(int x, int nl, int nr, int l, int r) {
    if (l <= nl && nr <= r)
        return mn[x];

    int mid = (nl + nr) >> 1;

    if (r <= mid)
        return query(ls(x), nl, mid, l, r);
    else if (l > mid)
        return query(rs(x), mid + 1, nr, l, r);
    else
        return min(query(ls(x), nl, mid, l, r), query(rs(x), mid + 1, nr, l, r));
}
} // namespace SMT

void dfs1(int u, int f) {
    fa[u] = f, dep[u] = dep[f] + 1, siz[u] = 1;

    for (int v : T.e[u]) {
        if (v == f)
            continue;

        dfs1(v, u), siz[u] += siz[v];

        if (siz[v] > siz[son[u]])
            son[u] = v;
    }
}

void dfs2(int u, int topf) {
    top[u] = topf, id[dfn[u] = ++dfstime] = u;

    if (son[u])
        dfs2(son[u], topf);

    for (int v : T.e[u])
        if (v != fa[u] && v != son[u])
            dfs2(v, v);
}

inline int query(int x, int y) {
    int res = inf;

    while (top[x] != top[y]) {
        if (dfn[top[x]] < dfn[top[y]])
            swap(x, y);

        res = min(res, SMT::query(1, 1, ext, dfn[top[x]], dfn[x]));
        x = fa[top[x]];
    }

    if (dfn[x] > dfn[y])
        swap(x, y);

    res = min(res, SMT::query(1, 1, ext, dfn[x], dfn[y]));

    if (x > n)
        res = min(res, a[fa[x]]);

    return res;
}
} // namespace TCD

void Tarjan(int u) {
    dfn[u] = low[u] = ++dfstime, sta[++top] = u;
    
    for (int v : G.e[u]) {
        if (!dfn[v]) {
            Tarjan(v), low[u] = min(low[u], low[v]);
            
            if (low[v] == dfn[u]) {
                T.insert(++ext, u), T.insert(u, ext);

                while (sta[top] != v)
                    T.insert(ext, sta[top]), T.insert(sta[top--], ext);

                T.insert(ext, sta[top]), T.insert(sta[top--], ext);
            }
        } else
            low[u] = min(low[u], dfn[v]);
    }
}

signed main() {
    cin >> n >> m >> q;

    for (int i = 1; i <= n; ++i)
        cin >> a[i];

    for (int i = 1; i <= m; ++i) {
        int u, v;
        cin >> u >> v;
        G.insert(u, v), G.insert(v, u);
    }

    ext = n;

    for (int i = 1; i <= n; ++i)
        if (!dfn[i])
            Tarjan(i);

    TCD::dfs1(1, 0), TCD::dfs2(1, 1);

    for (int i = 2; i <= n; ++i)
        st[TCD::fa[i]].insert(a[i]);

    for (int i = n + 1; i <= ext; ++i)
        a[i] = *st[i].begin();

    TCD::SMT::build(1, 1, ext);

    while (q--) {
        char op;
        int x, y;
        cin >> op >> x >> y;

        if (op == 'C') {
            TCD::SMT::update(1, 1, ext, TCD::dfn[x], y);

            if (x == 1) {
                a[x] = y;
                continue;
            }

            int f = TCD::fa[x];
            st[f].erase(st[f].find(a[x])), st[f].insert(a[x] = y);
            TCD::SMT::update(1, 1, ext, TCD::dfn[f], a[f] = *st[f].begin());
        } else
            cout << TCD::query(x, y) << '\n';
    }
    
    return 0;
}

P4606 [SDOI2018] 战略游戏

给定一张无向图,每次给出一个点集 \(S\) ,求有多少个点 \(u\) 满足 \(u \not \in S\) 且删掉点 \(u\)\(S\) 中的点不连通。

\(n, q \le 10^5\)

先建出圆方树,则变为询问 \(S\) 在圆方树上对应的连通子图中的不在 \(S\) 中的圆点个数。

记每个圆点和父亲方点的边权为 \(1\) ,则问题转化为 \(S\) 在圆方树上对应的极小连通子树的边权和,答案即为 \(dfn\) 相邻点距离和的一半。

注意要答案要减去 \(|S|\) ,且若子图中的深度最浅的节点是圆点,答案还要加上 \(1\)

弱化版(\(|S| = 2\)):P4320 道路相遇

#include <bits/stdc++.h>
using namespace std;
const int N = 1e6 + 7, LOGN = 21;

struct Graph {
    vector<int> e[N];
    
    inline void clear(int n) {
        for (int i = 1; i <= n; ++i)
            e[i].clear();
    }
    
    inline void insert(int u, int v) {
        e[u].emplace_back(v);
    }
} G, T;

int fa[N][LOGN], dfn[N], low[N], sta[N], dep[N], dis[N];

int n, m, q, ext, dfstime, top;

void Tarjan(int u) {
    dfn[u] = low[u] = ++dfstime, sta[++top] = u;
    
    for (int v : G.e[u]) {
        if (!dfn[v]) {
            Tarjan(v), low[u] = min(low[u], low[v]);
            
            if (low[v] == dfn[u]) {
                T.insert(++ext, u), T.insert(u, ext);

                while (sta[top] != v)
                    T.insert(ext, sta[top]), T.insert(sta[top--], ext);

                T.insert(ext, sta[top]), T.insert(sta[top--], ext);
            }
        } else
            low[u] = min(low[u], dfn[v]);
    }
}

void dfs(int u, int f) {
    fa[u][0] = f, dep[u] = dep[f] + 1, dis[u] = dis[f] + (u <= n), dfn[u] = ++dfstime;
    
    for (int i = 1; i < LOGN; ++i)
        fa[u][i] = fa[fa[u][i - 1]][i - 1];
    
    for (int v : T.e[u])
        if (v != f)
            dfs(v, u);
}

inline int LCA(int x, int y) {
    if (dep[x] < dep[y])
        swap(x, y);
    
    for (int h = dep[x] - dep[y]; h; h &= h - 1)
        x = fa[x][__builtin_ctz(h)];
    
    if (x == y)
        return x;
    
    for (int i = LOGN - 1; ~i; --i)
        if (fa[x][i] != fa[y][i])
            x = fa[x][i], y = fa[y][i];
    
    return fa[x][0];
}

inline int dist(int x, int y) {
    return dis[x] + dis[y] - dis[LCA(x, y)] * 2;
}

signed main() {
    int Task;
    scanf("%d", &Task);

    while (Task--) {
        scanf("%d%d", &n, &m), G.clear(n);
        
        for (int i = 1; i <= m; ++i) {
            int u, v;
            scanf("%d%d", &u, &v);
            G.insert(u, v), G.insert(v, u);
        }
        
        memset(dfn + 1, 0, sizeof(int) * n), memset(low + 1, 0, sizeof(int) * n);
        T.clear(n * 2), dfstime = top = 0, ext = n, Tarjan(1);
        dfstime = 0, dfs(1, 0);
        scanf("%d", &q);
        
        while (q--) {
            int k;
            scanf("%d", &k);
            vector<int> qry(k);
            
            for (int &it : qry)
                scanf("%d", &it);
            
            sort(qry.begin(), qry.end(), [](const int &x, const int &y) { return dfn[x] < dfn[y]; });
            int ans = 0;
            
            for (int i = 0; i < qry.size(); ++i)
                ans += dist(qry[i], qry[(i + 1) % qry.size()]);

            ans = ans / 2 - qry.size();
            
            if (LCA(qry.front(), qry.back()) <= n)
                ++ans;
            
            printf("%d\n", ans);
        }
    }
    
    return 0;
}

CF1763F Edge Queries

给定一张无向图,\(q\) 次询问,每次给出点对 \((s, t)\) ,求有多少条能出现在 \(s\)\(t\) 简单路径上的边满足删去后 \(s\)\(t\) 仍连通。

\(n, m, q \le 2 \times 10^5\)

构建圆方树,对于路径上的方点,若该方点表示的点双不为两点一边,则整个点双中的所有边都合法。

问题转化为树上路径求点权和,不难用树剖做到 \(O((n + q) \log n)\)

#include <bits/stdc++.h>
using namespace std;
const int N = 4e5 + 7, LOGN = 19;

struct Graph {
    vector<int> e[N];
    
    inline void insert(int u, int v) {
        e[u].emplace_back(v);
    }
} G, T;

int fa[N][LOGN], dfn[N], low[N], sta[N], sum[N], dep[N];

int n, m, q, dfstime, top, ext;

void Tarjan(int u) {
    dfn[u] = low[u] = ++dfstime, sta[++top] = u;

    for (int v : G.e[u]) {
        if (!dfn[v]) {
            Tarjan(v), low[u] = min(low[u], low[v]);

            if (low[v] == dfn[u]) {
                T.insert(++ext, u), T.insert(u, ext);

                while (sta[top] != v)
                    T.insert(ext, sta[top]), T.insert(sta[top--], ext);
                
                T.insert(ext, sta[top]), T.insert(sta[top--], ext);
            }
        } else
            low[u] = min(low[u], dfn[v]);
    }
}

void dfs1(int u, int f) {
    fa[u][0] = f, dep[u] = dep[f] + 1;

    for (int i = 1; i < LOGN; ++i)
        fa[u][i] = fa[fa[u][i - 1]][i - 1];

    for (int v : T.e[u])
        if (v != f)
            dfs1(v, u);
}

void dfs2(int u) {
    sum[u] = (sum[u] == 2 ? 0 : sum[u] / 2) + sum[fa[u][0]];

    for (int v : T.e[u])
        if (v != fa[u][0])
            dfs2(v);
}

inline int LCA(int x, int y) {
    if (dep[x] < dep[y])
        swap(x, y);

    for (int h = dep[x] - dep[y]; h; h &= h - 1)
        x = fa[x][__builtin_ctz(h)];

    if (x == y)
        return x;

    for (int i = LOGN - 1; ~i; --i)
        if (fa[x][i] != fa[y][i])
            x = fa[x][i], y = fa[y][i];

    return fa[x][0];
}

signed main() {
    scanf("%d%d", &n, &m);

    for (int i = 1; i <= m; ++i) {
        int u, v;
        scanf("%d%d", &u, &v);
        G.insert(u, v), G.insert(v, u);
    }

    ext = n, Tarjan(1), dfs1(1, 0);

    for (int u = 1; u <= n; ++u)
        for (int v : G.e[u]) {
            if (fa[fa[v][0]][0] == u)
                ++sum[fa[v][0]];
            else
                ++sum[fa[u][0]];
        }

    dfs2(1);
    scanf("%d", &q);

    while (q--) {
        int x, y;
        scanf("%d%d", &x, &y);
        int lca = LCA(x, y);
        printf("%d\n", sum[x] + sum[y] - sum[lca] - sum[fa[lca][0]]);
    }

    return 0;
}

P9167 [省选联考 2023] 城市建造

给定一张 \(n\) 个点 \(m\) 条边的无向连通图 \(G = (V, E)\),询问有多少该图的子图 \(G' = (V', E')\) 满足:

  • \(E' \ne \emptyset\)
  • \(G - E'\) 中恰有 \(|V'|\) 个连通块。
  • \(G - E'\) 中任意两个连通块大小之差不超过 \(k\)

答案对 \(998244353\) 取模。

\(3 \le n \le 10^5\)\(n - 1 \le m \le 2 \times 10^5\)\(0 \le k \le 1\)

由于 \(G - E'\) 恰有 \(|V'|\) 个连通块,故 \(V'\) 中的每个点在 \(G - E'\) 分别属于不同的连通块,且仅通过 \(E'\) 能使得 \(V'\) 连通。

对于选择的两个点,若存在一条简单路径连接它们,则路径上的每个点都要选择。进一步的,若一个点双里面选了两个点,则整个点双都要选。

建出圆方树,称删去一个方点表示选其所有邻域圆点,则删去的方点通过圆点直接构成一个连通块。

考虑第三条限制,删去选择的方点后,每个连通块大小(圆点数量)相差不超过 \(k\)

先考虑 \(k = 0\) 的情况,则连通块大小 \(d\)\(n\) 的因数。考虑枚举连通块大小 \(d\) ,统计方案数。

以带权重心 \(R\) 为根,若 \(R\) 为方点则令其相邻圆点为 \(R\) ,此时若选一个方点则祖先链上的方点都会被选择。令圆点权值为 \(1\) ,方点权值为 \(0\)\(siz_i\) 表示 \(i\) 子树内的权值和。分类讨论每个方点 \(u\) 是否被删去:

  • \(siz_u < d\) :不删 \(u\)
  • \(siz_u > d\) :删去 \(u\)
  • \(siz_u = d\) :若 \(u\) 只有一个儿子则删去 \(u\) ,否则不删 \(u\)

这是个必要条件,需要再判断一下该方案的合法性,可以在线维护 \(cnt_i\) 表示大小为 \(i\) 的连通块数量。从小到大枚举 \(d\) ,则每次都是不断合并 \(siz = d\) 的方点的领域,用并查集维护即可做到 \(O(n \alpha(n))\)

再考虑 \(k = 1\) 的情况,可以容斥,用钦定连通块大小为 \(d\)\(d + 1\) 的方案数减去连通块大小均为 \(d\) 的方案数。

对于 \(siz \ne d\) 的情况,按照 \(k = 0\) 的方法处理即可。否则考虑一个圆点所有 \(siz = d\) 的方点儿子,显然这些方点最多保留一个。先钦定删去所有 \(siz = d\) 的点,此时若一个方点的父亲圆点所在连通块大小 \(>1\) ,则其所有 \(siz = d\) 的儿子都必须删,否则只能保留任意一个。

注意在 \(d = 1\) 时一个圆点的所有 \(siz = d = 1\) 的方点儿子可以都删去,也可以保留一个,因为该圆点的父亲必然被删去,若不保留则儿子方点连通块大小都为 \(1\) ,若保留一个则该点连通块大小为 \(2\) 。因此答案要乘上其 \(siz = 1\) 的方点儿子的数量 \(+1\)

同样使用并查集维护即可做到 \(O(n \alpha(n))\)

#include <bits/stdc++.h>
using namespace std;
const int Mod = 998244353;
const int N = 2e5 + 7;

struct Graph {
    vector<int> e[N];
    
    inline void insert(int u, int v) {
        e[u].emplace_back(v);
    }
} G, T;

struct DSU {
    int fa[N], siz[N], cnt[N];
    
    inline void prework(int n) {
        iota(fa + 1, fa + n + 1, 1), fill(siz + 1, siz + n + 1, 1);
        memset(cnt + 1, 0, sizeof(int) * n), cnt[1] = n;
    }
    
    inline int find(int x) {
        while (x != fa[x])
            fa[x] = fa[fa[x]], x = fa[x];
    
        return x;
    }
    
    inline void merge(int x, int y) {
        x = find(x), y = find(y);

        if (x == y)
            return;

        --cnt[siz[x]], --cnt[siz[y]], ++cnt[siz[x] += siz[y]], fa[y] = x;
    }
} dsu;

vector<int> vec[N];

int dfn[N], low[N], sta[N], siz[N], mxsiz[N], fa[N], sum[N], ans1[N];
bool ans0[N];

int n, m, k, ext, dfstime, top, root;

inline int add(int x, int y) {
    x += y;
    
    if (x >= Mod)
        x -= Mod;
    
    return x;
}

inline int dec(int x, int y) {
    x -= y;
    
    if (x < 0)
        x += Mod;
    
    return x;
}

void Tarjan(int u) {
    dfn[u] = low[u] = ++dfstime, sta[++top] = u;

    for (int v : G.e[u]) {
        if (!dfn[v]) {
            Tarjan(v), low[u] = min(low[u], low[v]);

            if (low[v] == dfn[u]) {
                T.insert(++ext, u), T.insert(u, ext);

                while (sta[top] != v)
                    T.insert(ext, sta[top]), T.insert(sta[top--], ext);
                
                T.insert(ext, sta[top]), T.insert(sta[top--], ext);
            }
        } else
            low[u] = min(low[u], dfn[v]);
    }
}

void getroot(int u, int f, int Siz) {
    siz[u] = (u <= n), mxsiz[u] = 0;

    for (int v : T.e[u])
        if (v != f)
            getroot(v, u, Siz), siz[u] += siz[v], mxsiz[u] = max(mxsiz[u], siz[v]);

    mxsiz[u] = max(mxsiz[u], Siz - siz[u]);
    
    if (!root || mxsiz[u] < mxsiz[root])
        root = u;
}

void dfs(int u, int f) {
    fa[u] = f, siz[u] = (u <= n);

    for (int v : T.e[u])
        if (v != f)
            dfs(v, u), siz[u] += siz[v];
}

signed main() {
    scanf("%d%d%d", &n, &m, &k);

    for (int i = 1; i <= m; ++i) {
        int u, v;
        scanf("%d%d", &u, &v);
        G.insert(u, v), G.insert(v, u);
    }

    ext = n, Tarjan(1), getroot(1, 0, n);

    if (root > n)
        root = T.e[root][0];

    dfs(root, 0);

    for (int i = n + 1; i <= ext; ++i)
        vec[siz[i]].emplace_back(i);

    dsu.prework(n);

    for (int i = 1; i < n; ++i) {
        for (int u : vec[i])
            if (T.e[u].size() > 2)
                for (int j = 1; j < T.e[u].size(); ++j)
                    dsu.merge(T.e[u][0], T.e[u][j]);

        ans0[i] = (dsu.cnt[i] * i == n);
        vector<int> now;

        for (int u : vec[i]) {
            if (T.e[u].size() > 2)
                continue;

            if (dsu.siz[dsu.find(fa[u])] == 1) {
                dsu.merge(fa[u], T.e[u][T.e[u][0] == fa[u]]);
                now.emplace_back(fa[u]), sum[fa[u]] = 1;
            } else if (sum[fa[u]])
                ++sum[fa[u]];
        }

        if (dsu.cnt[i] * i + dsu.cnt[i + 1] * (i + 1) == n) {
            ans1[i] = 1;

            for (int u : now)
                ans1[i] = 1ll * ans1[i] * (sum[u] + (i == 1)) % Mod;
        }

        for (int u : now)
            sum[u] = 0;

        for (int u : vec[i])
            if (T.e[u].size() == 2)
                dsu.merge(T.e[u][0], T.e[u][1]);
    }

    if (!k)
        return printf("%d", count(ans0 + 1, ans0 + n, true)), 0;

    int ans = 0;

    for (int i = 1; i < n; ++i)
        ans = add(ans, ans1[i]);

    printf("%d", dec(ans, count(ans0 + 1, ans0 + n, true)));
    return 0;
}
posted @ 2024-07-17 10:59  wshcl  阅读(86)  评论(0)    收藏  举报