OI & ACM 笔记:D - 数据结构

一些技巧与思想也会归类于数据结构。

D - 数据结构 序列结构

树状数组

\(\mathrm{lowbit}(x)\) 函数:表示 \(x\) 的二进制表示中,最低位的 \(1\) 的数值大小,lowbit(x) = x & -x

\(c_i\) 的管辖区间\([i - \mathrm{lowbit}(i) + 1, i]\)

\(\mathcal{O}(n)\) 建树

  • 方法一:每一个节点的值是由所有与自己直接相连的儿子的值求和得到的,因此可以倒着考虑贡献。每次确定完 \(c_i\) 的值后,用 \(c_i\) 的值更新 \(c_{i + \mathrm{lowbit}(i)}\)
  • 方法二:预处理前缀和数组,则 \(c_i = \mathrm{pre}_i - \mathrm{pre}_{i - \mathrm{lowbit}_i}\)

\(\mathcal{O}(\log n)\) 查询第 \(k\):树状数组维护每个数的出现次数,倍增求解。

线段树

  • 任意区间 \([l, r]\) 在线段树上,至少需要 \(\mathcal{O}(\log n)\) 个节点覆盖。

动态开点线段树 & 线段树合并 & 线段树分裂

  • 动态开点线段树,单次插入新建节点个数 \(\mathcal{O}(\log A)\)。其中 \(A\) 表示值域大小。
  • 动态开点线段树可以使用懒标记,下传标记时需要新建结点(若标记对空节点无影响则不必新建节点)。
  • 遇到仅针对某个权值的修改和询问时,可以考虑对每个权值开一个动态开点线段树。
  • 线段树合并单次复杂度,取决于两棵线段树的交集大小;线段树合并总复杂度,取决于所有线段树的节点总数。
  • 线段树合并支持持久化,需要在合并时新建节点。
  • 在线段树合并的过程中,可以统计来自不同线段树且具有相对顺序关系的信息。例如逆序对。
  • 在线段树合并的过程中,在处理复杂的问题时,可以尝试分成以下几类讨论(其中 2, 3 是讨论重点):
    • (1)\(p, q\) 均为空节点:返回空节点。
    • (2)\(p, q\) 中分别有一个空节点和一个非空节点:特殊讨论。
    • (3)线段树递归到叶子节点(即 \(l = r\)):特殊讨论。
    • (4)\(p, q\) 均为非空节点:分别合并 \(p, q\) 的左儿子与 \(p, q\) 的右儿子,最后 update。
  • 线段树分裂单次复杂度 \(\mathcal{O}(\log A)\),新建节点个数 \(\mathcal{O}(\log A)\)
namespace SGT {
	const int pond = ...;

	int nClock, root[N];
	struct node {
		int lc, rc;
		int cnt;
	} t[pond];

	int create() {
		int p = ++ nClock;
		t[p].lc = t[p].rc = t[p].cnt = 0;
		return p;
	}

	void upd(int p) {
		t[p].cnt = t[t[p].lc].cnt + t[t[p].rc].cnt;
	}

	void insert(int &p, int l, int r, int x, int val) {
		if (!p) p = create();
		t[p].cnt += val;
		if (l == r) return;

		int mid = (l + r) >> 1;

		if (x <= mid)
			insert(t[p].lc, l, mid, x, val);
		else
			insert(t[p].rc, mid + 1, r, x, val);
	}

	int merge(int p, int q) {
		if (!p || !q) return p ^ q;
        t[p].cnt += t[q].cnt;
		t[p].lc = merge(t[p].lc, t[q].lc);
		t[p].rc = merge(t[p].rc, t[q].rc);
		return p;
	}

/* 可持久化线段树合并 
	int merge(int p, int q) {
		if (!p || !q) return p ^ q;
		int u = ++ nClock;
		t[u].cnt = t[p].cnt + t[q].cnt;
		t[u].lc = merge(t[p].lc, t[q].lc);
		t[u].rc = merge(t[p].rc, t[q].rc);
		return u; 
	}
*/

    void split_v(int p, int l, int r, int val, int &x, int &y) {
        if (!p)
            x = y = 0;
        else {
            int mid = (l + r) >> 1;
            if (mid < val)
                x = p, y = create(), split_v(t[p].rc, mid + 1, r, val, t[x].rc, t[y].rc);
            else if (mid == val)
                x = create(), y = create(), t[x].lc = t[p].lc, t[y].rc = t[p].rc;
            else
                x = create(), y = p, split_v(t[p].lc, l, mid, val, t[x].lc, t[y].lc);
            upd(x), upd(y);
        }
    }

	void split_s(int p, int sze, int &x, int &y) {
		if (!p)
			x = y = 0;
		else {
			if (t[t[p].lc].cnt < sze)
				x = p, y = create(), split_s(t[p].rc, size - t[t[p].lc].cnt, t[x].rc, t[y].rc);
			else if (t[t[p].lc].cnt == sze)
				x = create(), y = create(), t[x].lc = t[p].lc, t[y].rc = t[p].rc;
			else
				x = create(), y = p, split_s(t[p].lc, size, t[x].lc, t[y].lc);
			upd(x), upd(y);
		}
	}
}

主席树

  • 主席树可以作为线段树的前缀和来使用。
  • 主席树区间修改,可以标记永久化。
namespace SGT {
	int pond = ...;

	int nClock, root[N]; 
	struct node {
		int lc, rc;
		int cnt;
	} t[pond];

	void insert(int &p, int q, int l, int r, int x, int val) {
		p = ++ nClock, t[p] = t[q];
        t[p].cnt += val;
		if (l == r) return;

		int mid = (l + r) >> 1;

		if (x <= mid)
			insert(t[p].lc, t[q].lc, l, mid, x, val);
		else
			insert(t[p].rc, t[q].rc, mid + 1, r, x, val);
	}
}
namespace SGT {
	const int pond = ...;

	int nClock, root[N];
	struct node {
		int lc, rc;
		int sum;
		int add;
	} t[pond];

	void change(int &p, int q, int l, int r, int s, int e, int val) {
		p = ++ nClock, t[p] = t[q];
		if (s <= l && r <= e) { t[p].add += val; return; }
		t[p].sum += (std::min(r, e) - std::max(l, s) + 1) * val;

		int mid = (l + r) >> 1;

		if (s <= mid)
			change(t[p].lc, t[q].lc, l, mid, s, e, val);
		if (mid < e)
			change(t[p].rc, t[q].rc, mid + 1, r, s, e, val);
	}

	int ask(int p, int l, int r, int s, int e) {
		if (s <= l && r <= e)
			return t[p].sum + (r - l + 1) * t[p].add;

		int mid = (l + r) >> 1;
		int cur = (std::min(r, e) - std::max(l, s) + 1) * t[p].add;

		if (s <= mid)
			cur += ask(t[p].lc, l, mid, s, e);
		if (mid < e)
			cur += ask(t[p].rc, mid + 1, r, s, e);

		return cur;
	}
}

势能线段树

  • 势能线段树,用于维护修改操作使得被修改元素的势能迅速向零势能逼近的信息。
  • 在区间修改时,暴力往叶子节点递归。若递归途中遇到某个节点,该节点的对应区间内每个元素都不能进行有效修改,则在该节点 return
  • 在无修改操作时,时间复杂度为 \(\mathcal{O}(n\alpha\log n)\)。其中 \(\alpha\) 为有效修改次数上限。

区间开平方:一个正整数 \(x\) 被开方 \(\mathcal{O}(\log \log x)\) 次后会变成 \(1\)。若区间最大值 \(= 1\),则 return

区间取模:一个整数 \(x\)\(p\) 取模,若 \(x \geq p\),则 \(x\) 至少变小一半。若区间最大值 \(< p\),则 return

区间除法、区间加:整除一个数 \(p\),会使区间最大值与最小值的差至少变小一半。

  • \(\max - \min = 0\),则打加法标记。
  • \(\max - \min=1\)\(\left\lfloor\frac{\max}{p}\right\rfloor \neq \left\lfloor\frac{\min}{p}\right\rfloor\),则打加法标记。

区间按位与:一个二进制数 \(x\)\(v\) 进行按位与,相当于是把所有 \(v\)\(0\) 的位,在 \(x\) 中强制置为 \(0\)。若 \(v\)\(0\) 的位置集合被区间按位或和\(0\) 的位置集合包含,则 return

区间取最值(吉司机线段树):以区间取最小值为例。

  • 在线段树上的每一个节点 \(p\),需要维护:
    • \(\max\):区间最大值。
    • \(\mathrm{cnt}\):区间最大值的个数。
    • \(\mathrm{smax}\):区间严格次大值。
  • 在区间修改时,若递归途中遇到一个被询问区间完全包含的节点 \(p\) 时:
    • \(x \geq \max_p\),则 return
    • \(x \leq \mathrm{smax}_p\),则暴力往左右儿子递归。
    • \(\mathrm{smax}_p < x < \max_p\),则有 \(\mathrm{cnt}\)\(\max_p\) 被改为 \(x\)。打标记即可。
  • 无区间加时,时间复杂度 \(\mathcal{O}(n \log n)\)
  • 有区间加时,时间复杂度 \(\mathcal{O}(n \log^2 n)\)

历史线段树

李超线段树

  • 李超线段树,用于维护一个二维平面,支持插入一个定义域为 \([l, r]\) 的一次函数 \(kx + b\),查询某个横坐标上最大的函数值。
  • 李超线段树上的每一个节点,需要维护区间的优势线段(在 \(\mathrm{mid}\) 处取到最大函数值的线段),用标记永久化的方法处理。
  • 李超线段树支持持久化。
  • 李超线段树合并总复杂度,取决于所有线段树的节点总数。
  • 李超线段树合并支持持久化,需要在合并时新建节点。
  • 李超线段树仅支持插入函数,不支持删除函数。但可以考虑线段树分治。
