CF292D Connected Components 解题报告
1 题目链接
2 题目大意
题目 : 连接组件
题目大意 :
给定一个有 n n n个点 m m m条边的无向图。有 k k k次询问,每次要求断开连接着 l i l_i li与 r i r_i ri的边,并输出此时剩下的连通块数量(询问后的断开会恢复)。
3 解法分析
看到题时的内心OS : 这是搜索?搜索能做?我是掉并查集坑里了吗
这道题有个极其重要的点:
n
∈
[
2
,
500
]
n \in [2,500]
n∈[2,500]
m
∈
[
1
,
1
0
4
]
m \in [1,10^4]
m∈[1,104]
k
∈
[
1
,
2
×
1
0
4
]
k \in [1,2 \times 10^4]
k∈[1,2×104]
跟据我多年来的经验,
O
(
n
m
k
)
O(nmk)
O(nmk)的复杂度是可以卡过去的 (在座的大佬别学本蒟蒻) ,所以说
d
f
s
dfs
dfs完全是可以水过去的。
但是,我作为一个垃圾
O
i
e
r
Oier
Oier,显然是不满足于这个时间复杂度(主要是怕
H
a
c
k
Hack
Hack),通过对上一题的回忆,这道题还是可以尝试使用并查集这一数据结构来优化时间复杂度,可以直接优化至
O
(
m
k
log
n
)
O(mk\log{n})
O(mklogn)。
然而,这个时间复杂度想过
1
e
4
1e4
1e4级别的数据还是有些许困难,所以我们可以尝试优化并查集。
本蒟蒻在刚开始看这道题时,以为断开了就断开了(都是英语的锅),甚至为此WA
了一次。本蒟蒻的第一次提交时不知是怎么想的,竟用两个线段树分别维护前、后缀和,不过仔细想想还是有一定道理的 : 就相当于直接确定了
l
i
l_i
li前
r
i
r_i
ri后的两边情况。
通过刚才的方法,我们可以引申出一个复杂度更优的方法 : 用前后缀和来维护,每次询问时只需要将
[
1
,
l
i
]
[1,l_i]
[1,li]和
[
r
i
+
1
,
m
]
[r_i+1,m]
[ri+1,m]区间连接计算连通块个数即可。 其中时间复杂度为
O
(
n
k
log
n
)
O(nk\log{n})
O(nklogn)。
简要说明可行性 : 每个区间只是在每次询问后不计这个区间的边,也就相当于每次询问都是相互独立的,即任何一段区间的连通块个数和连边情况都是不会变的,因此这个方法完全可行。
但是凭借多年翻阅
c
s
d
n
csdn
csdn上奇妙算法的成果,这题还有一种不为人知(至少我没翻到)的做法 : 莫队算法中的回滚莫队。(本蒟蒻刚看完莫涛神犇的知乎)
但是,这道题又不是正宗的回滚莫队,因为并查集不支持删除,所以我们需要作一点变通:我们让右指针
r
r
r往回移而不是从块的右边往右移,而且如果一个询问在块内,我们不需要单独暴力处理,直接一起处理。
4 解法总结
4.1 d f s dfs dfs
O ( n m k ) O(nmk) O(nmk)暴搜即可。
4.2 不带优化的并查集
O ( m k log n ) O(mk\log{n}) O(mklogn)暴力即可。注意这里需要反向加边。
4.3 前、后缀和优化并查集
O ( n k log n ) O(nk\log{n}) O(nklogn)的时间复杂度很轻松就可以过。(这种舍空间求时间的方法何乐不为?)
4.4 回滚莫队优化并查集
时间复杂度貌似是
O
(
max
(
n
m
5
3
,
k
)
)
O(\max (nm^{\frac{5}{3}},k))
O(max(nm35,k))(没错我自己也不知道为什么没有
k
k
k)。
补充说明 : 而且在块与块的转移之间,假设当前刚刚做完第
i
i
i块,那么我们就需要将
[
1
,
(
i
−
1
)
×
b
l
o
c
k
+
1
]
[1,(i-1)\times block+1]
[1,(i−1)×block+1]里面的所有连边操作完成。因为
n
⩽
500
n\leqslant500
n⩽500,所以不会
T
L
E
TLE
TLE。
5 AC Code
Dalao代码 #001
// From aarr
// Rating 2425
#include <iostream>
#include <vector>
using namespace std;
const int N = 505;
int l, r, nparts;
vector <pair <int, int> > adj[N];
bool mark[N];
void dfs(int v) {
mark[v] = true;
for (auto p : adj[v]) {
// int u = adj[v][i].first, x = adj[v][i].second;
if (!(l <= p.second && p.second <= r) && !mark[p.first])
dfs(p.first);
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int n, m;
cin >> n >> m;
for (int i = 1; i <= m; i++) {
int u, v;
cin >> u >> v;
adj[u].push_back({v, i});
adj[v].push_back({u, i});
}
int k;
cin >> k;
for (int i = 0; i < k; i++) {
cin >> l;
cin >> r;
nparts = 0;
for (int j = 1; j <= n; j++) {
if (!mark[j]) {
dfs(j);
nparts++;
}
}
for (int j = 1; j <= n; j++) {
mark[j] = false;
}
cout << nparts << "\n";
}
return 0;
}//纯dfs做法
Dalao代码 #002
// From Heart_Blue
// Rating 2425
#include <cstdlib>
#include <cctype>
#include <cstring>
#include <cstdio>
#include <cmath>
#include <algorithm>
#include <vector>
#include <string>
#include <iostream>
#include <sstream>
#include <map>
#include <set>
#include <queue>
#include <stack>
#include <fstream>
#include <numeric>
#include <iomanip>
#include <bitset>
#include <list>
#include <stdexcept>
#include <functional>
#include <utility>
#include <ctime>
using namespace std;
typedef long long LL;
typedef unsigned long long ULL;
#define MEM(a,b) memset((a),(b),sizeof(a))
const LL INF = 1e9 + 7;
const int N = 2e5 + 10;
pair<int, int> p[N];
class UnionFind//xs NOIP根本用不了
{
int p[N];
public:
void init(int n = N)
{
memset(p, -1, sizeof(int)*n);//指针一向学得不好的本蒟蒻看不懂 盲猜就是单纯地赋值
}
int Find(int x)
{
int s = x;
while (p[s] >= 0) s = p[s];
while (x != s)
{
int t = p[x];
p[x] = s;
x = t;
}
return s;
}
void Union(int x, int y)
{
int px = Find(x);
int py = Find(y);
if (p[px] > p[py]) swap(px, py);
p[px] += p[py];
p[py] = px;
}
} uf;
int res[N];
vector<pair<int, int>> vp[N];
int main()
{
//freopen("input.txt", "r", stdin);
//freopen("output.txt", "w", stdout);
int n, m;
scanf("%d%d", &n, &m);
int lo = n;
uf.init(n + 1);
for (int i = 1; i <= m; i++)
{
scanf("%d%d", &p[i].first, &p[i].second);
if (uf.Find(p[i].first) != uf.Find(p[i].second))
uf.Union(p[i].first, p[i].second), lo--;
}
int q;
scanf("%d", &q);
for(int i =1; i <= q; i++)
{
int l, r;
scanf("%d%d", &l, &r);
vp[l].push_back({ r,i });
}
for (int i = 1; i <= m; i++)
{
if (vp[i].empty()) continue;
uf.init(n + 1);
sort(vp[i].begin(), vp[i].end());
int ans = n;
for (int j = 1; j < i; j++)
{
int x, y;
tie(x, y) = p[j];
if (uf.Find(x) != uf.Find(y))
uf.Union(x, y), ans--;
}
int cur = m;
while (!vp[i].empty())
{
int r, pos;
tie(r, pos) = vp[i].back();
vp[i].pop_back();
while (cur > r && ans != lo)
{
if (uf.Find(p[cur].first) != uf.Find(p[cur].second))
uf.Union(p[cur].first, p[cur].second), ans--;
cur--;
}
res[pos] = ans;
}
}
for (int i = 1; i <= q; i++) printf("%d\n", res[i]);
return 0;
}//并查集(无优化
蒟蒻代码 #001
#include <bits/stdc++.h>
using namespace std;
bool vis[507];
int n, m, l, r, q;
vector <pair <int, int> > v[507];
void dfs(int x) {
vis[x] = 1;
for (register int i = 0; i < v[x].size(); ++i) {
if (v[x][i].second >= l && v[x][i].second <= r)
continue;
if (vis[v[x][i].first])
continue;
dfs(v[x][i].first);
}
}
int main() {
scanf("%d%d", &n, &m);
for (register int i = 0, a, b; i < m; ++i) {
scanf("%d%d", &a, &b);
v[--a].push_back(make_pair(--b, i));
v[b].push_back(make_pair(a, i));
}
scanf("%d", &q);
while (q--) {
register int ans = 0;
scanf("%d%d", &l, &r);
--l;
--r;
for (register int i = 0; i < n; ++i)
vis[i] = 0;
for (register int i = 0; i < n; ++i)
if (!vis[i]) {
++ans;
dfs(i);
}
printf("%d\n", ans);
}
return 0;
}//纯dfs做法
蒟蒻代码 #002
#include <bits/stdc++.h>
#define inf 0x3f3f3f3f
#define M 10007
#define N 507
using namespace std;
int n, m;
int x[M], y[M];
struct node {
int f[N], cnt;
node() {
for (register int i = 1; i < N; ++i)
f[i] = i;
}
int find(int x) {
return f[x] == x ? x : f[x] = find(f[x]);
}
void merge(int x, int y) {
x = find(x);
y = find(y);
if (x != y)
f[x] = y, ++cnt;
}
} pre[M], suf[M];
int main() {
scanf("%d%d", &n, &m);
for (register int i = 1; i <= m; ++i) {
scanf("%d%d", &x[i], &y[i]);
pre[i] = pre[i - 1];
pre[i].merge(x[i], y[i]);
}
for (register int i = m; i; --i) {
suf[i] = suf[i + 1];
suf[i].merge(x[i], y[i]);
}
register int q;
scanf("%d", &q);
while (q--) {
register int l, r;
scanf("%d%d", &l, &r);
register node dsu = pre[l - 1];
for (register int i = 1; i <= n; ++i)
if (suf[r + 1].f[i])
dsu.merge(i, suf[r + 1].f[i]);
printf("%d\n", n - dsu.cnt);
}
return 0;
}//前、后缀和优化并查集
蒟蒻代码 #003
#include <bits/stdc++.h>
#define rep(i, a, b) for (register int (i) = (a); (i) <= (b); ++(i))
#define K 20007
#define M 10007
#define N 507
using namespace std;
int n, m, k;
int ans[K], fa2[N], fa3[N];
int total, block, bnum;
int x[M], y[M], ys[M];
struct node {
int l, r, id;
} q[K];
inline int read() {
register int sum = 0, fh = 1;
register char ch = getchar();
while (ch < '0' || ch > '9') {
if (ch == '-')
fh = -1;
ch = getchar();
}
while (ch >= '0' && ch <= '9') {
sum = (sum << 3) + (sum << 1) + (ch ^ 48);
ch = getchar();
}
return sum * fh;
}
bool cmp(const node &fir, const node &sec) {
if (ys[fir.l] ^ ys[sec.l])
return ys[fir.l] < ys[sec.l];
return fir.r > sec.r;
}
int gffa3(int x) {
return (fa3[x] == x) ? x : fa3[x] = gffa3(fa3[x]);
}
void hbfa3(int r) {
if (gffa3(x[r]) != gffa3(y[r]))
fa3[fa3[x[r]]] = fa3[y[r]];
}
int main() {
n = read(), m = read();
block = ceil(pow(m, 2.0 / 3.0));
bnum = ceil((double)m / block);
rep(i, 1, m)
x[i] = read(), y[i] = read();
rep(i, 1, m)
ys[i] = (i - 1) / block + 1;
k = read();
rep(i, 1, k) {
q[i].l = read(), q[i].r = read();
q[i].id = i;
}
rep(i, 1, n)
fa3[i] = i;
sort(q + 1, q + k + 1, cmp);
register int j = 1;
rep(i, 1, bnum) {
register int l = (i - 1) * block + 1, r = m;
while (j <= k) {
if (ys[q[j].l] > i)
break;
while (r > q[j].r)
hbfa3(r--);
rep(p, 1, n)
fa2[p] = fa3[p];//复制一份
while (l < q[j].l)
hbfa3(l++);
register int total = 0;
rep(p, 1, n)
if (gffa3(p) == p)
++total;
ans[q[j].id] += total;
l = (i - 1) * block + 1;
rep(p, 1, n)
fa3[p] = fa2[p];//回复数组
++j;
}
rep(p, 1, n)
fa3[p] = p;
rep(p, 1, min(m, i * block))
hbfa3(p);
}
rep(i, 1, k)
printf("%d\n", ans[i]);
return 0;
}//回滚莫队优化并查集
6 总结
这道题可以用的方法相当多啊 : 搜索、数据结构、算法优化……(应该还是有只不过本蒟蒻没有想到)
相关的资料真的是好多(光是莫队我就打开了4、5个网页)。
天知道这篇题解我用了多久。。五个小时(确信)。
最后要感谢大佬@yh2021shx 愿意与本蒟蒻讨论并查集与前、后缀和优化的时间复杂度及其相关问题。