首页 > 编程语言 >【算法】树分治

【算法】树分治

时间:2024-02-04 15:57:16浏览次数:31  
标签:tmp int maxs 分治 算法 siz 节点

1. 算法简介

树分治(Tree division),是处理树上路径类问题的算法。树分治又可以分为点分治边分治

考虑这样一个问题:给定一棵有 \(n\) 个点的树,询问树上距离为 \(k\) 的点对是否存在。

暴力的做法就是枚举两个点然后计算距离,统计答案。这样显然 \(O(n^2)\) 的。

我们发现瓶颈在于枚举的过程:我们希望快速地知道树上的路径信息,而不在乎路径上的端点。

这时候就需要使用树分治算法来优化时间。

2. 点分治

点分治是树分治的一种。

大家可能看出来了,上述的例题就是 P3806 【模板】点分治 1

对于一棵树而言,树上的路径无外乎两种:一种是经过根节点的,另一种是不经过根节点的。(前提是有根树,无根树可转为有根树)

对于经过根节点的路径,想要知道其路径信息是很容易的。但不经过根节点的路径就很难维护了(即所在子树相同)。以当前为根的树很难维护其子树路径的信息。这时候我们便可以删去当前根节点,分裂成许多以儿子节点为子树根的新树。

分裂之前:

image

分裂之后:

image

由于每一个节点都当过根节点,这样,树上的所有路径等能被统计到。

但我们会发现,当树为链时,分治的时间复杂度依然为 \(O(n)\) ,没有达到优化时间的目的。

这是,按照原来老老实实从根节点开始分治的方法已经不适用,这时候我们需要找到一个合适的点,使得分治之后,时间复杂度趋近于 \(O(\log n)\)

这个点就是树的重心

2.1 求重心

树的中心定义为:其所有的子树中最大的子树节点数最少。当删去此点时,生成的多棵新树会趋于平衡,这也会让点分治的时间复杂度趋于 \(O(\log n)\)。

找中心,便一边 \(dfs\) 整棵树即可。

设 \(maxs_x\) 表示 \(x\) 节点的最大的子树大小,\(siz_x\) 表示 \(x\) 节点的子树大小,\(rt\) 为选出的根。

考虑一个节点在原树上的位置。值得注意的是,当此节点不为根节点时,其子树包括其父辈之外的所有节点,像这样:

image

蓝色圈出部分为 'now' 节点的所有子树。

Code:

void getrt(int x, int fa) {
  siz[x] = 1, maxs[x] = 0;
  for (int i = h[x]; i; i = e[i].nx) {
    int y = e[i].v;
    if(y == fa || vis[y]) continue;
    getrt(y, x);
    siz[x] += siz[y];
    if(maxs[x] < siz[y]) maxs[x] = siz[y]; 
  }
  maxs[x] = max(maxs[x], sum - siz[x]);
  if(maxs[rt] > maxs[x]) rt = x;
}

注意!!!!,当分裂成多个子树之后,分治到子树时,则需重新找重心。来保证程序的时间复杂度。

由于重新找重心是在分治的过程中完成的,故总时间复杂度不会超过 \(O(n\log n)\)。

2.2 分治

找到了重心之后,便可以以重心为根进行分治。

设 \(vis_x\) bool 数组表示 \(x\) 节点是否被“删除”(删除的节点不能被再次遍历,也不能再次进行答案统计)。

由于每次进入下一层分治时要重新找重心,故分治时要及时把 \(maxs_rt\) 设置为 \(siz_y\)(最大不会超过 \(siz_y\)),根节点编号也要设置为 \(0\)。

找完之后,以新重心为根,继续分治,统计答案;

Code:

void divide(int x) {
  vis[x] = f[0] = 1;
  solve(x);
  for (int i = h[x]; i; i = e[i].nx) {
    int y = e[i].v;
    if(vis[y]) continue;//遍历过的点包含其父亲节点
    maxs[rt = 0] = sum = siz[y];
    getrt(y, 0);
    divide(rt);
  }
}

当你在统计答案时想要使用 \(siz\) 时,直接在 'getrt(y, 0);' 补一句 'getrt(rt, 0)' 即可

2.3 答案统计

这里因题而异,拿模板题举例。

设 \(f_x\) 表示 \(x\) 在当前状态内是否出现,\(tmp_i\) 存储有多少种不同的路径长度。

先可以把新树的点到根的距离求出来:

Code:

void getdis(int x, int fa) {
  tmp[++cnt] = dis[x];
  for (int i = h[x]; i; i = e[i].nx) {
    int y = e[i].v;
    if(y == fa || vis[y]) continue;
    dis[y] = dis[x] + e[i].w;
    getdis(y, x);
  }
}

