点分治(树分治)

点分治(树分治)算法解析

一、什么是树的重心?

重心的定义:    

     对于树中的某个节点u:

  • 删除u后,树会分裂成若干子树

  • 记其中最大的子树大小为max_subtree_size(u)

  • 重心是使max_subtree_size(u)最小的节点

        

重心的性质:
  • 一棵树至少有1个重心,最多2个重心

  • 如果有两个重心,它们必定相邻

  • 删除重心后,所有子树的大小 ≤ n/2(这是点分治效率的保证)

如何计算重心?

int sz[MAXN]; // 记录子树大小
int centroid = -1, min_max_sub = INT_MAX;

void dfs_centroid(int u, int fa, int total_nodes) {
    sz[u] = 1;
    int max_sub = 0;
    
    for (int v : tree[u]) {
        if (v == fa || vis[v]) continue;
        dfs_centroid(v, u, total_nodes);
        sz[u] += sz[v];
        max_sub = max(max_sub, sz[v]); // 子节点方向的子树
    }
    
    max_sub = max(max_sub, total_nodes - sz[u]); // 父节点方向的"子树"
    if (max_sub < min_max_sub) {
        min_max_sub = max_sub;
        centroid = u;
    }
}

// 调用方式:dfs_centroid(root, -1, n);

二、点分治算法思路

        寻找树的重心,将树分割成n颗大小近似相同的子树,操作每一颗子树,然后递归再次寻找每一颗子树的重心,从而再次操作每一颗子树······

        每次递归问题规模至少减半,形成递归树高度为O(log n)

为什么选择重心?

因为重心能保证树被均衡分割,递归深度为O(log n),避免退化成链的最坏情况。

三、一个具体问题

题目描述

给定一棵有 n 个点的树,询问树上距离为 k 的点对是否存在。

输入格式

第一行两个数 n,m。

第 2 到第 n 行,每行三个整数 u,v,w,代表树上存在一条连接 u 和 v 边权为 w 的路径。

接下来 m 行,每行一个整数 k,代表一次询问。

输出格式

对于每次询问输出一行一个字符串代表答案,存在输出 AYE,否则输出 NAY

io样例

                        输入 #1                                输出#1

                2 1                      AYE
                1 2 2
                2
解决代码
#include<iostream>
#include<vector>
#include<algorithm>

using namespace std;

const int maxn = 10010;
const int maxm = 110;
const int maxk = 10000000;

//边结构体
struct Edge{
  int to, w;
  Edge(int t, int weight) : to(t), w(weight){}
};


vector<Edge> G[maxn];            //邻接表存图
int n, m;
int tim = 0;                     //全局时间戳
int time_exist[maxk+1] = {0};    //记录距离的时间戳
int q[maxm+1];                   //存储所有查询的k值
int sz[maxn+1];                  //记录子树大小
bool vis[maxn+1] = {false};      //标记节点是否被处理
bool exist[maxk+1] = {false};    //记录距离是否存在
bool ans[maxm+1] = {false};      //存储每个查询的答案

//计算子树大小
void get_size(int u, int fa){
  sz[u] = 1;
  for(auto &e : G[u]){
    int v = e.to;
    if(v != fa && !vis[v]){
      get_size(v, u);
      sz[u] += sz[v];
    }
  }
}

//找树的重心
void get_root(int u, int fa, int total, int &root, int &min_size){
  int max_part = 0;//删除重心后最大子树的大小
  for(auto &e : G[u]){
    int v = e.to;
    if(v != fa && !vis[v]){
      get_root(v, u, total, root, min_size);
      max_part = max(max_part, sz[v]);
    }
  }
  max_part = max(max_part, total - sz[u]);
  if(max_part < min_size){// 找到更平衡的重心
    root = u;
    min_size = max_part;
  }
}

void dfs_dis(int u, int fa, int dis, vector<int> &temp){
  if(dis > maxk) return; //剪枝
  temp.push_back(dis);

  for(auto &e : G[u]){
    int v = e.to, w = e.w;
    if(v != fa && !vis[v]){
      dfs_dis(v, u, dis+w, temp);
    } 
  }
}

void solve(int u){
  vis[u] = true;// 标记当前分治中心已处理
  exist[0] = true;
  tim++;
  time_exist[0] = tim;

  for(auto &e : G[u]){
    int v = e.to, w = e.w;
    if(vis[v]) continue;

    vector<int> temp;
    dfs_dis(v, u, w, temp);//计算子树每一个点到重心的距离

    // 检查当前子树的距离能否与已记录距离配对
    for(int d : temp){
      for(int i = 0; i < m; i++){
        if(ans[i]) continue;
        int r = q[i] - d;
        if(r < 0 || r > maxk) continue;
        if(exist[r] && time_exist[r] == tim){
          ans[i] = true;
        }
      }
    }
    // 记录当前子树的所有距离
    for(int d : temp){
      exist[d] = true;
      time_exist[d] = tim;
   }
  }

  for(auto &e : G[u]){
    int v = e.to;
    if(!vis[v]){
      get_size(v, u);
      int total = sz[v];
      int root_v, min_size = n + 10;
      get_root(v, 0, total, root_v, min_size);
      solve(root_v);
    }
  }
}

int main (){
  ios::sync_with_stdio(false);
  cin.tie(0);
  cout.tie(0);

  cin >> n >> m;
  for(int i = 1; i < n; i++){
    int a, b, c;
    cin >> a >> b >> c;
    G[a].push_back(Edge(b, c));
    G[b].push_back(Edge(a, c));
  }
  for(int i = 0; i < m; i++)
    cin >> q[i];

  int root, min_size = n + 10;
  get_size(1, 0);
  get_root(1, 0, n, root, min_size);
  solve(root);

  for(int i = 0; i < m; i++){
    if(ans[i])  cout << "AYE\n";
    else cout << "NAY\n";
  }


  return 0;
}
时间复杂度分析
  • 找重心:O(n) 每次处理
  • 递归深度:O(log n) 层
  • 每层处理:所有节点O(n),每个节点处理m个查询
  • 总复杂度:O(n log n × m)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值