[图论入门]点分治

#1.0 点分治

#1.1 简单介绍

点分治 是一种解决树上统计问题的常用方法,本质思想就是选择一点作为分治中心,将原问题划分为几个相同的子树上的问题,进行递归解决。

常见题目中给出的树都是无根树(所需维护的信息与根节点是谁无关)。

常见的用于统计树上有关路径的信息。假设当前选定根节点为 \(rt\),则符合条件的路径必然是以下两种之一:

  • 经过 \(rt\) 或一端在 \(rt\) 上;
  • 不经过 \(rt\),在 \(rt\) 的子树上。

点分治仅仅是一种思想、方法,并没有固定的信息维护、转移方式。

#1.2 树的重心

注意到,点分治是要将问题递归解决的,那么树根的选择就很重要,假如我们的树是一条链,每次递归都会选择链的一端,那么递归层数就达到了 \(O(n)\) 的级别,那么怎么选择根节点呢?

答案是选择当前树的重心作为根节点。

对于树上的每一个点,计算其所有子树中最大的子树节点数,这个值最小的点就是这棵树的重心。而不难证明树的重心具有以下性质:

  • 以树的重心为根时,所有子树的大小都不超过整棵树大小的一半。

假如当前点 \(x\) 某个子树的大小超过整棵树的一半,那么选择 \(x\) 的最大子树的根 \(y\) 作为整棵树的根,显然会让最大子树的大小变小,那么点 \(y\) 比之前的 \(x\) 更适合是树的重心。

上面那条性质也就保证了我们的递归层数为 \(O(\log n).\)

#1.3 算法实现

#1.3.1 寻找重心

按照定义寻找即可。

void calcsiz(int x, int fa) {
    siz[x] = 1, mx[x] = 0;//初始化
    for (int i = head[x]; i; i = e[i].nxt)
      if (e[i].v != fa && !vis[e[i].v]) {
          /*vis[] 具体作用下面会讲*/
          calcsiz(e[i].v, x);
          siz[x] += siz[e[i].v];
          mx[x] = Max(mx[x], siz[e[i].v]);
      }
    mx[x] = Max(mx[x], sum - siz[x]);
    /*不难发现,假如以 x 为根,那么以 x 的 “父亲”
    为根的子树大小为整棵树的大小减以 x 为根的树的大小*/
    if (mx[x] < mx[rt]) rt = x;
}

#1.3.2 细节

注意到,每一次递归下去时,选择的根节点总是当前子树的重心,但是新的根不一定是之前的根的儿子,比如下面这张图:

第一次找到的根为 \(1\),递归下去后两颗子树上的重心分别为 \(4\)\(5\),都不是 \(1\) 的儿子,所以为了防止重复递归,应当每个节点进行点分治后加上标记,之后的递归不再进入已经打过标记的点,这也就是上面函数中 vis[] 的作用。

#2.0 例题

#2.1 P3806 【模板】点分治1

我们可以先将问题离线下来,一次点分治处理。

考虑一条长为 \(k\) 的路径,假设根为 \(rt\),那么一共有经过 \(rt\) 与不经过两种情况,不经过的情况可以递归处理。

来看经过 \(rt\) 的情况,那么如果在某棵子树中出现了距离 \(rt\)\(l\) 的链,那么如果 \(k-l\) 在其他的子树中出现,那么 \(k\) 就一定可以出现,注意到 \(k-l\)\(l\) 的出现顺序并无影响,所以便可以一棵一棵子树地维护,具体方法是:

  • \(\texttt{DFS}\) 处理当前子树上每个点与 \(rt\) 的距离;
  • \(\texttt{DFS}\) 的过程中,同时统计有哪些长度出现;
  • 与之前子树中出现的边结合,更新答案;
  • 将当前子树出现的信息并入。

最后要清空记录的之前子树中出现的边的信息,不要直接使用 memset(),应用队列保证时间复杂度正确。

之后便递归进入子树进行点分治。

const int N = 100010;
const int INF = 0x7fffffff;

struct Edge {
    int u, v, w;
    int nxt;
};
Edge e[N];

int n, m, cnt = 1, head[N], qs[N], ans[N];
int rt, sum, siz[N], mx[N], vis[N], tf[10000010];
int dist[N], dd[N], dcnt, q[N], frt, tal;