然后统计答案

Code:

void solve(int x) {
  int H = 1, t = 0;
  for (int i = h[x]; i; i = e[i].nx) {
    int y = e[i].v;
    if(vis[y]) continue;
    dis[y] = e[i].w;
    cnt = 0;
    getdis(y, x);
    For(j,1,cnt) {
      For(k,1,m) {
        if(Q[k] >= tmp[j]) ans[k] |= f[Q[k] - tmp[j]]; 
      }
    }
    For(j,1,cnt) {
      q[++t] = tmp[j];
      f[tmp[j]] = 1;
    }
  }
  while(H <= t) {
    f[q[H]] = 0;
    H++;
  }
}

3. 点分治例题

3.1 P4178 Tree

Proble

给定一棵有 \(n\) 个节点的树,每条边有边权,求出树上两点距离小于等于 \(k\) 的点对数量。

Solve

点分治模板。

考虑统计答案时,令路径长度数组为 \(tmp\),当新出现边权 \(tmp_x\) 时,看已有的边权里是否出现 \(tmp_y\) 使得 \(tmp_x + tmp_y \le k\)。推到得 \(tmp_y <= k - tmp_x\),所以将区间 \([0,k-tmp_x]\) 计入答案,再单点 \(tmp_x\) 加一即可。

树状数组可维护。

Code

#include <bits/stdc++.h>
#define int long long
#define rint register int
#define For(i,l,r) for(rint i=l;i<=r;++i)
#define FOR(i,r,l) for(rint i=r;i>=l;--i)
#define MOD 1000003
#define mod 1000000007
#define inf 0x3f3f3f3f3f3f3f3f

using namespace std;

namespace Read {
  template <typename T>
  inline void read(T &x) {
    x=0;T f=1;char ch=getchar();
    while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
    while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}
    x*=f;
  }
  template <typename T, typename... Args>
  inline void read(T &t, Args&... args) {
    read(t), read(args...);
  }
}

using namespace Read;

void print(int x){
  if(x<0){putchar('-');x=-x;}
  if(x>9){print(x/10);putchar(x%10+'0');}
  else putchar(x+'0');
  return;
}

const int N = 4e4 + 10;

struct Node {
  int v, w, nx;
} e[N << 1];

int n, h[N], k, tot, rt, sum, siz[N], maxs[N], t[N], dis[N], tmp[N], cnt, ans;

bool vis[N];

void add(int u, int v, int w) {
  e[++tot] = (Node) {v, w, h[u]};
  h[u] = tot;
}

void getrt(int x, int fa) {
  siz[x] = 1, maxs[x] = 0;
  for (int i = h[x]; i; i = e[i].nx) {
    int y = e[i].v;
    if(y == fa || vis[y]) continue;
    getrt(y, x);
    siz[x] += siz[y];
    if(maxs[x] < siz[y]) maxs[x] = siz[y]; 
  }
  maxs[x] = max(maxs[x], sum - siz[x]);
  if(maxs[rt] > maxs[x]) rt = x;
}

void getdis(int x, int fa) {
  tmp[++cnt] = dis[x];
  for (int i = h[x]; i; i = e[i].nx) {
    int y = e[i].v;
    if(y == fa || vis[y]) continue;
    dis[y] = dis[x] + e[i].w;
    getdis(y, x);
  }
}

int lb(int x) {
  return x & -x;
}

int qry(int x) {
  int Ans = 0;
  for (int i = x; i; i -= lb(i)) {
    Ans += t[i];
  }
  return Ans;
}

void upd(int x, int z) {
  for (int i = x; i <= k + 1; i += lb(i)) {
    t[i] += z;
  }
}

void solve(int x) {
  for (int i = h[x]; i; i = e[i].nx) {
    int y = e[i].v;
    if(vis[y]) continue;
    dis[y] = e[i].w;
    cnt = 0;
    getdis(y, x);
    For(j,1,cnt) {
      if(tmp[j] <= k) ans += qry(k - tmp[j] + 1);
    }
    For(j,1,cnt) {
      if(tmp[j] <= k) upd(tmp[j] + 1, 1);
    }
  }
  memset(t, 0, sizeof t);
  upd(1, 1);
}

void divide(int x) {
  vis[x] = 1;
  solve(x);
  for (int i = h[x]; i; i = e[i].nx) {
    int y = e[i].v;
    if(vis[y]) continue;
    maxs[rt = 0] = n; sum = siz[y];
    getrt(y, 0);
    divide(rt);
  }
}

