zoukankan      html  css  js  c++  java
  • 【笔记】 对不起,我不会树状数组

    一些奇怪的树状数组技巧

    先来个几个比较常用的:

    (O(n)) 初始化树状数组

    不要做 (O(n)) 次单点修改,类似拓扑排序从 (1 o n) 一个一个向直接祖先上传答案就行了。

    (O(1)) 清空树状数组

    常见于点分治题。

    做法是给每个树状数组的节点设置一个时间戳 (t_i),并记录当前时间戳 (Time)

    • 如果当前时间戳与节点时间戳相同,那么该节点的值是正确的;
    • 如果不同,这个位置上的值将被重置为 (0),并更新时间戳 (t_i gets Time)

    想清空时直接 (Timegets Time+1) 就行了。

    树状数组二分

    例题!

    支持单点修改,查询区间最后一个前缀和 (le k) 的位置。

    只需对 bit 代码稍作修改即可。

    
    class BIT{
    	int data[MX]
    	void add(int pos ,int val){
    		while(pos <= MX){
    			data[pos] += val;
    			pos += pos & -pos;
    		}
    	}
    	int lower_bound(int s){
    		int sum = 0 ,Ans = 0;
    		for(int i = lg2[MX] ; ~i ; --i){
    			Ans += 1 << i;
    			if(Ans >= MX || sum + data[Ans] > s)	Ans -= 1 << i;
    			else sum += data[Ans];
    		}return Ans + 1;
    	}
    }C;
    

    复杂度 (O(log n))

    树状数组区间加区间求和

    这是一个经典的问题,但新手(指我自己)往往无法想到如何解决。

    树状数组维护区间加,单点求和是这个问题的弱化版,做法是维护原数组的差分数组 (d)

    一次区间加只要修改差分数组的 (2) 个位置。

    一次单点查询只要查询差分数组的前缀和即可。

    那么区间和怎么求呢?显然我们可以做 (O(n)) 次单点求值。

    [S_x = sum_{i=1}^{x} sum_{j=1}^{i} d_j ]

    这样就得到了数组的原前缀和,两个前缀和相减就是区间和。

    不过这个式子显然可以化简为:

    [S_x = sum_{i=1}^{x} d_i imes (x-i+1) = (x+1)sum_{i=1}^{x} d_i - sum_{i=1}^{x} i imes d_i ]

    你非常惊讶地发现只要维护两个树状数组,一个存 (sum d_i),另一个存 (sum i imes d_i) 就可以了。

    例题!

    维护一个数组 (A),支持:

    • 区间加 (1)
    • 区间求 (sumlimits_{i=l}^{r} i imes A_i)

    这相当于是区间加等差数列,不妨考虑维护二阶差分序列 (dd)。再通过二阶前缀和的方法求出来原数组。

    [egin{aligned} S_x & = sum_{i=1}^{x} sum_{j=1}^{i} d_j \ & = sum_{i=1}^{x} sum_{j=1}^{i} sum_{k=1}^{j} dd_k \ & = sum_{k=1}^{x} dd_k sum_{j=k}^{x} sum_{i=j}^{x} 1 \ & = sum_{k=1}^x dd_k imes dfrac{(x-k+2)(x-k+1)}{2} \ & = dfrac{1}{2}sum_{k=1} dd_k imes (k^2 -(3+2x)k+x^2+3x+2) end{aligned} ]

    自然,你又只需要三个树状数组,分别维护 (sum dd_k)(sum k imes dd_k)(sum k^2 imes dd_k) 就行啦!

    不过常数好像有点大呢(笑)

    一份示例代码

    struct BIT{
    	LL data[MX];
    	void add(int x ,LL v){
    		while(x < MX){
    			data[x] = (data[x] + v + MOD) % MOD;
    			x += x & -x;
    		}
    	}
    	LL sum(int x){
    		LL s = 0;
    		while(x > 0){
    			s = (s + data[x]) % MOD;
    			x -= x & -x;
    		}
    		return s;
    	}
    };
    
    struct BIT3{
    	BIT A ,B ,C;
    	void add_d(int x ,LL v){
    		A.add(x ,v);
    		B.add(x ,x * v % MOD);
    		C.add(x ,x * v % MOD * x % MOD);
    	}
    	void add(int l ,int r ,LL v){
    		add_d(l ,l * v % MOD);
    		add_d(l + 1 ,(1 - l + MOD) * v % MOD);
    		add_d(r + 1 ,(MOD - r - 1) * v % MOD);
    		add_d(r + 2 ,r * v % MOD);
    	}
    	LL sum(LL x){
    		LL ans = (x * x + 3 * x + 2) % MOD * A.sum(x) % MOD;
    		ans = (ans - (3 + 2 * x) % MOD * B.sum(x) % MOD + MOD) % MOD;
    		ans = (ans + C.sum(x)) % MOD;
    		ans = (ans * iv) % MOD;
                      // iv 指的是 2 的逆元
    		return ans;
    	}
    	LL sum(int l ,int r){
    		return (sum(r) - sum(l - 1) + MOD) % MOD;
    	}
    }C3;
    

    树状数组单点修改区间最值

    前排提醒:复杂度只能做到修改 (O(log ^2n)),查询 (O(log ^2 n)),比较鸡肋。

    修改操作

    类比维护区间和时候的操作,一个节点 (k) 维护的是 (b_k = sumlimits_{i=k-lowbit(k)+1}^{k} a_i)

    维护区间最值时,一个节点 (k) 维护的就是 (b_k = maxlimits_{i=k-lowbit(k)+1}^{k}a_i)

    但,区间最值与区间和的区别就在于取最值没有逆运算,不可以通过差分的方式得到区间的答案。

    但好在,我们依然可以让树状数组像线段树,通过子树的信息更新当前节点的信息。

    树状数组上一个节点有最多 (O(log n)) 个孩子(不知道为什么的 emmm,建议重学 BIT),单点修改最多会更新 (O(log n)) 个节点。复杂度 (O(log^2 n))

    查询操作

    此时我们访问 (b) 数组的复杂度是 (O(1)) 的,所以不要有什么忌惮啦!

    显然最值操作不能差分。

    有一种想法是像线段树那样提取出树状数组上的一些节点,把它们的答案拼成整体答案。

    我们直接实现这个做法就可以了,假设询问区间为 ([l,r]),设查询函数为 int qmax(int l ,int r):

    • 如果 (r-lowbit(r)<l-1),返回 (max(a_r,operatorname{qmax}(l,r-1)))
    • 否则返回 (max(b_r , operatorname{qmax}(l,r-lowbit(r))))

    容易发现,我们会在 (O(log n)) 次递归后将最高位降低至少一位,所以复杂度是 (O(log^2 n))

    代码

    struct BIT{
    	int data[MX] ,a[MX];
    	void set(int x ,int v){
    		a[x] = v;
    		while(x < MX){
    			data[x] = v;
    			for(int i = 1 ; i < (x & -x) ; i <<= 1){
    				data[x] = std::max(data[x] ,data[x - i]);
    			}
    			x += x & -x;
    		}
    	}
    	int qmax(int l ,int r){
    		int ans = INT_MIN;
    		while(l <= r){
    			if(r - (r & -r) < l - 1){
    				ans = std::max(ans ,a[r]);
    				--r;
    			}
    			else{
    				ans = std::max(ans ,data[r]);
    				r -= r & -r;
    			}
    		}
    		return ans;
    	}
    }C;
    

    随时更新……

  • 相关阅读:
    csuoj 1111: 三家人
    csu oj 1339: 最后一滴血
    csuoj 1337: 搞笑版费马大定理
    csuoj 1334: 好老师
    csu oj 1330 字符识别?
    C++动态内存分配
    变量内存分配
    codevs 2235 机票打折
    contesthunter CH Round #64
    面试分享:一年经验初探阿里巴巴前端社招
  • 原文地址:https://www.cnblogs.com/imakf/p/14071208.html
Copyright © 2011-2022 走看看