这篇文章上次修改于 371 天前,可能其部分内容已经发生变化,如有疑问可询问作者。

ACM程序课算法笔记13——线线段树

线段树介绍

线段树的功能是维护一个数组,当然你可以用普通数组维护,线段树只是一种维护方式,它的特点是可以在O(LogN)的时间复杂度完成单点修改,区间修改,区间查询。

线段树结构

线段树的结构目的是为了维护区间的某个信息,比如总和,最大值,最小值等等。

按照树的思想,把区间进行多次的分割,同时,线段树使用数组存储树,使用数组表示树可以使根节点编号为1。

例如区间[1,5] 我们根据树先进行分割一次[1, 3]和[4, 5]这就是一个根节点变成两个子节点。同时在对它们俩进行一个拆分,此时[1,3]变成[1,2]和[3,3]。[4, 5]拆成[4,4]和[5,5]结构如下图

segt1.svg

还有一点需要注意的是,线段树不一定要维护总和,我们可以用一个节点来做一些例子:(此处的加就负责节点的合并)

struct Node{
		int valSum;
		int valMax;
		int valMin;
		int lazy;
		Node operator+(Node n2){
				Node newNode;
				newNode.valSum = valSum + n2.valSum;
				newNode.valMax = max(valMax, n2.valMax);
				newNode.valMin = min(valMin, n2.valMin);
				return newNode;
		}
    Node operator+(int val){
				Node newNode;
				newNode.valSum += val;
				newNode.valMax += val;
				newNode.valMin += val;
				return newNode;
		}
}

线段树的构建

对于建立一个这样的空间,我们可以发现很多都是在做重复的工作,所以不妨使用递归。再从下往上构建,颇有回溯味道。再根据数组储存树的基础知识,获取左子节点的方法是 index * 2 右子节点是 index * 2 +1

void build(vector<Node> &a, int left, int right, int arrNo) {
	  if (left == right) {
		    arr[arrNo] = a[left];
		    return;
	  }
	  int mid = (left + right) / 2; // 当然你可以选择防溢出
	  
	  int leftTreeNo = arrNo * 2;
	  int rightTreeNo = arrNo * 2 + 1;
	  
	  build(a, left, mid, leftTreeNo);
	  build(a, mid + 1, right, rightTreeNo);
		// 特征值向上转移,这里使用相加,当然还可以使用其它方式
	  arr[arrNo] = arr[leftTreeNo] + arr[rightTreeNo];
}

线段树的区间查询

线段树的区间查询不一定是,这里我们以求和为例子,也就是arr[1]储存1~5所有值的和,每个元素管理的区间的区间特征值作为它的值。同样,你也可以使用结构体来代表一个节点,这样就可以储存多个值。

线段树的区间查询有一个特点,一次查询最后是两个节点的和。例如当我求区间[2, 5] 实际上访问的就是节点[2, 2]和[3, 5]两个节点的和并。

为什么是两个:已知如果是多个一定连续,如果两个连续大概率合并成一个。

所以对于区间查询来说,主要问题就是如何找到这两个节点。

Node getsum(int queryLeft, int queryRight, int nodeLeft, int nodeRight, int node) {
	  if (queryLeft <= nodeLeft && nodeRight <= queryRight)
	    return arr[node];
	  
	  int mid = (nodeLeft + nodeRight) / 2; // 当然你可以选择防溢出
	  int leftTreeNo = node * 2;
	  int rightTreeNo = node * 2 + 1;
	  Node sum = {0, MAX, MIN, 0};
	  
	  if (queryLeft <= mid) 
			  sum = sum + getsum(queryLeft, queryRight, nodeLeft, mid, leftTreeNo);
	  if (mid < queryRight) 
			  sum = sum + getsum(queryLeft, queryRight, mid + 1, nodeRight, rightTreeNo);
	  return sum;
}

通过递归从上往下去寻找即可。见上代码。

线段树的区间修改

根据简单的判断我们可以得出,线段树如果要修改一个值,就要同步修改上面的所有值,但如果修改一个区间,同样我们要进行很大的成本修改,对此,咱们提出了懒惰标级。

懒惰标记

懒惰标记的目的就是延迟对具体底下的值进行修改。例如我要修改的是区间[1, 3]的所有值,那我先只改[1, 2]和[3, 3],如果我之后查询的是[1, 4] 和 [1, 5]那么实际上就没有必要落实到1和2个题元素上,那我们就把这个修改暂时留在[1, 2]的这个节点上,等到我真的要去查询[1, 1]或者[2, 2],我再把这个修改下沉。

