洛谷 P8969 幻梦 | Dream with Dynamic 题解

题目来源

Description

  • 给定一个长度为 n n n 的数列 { a n } \{a_n\} {an},给定 q q q 次操作,有如下 3 3 3 种操作:
  • A l r x,把区间 [ l , r ] [l,r] [l,r] 中的数分别加上 x x x
  • P l r,对于区间 [ l , r ] [l,r] [l,r] 中的数,令 a i ← p o p c o u n t ( a i ) a_i\leftarrow \mathrm{popcount}(a_i) aipopcount(ai),其中 p o p c o u n t ( x ) \mathrm{popcount}(x) popcount(x) 表示 x x x 的二进制表示中 1 1 1 的个数。
  • J p,查询 a p a_p ap 的值。
  • 1 ≤ n ≤ 3 × 1 0 5 , 1 ≤ q ≤ 1 0 6 , 1 ≤ a i , x ≤ 1 0 9 1\le n\le3\times10^5,1\le q\le10^6,1\le a_i,x\le10^9 1n3×105,1q106,1ai,x109

Solution

首先由于操作为区间修改+单点查询,很自然的想到运用线段树。

但由于线段树所维护的数据需要具有可结合性,即已知左右两个子节点的数据可以推出父亲节点的数据,所以本题需要某些特殊的操作来达到此目的。

区间加操作显然很好做,但是 p o p c o u n t \mathrm{popcount} popcount 则不具有结合性,考虑怎么进行结合。

由于在进行若干次区间加和区间 p o p c o u n t \mathrm{popcount} popcount(以下简称 P P P) 操作后,必然可以写作 P ( ⋯ P ( x + b u ) ⋯   ) + c u P(\cdots P(x+b_u)\cdots)+c_u P(P(x+bu))+cu 的形式(其中 x x x 为从子节点传上来的值,无法确定)。

那么我们可以令线段树上每个节点存储 b u , c u , f u , F u b_u,c_u,f_u,F_u bu,cu,fu,Fu,其中 b u b_u bu 表示该区间第一次 P P P 操作之前所加上的值的总和, c u c_u cu 表示该区间最后一次 P P P 操作后所加上的值的总和,而 f f f 为一个 64 64 64 位的数组,表示一组映射关系(初始设 f u [ i ] = i f_u[i]=i fu[i]=i),即 f P ( x + b u ) = P ( ⋯ P ( x + b u ) ⋯   ) + c u f_{P(x+b_u)}=P(\cdots P(x+b_u)\cdots)+c_u fP(x+bu)=P(P(x+bu))+cu

而由于我们无法确定 P ( x + b u ) P(x+b_u) P(x+bu) 的值,而其值域仅为 [ 0 , 64 ) [0,64) [0,64),那么我们每次操作就需要将所有 f i ∈ [ 0 , 64 ) f_{i\in[0,64)} fi[0,64) 的值求出来。

F u F_u Fu 0 / 1 0/1 0/1 表示该区间是否已经出现过 P P P 操作。

由于仅有单点查询,则无需考虑 p u s h u p \mathrm{pushup} pushup 操作,仅需考虑 p u s h d o w n \mathrm{pushdown} pushdown

设父亲节点为 u u u ,需要下传到的子节点为 v v v。则对于 v v v 的所有操作均位于 u u u 的操作之前,因为若 u u u 存在早于 v v v 的操作,必然会在之前的过程中下传。

那么考虑根据 F u , F v F_u,F_v Fu,Fv 的取值分类讨论:

  • F u = 0 F_u=0 Fu=0 F v = 0 F_v=0 Fv=0,说明均只存在区间加操作,显然令 b v ← b v + b u b_v\leftarrow b_v+b_u bvbv+bu 即可。
  • 否则,若 F u = 0 F_u=0 Fu=0 F v = 1 F_v=1 Fv=1,说明在完成 v v v 的最后一次 P P P 操作后,需要加上 c v c_v cv,并再次加上 b u b_u bu,那么令 c v ← c v + b u c_v\leftarrow c_v+b_u cvcv+bu 即可。
  • 否则,若 F u = 1 F_u=1 Fu=1 F v = 0 F_v=0 Fv=0,说明在完成 v v v 的加操作,再加上 b u b_u bu 之后完成 u u u 的一系列操作即可得出结果。而与 u u u 相比, v v v 只改变了 u u u 第一次 P P P 操作前的操作,对 f f f 数组无影响,那么令 f v ← f u , b v ← b v + b u , c v ← c u , F v ← 1 f_v\leftarrow f_u,b_v\leftarrow b_v+b_u,c_v\leftarrow c_u,F_v\leftarrow1 fvfu,bvbv+bu,cvcu,Fv1 即可
  • 否则,说明 f v [ P ( x + b v ) ] + c v f_v[P(x+b_v)]+c_v fv[P(x+bv)]+cv 代表 u u u 中的 x x x,那么令 f v [ i ] ← f u [ P ( f v [ i ] + c v + b u ) ] , c v ← c u f_v[i]\leftarrow f_u[P(f_v[i]+c_v+b_u)],c_v\leftarrow c_u fv[i]fu[P(fv[i]+cv+bu)],cvcu 即可。