/*head[], cnt 是存图的,qs[] 存储每一个询问,
ans[] 存储对应问题的结果,rt 是当前子树选定的根,
sum 是当前子树的总大小,siz[x] 是以 x 为根的子树的大小
mx[x] 记录若选择 x 为根,则他的最大子树的大小
vis[] 表示当前点是否已被处理
tf[x] 存储已处理子树上是否有长度为 x 的链
dist[x] 储存 x 距离 rt 的距离,
dd[x] 记录当前子树拥有的链(长度)
dcnt 记录当前子树到 rt 的链的个数,
q[], frt, tal 是队列,用于清空 tf[]*/

template <typename T>
inline T Max(const T a, const T b) {
    return a > b ? a : b;
}

inline void add(const int &u, const int &v, const int &w) {
    e[cnt].u = u, e[cnt].v = v, e[cnt].w = w;
    e[cnt].nxt = head[u], head[u] = cnt ++;
}

void calcsiz(int x, int fa) {
    siz[x] = 1, mx[x] = 0; //初始化
    for (int i = head[x]; i; i = e[i].nxt) {
        if (e[i].v == fa || vis[e[i].v]) continue;
        calcsiz(e[i].v,x);
        siz[x] += siz[e[i].v];
        mx[x] = Max(mx[x], siz[e[i].v]);
    }
    mx[x] = Max(mx[x], sum - siz[x]);
    if (mx[x] < mx[rt]) rt = x;
}

void calcdist(int x, int fa) {
    dd[++ dcnt] = dist[x];
    for (int i = head[x]; i; i = e[i].nxt)
      if (e[i].v != fa && !vis[e[i].v]) {
          dist[e[i].v] = dist[x] + e[i].w;
          calcdist(e[i].v, x);
      }
}

void dfz(int x, int fa) {
    frt = 0, tal = -1, q[++ tal] = 0;
    tf[0] = true, vis[x] = true; //初始化与进行标记
    for (int i = head[x]; i; i = e[i].nxt) {
        if (e[i].v == fa || vis[e[i].v]) continue;
        dist[e[i].v] = e[i].w;calcdist(e[i].v, x);
        for (int k = 1; k <= dcnt;k ++)
          for (int j = 1; j <= m;j ++) //枚举所有的询问
            if (qs[j] >= dd[k]) ans[j] |= tf[qs[j] - dd[k]];
        for (int j = 1; j <= dcnt; j ++)
          if (dd[j] < 10000010)
              q[++ tal] = dd[j],tf[dd[j]] = true;
            //观察数据范围可知,询问不会超过 1e7.
        dcnt = 0;
    }
    while (frt <= tal) tf[q[frt ++]] = false;
    /*递归进入子树*/
    for (int i = head[x]; i; i = e[i].nxt){
        if (e[i].v == fa || vis[e[i].v]) continue;
        sum = siz[e[i].v], mx[rt = 0] = INF;
        calcsiz(e[i].v, x); calcsiz(rt, -1);
        dfz(rt, x);
    }
}

int main() {
    scanf("%d%d", &n, &m);
    for (int i = 1; i < n; i ++) {
        int u, v, w;scanf("%d%d%d", &u, &v, &w);
        add(u, v, w);add(v, u, w);
    }
    for (int i = 1; i <= m; i ++) scanf("%d", qs + i);
    mx[rt = 0] = INF, sum = n;
    calcsiz(1, -1); calcsiz(rt, -1); dfz(rt, -1);
    for (int i = 1;i <= m;i ++)
      if (ans[i]) printf("AYE\n");
      else printf("NAY\n");
    return 0;
}

#2.2 P4178 Tree

点分治不多说,我们主要来看信息怎么维护。

我们当然可以统计出子树中每个点与 \(rt\) 距离进行维护,但那样太麻烦了,考虑简便一点的方法。

我们可以计算出每个点所属于 \(rt\) 的哪一棵子树 b[x],特别的,令 b[rt]=rt。将所有节点(包括 \(rt\))按照与 \(rt\) 的距离从小到大排序,得到数组 subt[],采用双指针 l,r 分别从头尾遍历。如果当前所指的点与 \(rt\) 距离和大于 \(k\),那么就让 r --,因为此时 l 右侧的点与 \(rt\) 的距离只会更大,直到 dist[subt[l]] + dist[subt[r]] <= k,那么此时 l+1r 这一段的点都是与 l 所指的点与 \(rt\) 的距离和小于等于 \(k\) 的,但是其中可能有与 \(l\) 所指的节点在同一子树内的,这种情况不合法,应当舍去,所以我们应当维护 cnt[x] 表示在区间 \([l + 1,r]\) 中以 \(x\) 为根的点的数量,更新的答案即为 r - l - cnt[subt[l]].

