📅  最后修改于: 2023-12-03 15:05:21.221000             🧑  作者: Mango
Sqrt(或平方根)分解是一种优化算法,它可以将一个原本复杂的操作转化为由多个简单操作组成的序列。具体地说,它就是在一个序列中,每个长度为 $\sqrt{n}(n$为序列长度$)$ 的连续子序列内,单独维护这个子序列中元素所需要的信息,从而实现一些常规数据结构的操作。
集合2问题是指在一棵n个节点的树上,m个询问,每个询问给出u和v,要求查询u和v的最近公共祖先(LCA)。暴力的做法是预处理树的深度,然后利用LCA的性质不断上升节点,直到两条链相交。这个暴力的做法的时间复杂度是O(mn)。
其中一种优化方法是Tarjan算法,可以将时间复杂度降到O(m+n)。具体实现可以见这里。
而Sqrt(或平方根)分解是另一种优化方法,时间复杂度为O(m sqrt{n})。具体实现如下:
先找到根节点并将它作为0号节点,做一遍$dfs$标记每个节点在时间戳中第一次出现的位置,然后将时间戳分成$\sqrt h$个段,记录每个段的最小深度(因为深度越小,一个节点越靠近根节点),并且记录每个段内所有节点的编号。这个步骤可以在$O(n)$的时间内完成。
对于每个询问(u, v),可以将(u, v)的路径分成$O(\sqrt h)$个连续段,对于每个阶段直接暴力扫描得到其中的最小深度对应的节点和深度,最后通过比较把整条路径的最小深度对应的节点找到即可。具体实现可以见以下代码:
int n, m, head[N], to[N << 1], nxt[N << 1], idx, dfs_cnt, dep[N], dfn[N], mn[(int)sqrt(N) + 5], mn_id[(int)sqrt(N) + 5], sz;
int st[(int)sqrt(N) + 5], ed[(int)sqrt(N) + 5], fa[N][17];
bool vis[N];
void add(int u, int v) {
to[++idx] = v, nxt[idx] = head[u], head[u] = idx;
}
void dfs(int u, int f) {
vis[u] = true, dfn[u] = ++dfs_cnt, dep[u] = dep[f] + 1, fa[u][0] = f;
for (int i = 1; i < 17; ++i) fa[u][i] = fa[fa[u][i - 1]][i - 1];
for (int i = head[u], v = to[i]; i; i = nxt[i], v = to[i]) {
if (v == f) continue;
dfs(v, u);
}
}
int LCA(int u, int v) {
if (dep[u] < dep[v]) swap(u, v);
for (int i = 16; i >= 0; --i) if (dep[fa[u][i]] >= dep[v]) u = fa[u][i];
if (u == v) return u;
for (int i = 16; i >= 0; --i) if (fa[u][i] != fa[v][i]) u = fa[u][i], v = fa[v][i];
return fa[u][0];
}
int main() {
scanf("%d %d", &n, &m);
for (int i = 1; i < n; ++i) {
int u, v;
scanf("%d %d", &u, &v);
add(u, v), add(v, u);
}
dfs(0, 0);
int B = sqrt(dfs_cnt);
sz = dfs_cnt / B;
for (int i = 1; i <= sz; ++i) {
st[i] = ed[i - 1] + 1;
ed[i] = st[i] + B - 1;
for (int j = st[i]; j <= ed[i]; ++j) {
mn[i] = mn_id[i] = dfn[st[i]];
if (j > dfs_cnt) break;
if (dep[st[i]] > dep[j]) mn[i] = dfn[j], mn_id[i] = j;
}
}
ed[sz] = dfs_cnt;
for (int i = 1; i <= m; ++i) {
int u, v;
scanf("%d %d", &u, &v);
int lca = LCA(u, v);
int pos1 = u, pos2 = v;
while (pos1 != lca) {
if (vis[pos1]) {
int block_id = (dfn[pos1] - 1) / B + 1;
for (int j = st[block_id]; j <= min(ed[block_id], dfn[pos1]); ++j) {
if (dep[j] < dep[mn_id[block_id]]) mn_id[block_id] = j;
}
} else {
if (dep[pos1] < dep[mn_id[(dfn[lca] - 1) / B + 1]]) mn_id[(dfn[lca] - 1) / B + 1] = pos1;
}
pos1 = fa[pos1][0];
}
while (pos2 != lca) {
if (vis[pos2]) {
int block_id = (dfn[pos2] - 1) / B + 1;
for (int j = st[block_id]; j <= min(ed[block_id], dfn[pos2]); ++j) {
if (dep[j] < dep[mn_id[block_id]]) mn_id[block_id] = j;
}
}
else {
if (dep[pos2] < dep[mn_id[(dfn[lca] - 1) / B + 1]]) mn_id[(dfn[lca] - 1) / B + 1] = pos2;
}
pos2 = fa[pos2][0];
}
int ans = mn[(dfn[lca] - 1) / B + 1];
for (int j = (dfn[lca] - 1) / B + 1; j <= sz; ++j) if (dep[mn_id[j]] < dep[ans]) ans = mn_id[j];
printf("%d\n", ans);
}
return 0;
}
Sqrt(或平方根)分解能够将一个复杂的操作优化成简单的操作的序列,其中应用的最多的就是预处理。在集合2问题中,通过对树进行分区,对于每次询问也快速定位它所处的分区,从而达到了较快的时间复杂度。