C++ 模板

编写编译器易优化、易读、易拓展、自解释的代码,并且配套文档。

C++ 模板的 代码文档比赛源码 都已放在 github

代码规范图论字符串 单独成篇

通用技巧

递归程序防止爆栈

  • 在 Windows 上,通常的方法是在 编译选项 中加入 -Wl,--stack=1000000000
  • 在 Linux 上,通常的方法是在运行程序前 在终端内 执行 ulimit -s unlimited (WSL1 下无法设置可惜)

C++ 黑魔法:\(n^2\) 过百万,编译器优化+指令集优化

博文ppt

但是实际上不需要这么麻烦,仅需在头部添加下面代码(codeforces 上支持)

#pragma GCC optimize("Ofast,no-stack-protector,unroll-loops")
#pragma GCC target("sse,sse2,sse3,ssse3,sse4.1,sse4.2,abm,mmx,avx,avx2,popcnt,tune=native")

示例:

bitset 高端压位卡常

典型应用,求传递闭包,高维偏序。

概率问题永不 TLE 的技巧

// 概率问题永不 TLE 的技巧
auto begin = std::chrono::steady_clock::now();
while ((std::chrono::steady_clock::now() - begin).count() < 5e8) { // 数值取时限的一半
	// do something
}

Python 输入样例(以备不时之需,用 PyPy3 提交)

用 Python 过的一次大数题:490C

# 多 case 输入
for _ in range(int(input())):
# 单行输入
    n = int(input())
# 两个元素一行输入
	a, b = map(int, input().split())

# 提前结束	
exit()

取平均值(防溢出,C++20 就有 mid 函数了)

(x & y) + ((x ^ y) >> 1) // 向下取整
(x | y) - ((x ^ y) >> 1) // 向上取整

交互式题目模板

gym101021: Guess the Number
需要 fflush(stdout);(对于 scanf/printf) 或 std:::cout << std::flush (对于 std::cin/std::cout) 来刷新缓冲区,不过 std::endl 会自动刷新一次缓冲区,所以此时可以省略。

注意 std::endl\n 的区别是前一个刷新缓冲区,后一个不刷新。仅在交互问题或者 debug 的时候使用 std::endl;

负数下标技巧

const int N = 1e5 + 2;
int aa[N];
int *a = (aa + N / 2);

可用于 \(O(1)\) 首尾插入或删除元素,访问第 \(i\) 个元素。
当然也可以用 std::deque 加一个标号,实现上述操作

优雅的输出技巧

int n = 10;
for(int i = 0; i < n; ++i) {
	std::cout << i << " \n"[i == n - 1];
}

带取整的函数取最值的技巧

  • 先考虑不取整的情况,然后一般这个值是可能的最小值或者最大值
  • 然后通过循环看是否满足取整的情况

输出全排列

void permutation(int n){
	std::vector<int> x(n);
	std::iota(x.begin(), x.end(), 1);
	do {
		std::for_each(x.begin(), x.end(),[](int i){
			std::cout << i;
		});
		std::cout << std::endl;
	} while (std::next_permutation(x.begin(), x.end()));
}

输出全排列的原理

首先初始状态从小到大排列,然后对每一个状态考虑它的后缀,如果后缀是从大到小排列,再考虑向前一位的后缀,直到不是从大到小排列,然后找比第一个位置大的最小值放在开头,其它位置排序。

import math
def permutation(n):
	ans = []
	cnt = math.factorial(n);
	r = list(range(1, n + 1))
	ans.append(r.copy())
	cnt -= 1
	while cnt > 0:
		i = n - 1
		while r[i - 1] > r[i]: i -= 1
		r[i:] = r[i:][::-1]
		j = i
		while r[j] < r[i - 1]: j += 1
		r[i - 1], r[j] = r[j], r[i - 1]
		ans.append(r.copy())
		cnt -= 1
	return ans	

for i in range(1,5):
	print(permutation(i))

通用知识

产生 log 的几个原因

  1. 二分,三分
  2. \(1 + \frac{1}{2} + \cdots \frac{1}{n} \sim \log n\)
  3. \(\frac{1}{2} + \cdots \frac{1}{p} \sim \log \log n\)
  4. 树状数组,线段树
  5. 重链剖分
  6. 倍增(太好用了,特别是 nxt 数组进行加速)
  7. 莫队(离线算法)

