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;
}

posted @ 2021-07-28 10:02  OvO1  阅读(39)  评论(0编辑  收藏  举报