zkw线段树是zkw大神搞的自底向上线段树,以常数小,代码短著称。然而zkw大神的原ppt中描述简单,想了好长时间才想粗来。
以下内容针对区间最小值,使用更好理解的递归方式描述。
定义
zkw线段树定义如下:
1. 它是一棵满二叉树
2. 他的叶节点是一个数
3. 每一个非叶节点是一个数,且这个数是它的两个孩子中的较小值
显然,zkw线段树和普通线段树类似。他的叶节点从左到右是一个数列A1..n,非叶节点存一些信息以便查询区间最小值。
由于它是一个满二叉树,可以用堆式储存法储存。特别的,由于叶节点的个数为2的正整数幂,对于数据规模n,叶子节点的实际个数为
性质
- 不难发现,一棵有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(区间大小)。列递归式:
解得算法最坏情况为
- 假设没有上升,则效率最好,列递归式
解得最好情况为
所以求区间最值复杂度为
点修改操作
点修改十分简单,因为第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); } }
复杂度分析
由递归版本得出递归式
解得
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; }
来源:https://www.cnblogs.com/ljt12138/p/6684395.html