Loading

广义串并联图

定义

不存在同胚于 \(K_4\) 的子图的无向图被称为广义串并联图。

通俗地说,就是不存在四个结点,满足其两两之间都有一条路径相连,且这些路径互不相交。

举例:树与仙人掌都是广义串并联图。

性质

  1. 广义串并联图是平面图;
  2. 广义串并联图可以通过以下的操作变成一个单点:
    • 删一度点(去断路):选择一个度数为 \(1\) 的点并删掉它和与它连接的边;
    • 缩二度点(消串联):选择一个度数为 \(2\) 的点,用一条边替换它和与它相连的两条边;
    • 叠合重边(消并联):选择两条重边,用一条边替换它们;
  3. 广义串并联图在进行上述操作中,每一条边都对应原图的一个子图。
  4. 上面的三种操作在一般图中的体现称为“广义串并联图方法”。对于一般图 \(G=(V,E)\),令 \(n=|V|\)\(m=|E|\),如果满足 \(m−n\le k\),其中 \(k\) 很小。运用前两种操作(当然三种操作都用也可以)对图进行操作可以使原图满足 \(n\le 2k\)\(m\le 3k\),然后一些题可以直接暴力做。证明考虑三种操作中 \(m−n\) 的值都不增,操作完后满足每个点的度数都不小于 \(3\),所以 \(3n\le 2m\),于是 \(n\le 2k\)\(m\le 3k\)

P6790 [SNOI2020] 生成树

P6790 [SNOI2020] 生成树

在仙人掌上,任取 \(4\) 个不同的点,两两之间最多有 \(4\) 条路径(在环上才能有 \(4\) 条),再加上一条也只有 \(5\) 条,少于 \(6\) 条。

所以原图是广义串并联图。

将贡献缩到边上,设 \(f_e\) 表示 \(e\) 代表的子图中,使得 \(e\) 的两个端点连通的生成树个数。

\(g_e\) 表示 \(e\) 代表的子图中,使得 \(e\) 的两个端点不连通的生成森林个数。

删一度点

设当前点为 \(u\),唯一的连边为 \((u,v)\)

只能通过边 \((u,v)\) 的子图使得 \((u,v)\) 联通,所以将答案乘上 \(f_{(u,v)}\)

最后删掉点 \(u\) 和边 \((u,v)\)

缩二度点

设当前点为 \(u\),两条连边分别为 \((u,x)\)\((u,y)\)

若要让 \(x,y\) 连通,则 \(u,x\)\(u,y\) 均要连通,所以有:

\[f_{(x,y)}=f_{(x,u)}\cdot f_{(u,y)} \]

如果 \(x,y\) 不连通,显然只会分成两个连通块,且 \(x,y\) 不属于同一个连通块。如果有三个连通块,最后一定无法形成连通的生成树。

所以需要满足 \(u,x\)\(u,y\) 恰好一组连通,所以有:

\[g_{(x,y)}=f_{(x,u)}\cdot g_{(u,y)}+g_{(x,u)}\cdot f_{(u,y)} \]

最后删掉点 \(u\) 和其连边,添加 \((u,v)\) 这一条边。

叠合重边

考虑两条边 \(e_1,e_2\),两条边的两个端点为 \(u,v\),设缩完后的边为 \(e_3\)

如果 \(u,v\) 不连通,则 \(e_1,e_2\) 两条边对应的子图都要不连通,所以有:

\[g_{e_3}=g_{e_1}\cdot g_{e_2} \]

如果 \(u,v\) 连通,则 \(e_1,e_2\) 两条边对应的子图恰好有一个连通,如果两个都联通肯定就不会是生成树了,所以有:

\[f_{e_3}=f_{e_1}\cdot g_{e_2}+g_{e_1}\cdot f_{e_2} \]

最后将 \(e_1,e_2\) 替换成 \(e_3\)

写法

最开始图中可能会出现重边,用“叠合重边”缩掉就行。

我们考虑用队列维护度数不超过 \(2\) 的点,如果是一度点就直接“删一度点”,如果是二度点就先“缩二度点”,再判断是否可以“叠合重边”。

维护 \(f,g\) 可以用 std::map 维护。

容易发现这样的做法是有正确性的,一定可以将广义串并联图缩成一个点。

代码

#include <bits/stdc++.h>

using namespace std;

const int MaxN = 2e5 + 5, Mod = 998244353;

int n, m, ans = 1;
map<int, array<int, 2>> mp[MaxN];
queue<int> q;

void Add(int u, int v, array<int, 2> f) {
    if (mp[u].count(v)) {
        auto g = mp[u][v];
        g[1] = (1LL * f[1] * g[0] + 1LL * f[0] * g[1]) % Mod;
        g[0] = 1LL * f[0] * g[0] % Mod;
        mp[u][v] = mp[v][u] = g;
    } else {
        mp[u][v] = mp[v][u] = f;
    }
}

