浅谈线段树、树状数组和分块
都要用到分配律,不满足的不能使用这种算法。
线段树
其实树状数组是比线段树简单的。但是因为树状数组能做的,线段树也能做,但是线段树能做树状数组不能做的。
建树
用一张图表示线段树:
呃呃呃可能有点抽象,把它改成线段就好理解了(但是上面这份图对于熟悉的人更清晰):
诶太丑了不放了,大家理解就好了(
其实和普通二叉树一样,只是线段树的每个节点存储的是一个区间,也就是这个区间的信息(和、积等)能通过子节点推出来。
给出建树代码:
点击查看代码
void build(int i , int l , int r){
tree[i] = {l,r,0,0};
if(l==r){
tree[i].sum = input[l];
return;
}
int mid = (l + r) / 2;
build(2*i,l,mid);
build(2*i+1,mid+1,r);
tree[i].sum = tree[2*i].sum + tree[2*i+1].sum;
}
这里我用的是结构体,方便一点,还有一种写法是在每次区间操作时重新计算两个端点,更节省空间。
区间修改
用区间加举例。
显然我们知道 \(a\times k=a\times (k/2)+a\times (k/2)\)。那么就可以对于要操作的区间,如果不能全部覆盖,那么传给它的子节点操作,然后重新计算该节点的值。
也很好理解,对吧。不过有一个问题,如果一个区间被完全覆盖,那么它的子节点的区间也要改变,但是实际上并不需要一直修改到最低端,这时候就要用到懒标记(\(lazytage\),简称 \(lz\))了。
懒标记
这就是线段树的精髓。懒标记思想就是要用的时候再下传,即不用的时候就放在那里,节省时间。注意懒标记要可以叠加,如果有多种区间操作记得分清先后顺序。
给处代码实现(包含区间加):
点击查看代码
void push_down(int i){
if(!tree[i].lz)return;
int k = tree[i].lz;
tree[2*i].sum += (tree[2*i].r - tree[2*i].l + 1) * k , tree[2*i+1].sum += (tree[2*i+1].r - tree[2*i+1].l + 1) * k;
tree[2*i].lz += k , tree[2*i+1].lz += k;
tree[i].lz = 0;
return;
}
void add(int i , int l , int r , int k){
if(tree[i].l>=l&&tree[i].r<=r){
tree[i].sum += (tree[i].r - tree[i].l + 1) * k , tree[i].lz += k;
return;
}<details>
push_down(i);
if(tree[2*i].r>=l)add(2*i,l,r,k);
if(tree[2*i+1].l<=r)add(2*i+1,l,r,k);
tree[i].sum = tree[2*i].sum + tree[2*i+1].sum;
}
区间查询
思想和区间加差不多,也是没有完全被覆盖就传给子节点操作,最后把区间和全部加起来。
点击查看代码
int search(int i , int l , int r){
if(tree[i].l>=l&&tree[i].r<=r)return tree[i].sum;
push_down(i);
int sum = 0;
if(tree[2*i].r>=l)sum += search(2*i,l,r);
if(tree[2*i+1].l<=r)sum += search(2*i+1,l,r);
return sum;
}
动态开点线段树
不是很常用但是有用。思想就是尽量少开节点,需要用在开新的节点。
注意这里需要记录左子结点的右子节点的编号。
需要用到标记永久化的思想。
树状数组
树状数组的码量比线段树少,也简单一些。
其中 \(c\) 就是树状数组。我们先不关心左边界,感受一下树状数组的工作原理:
假设要求 \(3\sim 5\) 的元素和,也就是求 \(1\sim 5\) 的元素和 \(-1\sim 2\) 的元素和。
首先找到 \(c_5\) 管辖 \(5\sim 5\),加入计数器,往前跳到 \(4\),发现 \(c_4\) 管辖 \(1\sim 4\),加入计数器,跳到到 \(0\),超出边界,结束。最后 \(1\sim 5\) 的和 \(=c_4+c_5\)。
同理 \(1\sim 5\) 的和 \(=c_2\),相减即可。
管辖区间
树状数组规定 \(c_x\) 的管辖区间长度为 \(2^k\),表示 \(x\) 的二进制中最后一个 \(1\) 和后面所有 \(0\) 组成的二进制数。
lowbit
上面被称为“\(x\) 的\(lowbit\)”,具体求法:
我们知道 \(-x\) 的二进制编码为 \(\sim x+1\),即对 \(x\) 的二进制编码按位取反,然后 \(+1\)。
例如 \(6\) 为110,则 \(-6\) 为010按位与一下 \(=\) 010,就求出来了 \(6\) 的 \(lowbit\)。
好的 \(x\) 的 \(lowbit=x\& (-x)\)
区间查询
一直往前跳 \(lowbit\) 就好。
点击查看代码
int search(int x){
int ans = 0;
for(;x>0;x-=lowbit(x))ans += tree[x];
return ans;
}
单点修改
只需要修改管辖 \(i\) 能对区间查询产生贡献的所有位置就行了,一直往后跳 \(lowbit\)。
点击查看代码
void add(int i , int x){
for(;i<=n;i+=lowbit(i))tree[i] += x;
}
区间修改
一般用到差分的思想,即只修改 \(l\) 和 \(r+1\),注意这里区间查询最后的输出也要改变一下。
建树
n次单点修改。
二维树状数组
再多加一个维度就好,修改或查询也是跳 \(lowbit\)。
权值线段树
一个序列 \(a\) 的权值数组 \(b\),满足 \(b[x]\) 的值为 \(x\) 在 \(a\) 中的出现次数。例如:\(a = (1, 3, 4, 3, 4)\) 的权值数组为 \(b = (1, 0, 2, 2)\)。很明显,\(b\) 的大小和 \(a\) 的值域有关。
若原数列值域过大,且重要的不是具体值而是值与值之间的相对大小关系,常离散化原数组后再建立权值数组。
另外,权值数组是原数组无序性的一种表示:它重点描述数组的元素内容,忽略了数组的顺序,若两数组只是顺序不同,所含内容一致,则它们的权值数组相同。
单点修改,查询全局第 \(k\) 小
在此处只讨论第 \(k\) 小,第 \(k\) 大问题可以通过简单计算转化为第 \(k\) 小问题。
该问题可离散化,如果原序列 \(a\) 值域过大,离散化后再建立权值数组 \(b\)。注意,还要把单点修改中的涉及到的值也一起离散化,不能只离散化原数组 \(a\) 中的元素。
对于单点修改,只需将对原数列的单点修改转化为对权值数组的单点修改即可。具体来说,原数组 \(a[x]\) 从 \(y\) 修改为 \(z\),转化为对权值数组 \(tree\) 的单点修改就是 \(tree[y]\) 单点减 \(1\),\(b[z]\) 单点加 \(1\)。
对于查询第 \(k\) 小,考虑二分 \(x\),查询权值数组中 \([1, x]\) 的前缀和,找到 \(x_0\) 使得 \([1, x_0]\) 的前缀和 \(< k\) 而 \([1, x_0 + 1]\) 的前缀和 \(\ge k\),则第 \(k\) 大的数是 \(x_0 + 1\)(注:这里认为 \([1, 0]\) 的前缀和是 \(0\))。
这样做时间复杂度是 \(O(log^2n)\) 的。
考虑用倍增替代二分。
设 \(x = 0\),\(sum = 0\),枚举 \(i\) 从 \(log_2n\) 降为 \(0\):
查询权值数组中 \([x + 1 \ldots x + 2^i]\) 的区间和 \(t\)。如果 \(sum + t < k\),扩展成功,\(x \gets x + 2^i\),\(sum \gets sum + t\) 否则扩展失败,不操作。
这样得到的 \(x\) 是满足 \([1 \ldots x]\) 前缀和 \(< k\) 的最大值,所以最终 \(x + 1\) 就是答案。
看起来这种方法时间效率没有任何改善,但事实上,查询 \([x + 1 \ldots x + 2^i]\) 的区间和只需访问 \(tree[x + 2^i]\) 的值即可。
如此一来,时间复杂度降低为 \(O(log n)\)。
全局逆序对
首先需要离散化。
接下来这样做:(倒序枚举,设 \(a_i=x\)):
- 查询 \([1,\ldots,x-1]\) 的和,答案加上这个数。
- \(tree[x]\) 加一。
这种很容易证明,在这里我就不证了。不严格逆序(\(i<j,a[i]\ge a[j]\),注意在这里 \(i \le j\) 与 \(i<j\) 不等价)对稍改一下就可以。
分块
分块的思想就是把序列分成很多快操作。
接下来统一块长为 \(S\)。
修改
如果区间在同一块中则暴力修改,最坏 \(O(S)\)。
否则分成两段未被完全覆盖的段(暴力修改)和其他被完全覆盖的段(整体修改),最坏 \(O(max(n/s,s))\)。
查询
和修改思想一样,未被完全覆盖就暴力查询,否则直接求整体。
不难发现块长最优为 \(\sqrt n\)。
点击查看代码
int op , l , r , k;cin >> op >> l >> r >> k;
if(op==0){
if(id[l]==id[r]){
for(int i=l;i<=r;i++)a[i] += k , s[id[l]] += k;
}else{
int i;
for(i=l;id[i]==id[l];i++)a[i] += k , s[id[l]] += k;
for(;i+b<r;i+=b)sum[id[i]] += k , s[id[i]] += b * k;
for(;i<=r;i++)a[i] += k , s[id[i]] += k;
}
}else{
int ans = 0;
if(id[l]==id[r]){
for(int i=l;i<=r;i++)ans += a[i] + sum[id[i]];
}else{
int i;
for(i=l;id[i]==id[l];i++)ans += a[i] + sum[id[i]];
for(;i+b<r;i+=b)ans += s[id[i]];
for(;i<=r;i++)ans += a[i] + sum[id[i]];
}
cout << ((ans % (k + 1) + (k + 1)) % (k + 1)) << "\n";
}

浙公网安备 33010602011771号