题目传送门:https://www.luogu.org/problemnew/show/P1600

感觉这两天在处理边界问题上有点神志不清......为了从80的暴力变成100,花了整整一个下午+一个晚上的时间(还好最后还是搞了出来)

题目大意:给你一棵树N个点的无根树,有M个人要从Si走到Ti,行走速度为每秒一条边。对于树上任意节点i,求出所有经过该点时行走时间恰好为Wi的路径数量。且这M个人到达终点后下一秒会立即消失。

先来说说暴力,写得妙的话,这题暴力可以拿80分(是不是很良心??)

这种题目考场上最好还是打暴力

25分(1到5号点):直接对于所有的任务,模拟从S跑到T,随后直接统计答案即可。

 bool dfs(int x,int fa,int t,int dep){
if(x==t){
if(dep==w[x]) ans[x]++;
return ;
}
for(int i=head[x];i;i=e[i].next) if(e[i].u!=fa){
if(dfs(e[i].u,x,t,dep+)){
if(dep==w[x]) ans[x]++;
return ;
}
}
return ;
}

25分暴力

另一个15分(6,7,8号点):我们可以将这M个人分为两类(Si>Ti和Si≤Ti),对于一个点i,我们通过二分统计Sj==i-W[i]和Sj==W[i]-i的数量,随后直接输出即可。

     if(op==){
dep[]=-; dfs(,);
for(int i=;i<=m;i++){
if(!in[a[i].t])
qx.push(a[i].t);
have[a[i].t]++;
in[a[i].t]=;
}
while(!qx.empty()){
node u=qx.top(); qx.pop();
int x=u.x;
if(!in[f[x]]) qx.push(f[x]),in[f[x]]=;
have[f[x]]+=have[x];
}
for(int i=;i<=n;i++)
if(w[i]==dep[i])
ans[i]=have[i];
for(int i=;i<=n;i++) printf("%d ",ans[i]);
return ;
}

一条链的暴力

另一个20分(9,10,11,12号点):考虑Si=1,不妨设整棵树的根为1,随后求出所有点的深度dep[i]。不难发现观察员i能观察到的选手必从其父亲方向跑来。开一个优先队列,以dep[Ti]为关键字,存储所有的Ti,借此维护f数组,f[i]表示从根节点跑来的人中经过点i(包括以i为终点的人)的人的数量。若点i满足w[i]==dep[i],则ans[i]=f[i],否则ans[i]=0。

 if(op==){
dep[]=-; dfs(,);
for(int i=;i<=m;i++){
if(!in[a[i].t])
qx.push(a[i].t);
have[a[i].t]++;
in[a[i].t]=;
}
while(!qx.empty()){
node u=qx.top(); qx.pop();
int x=u.x;
if(!in[f[x]]) qx.push(f[x]),in[f[x]]=;
have[f[x]]+=have[x];
}
for(int i=;i<=n;i++)
if(w[i]==dep[i])
ans[i]=have[i];
for(int i=;i<=n;i++) printf("%d ",ans[i]);
return ;
}

Si=1的20分暴力

另一个20分(13,14,15,16号点):由于Ti=1,假设整棵树根节点为1,不难发现所有观察员能观察到的选手必从其子节点跑来。先对整棵树进行广搜,处理dfs序和一个类似bfs序的东西(数组l和输入r),以及bfs序中选手出现次数的前缀和,其中l[x]表示深度为x的点在bfs序中最早出现的位置,r[x]表示深度为x的点在bfs序中最晚出现的位置。借助这些预处理出的数组,我们就可以通过二分在O(logn)的时间内求出以i为根的字树中深度为dep[i]+w[i]的节点数量,即答案。

 bool cmpd(int x,int y){
return dfn[x]<=dfn[y];
}
bool cmpl(int x,int y){
return low[x]<=low[y];
}
if(op==){
dep[]=-; dfs(,); t=;
for(int i=;i<=m;i++) have[a[i].s]++;
int last=,lastdep=-; q.push();
while(!q.empty()){
int u=q.front(); q.pop();
num[++t]=u;
numsum[t]=numsum[t-]+have[u];
if(dep[u]!=dep[last]){
r[dep[last]]=t-; l[dep[u]]=t;
lastdep=dep[last]; last=u;
}
for(int i=head[u];i;i=e[i].next) if(dep[e[i].u]!=lastdep){
q.push(e[i].u);
}
}
for(int i=;i<=n;i++){
int ceng=dep[i]+w[i];
if(!l[ceng]) continue;
int ll=lower_bound(num+l[ceng],num+r[ceng]+,i,cmpd)-num;
int rr=lower_bound(num+l[ceng],num+r[ceng]+,i,cmpl)-num;
if(ll>=rr) continue;
ans[i]=numsum[rr-]-numsum[ll-];
}
for(int i=;i<=n;i++) printf("%d ",ans[i]);
return ;
}

