[HNOI2015]开店-题解

题目【IN

目前只有开O2能过,STL的常数太大了,QWQ


题意简述,给你一棵树,有点权与边权,然后有很多询问,每次询问你点权在 L ∼ R L\sim R LR范围内的点到给点点 v v v的距离之和。


解法1:动态点分治

我们使用动态点分治来求取答案。

先构建出点分树,树高肯定是在 l o g n logn logn级别内的,然后对于一个分治点(重心),我们在上面记录三个值:

  • s 1 s1 s1表示该重心的子树点数
  • s 2 s2 s2表示该重心子树中的点到重心的距离和
  • s 3 s3 s3表示该重心的子树中的点到重心在点分树上的父亲的距离和

我们维护这三个值是因为计算答案时,对于子树内的点我们可以直接加上,对于祖先部分的点我们直接加上直接到祖先的距离,而对于其它部分的点,我们将其拆分为 u → l c a → v u\rightarrow lca\rightarrow v ulcav两条路来统计,然后再减去多余的。

那么对应的式子为
s 2 [ u ] + ∑ ( s 2 [ f [ p ] ] − s 3 [ p ] ) + ( s 1 [ f [ p ] ] − s 1 [ p ] ) × d i s t ( f [ p ] , u ) s2[u]+\sum (s2[f[p]]-s3[p])+(s1[f[p]]-s1[p])\times dist(f[p],u) s2[u]+(s2[f[p]]s3[p])+(s1[f[p]]s1[p])×dist(f[p],u)
其中 p p p开始 = u =u =u,然后不断往点分树的根跳即可。
但是在实际的统计中,我们这样实现:

  • 先加上子树到当前枚举的 p p p的距离和。
  • 然后如果当前的点 p ! = u p!=u p!=u,那么我们再加上当前点 p p p到点 u u u的距离乘以子树大小。
  • 如果点 p p p还有分治树上的父亲,那么我们还要减去当前点 p p p子树到其父亲距离和,还有点 p p p父亲 f [ p ] f[p] f[p]到点 u u u的距离乘以点 p p p的子树大小。

下面上一张图来详细的讲解一下为什么如此计算:
在这里插入图片描述

我们发现对于当前要求的点 u u u,如果我们 p = u p=u p=u,那么就直接加上当前子树到 p p p的距离和,由于还有父亲,所以我们看,当 p p p为父亲 v v v时,我们这样操作:

  • 先将 p p p所有子树内的点走到 p p p,代价为 s 2 s2 s2
  • 然后我们减去原来的 u u u的子树内的点多走的距离,就是它们先走到了 p p p,又从 p p p走回了 u u u,所以减去它们到 p p p的距离和,还有 p → u p\rightarrow u pu这条边被它们多走的距离。

那么操作的正确性就非常显然了。