产生根号的几个原因

  1. 朴素判断素数
  2. 整除分块:\(\lfloor \frac{n}{i} \rfloor\) 的值域是 \(O(\sqrt{n})\)
  3. \(\max(x + \frac{n}{x})\)
  4. 网络流中 HLPP(没读过这篇复杂度分析的论文,不懂)
  5. 分块处理,分块打表

const VS constexpr

从 2021-4-27 开始使用 constexpr。对于变量而言,它们的本质区别,一个编译器初始化,一个运行时初始化。

int VS long long

以前当 int 类型出现带模乘法的时候,我们一般就直接使用 LL 了,不用强制类型转化了。但是后来转而使用 int 了。

注意到 LL(a) * b % n1LL * a * b 在汇编意义下没有区别。两种情况哪个方便写那个。放弃 LL 的主要理由:

  • 本身就是 int 型的变量,为什么用 LL 存储呢?
  • 内存减少一半
  • 在 32 位机器上更快。

最大最小值分配律

\[\begin{aligned} \min(\max(x, a), b) = \max(\min(x, b), \min(a, b)) \\ \max(\min(x, a), b) = \min(\max(x, b), \max(a, b)) \end{aligned} \]

例题:Atcoder abc196E

cout, cerr, clog 的区别

cerr 可看作自动刷新缓冲区的 clog。 cout 和 clog 的区别就是 clog 直接打印在屏幕上,而我们文件输入输出的时候使用 freopen("out", "w", stdout) 后 cout 会输出到 out 文件中,而 clog 依然打印在屏幕中,这就很有用了。

动态规划思想

动态规划的能力犹如程序员的内功

水涨船高技巧

把一个集合中所有元素加一个常数,可以不操作,加在水位线上即可

Meet in Middle(拆半搜索法)

类似于动态规划,是一种思想。特别适合处理指数复杂度。

例题:AtCoder abc184F,当然针对此题可以深搜剪枝法。

Small to large(把小的合并到大的里面去)

例子:并查集(dus),map 的合并,树上启发式合并(dus on tree),重链剖分。

例题:600E题解

倍增思想(太强了)

例子:RMQ,LCA。nxt 数组进行加速

整除:C++ VS Python

  • C/C++ 中,整数除法 / 是向 0 取整(int(x)也是向 0 取整)
  • Python/Sagemath 中,整数除法 // 是向下取整。

在 C++ 中一定不要用 (x - 1) / n + 1 的姿势向上取整!

位运算

位运算的关系

  • 异或 1 改变,异或 0 不变
  • 某位异或位 0,表示此位相等,反之不等。
  • \(a \oplus b = (a \mid b) \oplus (a \And b)\)
  • \(a \oplus b = (a \mid b) - (a \And b)\)
  • \(a + b = (a \mid b) + (a \And b)\)
  • \(a + b = (a \oplus b) + 2 (a \And b)\)
  • (a & b) | c = (a | b) & (a | c)
  • (a | b) & c = (a & b) | (a & c)
  • (a | b) ^ 1 = (a ^ 1) & (b ^ 1)
  • (a & b) ^ 1 = (a ^ 1) | (b ^ 1)
  • (a | b) ^ c(a & b) ^ c 可以逐位转化,因此任何一个数 x 经过任意多次的&, |, ^ 运算最终都可以写成 ((x ^ a) & b) | c

最高位后面位取反 \(O(1)\)

int reverseBit(int n) {
	return n ^ ((1 << 32 - __builtin_clz(n)) - 1);
}

最低位 1 置 0: x = x & (x - 1)

树状数组中使用的 lowbit(x) = x & (-x) 得到 x 的最大 2 的幂次因子。

mask 位上暴力枚举

for (int i = n; i; i = (i - 1) & n) {
	// do something
}

Gosper's Hack:n 个集合中选 k 个

思路:想想怎么把 1011100 变成 110011

void GospersHack(int n, int k) {
	int cur = (1 << k) - 1;
	int limit = (1 << n);
	std::vector<int> r;
	while (cur < limit) {
		// do something
		int lb = cur & -cur;
		int r = cur + lb;
		cur = (((r ^ cur) >> 2) / lb) | r;
	}
}