const int N = 100010;
const int INF = 0x7fffffff;

struct Edge {
    int u, v, w;
    int nxt;
};
Edge e[N];

int n, m, cnt = 1, head[N], dcnt, res;
int siz[N], mx[N], rt, sum, vis[N], dist[N];
int b[N], subt[N], scnt[N];

/*head[], cnt 是存图的,rt 是当前子树选定的根,
sum 是当前子树的总大小,siz[x] 是以 x 为根的子树的大小
mx[x] 记录若选择 x 为根,则他的最大子树的大小
vis[] 表示当前点是否已被处理,dist[x] 储存 x 距离 rt 的距离,
dcnt 记录当前子树节点个数,b[x] 存储 x 属于 rt 的哪棵子树
subt[] 存储子树节点,scnt[x] 是上文中的 'cnt'*/

template <typename T>
inline T Max(const T a, const T b) {
    return a > b ? a : b;
}

template <typename T>
inline T Min(const T a, const T b) {
    return a < b ? a : b;
}

inline int cmp(const int &a, const int &b) {
    return dist[a] < dist[b];
}

inline void add(const int &u, const int &v, const int &w) {
    e[cnt].u = u, e[cnt].v = v, e[cnt].w = w;
    e[cnt].nxt = head[u], head[u] = cnt ++;
}

void calcsiz(int x, int fa) {
    siz[x] = 1, mx[x] = 0;
    for (int i = head[x]; i; i = e[i].nxt)
      if (e[i].v != fa && !vis[e[i].v]) {
          calcsiz(e[i].v, x);
          siz[x] += siz[e[i].v];
          mx[x] = Max(mx[x], siz[e[i].v]);
      }
    mx[x] = Max(mx[x], sum - siz[x]);
    if (mx[x] < mx[rt]) rt = x;
}

void calcdist(int x, int fa) {
    if (fa != rt) b[x] = b[fa];
    else b[x] = x;
    subt[++ dcnt] = x, scnt[b[x]] ++;
    for (int i = head[x]; i; i = e[i].nxt)
      if (e[i].v != fa && !vis[e[i].v])
        dist[e[i].v] = dist[x] + e[i].w,
        calcdist(e[i].v, x);
}

void dfz(int x, int fa) {
    subt[++ dcnt] = x, b[x] = x, dist[x] = scnt[x] = 0;
    for (int i = head[x]; i; i = e[i].nxt)
      if (e[i].v != fa && !vis[e[i].v]) {
          dist[e[i].v] = e[i].w; scnt[e[i].v] = 0;
          calcdist(e[i].v, x);
      }
    sort(subt + 1, subt + dcnt + 1, cmp);
    int l = 1, r = dcnt; vis[x] = true;
    while (l < r) {
        while (dist[subt[l]] + dist[subt[r]] > m)
          scnt[b[subt[r]]] --, r --;
        res += r - l - scnt[b[subt[l]]];
        l ++, scnt[b[subt[l]]] --;
    }
    dcnt = 0;
    for (int i = head[x]; i;i = e[i].nxt)
      if (e[i].v != fa && !vis[e[i].v]){
          sum = siz[e[i].v], mx[rt = 0] = INF;
          calcsiz(e[i].v, x); calcsiz(rt, -1);
          dfz(rt, x);
      }
}

int main() {
    nowt = time(0);
    scanf("%d", &n);
    for (int i = 1; i < n; i ++) {
        int u, v, w;scanf("%d%d%d", &u, &v, &w);
        add(u, v, w); add(v, u, w);
    }
    mx[rt = 0] = INF, sum = n;
    calcsiz(1, -1); calcsiz(rt, -1);
    scanf("%d", &m); dfz(rt, -1);
    printf("%d", res);
    return 0;
}

#2.3 P2634 [国家集训队]聪聪可可

题目实际上让求长度是 \(3\) 的倍数的有序点对的个数。

看到这题,笔者首先想到的是树形 \(\texttt{DP}\) 而非点分治,当然树形 \(DP\) 是可做的,与点分治转移的方式接近,且时间复杂度似乎更优一些,但树形 \(\texttt{DP}\) 并不是本文所讨论的内容,这里不做说明,来看看本题点分治的做法。

一样的考虑方式,假设根为 \(rt\),那么一共有经过 \(rt\) 与不经过两种情况,不经过的情况可以递归处理。

