AcWing 252. 树(点分治)
前置知识:
淀粉质将一颗树划分为重心以及若干颗子树,对于某个问题而言,划分为两个子问题:
\(1.\)问题在同一颗子树的内部,递归下去求解。
\(2.问题在两个子树之间,这时候必然经过重心,涉及到答案的合并,是题目的难点\)
关于为什么要选择重心:每次都将问题的范围缩小为原来的一半,使得总的时间复杂度为\(O(nlog^{2}n)\).此时的重心保证,除重心所在子树之外的其他子树之和小于总的子树和的一半即可。
步骤:
\(1.\)找出重心,并删除重心
\(2.\)求解子问题
本题思路:
本题让求长度不超过 \(K\) 的路径。考虑路径的两个点分别来自哪儿,将问题划分为三类:
- 路径的两点为不同的两颗子树里,这时路径必然经过重心
- 路径的两点在同一颗子树里,路径不经过重心
- 路径的一端为重心,另一端为子树中的点。
接下来讨论如何求解:
对于第一种情况,我们可以用\(dfs\)求出重心到其他子树中所有点的距离,放到一个数组里,问题就转化成了,从数组里任取两个数,使得他们的和\(<=k\),双指针即可求解;
那么第一种情况的缺点在哪呢,在用双指针算法求解的时候,无法保证两个点是在不同的子树中的,根据容斥原理,我们要删除第二种情况。维护两个数组,一个表示除了重心之外的所有点到重心的距离,一个表示枚举到的某颗子树里的所有点到重心的距离,就转化成了同样的问题,调用同一个函数就可以求解。
对于第三种情况,只需要在前面两种情况求解的基础上增加一个特判就好了。
处理完当前重心后,递归处理每一颗子树,处理过程也同上。
代码:
// Problem: 树
// Contest: AcWing
// URL: https://www.acwing.com/problem/content/254/
// Memory Limit: 10 MB
// Time Limit: 3000 ms
//
// Powered by CP Editor (https://cpeditor.org)
#pragma GCC optimize(1)
#pragma GCC optimize(2)
#pragma GCC optimize(3,"Ofast","inline")
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
typedef pair<ll, ll>PLL;
typedef pair<int, int>PII;
typedef pair<double, double>PDD;
#define I_int ll
inline ll read()
{
ll x = 0, f = 1;
char ch = getchar();
while(ch < '0' || ch > '9')
{
if(ch == '-')f = -1;
ch = getchar();
}
while(ch >= '0' && ch <= '9')
{
x = x * 10 + ch - '0';
ch = getchar();
}
return x * f;
}
inline void out(ll x){
if (x < 0) x = ~x + 1, putchar('-');
if (x > 9) out(x / 10);
putchar(x % 10 + '0');
}
inline void write(ll x){
if (x < 0) x = ~x + 1, putchar('-');
if (x > 9) write(x / 10);
putchar(x % 10 + '0');
puts("");
}
#define read read()
#define closeSync ios::sync_with_stdio(0);cin.tie(0);cout.tie(0)
#define multiCase int T;cin>>T;for(int t=1;t<=T;t++)
#define rep(i,a,b) for(int i=(a);i<=(b);i++)
#define repp(i,a,b) for(int i=(a);i<(b);i++)
#define per(i,a,b) for(int i=(a);i>=(b);i--)
#define perr(i,a,b) for(int i=(a);i>(b);i--)
ll ksm(ll a, ll b)
{
ll res = 1;
while(b)
{
if(b & 1)res = res * a ;
a = a * a ;
b >>= 1;
}
return res;
}
const int maxn=10010,maxm=2*maxn;
int n,m;
int h[maxn],idx,e[maxm],ne[maxm],w[maxm];
bool st[maxn];
int p[maxn],q[maxn],qt;
void add(int u,int v,int ww){
e[idx]=v,w[idx]=ww,ne[idx]=h[u],h[u]=idx++;
}
int get_size(int u,int fa){
//求子树大小
if(st[u]) return 0;
int res=1;
for(int i=h[u];~i;i=ne[i]){
int v=e[i];
if(v!=fa)
res+=get_size(v,u);
}
return res;
}
int get_wc(int u,int fa,int tot,int &wc){
///求重心
if(st[u]) return 0;
int sum=1,ms=0;
for(int i=h[u];~i;i=ne[i]){
int v=e[i];
if(v!=fa){
int tt=get_wc(v,u,tot,wc);
ms=max(ms,tt);
sum+=tt;
}
}
ms=max(ms,tot-sum);
if(ms<=tot/2) wc=u;
return sum;
}
void get_dist(int u,int fa,int dist,int &qt){//求所有点到重心的距离
if(st[u]) return ;
q[qt++]=dist;
for(int i=h[u];~i;i=ne[i]){
int v=e[i];
if(v!=fa){
get_dist(v,u,dist+w[i],qt);
}
}
}
int get(int a[],int k){//双指针求解合法情况
sort(a,a+k);
int res=0;
for(int i=k-1,j=-1;i>=0;i--){
while(j+1<i&&a[j+1]+a[i]<=m) j++;
j=min(j,i-1);
res+=j+1;
}
return res;
}
int calc(int u){
if(st[u]) return 0; //说明已经被当过重心,已经删除,不考虑
int res=0;
get_wc(u,-1,get_size(u,-1),u);//求出重心
st[u]=true;//删除重心
int pt=0;///总的数组的下标清空
for(int i=h[u];~i;i=ne[i]){//遍历所有的出边
int v=e[i];
qt=0;
get_dist(v,-1,w[i],qt);//求出所有点到重心的距离
res-=get(q,qt);//第二种情况,减去两个端点是同一颗子树里的数量
for(int k=0;k<qt;k++){//第三种情况
if(q[k]<=m) res++;//一个点为重心
p[pt++]=q[k];
}
}
res+=get(p,pt);///第一种情况,加上所有点的合法情况
for(int i=h[u];~i;i=ne[i]){
int v=e[i];//递归处理每一个子树
res+=calc(v);
}
return res;
}
int main(){
while(cin>>n>>m){//多组输入
if(!n&&!m) break;
// cout<<n<<" "<<m<<endl;
memset(h,-1,sizeof h);//初始化
idx=0;
memset(st,0,sizeof st);
qt=0;
rep(i,1,n-1){//输入边
int u=read,v=read,w=read;
//cout<<u<<" "<<v<<" "<<w<<endl;
add(u,v,w);add(v,u,w);
}
printf("%d\n",calc(0));//输出答案
}
return 0;
}