操作分块
概念
有若干修改、询问组成的操作序列,考虑把这个序列分块,然后维护块内和块间的贡献。类似于根号分治,平衡两种维护方式之间的复杂度。
一般而言,在图上乱搞 有大量操作并且难以用普通数据结构维护的时候可以考虑操作分块。可以这样考虑:
-
当仅有 \(\sqrt{n}\) 次操作时,如何在 \(O(n^2)\) 内维护?
-
如何在单块 \(O(\sqrt{n})\) 内维护前面 \(O(n)\) 长度对于当前块的影响?
-
是否存在根号分治可以平衡的暴力做法?
其中复杂度根据实际情况确定,不一定是根号。
时间复杂度可能较劣。
例题
题意
给定一张 \(n\) 个点 \(m\) 条边的无向图和 \(q\) 次询问,每条边有其边权 \(w\)。现可以:
-
修改某条边的边权
-
询问从点 \(u\) 出发,只经过边权小于等于给定值 \(k\) 的边可以到达的顶点数量
\(1 \leq n \leq 5 \times 10^4, 0 \leq m \leq 10^5, 1 \leq q \leq 10^5\)
思路
首先考虑将原操作序列分块。
有一个很显然的做法:只保留边权小于等于 \(k\) 的边,此时 \(u\) 所在的连通块大小即为答案。可以用并查集维护。
考虑块内修改对块内询问的影响:
显然当前块内没有修改过的边仍然保持前若干块的边权,于是可以直接先将这些边加入并查集。预先将所有边和块内询问按边权降序排序,则可以维护指针扫一次所有边,单块复杂度为 \(O(m \log m + m)\)
对于当前块内修改过的边,显然块长取 \(\sqrt{n}\) 时至多有 \(\sqrt{n}\) 次修改和 \(\sqrt{n}\) 次排序,于是可以考虑直接对于每个询问都扫一次块内的所有修改,之后再用可撤销并查集把块内的修改撤回,单块时间复杂度 \(O(n)\)。
考虑前面若干整块对于当前块的影响:
发现直接修改对应边的边权即可,总时间复杂度 \(O(n)\)。
当块长取 \(B\) 时,总时间复杂度为 \(O(B(m \log m + m + n))\)
代码
#include <cstdio>
#include <cmath>
#include <vector>
#include <algorithm>
using namespace std;
const int maxn = 5e4 + 5;
const int maxq = 1e5 + 5;
const int maxm = 1e5 + 5;
const int blk_sz = 320;
inline void swap(int &a, int &b) { int tmp = a; a = b, b = tmp; }
inline int read()
{
int res = 0;
char ch = getchar();
while ((ch < '0') || (ch > '9')) ch = getchar();
while ((ch >= '0') && (ch <= '9'))
{
res = res * 10 + ch - '0';
ch = getchar();
}
return res;
}
inline void write(int x)
{
if (x > 9) write(x / 10);
putchar(x % 10 + '0');
}
int n, m, q;
int st[blk_sz], ed[blk_sz];
int opt[maxq], b[maxq], r[maxq], s[maxq], w[maxq], ans[maxq];
int idx[maxm];
bool vis[maxm], chk[maxm];
vector<int> mdf, qry;
struct Edge
{
int u, v, w, id;
bool operator < (const Edge& rhs) const { return (w > rhs.w); }
} edge[maxm];
struct DSU
{
int top, stk[maxn];
int fa[maxn], sz[maxn];
inline void init() { for (int i = 1; i <= n; i++) fa[i] = i, sz[i] = 1; }
inline int get(int x) { return (fa[x] == x ? x : get(fa[x])); }
inline void merge(int x, int y)
{
x = get(x), y = get(y);
if (x == y) return;
if (sz[x] > sz[y]) swap(x, y);
stk[++top] = x;
sz[y] += sz[x], fa[x] = y;
}
inline void undo(int lst)
{
int u;
while (top > lst)
{
u = stk[top]; top--;
sz[fa[u]] -= sz[u], fa[u] = u;
}
}
inline int query(int x) { return sz[get(x)]; }
} dsu;
inline bool cmp(int a, int b) { return (w[a] > w[b]); }
int main()
{
int lst, cur;
int block, len;
n = read(), m = read();
for (int i = 1; i <= m; i++)
{
edge[i].id = i;
edge[i].u = read(), edge[i].v = read(), edge[i].w = read();
}
q = read();
block = sqrt(q);
len = (q + block - 1) / block;
for (int i = 1; i <= len; i++) st[i] = (i - 1) * block + 1, ed[i] = i * block;
ed[len] = q;
for (int i = 1; i <= q; i++)
{
opt[i] = read();
if (opt[i] == 1) b[i] = read(), r[i] = read();
else s[i] = read(), w[i] = read();
}
dsu.init();
for (int i = 1; i <= len; i++)
{
lst, cur = 1;
mdf.clear(), qry.clear();
sort(edge + 1, edge + m + 1);
for (int j = 1; j <= m; j++) idx[edge[j].id] = j;
for (int j = st[i]; j <= ed[i]; j++)
{
if (opt[j] == 1)
{
vis[b[j]] = true;
mdf.push_back(j);
}
else qry.push_back(j);
}
sort(qry.begin(), qry.end(), cmp);
reverse(mdf.begin(), mdf.end());
for (int j : qry)
{
while ((cur <= m) && (edge[cur].w >= w[j])) { if (!vis[edge[cur].id]) dsu.merge(edge[cur].u, edge[cur].v); cur++; }
lst = dsu.top;
for (int k : mdf)
{
if ((k < j) && (!chk[b[k]]))
{
if (r[k] >= w[j]) dsu.merge(edge[idx[b[k]]].u, edge[idx[b[k]]].v);
chk[b[k]] = true;
}
}
for (int k : mdf) if ((k >= j) && (!chk[b[k]]) && (edge[idx[b[k]]].w >= w[j])) dsu.merge(edge[idx[b[k]]].u, edge[idx[b[k]]].v);
ans[j] = dsu.query(s[j]);
for (int k : mdf) chk[b[k]] = false;
dsu.undo(lst);
}
dsu.undo(0);
for (int j = st[i]; j <= ed[i]; j++)
if (opt[j] == 1)
{
vis[b[j]] = false;
edge[idx[b[j]]].w = r[j];
}
}
for (int i = 1; i <= q; i++)
if (opt[i] == 2) write(ans[i]), putchar('\n');
return 0;
}