异或运算是一种很神奇用途很广的运算. 从性质上, 异或运算作为二元运算, 关于所有非负整数构成一个 Abel 群, 0 作为幺元, 每个元的逆元都是自身(等价于说 \(char(N ^ \star,xor)=2\))。

异或找出唯一出现奇数次的数

把这一堆数全体直接异或即可.

这个方法可以推广到找出两个只出现奇数次的, 其它出现偶数次的两个数, 方法就是先异或之后的值按照最高位进行标记然后分成两组, 再来一遍

数学

常用组合数公式及其直观解释

对任意实数,定义:\(\binom{\alpha}{k} = \frac{\alpha(\alpha - 1) \cdots (\alpha - k + 1)}{k !}\) 所以我们有:

\[\binom{-n}{k} = (-1)^{k} \binom{n + k - 1}{k} \]

\[\binom{n + 1}{k + 1} = \binom{n}{k} + \binom{n}{k + 1} \]

最后一个数,先还是不选,这是一个问题

\[{n \choose k}{k \choose i}  = {n \choose i} {n - i \choose k - i} \]

组合意义理解:\(n\) 个人中选出 \(i\) 个一流人才, \(k - i\) 个二流人才

\[{n + m \choose k} = \sum_{i + j = k} {n \choose i} {m \choose j} \]

组合意义理解:\(n, m\) 两个堆选出 \(k\) 个人

拓展 Euler 定理

数论中欧拉定义说:若 \(\gcd(a, m) = 1\)\(a^{\phi(m)} \equiv 1 \mod m\)

类似于拓展的 Fermat 小定理:\(a^p \equiv a \mod p\),我们有拓展 Euler 定理:

\[a^n \equiv a^{n \mod \phi(m) + phi(m)} \mod m \]

证明对 \(m\) 素因子分解,再利用 Euler 函数是可乘函数,显然。

求原根

首先,模 \(m\) 有原根的充要条件是:\(m=2,4,p^a,2p^a\),其中 \(p\) 为奇素数。

对于求模奇素数 \(p\) 的原根方法:对 \(p-1\) 素因子分解:\(p-1 = p_1^{a_1} \cdots p_s^{a_s}\) 若恒有

\[g^{\frac{p-1}{p_i}} \neq 1(\mod \; p) \]

\(g\) 是 模 \(p\) 的原根。对于 \(p^a\) 的原根 为 \(g\)\(g + p\),若 \(p^a\) 的原根为 \(g_a\)\(2p^a\) 的原根为 \(g_a\)\(g_a + p^a\) 的中奇数者。所有原根:可以先求出一个原根,然后所有的数便是 \(g^0, g^1, \cdots, g^{\phi(m) - 1}\), 所有原根就是那些 \(\gcd(i, \phi(m)) = 1\)\(g^i\) (证明见 P150《数论基础》潘承洞)。

基于上述方法的代码实现

另外更好的做法:

我们首先求出 \(m = \phi(n)\),然后一个个的搜,搜索的时候附带把很多点都给剔除了,所以很快就能找到!具体代码

自然数方幂和精确版

#include <boost/multiprecision/cpp_int.hpp>
using BINT = boost::multiprecision::cpp_int;

BINT f[N];
BINT getPowSum(LL n, int k) { // k<1000
	if (k == 0) return BINT(n);
	if (p[1] != 2) spf();
	int nk = 2 * k + 1;
	f[0] = 0;
	f[1] = 1;
	auto bPow = [](BINT x, int n) -> BINT {
		BINT r(1);
		for (; n; x *= x, n >>= 1) if (n&1) r *= x;
		return r;
	};
	for (int i = 2; i <= nk + 1; ++i) {
		if (sp[i] == i) f[i] = bPow(BINT(i), k);
		else f[i] = f[sp[i]] * f[i / sp[i]];
	}
	for (int i = 1; i <= nk; ++i) f[i] += f[i - 1];
	if (n <= nk) return f[n];
	BINT res = 0, tl = 1, tr = 1;
	for (int i = nk - 1; i >= 0; --i) tr = tr * (n - i - 1) / (nk - i);
	for (int i = 0; i <= nk; ++i) {
		if ((nk - i) & 1) res -= f[i] * tl * tr;
		else res += f[i] * tl * tr;
		tl = tl * (n - i) / (i + 1);
		tr = tr * (nk - i) / (n - i - 1);
	}
	return res;
}

