首页 > 其他分享 >OI 笔记:D - 数据结构

OI 笔记:D - 数据结构

时间:2022-12-19 10:34:46浏览次数:60  
标签:lc OI int void 笔记 Fa rc 数据结构 mathrm

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

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)\)。

历史线段树

猫树

  • 建猫树时,采用堆式建树(节点 \(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; 

		// [l, mid] ...
	
		// [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; 

		// [l, mid] ...

		// [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 ...;
	}
}

李超线段树

楼房重建式线段树

参考: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]])\)。

平衡树

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,需要保证第二棵树按中序遍历的顺序插入)。
namespace BST {
	int nClock, root;
	struct splay {
		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 p, int val) {
		int x = ++ nClock;
		t[x].lc = t[x].rc = 0, t[x].Fa = p;
		t[x].val = val;
		t[x].sze = 1;
		t[x].rev = 0;
		return x;
	}

	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 target) {
		int p;
		while (p = t[x].Fa, p != target) {
			if (t[p].Fa != target) rotate(dir(x) == dir(p) ? p : x);
			rotate(x);
		}
		if (!target) root = x;
	}
	
	void insert(int val) {
		int x = root, p = 0;
		while (x) p = x, x = t[x].son[val > t[x].val];
	
		x = New(p, val), t[p].son[val > t[p].val] = x;
	
		splay(x, 0);
	}
	
	int Get_rk(int k) {
		int x = root, ans = 0;
		while (x) {
			spread(x);
			if (t[t[x].lc].sze >= k) x = t[x].lc;
			else if (t[t[x].lc].sze + 1 == k) { ans = x; break; }
			else k -= t[t[x].lc].sze + 1, x = t[x].rc;
		}
		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;
		}
	}
*/
}

笛卡尔树

  • 每个节点由一个二元组 \((\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];

	int top, stk[N];
	void build() {
		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];
	}
}

分块

  • 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.
			}
		}
	}
}

树上莫队

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

  • 不妨设 \(\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[anc[i][y]]) 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)
			// calc ans[q[i].id] with q[i].z
		else
			// calc ans[q[i].id]
	}
}

莫队二次离线

序列分治

最值分治

线段树分治

线段树优化建图

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 - 数据结构 树上结构

重链剖分

  • 重链优先 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。

\(\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 操作,可以实现从某节点到根路径上的标记覆盖,可以实现从某节点到根路径上标记的批量处理(因为每条实链的标记都是相同的)。由于根是不变的,就不必写翻转标记了。
  • LCT 可以维护子树信息(需要满足可减性),需要维护虚子树的信息总和,只有 accesslink 操作需要改变。
  • LCT 可以维护黑白树
  • LCT 可以维护双连通分量。
  • LCT 可以维护生成树。
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);
	}
}
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;

    // ...
}
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);
	}
}

点分治、点分树

参考: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 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, 0), solve(A_rt);
	}
}

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

Trick

换根

  • 钦定以 \(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})\) 中深度最大者。

标签:lc,OI,int,void,笔记,Fa,rc,数据结构,mathrm
From: https://www.cnblogs.com/cjtcalc/p/16948343.html

相关文章

  • OI 笔记:A - 基础算法
    A-基础算法语言基础语言基础编译指令:-std=c++11:c++11标准。-O2:O2优化。-Wl,--stack=1280000000:开栈。-Wall:显示所有警告。-Wextra:检测可疑代码并生成警告。......
  • OI 笔记:C - 数学知识
    C-数学知识数学分析参考教材:数学分析教材,常庚哲/史济怀编著,中国科学技术大学出版社出版。3.1:导数的定义定义3.1.1:若函数\(f\)在点\(x_0\)的近旁有定义,且极......
  • delphi D11编程语言手册 学习笔记(P424-477) 泛型
      这本书可以在Delphi研习社②群256456744的群文件里找到.书名:Delphi11AlexandriaEdition.pdf 泛型在C++中叫做类型模板(templateclasses),单从字面上理......
  • 数据结构 玩转数据结构 7-3 集合类的复杂度分析
    0课程地址https://coding.imooc.com/lesson/207.html#mid=13705 1重点关注1.1结论使用二叉树实现集合Set性能优于使用链表实现集合Set. ......
  • Ynoi 切(受)题(虐)记录
    防止忘记做法大致按照切题顺序简略写一下思路,同时造福一下社会。(不过我怀疑大概只有我能看懂我自己写的。)缓慢更新。P4119[Ynoi2018]未来日记最初分块。人生第一道......
  • android view
    ReactNative已经封装了大部分最常见的组件,譬如ScrollView和TextInput,但不可能封装全部组件。在这个例子里,我们来看看为了让JavaScript中可以使用ImageView,需要做哪些......
  • [常用工具] shell脚本快速入门笔记
    Shell是一个用C语言编写的程序,它是用户使用Linux的桥梁。Shell脚本(shellscript),是一种为shell编写的脚本程序。业界所说的shell通常都是指shell脚本,但要知道,sh......
  • [编程基础] Python字符串替换笔记
    Python字符串替换笔记Python字符串替换笔记主要展示了如何在Python中替换字符串。Python中有以下几种替换字符串的方法,本文主要介绍前三种。replace方法(常用)translate......
  • P7710 [Ynoi2077] stdmxeypz 题解
    对@LHFdalao的题解进行一些补充说明。因为他讲的实在是太难懂了。最后在@_•́へ•́╬_dalao的帮助下才勉强知道怎么做。不过他的代码非常简洁易懂,有需求的可以去看......
  • selenium借助AutoIt识别上传(下载)详解
    AutoIt目前最新是v3版本,这是一个使用类似BASIC​​脚本语言​​​的​​免费软件​​​,它设计用于Windows GUI(​​图形用户界面​​)中进行自动化操作。它利用模拟键盘......