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;
}
posted @ 2024-05-22 23:24  MessageBoxA  阅读(8)  评论(0编辑  收藏  举报