需要下载boost 包 类似的包还有 NTL,GMP

生成函数

Polya 说过:生成函数就像袋子,把一个个零碎的小部件放在一个袋子里,就可以优雅高效的只关注袋子了。

在 codeforces 上 zscoder 大佬给了一个 入门教程进阶教程 还有 MiFaFaOvO终极教程

生成函数分两种:Original generating function,Expentional generating function,选择哪一种是看问题中是否牵扯组合数。无论哪一种都能保存原数列的全部信息,并且由于级数可以使用微积分和常微分方程的技术,所以会变得更好处理。然后大概率可以优化算法复杂度 \(O(n^2) \to O(n \log n)\)

关于生成函数多项式的处理:https://cp-algorithms.com/algebra/polynomial.html

多项式高效运算模板:https://github.com/e-maxx-eng/e-maxx-eng-aux/blob/master/src/polynomial.cpp

生成函数一般的处理思路:计算生成函数,分解成有分母不超过二次的分式之和,然后每一个二次的分母部分找一个递推数列来搞定。

OI-wiki 多项式运算

多项式计数杂谈 很值得学习一下。

多项式取对数和指数

\(B(z) = e^{A(z)}\),即 \(A(z) = \ln B(z)\) (不妨假设 \(A(0) = 0\) 或等价地 \(B(0) = 1\))