Ti=1的20分暴力

完整的80分组合暴力代码如下:

 #include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<queue>
#define M 310000
#define lowbit(x) (x&(-x))
using namespace std;
struct edge{int u,next;}e[M*]={}; int head[M]={},use=;
void Add(int x,int y){use++;e[use].u=y;e[use].next=head[x]; head[x]=use;}
struct st{
int s,t; st(){s=t=;}
st(int ss,int tt){s=ss; t=tt;}
friend bool operator <(st a,st b){if(a.s==b.s) return a.t<b.t; return a.s<b.s;}
}a[M],b[M];
int n,m,w[M]={},ans[M]={},la[M]={},ra[M]={},lb[M]={},rb[M]={};
bool dfs(int x,int fa,int t,int dep){
if(x==t){
if(dep==w[x]) ans[x]++;
return ;
}
for(int i=head[x];i;i=e[i].next) if(e[i].u!=fa){
if(dfs(e[i].u,x,t,dep+)){
if(dep==w[x]) ans[x]++;
return ;
}
}
return ;
}
int dfn[M]={},low[M]={},t=,dep[M]={};
int p[M]={},have[M]={},num[M]={},numsum[M]={},l[M]={},r[M]={};
void add(int x,int k){
for(int i=x;i<=n;i+=lowbit(i)) p[i]+=k;
}
int sum(int x){
int k=; for(int i=x;i;i-=lowbit(i)) k+=p[i];
return k;
}
int f[M]={};
void dfs(int x,int fa){
f[x]=fa;
dep[x]=dep[fa]+; dfn[x]=++t;
for(int i=head[x];i;i=e[i].next) if(e[i].u!=fa)
dfs(e[i].u,x);
low[x]=t;
}
queue<int> q; bool cmpd(int x,int y){
return dfn[x]<=dfn[y];
}
bool cmpl(int x,int y){
return low[x]<=low[y];
}
struct node{
int x; node(){x=;}
node(int xx){x=xx;}
friend bool operator <(node a,node b){return dep[a.x]<dep[b.x];}
};
priority_queue<node> qx;
bool in[M]={};
int main(){
freopen("running.in","r",stdin);
freopen("running.out","w",stdout);
scanf("%d%d",&n,&m);
for(int i=;i<n;i++){
int x,y; scanf("%d%d",&x,&y);
Add(x,y); Add(y,x);
}
for(int i=;i<=n;i++) scanf("%d",w+i);
for(int i=;i<=m;i++) scanf("%d%d",&a[i].s,&a[i].t);
int op=n%;
if(op<=||op>=){
for(int i=;i<=m;i++) dfs(a[i].s,,a[i].t,);
for(int i=;i<=n;i++) printf("%d ",ans[i]);
return ;
}
if(op==){
sort(a+,a+m+);
int na=,nb=;
for(int i=;i<=n;i++){
if(a[i].s<=a[i].t) a[++na]=a[i];
else b[++nb]=a[i];
}
a[na+]=st(,);
for(int i=;i<=na+;i++){
if(a[i].s==a[i-].s) continue;
ra[a[i-].s]=i-; la[a[i].s]=i;
}
for(int i=;i<=nb+;i++){
if(b[i].s==b[i-].s) continue;
rb[b[i-].s]=i-; lb[b[i].s]=i;
}
for(int i=;i<=n;i++){
int lid=i-w[i],rid=i+w[i];
if(lid>&&la[lid]){
int id=lower_bound(a+la[lid],a+ra[lid]+,st(lid,i))-a;
if(id==ra[lid]+) goto loop;
ans[i]+=ra[lid]-id+;
}
loop:;
if(rid<=n&&lb[rid]){
int id=upper_bound(b+lb[rid],b+rb[rid]+,st(rid,i))-b;
ans[i]+=id-lb[rid];
}
}
for(int i=;i<=n;i++) printf("%d ",ans[i]);
return ;
}
if(op==){
dep[]=-; dfs(,);
for(int i=;i<=m;i++){
if(!in[a[i].t])
qx.push(a[i].t);
have[a[i].t]++;
in[a[i].t]=;
}
while(!qx.empty()){
node u=qx.top(); qx.pop();
int x=u.x;
if(!in[f[x]]) qx.push(f[x]),in[f[x]]=;
have[f[x]]+=have[x];
}
for(int i=;i<=n;i++)
if(w[i]==dep[i])
ans[i]=have[i];
for(int i=;i<=n;i++) printf("%d ",ans[i]);
return ;
}
if(op==){
dep[]=-; dfs(,); t=;
for(int i=;i<=m;i++) have[a[i].s]++;
int last=,lastdep=-; q.push();
while(!q.empty()){
int u=q.front(); q.pop();
num[++t]=u;
numsum[t]=numsum[t-]+have[u];
if(dep[u]!=dep[last]){
r[dep[last]]=t-; l[dep[u]]=t;
lastdep=dep[last]; last=u;
}
for(int i=head[u];i;i=e[i].next) if(dep[e[i].u]!=lastdep){
q.push(e[i].u);
}
}
for(int i=;i<=n;i++){
int ceng=dep[i]+w[i];
if(!l[ceng]) continue;
int ll=lower_bound(num+l[ceng],num+r[ceng]+,i,cmpd)-num;
int rr=lower_bound(num+l[ceng],num+r[ceng]+,i,cmpl)-num;
if(ll>=rr) continue;
ans[i]=numsum[rr-]-numsum[ll-];
}
for(int i=;i<=n;i++) printf("%d ",ans[i]);
return ;
}
}

