[lnsyoj2927] 连通块

题意

给出一个包含 \(n\) 个点的图,\(i\)\(j\) 之间有边的条件是 \(\gcd(a_i,a_j)\) 为合数。现要删除一个点,使得最大连通块最小。

sol

本题灵感来源于苣佬 lnlmz ,让我们拜谢他

暴力做法(\(O(n^2\log n)\))

首先暴力地把图建出来(\(O(n^2 \log n)\));
然后暴力地对于每一个点模拟删除后最大连通块(\(O(n^2)\))。
非常暴力
\(n \le 10^5\),因此 \(O(n^2)\) 的复杂度肯定是不够的。因此尝试对上述步骤进行优化。

建图

容易发现,如果几个点的点权都为某个合数的倍数,那么这几个点一定会形成一个点双连通分量。因此为了优化建图,可以借助圆方树的思想,将这些点与该合数对应的虚拟点相连,同时连通性不会改变。显然,我们希望这样的合数尽可能少,因此这些合数的质因数必须都为 \(2\),这样建图部分的时间复杂度就被优化为了 \(O(n\log\log n + n\log V)\)\(V\) 为值域)。
至于为什么还有一个 \(n \log \log n\),因为屑蒟蒻太菜了,并不会欧拉筛,只能使用埃氏筛((

求最大连通块

仔细分析,会发现最终答案只与次大连通块的结点数和最大连通块有关,其他连通块没有意义,这一部分可使用并查集处理。
然后需要求出删除一个点后,最大连通块最小的可能结果。连通图上的这种问题并不好求,但该问题在树上即为树的重心,可以 \(O(n)\) 递推得到。需要将连通图转化为树,并要保证连通性不变,因此考虑(广义)圆方树。建出广义圆方树后,将原图中点的点权设为 \(1\),虚拟点和方点的点权设为 \(0\),使用类似树上重心的做法即可解决(注意只在遍历到原图中点时才计算答案)。

Be Better?

上面的做法已经足够在 \(5000ms\) 内通过,但此做法常数略大。
仔细思考,求最大连通块时真的需要将圆方树建出来吗?方点点权为 \(0\),无需计算答案,并且只与该点双连通分量的所有点连接,因此,当需要累加方点的答案时,只需要将该方点代表的点双连通分量的所有其他点的点权求和即可。

代码

#include <iostream>
#include <algorithm>
#include <cstring>
#include <stack>

using namespace std;

const int N = 3000005, M = 1e7;

int h[N], e[N << 1], ne[N << 1], idx;
int n, cnt; 
int fa[N], d[N], ha[M + 5];
int fact[10], times[10], fcnt;
int minf[M + 5];
bool not_prime[M + 5];
int ans, tot;
int dfn[N], low[N], timestamp;
int sz[N];
stack<int> s;

void init(){
    while (!s.empty()) s.pop();
    memset(dfn, 0, sizeof dfn);
    timestamp = 0;
    memset(h, -1, sizeof h);
    memset(ha, 0, sizeof ha);
    idx = 0;
    for (int i = 1; i <= n; i ++ ) fa[i] = i, d[i] = 1;
    cnt = n;
}

void est(){
    for (int i = 2; i <= M; i ++ ) 
        if (!not_prime[i]) {
            minf[i] = i;
            for (int j = i * 2; j <= M; j += i) 
                if (!not_prime[j]) {
                    not_prime[j] = true;
                    minf[j] = i;
                }
        }
}

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

int find(int x){ 
    if (x == fa[x]) return x;
    else return fa[x] = find(fa[x]);
}

void merge(int x, int y){ 
    int fax = find(x), fay = find(y);
    if (fax == fay) return ;
    fa[fax] = fay;
    d[fay] += d[fax];
}

void tarjan(int u){
    dfn[u] = low[u] = ++ timestamp;
    s.push(u);
    sz[u] = 0;
    int maxb = 0;
    for (int i = h[u]; ~i; i = ne[i]) {
        int j = e[i];
        if (!dfn[j]) {
            tarjan(j);
            low[u] = min(low[u], low[j]);
            if (low[j] == dfn[u]) {
                int res = 0, t;
                do {
                    t = s.top();
                    s.pop();
                    res += sz[t];
                } while (t != j );
                maxb = max(maxb, res);
                sz[u] += res;
            }
        }
        else low[u] = min(low[u], dfn[j]);
    }
    if (u <= n) {
        sz[u] ++ ;
        ans = min(ans, max(maxb, tot - sz[u]));
    }
}

void solve(){ 
    scanf("%d", &n);
    init();
    for (int i = 1; i <= n; i ++ ) {
        int t;
        scanf("%d", &t);
        fcnt = 0;
        while (t > 1) {
            fact[ ++ fcnt] = minf[t];
            times[fcnt] = 0;
            while (minf[t] == fact[fcnt]) times[fcnt] ++ , t /= fact[fcnt];
        }
        for (int x = 1; x <= fcnt; x ++ )
            for (int y = x + (times[x] <= 1); y <= fcnt; y ++ ) {
                int &id = ha[fact[x] * fact[y]];
                if (!id) id = ++ cnt, fa[cnt] = cnt, d[cnt] = 0;
                add(id, i), add(i, id);
                merge(id, i);
            }
    }

    int maxn = 0, smax = -1;
    for (int i = 1; i <= n; i ++ )
        if (find(i) == i) {
            if (d[i] > d[maxn]) smax = d[maxn], maxn = i;
            else smax = max(d[i], smax);
        }
    ans = tot = d[maxn];
    tarjan(maxn);

    printf("%d\n", max(smax, ans));
}

int main(){
    est();
    int T;
    scanf("%d", &T);
    while (T -- ) solve();
    return 0;
}

蒟蒻犯的若至错误

  • 更新最大值时没有更新次大值
  • 增加新虚拟点时没有重置并查集大小
  • 发现点双连通分量时弹出至父节点
posted @ 2025-07-14 15:44  是一只小蒟蒻呀  阅读(22)  评论(0)    收藏  举报