[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;
}
蒟蒻犯的若至错误
- 更新最大值时没有更新次大值
- 增加新虚拟点时没有重置并查集大小
- 发现点双连通分量时弹出至父节点

浙公网安备 33010602011771号