其实光Ti=1的情况,就已经称得上NOIP day1 T2了.....

下面说下正解:

题目的数据范围(特别是Ti和Si等于1的情况)是特别有启发作用的。对于一组路径(Si,Ti),我们可以考虑将它划分为两个询问(Si,lca)和(lca,Ti),分别进行求和。

考虑从Si到lca(即从下往上的走)的情况,若一组路径能对点x产生贡献,则必然满足dep[x]+w[x]==dep[Si]且dep[x]≤dep[lca]。我们可以考虑维护一个数组cnt,cnt[i]表示dep[v]==i的节点v的数量。对于每个节点x,维护一个数组cnt,且节点v的范围仅限于以x为根的子树。则该部分答案为cnt[dep[x]+w[x]]。cnt向其父节点fa[x]回溯时,减去所有以x为lca的路径对cnt产生的贡献。该过程仅需一遍dfs

考虑lca到Ti(从上往下走)的情况,若能对点x产生贡献,则必然满足dep[x]-w[x]==dep[lca]-dis(Si,lca)。同理开一个数组cnt,cnt[i]表示dep[v]-dis(Si,lca)==i的节点v的数量。其余处理部分与Si到lca相同。

上述两种方法,分别需要n个cnt数组,且单次更新其父节点cnt值所需时间为O(n),空间复杂度和事件复杂度均为O(n^2),直接写显然是不行的。我们考虑使用权值线段树去维护cnt数组,同时处理出整棵树后序遍历的dfs序。假设当前需求Si到lca的路径对x的贡献,则贡献为cnt[low[x]][dep[x]+w[x]]-cnt[dfn[x]-1][dep[x]+w[x]]。同理可得lca到Ti对x的贡献。

至此,时间复杂度和空间复杂度均降低至O(n log n)。

PS:该方法需处理很多边界问题,请特别注意!(比如说统计lca处的答案),同时在处理lca到Ti的情况时,cnt[i]的下标可能为负数,需要做一些特殊处理。

 #include<iostream>