const int inf = 0x3f3f3f3f; 

struct func {
	int k, b;

	func() { k = b = 0; }
	func(int A, int B) : k(A), b(B) {}

	int calc(int x) {
		return k * x + b; 
	}
};

namespace SGT {
	const int pond = ...;

	int nClock, root[N];
	struct node {
		int lc, rc;
		func p;
	} t[pond];

	int create() {
		int p = ++ nClock;
		t[p].lc = t[p].rc = 0, t[p].p = func();
		return p;
	}

	void insert(int &p, int l, int r, func q) {
		if (!p) {
			p = create(), t[p].p = q;
			return;
		}

		int pl = t[p].p.calc(l), pr = t[p].p.calc(r);
		int ql = q.calc(l), qr = q.calc(r);

		if (pl >= ql && pr >= qr) return;
		if (ql >= pl && qr >= pr) { t[p].p = q; return; }

		int mid = (l + r) >> 1;

		if (t[p].p.calc(mid) < q.calc(mid))
			std::swap(t[p].p, q), std::swap(pl, ql);

		if (pl <= ql)
			insert(t[p].lc, l, mid, q);
		else
			insert(t[p].rc, mid + 1, r, q); 
	}

	void insert(int &p, int l, int r, int s, int e, func q) {
		if (!p) p = create();
		if (s <= l && r <= e) {
			insert(p, l, r, q);
			return;
		}

		int mid = (l + r) >> 1;

		if (s <= mid)
			insert(t[p].lc, l, mid, s, e, q);
		if (mid < e)
			insert(t[p].rc, mid + 1, r, s, e, q);
	}

	int merge(int p, int q, int l, int r) {
		if (!p || !q) return p ^ q;

		int mid = (l + r) >> 1;

		t[p].lc = merge(t[p].lc, t[q].lc, l, mid);
		t[p].rc = merge(t[p].rc, t[q].rc, mid + 1, r);
		insert(p, l, r, t[q].p);

		return p;
	}

	int ask(int p, int l, int r, int x) {
		if (!p) return -inf;
		if (l == r) return t[p].p.calc(x);
		int mid = (l + r) >> 1;
		if (x <= mid)
			return std::max(ask(t[p].lc, l, mid, x), t[p].p.calc(x));
		else
			return std::max(ask(t[p].rc, mid + 1, r, x), t[p].p.calc(x));
	}
}

楼房重建式线段树

参考:https://www.cnblogs.com/PinkRabbit/p/Segment-Tree-and-Prefix-Maximums.html

写法 1:当需要维护的信息满足可减性时,可以考虑采用写法 1。

  • 在线段树上的每一个节点 \(p\),需要维护:

    • \(\max[p]\):区间最大值。
    • \(\mathrm{dat}[p]\):区间内所有前缀最大值的信息和。
  • 定义函数 \(\mathrm{calc}(p, \mathrm{pre})\) 表示以 \(\mathrm{pre}\) 为初始的前缀最大值时,区间内所有前缀最大值的信息和。

    • \(p\) 为叶节点,则 \(\mathrm{calc}(p, \mathrm{pre}) = [\mathrm{pre} < \mathrm{max}[p]]\)
    • \(\mathrm{pre} < \max[\mathrm{lc}[p]]\),此时考虑完左子树后前缀最大值必定为 \(\max[\mathrm{lc}[p]]\),而右子树的信息和可以通过做差得到。

    \[\mathrm{calc}(p, \mathrm{pre}) = \mathrm{calc}(\mathrm{lc}[p], \mathrm{pre}) + (\mathrm{dat}[p] \ \mathrm{deduct} \ \mathrm{dat}[\mathrm{rc}[p]]) \]

    • \(\mathrm{pre} \geq \max[\mathrm{lc}[p]]\),此时左子树不会出现前缀最大值,只需考虑右子树即可。

    \[\mathrm{calc}(p, \mathrm{pre}) = \mathrm{calc}(\mathrm{rc}[p], \mathrm{pre}) \]

  • update 时,有 \(\mathrm{dat}[p] = \mathrm{dat}[\mathrm{lc}[p]] + \mathrm{calc}(\mathrm{rc}[p], \max[\mathrm{lc}[p]])\)

写法 2:当需要维护的信息不满足可减性时,可以考虑采用写法 2。

  • 在线段树上的每一个节点 \(p\),需要维护:

    • \(\max[p]\):区间最大值。
    • \(\mathrm{dat}[p]\):以左子树的区间最大值为初始的前缀最大值时,右子树内所有前缀最大值的信息和。特别地,叶子无定义。
  • 定义函数 \(\mathrm{calc}(p, \mathrm{pre})\) 表示以 \(\mathrm{pre}\) 为初始的前缀最大值时,区间内所有前缀最大值的信息和。

    • \(p\) 为叶节点,则 \(\mathrm{calc}(p, \mathrm{pre}) = [\mathrm{pre} < \mathrm{max}[p]]\)
    • \(\mathrm{pre} < \mathrm{max}[\mathrm{lc}[p]]\),同上

    \[\mathrm{calc}(p, \mathrm{pre}) = \mathrm{calc}(\mathrm{lc}[p], \mathrm{pre}) + \mathrm{dat}[p] \]

    • \(\mathrm{pre} \geq \max[{\mathrm{lc}[p]}]\),同上

    \[\mathrm{calc}(p, \mathrm{pre}) = \mathrm{calc}(\mathrm{rc}[p], \mathrm{pre}) \]

  • update 时,有 \(\mathrm{dat}[p] = \mathrm{calc}(\mathrm{rc}[p], \max[\mathrm{lc}[p]])\)

猫树

  • 建猫树时,采用堆式建树(节点 \(p\) 的左、右儿子分别为 \(2p\)\(2p + 1\))。具体实现时不必显式地将左右儿子的结构建出来,只需记录每一层深度的具体信息,与每个元区间 \([l, l]\) 对应的叶子节点编号即可。
  • 若将序列长度补成 \(2\) 的整数次幂:不难发现左儿子编号为父节点编号左移一位,右儿子编号为父节点编号左移一位再加一。
    • \([l, l]\)\([r, r]\) 的代表节点 \(x, y\) 的 LCA 即为 \(x, y\) 二进制下从高位向低位的 LCP。
    • \([l, l]\)\([r, r]\) 的代表节点为 \(x, y\),则 LCA 编号为 x >> logx[x ^ y],深度为 logx[x] - logx[x ^ y]。特别要注意的是 logx[0] = 0
namespace SGT {
	int dep[N * 4];

	int idx[N];
	int f[logN][N];

	void build(int p, int l, int r) {
		dep[p] = dep[p / 2] + 1;
	
		int mid = (l + r) >> 1; 

		// Calculate the data of the interval [l, mid]

		// Calculate the data of the interval [mid + 1, r]

		if (l == r) idx[l] = p;
		else build(p * 2, l, mid), build(p * 2 + 1, mid + 1, r);
	}

	int ask(int l, int r) {
		int x = idx[l], y = idx[r];
	
		if (dep[x] > dep[y]) std::swap(x, y);
		while (dep[x] < dep[y]) y /= 2;

		while (x ^ y) x /= 2, y /= 2;

		return ...;
	}
}
namespace SGT {
	const int H = ...;

	int h, logx[H];

	int idx[H];
	int f[logH][H];

	void init() {
		h = 1;
		while (h < n) h <<= 1;

		logx[0] = 0;
		for (int i = 1; i <= h; i ++) logx[i] = logx[i >> 1] + 1;
	}

	void build(int p, int l, int r, int dep) {
		int mid = (l + r) >> 1; 

		// Calculate the data of the interval [l, mid]

		// Calculate the data of the interval [mid + 1, r]

		if (l == r) idx[l] = p;
		else build(p * 2, l, mid, dep + 1), build(p * 2 + 1, mid + 1, r, dep + 1);
	}
	
	int ask(int l, int r) {
		int x = idx[l], y = idx[r];
		int z_dep = logx[x] - logx[x ^ y];

		return ...;
	}
}

平衡树

splay

  • splay 核心思想:每次操作完后,通过双旋 & 单旋将相关节点 \(x\) 旋转至根,以此维持树高稳定。
  • splay 的三种旋转:
    • \(x\) 的父亲 \(p\) 是根节点时,直接 zig/zag \(x\)
    • \(x\) 和上两代祖先位于一条链上:先 zig/zag \(p\),再 zig/zag \(x\)
    • \(x\) 和上两代祖先分叉时:先 zig/zag \(x\),再 zig/zag \(x\)
  • splay 基于均摊分析,不支持持久化。
  • splay 启发式合并的时间复杂度 \(\mathcal{O}(n \log n)\)(splay 是 Dynamic Finger Search Tree,需要保证第二棵树按中序遍历的顺序插入)。
const int inf = 0x3f3f3f3f;
namespace BST {
	int nClock, root;
	struct node {
		int son[2], Fa;
		int val;
		int sze;
		bool rev;

		#define lc son[0]
		#define rc son[1]

		void mk_rev() {
			std::swap(lc, rc);
			rev ^= 1;
		}
	} t[N];

	int create(int Fa_p, int val) {
		int p = ++ nClock;
		t[p].lc = t[p].rc = 0, t[p].Fa = Fa_p;
		t[p].val = val;
		t[p].sze = 1;
		t[p].rev = 0;
		return p;
	}

	void upd(int p) {
		t[p].sze = t[t[p].lc].sze + t[t[p].rc].sze + 1;
	}

	void spread(int p) {
		if (t[p].rev) {
			if (t[p].lc) t[t[p].lc].mk_rev();
			if (t[p].rc) t[t[p].rc].mk_rev();
			t[p].rev = 0;
		}
	}

	int dir(int x) {
		return t[t[x].Fa].rc == x;
	}

