zoukankan      html  css  js  c++  java
  • 【bzoj4785】[Zjoi2017]树状数组 线段树套线段树

    题目描述

    漆黑的晚上,九条可怜躺在床上辗转反侧。难以入眠的她想起了若干年前她的一次悲惨的OI 比赛经历。那是一道基础的树状数组题。给出一个长度为 n 的数组 A,初始值都为 0,接下来进行 m 次操作,操作有两种:

    1 x,表示将 Ax 变成 (Ax + 1) mod 2。
    2 l r,表示询问 sigma(Ai) mod 2,L<=i<=r
    尽管那个时候的可怜非常的 simple,但是她还是发现这题可以用树状数组做。当时非常young 的她写了如下的算法:
    1: function Add(x)
    2: while x > 0 do
    3: A
    x ← (Ax + 1) mod 2
    4: x ← x - lowbit(x)
    5: end while
    6: end function
    7:
    8: function Find(x)
    9: if x == 0 then
    10: return 0
    11: end if
    12: ans ← 0
    13: while x ≤ n do
    14: ans ← (ans + Ax) mod 2
    15: x ← x + lowbit(x)
    16: end while
    17: return ans
    18: end function
    19:
    20: function Query(l, r)
    21: ansl ← Find(l - 1)
    22: ansr ← Find(r)
    23: return (ansr - ansl + 2) mod 2
    24: end function
    其中 lowbit(x) 表示数字 x 最高的非 0 二进制位,例如 lowbit(5) = 1, lowbit(12) = 4。进行第一类操作的时候就调用 Add(x),第二类操作的时候答案就是 Query(l, r)。如果你对树状数组比较熟悉,不难发现可怜把树状数组写错了: Add和Find 中 x 变化的方向反了。因此这个程序在最终测试时华丽的爆 0 了。然而奇怪的是,在当时,这个程序通过了出题人给出的大样例——这也是可怜没有进行对拍的原因。现在,可怜想要算一下,这个程序回答对每一个询问的概率是多少,这样她就可以再次的感受到自己是一个多么非的人了。然而时间已经过去了很多年,即使是可怜也没有办法完全回忆起当时的大样例。幸运的是,她回忆起了大部分内容,唯一遗忘的是每一次第一类操作的 x的值,因此她假定这次操作的 x 是在 [li, ri] 范围内 等概率随机 的。具体来说,可怜给出了一个长度为 n 的数组 A,初始为 0,接下来进行了 m 次操作:
    1 l r,表示在区间 [l, r] 中等概率选取一个 x 并执行 Add(x)。
    2 l r,表示询问执行 Query(l, r) 得到的结果是正确的概率是多少。

    输入

    第一行输入两个整数 n, m。
    接下来 m 行每行描述一个操作,格式如题目中所示。
    N<=10^5,m<=10^5,1<=L<=R<=N

    输出

    对于每组询问,输出一个整数表示答案。如果答案化为最简分数后形如 x/y,那么你只需要输出 x*y^?1 mod 998244353 后的值。(即输出答案模 998244353)。

    样例输入

    5 5
    1 3 3
    2 3 5
    2 4 5
    1 1 3
    2 2 5

    样例输出

    1
    0
    665496236


    题解

    线段树套线段树

    “如果你对树状数组比较熟悉,不难发现”本题中树状数组求的是后缀和。

    那么当$l-1 eq 0$时(等于0时再单独讨论),求出的结果即为$sumlimits_{i=l-1}^{r-1}A_i$,若与$sumlimits_{i=l}^rA_i$相等,则要求$A_{l-1}=A_r$。所以只需要求出$A_{l-1}=A_r$的概率即可。

    我们想,对于修改操作[l,r],如果已经确定了左端点t和右端点k,如何更新t与k(k>t)相等的概率呢?

    肯定是要分情况讨论,当然其中只有当$t$或$kin[l,r]$时才会产生影响。

    1.当$tin[1,l-1]$,$kin[l,r]$时,不影响的概率为1-p

    2.当$tin[l,r]$,$kin[l,r]$时,不影响的概率为1-2p

    3.当$tin[l,r]$,$kin[r+1,n]$时,不影响的概率为1-p。

    如果确定了t,我们显然可以使用线段树维护这三段区间。至于概率的问题,如果原来相等的概率为p,不影响的概率为q,那么新的相等的概率显然为$p·q+(1-p)(1-q)$。并且这个式子满足交换律和结合律,因此更新顺序是不需要考虑的(并且可以标记永久化)。

    而由于t的存在情况也是连续的区间,所以我们还需要一颗线段树维护左端点t,所以需要线段树套线段树,即二维线段树。

    具体实现:使用类似于标记永久化的思想,选择一段外层区间和内层区间,就把(外层区间对应的外层节点)对应的(内层区间对应的内层节点)更新。

    至于查询[l,r],则查找(外层线段树中l-1对应的节点)对应的(内层线段树中r对应的节点)。因为永久化了标记,所以所有经过的节点对答案的贡献都需要记录到答案中(特别是外层线段树)。

    以上就是$l eq 1$的情况,至于l=1的情况,同理,要保证的是r的前缀和等于后缀和,采用同样的思路维护一下就好了,具体见代码中对外层线段树0节点的操作。

    代码真心不长~

    #include <cstdio>
    #include <cstring>
    #include <algorithm>
    #define N 100010
    using namespace std;
    typedef long long ll;
    const ll mod = 998244353;
    int root[N << 2] , ls[N << 8] , rs[N << 8] , tot , n;
    ll sum[N << 8];
    ll cal(ll x , ll y)
    {
    	return (x * y + (1 - x + mod) * (1 - y + mod)) % mod;
    }
    ll pow(ll x , ll y)
    {
    	ll ans = 1;
    	while(y)
    	{
    		if(y & 1) ans = ans * x % mod;
    		x = x * x % mod , y >>= 1;
    	}
    	return ans;
    }
    void update(int b , int e , ll v , int l , int r , int &x)
    {
    	if(!x) x = ++tot , sum[x] = 1;
    	if(b <= l && r <= e)
    	{
    		sum[x] = cal(sum[x] , v);
    		return;
    	}
    	int mid = (l + r) >> 1;
    	if(b <= mid) update(b , e , v , l , mid , ls[x]);
    	if(e > mid) update(b , e , v , mid + 1 , r , rs[x]);
    }
    ll query(int p , int l , int r , int x)
    {
    	if(!x) return 1;
    	if(l == r) return sum[x];
    	int mid = (l + r) >> 1;
    	if(p <= mid) return cal(sum[x] , query(p , l , mid , ls[x]));
    	else return cal(sum[x] , query(p , mid + 1 , r , rs[x]));
    }
    void modify(int p , int q , ll v , int b , int e , int l , int r , int x)
    {
    	if(p <= l && r <= q)
    	{
    		update(b , e , v , 1 , n , root[x]);
    		return;
    	}
    	int mid = (l + r) >> 1;
    	if(p <= mid) modify(p , q , v , b , e , l , mid , x << 1);
    	if(q > mid) modify(p , q , v , b , e , mid + 1 , r , x << 1 | 1);
    }
    ll solve(int p , int q , int l , int r , int x)
    {
    	if(l == r) return query(q , 1 , n , root[x]);
    	int mid = (l + r) >> 1;
    	if(p <= mid) return cal(query(q , 1 , n , root[x]) , solve(p , q , l , mid , x << 1));
    	else return cal(query(q , 1 , n , root[x]) , solve(p , q , mid + 1 , r , x << 1 | 1));
    }
    int main()
    {
    	int m , opt , l , r;
    	ll p;
    	scanf("%d%d" , &n , &m);
    	while(m -- )
    	{
    		scanf("%d%d%d" , &opt , &l , &r);
    		if(opt == 1)
    		{
    			p = pow(r - l + 1 , mod - 2);
    			if(l > 1) modify(1 , l - 1 , (1 - p + mod) % mod , l , r , 0 , n , 1) , modify(0 , 0 , 0 , 1 , l - 1 , 0 , n , 1);
    			if(r < n) modify(l , r , (1 - p + mod) % mod , r + 1 , n , 0 , n , 1) , modify(0 , 0 , 0 , r + 1 , n , 0 , n , 1);
    			modify(l , r , (1 - (p << 1) % mod + mod) % mod , l , r , 0 , n , 1) , modify(0 , 0 , p , l , r , 0 , n , 1);
    		}
    		else printf("%lld
    " , solve(l - 1 , r , 0 , n , 1));
    	}
    	return 0;
    }
    

     

  • 相关阅读:
    【读书笔记】iOS-类别
    【读书笔记】iOS-特性
    【读书笔记】iOS-对象初始化
    【读书笔记】iOS-内存管理
    iOS---类方法(静态方法)和实例方法
    iOS ---Extension编程指南
    Swift学习与复习
    iOS----Xcode6或者Xcode7设置LaunchImage图标
    iOS----------使用 Xcode6或Xcode7配置.pch文件
    iOS开发----优秀文章推荐
  • 原文地址:https://www.cnblogs.com/GXZlegend/p/7076672.html
Copyright © 2011-2022 走看看