#include<cstdio>
#include<cstring>
#include<vector>
#define M 310000
using namespace std;
struct edge{int u,next;}e[M*]={}; int head[M]={},use=;
void adde(int x,int y){use++;e[use].u=y;e[use].next=head[x]; head[x]=use;}
int f[M][]={},dep[M]={};
int w[M]={},s[M]={},t[M]={},lca[M]={},n,m;
int Cnt[M*]={},*cnt=&Cnt[M*],ans[M]={},Add[M]={};
vector<int> add[M],del[M],Del[M];
void dfs(int x,int fa){
dep[x]=dep[fa]+; f[x][]=fa;
for(int i=;i<;i++) f[x][i]=f[f[x][i-]][i-];
for(int i=head[x];i;i=e[i].next) if(e[i].u!=fa) dfs(e[i].u,x);
}
int getlca(int x,int y){
if(dep[x]<dep[y]) swap(x,y); int cha=dep[x]-dep[y];
for(int i=;i>=;i--) if((<<i)&cha) x=f[x][i];
for(int i=;i>=;i--) if(f[x][i]!=f[y][i]) x=f[x][i],y=f[y][i];
if(x!=y) return f[x][]; return x;
} struct sgt{int lx,rx,num;}a[M*];
int root[M]={},dfn[M]={},low[M]={},T=,nuse=;
int query(int x,int k,int l,int r){
if(!x||k>r) return ;//注意在向下找深度更深的点时k可能会>r
if(l==r) return a[x].num;
int mid=(l+r)>>;
if(k<=mid) return query(a[x].lx,k,l,mid);
else return query(a[x].rx,k,mid+,r);
}
int updata(int x,int k,int l,int r,int zhi){
int nowid=++nuse;
a[nowid].lx=a[x].lx; a[nowid].rx=a[x].rx; a[nowid].num=a[x].num;
if(l==r) a[nowid].num+=zhi;
else{
int mid=(l+r)>>;
if(k<=mid) a[nowid].lx=updata(a[nowid].lx,k,l,mid,zhi);
else a[nowid].rx=updata(a[nowid].rx,k,mid+,r,zhi);
}
return nowid;
} void dfs2(int x,int fa){//由lca到t
dfn[x]=T;
for(int i=head[x];i;i=e[i].next) if(e[i].u!=fa){
dfs2(e[i].u,x);
}
low[x]=++T; root[T]=root[T-];
int siz=add[x].size();
for(int i=;i<siz;i++)
root[T]=updata(root[T],dep[x]-add[x][i],-n,n,);
siz=del[x].size();
ans[x]+=query(root[T],dep[x]-w[x],-n,n)-query(root[dfn[x]],dep[x]-w[x],-n,n);
for(int i=;i<siz;i++)
root[T]=updata(root[T],dep[x]-del[x][i],-n,n,-);
} void dfs3(int x,int fa){//由s到lca
dfn[x]=T;
for(int i=head[x];i;i=e[i].next) if(e[i].u!=fa){
dfs3(e[i].u,x);
}
low[x]=++T;
root[T]=updata(root[T-],dep[x],,n,Add[x]);
int siz=Del[x].size();
for(int i=;i<siz;i++)
root[T]=updata(root[T],dep[x]+Del[x][i],,n,-);
ans[x]+=query(root[T],dep[x]+w[x],,n)-query(root[dfn[x]],dep[x]+w[x],,n);
} int main(){
freopen("running.in","r",stdin);
freopen("running.out","w",stdout);
scanf("%d%d",&n,&m);
for(int i=;i<n;i++){
int x,y; scanf("%d%d",&x,&y);
adde(x,y); adde(y,x);
}
dfs(,);
for(int i=;i<=n;i++) scanf("%d",w+i);
for(int i=;i<=m;i++){
int s,t,lca; scanf("%d%d",&s,&t);
lca=getlca(s,t);
add[t].push_back(dep[s]+dep[t]-*dep[lca]);
del[lca].push_back(dep[s]-dep[lca]);
Add[s]++;
Del[lca].push_back(dep[s]-dep[lca]);
//以上这几句话真的累死我了.....
}
dfs2(,);
memset(a,,sizeof(a)); memset(root,,sizeof(root)); T=;
memset(dfn,,sizeof(dfn)); memset(low,,sizeof(low)); nuse=;
dfs3(,);
for(int i=;i<=n;i++) printf("%d ",ans[i]);
}