	void rotate(int x) {
		int y = t[x].Fa, z = t[y].Fa, k = dir(x);
		t[x].Fa = z; if (z) t[z].son[t[z].rc == y] = x;
		t[y].son[k] = t[x].son[k ^ 1], t[t[x].son[k ^ 1]].Fa = y;
		t[x].son[k ^ 1] = y, t[y].Fa = x;
		upd(y), upd(x);
	}

	void splay(int x, int tar) {
		int p;
		while (p = t[x].Fa, p != tar) {
			if (t[p].Fa != tar) rotate(dir(x) == dir(p) ? p : x);
			rotate(x);
		}
		if (!tar) root = x;
	}

	int find(int val) {
		int x = root, ans = 0;
		while (x) {
			spread(x);
			if (t[x].val == val) { ans = x; break; }
			x = t[x].son[val > t[x].val];
		}

		return ans;
	}

	int find_max(int p) {
		int x = p;
		while (spread(x), t[x].rc) x = t[x].rc;
		return x;
	}

	void insert(int val) {
		int x = root, Fa_x = 0;
		while (x) {
			spread(x);
			Fa_x = x;
			x = t[x].son[val > t[x].val];
		}

		x = create(Fa_x, val), t[Fa_x].son[val > t[Fa_x].val] = x;

		splay(x, 0);
	}

	void remove(int val) {
		int x = find(val), p;

		if (!x) return;

		splay(x, 0);

		if (t[x].lc) {
			p = find_max(t[x].lc), splay(p, x);

			t[p].rc = t[x].rc, t[t[x].rc].Fa = p, t[p].Fa = 0, upd(p);

			root = p;
		} else {
			t[t[x].rc].Fa = 0;

			root = t[x].rc;
		}
	}

	int ask_rk(int val) {
		int x = root, Fa_x = 0, cnt = 0;
		while (x) {
			spread(x);
			Fa_x = x;

			if (val > t[x].val) {
				cnt += t[t[x].lc].sze + 1;
				x = t[x].rc;
			} else {
				x = t[x].lc;
			}
		}

		splay(Fa_x, 0);
		return cnt + 1;
	}

	int ask_val(int rk) {
		int x = root, Fa_x = 0, ans = 0;
		while (x) {
			spread(x);
			Fa_x = x;
 
			if (rk <= t[t[x].lc].sze) {
				x = t[x].lc;
			} else if (rk == t[t[x].lc].sze + 1) {
				ans = t[x].val;
				break;
			} else {
				rk -= t[t[x].lc].sze + 1;
				x = t[x].rc;
			}
		}

		splay(Fa_x, 0);
		return ans;
	}

	int ask_prev(int val) {
		int x = root, Fa_x = 0, ans = -inf;
		while (x) {
			spread(x);
			Fa_x = x;

			if (val <= t[x].val) {
				x = t[x].lc;
			} else {
				ans = std::max(ans, t[x].val);
				x = t[x].rc;
			}
		}

		splay(Fa_x, 0);
		return ans;
	}

	int ask_next(int val) {
		int x = root, Fa_x = 0, ans = inf;
		while (x) {
			spread(x);
			Fa_x = x;

			if (val >= t[x].val) {
				x = t[x].rc;
			} else {
				ans = std::min(ans, t[x].val);
				x = t[x].lc;
			}
		}

		splay(Fa_x, 0);
		return ans;
	}
}

FHQ treap

  • treap 的每个节点有两个关键字 \(\mathrm{val}\)\(\mathrm{dat}\),其中数值 \(\mathrm{val}\) 满足 BST 性质,随机键值 \(\mathrm{key}\) 满足 heap 性质。故 treap 为笛卡尔树。
  • treap 的随机性,可以保证期望树高为 \(\mathcal{O}(\log n)\),分析同快速排序。
  • 当 FHQ treap 用作区间树时,可以用笛卡尔树的建树方式 \(\mathcal{O}(n)\) 建树。
  • FHQ treap 的合并操作,不能合并值域有交集的两棵树。
  • FHQ treap 支持持久化,需要在分裂时复制节点。涉及到打标记时,需要在标记下传时复制节点。在合并时要用 rand() % (t[p].sze + t[q].sze) < t[p].sze 来代替随机键值大小的判断。
  • FHQ treap 可以用持久化的方式复制区间。
namespace BST {
	int nClock, root;
	struct node {
		int lc, rc;
		int val, key;
		int sze;
		bool rev;

		void mk_rev() {
			std::swap(lc, rc);
			rev ^= 1;
		}
	} t[N];

	int create(int val) {
		int p = ++ nClock;
		t[p].lc = t[p].rc = 0;
		t[p].val = val, t[p].key = rand();
		t[p].sze = 1;
		t[p].rev = 0;
		return p;
	}

/* 可持久化平衡树
	int copy(int q) {
		int p = ++ nClock; t[p] = t[q];
		return p;
	} 
*/

	void upd(int p) {
		t[p].sze = t[t[p].lc].sze + t[t[p].rc].sze + 1;
	}

	void spread(int p) {
		if (t[p].rev) {
			if (t[p].lc) t[t[p].lc].mk_rev();
			if (t[p].rc) t[t[p].rc].mk_rev();
			t[p].rev ^= 1;
		}
	}

/* 可持久化平衡树
	void spread(int p) {
		if (t[p].rev) {
			if (t[p].lc) t[p].lc = copy(t[p].lc), t[t[p].lc].mk_rev();
			if (t[p].rc) t[p].rc = copy(t[p].rc), t[t[p].rc].mk_rev();
			t[p].rev = 0;
		}
	}
*/

	void split_v(int p, int val, int &x, int &y) {
		if (!p)
			x = y = 0;
		else {
			spread(p);
			if (t[p].val <= val)
				x = p, split_v(t[p].rc, val, t[x].rc, y);
			else
				y = p, split_v(t[p].lc, val, x, t[y].lc);
			upd(p);
		}
	}

	void split_s(int p, int sze, int &x, int &y) {
		if (!p)
			x = y = 0;
		else {
			spread(p);
			if (t[t[p].lc].sze < sze)
				x = p, split_s(t[p].rc, sze - t[t[p].lc].sze - 1, t[x].rc, y);
			else
				y = p, split_s(t[p].lc, sze, x, t[y].lc);
			upd(p);
		}
	}

/* 可持久化平衡树
	void split_v(int p, int val, int &x, int &y) {
		if (!p)
			x = y = 0;
		else {
			p = copy(p), spread(p);
			if (t[p].val <= val)
				x = p, split_v(t[p].rc, val, t[x].rc, y);
			else
				y = p, split_v(t[p].lc, val, x, t[y].lc);
			upd(p);
		}
	}

	void split_s(int p, int sze, int &x, int &y) {
		if (!p)
			x = y = 0;
		else {
			p = copy(p), spread(p);
			if (t[t[p].lc].sze < sze)
				x = p, split_s(t[p].rc, sze - t[t[p].lc].sze - 1, t[x].rc, y);
			else
				y = p, split_s(t[p].lc, sze, x, t[y].lc);
			upd(p);
		}
	}
*/

	int merge(int p, int q) {
		if (!p || !q) return p ^ q;
		if (t[p].key > t[q].key) {
            spread(p);
			t[p].rc = merge(t[p].rc, q), upd(p);
			return p;
		} else {
            spread(q);
			t[q].lc = merge(p, t[q].lc), upd(q);
			return q;
		}
	}

/* 可持久化平衡树 
	int merge(int p, int q) {
		if (!p || !q) return p ^ q;
		if (rand() % (t[p].sze + t[q].sze) < t[p].sze) {
            spread(p);
			t[p].rc = merge(t[p].rc, q), upd(p);
			return p;
		} else {
            spread(q);
			t[q].lc = merge(p, t[q].lc), upd(q);
			return q;
		}
	}
*/
}

树套树

树状数组套线段树

  • 常用于动态维护线段树的前缀和。

线段树套线段树

  • 常用于动态维护一个 \(n \times m\) 的矩形。涉及到区间修改时,需要标记永久化。

线段树套平衡树

  • 常用于动态维护平衡树的区间和。

笛卡尔树

  • 每个节点由一个二元组 \((\mathrm{key}, \mathrm{val})\) 组成,其中键值 \(\mathrm{key}\) 满足 BST 性质,权值 \(\mathrm{val}\) 满足 heap 性质。
  • 若每个节点的 \(\mathrm{key}, \mathrm{val}\) 均互不相同,则笛卡尔树的形态是唯一的。

建树:以大根堆为例,将所有元素按 \(\mathrm{key}\) 从小到大排序。每次插入的元素必然在该笛卡尔树的右链上(BST 性质),在右链查找第一个大于等于当前值的节点,将链拆成两部分,上半部分的下端接在当前节点的父亲上,下半部分的上端接在当前节点的左儿子上。

namespace CAR {
	int root;
	struct node {
		int lc, rc;
	} t[N];

	void build() {
        static int top, stk[N];
		for (int i = 1; i <= n; i ++) {
			int k = top;
			while (k && a[stk[k]] < a[i]) k --;

			if (k) t[stk[k]].rc = i;
			if (k < top) t[i].lc = stk[k + 1];

			stk[top = k + 1] = i;
		}

		root = stk[1];
	}
}
  • 对序列 \(a\) 建笛卡尔树,则 \(v\)\(u\) 的祖先当且仅当 \(a_v\)\(u, v\) 之间的最小值。

分块

  • Bn 为分块大小,t 为分块数量,Lp[], Rp[] 为当前块的左右端点,belong[] 为当前点所属块的编号。