int main() {
    ios::sync_with_stdio(0), cin.tie(0);
    cin >> n >> m;
    for (int i = 1, u, v; i <= m; i++) {
        cin >> u >> v;
        Add(u, v, {1, 1});
    }
    for (int i = 1; i <= n; i++) {
        if (mp[i].size() <= 2) q.push(i);
    }
    for (int u; q.size(); q.pop()) {
        u = q.front();
        if (mp[u].size() == 1) {
            ans = 1LL * ans * (mp[u].begin() -> second)[1] % Mod;
        }
        if (mp[u].size() == 2) {
            auto [x, f] = *mp[u].begin();
            auto [y, g] = *mp[u].rbegin();
            array<int, 2> tmp;
            tmp[0] = (1LL * f[1] * g[0] + 1LL * f[0] * g[1]) % Mod;
            tmp[1] = 1LL * f[1] * g[1] % Mod;
            Add(x, y, tmp);
        }
        for (auto [v, f] : mp[u]) {
            mp[v].erase(u);
            if (mp[v].size() <= 2) q.push(v);
        }
        mp[u].clear();
    }
    cout << ans;
    return 0;
}

qoj856 Cactus

qo856 Cactus

观察到仙人掌是广义串并联图,考虑使用广义串并联图方法做。

\(f_e\) 表示 \(e\) 代表的子图中满足条件,已经确定两个端点的颜色,且两个端点颜色相同的方案数。

\(g_e\) 表示 \(e\) 代表的子图中满足条件,已经确定两个端点的颜色,且两个端点颜色不同的方案数。

下面只讲转移,边的变化跟上一道题一摸一样。

删一度点

将答案乘上 \(f+(k-1)g\)

这里的答案表示确定其中一个点的颜色后方案数是多少,所以最后答案还要乘以 \(k\) 才能输出。

缩二度点

设两条边分别为 \((u,v),(v,w)\),分别记为 \(e_1,e_2\),缩完后的边记为 \(e_3\)

如果 \(u,w\) 颜色相同,要么 \(u,v,w\) 颜色均相同,或者 \(u,v\) 不相同且 \(v,w\) 不相同,所以:

\[f_{e_3}=f_{e_1}\cdot f_{e_2}+(k-1)\cdot g_{e_1}\cdot g_{e_2} \]

如果 \(u,w\) 颜色不同,要么 \(u,v\) 颜色相同且 \(v,w\) 颜色不同(对称过去是另一种情况),或者 \(u,v\) 颜色不同且 \(v,w\) 颜色不同,所以:

\[f_{e_3}=f_{e_1}\cdot g_{e_2}+g_{e_1}\cdot f_{e_2}+(k-2)\cdot g_{e_1}\cdot g_{e_2} \]

叠和重边

\(f,g\) 分别相乘就行了。

代码

#include <bits/stdc++.h>

using namespace std;

const int MaxN = 3e5 + 5, Mod = 1e9 + 7;

int T, n, m, k, ans = 1;
map<int, array<int, 2>> mp[MaxN];
queue<int> q;

void Add(int u, int v, array<int, 2> f) {
    if (mp[u].count(v)) {
        auto g = mp[u][v];
        g[0] = 1LL * f[0] * g[0] % Mod;
        g[1] = 1LL * f[1] * g[1] % Mod;
        mp[u][v] = mp[v][u] = g;
    } else {
        mp[u][v] = mp[v][u] = f;
    }
}

void Clear() {
    ans = 1;
}

int main() {
    ios::sync_with_stdio(0), cin.tie(0);
    for (cin >> T; T; T--, Clear()) {
        cin >> n >> m >> k;
        for (int i = 1, u, v; i <= m; i++) {
            cin >> u >> v, Add(u, v, {0, 1});
        }
        for (int i = 1; i <= n; i++) {
            if (mp[i].size() <= 2) q.push(i);
        }
        for (int u; q.size(); q.pop()) {
            u = q.front();
            if (mp[u].size() == 1) {
                auto [v, f] = *mp[u].begin();
                ans = 1LL * ans * (f[0] + 1LL * f[1] * (k - 1) % Mod) % Mod;
            }
            if (mp[u].size() == 2) {
                auto [x, f] = *mp[u].begin();
                auto [y, g] = *mp[u].rbegin();
                array<int, 2> tmp;
                tmp[0] = (1LL * f[0] * g[0] + 1LL * f[1] * g[1] % Mod * (k - 1)) % Mod;
                tmp[1] = (1LL * f[0] * g[1] + 1LL * f[1] * g[0] + 1LL * f[1] * g[1] % Mod * (k - 2)) % Mod;
                Add(x, y, tmp);
            }
            for (auto [v, f] : mp[u]) {
                mp[v].erase(u);
                if (mp[v].size() <= 2) q.push(v);
            }
            mp[u].clear();
        }
        cout << 1LL * ans * k % Mod << '\n';
    }
    return 0;
}

AT_abc419_g [ABC419G] Count Simple Paths 2

AT_abc419_g [ABC419G] Count Simple Paths 2

注意到暴搜时间复杂度是 \(O(n2^k)\),所以考虑缩小点数,可以用广义串并联图的前两个操作将原图变成点数为 \(O(k)\) 的图,最后暴搜就可以得到答案,时间复杂度 \(O(k2^k)\)

参考资料

杂题选讲 #2:广义串并联图 - FFTotoro

posted @ 2026-08-02 15:48  liruixiong0101  阅读(9)  评论(0)    收藏  举报