点分治模板 / p3806


前言

点分治好难Orz,感觉比SAM还难一些,模板码也有六七行的样子,学了好久…

点分治是一种解决树上点对、树上路径的问题的分治方法

由于树上数据难以存储至常规数据结构中,因此朴素做法往往有着难以接收的复杂度

而利用分支/二分思想,则可以降下一个数量级,可以处理大规模的树上问题,一般时间复杂度可以降至 O ( n log ⁡ n ) O(n \log n) O(nlogn)

那么接下来简要介绍一些这个的算法的流程以及实现

先推荐一个网址: bilibili点分治上面的up主讲的挺好

在了解点分治之前,需要有关于 树的重心 的前置知识

好下面来介绍它的具体流程,先放张经典的图:

点对数无非三种,如上述图所示,

首先第一步是决策 S 点选为何点,直观的可以感受到就是选择比较靠中心的点,我们一般用重心,方便求,也有着良好的时间复杂度,我们用getroot()这个函数去求它,大致流程是dfs遍历整个树,谁的最大子树的节点数最小,谁就是重心

选好 S 点后我们考虑上述三种情况,(1)好办,递归分治即可,(3)也好说,可以和(1)放在一块处理,关键是(2)这个不是很好解决

为此,我们专门构造一个函数calc()用来处理(2),具体想法是, S 的子树个数是有限的,那么我们挨个挨个来处理它的子树,

以下面的例题为例,要求满足 d i s ( i , j ) = k dis(i,j)=k dis(i,j)=k的点对,一个朴素的思想是,枚举不同子树的节点一一比较,但这样时间复杂度又变成 O ( n 2 ) O(n^2) O(n2)了,这样不好,点分治的处理手段是类似于分桶的思想,在处理子树时,把子树中所有结点到S的距离储存起来,将其存储至桶内

比如说当我们处理完(2)左面的子树后,再处理下面的子树,将下面子树的所有节点到 S 的距离求出来,假如对某个节点 W 它到 S 的距离为 d i s w dis_w disw,而之前处理的子树中,又恰好有个节点 V 满足 d i s w + d i s v = k dis_w+dis_v =k disw+disv=k,那很好,我们把这个加到答案中,直至遍历完 S 所有子树的所有节点

到这里,整个过程差不多就要结束了,我们大致第可以感受到分治是如何对时间复杂度进行优化的,它类似于二分似的将整个树分为若干子树,子树内再递归处理,子树与子树之间则是难点,处理子树与子树间关系时,开了一个桶,开存储节点到 S 的路径,每处理一个节点时,对离线所存储的询问进行遍历,更新询问的答案,进而保障了时间复杂度还在 O ( n ) O(n) O(n)

一、 例题 p3806

题目链接 洛谷p3806

二、 思路及代码

1. 思路

很经典的点分治入门题,求点对 (i, j) 距离 = k,是否存在

对calc函数进行稍较修改,套模板,即可

2. 代码

代码如下 :

#include <cstring>
#include <iostream>
#include <vector>
using namespace std;
const int maxn = 1e4 + 5;
const int maxk = 1e7 + 5;
struct e {
  int to, w, next;
} edge[maxn << 1];
int cnt, head[maxn];
void addedge(int u, int v, int w) {
  edge[cnt] = (e){v, w, head[u]};
  head[u] = cnt++;
}
// root记录重心,sum记录当前树大小,tot记录根过根路径数
int n, m, root, sum, tot;
// siz记录子树大小,maxp记录重子树节点数
int siz[maxn], maxp[maxn], q[105];
// tmpd记录算出的距离,dis[i]为root与i之间的距离
int tmpd[maxn], dis[maxn];
// have记录路径是否存在,ans记录询问答案,vis记录被删根
bool have[maxk], ans[105], vis[maxn];
void getroot(int u, int fa) {
  siz[u] = 1, maxp[u] = 0;
  for (int i = head[u]; ~i; i = edge[i].next) {
    int v = edge[i].to;
    if (v == fa || vis[v]) continue;
    getroot(v, u);
    siz[u] += siz[v];
    maxp[u] = max(maxp[u], siz[v]);
  }
  maxp[u] = max(maxp[u], sum - siz[u]);
  if (maxp[u] < maxp[root]) root = u;
}
void getdis(int u, int fa) {  // tmpd记录子树过根路径
  tmpd[tot++] = dis[u];       // tot记录子树过根路径数
  for (int i = head[u]; ~i; i = edge[i].next) {
    int v = edge[i].to;
    if (v == fa || vis[v]) continue;
    dis[v] = dis[u] + edge[i].w;
    getdis(v, u);
  }
}
void calc(int u) {  // 计算过根路径
  vector<int> vec;
  for (int i = head[u]; ~i; i = edge[i].next) {
    int v = edge[i].to;
    if (vis[v]) continue;
    tot = 0, dis[v] = edge[i].w;
    getdis(v, u);
    for (int j = 0; j < tot; j++)  // tot记录子树过根路径数
      for (int k = 0; k < m; k++)  // tmpd记录子树过根路径
        if (q[k] >= tmpd[j]) ans[k] |= have[q[k] - tmpd[j]];
    for (int j = 0; j < tot; j++) {
      if (tmpd[j] > 1e7) continue;
      vec.push_back(tmpd[j]);
      have[tmpd[j]] = true;  // have[i]记录距离i的路径是否存在
    }
  }
  for (int i = 0; i < vec.size(); i++) have[vec[i]] = false;
}
void divide(int u) {
  vis[u] = have[0] = true;  // 删除根结点,0 路径存在
  calc(u);                  // 更新不同 子树对 所贡献的答案
  for (int i = head[u]; ~i; i = edge[i].next) {
    int v = edge[i].to;
    if (vis[v]) continue;
    maxp[root = 0] = sum = siz[v];
    getroot(v, 0);
    getroot(root, 0);  // 两次get时是为了更新siz[]
    divide(root);
  }
}
int main() {
  //   freopen("in.txt", "r", stdin);
  //   freopen("out.txt", "w", stdout);
  memset(head, -1, sizeof(head));
  scanf("%d%d", &n, &m);
  int u, v, w;
  for (int i = 1; i < n; i++) {
    scanf("%d%d%d", &u, &v, &w);
    addedge(u, v, w), addedge(v, u, w);
  }
  for (int i = 0; i < m; i++) scanf("%d", &q[i]);
  maxp[root = 0] = sum = n;  // !init
  getroot(1, 0);
  getroot(root, 0);
  divide(root);
  for (int i = 0; i < m; i++) printf("%s\n", ans[i] ? "AYE" : "NAY");
  return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值