int n, Bn;
int t, Lp[B_sze], Rp[B_sze], belong[N];
int main() {
	for (int i = Bn; i <= n; i += Bn) t ++, Lp[t] = i - Bn + 1, Rp[t] = i;
	if (Rp[t] < n) t ++, Lp[t] = Rp[t - 1] + 1, Rp[t] = n;

	for (int i = 1; i <= n; i ++) belong[i] = (i - 1) / Bn + 1;
}
  • 当遇到单点修改、区间查询时,可以考虑用分块优化到 \(\mathcal{O}(1)\) 修改 \(\mathcal{O}(\sqrt{n})\) 查询。
  • 当遇到区间修改、单点查询时,可以考虑用分块优化到 \(\mathcal{O}(\sqrt{n})\) 修改 \(\mathcal{O}(1)\) 查询。
  • 当遇到区间修改、区间查询时,可以考虑用差分转化为单点修改、区间查询。
  • 当遇到限定 \(i \to i + k_i\),询问从 \(x\) 出发首次 \(\geq y\) 的行动步数时,可以预处理每个元素走出所在块后到达的点及所需步数。
  • 当遇到区间 \([l, r]\) 的子区间 \([l', r'](l \leq l' \leq r' \leq r)\) 统计问题时,可以尝试分成以下几类讨论:
    • \(l', r'\) 均在整块内。
    • \(l', r'\) 中分别有一个在整块内、一个在散块内。
    • \(l'\) 在左散块,\(r'\) 在右散块。
    • \(l', r'\) 均在左散块或 \(l', r'\) 均在右散块。
  • 特别要注意的是,在使用 sqrt() 前要调用 #include <cmath>

莫队

普通莫队

  • 最优排序方式等价于平面曼哈顿距离最短哈密顿路径,这是 NP 完全问题。
  • 莫队排序方式:
    • 第一关键字升序:左端点 \(l\) 所在块编号。
    • 第二关键字升序:右端点 \(r\)
  • 奇偶性优化:当 \(\mathrm{belong}[l]\) 为奇数时,第二关键字升序;当 \(\mathrm{belong}[l]\) 为偶数时,第二关键字降序。
  • 当左右端点扩展时,为避免出现 \(l > r\) 的情况,尽量保证区间先扩大,后缩小。
  • \(n, m\) 同阶时,分块大小 \(\mathcal{O}(\sqrt{n})\),时间复杂度 \(\mathcal{O}(n\sqrt{n})\)
  • \(n, m\) 不同阶时,分块大小 \(\mathcal{O}(nm^{-\frac{1}{2}})\),时间复杂度 \(\mathcal{O}(nm^{\frac{1}{2}})\)
int n, m;
int a[N];

int Bn;
int belong[N];

struct qry {
	int l, r, id;
	bool operator < (const qry &rhs) const {
		if (belong[l] ^ belong[rhs.l]) return l < rhs.l;
		return belong[l] & 1 ? r < rhs.r : r > rhs.r;
	}
} q[M];

int ans[M];

int main() {
	for (int i = 1; i <= m; i ++)
		std::cin >> q[i].l >> q[i].r, q[i].id = i;

	for (int i = 1; i <= n; i ++) belong[i] = (i - 1) / Bn + 1;

	std::sort(q + 1, q + 1 + m);

	int l = 1, r = 0;
	for (int i = 1; i <= m; i ++) {
		while (r < q[i].r) add(a[++ r]);
		while (l > q[i].l) add(a[-- l]);
		while (r > q[i].r) dec(a[r --]);
		while (l < q[i].l) dec(a[l ++]);
		// ans[q[i].id] = ...
	}
}

高维莫队

  • 对于 \(k\) 维莫队,对前 \(k - 1\) 维分块,第 \(i \in [1, k - 1]\) 关键字为第 \(i\) 维坐标所在块编号,第 \(k\) 关键字为第 \(k\) 维坐标。
  • 分块大小 \(\mathcal{O}(nm^{-\frac{1}{k}})\),时间复杂度 \(\mathcal{O}(nm^{1 - \frac{1}{k}})\)

带修莫队

  • 普通莫队是不支持修改的。
  • 可以给每个询问附加一个时间戳 \(t\),来表示回答该次询问时已经经过了多少次修改。转化为三维莫队。
  • \(n, m\) 同阶时,分块大小 \(\mathcal{O}(n^{\frac{2}{3}})\),时间复杂度 \(\mathcal{O}(n^{\frac{5}{3}})\)
  • \(n, m\) 不同阶时,分块大小 \(\mathcal{O}(nm^{-\frac{1}{3}})\),时间复杂度 \(\mathcal{O}(nm^{\frac{2}{3}})\)
int n, m;
int a[N];

int Bn;
int belong[N];

int mc;
struct cg {
	int x, v;
} c[M];

int mq;
struct qry {
	int l, r, t, id;
	bool operator < (const qry &rhs) const {
		if (belong[l] ^ belong[rhs.l]) return l < rhs.l;
		if (belong[r] ^ belong[rhs.r]) return r < rhs.r;
		return t < rhs.t;
	}
} q[M];

int ans[M];

int main() {
	for (int i = 1; i <= m; i ++) {
		if (opt[0] == 'C') {
			mc ++;
			std::cin >> c[mc].x >> c[mc].v;
		} else {
			mq ++;
			std::cin >> q[mq].l >> q[mq].r, q[mq].t = mc, q[mq].id = mq;
		}
	}

	for (int i = 1; i <= n; i ++) belong[i] = (i - 1) / Bn + 1;

	std::sort(q + 1, q + 1 + mq);

	int l = 1, r = 0, t = 0;
	for (int i = 1; i <= mq; i ++) {
		while (r < q[i].r) add(a[++ r]);
		while (l > q[i].l) add(a[-- l]);
		while (r > q[i].r) dec(a[r --]);
		while (l < q[i].l) dec(a[l ++]);
		while (t < q[i].t) {
			t ++;
			if (q[i].l <= c[t].x && c[t].x <= q[i].r) dec(a[c[t].x]), add(c[t].v);
			std::swap(a[c[t].x], c[t].v);
		}
		while (t > q[i].t) {
			if (q[i].l <= c[t].x && c[t].x <= q[i].r) dec(a[c[t].x]), add(c[t].v);
			std::swap(a[c[t].x], c[t].v);
			t --;
		}
		// ans[q[i].id] = ...
	}
}

回滚莫队

  • 用于解决一类插入容易、删除不易的莫队问题。
  • 当操作方便撤销时,可以考虑直接撤销。或是用栈记录操作,沿着栈撤销。
  • 当操作不方便撤销时,可以考虑加入这些数对答案的影响。
int n, m;
int a[N];

int Bn;
int belong[N];

struct qry {
	int l, r, id;
	bool operator < (const qry &rhs) const {
		if (belong[l] ^ belong[rhs.l]) return l < rhs.l;
		return r < rhs.r;
	}
} q[M];

int ans[M];

void add(int x, bool rem) {}

int main() {
	for (int i = 1; i <= m; i ++)
		std::cin >> q[i].l >> q[i].r, q[i].id = i;

	for (int i = 1; i <= n; i ++) belong[i] = (i - 1) / Bn + 1;

	std::sort(q + 1, q + 1 + m);

	for (int x = 1, y = 1; x <= m; x = y = y + 1) {
		while (y < m && belong[q[x].l] == belong[q[y + 1].l]) y ++;

		int rightpos = belong[q[x].l] * Bn;

		// init ...

		int l = rightpos + 1, r = rightpos;
		for (int i = x; i <= y; i ++) {
			if (q[i].r <= rightpos) {
				for (int j = q[i].l; j <= q[i].r; j ++) add(j, 1); 

				// ans[q[i].id] = ...

				// Return to the last state.
			} else {
				while (r < q[i].r) add(++ r, 0);

				// Record the current state.

				while (l > q[i].l) add(-- l, 1);

				// ans[q[i].id] = ...

				// Return to the last state.
			}
		}
	}
}

树上莫队

欧拉序 2:在 dfs 时,第一次访问到该节点时将该节点加入欧拉序(记作 \(\mathrm{In}_x\)),从该节点回溯时将该节点加入欧拉序(记作 \(\mathrm{Out}_x\))。

  • 对于从 \(x\)\(y\) 的路径,不妨设 \(\mathrm{In}_x < \mathrm{In}_y\)
    • \(\mathrm{LCA}(x, y) = x\),则从 \(x\)\(y\) 的路径上的点为欧拉序 \([\mathrm{In}_x, \mathrm{In}_y]\) 中只出现过一次的点。
    • \(\mathrm{LCA}(x, y) \neq x\),则从 \(x\)\(y\) 的路径上的点为欧拉序 \([\mathrm{Out}_x, \mathrm{In}_y]\) 中只出现过一次的点与 \(\mathrm{LCA}(x, y)\)
int n, m;
int a[N];

int tot, head[N], ver[N * 2], Next[N * 2];
void add_edge(int u, int v) {
	ver[++ tot] = v;    Next[tot] = head[u];    head[u] = tot;
}

int dep[N];
int anc[logN + 1][N];

int In[N], Out[N];
int eul_len, eul[N * 2];

int Bn;
int belong[N * 2];

void dfs(int u, int fu) {
	dep[u] = dep[fu] + 1;

	anc[0][u] = fu;
	for (int i = 1; i <= logN; i ++) anc[i][u] = anc[i - 1][anc[i - 1][u]];

	eul[++ eul_len] = u, In[u] = eul_len;

	for (int i = head[u]; i; i = Next[i]) {
		int v = ver[i];
		if (v == fu) continue;

		dfs(v, u);
	}

	eul[++ eul_len] = u, Out[u] = eul_len;
}

int lca(int x, int y) {
	if (dep[x] > dep[y]) std::swap(x, y);
	for (int i = logN; i >= 0; i --)
		if (dep[x] <= dep[y] - (1 << i)) y = anc[i][y];
	if (x == y) return x;
	for (int i = logN; i >= 0; i --)
		if (anc[i][x] != anc[i][y]) x = anc[i][x], y = anc[i][y];
	return anc[0][x];
}

struct ask {
	int l, r, z, id;
	bool operator < (const qry &rhs) const {
		if (belong[l] ^ belong[rhs.l]) return l < rhs.l;
		return belong[l] & 1 ? r < rhs.r : r > rhs.r;
	}
} q[M];