以下是关于懒惰标记的区间更新(区间增加或者区间减少)

void update(int queryLeft, int queryRight, int nodeLeft, int nodeRight, int node, int add) {
  if (queryLeft <= nodeLeft && nodeRight <= queryRight) {
		  arr[node] = arr[node] + (nodeRight - nodeLeft + 1) * add;
        
	    arr[node].lazy += add;
	    return;
  } 
  int mid = (nodeLeft + nodeRight) / 2;
  int leftTreeNo = node * 2;
  int rightTreeNo = node * 2 + 1;
  
  if (arr[node].lazy and nodeLeft != nodeRight) { // 向下转移懒惰标记
	    arr[leftTreeNo] = arr[leftTreeNo] + arr[node].lazy * (mid - nodeLeft + 1);
	    arr[rightTreeNo] = arr[rightTreeNo] + arr[node].lazy * (nodeRight - mid);
	    
	    arr[leftTreeNo].lazy += arr[node].lazy;
	    arr[rightTreeNo].lazy += arr[node].lazy;
	    
	    arr[node].lazy = 0;
  }
  
  if (queryLeft <= mid) 
		  update(queryLeft, queryRight, nodeLeft, mid, leftTreeNo, add);
  if (mid < queryRight) 
		  update(queryLeft, queryRight, mid + 1, nodeRight, rightTreeNo, add);
	
  arr[node] = arr[leftTreeNo] + arr[rightTreeNo];
}

以下是关于懒惰标记的区间查询

我们说过,一旦查询到下面,就需要对懒惰标记进行下移

Node getsum(int queryLeft, int queryRight, int nodeLeft, int nodeRight, int node) {
	  if (queryLeft <= nodeLeft && nodeRight <= queryRight)
	    return arr[node];
	  
	  int mid = (nodeLeft + nodeRight) / 2;
	  int leftTreeNo = node * 2;
	  int rightTreeNo = node * 2 + 1;
	  
	  if (arr[node].lazy and nodeLeft != nodeRight) { // 向下转移懒惰标记
	    arr[leftTreeNo] = arr[leftTreeNo] + arr[node].lazy * (mid - nodeLeft + 1);
	    arr[rightTreeNo] = arr[leftTreeNo] + arr[node].lazy * (nodeRight - mid);
	    
	    arr[leftTreeNo].lazy += arr[node].lazy;
	    arr[rightTreeNo].lazy += arr[node].lazy;
	    
	    arr[node].lazy = 0;
  }
	  
	  Node sum = {0, MAX, MIN, 0};
	  if (queryLeft <= mid) 
			  sum = sum + getsum(queryLeft, queryRight, nodeLeft, mid, leftTreeNo);
	  if (mid < queryRight) 
			  sum = sum + getsum(queryLeft, queryRight, mid + 1, nodeRight, rightTreeNo);
	  return sum;
}

当然也可以把对区间的操作从“都增加一个值add”变成“都变成一个值x”。

此时的lazy标签就代表要改的值了,但需要注意一点,主要另一个值来判断是否要修改,因为lazy标签为0时,代表的是全部值改为0而并不是不修改元素。

一些优化

  • 在叶子节点处无需下放懒惰标记,所以懒惰标记可以不下传到叶子节点。
  • 动态创建子节点,在不访问的时候就不创建节点。
  • 标记永久化:如果确定懒惰标记不会在中途被加到溢出,那么就可以将标记永久化。标记永久化可以避免下传懒惰标记,只需在进行询问时把标记的影响加到答案当中,从而降低程序常数。也就是不需要修改子节点的值,只需要一直利用路途中的懒惰标记即可。

25.3.4更新

jiangly线段树板子

#define ls p << 1
#define rs p << 1 | 1
#define Mid (pl + pr >> 1) 
template<class Info, class Tag>
struct LazySegmentTree {
    int n;
    vector<Info> info;
    vector<Tag> tag; 
    LazySegmentTree(int siz) : n(siz) {
        info.resize(n << 2);
        tag.resize(n << 2);
    }

    LazySegmentTree(vector<Info>& init, int siz) : n(siz) {
        info.resize(n << 2);
        tag.resize(n << 2);
        auto build = [&](auto&& self, int p, int pl, int pr) -> void {
            if (pl == pr) {
                info[p] = init[pl];
                return;
            }
            self(self, ls, pl, Mid);
            self(self, rs, Mid + 1, pr);
            push_up(p);
        };
        build(build, 1, 1, n);
    }
    