那么 \(B'(z) = A'(z) \cdot B(z)\), 所以 \([z^{n - 1}] B'(z) = \sum_{k = 0}^{n - 1} [z^k] A'(z) \cdot B(z) [z^{n - 1 - k}] = \sum_{k = 1}^{n} [z^{k - 1}] A'(z) \cdot B(z) [z^{n-k}]\),从而

\[n [z^n] B(z) = \sum_{k = 1}^n k [z^k] A(z) \cdot B(z) [z^{n - k}] \]

上式等价于

\[n [z^n] A(z) = n [z^n] B(z) - \sum_{k = 1}^{n - 1} k [z^k] A(z) \cdot B(z) [z^{n - k}] \]

参考:Soulist

差分约束

\(n\) 个变量,\(m\) 个约束条件,每个约束条件都形如 \(x_i - x_j \leq c_k\),此时我们从节点 j 向 i 连一条长度为 \(c_k\) 的有向边,(如果有等于号,我们就连两条),设 dist[0] = 0,然后 0 节点向所有节点连一条长度为 0 的有向边。跑单源最短路,如果环中有负环,那么无解,否则 \(x_i = dist[i]\) 为一组解。

可用图论中 Bellman-Ford 算法,或 spfa(随笔图跑的快),例题:LOJ P1993spfa 做法Bellman-Ford 做法

变式:\(\frac{x_i}{x_j} \leq c_k\)(取 log 即可)

博弈

数据结构

逆序数

  1. 直接求 \(O(n^2)\) 没啥好写的。
  2. 把原数组每个位置进行编号,排序,然后每次把最大的数的编号丢进树状数组中,丢进去先看这个编号前面有多少个数,累加一下就可以了,\(O(n^2)\),结合下面树状数组的知识还是很简单的。
  3. 带离散化的树状数组(就是如果元素的数值特别大,树状数组内存就不够了,所以需要离散化一下)
  4. 归并的求(不会也不想搞 0.0)
  5. 逐位处理(代码如下)

树状数组加强版(区间更新,区间求和,编号从 1 开始)

有了单点更新的树状数组,只需简单利用差分就可以变成区间的更新了。
设原始数组为 a[1 ~ n], 定义 c[i] = a[i] - a[i - 1], (a[0] = 0) 显然

\[\sum_{i = 1}^m a_i = \sum_{i = 1}^m (m - i + 1) c_i = m \sum_{i = 1}^m c_i - \sum_{i = 1}^m (i - 1) c_i \]

比如对区间 [l, r] 做更新,那么就只需更新两点:r + 1, l ,套用之前的类就行了。

注意在树状数组中搜索本来应该是 \(O(\log ^2 n)\),但是因为在 \(2^i\) 的位置搜索时,一步到位。所以复杂度会降到 \(O(\log n)\)理论依据

线段树节点上届

首先显然总节点 \(m\) 上界为 \(4n\),并且可以证明 \(\frac{m}{n}\) 的上确界为 \(4\),下确界为 \(2\) 注意到如果 \(n = 2^k + 2^{j + 1}\) 时,则 \(m = 2 ^{k + 1} + 2^k + \cdots 2^{k - j} + 1\),所以 \(\frac{m}{n} = \frac{4 - 2^{-j} + 2^{-k}}{1 + 2^{j + 1 - k}}\),对任意 \(\epsilon > 0\) 存在 \(j\) 使得 \(4 - 2 ^{-j} > 4 - \epsilon\), 然后让 \(k\) 趋于无穷,那么显然 \(\frac{m}{n}\) 上极限为 \(4\).(\(n = 40\) 时, \(\frac{m}{n} > 3\)\(n = 2^20 + 2^10 = 1049600\) 时,\(\frac{m}{n} > 3.99\)

和与最大值的线段树模板(如果单纯求和,可以用树状数组),现在使用左闭右开线段树,且使用吉老师 pushTag 版本

三分法简单版

单峰函数都可以用,区别就是选择大于号还是小于号

int l = 1, r = 1e9;
while (r > l) {
	int m = (r - l) / 3;
	int lm = l + m, rm = r - m;
	if (f(lm) < f(rm)) r = rm - 1;
	else l = lm + 1;
}
return l;

标准三分法用黄金分割的原因

我们不妨设原始区间为 [0, 1],我们在其中选两个点 0 < a < b < 1,然后比较 f(a)f(b),然后再相应改变区间。然后重复上述过程。如果我们能充分利用计算过的值,也就是说假设更新后的区间为 [0, b] 那么我们自然想让 a 的计算值充分被利用,所以我们想再选的两个点的其中一个是 a,如果更新后区间为 [a, 1] 同理。也就是说我们有策略

\[\frac{a}{b} = b, \frac{b - a}{1 - a} = a \]

化简可得 \(b(1 + b) = 1\),即 \(b = \frac{\sqrt{5} - 1}{2}, a = b ^ 2 = \frac{3 - \sqrt{5}}{2} = 1 - b\)

注意到上述 \(b\) 的值正好是黄金分割 0.618...

背包

const int MAX = 1e5 + 5;
int r[MAX];
void pack(int cash, int num, int v, int w) {
	if (num == 0 || w == 0 || v == 0) return;
	if (num == 1) { //  0-1背包
		for (int i = cash; i >= v; --i)
			r[i] = max(r[i], r[i - v] + w);
		return;
	}
	if (num * v >= cash - v + 1) { //完全背包
		for (int i = v; i <= cash; ++i)
			r[i] = max(r[i], r[i - v] + w);
		return;
	}
	int q[MAX], s[MAX], head, tail;
	for (int j = 0; j < v; ++j) { //多重背包
		q[0] = r[j];
		s[0] = head = tail = 0;
		for (int i = 1, k = j + v; k <= cash; ++i, k += v) {
			q[i] = r[k] - i * w;
			while (s[head] < i - num) ++head;
			while (head <= tail && q[tail] < q[i]) --tail;
			s[++tail] = i;
			q[tail] = q[i];
			r[k] = q[head] + i * w;
		}
	}
}

堆与 STL 优先队列

可以使用 C++STL 的 priority_queue,查找可用 lower_bound 和 upper_bound。C++ STL 中优先队列是用堆来实现的。用途十分广泛,例如加速最小生成树,拓扑排序,等等。
堆的实现一般是用数组。 我们可以用 1 作为树的根, 对每一个节点 \(x\), 它的两个节点分别就是 \(2x\)\(2x + 1\) 平时都用 x << 1, x << 1 | 1 表示。 堆只支持三个操作:

  1. 插入一个节点(我们实现时是插入最尾部, 这样保证了是一个完全二叉树) \(O(\log n)\)
  2. 删除最大键值节点(删除根元素的值) \(O(\log n)\)
  3. 输出最大键值节点(查看根元素的值) \(O(1)\)

单调队列:解决滑动窗口问题(固定长度内的最值问题)

知乎 Pecco 讲的很好(建议直接去看它的讲解):

如果一个选手比你小还比你强,你就可以退役了。——单调队列的原理

// 求每个长度为 m 的区间最大值的编号
std::vector<int> monicDequeMax(std::vector<int> &a, int m) {
	std::vector<int> r;
	std::deque<int> Q;
	for (int i = 0; i < a.size(); ++i) {
		if (!Q.empty() && i - Q.front() >= m) Q.pop_front();
		// 如果求最小值,大于号改成小于号即可
		while (!Q.empty() && a[i] > a[Q.back()]) Q.pop_back();
		Q.push_back(i);
		// 如果需要返回值,就在下面加入 a[Q.front()]
		if (i >= m - 1) r.emplace_back(Q.front());
	}
	return r;
}

例题:LOJ P2216:这个是二维的,我们可以一维一维的处理

单调队列优化 DP

例题:LOJ P2034:取数字使得和最大,但是不能取连续 k 个数

肯定是 dp 问题,如果把 dp[i] 定义成取第 i 个,前 i 个结果的最值,会发现很搞。
因此我们反过来考虑。考虑删除若干个数,且删除的间隔不超过 k,求删除的最小和。最终答案就是总和减去最小和。设 dp[i] 表示删除 i,且满足性质的前 i 个数的答案。那么显然 \(dp[i] = a[i] i \leq k\)\(dp[i] = a[i] + \min_{i - k \leq j \leq i - 1} dp[j]\)。注意最终答案不是总和减去 dp 的 最小值,而是 \(dp[n - k - 2, \cdots, n - 1]\) 的最小值。

#include <bits/stdc++.h>
#define watch(x) std::cout << (#x) << " is " << (x) << std::endl
using LL = long long;
int main() {
	// freopen("in", "r", stdin);
	std::ios::sync_with_stdio(false);
	std::cin.tie(nullptr);
	int n, k;
	std::cin >> n >> k;
	std::vector<int> a(n), dp(n);
	LL s = 0;
	for (auto &x : a) {
		std::cin >> x;
		s += x;
	}
	std::deque<int> Q;
	for (int i = 0; i < n; ++i) {
		dp[i] = a[i];
		if (i >= k + 1) dp[i] += dp[Q.front()];
		if (!Q.empty() && i - Q.front() >= k + 1) Q.pop_front();
		while (!Q.empty() && dp[i] <= dp[Q.back()]) Q.pop_back();
		Q.push_back(i);
	}
	std::cout << s - *std::min_element(dp.end() - k - 1, dp.end());
	return 0;
}

单调栈:形式更简单应用更广

知乎 Pecco 的精彩讲解:维护一个栈,当我们碰上一个新元素,我们知道,越靠近栈顶的元素离新元素位置越近。所以不断比较新元素与栈顶,如果新元素比栈顶大,则可断定新元素就是栈顶的下一个更大元素,于是弹出栈顶并继续比较。直到新元素不比栈顶大,再将新元素压入栈。显然,这样形成的栈是单调递减的。

应用一:求下一个比自身大的元素位置(下可以改成上,大可以改成小)

洛谷模板题:LOJ P5788

应用二:两个元素间所有元素均(不)大/小于这二者。

洛谷进阶题:LOJ P1823,问有多少对元素,它们之间没有比它们都大的元素。

单调栈优化 DP

应用一优化 DP 例题:1313C2,首先最优答案肯定时先递增后递减的。相当于有一个制高点,枚举制高点,自然有 \(O(n^2)\) 的算法。但是可以优化到 \(O(n)\)

应用二优化 DP 例题:1407D,每次跳跃,它们之间的元素都严格大于它们或者严格小于它们。首先设 dp[i] 为到达 i 最小跳跃数,那么显然 \(\displaystyle dp[i] = \min_{j \to i} dp[j] + 1\)。我们可以用两个单调栈来看那些 j 能跳到 i。

几何

分治法求平面最短距离(任何距离都适用)

首先根据横坐标排序,然后取中位数假设处理好了左右两边的值,然后合并中间的值,首先距离中心点的横坐标不能超过已知的最小值,然后把筛出来的点按照纵坐标排序,然后 \(O(n)\) 更新答案。总题复杂度 \(O(n \log^2 n)\),如果使用归并排序理论复杂度为 \(O(n \log n)\),但是实际效果并不如直接排序。

例题:[https://www.luogu.com.cn/problem/P1429] 和 LOJ P6247

using Point = std::pair<double, double>;
// 这里不要用 dist2,否则很多比较的地方都要平方,反而不优雅了。
double dist (const Point& p, const Point &q) {
	double x = q.first - p.first, y = q.second - p.second;
	return std::sqrt(x * x + y * y);
};
double minDist(std::vector<Point> a) {
	double d = DBL_MAX;
	int n = a.size();
	if (n <= 1) return d;
	std::sort(a.begin(), a.end());
	std::function<void(int, int)> merge = [&](int l, int r) {
		if (r - l <= 1) return;
		if (r - l == 2) {
			d = std::min(d, dist(a[l], a[l + 1]));
			return;
		}
		int m = (l + r) / 2;
		merge(l, m);
		merge(m + 1, r);
		std::vector<Point> p;
		for (int i = m - 1; i >= l && a[m].first - a[i].first < d; --i) {
			p.emplace_back(a[i].second, a[i].first);
		}
		for (int i = m; i < r && a[i].first - a[m].first < d; ++i) {
			p.emplace_back(a[i].second, a[i].first);
		}
		std::sort(p.begin(), p.end());
		for (int i = 0; i < p.size(); ++i) {
			for (int j = i + 1; j < p.size() && p[j].first - p[i].first < d; ++j) {
				d = std::min(d, dist(p[i], p[j]));
			}
		}
	};
	merge(0, n);
	return d;
}

分治法还能求两个点作为对交的矩阵的最大面积

三维偏序之陈丹琪分治(仅支持离线查询,还是直接暴力好用~)

如果带更新怎么处理呢?先预处理求出,之后更新一个计算一个更新?(那也不太行呀)

一般地,我们考虑可以考虑 \(k\) 维偏序,设有 \(n\)\(k\) 维向量,\(a_j \leq a_i\) 当且仅当所有的下标都满足小于等于关系,想知道对任意 \(i\) 有多少个 \(j \neq i\) 使得 \(a_j \leq a_i\)

有复杂度 \(O(n \log^k n)\) 的算法,因此在 \(k > 3\) 时,我们会选择直接 \(O(n^2)\) 暴力解决问题(见下小节)。

  • \(k = 1\) 时,我们直接排序,假设没有相同元素,那么它们排完序之后的位置就是答案,有相同的数字的话可以先合并,也可以用 upper_bound 查找出结果。复杂度 \(O(n \log n)\)
  • \(k = 2\) 时,我们先对第一个坐标偏序,再来一个树状数组,一个个的加入元素,加入之前可以查询结果。这也是求逆序数的操作(如果数据值域范围很大,可以离散化处理一下,仅需对要加入树状数组的那一维离散化,排序可以使用下标排序,就可以避免使用 tuple)。

因此三维偏序是一个空缺的问题,就有大名鼎鼎的 cdq 分治。

模板例题:LOJ P3810,这个题的题解中,有人讲的很好,echo6342:

cdq 分治每次计算前一半对后一半的影响。具体地,假设三维分别是 \(x, y, z\),先按 \(x\) 排序。分治时每次将前半边、后半边分别按 \(y\) 排序。虽然现在 \(x\) 的顺序被打乱了,但是前半边还是都小于后半边的,所以要是只计算前半边对后半边的偏序关系,是不会受到 \(x\) 的影响的。维护后一半的指针 i,前一半的指针 j,每次将 i 后移一位时,若 \(y[j] \leq y[i]\) 则不断后移 j,并不断将 z[j] 加入树状数组。然后再查询树状数组中有多少数小于等于 z[i]。 最后要清空树状数组(注意清空的时候不能直接清空,而是根据更新的命令,反向一次命令来清空,否则一直开树状数组耗时的),还有就是要去重贼麻烦,还是弃用吧。

struct Node {
	int x, y, z, id, w;
	bool operator<(const Node &A) const {
		if (x == A.x) return y == A.y ? z < A.z : y < A.y;
		return x < A.x;
	}
};
// ans[i] 表示 小于或等于 a[i] 的元素个数
std::vector<int> cdq(std::vector<Node> &a, int k) {
	// 先按照 y 排序,免得后面代码写的太麻烦
	std::vector<int> ans(a.size());
	std::sort(a.begin(), a.end());
	// 去重操作
	int last = 0;
	for (int i = 1; i < a.size(); ++i) {
		if (a[i].x != a[i - 1].x || a[i].y != a[i - 1].y || a[i].z != a[i - 1].z) {
			int t = i - last - 1;
			for (int j = last; j < i; ++j) {
				ans[a[j].id] = t;
				a[j].w = 0;
			}
			a[i - 1].w = i - last;
			last = i;
		}
	}
	int t = a.size() - last - 1;
	for (int i = last; i < a.size(); ++i) {
		ans[a[i].id] = t;
		a[i].w = 0;
	}
	a.back().w = a.size() - last;
	TreeArray A(k);
	auto cmpy = [](const Node &lhs, const Node &rhs) {
		return lhs.y < rhs.y;
	};
	std::function<void(int, int)> divide = [&](int l, int r) {
		if (r - l <= 1) return;
		int m = (l + r) / 2;
		divide(l, m);
		divide(m, r);
		std::sort(a.begin() + l, a.begin() + m, cmpy);
		std::sort(a.begin() + m, a.begin() + r, cmpy);
		int t = l;
		for (int i = m; i < r; ++i) {
			while (t < m && a[t].y <= a[i].y) {
				A.add(a[t].z, a[t].w);
				++t;
			}
			ans[a[i].id] += A.sum(a[i].z);
		}
		for (int i = l; i < t; ++i) A.add(a[i].z, -a[i].w);
	};
	divide(0, a.size());
	return ans;
}

k 维偏序(暴力 bitset 优化,分块时间换空间) \(O(\frac{k n^2}{w})\)

const int N = 4e4 + 2;
// a 是 k * n 矩阵表示 n 个 k 维向量,输出每个小于自身的向量个数
std::vector<int> partialOrder(std::vector<std::vector<int>> &a) {
// 直接暴力不太行,所以需要时间换空间,具体说就是分块。
	int k = a.size(), n = a[0].size();
	using Node = std::vector<std::pair<int, int>>;
	std::vector<Node> f(k, Node(n));
	for (int i = 0; i < k; ++i) {
		for (int j = 0; j < n; ++j) f[i][j] = {a[i][j], j};
		std::sort(f[i].begin(), f[i].end());
	}
	int sn = std::sqrt(n);
	using Data = std::vector<std::bitset<N>>;
	std::vector<Data> bs(k, Data(n / sn + 1));;
	for (int i = 0; i < k; ++i) {
		std::bitset<N> now;
		for (int j = 0; j < n; ++j) {
			if (j % sn == 0) bs[i][j / sn] = now;
			now.set(f[i][j].second);
		}
		if (n % sn == 0) bs[i][n / sn] = now;
	}
	auto getbst = [&](int i, int val) -> std::bitset<N> {
		// 如果求小于或等于的个数,这里要改成 upper_bound 并且要用 INT_MAX,还有最终答案减 1(去掉自身)
		int j = std::lower_bound(f[i].begin(), f[i].end(), std::make_pair(val, INT_MIN)) - f[i].begin();
		std::bitset<N> r = bs[i][j / sn];
		for (int t = j / sn * sn; t < j; ++t) r.set(f[i][t].second);
		return r;
	};
	std::vector<int> r(n);
	for (int j = 0; j < n; ++j) {
		std::bitset<N> now; now.set();
		for (int i = 0; i < k; ++i) {
			now &= getbst(i, a[i][j]);
		}
		r[j] = now.count();
	}
	return r;
}

模板例题:LOJ U66865,可参考 小蒟蒻 yyb 的博客 中的 ppt 实现。其它例题:偏序++

虽然三维偏序问题用 cdq 分治更好,但是用 bitset 暴力过题还是没啥问题的,例如 LOJ P3810

递归算法复杂度定理(算法导论)

递归算法复杂度定理

posted @ 2021-06-23 04:04  izlyforever  阅读(200)  评论(0编辑  收藏  举报