int ans[M]; 

bool state[N];

void attend(int p) {
	state[p] ^= 1;
    state[p] ? add(a[p]) : dec(a[p]);
}

int main() {
	dfs(1, 0);

	for (int i = 1, x, y, z; i <= m; i ++) {
		std::cin >> x >> y, z = lca(x, y);
		if (In[x] > In[y]) std::swap(x, y);

		if (z == x)
			q[i].l = In[x], q[i].r = In[y], q[i].z = 0, q[i].id = i;
		else
			q[i].l = Out[x], q[i].r = In[y], q[i].z = z, q[i].id = i;
	}

	for (int i = 1; i <= eul_len; i ++) belong[i] = (i - 1) / Bn + 1;

	sort(q + 1, q + 1 + m, cmp);

	int l = 1, r = 0;
	for (int i = 1; i <= m; i ++) {
		while (r < q[i].r) attend(eul[++ r]);
		while (l > q[i].l) attend(eul[-- l]);
		while (r > q[i].r) attend(eul[r --]);
		while (l < q[i].l) attend(eul[l ++]);

		if (q[i].z)
			// Calculate ans[q[i].id] with q[i].z
		else
			// Calculate ans[q[i].id]
	}
}

莫队二次离线

序列分治

  • 在序列分治的过程中,取分治重心 \(\mathrm{mid} = \left\lfloor \frac{l + r}{2} \right\rfloor\),尝试分成以下几类讨论(其中 3 是讨论重点):
    • (1)分治递归到叶子节点(即 \(l = r\))。
    • (2)左右端点均在区间 \([l, \mathrm{mid}]\) 或左右端点均在区间 \([\mathrm{mid} + 1, r]\)
    • (3)左端点在区间 \([l, \mathrm{mid}]\),右端点在区间 \([\mathrm{mid} + 1, r]\)
  • 序列分治的分治树为猫树。

最值分治

  • 在最值分治的过程中,取分治重心 \(p\)\([l, r]\) 中的最值,尝试分成以下几类讨论(其中 3 是讨论重点):
    • (1)分治递归到叶子节点(即 \(l = r\))。
    • (2)左右端点均在区间 \([l, p - 1]\) 或左右端点均在区间 \([p + 1, r]\)
    • (3)左端点在区间 \([l, p]\),右端点在区间 \([p, r]\)。此时的子区间 \([l', r']\) 中的最值必为 \(a_p\)。统计的过程通常需要配合启发式合并。
  • 最值分治的分治树为笛卡尔树。

CDQ 分治

  • 在序列分治的过程中,取分治重心 \(\mathrm{mid} = \left\lfloor \frac{l + r}{2} \right\rfloor\),尝试分成以下几类讨论(其中 3 是讨论重点):
    • (1)分治递归到叶子节点(即 \(l = r\))。
    • (2)递归计算区间 \([l, \mathrm{mid}]\) 中的答案。
    • (3)统计区间 \([l, \mathrm{mid}]\) 对区间 \([\mathrm{mid} + 1, r]\) 的贡献。
    • (4)递归计算区间 \([\mathrm{mid} + 1, r]\) 中的答案。

整体二分

  • 在整体二分的过程中,取二分中点 \(\mathrm{mid} = \left\lfloor \frac{l + r}{2} \right\rfloor\),将答案 \(\leq \mathrm{mid}\) 的询问分到区间 \([l, \mathrm{mid}]\) 中递归计算,将答案 \(> \mathrm{mid}\) 的询问分到区间 \([\mathrm{mid} + 1, r]\) 中递归计算。

二进制分组

线段树分治

线段树优化建图

Trick

区间加、区间查询 转 单点加、区间查询

  • 对于原序列 \(a\),记其差分序列为 \(b\)

\[\begin{aligned} \sum\limits_{i = 1}^r a_i & = \sum\limits_{i = 1}^r \sum\limits_{j = 1}^i b_j \\ & = \sum\limits_{j = 1}^r b_j \cdot (r - j + 1) \\ & = (r + 1) \sum\limits_{j = 1}^r b_j - \sum\limits_{j = 1}^r j \cdot b_j \end{aligned} \]

连续段计数

  • 无重复元素:统计所有满足 \(\mathrm{\max} - \mathrm{min} = r - l\) 的区间个数。线段树 & 单调栈。
  • 有重复元素:统计所有满足 \(\mathrm{\max} - \mathrm{min} + \mathrm{cnt} = r - l\) 的区间个数,其中 \(\mathrm{cnt}\) 为区间重复元素个数。维护每个位置上一个相同数的位置,线段树 & 单调栈。

D - 数据结构 树上结构

最近公共祖先 LCA

树上倍增

  • \(\mathcal{O}(n \log n) - \mathcal{O}(\log n)\)

重链剖分

  • \(\mathcal{O}(n) - \mathcal{O(\log n)}\)

欧拉序 & ST 表

  • \(\mathcal{O}(n \log n) - \mathcal{O}(1)\)

欧拉序 1:在 dfs 时,第一次访问到该节点时将该节点加入欧拉序(记作 \(\mathrm{Fir}_x\)),回溯到该节点时将该节点加入欧拉序。

  • 不妨设 \(\mathrm{Fir}_x < \mathrm{Fir}_y\),则 \(\mathrm{LCA}(u, v)\) 为欧拉序 \([\mathrm{Fir}_x, \mathrm{Fir}_y]\) 中深度最小的节点。用 ST 表维护。
int tot, head[N], ver[N * 2], Next[N * 2];
void add_edge(int u, int v) {
	ver[++ tot] = v;    Next[tot] = head[u];    head[u] = tot;
}

int dep[N];

int Fir[N];
int eul_len, eul[N * 2];

void dfs(int u, int fu) {
	dep[u] = dep[fu] + 1;

	eul[++ eul_len] = u, Fir[u] = eul_len;

	for (int i = head[u]; i; i = Next[i]) {
		int v = ver[i];
		if (v == fu) continue;

		dfs(v, u), eul[++ eul_len] = u;
	}
}

namespace eul_ST {
	const int SZ = N * 2;

	int logx[SZ];
	int f[logSZ + 1][SZ];

	int great(int x, int y) {
		return dep[x] < dep[y] ? x : y;
	}

	void init() {
		logx[0] = -1;
		for (int i = 1; i <= eul_len; i ++) logx[i] = logx[i >> 1] + 1;

		for (int i = 1; i <= eul_len; i ++) f[0][i] = eul[i];
		for (int j = 1; j <= logSZ; j ++)
			for (int i = 1; i <= eul_len - (1 << j) + 1; i ++)
				f[j][i] = great(f[j - 1][i], f[j - 1][i + (1 << (j - 1))]);
	}

	int query(int l, int r) {
		int k = logx[r - l + 1];
		return great(f[k][l], f[k][r - (1 << k) + 1]);
	}
}

int lca(int x, int y) {
	int l = Fir[x], r = Fir[y]; if (l > r) std::swap(l, r);
	return eul_ST::query(l, r);
}

树的重心

  • 一个点是树的重心,当且仅当它的最大子树大小不超过整棵树的一半
  • 树中所有点到重心的距离和是最小的。
  • 树至多有两个重心。若树有两个重心,则这两个重心相邻,此时树有偶数个节点,可以被划分成大小相等的两支。
  • 在两棵树之间加一条边,新树的重心在原来两棵树的重心的简单路径上。
  • 在一颗树上添加或删除一个叶子,重心至多只移动一条边的距离。

树的直径

  • 在边权非负的前提下,合并两个点集时,新点集的直径端点,必定来自两个点集四个端点中的某两个,共六种情况。
  • 在边权非负的前提下,钦定点对 \((u, v)\) 作为直径端点的充要条件是,不存在其他点 \(w\) 使得 \(\mathrm{dist}(u, w) > \mathrm{dist}(u, v)\)\(\mathrm{dist}(v, w) > \mathrm{dist}(u, v)\)

树的同构

\(T_1, T_2\) 同构:若 \(T_1, T_2\) 为同构树,当且仅当存在一个 \(1 \sim n\) 的排列 \(f\),使得 \((i, j) \in E \Leftrightarrow (f_i, f_j) \in E\)

有根树自同构树计数:对于每个点的每个儿子的子树,按照同构(指去掉标号、儿子间不区分顺序后俩子树相同)划分出等价类,按照每种等价类的大小的阶乘乘入答案。

无根数自同构数计数:需要找到一个代表节点,当重心唯一时,取重心为根;当重心不唯一时,在重心之间加一点,取该点为根。转化为有根树计算。

重链剖分

  • 重链优先 dfs 序列:
    • 任意子树在 dfs 序上,都对应一个连续区间。
    • 任意重链在 dfs 序上,都对应一个连续区间。
  • 向根的方向经过一条轻边后,所在子树大小至少乘 \(2\)
  • 任意一点到根路径上的所有边,可分成 \(\mathcal{O}(\log n)\) 个重路径的不交并。
  • 任意两点之间路径上的所有边,可分成 \(\mathcal{O}(\log n)\) 个重路径的不交并。
int tot, head[N], ver[N * 2], Next[N * 2];
void add_edge(int u, int v) {
	ver[++ tot] = v;    Next[tot] = head[u];    head[u] = tot;
}

int dep[N], sze[N], Fa[N], son[N];

void dfs1(int u) {
	dep[u] = dep[Fa[u]] + 1;
	sze[u] = 1;

	for (int i = head[u]; i; i = Next[i]) {
		int v = ver[i];
		if (v == Fa[u]) continue;

		Fa[v] = u;
		dfs1(v);

		sze[u] += sze[v];
		if (sze[v] > sze[son[u]]) son[u] = v;
	}
}

int dfsClock, dfn[N], low[N];
int chain_top[N];

