zoukankan      html  css  js  c++  java
  • NOIP2016(D1T2)天天爱跑步题解

    首先声明这不是一篇算法独特的题解,仍然是“LCA+桶+树上差分”,但这篇题解是为了让很多很多看了很多题解仍然看不懂的朋友们看懂的,其中就包括我,我也在努力地把解题的“思维过程”呈现出来,希望能帮助到别人。实在是佩服那些考场AC的大牛,再次向你们献上敬意!

    1. 第一步
    • 首先可以初步判断这个题肯定要计算LCA,方法有倍增/Tarjan-DFS,我们就写个简单的倍增吧,使用链式前向星存储边。
    • 选择1号结点开始dfs,别的结点也可以
    • dfs过程中计算fa[][]数组(fa[x][i]表示 (x) 结点的 (2^i) 代祖先是谁)和deep[]数组(deep[x]表示结点 (x) 在树中的深度)
    #include<bits/stdc++.h>
    using namespace std;
    const int SIZE=300000;
    int n, m, tot, h[SIZE], deep[SIZE], fa[SIZE][20], w[SIZE];      //w[i]表示i结点出现观察员的时间
    struct edge
    {
    	int to, next;
    }E[SIZE*2], e1[SIZE*2], e2[SIZE*2];                             //边集数组e1,e2留待备用
    
    void add(int x, int y)                                          //加边函数
    {
    	E[++tot].to=y;
    	E[tot].next=h[x];
    	h[x]=tot;
    }
    
    void dfs1(int x)                                                //dfs的过程中完成“建树”,预处理fa[][]数组, 计算deep[]数组
    {
    	for(int i=1; (1<<i)<=deep[x]; i++)
    		fa[x][i]=fa[fa[x][i-1]][i-1];                           //x的2^i代祖宗就是x的2^{i-1}代祖宗的2^{i-1}代祖宗
    	for(int i=h[x]; i; i=E[i].next)
    	{
    		int y=E[i].to;
    		if(y==fa[x][0])	continue;                               //如果y是父结点,跳过
    		fa[y][0]=x;
    		deep[y]=deep[x]+1;
    		dfs1(y);
    	}
    }
    
    int get_lca(int x, int y)                                      //计算x和y的最近公共祖先
    {
    	if(x==y) return x;                                         //没有这一行,遇到 lca(x, x) 这样的询问时会挂掉
    	if(deep[x]<deep[y]) swap(x, y);                            //保持x的深度大于y的深度
    	int t=log(deep[x]-deep[y])/log(2);
    	for(int i=t; i>=0; i--)                                    //x向上跳到和y同样的深度
    	{
    		if(deep[fa[x][i]]>=deep[y])
    			x=fa[x][i];
    		if(x==y)
    			return x;
    	}
    	t=log(deep[x])/log(2);
    	for(int i=t; i>=0; i--)                                    //x和y一起向上跳
    	{
    		if(fa[x][i]!=fa[y][i])
    			x=fa[x][i], y=fa[y][i];
    	}
    	return fa[x][0];
    }
    
    int main()                                                     //先把主函数写上一部分
    {
    	scanf("%d%d", &n, &m);
    	for(int i=1; i<n; i++)
    	{
    		int u, v;
    		scanf("%d%d", &u, &v);
    		add(u, v);
    		add(v, u);
    	}
    	deep[1]=1;
    	fa[1][0]=1;
    	dfs1(1);
    	for(int i=1; i<=n; i++) scanf("%d", &w[i]);
    	
        /////////////////////////////////////////////////////////////
        ////////////////////////未完待续///////////////////////////
        /////////////////////////////////////////////////////////////
        
    	return 0;
    }
    
    2. 第二步

    大概分析一下,m个玩家对应m条路径,有了起点和终点的 lca 后,如果我们模拟这个过程:

    直觉

    • 从起点 (S_i) 跑到 (LCA) 在树长得很匀称的情况下为 (O(lgn))
    • 从起点 (LCA) 跑到 (T_i) 在树长得很匀称的情况下为 (O(lgn))
    • 因此,模拟一个玩家的跑步过程为 (O(lgn)),m个玩家为 (O(mlgn))
    • 理想情况下是可行的,但现实就是不理想
    • 题目清楚告诉你,树会退化成一条链,因此模拟一个过程变成 (O(n)),总的就是。。。(O(mn)),必挂无疑
    • 此法不是正解!

    尝试

    • 我们能不能改变模拟跑步的过程,从 (O(n)) 优化到 (O(lgn)) 呢?思前想后不可能,有 (n) 个观察员矗在那里,你可以对哪个视而不见?
    • 路已走到尽头

    转换

    • 这时候需要放大招,转换思想!或许解决问题的思路压根就不是一个玩家一个玩家模拟,而是整体处理呢?
    • 也就是说,我们不枚举每个运动员而是枚举每个观察员i,看看哪些结点会为这个观察员i做贡献(刚好在(w_i)秒跑到他这儿)。
    • 枚举观察员的过程就是DFS整颗树的过程,我们可以在 (O(n)) 内搞定!
    • 对于观察员i,哪些人会为他做贡献呢?

    深入分析

    • 对于结点 (P), 如果他位于一条起点、终点分别为 (s_i)(t_i) 的跑步路径上,如何判断这名选手会不会为 (P) 作贡献呢?
    • 分情况考虑
    • 如果 (P) 是在从 (s_i)(LCA) 的路上,如下图:

    • 我们可以得出结论:当起点 $ s_i $ 满足 $ deep[s_i]=w[P]+deep[P] $时,起点 (s_i)会为 (P) 观察员做一个贡献(运动员从(s_i)出发,可以被(P)处的观察员在(w[P])秒看到)

    • 如果 (P) 是在从 (LCA)(t_i) 的路上,如下图:

    • 定义 (dist[s_i, t_i])为从 (s_i)出发到(t_i)的路径长度,如果运动员从(s_i)出发,可以被(P)处的观察员在(w[P])秒观察到,可以由上图得出以下式子:
    • (dist[s_i, t_i]-w[P]=deep[t_i]-deep[P]),移项后得到:
    • $ dist[s_i, t_i]-deep[t_i]=w[P]-deep[P] $
    • 我们可以得出结论:当终点 $ t_i $ 满足 $ dist[s_i, t_i]-deep[t_i]=w[P]-deep[P] $时,终点 (t_i)会为 (P) 观察员做一个贡献
    • 做一个重要的总结:上行过程中,满足条件的起点可以做贡献,下行过程中,满足条件的终点可以做贡献,但无论是哪一种情形,能对 (P) 做贡献的起点或终点一定都在以(P)为根的子树上,这使得可以在DFS回溯的过程中处理以任意节点为根的子树。
    3. 第三步

    如何统计子树贡献

    • 递归以(P)为根的子树时,可以统计出其子树中所有的起点和终点对它的贡献
    • 这里又需要转换
    • 子树中有的起点和终点对(P)产生了贡献,有些不对其产生贡献但对(P)以外的结点产生了贡献
    • 所以我们不能枚举每个点(子树根),找子树中哪些点对其产生贡献,这样复杂度就上去了
    • 而是对于树上的任何一个起点和终点,把其产生的贡献放在桶里面,回溯到子树根的时候再到桶里面查询结果
    • 有人产生疑问了,也是很多人看不懂这里桶用法的地方,疑问如图:

    • (c)点产生贡献放在桶的(deep[c])位置,计算(b)点获得的贡献时当然是从(bucket1[deep[b]+w[b]])位置获取,于是得到1个贡献,你发现(a)结点也是用的同一个桶,这个还好,因为(c)确实给他做了贡献,可是(e)点呢?他是不应该获得贡献的!既然我会给和我无关的结点做贡献,那么其它无关的结点难免也会给我做贡献!
    • 问题总结一下,对于一个点(P)来说,究竟哪些点在桶里面产生的贡献才是有效的。
    • 答案是:(P)为根递归整颗子树过程中在桶内产生的差值才是有效的

    还要考虑一种情况

    • 先看图:

    • 看懂了吗?对于以(P)为根的内部路径(不经过(P)),这条路径的起点和终点产生的贡献是不应该属于(P)
    • 所以dfs过程中,在统计当前结点作为起点和终点所产生的贡献后,继而计算出当前结点作为“根”上的差值后,在回溯过程中,一定要减去以当前结点为(LCA)的起点、终点在桶里产生的贡献,这部分贡献在离开这个子树后就没有意义了。

    代码说明

    • e1,tot1,h1,add1是使用链式前向星的方法存储每个结点作为终点对应的路径集合
    • e2,tot2,h2,add2是使用链式前向星的方法存储每个结点作为LCA对应的路径集合
    • b1,b2是两组桶,分别用于上行阶段下行阶段的贡献统计
    • js[SIZE]用于统计以每个结点作为起点的路径条数
    • dist[SIZE], s[SIZE], t[SIZE]用于统计m条路径对应的长度,起点和终点信息
    • ans[SIZE]存储最后输出的答案,是每个结点观察员看到的人数
    int tot1, tot2, h1[SIZE], h2[SIZE];
    void add1(int x, int y)
    {
    	e1[++tot1].to=y;
    	e1[tot1].next=h1[x];
    	h1[x]=tot1;
    }
    
    void add2(int x, int y)
    {
    	e2[++tot2].to=y;
    	e2[tot2].next=h2[x];
    	h2[x]=tot2;
    }
    
    int b1[SIZE*2], b2[SIZE*2], js[SIZE], dist[SIZE], s[SIZE], t[SIZE], ans[SIZE];
    
    void dfs2(int x)
    {
    	int t1=b1[w[x]+deep[x]], t2=b2[w[x]-deep[x]+SIZE];      //递归前先读桶里的数值,t1是上行桶里的值,t2是下行桶的值
    	for(int i=h[x]; i; i=E[i].next)                         //递归子树
    	{
    		int y=E[i].to;
    		if(y==fa[x][0]) continue;
    		dfs2(y);
    	}
    	b1[deep[x]]+=js[x];                                     //上行过程中,当前点作为路径起点产生贡献,入桶
    	for(int i=h1[x]; i; i=e1[i].next)                       //下行过程中,当前点作为路径终点产生贡献,入桶
    	{
    		int y=e1[i].to;
    		b2[dist[y]-deep[t[y]]+SIZE]++;
    	}
    	ans[x]+=b1[w[x]+deep[x]]-t1+b2[w[x]-deep[x]+SIZE]-t2;   //计算上、下行桶内差值,累加到ans[x]里面
    	for(int i=h2[x]; i; i=e2[i].next)                       //回溯前清除以此结点为LCA的起点和终点在桶内产生的贡献,它们已经无效了
    	{
    		int y=e2[i].to;
    		b1[deep[s[y]]]--;                                   //清除起点产生的贡献
    		b2[dist[y]-deep[t[y]]+SIZE]--;                      //清除终点产生的贡献
    	}
    }
    
    int main()
    {
    ////////////////重复部分跳过////////////
    ////////////////文末提供完整代码////////
    	for(int i=1; i<=m; i++)                                 //读入m条询问
    	{
    		scanf("%d%d", &s[i], &t[i]);
    		int lca=get_lca(s[i], t[i]);                        //求LCA
    		dist[i]=deep[s[i]]+deep[t[i]]-2*deep[lca]];         //计算路径长度
    		js[s[i]]++;                                         //统计以s[i]为起点路径的条数,便于统计上行过程中该结点产生的贡献
    		add1(t[i], i);                                      //第i条路径加入到以t[i]为终点的路径集合中
    		add2(lca, i);                                       //把每条路径归到对应的LCA集合中
    		if(deep[lca]+w[lca]==deep[s[i]]) ans[lca]--;        //见下面的解释
    	}
    	dfs2(1);                                                //dfs吧!
    	for(int i=1; i<=n; i++) printf("%d ", ans[i]); 
    	return 0;
    }
    

    一些重要补充

    • 上述代码中有一行未加解释if(deep[lca]+w[lca]==deep[s[i]]) ans[lca]--;
    • 考虑路径是这样的,如图:

    • 这个图可能不太好懂,意思是:

    • 如果路径起点或终点刚好为LCA且LCA处是可观察到运动员的,那么我们在上行统计过程中和下行统计过程中都会对该LCA产生贡献,这样就重复计数一次!

    • 好在这种情况很容易发现,我们提前预测到,对相应的结点进行ans[x]--即可。

    • 此外,在使用第二个桶时,下标是w[x]-deep[x]会成为负数,所以使用第二个桶时,下标统一+SIZE,向右平移一段区间,防止下溢。

    4. 结束

    我不知道自己说清楚没有,但愿大家不要拍砖头!下面是完整代码

    #include<bits/stdc++.h>
    using namespace std;
    const int SIZE=300000;
    int n, m, tot, h[SIZE], deep[SIZE], fa[SIZE][20], w[SIZE];
    struct edge
    {
    	int to, next;
    }E[SIZE*2], e1[SIZE*2], e2[SIZE*2];
    
    void add(int x, int y)
    {
    	E[++tot].to=y;
    	E[tot].next=h[x];
    	h[x]=tot;
    }
    
    int tot1, tot2, h1[SIZE], h2[SIZE];
    void add1(int x, int y)
    {
    	e1[++tot1].to=y;
    	e1[tot1].next=h1[x];
    	h1[x]=tot1;
    }
    void add2(int x, int y)
    {
    	e2[++tot2].to=y;
    	e2[tot2].next=h2[x];
    	h2[x]=tot2;
    }
    
    void dfs1(int x)
    {
    	for(int i=1; (1<<i)<=deep[x]; i++)
    		fa[x][i]=fa[fa[x][i-1]][i-1];
    	for(int i=h[x]; i; i=E[i].next)
    	{
    		int y=E[i].to;
    		if(y==fa[x][0])	continue;
    		fa[y][0]=x;
    		deep[y]=deep[x]+1;
    		dfs1(y);
    	}
    }
    
    int get_lca(int x, int y)
    {
    	if(x==y) return x;
    	if(deep[x]<deep[y]) swap(x, y);
    	int t=log(deep[x]-deep[y])/log(2);
    	for(int i=t; i>=0; i--)
    	{
    		if(deep[fa[x][i]]>=deep[y])
    			x=fa[x][i];
    		if(x==y)
    			return x;
    	}
    	t=log(deep[x])/log(2);
    	for(int i=t; i>=0; i--)
    	{
    		if(fa[x][i]!=fa[y][i])
    			x=fa[x][i], y=fa[y][i];
    	}
    	return fa[x][0];
    }
    
    int b1[SIZE*2], b2[SIZE*2], js[SIZE], dist[SIZE], s[SIZE], t[SIZE], l[SIZE], ans[SIZE];
    void dfs2(int x)
    {
    	int t1=b1[w[x]+deep[x]], t2=b2[w[x]-deep[x]+SIZE];
    	for(int i=h[x]; i; i=E[i].next)
    	{
    		int y=E[i].to;
    		if(y==fa[x][0]) continue;
    		dfs2(y);
    	}
    	b1[deep[x]]+=js[x];
    	for(int i=h1[x]; i; i=e1[i].next)
    	{
    		int y=e1[i].to;
    		b2[dist[y]-deep[t[y]]+SIZE]++;
    	}
    	ans[x]+=b1[w[x]+deep[x]]-t1+b2[w[x]-deep[x]+SIZE]-t2;
    	for(int i=h2[x]; i; i=e2[i].next)
    	{
    		int y=e2[i].to;
    		b1[deep[s[y]]]--;
    		b2[dist[y]-deep[t[y]]+SIZE]--;
    	}
    }
    
    int main()
    {
    	scanf("%d%d", &n, &m);
    	for(int i=1; i<n; i++)
    	{
    		int u, v;
    		scanf("%d%d", &u, &v);
    		add(u, v);
    		add(v, u);
    	}
    	deep[1]=1;
    	fa[1][0]=1;
    	dfs1(1);
    	for(int i=1; i<=n; i++) scanf("%d", &w[i]);
    	for(int i=1; i<=m; i++)
    	{
    		scanf("%d%d", &s[i], &t[i]);
    		int lca=get_lca(s[i], t[i]);
    		dist[i]=deep[s[i]]+deep[t[i]]-2*deep[lca];
    		js[s[i]]++;
    		add1(t[i], i);
    		add2(lca, i);
    		if(deep[lca]+w[lca]==deep[s[i]]) ans[lca]--;
    	}
    	dfs2(1);
    	for(int i=1; i<=n; i++) printf("%d ", ans[i]);
    	return 0;
    }
    
  • 相关阅读:
    c# System.Object类和数据的安全转型
    计算机内存的组织方式
    c# ref和out参数
    C# 复制值类型的变量和类
    PCB 布线,直角线,差分线,蛇形线
    c# 静态方法和数据
    c# 类的知识
    appium中从activity切换到html
    No Desktop License Servers available to provide a license
    adb命令连接Android模拟器夜神模拟器
  • 原文地址:https://www.cnblogs.com/lfyzoi/p/10221884.html
Copyright © 2011-2022 走看看