• P1600 天天爱跑步[桶+LCA+树上差分]


    题目描述

    小c同学认为跑步非常有趣,于是决定制作一款叫做《天天爱跑步》的游戏。《天天爱跑步》是一个养成类游戏,需要玩家每天按时上线,完成打卡任务。

    这个游戏的地图可以看作一一棵包含 nn个结点和 n-1n−1条边的树, 每条边连接两个结点,且任意两个结点存在一条路径互相可达。树上结点编号为从11到nn的连续正整数。

    现在有mm个玩家,第ii个玩家的起点为 S_iS**i,终点为 T_iT**i 。每天打卡任务开始时,所有玩家在第00秒同时从自己的起点出发, 以每秒跑一条边的速度, 不间断地沿着最短路径向着自己的终点跑去, 跑到终点后该玩家就算完成了打卡任务。 (由于地图是一棵树, 所以每个人的路径是唯一的)

    小c想知道游戏的活跃度, 所以在每个结点上都放置了一个观察员。 在结点jj的观察员会选择在第W_jW**j秒观察玩家, 一个玩家能被这个观察员观察到当且仅当该玩家在第W_jW**j秒也理到达了结点 jj 。 小C想知道每个观察员会观察到多少人?

    注意: 我们认为一个玩家到达自己的终点后该玩家就会结束游戏, 他不能等待一 段时间后再被观察员观察到。 即对于把结点jj作为终点的玩家: 若他在第W_jW**j秒前到达终点,则在结点jj的观察员不能观察到该玩家;若他正好在第W_jW**j秒到达终点,则在结点jj的观察员可以观察到这个玩家。

    解析

    可能是目前为止我做过的最难的题了。。。(话说这题为什么不是黑的)

    再吐槽一句,考场上谁想得出这种神仙算法啊?!勉强拿个80暴力分都不容易啊!


    在讲这题前,首先要引入一个船新的概念:全局桶

    还有一些老东西:树上差分,LCA。

    【全局桶】

    全局桶,顾名思义,也是种桶,但是其维护的不是对象对应的下标,而是类似用桶来装权值(怎么说着那么奇怪)。精确的说,它维护的是对象产生的值出现的次数,而不是对象出现的次数。有点说不清楚,解释一下,比如这题,我们的全局桶将要维护的是每一个玩家的起点、终点对其跑步的路径所造成的影响,这个影响也就是起点、终点、路径上的观察员共同产生的一个值,下面会详细讲。我们会看到,它不维护树上某一个点的权值,而是作为总体的计数数组而存在。

    【LCA】

    即最近公共祖先,不必多言,看名字就知道啥意思。

    【树上差分】

    为了快速计算子树和,或维护子树的一些满足区间可减性(即满足该信息的前缀和可以相减,其可作为前缀和的逆运算)的信息时,树上差分可以很好胜任。类比序列上的差分,假设我们有一棵树(T),要在一条(ssim t)的路径上对每个点的权值加(k),我们可以对差分数组执行以下操作:在(s)(k),在(t)(k),在(lca(s,t))(k),在(father(lca(s,t)))(k),那么以(lca(s,t))为根的子树前缀和(从叶子节点向根节点做前缀和,根节点算在内)就是原来的子树和。(树剖或LCT可取而代之)


    解释完概念之后,我们来着手这道题的解决。

    分析题目,容易得出每个玩家其实就是对(ssim t)的路径上的点依次增加了(1sim len(s,t))的权值。故有暴力算法,对每个运动员,我们处理出他跑的路径对树的贡献,最后暴力dfs整棵树统计答案,复杂度(O(nm))。优化的好的话就能得到80分了。。。

    似乎和树上差分没什么关系?

    我们不妨转化一下思维,逆向看问题。

    对于树上每个节点,它都有可能作为某一对起点和终点的LCA,假设某个在(ssim t)上的观察员为(x)。而(x)对最终答案产生贡献仅当(deep[x]+w[x]=deep[s])(deep[x]+deep[s]-deep[lca(s,t)]*2=w[x])时。这是比较显而易见的表达方式,然后我们将其移项,使得其呈现规律性:(deep[s]=deep[x]+w[x])(deep[s]-deep[lca(s,t)]*2=w[x]-deep[x])。好的!现在关于(x)的式子都在等号左边了。这就方便了我们来维护这些等式,但这也是难点所在。

    根据以上分析,我们就得分(ssim lca(s,t))(lca(s,t) sim t)两条路径讨论了。


    我们先考察稍微简单一点的(ssim lca(s,t))这条路径。

    不难想到,对于一个玩家,它的起点(s)会给(ssim lca(s,t))上的所有点(x)造成(deep[s])的贡献,只有当(x)的贡献(deep[x]+w[x])与其相等时,(x)才能观察到该玩家,该点答案(+1)。换句话说,就是对该路径上所有点加上(deep[x])。你想到了什么?没错,树上差分,这是很自然的一个推导过程。假设这个差分数组为数组(c),此时我们对(c[s])(deep[s]),对(c[lca(s,t)])(deep[x])

    对于(ssim lca(s,t))这条路径呢,我们就给(c[lca(s,t)])减去(deep[s]-deep[lca(s,t)]*2),给(c[t])加上(deep[s]-deep[lca(s,t)]*2)。等等,LCA怎么算重了???没事,你只减一条路上LCA就行,你会发现在LCA这里,两个等式是等价的。复杂度因人而异,但肯定不会超过(O(nm))啦。

    这样做,你需要线段树合并,或者别的数据结构来维护这个差分数组。你愉快的发现,你T了。


    我们还没拿出来全局桶呢,不得不说,这个办法太秀了,也比较抽象。我们再逆向一下思维,考虑所有点作为LCA时的情况

    说白了其实就是上面那种方法逆过来搞,可以实现一种先处理所有玩家最后统计答案的算法,复杂度可以达到(O((n+m)log(n+m)))

    这个全局桶,它的好处就是去掉了许多无用的条件,只保留最少的我们所需要的条件。

    建立全局桶(bucket),对每种类型的贡献(比如(deep[x]+w[x]))进行计数。

    对树上每一个节点开4个多重集(比如vector),将所有经过该节点的玩家的起点放进一个集合,终点放进一个集合,将该节点作为一条路径的终点放进一个集合,将该节点作为一条路径的起点放进一个集合。首先起点和终点直接的路径时一定的,且一定会经过它们的LCA。那么对于某个节点(x),第一个集合也就对应着所有经过(x)的路径,或者说以(x)为LCA的起点的贡献,第二个集合对应着所有以(x)为LCA的终点的贡献。

    最后,dfs整颗树,计算树上前缀和。具体来说,对于(ssim lca(s,t)),就是递归进入在这条路径上的点(x)时,对于第一个集合中的点集(i_1),给(bucket[deep[i_1]]-1),对于第二个集合,也给(bucket[deep[i_2]]-1),对于第三个集合,给(bucket[deep[i_3]]+1),对第四个集合,给(bucket[deep[i_4]]+1)。等我们又回溯到(x)时,它的子树已经对桶完成更新,意即所有能被(x)观察到的玩家都被算进了(bucket[deep[x]+w[x]]),因此此时(bucket[deep[x]+w[x]])前后的差值就是(x)点对答案的贡献。

    发现又加重了,我们最后减掉就行。

    注意,对于(lca(s,t)sim t)这条路径,统计的时候桶下标会溢出,我们要离散化或者平移坐标处理。

    回顾树上差分方法,你会发现二者其实在做同一件事情,而后者真是惊艳到我了。

    这里给出一种全局桶的做法,简化了第四个集合(其实是“借鉴”题解的):

    参考代码

    #include<cstdio>
    #include<iostream>
    #include<cmath>
    #include<cstring>
    #include<ctime>
    #include<cstdlib>
    #include<algorithm>
    #include<queue>
    #include<set>
    #include<map>
    #define N 600010
    #define M 300010
    using namespace std;
    inline int read()
    {
    	int f=1,x=0;char c=getchar();
    	while(c<'0'||c>'9'){if(c=='-')f=-1;c=getchar();}
    	while(c>='0'&&c<='9'){x=x*10+c-'0';c=getchar();}
    	return x*f;
    }
    int f[31][N],n,m,a[N+M],c[N],dep[N],t,sum[N],ans[N];
    vector<int> v1[M],v2[M],v3[M];
    struct rec{
    	int next,ver;
    }g[N];
    int head[N],tot;
    struct node{
    	int s,t,dis,lca;
    }s[M];
    inline void add(int x,int y)
    {
    	g[++tot].ver=y;
    	g[tot].next=head[x],head[x]=tot;
    }
    inline void init()
    {
    	queue<int> q;
    	q.push(1);dep[1]=1;
    	while(q.size()){
    		int x=q.front();q.pop();
    		for(int i=head[x];i;i=g[i].next){
    			int y=g[i].ver;
    			if(dep[y]) continue;
    			dep[y]=dep[x]+1;
    			f[0][y]=x;
    			for(int j=1;j<=t;++j)
    				f[j][y]=f[j-1][f[j-1][y]];
    			q.push(y);
    		}	
    	}	
    }
    inline int lca(int x,int y)
    {
    	if(dep[y]>dep[x]) swap(x,y);
    	for(int j=t;j>=0;--j)
    		if(dep[y]<=dep[f[j][x]]) x=f[j][x];
    	if(x==y) return x;
    	for(int j=t;j>=0;--j)
    		if(f[j][x]!=f[j][y]) x=f[j][x],y=f[j][y];
    	return f[0][x];
    }
    inline void dfs1(int x,int fa)
    {
    	int cnt=c[dep[x]+a[x]+M];
    	for(int i=head[x];i;i=g[i].next){
    		int y=g[i].ver;
    		if(y==fa) continue;
    		dfs1(y,x);
    	}
    	c[dep[x]+M]+=sum[x];
    	ans[x]+=c[dep[x]+a[x]+M]-cnt;
    	for(int i=0;i<v1[x].size();++i)
    		c[v1[x][i]+M]--;
    }
    inline void dfs2(int x,int fa)
    {
    	int cnt=c[a[x]-dep[x]+M];
    	for(int i=head[x];i;i=g[i].next){
    		int y=g[i].ver;
    		if(y==fa) continue;
    		dfs2(y,x);
    	}	
    	for(int i=0;i<v3[x].size();++i)
    		c[v3[x][i]+M]++;
    	ans[x]+=c[a[x]-dep[x]+M]-cnt;
    	for(int i=0;i<v2[x].size();++i)
    		c[v2[x][i]+M]--;
    }
    int main()
    {
    	n=read(),m=read();
    	t=log2(n)+1;
    	for(int i=1;i<n;++i){
    		int u=read(),v=read();
    		add(u,v),add(v,u);
    	}
    	init();
    	for(int i=1;i<=n;++i) a[i]=read();
    	for(int i=1;i<=m;++i){
    		int f=read(),t=read();
    		s[i].s=f,s[i].t=t;
    		s[i].lca=lca(f,t);
    		s[i].dis=dep[f]+dep[t]-dep[s[i].lca]*2;
    		sum[f]++;
    		v1[s[i].lca].push_back(dep[f]);
    		v2[s[i].lca].push_back(dep[f]-dep[s[i].lca]*2);
    		v3[t].push_back(dep[f]-dep[s[i].lca]*2);
    	}
    	dfs1(1,-1);
    	dfs2(1,-1);
    	for(int i=1;i<=m;++i)
    		if(dep[s[i].s]==dep[s[i].lca]+a[s[i].lca]) ans[s[i].lca]--;
    	for(int i=1;i<=n;++i)
    		printf("%d ",ans[i]);
    	return 0;
    }
    
  • 相关阅读:
    08简单推导:手机尾号评分
    07简单推导:生日蜡烛
    06普通推导
    05简单推导:猴子吃桃
    简单推导
    03map用法
    List题目
    02List的使用
    01基础知识
    HDU
  • 原文地址:https://www.cnblogs.com/DarkValkyrie/p/11623100.html
Copyright © 2020-2023  润新知