void dfs2(int u, int P) {
	dfsClock ++;
	dfn[u] = dfsClock, idx[dfsClock] = u;

	chain_top[u] = P;

	if (son[u]) dfs2(son[u], P);
	for (int i = head[u]; i; i = Next[i]) {
		int v = ver[i];
		if (v == Fa[u] || v == son[u]) continue;

		dfs2(v, v);
	}
}

void chain_operate(int x, int y) {
	while (chain_top[x] ^ chain_top[y]) {
		if (dep[chain_top[x]] > dep[chain_top[y]]) std::swap(x, y);
		// Operate on the [dfn[chain_top[y]], dfn[y]].
		y = Fa[chain_top[y]];
	}

	if (dep[x] > dep[y]) std::swap(x, y);
	// Operate on the [dfn[x], dfn[y]].
}

长链剖分

  • 所有长链的长度和为 \(n\)
  • 任意一点到根路径上的所有边,可分成 \(\mathcal{O}(\sqrt{n})\) 个长路径的不交并。
int tot, head[N], ver[N * 2], Next[N * 2];
void add_edge(int u, int v) {
	ver[++ tot] = v;    Next[tot] = head[u];    head[u] = tot;
}

int dep[N], max_dep[N], Fa[N], son[N];

void dfs1(int u) {
	max_dep[u] = dep[u] = dep[Fa[u]] + 1;

	for (int i = head[u]; i; i = Next[i]) {
		int v = ver[i];
		if (v == Fa[u]) continue;

		Fa[v] = u; 
		dfs1(v);

		if (max_dep[v] > max_dep[u]) max_dep[u] = max_dep[v], son[u] = v; 
	}
}

int chain_top[N];

void dfs2(int u, int P) {
	chain_top[u] = P;

	if (son[u]) dfs2(son[u], P);
	for (int i = head[u]; i; i = Next[i]) {
		int v = ver[i];
		if (v == Fa[u] || v == son[u]) continue;

		dfs2(v, v);
	}
}

长链剖分优化 dp:长链剖分可以优化附带深度维的 dp。

  • 遍历每一个节点,统计每一个节点子树内的信息,设当前遍历到了节点 \(u\)
    • 递归计算 \(u\) 的长子树的信息,将长子树的 dp 信息继承给 \(u\)
    • 递归计算 \(u\) 的所有短子树的信息,将短子树的 dp 信息合并计入 \(u\)
  • 具体实现中,可以开一个内存池 pool,为一条长链统筹分配内存,通常链上不同的节点有不同的首指针,便于继承信息。因此,在完整地做完一遍 dp 后,只有长链链首的节点保留了正确的 dp 值。

\(\mathcal{O}(n \log n) - \mathcal{O}(1)\) 树上 \(k\) 级祖先

  • 任意一点 \(x\)\(k\) 级祖先,其所在的长链长度 \(\geq k\)
  • 对树进行长链剖分,预处理:
    • 每个点的树上 \(2^k\) 级祖先。
    • 每条长链(记该长链长度为 \(\mathrm{len}\))从长链顶端向上的 \(\mathrm{len}\) 个祖先和向下的 \(\mathrm{len}\) 个儿子。
    • 每个二进制数的最高位。
  • 每次询问 \(x\)\(k\) 级祖先:
    • \(h\)\(k\) 的最高二进制位。利用倍增数组先让 \(x\) 跳到 \(x\)\(2^h\) 级祖先。
    • 记剩余级数为 \(k' = k - 2^h\)。由于当前 \(x\) 所在长链长度 \(\mathrm{len} \geq 2^h \geq k - 2^h = k'\),可以先让 \(x\) 跳到 \(x\) 所在长链顶端,若剩余级数为正,则利用预处理的数组向上跳求出答案;若剩余级数为负,则利用预处理的数组向下跳求出答案。
int tot, head[N], ver[N * 2], Next[N * 2];
void add_edge(int u, int v) {
	ver[++ tot] = v;    Next[tot] = head[u];    head[u] = tot;
}

int dep[N], max_dep[N], Fa[N], son[N];
int anc[logN + 1][N]; 

void dfs1(int u) {
	max_dep[u] = dep[u] = dep[Fa[u]] + 1;

	anc[0][u] = Fa[u];
	for (int i = 1; i <= logN; i ++) anc[i][u] = anc[i - 1][anc[i - 1][u]];

	for (int i = head[u]; i; i = Next[i]) {
		int v = ver[i];
		if (v == Fa[u]) continue;

		Fa[v] = u; 
		dfs1(v);

		if (max_dep[v] > max_dep[u]) max_dep[u] = max_dep[v], son[u] = v; 
	}
}

int chain_top[N];
std::vector<int> up[N], dn[N];

void dfs2(int u, int P) {
	chain_top[u] = P;

	if (u == P) {
		int len = max_dep[u] - dep[u] + 1;
		for (int step = len, x = u; step; step --, x = Fa[x]) up[u].push_back(x);
		for (int step = len, x = u; step; step --, x = son[x]) dn[u].push_back(x);
	}

	if (son[u]) dfs2(son[u], P);
	for (int i = head[u]; i; i = Next[i]) {
		int v = ver[i];
		if (v == Fa[u] || v == son[u]) continue;

		dfs2(v, v);
	}
}

int logx[N];

int find(int x, int k) {
	if (!k) return x;
	int h = logx[k];
	k -= (1 << h), x = f[x][h];
	k -= dep[x] - dep[chain_top[x]], x = chain_top[x];
	return k >= 0 ? up[x][k] : dn[x][-k]; 
}

int main() {
	logx[0] = -1;
	for (int i = 1; i <= n; i ++) logx[i] = logx[i >> 1] + 1;

	// ...
}

LCT

  • LCT 的辅助树由 splay 森林组成,每一棵 splay 都维护了一条偏爱链,splay 的中序遍历对应着偏爱链从链顶至链底。
  • LCT 的每一棵 splay 的顶端节点,其父不认子。
  • LCT 的操作注意事项:
    • find_root(x):在找到根之后,需要将根 splay 上去,否则复杂度可能会退化。
    • link(x, y):在连边之前,需要判断 \(x, y\) 所在连通块的根是否不同。
    • cut(x, y):在执行完 make_root(x), access(y), splay(y) 之后,需要判断 \(y\) 的左子树是否有且仅有 \(x\)
  • LCT 经过 access 后,跨越非偏爱边的数量是 \(\mathcal{O}(\log n)\) 的。
  • LCT 的 access 操作,可以实现从某节点到根路径上的标记覆盖,可以实现从某节点到根路径上标记的批量处理(因为每条实链的标记都是相同的)。由于根是不变的,就不必写翻转标记了。
namespace LCT {
	struct node {
		int son[2], Fa;
		int val;
		int sze;
		bool rev;

		#define lc son[0]
		#define rc son[1]

		void init(int x) {
			lc = rc = 0, Fa = 0;
			val = x;
			sze = 1;
			rev = 0;
		}

		void mk_rev() {
			std::swap(lc, rc);
			rev ^= 1;
		}
	} t[N];

	void upd(int p) {
		t[p].sze = t[t[p].lc].sze + t[t[p].rc].sze + 1;
	}

	void spread(int p) {
		if (t[p].rev) {
			if (t[p].lc) t[t[p].lc].mk_rev();
			if (t[p].rc) t[t[p].rc].mk_rev();
			t[p].rev = 0;
		}
	}

	int dir(int x) {
		return t[t[x].Fa].rc == x;
	}

	int is_top(int x) {
		return t[t[x].Fa].lc != x && t[t[x].Fa].rc != x;
	}

	void rotate(int x) {
		int y = t[x].Fa, z = t[y].Fa, k = dir(x);
		t[x].Fa = z; if (!is_top(y)) t[z].son[t[z].rc == y] = x;
		t[y].son[k] = t[x].son[k ^ 1], t[t[x].son[k ^ 1]].Fa = y;
		t[x].son[k ^ 1] = y, t[y].Fa = x;
		upd(y), upd(x);
	}

	void splay(int x) {
		static int top, stk[N], p;

		stk[top = 1] = x;
		for (p = x; !is_top(p); p = t[p].Fa) stk[++ top] = t[p].Fa;
		while (top) spread(stk[top --]);

		while (p = t[x].Fa, !is_top(x)) {
			if (!is_top(p)) rotate(dir(x) == dir(p) ? p : x);
			rotate(x);
		}
	}

	void access(int x) {
		for (int y = 0; x; y = x, x = t[x].Fa) {
			splay(x);
			t[x].rc = y;
			upd(x); 
		}
	}

	int find_root(int x) {
		access(x), splay(x);
		while (spread(x), t[x].lc) x = t[x].lc;
		return splay(x), x;
	}

	void make_root(int x) {
		access(x), splay(x), t[x].mk_rev();
	}

	void link(int x, int y) {
		if (find_root(x) == find_root(y)) return;
		make_root(x), t[x].Fa = y;
	}

	void cut(int x, int y) {
		make_root(x), access(y), splay(y);
		if (t[y].lc != x || t[x].rc) return;
		t[y].lc = t[x].Fa = 0, upd(y);
	}
}

LCT 维护子树信息:维护虚子树的信息总和(需要满足可减性),只有 accesslink 操作需要改变。

namespace LCT {
	struct node {
		int son[2], Fa;
		int sze;
		int vir;
		bool rev;

		#define lc son[0]
		#define rc son[1]

		void init() {
			lc = rc = 0, Fa = 0;
			sze = 1;
			vir = 0;
			rev = 0;
		}

		void mk_rev() {
			std::swap(lc, rc);
			rev ^= 1;
		}
	} t[N];

	void upd(int p) {
		t[p].sze = t[t[p].lc].sze + t[t[p].rc].sze + 1 + t[p].vir;
	}

	void spread(int p) {
		if (t[p].rev) {
			if (t[p].lc) t[t[p].lc].mk_rev();
			if (t[p].rc) t[t[p].rc].mk_rev();
			t[p].rev = 0;
		}
	}

