NOI模拟 树数术
涉及知识点:树、倍增、单调栈
题意
给你一颗有 \(n\ (\leq 7\times10^5)\) 个节点的树,再给你一个长为 \(m\ (\leq7\times10^5)\) 的序列,序列中的值代表树上点的编号,有 \(q\) 次询问,每次询问取原序列的 \(k\ (\sum k\leq7\times10^5)\) 个子区间并连起来成为一段新的序列,问新的序列中有多少个点 \(i\) 满足 \(\forall j\in [1,i]\),\(a_j\) 在 \(a_i\) 的子树内。
思路
我们记 \(f(i)=k\) 为 \(i\) 后面出现的第一个 \(k\) 满足 \(a_{i\sim k}\) 均在 \(a_k\) 的子树内,那么我们可以在原序列上跳下一个 \(f(i)\) 不断查询,记 nxt[i][j]
为 \(i\) 跳 \(2^j\) 次的点。对于第一个区间 \([l_1,r_1]\),直接用倍增跳到第一个大于 \(r_1\) 的地方;对于第二及以后的区间 \([l_i,r_i]\),从包含前面所有区间的祖先开始跳,直到跳出 \(r_i\),而这样“包含前面所有区间的祖先”一定也是之前上一个区间结尾的点的后继,所以起点也可以直接从前面跳过来找。
思维不是非常复杂,就是跳的时候要仔细一点,具体实现看代码。
代码
#include<bits/stdc++.h>
using namespace std;
#ifdef ONLINE_JUDGE
#define getchar __getchar
inline char __getchar(){
static char ch[1<<20],*l,*r;
return (l==r&&(r=(l=ch)+fread(ch,1,1<<20,stdin),l==r))?EOF:*l++;
}
#endif
template<class T>inline void rd(T &x){
T res=0,f=1;
char ch=getchar();
while(ch<'0' || ch>'9'){if(ch=='-')f=-1; ch=getchar();}
while('0'<=ch && ch<='9'){res=res*10+ch-'0';ch=getchar();}
x=res*f;
}
template<class T>inline void wt(T x,char endch='\0'){
static char wtbuff[20];
static int wtptr;
if(x==0){
putchar('0');
}
else{
if(x<0){x=-x;putchar('-');}
wtptr=0;
while(x){wtbuff[wtptr++]=x%10+'0';x/=10;}
while(wtptr--) putchar(wtbuff[wtptr]);
}
if(endch!='\0') putchar(endch);
}
typedef long long LL;
const int MAXN=7e5+5,MAXB=21;
int n,m,q,k,arr[MAXN],ecnt=0,head[MAXN];
int dfnmin[MAXN],dfnmax[MAXN],dfncnt=0;//dfnmin子树最小点 dfnmax子树最大点 dfn在两者之间说明在子树内
int nxt[MAXN][MAXB],nxtl[MAXN][MAXB],nxtr[MAXN][MAXB],lg[MAXN];
LL ans;
struct EDGE{
int v,nxt;
}e[MAXN<<1];
inline void addedge(const int& u,const int& v){
e[++ecnt].v=v;
e[ecnt].nxt=head[u];
head[u]=ecnt;
}
inline bool check(int id,int l,int r){
// assert(l<=r);
return dfnmin[arr[id]]<=l && r<=dfnmax[arr[id]];
}
void dfs(int x,int fa){
dfnmin[x]=++dfncnt;
for(int i=head[x];i;i=e[i].nxt){
if(e[i].v==fa) continue;
dfs(e[i].v,x);
}
dfnmax[x]=dfncnt;
}
int main(){
// freopen("tree.in","r",stdin);
// freopen("tree.out","w",stdout);
rd(n);rd(m);rd(q);
lg[1]=0;
for(int i=2;i<=m;i++){
lg[i]=lg[i/2]+1;
}
for(int i=1,u,v;i<n;i++){
rd(u);rd(v);
addedge(u,v);addedge(v,u);
}
dfs(1,-1);
for(int i=1;i<=m;i++){
rd(arr[i]);
}
nxt[m+1][0]=m+1;
stack<int>st;
for(int i=m;i>=1;i--){
//单调栈找后面第一个全在子树内的区间
while(!st.empty() && !check(st.top(),dfnmin[arr[i]],dfnmax[arr[i]])) st.pop();
if(!st.empty()) nxt[i][0]=st.top();
else nxt[i][0]=m+1;
nxtl[i][0]=dfnmin[arr[i]];nxtr[i][0]=dfnmax[arr[i]];
st.push(i);
}
for(int j=1;j<MAXB;j++){
for(int i=1;i<=m+1;i++){
nxt[i][j]=nxt[nxt[i][j-1]][j-1];//nxt: i跳j次的点
if(i+(1<<(j-1))<=m) nxtl[i][j]=min(nxtl[i][j-1],nxtl[i+(1<<(j-1))][j-1]);
else nxtl[i][j]=nxtl[i][j-1];
if(i+(1<<(j-1))<=m) nxtr[i][j]=max(nxtr[i][j-1],nxtr[i+(1<<(j-1))][j-1]);
else nxtr[i][j]=nxtr[i][j-1];
//nxtl nxtr: i跳j次一共覆盖了哪些点
}
}
while(q--){
ans=0;
int l,r,ptr,lall=dfncnt+1,rall=0;
rd(k);
while(k--){
rd(l);rd(r);ptr=l;
if(!check(ptr,lall,rall)){//跳起点
for(int i=lg[m];i>=0;i--){
if(nxt[ptr][i]<=r && !check(nxt[ptr][i],lall,rall)) ptr=nxt[ptr][i];
}
ptr=nxt[ptr][0];
}
if(ptr<=r){//跳终点(跳出r)
ans++;
for(int i=lg[m];i>=0;i--){
if(nxt[ptr][i]<=r) ptr=nxt[ptr][i],ans+=(1<<i);
}
}
int lglen=lg[r-l+1];
lall=min(lall,min(nxtl[l][lglen],nxtl[r-(1<<lglen)+1][lglen]));
rall=max(rall,max(nxtr[l][lglen],nxtr[r-(1<<lglen)+1][lglen]));
//lall rall: 更新前面所有区间的覆盖的点
}
wt(ans,'\n');
}
return 0;
}