题解 P5280 【[ZJOI2019]线段树】

shadowice1984

2019-04-02 17:14:28

Solution

~~4年zjoi3年线段树~~ ~~zjoi的开题顺序永远是231~~ ~~zjoi只有数据结构题能做~~ 说实话这题真的该蓝,因为**它 实 在 是 太 简 单 了** ## 前置芝士:线段树 你只需要提高组的基础线段树芝士就可以通过此题 ________ ## 本题题解 ### 一个小转化 首先你会发现那个复制线段树操作其实是假的 通过归纳法我们就可以证明,假设经过了$m$次操作1,那么你手中的$2^m$棵线段树刚好对应了操作集合的$2^m$个子集 因此我们的操作2实际上在询问对于每一个操作集合的子集,线段树上为1的tag个数之和 ### dp! 根据计数题的一般套路,我们应该考虑每一个节点对答案的贡献, 所以我们不妨设$dp(i)$表示节点的tag值为1的方案数 但是这样设我们会很快的发现一个问题是,我们每执行一次操作,线段树上所有节点的dp值都会变动,尽管你会发现只有$O(logn)$个节点的tag可能会发生变动,其他节点的tag应该和上一次操作一模一样,但是这些节点的dp值都要乘2…… 这样设计dp实在是太蠢了,所以不如让我们想点trick,减少dp值变动的点的个数 ### 方案数转概率 我们重新定义一下dp数组的定义$dp(i)$表示,如果我们等概率的从$2^m$个操作集合当中选择一个操作集合并执行,那么$i$的tag是1的**概率** 这样搞有什么好处呢? 如果这个节点在这一次修改当中没有被改到,那么这个点是1的概率将**保持不动**,那么一次修改下来只有$O(logn)$个节点的$dp$值会发生变动,我们就可以暴力更改这些点的dp值了 此时我们把各个点是1的概率加起来就得到了线段树中是1的tag的期望个数了 啊可是答案要算的是1的个数之和不是期望啊? 平常我们算期望的时候是计算所有方案下的和然后除总的方案数,这里我们反过来,算出期望然后乘总的方案数,这样我们就能算出来1的个数之和啦~ 有了船新的dp定义之后自然要推转移啦~ 为了规避繁杂的细节和分类讨论,我们需要重新看待一次修改的过程 正常状态下,我们会把线段树的修改操作看成按照一定顺序连续执行的pushdown,这样会很好写也很好实现 但是这样你会发现你的转移不好推了,怎么办呢? 考虑这样一个问题:如果知道了操作之前的线段树长什么,那么我们能不能直接推出操作之后的线段树呢? 答案是可行的~,我们把线段树的节点分为3类(似乎这样设计的状态会比分5种情况讨论的做法细节少一点?) ![](https://cdn.luogu.com.cn/upload/pic/55708.png) 当修改区间$[3,5]$的时候,dp值会发生变化的的点在图中被染成了红色,灰色,和黑色 接下来我们分情况讨论一下红点和灰点和黑点的转移情况 #### 红点 >容易看出,只要执行了这一次操作那么红点的tag必然是0,所以这里的转移只需要把红点的dp值除2就行了 >$$dp(u)=dp(u)/2$$ #### 黑点 >容易看出,只要执行了这一次操作那么黑点的tag必然是1,所以这里我们把黑点的dp值加1然后除2就行了 >$$dp(u)=(dp(u)+1)/2$$ #### 灰点 >容易看出,我们什么性质也看不出来 >因为即使执行了这次操作,灰点的tag也可能是0 >因此灰点莫得转移,这个状态定义翻!车!了! ## 新的状态定义 因为灰点没法转移,所以我们要定义一些新的状态,让我们可以转移 考虑一个灰点的tag什么时候会变成1,我们发现只要这个灰点到线段树的根的路径上有tag为1的节点,那么这个灰点的tag在操作之后就是1 因此我们新增一个$fdp(i)$状态表示这个点到根的路径上有1的概率 $fdp(i)$的引入会带来一些问题,就是一次修改之后$fdp$值发生变化的节点数目不再是$O(logn)$的了,这个问题我们过一会再说,先让我们重新推一推转移 ### 红点 >容易看出,执行了这次操作后,红点到根的路径全部是红点,所以红点到根的路径不会有1,因此直接把fdp值除2就行了 >$$fdp(u)=fdp(u)/2$$ ### 灰点 >容易看出,执行了这次操作之后,如果灰点到根的路径上有1,那么灰点的tag就是1,否则不是1,所以把灰点的dp值加上fdp值除2就行了 >另一个性质是对于在灰点子树当中的节点,修改之前如果到根的路径上有1,那么这个1最多是被push到灰点上而已,因此这一次修改并不会改变这个节点到根的路径上是否有1这个命题的真假,所以灰点子树当中的节点fdp值并不会动 >$$dp(u)=(dp(u)+fdp(u))/2$$ ### 黑点 >容易看出,执行这次操作之后,对于黑点子树当中的所有点,他们到根的路径上肯定有1了,那么我们把fdp值加上1然后除2就行了 >$$fdp(u)=(fdp(u)+1)/2$$ ## 打标记 上面的转移方程涉及到对黑点子树当中的所有点做$x=(x+1)/2$这样的操作,显然我们不能暴力做,所以我们维护一个懒标记表示这个点被执行了几次这样的操作 容易看出,如果一个节点原来的值是$val$,被连续执行了$n$次操作,那么这个点的真实值将会是 $$(val+2^n-1)/2^n$$ 然后我们就可以打标记维护fdp值了,每次修改的时候用bfs找出所有的红点黑点灰点按照转移方程转移即可~ 总复杂度$O(mlogn)$,不是很清楚3000ms的时限意义何在…… ```C // luogu-judger-enable-o2 #include<cstdio> #include<algorithm> using namespace std;const int N=1e5+10;typedef unsigned long long ll;const ll mod=998244353; const ll iv2=(mod+1)/2;ll imi[N];ll dmi[N];ll mi[N];int n;int m; # define md(x) (x=(x>=mod)?x-mod:x) struct linetree { ll sum;ll dp[N<<2];ll fdp[N<<2];int l[N<<2];int r[N<<2];int add[N<<2]; int qu1[N];int hd1;int qu2[N];int hd2;int qu3[N];int hd3; int qu[N];int hd;int tl; inline void chan(int p,int tmp) {sum+=mod-dp[p];md(sum);dp[p]=(dp[p]+tmp)*iv2%mod;sum+=dp[p];md(sum);} inline void pd(int p)//下传标记 { if(add[p]!=0) { fdp[p<<1]=(fdp[p<<1]+dmi[add[p]])*imi[add[p]]%mod; fdp[p<<1|1]=(fdp[p<<1|1]+dmi[add[p]])*imi[add[p]]%mod; add[p<<1]+=add[p];add[p<<1|1]+=add[p];add[p]=0; } }inline void extract(int dl,int dr)//bfs { hd=1;tl=0;qu[++tl]=1;hd1=hd2=hd3=0; while(hd<=tl) { int nw=qu[hd++]; if(dl<=l[nw]&&r[nw]<=dr){qu2[++hd2]=nw;continue;} if(r[nw]<dl||l[nw]>dr){qu3[++hd3]=nw;continue;} qu1[++hd1]=nw;pd(nw);qu[++tl]=nw<<1;qu[++tl]=nw<<1|1; } }inline void modify(int dl,int dr)//按照转移方程转移同时打标记 { extract(dl,dr);for(int i=1;i<=hd1;i++)chan(qu1[i],0); for(int i=1;i<=hd2;i++)chan(qu2[i],1); for(int i=1;i<=hd2;i++)add[qu2[i]]++,fdp[qu2[i]]=(fdp[qu2[i]]+1)*iv2%mod; for(int i=1;i<=hd3;i++)chan(qu3[i],fdp[qu3[i]]); for(int i=1;i<=hd1;i++)fdp[qu1[i]]=fdp[qu1[i]]*iv2%mod; }inline void build(int p,int pl,int pr)//建树 { if(pl>pr)return;l[p]=pl;r[p]=pr;if(pl==pr)return; int mid=(pl+pr)>>1;build(p<<1,pl,mid);build(p<<1|1,mid+1,pr); } }lt; int main() { imi[0]=1;for(int i=1;i<N;i++)imi[i]=imi[i-1]*iv2%mod; mi[0]=1;for(int i=1;i<N;i++)mi[i]=mi[i-1]*2%mod; dmi[0]=0;for(int i=1;i<N;i++)dmi[i]=(mi[i]+mod-1)%mod; scanf("%d%d",&n,&m);lt.build(1,1,n);int tot=0; for(int i=1,tp,l,r;i<=m;i++) { scanf("%d",&tp); if(tp==1)scanf("%d%d",&l,&r),lt.modify(l,r),tot++; else printf("%lld\n",lt.sum*mi[tot]%mod); }return 0;//拜拜程序~ } ```