更新完后记得将 u u u 节点内存储的所有数据清空。

在这里插入图片描述

区间加操作对整个区间进行修改时进行的操作:若 F p = 1 F_p=1 Fp=1,则令 c p ← c p + x c_p\leftarrow c_p+x cpcp+x;否则令 b p ← b p + x b_p\leftarrow b_p+x bpbp+x

区间 P P P 操作对整个区间进行修改时进行的操作:若 F p = 1 F_p=1 Fp=1,则令 f p [ i ] ← P ( f p [ i ] + c p ) , c p ← 0 f_p[i]\leftarrow P(f_p[i]+c_p),c_p\leftarrow0 fp[i]P(fp[i]+cp),cp0;否则令 F p ← 1 F_p\leftarrow1 Fp1

单点查询访问到线段树上编号为 p p p 的叶子节点 [ l , l ] [l,l] [l,l] 即返回 f p [ P ( a l ) + b p ] + c p f_p[P(a_l)+b_p]+c_p fp[P(al)+bp]+cp

其他操作按照线段树基本方式完成即可。

Code

#include <bits/stdc++.h>
using namespace std;
int n,q,a[300005];
struct Node{
    int l,r;
    long long b,c; 
    short f[70]; // 初始时 f[i]=i(不开short可能爆空间)
    bool F;
    // 存储形如P(...P(x+b)...)+c的信息(x表示从子节点上来的值),b为第一次P前加操作的总和
    // F若为0说明还没有P操作,否则f[P(x+b)]=P(...P(x+b)...)+c,c为最后一次P后加操作的总和
}tree[1200005];
void pushdown(int u,int v){
    if (!tree[u].F&&!tree[v].F) tree[v].b+=tree[u].b;
    else if (!tree[u].F&&tree[v].F) tree[v].c+=tree[u].b;
    else if (tree[u].F&&!tree[v].F){
        for (int i=0;i<=64;i++) tree[v].f[i]=tree[u].f[i];
        tree[v].b+=tree[u].b;
        tree[v].c=tree[u].c;
        tree[v].F=1;
    }
    else{
        for (int i=0;i<=64;i++) tree[v].f[i]=tree[u].f[__builtin_popcountll(tree[v].f[i]+tree[v].c+tree[u].b)];
        tree[v].c=tree[u].c;
    }
}
void clear(int p){
	tree[p].b=tree[p].c=tree[p].F=0;
    for (int i=0;i<=64;i++) tree[p].f[i]=short(i);
}
void build(int p,int l,int r){
    tree[p].l=l,tree[p].r=r;
    clear(p);
    if (l==r){
        tree[p].b=a[l];
        return;
    }
    int Mid=(l+r)>>1;
    build(p<<1,l,Mid);
    build(p<<1|1,Mid+1,r);
}
void update_add(int p,int l,int r,int x){
    if (l<=tree[p].l&&tree[p].r<=r){
        if (tree[p].F) tree[p].c+=x;
        else tree[p].b+=x;
        return;
    }
    pushdown(p,p<<1),pushdown(p,p<<1|1),clear(p);
    int Mid=(tree[p].l+tree[p].r)>>1;
    if (l<=Mid) update_add(p<<1,l,r,x);
    if (r>Mid) update_add(p<<1|1,l,r,x);
}
void update_pop(int p,int l,int r){
    if (l<=tree[p].l&&tree[p].r<=r){
        if (tree[p].F){
            for (int i=0;i<=64;i++) tree[p].f[i]=__builtin_popcountll(tree[p].f[i]+tree[p].c);
            tree[p].c=0;
        }
        else tree[p].F=1;
        return;
    }
    pushdown(p,p<<1),pushdown(p,p<<1|1),clear(p);
    int Mid=(tree[p].l+tree[p].r)>>1;
    if (l<=Mid) update_pop(p<<1,l,r);
    if (r>Mid) update_pop(p<<1|1,l,r);
}
long long query(int p,int x){
    if (tree[p].l==tree[p].r){
        if (tree[p].F) return tree[p].f[__builtin_popcountll(tree[p].b)]+tree[p].c;
        else return tree[p].b;
    }
    pushdown(p,p<<1),pushdown(p,p<<1|1),clear(p);
    int Mid=(tree[p].l+tree[p].r)>>1;
    if (x<=Mid) return query(p<<1,x);
    return query(p<<1|1,x);
}
int main(){
    scanf("%d%d",&n,&q);
    for (int i=1;i<=n;i++) scanf("%d",&a[i]);
    build(1,1,n);
    for (int i=1,l,r,x;i<=q;i++){
        char op;
        scanf(" %c",&op);
        if (op=='A'){
            scanf("%d%d%d",&l,&r,&x);
            update_add(1,l,r,x);
        }
        if (op=='P'){
            scanf("%d%d",&l,&r);
            update_pop(1,l,r);
        }
        if (op=='J'){
            scanf("%d",&x);
            printf("%lld\n",query(1,x));
        }
    }
    return 0;
}

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值