前言
刚刚学习完点分治的基础知识,写完一道模板题后来写一写来理解下所学习的东西。
正文
分治的思想主要是将一个主问题分解为一个个子问题,而子问题与主问题有着同样的性质,并且主问题的答案与各个子问题合并后相同。而点分治就是将一棵树通过对点进行选取拆分,通过处理子树递归得到答案。
分治点的选择(分)
如果将一棵树以一个点拆分成多个子树,而对子树进行同样的拆分操作,那么明显每次将树拆分成各个子树大小尽量相同较为优先。假如说一棵树正好是一条链,那么比起从链的一端一步步向下拆分,肯定是每次从中间拆分的次数比较少。所以每次拆分点的最大子树大小影响力而一棵树最大子树最小的点正好有一个名称,叫做树的重心。
对问题的处理(治)
将树拆分的问题解决后便是如何对一个问题进行处理,这里以POJ-1741(模板题)http://poj.org/problem?id=1741为例子进行说明(qwq太弱了(⊙=⊙))。题目要求在树上有多少对点,是两点之间距离小于等于K的。
对于这个问题是将结果分为经过重心与不经过重心进行分别处理。这样子的话我们处理完经过重心的答案时,便可以将点摘去,然后不经过重心的答案便只用在子树中处理便可以了。有一点需要注意的是经过重心的答案可能是从一个子树上来的,重心到这个子树上的一些边可能被计算两次,不符合要求,那么这种数量不能计算到最终答案中。所以这时候我们需要容斥一下,将子树上的答案减去就行了。
代码实现
#include <iostream> #include <cstring> #include <string> #include <algorithm> #include <cmath> #include <cstdio> #include <queue> #include <stack> #include <map> #define ull unsigned long long #define met(a, b) memset(a, b, sizeof(a)) #define lowbit(x) (x&(-x)) #define MID (l + r) / 2 #define ll long long #define cinint(a) scanf("%d", &a) #define cinll(a) scanf("%lld", &a) #define cindouble(a) scanf("%lf", &a) #define cinstr(a) scanf(" %s", a) #define coutint(a) printf("%d ", a) #define coutll(a) printf("%lld ", a) #define coutdouble(a) printf("%lf ", a) using namespace std; const int maxn = 1e4 + 7; const ll mod = 1e9 + 7; const int inf = 0x3f3f3f3f; const ll INF = 0x3f3f3f3f3f3f3f3f;
//前向星存图 struct Edge { ll to, dist, net; }MAP[maxn*2]; ll head[maxn], cnt;
//
ll Dist[maxn]; //记录各个点的深度 ll arr[maxn], tail; //将各个深度记录算答案用 ll sz[maxn], msz[maxn], all, rt; //分别用作记录各个点的子树大小,各个点的最大子树大小,子树总大小,重心位置 bool vis[maxn]; //记录点是否被摘去 ll n, k; ll res; //最终答案 void init() { for(ll i = 1; i <= n; i++) { head[i] = -1; vis[i] = 0; } rt = cnt = res = 0; msz[rt] = INF; all = n; } void addMAP(ll u, ll v, ll c) { MAP[cnt] = (Edge){v, c, head[u]}; head[u] = cnt++; }
//寻找重心位置 void Findrt(ll pos, ll pre) { sz[pos] = 1; msz[pos] = 1; for(ll i = head[pos]; i != -1; i = MAP[i].net) { ll to = MAP[i].to; if(vis[to] || to == pre) continue; Findrt(to, pos); sz[pos] += sz[to]; msz[pos] = max(sz[to], msz[pos]); } msz[pos] = max(all - sz[pos], msz[pos]); //注意除去下面子树,进来方向的也算子树 if(msz[pos] < msz[rt] || rt == 0) rt = pos; //要求最大子树最小 }
//计算以当前点为根时的各点深度 void getDist(ll pos, ll pre) { arr[++tail] = Dist[pos]; for(ll i = head[pos]; i != -1; i = MAP[i].net) { ll to = MAP[i].to; if(to == pre || vis[to]) continue; Dist[to] = Dist[pos] + MAP[i].dist; getDist(to, pos); } } ll con(ll pos, ll dist) { Dist[pos] = dist; tail = 0; ll sum = 0; getDist(pos, 0); sort(arr+1, arr+1+tail); ll l = 1, r = tail; while(l < r) { while(l < r && arr[l] + arr[r] > k) r--; //题目要求两点距离小于等于K,便是两点到根节点距离相加小于等于K sum += r - l; l++; } return sum; } ll Spot_Divide_Conquer(ll pos) { Findrt(pos, 0); //对于每棵树处理时先寻找重心rt vis[rt] = 1; //将重心rt摘去 res += con(rt, 0); //con函数计算经过该点点对答案的恭喜 for(ll i = head[rt]; i != -1; i = MAP[i].net) { ll to = MAP[i].to; if(vis[to]) continue;
//减去各个子树对答案的影响 all = sz[to]; rt = 0; res -= con(to, MAP[i].dist);
// Spot_Divide_Conquer(to); } } int main() { while(1) { cinll(n); cinll(k); if(!n && !k) break; init(); for(ll i = 1; i < n; i++) { ll u, v, c; cinll(u); cinll(v); cinll(c); addMAP(u, v, c); addMAP(v, u, c); } Spot_Divide_Conquer(1); coutll(res); } return 0; }