来看经过 \(rt\) 的情况,能够拼成 \(3\) 的倍数的情况只有以下 \(2\) 种:

  • 两边链长度都是 \(3\) 的倍数;
  • 一边链长度模 \(3\)\(1\),另一边模 \(3\)\(2\)

我们可以统计每棵子树上与 \(rt\) 的距离 \(3\) 的倍数、长度模 \(3\)\(1\)、模 \(3\)\(2\) 的点的个数 \(b_0,b_1,b_2\),设前面的子树上所有与 \(rt\) 的距离 \(3\) 的倍数、长度模 \(3\)\(1\)、模 \(3\)\(2\) 的点的个数分别为 \(db_0,db_1,db_2\),那么这棵子树上可贡献的数量为

\[b_0\times db_0+b_1\times db_2+b_2\times db_1+b_0, \]

累加入答案,之后将 \(b\)\(db\) 合并。最后递归进入子树进行点分治。

注意到,\((1,2)\)\((2,1)\) 是两种不同的答案,单个点(即路径长度为 \(0\))也是一种答案,故统计出的答案应当乘二再加上总共点的个数。总共有 \(n^2\) 种不同的路径,别忘要分子分母化为互质的两个数。

const int N = 100010;
const int INF = 0x3fffffff;

struct Edge {
    int u, v, w;
    int nxt;
};
Edge e[N];

int n, cnt = 1, head[N], vis[N], db[5], res;
int siz[N], mx[N], rt, sum, b[5], dist[N], dcnt;

template <typename T>
inline T Max(const T a, const T b) {
    return a > b ? a : b;
}

template <typename T>
inline T Min(const T a, const T b) {
    return a < b ? a : b;
}

int gcd(int a, int b){
    if (!b) return a;
    return gcd(b, a % b);
}

inline void add(const int &u, const int &v, const int &w) {
    e[cnt].u = u, e[cnt].v = v, e[cnt].w = w;
    e[cnt].nxt = head[u], head[u] = cnt ++;
}

void calcsiz(int x, int fa) {
    siz[x] = 1, mx[x] = 0;
    for (int i = head[x]; i; i = e[i].nxt)
      if (e[i].v != fa && !vis[e[i].v]) {
          calcsiz(e[i].v, x);
          siz[x] += siz[e[i].v];
          mx[x] = Max(mx[x], siz[e[i].v]);
      }
    mx[x] = Max(mx[x], sum - siz[x]);
    if (mx[x] < mx[rt]) rt = x;
}

void calcdist(int x, int fa) {
    b[dist[x] % 3] ++;
    for (int i = head[x]; i; i = e[i].nxt)
      if (e[i].v != fa && !vis[e[i].v])
        dist[e[i].v] = dist[x] + e[i].w,
        calcdist(e[i].v, x);
}

void dfz(int x, int fa) {
    vis[x] = true, db[0] = db[1] = db[2] = 0, dist[x] = 0;
    for (int i = head[x]; i; i = e[i].nxt) {
        if (e[i].v == fa || vis[e[i].v]) continue;
        b[0] = b[1] = b[2] = 0;
        dist[e[i].v] = e[i].w;
        calcdist(e[i].v, x);
        res += b[0] * db[0] + b[1] * db[2] + b[2] * db[1];
        db[0] += b[0], db[1] += b[1], db[2] += b[2];
    }
    res += db[0];
    for (int i = head[x]; i; i = e[i].nxt) {
        if (e[i].v == fa || vis[e[i].v]) continue;
        sum = siz[e[i].v], mx[rt = 0] = INF;
        calcsiz(e[i].v, x); calcsiz(rt, -1);
        dfz(rt, x);
    }
}

int main() {
    scanf("%d", &n);
    for (int i = 1; i < n; i ++) {
        int u, v, w;
        scanf("%d%d%d", &u, &v, &w);
        add(u, v, w); add(v, u, w);
    }
    sum = n, mx[rt = 0] = INF;
    calcsiz(1, -1); calcsiz(rt, -1);
    dfz(rt, -1); res = res * 2 + n;
    int gd = gcd(res, n * n);
    printf("%d/%d", res / gd, n * n / gd);
    return 0;
}

#2.4 总结

结合上面 \(3\) 道例题,不难发现,点分治并没有固定的信息维护方式,仅提供了一种方法、思路,是分治思想在树上的运用,具体怎样转移、维护还需要分析所需维护的信息的性质。

参考资料

[1] 树分治 - OI Wiki

posted @ 2021-06-24 16:53  Dfkuaid  阅读(1067)  评论(2编辑  收藏  举报