    void push_up(int p) {
        info[p] = info[ls] + info[rs];
    }

    void apply(int p, const Tag& t) {
        info[p].apply(t);
        tag[p].apply(t);
    }

    void push_down(int p) {
        apply(ls, tag[p]);
        apply(rs, tag[p]);
        tag[p] = Tag();
    }
    
    void update(int x, int p, int pl, int pr, const Info& a) {
        if (pl == pr) {
            info[p] = a;
            return;
        }
        push_down(p);
        if (x <= Mid) update(x, ls, pl, Mid, a);
        else update(x, rs, Mid + 1, pr, a);
        push_up(p);
    }

    void update(int x, const Info& a) {
        update(x, 1, 1, n, a);
    }

    void update(int L, int R, int p, int pl, int pr, const Tag& t) {
        if (L <= pl && pr <= R) {
            apply(p, t);
            return;
        }
        push_down(p);
        if (L <= Mid) update(L, R, ls, pl, Mid, t);
        if (R > Mid) update(L, R, rs, Mid + 1, pr, t);
        push_up(p);
    }

    void update(int L, int R, const Tag& t) {
        update(L, R, 1, 1, n, t);
    }
    
    Info query(int L, int R, int p, int pl, int pr) {
        if (L > pr || R < pl) return Info();
        if (L <= pl && pr <= R) return info[p];
        push_down(p);
        return query(L, R, ls, pl, Mid) + query(L, R, rs, Mid + 1, pr);
    }

    Info query(int L, int R) {
        return query(L, R, 1, 1, n);
    }
    
    bool check(const Info& a) {
        return 1;
    }

    Info temp;
    int findFirst(int p, int pl, int pr, int L) {
        if (pr < L) return -1;
        if (pl >= L && !check(temp + info[p])) {
            temp = temp + info[p];
            return -1;
        }
        if (pl == pr) return pl;
        push_down(p);
        int res = findFirst(ls, pl, Mid, L);
        if (res == -1) {
            res = findFirst(rs, Mid + 1, pr, L);
        }
        return res;
    }
    
    int findFirst(int L) {
        temp = Info();
        return findFirst(1, 1, n, L);
    }
};

有更好的应用性,根据自己的使用选择。

// 懒标记,默认所有的元素都有一个初始的懒标记
struct Tag {
	int x {-1}; // 需要一个标记来代表当前标记是否为空,即所有元素都有一个空标记。
	void apply(const Tag &t) {
		// 懒标记结合
	}
};

struct Info {
	// 自行定义结构内函数 例如区间如下定义
	int l, r;
	Info() {}
	Info(int pos, int val) {
		l = r = pos;// 这是一个维护区间的定义
	}

	void apply(const Tag &t) {
		if (t.x == -1) {
			return; // 无论Tag是否为空,在pushdown都会对元素进行apply一个初始标记
		}
		// 懒标记非空,将懒标记生效,运行完后,该节点上的懒标记会被重新初始化。
	}
};

Info operator+ (const Info &a, const Info &b) {
	// 处理两个节点相加的情况,需要自行判断结合。
	return newNode;
}

以下作为使用例,多调试多使用。

struct Tag {
	int x {-1};

	void apply(const Tag &t) {
		if (t.x > 0) {
			x = t.x;
		}
	}
};

struct Info {
	int st[4] = {0};
	int l = 0, r = 0;

	ll f[4] = {0};

	Info() {}
	Info(int p, int x) {
		l = r = p;
		for (int i = 1; i <= 3; i++) {
			st[i] = i == x ? x : 6 - x - i;
			f[i] = i == x ? 0 : 1;
		}
	}

	void apply(const Tag &t) {
		if (t.x == -1) {
			return;
		}
		for (int i = 1; i <= 3; i++) {
			st[i] = i == t.x ? t.x : 6 - t.x - i;
			f[i] = i == t.x ? 0 : 1;
		}
	}
};

Info operator+ (const Info &a, const Info &b) {
	if (a.l == 0) {
		return b;
	}

	if (b.l == 0) {
		return a;
	}
	Info c;
	c.l = a.l;
	c.r = b.r;

	ll v = t[a.r - a.l + 1];
	for (int i = 1; i <= 3; i++) {
		c.st[i] = a.st[b.st[i]];
		c.f[i] = (v * b.f[i] % mod + a.f[b.st[i]]) % mod;
	}

	return c;
}