	int dir(int x) {
		return t[t[x].Fa].rc == x;
	}

	bool is_top(int x) {
		return t[t[x].Fa].lc != x && t[t[x].Fa].rc != x;
	}

	void rotate(int x) {
		int y = t[x].Fa, z = t[y].Fa, k = dir(x);
		t[x].Fa = z; if (!is_top(y)) t[z].son[t[z].rc == y] = x;
		t[y].son[k] = t[x].son[k ^ 1], t[t[x].son[k ^ 1]].Fa = y;
		t[x].son[k ^ 1] = y, t[y].Fa = x;
		upd(y), upd(x);
	}

	void splay(int x) {
		static int top, stk[N], p;

		stk[top = 1] = x;
		for (p = x; !is_top(p); p = t[p].Fa) stk[++ top] = t[p].Fa;
		while (top) spread(stk[top --]);

		while (p = t[x].Fa, !is_top(x)) {
			if (!is_top(p)) rotate(dir(x) == dir(p) ? p : x);
			rotate(x);
		}
	}

	void access(int x) {
		for (int y = 0; x; y = x, x = t[x].Fa) {
			splay(x);
			t[x].vir += t[t[x].rc].sze - t[y].sze;
			t[x].rc = y;
			upd(x);
		}
	}

	void make_root(int x) {
		access(x), splay(x), t[x].mk_rev();
	}

	void link(int x, int y) {
		if (find_root(x) == find_root(y)) return;
		make_root(x), make_root(y);
		t[x].Fa = y, t[y].vir += t[x].sze, t[y].sze += t[x].sze;
	}

	void cut(int x, int y) {
		make_root(x), access(y), splay(y);
		if (t[y].lc != x || t[x].rc) return;
		t[y].lc = t[x].Fa = 0, upd(y);
	}
}

LCT 维护生成树

namespace LCT {
	const int pond = N + M;

	int nClock;
	struct node {
		int son[2], Fa;
		int val;
		int max_id;
		bool rev;

		#define lc son[0]
		#define rc son[1]

		void init(int x, int p) {
			lc = rc = 0, Fa = 0;
			val = x;
			max_id = p;
			rev = 0;
		}

		void mk_rev() {
			std::swap(lc, rc);
			rev ^= 1;
		}
	} t[pond];

	void upd(int p) {
		t[p].max_id = p;
		if (t[t[t[p].lc].max_id].val > t[t[p].max_id].val)
			t[p].max_id = t[t[p].lc].max_id;
		if (t[t[t[p].rc].max_id].val > t[t[p].max_id].val)
			t[p].max_id = t[t[p].rc].max_id;
	}

	void spread(int p) {
		if (t[p].rev) {
			if (t[p].lc) t[t[p].lc].mk_rev();
			if (t[p].rc) t[t[p].rc].mk_rev();
			t[p].rev = 0;
		}
	}

	int dir(int x) {
		return t[t[x].Fa].rc == x;
	}

	bool is_top(int x) {
		return t[t[x].Fa].lc != x && t[t[x].Fa].rc != x;
	}

	void rotate(int x) {
		int y = t[x].Fa, z = t[y].Fa, k = dir(x);
		t[x].Fa = z; if (!is_top(y)) t[z].son[t[z].rc == y] = x;
		t[y].son[k] = t[x].son[k ^ 1], t[t[x].son[k ^ 1]].Fa = y;
		t[x].son[k ^ 1] = y, t[y].Fa = x;
		upd(y), upd(x);
	}

	void splay(int x) {
		static int top, stk[pond], p;

		stk[top = 1] = x;
		for (p = x; !is_top(p); p = t[p].Fa) stk[++ top] = t[p].Fa;
		while (top) spread(stk[top --]);

		while (p = t[x].Fa, !is_top(x)) {
			if (!is_top(p)) rotate(dir(x) == dir(p) ? p : x);
			rotate(x);
		}
	}

	void access(int x) {
		for (int y = 0; x; y = x, x = t[x].Fa) {
			splay(x);
			t[x].rc = y;
			upd(x);
		}
	}

	int find_root(int x) {
		access(x), splay(x);
		while (spread(x), t[x].lc) x = t[x].lc;
		return splay(x), x;
	}

	void make_root(int x) {
		access(x), splay(x), t[x].mk_rev();
	}

	void link(int x, int y) {
		make_root(x), t[x].Fa = y;
	}

	void extend(int x, int y, int z) {
		t[++ nClock].init(z, nClock);

		if (find_root(x) != find_root(y)) {
			link(x, nClock), link(y, nClock);
		} else {
			make_root(x), access(y), splay(y);

			if (t[t[y].max_id].val <= z) return;

			int p = t[y].max_id;
			splay(p), t[t[p].lc].Fa = t[t[p].rc].Fa = 0;

			link(x, nClock), link(y, nClock);
		}
	}
}

int main() {
	for (int i = 1; i <= n; i ++) LCT::t[i].init(-inf, i);
	LCT::nClock = n;

    // ...
}

LCT 维护双连通分量

LCT 维护黑白转色树

点分治、点分树

参考:https://liu-cheng-ao.blog.uoj.ac/blog/2969

  • 点分治本质上是序列分治的树上衍生。
  • 在点分治过程中,将本层中与上一层重心相邻的点记作本层的根。若将以 " 上一层的根 " 作为根时的子树大小作为本层总节点数,虽然在本层的根在上一层的根与上一层的重心简单路径上时,本层总结点数会计算错误,但不影响复杂度。
int tot, head[N], ver[N * 2], Next[N * 2];
void add_edge(int u, int v) {
	ver[++ tot] = v;    Next[tot] = head[u];    head[u] = tot;
}

bool ban[N];

int sz[N], mp[N];
int A_sz, A_rt;

void Get_rt(int u, int fu) {
	sz[u] = 1, mp[u] = 0;

	for (int i = head[u]; i; i = Next[i]) {
		int v = ver[i];
		if (v == fu || ban[v]) continue;

		Get_rt(v, u);

		sz[u] += sz[v];
		if (sz[v] > mp[u]) mp[u] = sz[v];
	}

	if (A_sz - sz[u] > mp[u]) mp[u] = A_sz - sz[u];
	if (A_rt == 0 || mp[u] < mp[A_rt]) A_rt = u;
}

void solve(int u) {
	ban[u] = 1;

	// Calculate the data of path through point u.

	for (int i = head[u]; i; i = Next[i]) {
		int v = ver[i];
		if (ban[v]) continue;

		A_sz = sz[v], A_rt = 0;
		Get_rt(v, u), solve(A_rt);
	}
}

int main() {
	A_sz = n, A_rt = 0;
	Get_rt(1, 0), solve(A_rt);
}
  • 点分树本质上是点分治的分治树。
  • 点分树中任意两点 \(u, v\)\(\mathrm{LCA}(u, v)\) 一定在原树中 \(u, v\) 的简单路径上。
int tot, head[N], ver[N * 2], Next[N * 2];
void add_edge(int u, int v) {
	ver[++ tot] = v;    Next[tot] = head[u];    head[u] = tot;
}

namespace PDT {
	int rt;

	int Fa[N];
	std::vector<int> son[N];

	void add_edge(int u, int v) {
		son[u].push_back(v), Fa[v] = u;
	}
}

bool ban[N];

int sz[N], mp[N];
int A_sz, A_rt;

void Get_rt(int u, int fu) {
	sz[u] = 1, mp[u] = 0;

	for (int i = head[u]; i; i = Next[i]) {
		int v = ver[i];
		if (v == fu || ban[v]) continue;

		Get_rt(v, u);

		sz[u] += sz[v];
		if (sz[v] > mp[u]) mp[u] = sz[v];
	}

	if (A_sz - sz[u] > mp[u]) mp[u] = A_sz - sz[u];
	if (A_rt == 0 || mp[u] < mp[A_rt]) A_rt = u;
}

void solve(int u) {
	ban[u] = 1;

	for (int i = head[u]; i; i = Next[i]) {
		int v = ver[i];
		if (ban[v]) continue;

		A_sz = sz[v], A_rt = 0;
		Get_rt(v, u), PDT::add_edge(u, A_rt), solve(A_rt);
	}
}

int main() {
	A_sz = n, A_rt = 0;
	Get_rt(1, 0), PDT::rt = A_rt, solve(A_rt);
}

虚树

  • 对于关键点点集 \(S\),在包含关键点点集 \(S\) 的极小联通子图中,分叉节点(儿子数 \(\geq 2\) 的节点)的数量不超过 \(|S| - 1\)
  • 对于关键点点集 \(S\),记 \(u_i\) 为点集 \(S\) 中的点按 dfs 序排序后的结果,则

\[\{\mathrm{LCA}(u_i, u_j) \mid 1 \leq i < j \leq |S|\} = \{ \mathrm{LCA}(u_i, u_{i + 1}) \mid 1 \leq i < |S| \} \]

int tot, head[N], ver[N * 2], Next[N * 2];
void add_edge(int u, int v) {
	ver[++ tot] = v;    Next[tot] = head[u];    head[u] = tot;
}

int dfsClock, dfn[N];

int dep[N];
int anc[logN + 1][N];

void dfs(int u, int fu) {
	dfn[u] = ++ dfsClock;

	dep[u] = dep[fu] + 1;

	anc[0][u] = fu;
	for (int i = 1; i <= logN; i ++) anc[i][u] = anc[i - 1][anc[i - 1][u]];

	for (int i = head[u]; i; i = Next[i]) {
		int v = ver[i];
		if (v == fu) continue;

		dfs(v, u);
	}
}

int lca(int x, int y) {
	if (dep[x] > dep[y]) std::swap(x, y);
	for (int i = logN; i >= 0; i --)
		if (dep[x] <= dep[y] - (1 << i)) y = anc[i][y];
	if (x == y) return x;
	for (int i = logN; i >= 0; i --)
		if (anc[i][x] != anc[i][y]) x = anc[i][x], y = anc[i][y];
	return anc[0][x];
}

