📜  Sqrt(或平方根)分解|集合2(O(sqrt(height))时间中树的LCA)(1)

📅  最后修改于: 2023-12-03 15:05:21.221000             🧑  作者: Mango

Sqrt(或平方根)分解与集合2

什么是Sqrt(或平方根)分解?

Sqrt(或平方根)分解是一种优化算法,它可以将一个原本复杂的操作转化为由多个简单操作组成的序列。具体地说,它就是在一个序列中,每个长度为 $\sqrt{n}(n$为序列长度$)$ 的连续子序列内,单独维护这个子序列中元素所需要的信息,从而实现一些常规数据结构的操作。

Sqrt(或平方根)分解在集合2中的应用

集合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问题中,通过对树进行分区,对于每次询问也快速定位它所处的分区,从而达到了较快的时间复杂度。