signed main() {
  read(n);
  For(i,1,n-1) {
    int u, v, w;
    read(u, v, w);
    add(u, v, w);
    add(v, u, w);
  }
  read(k);
  maxs[0] = sum = n;
  getrt(1, 0);
  upd(1, 1);
  divide(rt);
  cout << ans << '\n';
  return 0;
}

标签:tmp,int,maxs,分治,算法,siz,节点
From: https://www.cnblogs.com/Daniel-yao/p/18005778

相关文章

  • 基础算法(十四)离散化+二分 ---以题为例
    假定有一个无限长的数轴,数轴上每个坐标上的数都是 0。现在,我们首先进行 n 次操作,每次操作将某一位置 x 上的数加 c。接下来,进行 m 次询问,每个询问包含两个整数 l 和 r,你需要求出在区间 [l,r] 之间的所有数的和。输入格式第一行包含两个整数 n 和 m。接下来 ......
  • 点分治
    点分治定义树上的分治先求一个点的答案,然后求子树树上距离小于等于k的点对数量枚举一个点p求解经过p的点对贡献,然后递归解决子树为了降低分治复杂度,要求重心,求重心要限定子树范围内,添加vis防止上访,求dis也要ans要减去在同一个子树重心的子树小于\(n/2\),所以调用递归......
  • CDQ分治
    CDQ分治引入偏序问题对于每个有序对\((a_i,b_i)\)求有多少个有序对\((a_j,a_j)\)\(a_i<a_j,b_i<b_j\)暴力\(O(n^2)\)按\(a\)排序,问题为求顺序对,cdq分治定义解决特定种类问题的算法,统计左区间对右区间的贡献,一个点所得贡献必然计算,且区间划分不重不漏实现要注意有......
  • 很好用的python游戏环境(续):强化学习算法走迷宫游戏环境(导航问题 navigation):分享一个pyt
    前文分享了一个python下的maze游戏环境,本文再给出一个不错的实现项目,这个项目的实现更加的简单,并且可视化界面做的很好看,是用tkinter框架做的可视化:相关:迷宫游戏python实现Github地址:https://github.com/wonanut/Maze-Game/tree/Maze-game-v1.0.7......
  • 很好用的python游戏环境:强化学习算法走迷宫游戏环境(导航问题 navigation):分享一个pyth
    项目的GitHub地址(作者:莫凡):https://github.com/MorvanZhou/mmaze运行的示例代码:importmmazestart=(0,0)end=(10,10)m=mmaze.generate(width=11,height=11,symmetry="horizontal")solutions=m.solve(start=start,end=end)m.plot(solution=solutions[0],star......
  • 国产AI模型和美国顶级AI模型的距离在哪?—— 算力?算法?数据?
    前段时间去了长春一汽,聊了ReinforcementLearning方面的工作,既是面试,也是谈了谈意向,最后全部OK,本打算是签合同了,结果HR说要求有三年的社保缴纳证明工作经验,最后说可以减到24个月,不过说来也是有意思,我这人还真没社保,这就尴尬了,最后说这是上面的文件,国企就这要求,后来也只能作罢,但是......
  • 图论算法学习笔记
    ybt1376floyd#include<iostream>#include<climits>#include<cstring>#include<queue>#include<vector>#defineinfinity0x3f3f3f3f#defineN105intn,m,G[N][N],dist[N][N];intmain(){ memset(dist,infinity,sizeof(dist)); st......
  • Problem P06. [算法课分治] 找到 k 个最长重复字符串
    注意是在该子字符串内每个字符的出现次数都不少于k。可以采用分治的方法,函数找一个不符合条件的字符,然后将字符串分成两个子字符串,就这样进行递归运算,每次找到符合条件的子字符串就判断一波长度,然后将最长的长度值存下来。#include<iostream>#include<bits/stdc++.h>#includ......
  • 贪心算法
    贪心算法(就是猜测再试试证明)905.区间选点905.区间选点-AcWing题库 给定 N个闭区间 [ai,bi],请你在数轴上选择尽量少的点,使得每个区间内至少包含一个选出的点。输出选择的点的最小数量。位于区间端点上的点也算作区间内。输入格式第一行包含整数 N,表示区间数。接下......
  • 快速排序算法
    快速排序算法的核心思想是分治法(DivideandConquer)。快速排序算法通过以下步骤实现排序:1.选择基准值 :从数列中选择一个元素作为基准值(pivot),通常选择第一个元素。2.分区操作 :重新排列数列,使得所有小于基准值的元素都移到基准的前面,而所有大于基准值的元素都移到基准的后......