一些不体系但常见的小型科技

一 整数分块

对于定式

\[\sum_{i = 1}^{n} \lfloor \frac{n}{i} \rfloor \]

可以在 \(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) 乘

\[ab = \lfloor \frac{ab}{m} \rfloor m + ab \bmod m \]

  1. \(unsigned\) 溢出切高位
    于是可以用 \(ull\) 表示出 \(ab\)\(\lfloor \frac{ab}{m} \rfloor m\) ,然后通过 \(ab - \lfloor \frac{ab}{m} \rfloor m\) 得到 \(ab \bmod m\)
  2. \(long\ double\) 的精度 \(64\) 位,\(long\ long\)\(64\)\(Byte\) ,于是 \(long\ long\) 的结果可以被 \(long\ double\) 表示。
  3. 浮点数可以损失精度表示超大数,于是可以选择 \(long\ double\) 表示表示不超过 \(ull\) 的运算结果 \(\frac{ab}{m}\) ,然后再取 \(ull\)
  4. 浮点数运算的中间精度丢失,会导致取整后有最多 \(\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\}\)

\[\begin{aligned} A^{n} &= A^{\sum_{i = 0}^{k} a_i \cdot 2^{i}}\ (0 \leq a_i < 2) \\ &= \prod_{i = 0}^{k} A^{a_i \cdot 2^{i}} \\ &= \prod_{i = 0}^{k} A^{2^{i} \varepsilon(a_i = 1)} \end{aligned} \]

  1. 又有

\[\begin{aligned} A^{2^{i + 1}} &= A^{\sum_{j = 0}^{2 - 1} 2^{i}} \\ &= A^{2^{i} + 2^{i}} \\ &= A^{2^{i}} \times A^{2^{i}} \end{aligned} \]

于是有

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\) 是集合大小)

更详细地,我们能够在某类计算贡献的题目中见到一类问题,求

\[\sum_{i = 0}^{2^{n - 1}} f(i) \sum_{j = 0}^{i} [j \&\ i = j] \]

于是有

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 解。

  1. 通过一定的观察分析结合已知知识,找到一种基于 nativ 优化的优解法
  2. 通过样例手玩、打表猜测出 nativ 解法优化的方法
  3. 极小概率地,通过样例手玩、打表,找到另一种不基于 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 冲突。

整数快写:

竞赛中很少出现大量输出的情景,优势在于:

  1. 易懂、好写
  2. 可以输出 int128

传入一个 T 类型的整数 x 。

  1. if \(res < 0\) ,putchar('-') 并 x = -x;
  2. if \(x > 9\) ,write(x / 10) ,一直递归下去;
  3. 向上回溯时,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');
}

整数快读:
整数快读是一个小模拟。

  1. getchar() 接收以 res 为前缀的输入流。也可能直接就是文件末尾,gerchar() 会返回“流结束符 EOF”。
  2. 最先要判断是否读入合法。即 if(c = getchar(), getchar() == EOF) return 0 。
  3. 如果读入合法,则看第一个流符号是否为负号,以定义 sgn 和 res 。
  4. 将前缀输入流读完,计算 |res|。并让 res *= sgn;
  5. 读入成功将返回 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;
}
posted @ 2024-03-02 17:55  zsxuan  阅读(4)  评论(0编辑  收藏  举报