zkw线段树是zkw大神搞的自底向上线段树,以常数小,代码短著称。然而zkw大神的原ppt中描述简单,想了好长时间才想粗来。
以下内容针对区间最小值,使用更好理解的递归方式描述。
定义
zkw线段树定义如下:
1. 它是一棵满二叉树
2. 他的叶节点是一个数
3. 每一个非叶节点是一个数,且这个数是它的两个孩子中的较小值
显然,zkw线段树和普通线段树类似。他的叶节点从左到右是一个数列A1..n,非叶节点存一些信息以便查询区间最小值。
由于它是一个满二叉树,可以用堆式储存法储存。特别的,由于叶节点的个数为2的正整数幂,对于数据规模n,叶子节点的实际个数为
2lgn+1
,没有数据的节点用
∞
填充。
性质
- 不难发现,一棵有tn个节点的满二叉树有tn-1个非叶子节点。所以在堆式结构中,第i个叶子节点的位置是
tree[tn-1+i]
。这是一个很重要的性质,zkw线段树的许多操作是建立在他的基础上的。 - 由1容易得出,对于树上(堆中)任意一个位置i,如果i是偶数,那么它是一个左孩子;否则是一个右孩子。
1
2 3
4 5 6 7
//显然,所有奇数都是右孩子,偶数都是左孩子
建立数据结构
直接用静态数组建立即可,i的左右孩子分别为i*2,i*2+1
const int maxn = 100000;
// 最多节点数
int tree[maxn*4]; // 足够大,防止越界
int n;
// 数据规模
int tn;
// 叶子节点个数
我们定义一些函数来方便下面的操作
inline int twice(int a)
{
return a<<1;
// 二倍
}
inline int half(int a)
{
return a>>1;
// 一半
}
inline bool rightc(int a)
{
return a&1;
// 性质2得出
}
inline void fix(int i) {
tree[i] = min(tree[twice(i)], tree[(twice(i)+1)]);
// 求tree[i]并更新
}
建树
zkw线段树的建树和普通线段树一样,这里不在赘述。不过由于使用了堆式储存,代码大大简化。
int make_tree(int i)
{
if (i >= tn)
return tree[i];
// 到达叶子节点
return tree[i] = min(
make_tree(twice(i)),
make_tree(twice(i)+1));
// 否则为左右孩子最小值
}
其中,叶节点即 tree[tn]及以后内容已经被输入,因此直接返回即可。
非递归方式也很简单,递推即可。
void make_tree()
{
for (int i=tn-1; i>=1; i--)
fix(i);
}
复杂度显然为Θ(n)。
查询区间最小值
这就是zkw线段树的精华所在。zkw线段树的更新和查询都是 自底向上 的。他的查询i..j最小值递归方法如下:
1. 找到i, j的实际位置(在调用时处理),为i = tn-1+i, j = tn-1+j
2. 如果j=i,原区间最小值为tree[i];如果j-i==1,原区间最小值为
3. 如果i 是右孩子 ,则原区间最小值为tree[i]和区间i+1..j最小值中的较小值;如果j 是左孩子 ,则原区间最小值是tree[j]和区间i..j-1最小值中的较小值。
4. 否则原区间最小值为区间i/2..j/2的最小值
这种方法的正确性是显然的,只要自己动手试一试就可以明白。2是边界,3是fix;4则运用了zkw线段树的性质——i/2包含了i,i+1两个子树中的最小值,这意味着i+1这个子树不必再进行计算,只需要直接使用i/2的值。
这是一个良好的 尾递归 算法,这意味着即使你不将递归改为循环,编译器也会自动优化他从而使他的效率可以与循环媲美。我们在最后给出循环版本的算法描述。
/*
* 查询i..j的区间应该调用
* ask(tn-1+i, tn-1+j)
*/
int ask (int i, int j)
{
if (i == j)
return tree[i];
// 只包含一个元素
if (j-i == 1)
return min(tree[i],tree[j]);
// case 2
if (rightc(i))
return min(tree[i],ask(i+1,j));
if (!rightc(j))
return min(tree[j],ask(i,j-1));
// case 3
return ask(half(i),half(j));
// case 4
}
分析算法复杂度
不妨将操作3称为“平移”(左右两边算一次操作),操作4称为“上升”。显然平移操作越多,算法运行的越慢。然而 不可能连续进行两次平移操作 ,所以最多只有一半的操作是平移。
再分析上升。显然的是,一次上升后,区间大小j-i会变成 j/2-i/2 = (j-i)/2
,即区间缩小一倍。
- 假设共进行一半平移操作。
既然这样,就可以把一次平移和一次上升看作一个整体。n为j-i(区间大小)。列递归式:
解得算法最坏情况为
O(lgn)
。
- 假设没有上升,则效率最好,列递归式
解得最好情况为
Ω(lgn)
。
所以求区间最值复杂度为
Θ(lgn)
。
点修改操作
点修改十分简单,因为第i个数简单的是 tree[tn-1+i]
,所以不再有向下试探的操作,只需要自顶向上的修改即可。
递归形式的代码(同样是尾递归的)
// tree[tn-1+i] = j
// change(tn-1+i)
void change(int i) {
if (i == 1) return;
fix(half(i));
change(half(i));
}
循环也不难得出
void change(int i, int j) {
i += tn-1;
tree[i] = j;
i = half(i);
while (i != 1) {
fix(i);
i = half(i);
}
}
复杂度分析
由递归版本得出递归式
解得 T(n)=Θ(lgn)
zkw线段树的空间优越性
你也许会认为开一个so big的数组空间会爆,实际上恰恰相反。 zkw线段树的空间利用率高于普通线段树 。这是因为普通线段树有大量的 指针 占用空间,zkw只使用下标索引,空间大大降低。
经测试,zkw线段树占用空间为普通线段树的一半(最大数据)。
zkw线段树为什么是高效的
zkw线段树自底向下是他的先天优势,这意味着他不再需要向下试探。zkw线段树算法是 一次方法 ,因此不论是递归还是循环都减少了一半的工作量。且自底向上不需要区间覆盖的几种情况, 编程复杂度 大大降低。
完整程序
例题为tyvj模板题 忠诚2
代码本来可以更紧凑,但是为了保留可读性,写的并没有zkw大神的魔性 。
最后Orz zkw大神
循环版
#include <iostream>
#include <cstdio>
#include <cmath>
#include <cstring>
using namespace std;
const int maxn = 100000;
int tree[maxn*4];
int n;
int tn;
inline int twice(int a)
{
return a<<1;
}
inline int half(int a)
{
return a>>1;
}
inline bool rightc(int a)
{
return a&1;
}
inline void fix(int i) {
tree[i] = min(tree[twice(i)], tree[(twice(i)+1)]);
}
void make_tree()
{
for (int i=tn-1; i>=1; i--) fix(i);
}
int ask (int i, int j)
{
int ans = 100000000;
for (i+=tn-1,j+=tn-1; j>i;i=half(i),j=half(j)) {
if (rightc(i)) ans = min(ans,tree[i++]);
if (!rightc(j)) ans = min(ans,tree[j--]);
}
if (i == j) ans = min(ans, tree[j]);
return ans;
}
void change(int i, int j) {
i += tn-1;
tree[i] = j;
i = half(i);
while (i != 1) {
fix(i);
i = half(i);
}
}
int main() {
int m,x,y;
int c;
scanf ("%d%d", &n,&m);
memset(tree,127,sizeof tree);
tn = n*2;
tn = 1<<(int)(log(tn)/log(2));
for (int i = 1; i <= n; i++)
scanf ("%d", &tree[tn-1+i]);
make_tree();
for (int i = 1; i <= m; i++) {
scanf("%d %d %d",&c,&x,&y);
if (c == 1)
printf("%d ",ask(x, y));
else {
change(x, y);
}
}
return 0;
}
递归版
#include <iostream>
#include <cstdio>
#include <cmath>
#include <cstring>
using namespace std;
const int maxn = 100000;
inline int twice(int a)
{
return a<<1;
}
inline int half(int a)
{
return a>>1;
}
inline bool rightc(int a)
{
return a&1;
}
int tree[maxn*4];
int n;
int tn;
int make_tree(int i)
{
if (i >= tn)
return tree[i];
return tree[i] = min(
make_tree(twice(i)),
make_tree(twice(i)+1));
}
int ask (int i, int j)
{
if (i == j)
return tree[i];
if (j-i == 1)
return min(tree[i],tree[j]);
if ( rightc (i))
return min(tree[i],ask(i+1,j));
if (! rightc (j))
return min(tree[j],ask(i,j-1));
return ask(half(i),half(j));
}
inline void fix(int i) {
tree[i] = min(tree[twice(i)], tree[(twice(i)+1)]);
}
void change(int i) {
if (i == 1) return;
fix(half(i));
change(half(i));
}
int main() {
int m,x,y;
int c;
scanf ("%d%d", &n,&m);
memset(tree,127,sizeof tree);
tn = n*2;
tn = 1<<(int)(log(tn)/log(2));
for (int i = 1; i <= n; i++)
scanf ("%d", &tree[tn-1+i]);
make_tree(1);
for (int i = 1; i <= m; i++) {
scanf("%d %d %d",&c,&x,&y);
if (c == 1)
printf("%d ",ask(tn-1+x, tn-1+y));
else {
tree[tn-1+x] = y;
change(tn-1+x);
}
}
return 0;
}