而对于权值在 L ∼ R L\sim R LR的限制,我们可以利用前缀和的思想,求 1 ∼ L − 1 1\sim L-1 1L1 1 ∼ R 1\sim R 1R的答案相减即可得到,所以用一个 s e t set set或者 v e c t o r vector vector存下来即可(C++中的 S T L STL STL

那么每次询问时即可在点分树上跳父亲统计答案即可,复杂度为 l o g 2 n log^2n log2n,预处理点分树 n l o g 2 n nlog^2n nlog2n,预处理 L C A \rm LCA LCA我们用 R m q \rm Rmq Rmq来,所以为 n l o g n nlogn nlogn,查询 L C A \rm LCA LCA的复杂度变成 O ( 1 ) O(1) O(1),所以总的复杂度为 n l o g n + n l o g 2 n + m l o g 2 n nlogn+nlog^2n+mlog^2n nlogn+nlog2n+mlog2n,对于 2 e 5 2e5 2e5的数据再加上大常数,所以要开 O 2 O2 O2优化。

代码丑陋QAQ

#include<vector>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<iostream>
#define ll long long
using namespace std;
const int M=5e5+10;
int n,Q,A;
int age[M];
struct ss{
	int to,last;ll len;
	ss(){}
	ss(int a,int b,ll c):to(a),last(b),len(c){}
}g[M<<1];
int head[M],cnt;
void add(int a,int b,ll c){
	g[++cnt]=ss(b,head[a],c);head[a]=cnt;
	g[++cnt]=ss(a,head[b],c);head[b]=cnt;
}

struct node{
	int p;
	ll s1,s2,s3;
	node(){}
	node(int a,ll b,ll c,ll d):p(a),s1(b),s2(c),s3(d){}
	bool operator <(const node &a)const{return p<a.p;}
};
vector <node> rec[M];
#define pb push_back
typedef vector<node>::iterator iter;

int pos[M],tim,lgp[M],lg;
ll lcaq[25][M],dis[M];
//rmq求lca来求距离 
void dfs_rmq(int a,int b){
	lcaq[0][pos[a]=++tim]=dis[a];
	for(int i=head[a];i;i=g[i].last){
		if(g[i].to==b) continue;
		dis[g[i].to]=dis[a]+g[i].len;
		dfs_rmq(g[i].to,a);
		lcaq[0][++tim]=dis[a];
	}
}
void init_rmq(){
	lgp[2]=lgp[3]=1;
	for(int i=4;i<=tim;i++)lgp[i]=lgp[i>>1]+1;
	for(lg=1;(1ll<<lg)<=tim;++lg);
	for(int i=1;i<=lg;i++){
		for(int j=1;j+(1<<(i-1))<=tim;j++){
			lcaq[i][j]=min(lcaq[i-1][j],lcaq[i-1][j+(1<<(i-1))]);
		}
	}
}
ll dist(int a,int b){
	ll now=dis[a]+dis[b];
	a=pos[a];b=pos[b];
	if(!a||!b) return 0;
	if(a>b)swap(a,b);
	int k=lgp[b-a+1];
	ll lcaa=min(lcaq[k][a],lcaq[k][b-(1<<k)+1]);
	return now-(lcaa<<1);
}
int sze[M],f[M],son[M],totsze,root;
bool vis[M];
//找重心 
void findroot(int a,int fa){
	sze[a]=1;son[a]=0;
	for(int i=head[a];i;i=g[i].last){
		int v=g[i].to;
		if(v==fa||vis[v]) continue;
		findroot(v,a);
		sze[a]+=sze[v];
		if(sze[v]>son[a])son[a]=sze[v];
	}
	if(totsze-sze[a]>son[a])son[a]=totsze-sze[a];
	if(son[a]<son[root])root=a;
}

void getall(int a,int fa,int o){
	//求信息 
	rec[o].pb(node(age[a],1,dist(a,o),dist(a,f[o])));
	for(int i=head[a];i;i=g[i].last){
		if(g[i].to==fa||vis[g[i].to]) continue;
		getall(g[i].to,a,o);
	}
}

void find(int a){
	//找点分树,并求取信息 
	vis[a]=1;
	getall(a,0,a);
	rec[a].pb(node(-1,0,0,0));
	sort(rec[a].begin(),rec[a].end());
	for(int i=0,sz=rec[a].size()-1;i<sz;i++){
		rec[a][i+1].s1+=rec[a][i].s1;
		rec[a][i+1].s2+=rec[a][i].s2;
		rec[a][i+1].s3+=rec[a][i].s3;
	}//前缀和 
	for(int i=head[a];i;i=g[i].last){
		if(vis[g[i].to]) continue;
		root=0;totsze=sze[g[i].to];
		findroot(g[i].to,0);
		f[root]=a;
		find(root);
	}
}

node calc(int o,int l,int r){
	//以前缀和的形式保存:s1子树大小,s2子树到根,s3子树到根的分治父亲 
	if(!o) return node(0,0,0,0);
	iter a=upper_bound(rec[o].begin(),rec[o].end(),node(r,0,0,0));--a;
	iter b=upper_bound(rec[o].begin(),rec[o].end(),node(l-1,0,0,0));--b;
	ll s1=(a->s1)-(b->s1),s2=(a->s2)-(b->s2),s3=(a->s3)-(b->s3);
	return node(0,s1,s2,s3);
}

ll getans(int o,int l,int r){
	ll ans=0;
	for(int a=o;a;a=f[a]){
		node now=calc(a,l,r);
		ans+=now.s2;//首先由子树到当前的根的贡献 
		if(a!=o) ans+=now.s1*dist(a,o);//当前的点不是最开始询问点则需要加上子树到当前点a的多余贡献 
		if(f[a]) ans-=now.s3+now.s1*dist(f[a],o);//如果有分治父亲,那么需要减去子树到根父亲和根到父亲的贡献。 
		//因为到当前这个点的距离可以分成三种,一种是子树内的直接到,一种是祖先部分的也是直接到,一种为另外一子树内的点
		//需要分成两部分,u->lca->v,所以这样就能统计出所有的答案。 
	}
	return ans;
}
ll zans;
ll a,b,c;
int main(){
	scanf("%d%d%d",&n,&Q,&A);
	for(int i=1;i<=n;i++)scanf("%d",&age[i]);
	for(int i=1;i<n;i++){
		scanf("%lld%lld%lld",&a,&b,&c);
		add(a,b,c);
	}
	dfs_rmq(1,0);
	init_rmq();
	root=0;son[0]=M;totsze=n;
	findroot(1,0);
	find(root);
	while(Q--){
		scanf("%lld%lld%lld",&a,&b,&c);
		b=(1ll*b+zans)%A;c=(1ll*c+zans)%A;
		if(b>c)swap(b,c);
		zans=getans(a,b,c);
		cout<<zans<<'\n';
	}
	return 0;
} 
  • 动态点分治类似题目【IN-Luogu

解法2:主席树+树剖

我们可以发现,一个询问点的答案为如下式子:
( ∑ i = 1 n d i s ( i ) ) + n × d i s ( u ) − 2 × ∑ i = 1 n d i s ( l c a ( i , u ) ) \left(\sum\limits_{i=1}^ndis(i)\right)+n\times dis(u)-2\times\sum\limits_{i=1}^ndis(lca(i,u)) (i=1ndis(i))+n×dis(u)2×i=1ndis(lca(i,u))

其中 d i s ( i ) dis(i) dis(i)表示 i i i到根节点的距离。

这个式子和动态点分治中维护的信息是十分类似的,我们开始先加上所有点到根的距离,然后对于一部分点 v v v,它必须走 v → l c a → u v\rightarrow lca\rightarrow u vlcau这条路,所以还有根节点回来的路径为 n × d i s ( u ) n\times dis(u) n×dis(u),但是这样无疑会多算一些边,所以我们利用差分的思想,一条路影响是不会超过 l c a lca lca的上方,所以我们减去 l c a lca lca到根的距离,由于一条路是两个点,所以减两次,那么便是答案了。

对于信息的维护,我们用线段树+树链剖分即可,但是有点权 L ∼ R L\sim R LR的限制,所以我们用主席树即可做到维护, l o g 2 n log^2n log2n的查询。

但是此题空间限制较小(对于主席树来说),所以我们使用标记永久化,减少新的节点的开销。

下面上代码的连接我没有打主席树的代码,所以借用的他人的
IN-Luogu

复杂度 O ( n + n l o g 2 n + m l o g 2 n ) O(n+nlog^2n+mlog^2n) O(n+nlog2n+mlog2n),常数较为小一点(因为没有使用过多的 S T L STL STL)。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

VictoryCzt

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值