1.题目描述:
传送门:https://www.luogu.org/problem/show?pid=1600
超级大火题啊,简直比T3还要难(难很多)。
先总结一下做法:LCA+树上差分;
部分分做法:
为什么要先讲部分呢?因为满分算法就是由部分分算法推导来的。
这里要讲的部分分是第4档部分分:
如果是一条链,那么行走方向只有左右吧。
那么我们就把左右方向拆开来处理,这里以处理向右走(左走同理)为例:
当行走方向朝右时,一定有:S <= T 。 稍微想一想我们就可以发现:
若在 i 结点的观察员可以观察到第 j 个人,则一定有 W[ i ] == i - S[ j ],并且j走到i时还未到终点。
我们进行移项:S[ j ] = i - W[ i ],这是一个定值!!!!
这意味这,我们只用求解在 S[ j ] 到 T[ j ]中间,有多少个 结点k 满足:k - W[k] == S[ j ];
我们可以用差分来做这个东西:
记tot[ i ]表示当前以 i 结点为起点的且未到终点的玩家个数
我们预先在每一个终点处挂上其对应的起点位置。 然后从1结点开始处理,向后一直循环到N结点。
每次在处理一个结点 i 时,分为以下三步:
A.将以此结点为起点的玩家加入答案。
B.计算: Ans[i] = Ans[i] + tot[ i-W[i] ];
C.将以此结点为终点的玩家退出答案(即将其对应起点的tot--)
这样一路统计下去,然后反向处理朝朝左移动的情况。
根据定义可以得知,结点 i 的答案最终存在Ans[ i ]中
满分做法:
有了部分分的算法,我们就可以来尝试满分算法了。
其实,部分是给了一定的提示的:S = 1,T = 1—(@_@)
这其实就是暗示我们要吧路径拆成向上(S -> LCA)与向下(LCA -> T)两部分啦。
A.向上路径:
与之前部分分算分思考方式相同:
差分,但原先在S时加tot[S],在T时减tot[S],现在加减就要改变了。
显然根据树上差分思想,是要在LCA(S,T),T上做处理。
现在对于 i结点,若其观察员可以观察到某个人k,则要满足什么呢?
不难推出: deep[ i ] + W[ i ] == deep[ S[k] ];
由于方向一定是从S向LCA跑的,我们就要用差分使这一路径上的所有点都可以被贡献。
具体做法即在S时,tot[dep[S]]++;在LCA时,tot[dep[S]]–;
那么对于 i 结点,答案即更新为: Ans[ i ] = Ans[ i ] + tot[ dep[i]+W[i] ];
这里有一个要注意的地方:由于对于dep[k],可能统计了非LCA子树内结点,所以答案更新应该要作差:
int tardep = dep[u]+W[u],bef = tot[tardep];
BalaBalaBala......
Ans[u] = Ans[u] + tot[tardep] - bef;
B.向下路径:
向下路径的处理其实与向上处理的方法基本一样。
S[ k ] == dep[ i ] - W[ i ]时是可以观察到的。
但我们注意到dep[ i ] - W[ i ]有可能为一个负数,这就赋予其一定的特殊性。
对于这个问题,我的处理方法是建立一个”虚点”(深度)与之对应:
例如下面这棵树:
S = 3,T = 4 ,路径为3—->4,W[2] = 2
那么这时候2号结点可以观察的点满足:dep[S[k]] = dep[2]-W[2] = 0;
可是这里不存在第0层,那我们就人为搞一个第0层。
这样我们原先的tot[dep[S]]++,tot[dep[S]]–,也就变为tot[dep[虚点]]++/–;
所以在上面这个例子中,就为:tot[0]++ / tot[0]–
当然,此时所有的层数都要加一个很大的数(防止负数下标)。
int tardep = dep[u]-W[u]+BKS (BKS == 300000)
这里还有一个需要注意的地方:由于树的特点,我们统计答案必须从子节点向其父亲结点转移。
这也就要求我们将移动方向转动一下,即:
移动到T时,tot[ dep[对应S<虚点>] ]++;
移动到LCA时,tot[ dep[对应S<虚点>] ]--;
然后剩下的做法与向上路径完全相同。
具体实现代码(内详细注释):
#include<cstdio>
#include<cstdlib>
#include<iostream>
#include<algorithm>
#include<cstring>
#include<vector>
#define IL inline
#define maxn 300000
#define BKS 300000 //防止出现负下标所加的大数
using namespace std;
inline int gi(){
int date = 0,m = 1; char ch = 0;
while(ch!='-'&&(ch<'0'||ch>'9'))ch = getchar();
if(ch == '-'){m = -1 ; ch = getchar();}
while('0'<=ch && ch<='9'){date = date * 10 + ch - '0';ch = getchar();}
return date * m;
}
inline void write(int ac,int deep){
if(ac>9)write(ac/10,deep+1);
putchar(ac%10+'0'); if(!deep)putchar(' ');
}
int head[maxn],cnt; //建图链式前向星
struct Road{int to,next;}t[2*maxn+2]; //建图粮食前向星
vector<int>Insert[maxn],Delete[maxn]; //每个结点所挂的对应起点位置(添加、删除)
int fa[maxn][30],dis[maxn],dep[maxn]; //倍增找父亲,距离根节点的距离,所在深度
int tot[2*maxn+5]; //第i层为起点的玩家个数
int W[maxn],Ans[maxn],N,M; //题中所述w,第i个结点的答案,题中所述N、M
struct Player
{
int S,T,lca,len;
IL void Read(){S = gi(); T = gi();}
IL void LCA() //求解S、T的LCA
{
int f1 = S,f2 = T;
if(dep[f1]<dep[f2])swap(f1,f2);
for(int i = log2(dep[f1]+1); i>=0 ; i--)
if( (1<<i)&(dep[f1]-dep[f2]) )f1 = fa[f1][i];
if(f1 == f2){lca = f1; return;}
for(int i = log2(f1)+1; i >= 0 ; i --){
if(fa[f1][i] == fa[f2][i])continue;
f1 = fa[f1][i]; f2 = fa[f2][i];
}lca = fa[f1][0];
}
IL void Getlen(){len = dis[S]+dis[T]-2*dis[lca];} //求解S——>T的距离
}p[maxn];
//倍增顺便求解dis,dep,fa;
void Dfs(int u,int fth,int deep,int Dist)
{
dis[u] = Dist;
dep[u] = deep; fa[u][0] = fth;
for(int i = 1; i <= log2(deep); i ++)
fa[u][i] = fa[ fa[u][i-1] ][i-1];
for(int i = head[u];i;i = t[i].next)
{
int v = t[i].to;
if(v == fth)continue;
Dfs(v,u,deep+1,Dist+1);
}return;
}
//求解向上路径部分
void DfsUp(int u,int fth)
{
int tardep = dep[u]+W[u],bef = tot[tardep]; //bef用于去除非当前子树的贡献
for(int i = head[u];i;i = t[i].next)
{
int v = t[i].to;
if(v == fth)continue;
DfsUp(v,u);
}
for(int i = 0; i < Insert[u].size(); i ++)tot[Insert[u][i]]++; //以u为起点的加入贡献中
Ans[u] = Ans[u] + tot[tardep] - bef; //统计贡献
for(int i = 0; i < Delete[u].size(); i ++)tot[Delete[u][i]]--; //去除以u为终点的贡献
}
//求解向下路径部分( T时++,LCA时--,注意从下往上统计答案 )
void DfsDown(int u,int fth)
{
int tardep = dep[u]-W[u]+BKS,bef = tot[tardep]; //+BKS:建立一个虚点
for(int i = head[u];i;i = t[i].next)
{
int v = t[i].to;
if(v == fth)continue;
DfsDown(v,u);
}
for(int i = 0; i < Insert[u].size(); i ++)tot[Insert[u][i]+BKS]++;
Ans[u] = Ans[u] + tot[tardep] - bef;
for(int i = 0; i < Delete[u].size(); i ++)tot[Delete[u][i]+BKS]--;
}
int main()
{
N = gi(); M = gi();
for(int i = 1; i <= N-1; i ++)
{
int u = gi(),v = gi();
t[++cnt] = (Road){v,head[u]}; head[u] = cnt;
t[++cnt] = (Road){u,head[v]}; head[v] = cnt;
}
for(int i = 1; i <= N ; i ++)W[i] = gi();
Dfs(1,0,1,0);
for(int i = 1; i <= M ; i ++){
p[i].Read();
p[i].LCA();
p[i].Getlen();
}
//Step1: 处理向上的路径:
for(int i = 1; i <= N ; i ++){
Insert[i].clear();Delete[i].clear();}
memset(tot,0,sizeof(tot));
for(int i = 1; i <= M ; i ++)
{
Insert[p[i].S].push_back( dep[p[i].S] ); //计算每个点所挂的加入内容
Delete[p[i].lca].push_back( dep[p[i].S] ); //计算每个点所挂的删除内容
}
DfsUp(1,0);
//Step2: 处理向下的路径:
for(int i = 1; i <= N ; i ++){
Insert[i].clear();Delete[i].clear();}
memset(tot,0,sizeof(tot));
for(int i = 1; i <= M ; i ++)
{
Insert[p[i].T].push_back( dep[p[i].T]-p[i].len ); //计算每个点所挂的加入内容
Delete[p[i].lca].push_back( dep[p[i].T]-p[i].len ); //计算每个点所挂的删除内容
}
DfsDown(1,0);
//Step3:LCA在分解为上下时 都算了一遍,去重:
for(int i = 1;i <= M ; i ++)
if(dep[p[i].lca]+W[p[i].lca] == dep[p[i].S])
Ans[p[i].lca]--;
//Step4:输出答案啦......
for(int i = 1;i <= N ; i ++)write(Ans[i],0);
return 0;
}