int m, cru[N];

bool cmp(int x, int y) { return dfn[x] < dfn[y]; }

namespace VT {
	int tim, tim_mark[N];

	int rt;

	int tot, head[N], ver[N], Next[N];
	void add_edge(int u, int v) {
		ver[++ tot] = v;    Next[tot] = head[u];    head[u] = tot;
	}

	void init(int u) { head[u] = 0; }

	void build() {
		static int top, stk[N];

		tot = 0;

		tim ++;
		for (int i = 1; i <= m; i ++) tim_mark[cru[i]] = tim;

		std::sort(cru + 1, cru + 1 + m, cmp);

		init(cru[1]), stk[top = 1] = cru[1];
		for (int i = 2; i <= m; i ++) {
			int x = cru[i], z = lca(stk[top], x);

			while (top > 1 && dep[z] <= dep[stk[top - 1]])
				add_edge(stk[top - 1], stk[top]), top --;

			if (stk[top] != z)
				init(z), add_edge(z, stk[top]), stk[top] = z;

			init(x), stk[++ top] = x;
		}

		while (top > 1)
			add_edge(stk[top - 1], stk[top]), top --;

		rt = stk[1];
	}
}
int tot, head[N], ver[N * 2], Next[N * 2];
void add_edge(int u, int v) {
	ver[++ tot] = v;    Next[tot] = head[u];    head[u] = tot;
}

int dfsClock, dfn[N];

int dep[N], sze[N];
int anc[logN + 1][N];

void dfs(int u, int fu) {
	dfn[u] = ++ dfsClock;
	sze[u] = 1; 

	dep[u] = dep[fu] + 1;

	anc[0][u] = fu;
	for (int i = 1; i <= logN; i ++) anc[i][u] = anc[i - 1][anc[i - 1][u]];

	for (int i = head[u]; i; i = Next[i]) {
		int v = ver[i];
		if (v == fu) continue;

		dfs(v, u);
		sze[u] += sze[v];
	}
}

int lca(int x, int y) {
	if (dep[x] > dep[y]) std::swap(x, y);
	for (int i = logN; i >= 0; i --)
		if (dep[x] <= dep[y] - (1 << i)) y = anc[i][y];
	if (x == y) return x;
	for (int i = logN; i >= 0; i --)
		if (anc[i][x] != anc[i][y]) x = anc[i][x], y = anc[i][y];
	return anc[0][x];
}

bool contain(int x, int y) {
	return dfn[x] <= dfn[y] && dfn[y] <= dfn[x] + sze[x] - 1;
}

int m, cru[N * 2];

bool cmp(int x, int y) { return dfn[x] < dfn[y]; }

namespace VT {
	int tim, tim_mark[N];

	int Fa[N];

	void build() {
		static int top, stk[N];

		tim ++;
		for (int i = 1; i <= m; i ++) tim_mark[cru[i]] = tim;

		std::sort(cru + 1, cru + 1 + m, cmp);
		for (int i = 1, om = m; i < om; i ++) cru[++ m] = lca(cru[i], cru[i + 1]);
		std::sort(cru + 1, cru + 1 + m, cmp);
		m = std::unique(cru + 1, cru + 1 + m) - cru - 1;

		top = 0;
		for (int i = 1; i <= m; i ++) {
			int x = cru[i];

			while (top && !contain(stk[top], x)) top --;
			Fa[x] = top ? stk[top] : 0;
			stk[++ top] = x;
		}
	}
}

DSU on tree

  • 遍历每一个节点,统计每一个节点子树内的信息,设当前遍历到了节点 \(u\)
    • 递归计算 \(u\) 的所有轻子树的信息,回溯时需要撤销贡献。
    • 递归计算 \(u\) 的重子树的信息,回溯时不需要撤销贡献。
    • 遍历 \(u\) 的所有轻子树,统计所有轻子树的贡献。
  • 一个节点在重子树内被统计 \(1\) 次,在轻子树内被统计 \(\mathcal{O}(\log n)\) 次(取决于该点到根有多少条轻边)。
int tot, head[N], ver[N * 2], Next[N * 2];
void add_edge(int u, int v) {
	ver[++ tot] = v;    Next[tot] = head[u];    head[u] = tot;
} 

int dfsClock, dfn[N], idx[N];

int sze[N], son[N];

void dfs(int u, int fu) {
	dfsClock ++;
	dfn[u] = dfsClock, idx[dfsClock] = u;

	sze[u] = 1;

	for (int i = head[u]; i; i = Next[i]) {
		int v = ver[i];
		if (v == fu) continue;

		dfs(v, u);

		sze[u] += sze[v];
		if (sze[v] > sze[son[u]]) son[u] = v;
	}
}

void solve(int u, int fu, bool rem) {
	for (int i = head[u]; i; i = Next[i]) {
		int v = ver[i];
		if (v == fu || v == son[u]) continue;

		solve(v, u, 0);
	}

	if (son[u]) solve(son[u], u, 1);

	for (int i = head[u]; i; i = Next[i]) {
		int v = ver[i];
		if (v == fu || v == son[u]) continue;

		int nl = dfn[v], nr = dfn[v] + sze[v] - 1;
		for (int ni = nl; ni <= nr; ni ++) {
			int x = idx[ni];
			add(x);
		}
	}

	add(u);

	// ans[u] = ...

	if (!rem) {
		int nl = dfn[u], nr = dfn[u] + sze[u] - 1;
		for (int ni = nl; ni <= nr; ni ++) {
			int x = idx[ni];
			dec(x);
		}
	}
}

Trick

树上倍增

  • 现要求 \(x \to y\) 的信息和,若该信息满足 " 可重复贡献性 ",则可以将 \(x \to y\) 拆成 \(x \to \mathrm{LCA}(x, y)\)\(y \to \mathrm{LCA}(x, y)\) 两段直链,将每一段直链分别拆成自链底向上、自链顶向下的长度为 \(2\) 的极大整数次幂的链,用三次合并计算四个树上倍增数组的信息和即可。

树上差分

  • 对于大小为 \(m\) 的关键点点集 \(S\),现在要求包含关键点点集 \(S\) 的极小联通子图的边权和,记 \(u_i\) 为点集 \(S\) 中的点按 dfs 序排序后的结果,则答案为

\[\frac{1}{2}\sum\limits_{i = 1}^{m} \mathrm{dist}(u_i, u_{(i \bmod m) + 1}) \]

  • 对于大小为 \(m\) 的关键点点集 \(S\),现在要给包含关键点点集 \(S\) 的极小联通子图的所有点加上 \(d\),考虑树上差分后做一遍子树和,记 \(u_i\) 为点集 \(S\) 中的点按 dfs 序排序后的结果,则

    • 对于 \(1 \leq i \leq m\),令 \(u_i\) 的差分数组加上 \(d\)
    • 对于 \(1 \leq i < m\),令 \(\mathrm{LCA}(u_i, u_{i + 1})\) 的差分数组减去 \(d\)。特别地,令 \(\mathrm{fa}_{\mathrm{LCA}(u_1, u_m)}\) 的差分数组减去 \(d\)
  • 对于大小为 \(m\) 的关键点点集 \(S\),现在要给包含关键点点集 \(S\) 的极小联通子图的所有边加上 \(d\),考虑树上差分后做一遍子树和,记 \(u_i\) 为点集 \(S\) 中的点按 dfs 序排序后的结果,则

    • 对于 \(1 \leq i \leq m\),令 \(u_i\) 的差分数组加上 \(d\)
    • 对于 \(1 \leq i \leq m\),令 \(\mathrm{LCA}(u_i, u_{(i \bmod m) + 1})\) 的差分数组减去 \(d\)

路径加、子树查询 转 单点加、区间查询

  • 对于一个路径加 \((\mathrm{path}(x, y), +d)\),记 \(z = \mathrm{LCA}(x, y)\),将其拆成四个到根节点的路径加

\[(\mathrm{path}(1, x), +d) \\ (\mathrm{path}(1, y), +d) \\ (\mathrm{path}(1, z), -d) \\ (\mathrm{path}(1, \mathrm{fa}_{z}), -d) \]

  • 一个到根节点的路径加 \((\mathrm{path}(1, x), d)\),会对 \(x\) 的祖先节点 \(u\) 产生 \(d \cdot \mathrm{dep}_x - d \cdot (\mathrm{dep}_{u} - 1)\) 的贡献。

换根

  • 钦定以 \(1\) 为根,考虑换根对信息的影响。
  • 设当前的根为 \(\mathcal{R}\)
    • \(x\)\(\mathcal{R}\),则以 \(\mathcal{R}\) 为根时 \(x\) 的子树,为整棵树
    • \(x\)\(\mathcal{R}\) 的祖先,则以 \(\mathcal{R}\) 为根时 \(x\) 的子树,为整棵树刨去以 \(1\) 为根时 \(s\) 的子树,其中 \(s\)\(x\)\(\mathcal{R}\) 方向上的儿子。
    • 若是其他情况,则以 \(\mathcal{R}\) 为根时 \(x\) 的子树,为\(1\) 为根时 \(x\) 的子树
  • 设当前的根为 \(\mathcal{R}\),则以 \(\mathcal{R}\) 为根时的 \(\mathrm{LCA}(x, y)\),为以 \(1\) 为根时 \(\mathrm{LCA}(x, y), \mathrm{LCA}(x, \mathcal{R}), \mathrm{LCA}(y, \mathcal{R})\) 中深度最大者。

森林 / 沙漠 连通块计数

  • 森林:连通块个数 \(=\) 点数 \(-\) 边数。
  • 沙漠:连通块个数 \(=\) 点数 \(-\) 边数 \(+\) 环数。
posted @ 2022-12-19 10:23  Calculatelove  阅读(347)  评论(0编辑  收藏  举报