【NOIP2016 Day1 T2】天天爱跑步的更多相关文章

  1. [NOIP2016 DAY1 T2]天天爱跑步-[差分+线段树合并][解题报告]

    [NOIP2016 DAY1 T2]天天爱跑步 题面: B[NOIP2016 DAY1]天天爱跑步 时间限制 : - MS 空间限制 : 565536 KB 评测说明 : 2s Description ...

  2. Luogu P1600[NOIP2016]day1 T2天天爱跑步

    号称是noip2016最恶心的题 基本上用了一天来搞明白+给sy讲明白(可能还没讲明白 具体思路是真的不想写了(快吐了 如果要看,参见洛谷P1600 天天爱跑步--题解 虽然这样不好但我真的不想写了 ...

  3. NOIP2016 DAY1 T2天天爱跑步

    传送门 题目描述 小c同学认为跑步非常有趣,于是决定制作一款叫做<天天爱跑步>的游戏.«天天爱跑步»是一个养成类游戏,需要玩家每天按时上线,完成打卡任务. 这个游戏的地图可以看作一一棵包含 ...

  4. NOIP2016 Day1 T2 天天爱跑步(树上差分,LCA)

    原文链接 原题链接 题目描述 小c同学认为跑步非常有趣,于是决定制作一款叫做<天天爱跑步>的游戏.<天天爱跑步>是一个养成类游戏,需要玩家每天按时上线,完成打卡任务. 这个游戏 ...

  5. 【NOIP2016】DAY1 T2 天天爱跑步

    [NOIP2016]DAY1 T2 天天爱跑步 Description 小c同学认为跑步非常有趣,于是决定制作一款叫做<天天爱跑步>的游戏.?天天爱跑步?是一个养成类游戏,需要玩家每天按时 ...

  6. 【NOIP 2016】Day1 T2 天天爱跑步

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

  7. [NOIp2016提高组]天天爱跑步

    题目大意: 有一棵n个点的树,每个点上有一个摄像头会在第w[i]秒拍照. 有m个人再树上跑,第i个人沿着s[i]到t[i]的路径跑,每秒钟跑一条边. 跑到t[i]的下一秒,人就会消失. 问每个摄像头会 ...

  8. P1600 [NOIP2016 提高组] 天天爱跑步 (树上差分)

    对于一条路径,s-t,位于该路径上的观察员能观察到运动员当且仅当以下两种情况成立:(d[ ]表示节点深度) 1.观察员x在s-lca(s,t)上时,满足d[s]=d[x]+w[x]就能观察到,所以我们 ...

  9. [luogu]P1600 天天爱跑步[LCA]

    [luogu]P1600 [NOIP 2016]天天爱跑步 题目描述 小c同学认为跑步非常有趣,于是决定制作一款叫做<天天爱跑步>的游戏.«天天爱跑步»是一个养成类游戏,需要玩家每天按时上 ...

随机推荐

  1. 201521123072《java程序设计》第十四周学习总结

    201521123072<java程序设计>第十四周学习总结 1. 本周学习总结 1.1 以你喜欢的方式(思维导图或其他)归纳总结多数据库相关内容. 2. 书面作业 1. MySQL数据库 ...

  2. 201521123034《Java程序设计》第十三周学习总结

    1. 本周学习总结 以你喜欢的方式(思维导图.OneNote或其他)归纳总结多网络相关内容. 2. 书面作业 1. 网络基础 1.1 比较ping www.baidu.com与ping cec.jmu ...

  3. 201521123044 《Java程序设计》第12周学习总结

    1. 本章学习总结 1.1 以你喜欢的方式(思维导图或其他)归纳总结多流与文件相关内容. 2. 书面作业 将Student对象(属性:int id, String name,int age,doubl ...

  4. 201521123051《Java程序设计》第十三周学习总结

    1. 本周学习总结 以你喜欢的方式(思维导图.OneNote或其他)归纳总结多网络相关内容. 2. 书面作业 1. 网络基础 1.1 比较ping www.baidu.com与ping cec.jmu ...

  5. 201521123003《Java程序设计》第13周学习总结

    1. 本周学习总结 以你喜欢的方式(思维导图.OneNote或其他)归纳总结多网络相关内容. 2. 书面作业 1. 网络基础 1.1 比较ping www.baidu.com与ping cec.jmu ...

  6. 手写Maven的archetype项目脚手架

    一.背景 maven是一个很好的代码构建工具,采用“约定优先于配置”的原则进行项目管理,相信很多的java开发者应该都了解maven并可能在工作当中都是通过maven来管理项目的,在创建的项目的时候, ...

  7. linux c函数指针的应用

    头文件:1.h #include<stdio.h> int nul_func(); int test1(int a,int b); int test2(int a,int b,int c) ...

  8. Apache Spark 2.2.0 中文文档 - 概述 | ApacheCN

    Spark 概述 Apache Spark 是一个快速的, 多用途的集群计算系统. 它提供了 Java, Scala, Python 和 R 的高级 API,以及一个支持通用的执行图计算的优化过的引擎 ...

  9. CentOS服务器上的 git 包版本控制

    本文衔接上文"记录一次无聊的(经历了Nodejs -> Shell -> C)的探索问题过程",服务器上git版本是1.8.3.1,使用的pm2来管理nodejs进程, ...

  10. 查找Oracle数据库中的重复记录

    本文介绍了几种快速查找ORACLE数据库中的重复记录的方法. 下面以表table_name为例,介绍三种不同的方法来确定库表中重复的记录 方法1:利用分组函数查找表中的重复行:按照某个字段分组,找出行 ...