一些不体系但常见的小型科技
一 整数分块
对于定式
可以在 \(O\sqrt{n}\) 时间内算出。
感受 \(n \frac{1}{x}\ s.t.\ 1 \leq x \leq n\) 的曲线,线下整点一定是非递增、且分段分布的。
取其中一段 \([l, r]\) 可以满足 \(\forall i,\ s.t.\ l \leq i \leq r\) 有 \(d = \lfloor \frac{n}{i} \rfloor\) 相等,且可以感受到 \(r \mid n\) 。
于是有代码
for (ll l = 1; l <= r; l++) {
ll d = n / l, r = n / d;
// (r - l + 1)
l = r;
}
定理:\(\lfloor \frac{x}{n} \rfloor\ s.t.\ 1 \leq x \leq n\) 的曲线整点有 \(O(\sqrt{n})\) 段。
证明:
\(\forall x(1 \leq x \leq \sqrt{n})\) ,由 \(\lfloor \frac{n}{x} \rfloor\) 不会超过 \(\sqrt{n}\) 个。
\(\forall x(\sqrt{n} \leq x \leq n)\) ,有 \(\forall \lfloor \frac{n}{x} \rfloor < \lfloor \sqrt{n} \rfloor\) ,由鸽巢原理 \(\lfloor \frac{n}{x} \rfloor\) 不会超过 \(\sqrt{n}\) 个。
由于整除分块的取值单调不增,于是每个取值为一段,于是共有 \(O(\sqrt{n})\) 段。
\(\square\)
二 longlong O(1) 乘
- \(unsigned\) 溢出切高位
于是可以用 \(ull\) 表示出 \(ab\) 和 \(\lfloor \frac{ab}{m} \rfloor m\) ,然后通过 \(ab - \lfloor \frac{ab}{m} \rfloor m\) 得到 \(ab \bmod m\) 。 - \(long\ double\) 的精度 \(64\) 位,\(long\ long\) 有 \(64\) 位 \(Byte\) ,于是 \(long\ long\) 的结果可以被 \(long\ double\) 表示。
- 浮点数可以损失精度表示超大数,于是可以选择 \(long\ double\) 表示表示不超过 \(ull\) 的运算结果 \(\frac{ab}{m}\) ,然后再取 \(ull\) 。
- 浮点数运算的中间精度丢失,会导致取整后有最多 \(\pm 1\) 的误差。
于是有
ll modmul(ll a, ll b, ll m) {
ll res = (ull)a * b - (ull)((long double)a * b / m) * m;
if (res > m) res -= m;
if (res < 0) res += m;
return res;
}
三 快速幂
对于类型为 \(T\) 的数 \(A\) ,类型为十进制整数的幂 \(n\) 。\(T = \{\textbf{整数形,方阵形,复数形},\cdots\}\)
- 有
- 又有
于是有
T ksm(T a, ll n) {
T res = a.e; // a 的单位元
for (;n;n>>=1,a=a*a) if (n & 1)
res *= a;
return res;
}
四 \(O(n \varepsilon)\) 排序:基数排序
4.1 桶排序
给定 \(n\) 个数,值域范围 \(m\) ,可以用桶排序做到 \(O(n + m))\) 排序。
原理:用数组开桶奖权值存储,然后对桶进行遍历得到排序。只有一行代码。
for (int i = 1; i <= n; i++) c[a[i]]++;
for (int i = 0; i <= m; i++) {
for (int j = 0; j < c[i]; j++) {
std::cout << i << "\n";
}
}
优点:\(O(n + m)\) 的时间复杂度某些情况下可以做到很快。
缺点:\(m\) 很大时不能使用,排序没有稳定性。
4.2 计数排序
计数排序基于桶排序。可以得到两个数组 \(na\) 和 \(pa\) 。
\(na_i\) 为这一次排序前,第 \(i\) 个位置在现在的排名。
\(pa_i\) 为这一次排序后,第 \(i\) 个位置在之前的排名。
对桶求一个前缀和得到计数数组,于是计数数组存储即以它下标为权值的数最后出现的位置。
倒序遍历可得到 \(na\) 数组,然后通过 \(na\) 数组得到 \(pa\) 数组。
排序后第 \(na[i]\) 个数为 \(a[i]\) ,排序后第 \(i\) 个数即 \(pa[a[i]]\) 。
for (int i = 1; i <= n; i++) sa[i] = i;
void CountingSort(){
for (int i = 1; i <= m; i++) c[i] = 0; // 初始化
for (int i = 1; i <= n; i++) c[a[i]]++;
for (int i = 1; i <= m; i++) c[i] += c[i - 1];
for (int i = n; i; --i) na[pa[i]] = c[a[i]]--;
for (int i = 1; i <= n; i++) pa[na[i]] = i;
}
优点:\(O(n + m)\) 的复杂度某些情况下可以做到很快。\(na\) 和 \(pa\) 在一些数据结构中是重要的预处理。
时间复杂度:\(m\) 很大时不能使用。
4.3 基数排序
基数排序基于计数排序,计数排序的多关键字排序。(显然计数排序的方法无法重载小于号)
应用时当需要排的数按"字典序的逆序"分解成多个类,再每个类做计数排序。
排序会符合染色覆盖的思想,按"字典序"多关键排序。
如排序 \(n\) 个数,值域范围 \(m\) ,分解成 \(k = \log_{10} m\) 个类。
void RadixSort(){
for (int i = 1; i <= n; i++) sa[i] = i;
for (ll p = 1; p <= k; p *= 10) {
for (int i = 1; i <= n; i++) {
a[i] = arr[i] / p % 10;
}
CountingSort();
}
}
时间复杂度: \(O(n k)\) 。\(k\) 为分的组数,一般为按符集分解或按数位分解,通常是一个很小的常数。
五 枚举非空子集、超集
5.1 枚举子集
我们知道枚举 \(111 \cdots 111\) (\(n\) 个 \(1\))的子集方法为
for (int i = 1; i < (1 << n); i++)
这是显然的。
但若枚举 \(m\) 的非空子集则是
for (int i = m; i; i = (i - 1) & m)
比如 \(m = 101\) ,得到 \(i = 101\) 。
\(i - 1 = 100\) ,\(i := 100 \&\ 101 = 100\) 。
\(i - 1 = 011\) ,\(i := 011 \&\ 101 = 001\) 。
\(i - 1 = 0\) 。
正确性:在感性上理解为用 \(i\) 从大到小枚举 \(m\) 的子集,找到最大的 \(nxt\) 满足 \(nxt \leq i - 1\) 且 \(nxt \in m\) ,然后有 \(i = nxt\)。
这个复杂度不会超过 \(2^n\) 。(\(n\) 是集合大小)
更详细地,我们能够在某类计算贡献的题目中见到一类问题,求
于是有
int ans = 0;
for (int i = 1; i < (1 << n); i++) {
int cnt = 0;
for (int j = i; j; j = (j - 1) & i) {
cnt += j;
}
f[i] *= cnt;
ans += f[i]
}
这组贡献计算的时间代码复杂度是 \(O(3^n)\) 。
时间复杂度分析:对于任意一个数位只有三种可能,只出现在 \(i\) ,只出现在 \(j\) ,出现在 \(i\) 和 \(j\) 。于是集合大小为 \(n\) 时复杂度为 \(O(3^n)\) 。
5.2 枚举超级
枚举 \(m\) 的超集
for (int i = m; ; i = i + 1 | m) {
// if (i == 超集上限) break;
}
正确性:在感性理解上为用 \(i\) 枚举 \(m\) 的超集,找到最小的 \(nxt\) 满足 \(nxt \geq i + 1\) 且 \(m \in nxt\) 并让 \(i := nxt\) 。当找到超集上限时,计算完贡献后跳出程序。
求解一类计算贡献的代码
for (int i = 1; i < (1 << m); i++) {
for (int j = i; ; j = (j + 1) | m) {
if (h == (1 << m) - 1) break;
}
}
时间复杂度:\(O(3^n)\) ,\(n\) 是最大集合的大小。
六 对拍
6.1 竞赛最强证明法:反证
众所周知,在算法竞赛中我们通常能很快发现题目的一种 nativ 解。
- 通过一定的观察分析结合已知知识,找到一种基于 nativ 优化的优解法
- 通过样例手玩、打表猜测出 nativ 解法优化的方法
- 极小概率地,通过样例手玩、打表,找到另一种不基于 nativ 解法的更有解法
可以说很大程度上,解决问题都需要经过一定的猜测。
常见的,通过知识、经验、观察力、打表的结合,猜测出一种方案后,数学上我们通常需要进行数学反证。计算机领域还能加上 \(AC\) 反证或暴力伪证。
OI 赛制中,显然只有暴力反证和数学反证两种方法,我们可以指使计算机执行暴力反证然后继续求解其他题目,于是计算机领域通常会选择暴力反证。
6.2 对拍理念
对拍便是一种暴力反证。我们需要将标准代码 std.cpp 、暴力代码 force.cpp 和数据生成器代码 data.cpp 放到一个文件夹中分别编译并生成可执行文件,然后在这个文件夹中编译对拍代码 check.cpp ,并运行 check.exe 。
暴力代码即 nativ 代码,标准代码即待反证代码。现在关键的是如何编写 check.cpp 。
6.3 data.cpp 的编写
三份代码中 force.cpp 和 std.cpp 是必须写的,check.cpp 是固定的,于是我们每次只需要重写数据生成器即可。
不需要考虑大数定理,只需要纯离散分布即可。
#include <bits/stdc++.h>
#inlucde <random>
signed main() {
std::random_device rd;
std::mt19937 rnd(rd);
std::uniform_int_distribution<long long> limit_n(l1, r1), limit_m(l2, r2);
while (1) {
n = limit_b(rnd), m = limit_m(rnd);
std::cout << n << " " << m << std::endl;
}
return 0;
}
6.4 check.cpp 的编写
#include <bis/stdc++.h>
signed main() {
system("data.exe > data.txt"); // 运行 data.exe 输入结果到 data.txt
system("force.exe < data.txt > force.txt"); // force.exe 用 data.txt 运行并输入结果到 force.txt
system("std.exe < data.txt > std.txt"); // std.exe 用 data.txt 运行并输入结果到 std.txt
system("fc std.txt force.txt"); // 对 std.txt 和 force.txt 进行文件比较
return 0;
}
七 ST 表
八 拉链散列表(HashMap)
注意注意注意!注意理解负载因子,这不是无脑数据结构。
通常足以解决 \(n \leq 10^{5}\) 的问题,转化为 \(O(1)\) 。
九 整数快读、快写
只说明最朴素、有效、易于实现的版本。
禁用 iostream 并关闭 iostrem 优化。 快读快写调用 C 语言中的快速字符输出,与 C++ 的标准 ios 冲突。
整数快写:
竞赛中很少出现大量输出的情景,优势在于:
- 易懂、好写
- 可以输出 int128
传入一个 T 类型的整数 x 。
- if \(res < 0\) ,putchar('-') 并 x = -x;
- if \(x > 9\) ,write(x / 10) ,一直递归下去;
- 向上回溯时,putchar(x \bmod 10 + '0') 。
template<class T>
inline void write<T x> {
if (x < 0) putchar('-'), x = -x;
if (x > 9) write(x / 10);
putchar(x % 10 + '0');
}
整数快读:
整数快读是一个小模拟。
- getchar() 接收以 res 为前缀的输入流。也可能直接就是文件末尾,gerchar() 会返回“流结束符 EOF”。
- 最先要判断是否读入合法。即 if(c = getchar(), getchar() == EOF) return 0 。
- 如果读入合法,则看第一个流符号是否为负号,以定义 sgn 和 res 。
- 将前缀输入流读完,计算 |res|。并让 res *= sgn;
- 读入成功将返回 1 。
template<class T>
inline bool read(T &res) {
char c = getchar(); int sgn;
if (c == EOF) return 0;
if (c == '-') sgn = -1, res = 0;
else sgn = 1, res = c - '0';
while (c = getchar(), c >= '0' && c <= '0') res = res * 10 + (c - '0');
res *= sgn;
return 1;
}