【NOIP2016 Day1 T2】天天爱跑步

时间:2021-11-02 07:07:48

题目传送门: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]);
}