线段树杂谈

动态开点线段树

引入

普通的线段树写法有一个显然的缺点——空间。堆式存贮使得我们开线段树时需要用 $ 4n $ 的空间。冗余空间高达 $ 2n $ 。而且,在大多数情况下线段树中并不是每个节点都会被用到,这时我们就可以使用动态开点线段树,不仅所用的空间小,实现起来的代码也比普通线段树短。

思想

动态开点线段树,顾名思义,就是一棵使用动态开点的线段树。(废话

在常用的堆式存储中,我们用 $ p \times 2 $ 和 $ p \times 2 + 1 $ 来表示节点 $ p $ 的两个儿子。而在动态开点线段树中,我们用 $ lson $ 和 $ rson $ 两个数组来记录每个节点的两个儿子的编号。并且 节点只有在被用到的时候才创建。这样,我们就能有效避免冗余空间的出现。

在修改时,如果我们发现当前节点并没有在线段树上,那么我们就创建这个节点。

//模板(区间修改)
void modify(int &p, int L, int R, int l, int r, int c) {//L,R表示当前节点所包含的区间,l, r表示修改区间
    //注意到是&p,这样可以使得上一层递归中不用再次手动修改传入的节点
    if(!p) p = ++tot;//创建新节点
    if(l <= L && R <= r) {
        ······//修改
        return ;
    }
    pushdown(p, L, R);
    int mid = L + R >> 1;
    if(l <= mid) modify(t[p].ls, L, mid, l, r, c);
    if(r > mid) modify(t[p].rs, mid + 1, R, l, r, c);
    pushup(p);
}

在询问时,如果当前节点并未被创建,那么就可以返回 $ 0 $ 。这是因为如果当前节点没有被创建,说明这个区间没有被修改过,换句话说,这个区间所维护的东西不存在,即为 $ 0 $。

//模板(区间查询)
int query(int p, int L, int R, int l, int r) {
    if(!p) return 0;
    if(l <= L && R <= r) return ···;//返回区间所维护的东西
    pushdown(p, L, R);
    int mid = L + R >> 1, res = 0;
    if(l <= mid) ···; //查询并更新
    if(r > mid) ···;
    return res;
}

如果线段树上有初值的话,我们可以将其看做若干个修改。这样就不用写 $ build $ 函数了。

例题

$ \color {#52c41a} P3372 【模板】线段树 1 $

题意

如题,已知一个数列,你需要进行下面两种操作:

  1. 将某区间每一个数加上 $ k $ 。
  2. 求出某区间每一个数的和。
思路

线段树维护。

代码
#include<bits/stdc++.h>
#define int long long
#define Debug puts("Oops!")
using namespace std;

const int N = 1e5 + 5, M = 5e5 + 5;

int n, m;

struct Node {
    int ls, rs;
    int lazy, dat;
};

struct Segment_Tree {
    int root, tot;
    Node t[N << 1];
    void pushup(int p) {t[p].dat = t[t[p].ls].dat + t[t[p].rs].dat;}
    void pushdown(int p, int l, int r) {
        if(!t[p].ls) t[p].ls = ++tot;
        if(!t[p].rs) t[p].rs = ++tot;
        t[t[p].ls].lazy += t[p].lazy;
        t[t[p].rs].lazy += t[p].lazy;
        int mid = l + r >> 1;
        t[t[p].ls].dat += t[p].lazy * (mid - l + 1);
        t[t[p].rs].dat += t[p].lazy * (r - mid);
        t[p].lazy = 0;
    }
    void add(int &p, int L, int R, int l, int r, int c) {
        if(!p) p = ++tot;
        if(l <= L && R <= r) {
            t[p].dat += c * (R - L + 1);
            t[p].lazy += c;
            return ;
        }
        pushdown(p, L, R);
        int mid = L + R >> 1;
        if(l <= mid) add(t[p].ls, L, mid, l, r, c);
        if(r > mid) add(t[p].rs, mid + 1, R, l, r, c);
        pushup(p);
    }
    int query(int p, int L, int R, int l, int r) {
        if(!p) return 0;
        if(l <= L && R <= r) return t[p].dat;
        pushdown(p, L, R);
        int mid = L + R >> 1, res = 0;
        if(l <= mid) res += query(t[p].ls, L, mid, l, r);
        if(r > mid) res += query(t[p].rs, mid + 1, R, l, r);
        return res;
    }
}st;

inline int read() {
	int x = 0, f = 1; char c = getchar();
	while(!isdigit(c)) {if(c == '-') f = -1; c = getchar();}
	while(isdigit(c)) x = x * 10 + c - '0', c = getchar();
	return x * f;
}

signed main() {
//	freopen(".in", "r", stdin);
//	freopen(".out", "w", stdout);
    n = read(), m = read();
    for(int i = 1, x; i <= n; i++) {
        x = read();
        st.add(st.root, 1, n, i, i, x);
    }
    while(m--) {
        int op = read(), l =  read(), r = read();
        if(op == 1) {
            int x = read();
            st.add(st.root, 1, n, l, r, x);
        }
        else {
            cout << st.query(st.root, 1, n, l, r) << endl;
        }
    }
	return 0;
}

线段树合并

引入

顾名思义,线段树合并是指建立一棵新的线段树,这棵线段树的每个节点都是两棵原线段树对应节点合并后的结果。它常常被用于维护树上或是图上的信息。

过程

线段树合并的过程其实就是暴力遍历两颗线段树上的每个节点,并将其上的内容合并。

线段树合并的时间复杂度

显然,对于两颗满的线段树,合并操作的复杂度是 $ O(n\log n) $ 的。但实际情况下使用的常常是权值线段树,总点数和 $ n $ 的规模相差并不大。并且合并时一般不会重复地合并某个线段树,所以我们最终增加的点数大致是 $ n\log n $ 级别的。这样,总的复杂度就是 $ O(n\log n) $ 级别的。当然,在一些情况下,可并堆可能是更好的选择。

模板
void merge(int &x, int y, int l, int r) {
    //和动态开点线段树一样,&x可以减少码长。
    if(!x) {x = y; return ;}
    if(!y) return ;
    if(l == r) {t[x].s += t[y].s; return ;}
    int mid = l + r >> 1;
    merge(t[x].ls, t[y].ls, l, mid);
    merge(t[x].rs, t[y].rs, mid + 1, r);
    pushup(x);
}

例题

$ \color {#9d3dcf} P4556 [Vani有约会] 雨天的尾巴 /【模板】线段树合并 $

题意

首先村落里的一共有 $ n $ 座房屋,并形成一个树状结构。然后救济粮分 $ m $ 次发放,每次选择两个房屋 $ (x, y) $,然后对于 $ x $ 到 $ y $ 的路径上(含 $ x $ 和 $ y $)每座房子里发放一袋 $ z $ 类型的救济粮。

然后深绘里想知道,当所有的救济粮发放完毕后,每座房子里存放的最多的是哪种救济粮。

思路

显然可以想到使用树上差分来快速维护 $ x $ 到 $ y $ 的区间操作。

考虑如何快速更新差分数组。

容易想到在每个节点上开一棵动态开点线段树,维护每种救济粮的袋数以及区间最大值以及它的位置。

所有操作结束后树上前缀和恢复原数组,使用线段树合并。

代码
#include<bits/stdc++.h>
#define int long long
#define Debug puts("Oops!")
using namespace std;

const int N = 5e6 + 5, M = 5e5 + 5;

int n, m; 
int res[N];

int cnt = -1, head[N];
struct Edge {
	int nxt, to;
}e[N << 1];

void addedge(int u, int v) {
	cnt++;
	e[cnt].nxt = head[u];
	e[cnt].to = v;
	head[u] = cnt;
}

struct Node {
	int ls, rs;
	int s, pos;
};

struct Segment_Tree {
	int root[N];//储存每个节点的线段树的根节点编号
	Node t[N];
	int tot;
	void pushup(int p) {
		if(t[t[p].ls].s >= t[t[p].rs].s) t[p].s = t[t[p].ls].s, t[p].pos = t[t[p].ls].pos;
		else t[p].s = t[t[p].rs].s, t[p].pos = t[t[p].rs].pos;
	}
	void modify(int &p, int l, int r, int x, int c) {
		if(!p) p = ++tot;
		if(l == r) {
			t[p].s += c, t[p].pos = x;
			return ;
		}
		int mid = l + r >> 1;
		if(x <= mid) modify(t[p].ls, l, mid, x, c);
		if(x > mid) modify(t[p].rs, mid + 1, r, x, c);
		pushup(p);
	}
	void merge(int &x, int y, int l, int r) {
		if(!x) {x = y; return ;}
		if(!y) return ;
		if(l == r) {t[x].s += t[y].s; return ;}
		int mid = l + r >> 1;
		merge(t[x].ls, t[y].ls, l, mid);
		merge(t[x].rs, t[y].rs, mid + 1, r);
		pushup(x);
	}
}st;

//树剖求lca
int fa[N], son[N], dep[N], sz[N];
void dfs1(int x, int father) {
	fa[x] = father;
	sz[x] = 1;
	dep[x] = dep[fa[x]] + 1;
	for(int i = head[x]; ~i; i = e[i].nxt) {
		int y = e[i].to;
		if(y == father) continue;
		dfs1(y, x);
		sz[x] += sz[y];
		if(sz[y] > sz[son[x]]) son[x] = y;
	}
}

int top[N];
void dfs2(int x, int topx) {
	top[x] = topx;
	if(!son[x]) return ;
	dfs2(son[x], topx);
	for(int i = head[x]; ~i; i = e[i].nxt) {
		int y = e[i].to;
		if(y == fa[x] || y == son[x]) continue;
		dfs2(y, y);
	}
}

int lca(int x, int y) {
	while(top[x] != top[y]) {
		if(dep[top[x]] < dep[top[y]]) swap(x, y);
		x = fa[top[x]];
	}
	if(dep[x] > dep[y]) swap(x, y);
	return x;
}

//树上前缀和
void solve(int x) {
	for(int i = head[x]; ~i; i = e[i].nxt) {
		int y = e[i].to;
		if(y == fa[x]) continue;
		solve(y);
		st.merge(st.root[x], st.root[y], 1, 100000);
	}
	res[x] = st.t[st.root[x]].pos;
	if(st.t[st.root[x]].s == 0) res[x] = 0;
}

inline int read() {
	int x = 0, f = 1; char c = getchar();
	while(!isdigit(c)) {if(c == '-') f = -1; c = getchar();}
	while(isdigit(c)) x = x * 10 + c - '0', c = getchar();
	return x * f;
}

signed main() {
//	freopen(".in", "r", stdin);
//	freopen(".out", "w", stdout);
	memset(head, -1, sizeof head);
	n = read(), m = read();
	for(int i = 1, u, v; i < n; i++) {
		u = read(), v = read();
		addedge(u, v);
		addedge(v, u);
	}
	dfs1(1, 0);
	dfs2(1, 1);
	st.tot = n;
	while(m--) {
		int x = read(), y = read(), z = read(), f = lca(x, y);
        //树上差分
		st.modify(st.root[x], 1, 100000, z, 1);
		st.modify(st.root[y], 1, 100000, z, 1);
		st.modify(st.root[f], 1, 100000, z, -1);
		st.modify(st.root[fa[f]], 1, 100000, z, -1);
	}
	solve(1); 
	for(int i = 1; i <= n; i++) cout << res[i] << endl;
	return 0;
}

李超线段树

李超线段树是针对一类特定的题目而存在的:

$\color{#9d3dcf} 【模板】李超线段树 / [HEOI2013] Segment $

题目描述

要求在平面直角坐标系下维护两个操作:

  1. 在平面上加入一条线段。记第 \(i\) 条被插入的线段的标号为 \(i\)
  2. 给定一个数 \(k\),询问与直线 \(x = k\) 相交的线段中,交点纵坐标最大的线段的编号。
思路

线段不是很好处理,所以我们可以把它看做一个定义域为 $ (x_0, x_1) $ 的一次函数。

李超线段树的一个节点 $ (l, r) $ 里存的是在 $ mid = (l + r) / 2 $ 这一位置上最大的,且定义域包含 $ (l, r) $ 的一次函数。

对于一条新线段 $ f $,它能更新的节点只有被它完全包含的节点,所以我们现在线段树上找到那些被其完全包含的最大的节点。

如果新线段在 $ mid $ 上的值比原线段 $ g $ 大,则交换 $ f $ 和 $ g $。

考虑若新线段在 $ mid $ 上的值小于原线段如何更新子节点。

注意到由于线段是直的,所以 肯定有一条线段只能作为左半边的答案或只能作为右半边的答案

1.若 $ f $ 在左端点上大于 $ g $,则递归左子树用 $ f $ 更新。
2.若 $ f $ 在右端点上大于 $ g $,则递归右子树用 $ f $ 更新。

查询时我们可以用标记永久化的思想,对从根节点到 $ x $ 路径上的所有函数取 $ max $ 。

代码
#include<bits/stdc++.h>
#define int long long
#define Debug puts("Oops!")
using namespace std;

const int N = 1e5 + 5, M = 5e5 + 5;
const double eps = 1e-9;

int n, len;
pair<double, double> pl[N];

struct Segment_Tree {
    struct Node {int l, r, dat;} t[N << 2];
    #define l(p) p << 1
    #define r(p) p << 1 | 1
    void build(int p, int l, int r) {
        t[p].l = l, t[p].r = r;
        if(l == r) return ;
        int mid = l + r >> 1;
        build(l(p), l, mid); build(r(p), mid + 1, r);
    }
    void addline(int ax, int ay, int bx, int by) {
        len++;
        if(ax == bx) pl[len] = {0, max(ay, by)};
        else pl[len] = {(by - ay) * 1.0 / (bx - ax), ay - (by - ay) * 1.0 / (bx - ax) * ax};
    }
    double calc(int p, int x) { return pl[p].first * x + pl[p].second; }
    void update(int p, int id) {
        if(t[p].dat == 0) {t[p].dat = id; return ;}
        int mid = t[p].l + t[p].r >> 1;
        double t1 = calc(id, mid), t2 = calc(t[p].dat, mid);
        if(t1 - t2 > eps || (abs(t1 - t2) <= eps && t[p].dat > id)) swap(t[p].dat, id);
        double lf = calc(id, t[p].l), rf = calc(id, t[p].r);
        double lg = calc(t[p].dat, t[p].l), rg = calc(t[p].dat, t[p].r);
        if(lf - lg > eps || (abs(lf - lg) <= eps && t[p].dat > id)) update(l(p), id);
        if(rf - rg > eps || (abs(rf - rg) <= eps && t[p].dat > id)) update(r(p), id);
    }
    void modify(int p, int l, int r, int id) {
        if(l <= t[p].l && t[p].r <= r) {update(p, id); return ;}
        int mid = t[p].l + t[p].r >> 1;
        if(l <= mid) modify(l(p), l, r, id);
        if(r > mid) modify(r(p), l, r, id);
    }
    int query(int p, int x) {
        if(t[p].l == t[p].r) return t[p].dat;
        int mid = t[p].l + t[p].r >> 1, res = t[p].dat, tmp;
        if(x <= mid) tmp = query(l(p), x);
        else tmp = query(r(p), x);
        if(calc(tmp, x) - calc(res, x) > eps || (abs(calc(tmp, x) - calc(res, x)) <= eps && tmp < res)) res = tmp;
        return res;
    }
}st;

inline int read() {
    int x = 0, f = 1; char c = getchar();
    while(!isdigit(c)) {if(c == '-') f = -1; c = getchar();}
    while(isdigit(c)) x = x * 10 + c - '0', c = getchar();
    return x * f;
}

signed main() {
    //	freopen(".in", "r", stdin);
    //	freopen(".out", "w", stdout);
    n = read();
    st.build(1, 1, 80000);
    int lst = 0;
    while(n--) {
        int op = read();
        if(op == 0) {
            int x = (read() + lst + 39988) % 39989 + 1;
            lst = st.query(1, x);
            printf("%lld\n", lst);
        }
        else {
            int ax = read(), ay = read(), bx = read(), by = read();
            ax = (ax + lst + 39988) % 39989 + 1; bx = (bx + lst + 39988) % 39989 + 1;
            ay = (ay + lst + 999999999) % 1000000000 + 1; by = (by + lst + 999999999) % 1000000000 + 1;
            if(ax > bx) swap(ax, bx), swap(ay, by);
            st.addline(ax, ay, bx, by);
            st.modify(1, ax, bx, len);
        }
    }
    return 0;
}

线段树分裂

待续

线段树维护单调栈

待续

势能线段树

待续

吉司机线段树

待续

posted @ 2024-08-15 10:27  zeta炀  阅读(28)  评论(0)    收藏  举报