各算法做法总汇

· · 个人记录

ST 表

ST 表可以处理可重复贡献问题。

先来看一个例子: RMQ (区间最值问题).

f(i,j) 是区间 [i,i + 2^j] 内最值,则:

f(i,j) = \max { \begin{cases} &f(i,j - 1)& \\ &f(i + 2^{j - 1}, j - 1)& \\ \end{cases} }

证明:

根据题意,f(i,j) 是区间 [i,i + 2^{j}] 内最值。

RMQ 问题为可重复贡献问题,也就是满足 x \operatorname{opr} x = x.

则可以把区间 [i,j] 分成两个区间:[i,i + 2^{j - 1}][i + 2^{j - 1},i + 2^{j}] ,并求二者分别的最值,然后求两者分别所得的最值的最值。

f(i,j) = \max\{f(i,j - 1),f(i + 2^{j - 1},j - 1\}

特殊的,当 j = 0 时,f(i,j) = a[i]

query(l, r) 是区间 [l,r] 内最值,则:

\text{query}(l,r) = \max\{f(l,log_2 (r - l + 1),f(r - 2^{r - l + 1} + 1,log_2 (r - l + 1))\}

这就能查询 \mathcal{O}(1) 啦。

线段树

待填坑。

树状数组

待填坑。

单调栈

跟单调队列功能一毛一样。

#include <bits/stdc++.h>
using namespace std;
int STACK[3000005];
int a[3000005];
int f[3000005];
int n;
int top;
int main()
{
    cin >> n;
    for(int i = 1;i <= n;i ++)
    {
        cin >> a[i];
    }
    for(int i = 1;i <= n;i ++)
    {
        if(a[i] <= a[STACK[top]] || top == 0)//杀不动或没人杀
        {
            top ++;
            STACK[top] = i;//入栈 
        } 
        else
        {
            while(a[i] > a[STACK[top]] && top != 0)//杀得动且有人杀
            {
                f[STACK[top]] = i; 
                top --;//出栈 
            } 
        } 
        top ++;
        STACK[top] = i;//入栈 
    }
    for(int i = 1;i <= n;i ++)
    {
        cout << f[i] << ' ';
    } 
    return 0;
}

LCS

f(i,j) 为序列 A 考虑到第 i 个人时,序列 B 考虑到第 j 个人时所得的最长公共子序列长度,则:

f(i,j) = \max\{f(i - 1,j),f(i,j - 1)\}

特殊的,当 A_i = B_i 时,则:

f(i,j) = \max\{f(i - 1,j),f(i,j - 1),f(i - 1,j - 1) + 1\}

逆元

注: p 一定为质数。

欧拉函数

\varphi(n)n 的互质数数目,则:

\varphi(n) = n * (1 - \frac{1}{p_1}) * (1 - \frac{1}{p_2}) * ... * (1 - \frac{1}{p_k})

费马小定理

a^{p-1} \equiv 1 \pmod p

FFT

也称狒狒贴,全名 Fast Fast TLE.

官方名称:快速傅里叶变换。是一个用于求多项式卷积的 \mathcal{O}(N \log_2 N) 算法。

该部分的 N, x 大多为 2^k (k \in N^+), 所以高位要记得补 0.

多项式

顾名思义就是一堆单项式的和。

多项式可以表示成两种形式:

系数表示法:

\sum_{i = 0}^N{a_i x^i}

点值表示法:

就是取 N + 1 个值代入算出来,表示成点。可以通过一个 N + 1 的点集确定一个 N 次多项式。

卷积

符号: *.

输出 等于 输入系统 的卷积,即:\text{Output} = \text{Input} * \text{System}.

多项式的卷积:函数 F(x)G(x) 的乘积。

复数相关

虚数单位 i = \sqrt{-1}. 即 i^2 = -1.

虚数:形如 ai 的数。

复数:形如 ai + b 的数。其中 a 被称为虚部, b 被称为实部。

复数的共轭:虚部相同,实部互为相反数的复数互为共轭。复数 x 的共轭表示为 \bar{x}.

复数运算:

加减乘都跟实数差不多。

除法就是分数线上下同乘上分母的共轭。

复数的表示:向量, ai + b 表示 (0, 0)(a, b) 的一个向量。

模长: \sqrt{a^2 + b^2}, 向量的长度。

其他奇怪的东西

单位圆

以原点为圆心, 1 为半径的圆。

单位根

\omega^n_n = 1, 则称 \omega_nn 次单位根。

nn 次单位根,在复平面上表示的话恰好能等分单位圆。

逆时针标号单位根,令 \omega_n^k 为第 kn 次单位根。

有三个性质:

  1. 消去引理: \omega_{dn}^{dk} = \omega_{n}^{k}.

显然。

  1. 折半引理:若 n 为偶数,则 \omega_n^{k + \frac{n}{2}} = -\omega_n^k.

需要欧拉公式:

e^{i\theta} = \cos{\theta} + i \cdot \sin{\theta}

对欧拉公式进行乱搞可知:

e^{\pi i} = -1,e^{2 \pi i} = 1

综合:

\begin{aligned} \omega_n^{k + \frac{n}{2}} &= (e^{\frac{2 \pi i}{n}})^{k + \frac{n}{2}}\\ &= (e^{\frac{2 \pi i}{n}})^k \cdot e^{\pi i}\\ &= -\omega_n^k \end{aligned}

几何证明:既然是绕了半圈,必然与原数互为相反数。

  1. 求和引理:若 n \nmid k (n, k \in N^+), 则有 \sum_{j = 0}^{n - 1}{(\omega_n^k)^j} = 0.

用等比数列求和即可得知。

狒狒贴有什么用

我们知道暴力乘的效率奇低,需要 \mathcal{O}(N^2).

但是我们又观察到若有 F(x) 上的点 (x, y_1), G(x) 上的点 (x, y_2), 则有 F * G 上的点 (x, y_1 \cdot y_2). 这很显然。

那么既然点值表示可以确定多项式,那么就可以将已知的点每个 y 都相乘得到新的点,再反推回去。

这说明了我们可以用 \mathcal{O}(N) 的时间求解给出了点值表示的多项式卷积。

可是代入不要时间吗?这样复杂度又成了 \mathcal{O}(N^2).

没事。不要灰心,狒狒贴能解决!

狒狒贴的原理

就两个字:分治。

第一步是 DFT, 即离散傅里叶变换。

就是由于单位根的折半引理的前提,所以我们考虑对每一项的系数分类讨论:

那么对于 F(x) = \sum_{i = 0}^{N - 1}{a_i x^i}, 令:

F_0(x) = a_0 + a_2 x^2 + \cdots + a_{n - 2} x^{\frac{n}{2} - 1}, F_1(x) = a_1 + a_3 x^2 + \cdots + a_{n - 1} x^{\frac{n}{2} - 1}

则显然:

F(x) = F_0(x^2) + x \cdot F_1(x^2)

代入单位根 \omega, 只用考虑 k < \frac{n}{2} 的,毕竟另一半就是重复的问题。

先代入 \omega_n^k, 根据消去引理得:

\begin{aligned} F(\omega_n^k) &= F_0(\omega_n^{2k}) + \omega_n^k \cdot F_1(\omega_n^{2k}) \\ &= F_0(\omega_{\frac{n}{2}}^k) + \omega_n^k \cdot F_1(\omega_\frac{n}{2}^k) \\ \end{aligned}

这样就可以将两个子问题的 n 砍去一半,这样复杂度就可能下降啦。

不过还要考虑另一边呢。代入 \omega_n^{k + \frac{n}{2}}, 得:

\begin{aligned} F(\omega_n^{k + \frac{n}{2}}) &= F_0(\omega_n^{2k + n}) + \omega_n^{k + \frac{n}{2}} \cdot F_1(\omega_n^{2k + n}) \\ &= F_0(\omega_n^{2k} \cdot \omega_n^n) + \omega_n^{k + \frac{n}{2}} \cdot F_1(\omega_n^{2k} \cdot \omega_n^n) \\ &= F_0(\omega_n^{2k}) + \omega_n^{k + \frac{n}{2}} \cdot F_1(\omega_n^{2k}) \\ &= F_0(\omega_n^{2k}) - \omega_n^k \cdot F_1(\omega_n^{2k}) \\ \end{aligned}

可以发现分治后只需求两个子问题,而且每一个子问题的规模缩小了一半。典型的 \mathcal{O}(N \log_2 N) 算法。

突然发现做完 DFT 还需要再变回去,那就有了 IDFT, 即离散傅里叶逆变换 这玩意简直跟 DFT 一毛一样。

IDFT 相当于将 DFT 的结果变成系数表示. DFT 可以被描述为:

\left [ \begin{matrix} y_0 \\ y_1 \\ y_2 \\ y_3 \\ \vdots \\ y_{n - 1} \\ \end{matrix} \right ] = \left [ \begin{matrix} & 1 & 1 & 1 & 1 & \ldots & 1 & \\ & 1 & \omega_n^1 & \omega_n^2 & \omega_n^3 & \ldots & \omega_n^{n - 1} & \\ & 1 & \omega_n^2 & \omega_n^4 & \omega_n^6 & \ldots & \omega_n^{2(n - 1)} & \\ & 1 & \omega_n^3 & \omega_n^6 & \omega_n^9 & \ldots & \omega_n^{3(n - 1)} & \\ & \vdots & \vdots & \vdots & \vdots & \ddots & \vdots & \\ & 1 & \omega_n^{n - 1} & \omega_n^{2(n - 1)} & \omega_n^{3(n - 1)} & \ldots & \omega_n^{(n - 1)^2} & \\ \end{matrix} \right ] \times \left [ \begin{matrix} a_0 \\ a_1 \\ a_2 \\ a_3 \\ \vdots \\ a_{n - 1} \\ \end{matrix} \right ]

已知最左边和中间的那个矩阵,要还原成最右边的矩阵,那么就考虑让 DFT 的结果乘上中间矩阵的逆矩阵。

然鹅这个逆矩阵又不好求,咋办?

先摆结论:逆矩阵就是每一项取倒数再除以 N. 为啥呢?

证明:

倒推嘛。设:

c_k = \sum_{i = 0}^{n - 1}{y_i \cdot (\omega_n^{-k})^i}

暴力展开计算:

\begin{aligned} c_k &= \sum_{i = 0}^{n - 1}{y_i \cdot (\omega_n^{-k})^i} \\ &= \sum_{i = 0}^{n - 1}{(\sum_{j = 0}^{n - 1} {a_j \cdot (\omega_n^i)^j}) \cdot (\omega_n^{-k})^i} \\ &= \sum_{i = 0}^{n - 1}{(\sum_{j = 0}^{n - 1} {a_j \cdot (\omega_n^j)^i}) \cdot (\omega_n^{-k})^i} \\ &= \sum_{i = 0}^{n - 1}{ \sum_{j = 0}^{n - 1} {a_j \cdot (\omega_n^j)}^i \cdot (\omega_n^{-k})^i} \\ &= \sum_{i = 0}^{n - 1}{ \sum_{j = 0}^{n - 1} {a_j \cdot (\omega_n^{j - k})}^i} \\ &= \sum_{j = 0}^{n - 1}{a_j \cdot (\sum_{i = 0}^{n - 1} {(\omega_n^{j - k})}^i}) \\ \end{aligned}

S(x) = \sum_{i = 0}^{n - 1}{x^i}.

\omega_n^k 代入得:

S(\omega_n^k) = 1 + \omega_n^k + (\omega_n^k)^2 + \cdots + (\omega_n^k)^{n - 1}

乘上个 \omega_n^k 可得:

\omega_n^k \cdot S(\omega_n^k) = \omega_n^k + (\omega_n^k)^2 + (\omega_n^k)^3 + \cdots + (\omega_n^k)^n

相减可得:

\begin{aligned} (\omega_n^k - 1) \cdot S(\omega_n^k) &= \omega_n^{n k} - 1 \\ S(\omega_n^k) &= \frac{(\omega_n^n)^k - 1}{\omega_n^k - 1} \end{aligned}

k \ne 0 时,该柿子有意义,因为 \omega_0^k = 1. 又易知 (\omega_n^n)^k = 1, 所以可知 S(\omega_n^k) = 0.

k = 0 时,显然 S(\omega_n^k) = n.

那就拿这个结论去考虑上面的柿子。发现当 j \ne k 时全都是 0, 除了一次 j = k 结果为 n. 所以直接可以进行大化简了:

c_k = n \cdot a_k

a_k = \frac{c_k}{n}. 这样我们就做完啦。

狒狒贴的强劲优化

你把你刚写好的 FFT 交上洛谷,期待着 AC 的时候,突然一个橙黄的 77 分映入眼帘!

唉果然递归效率太低了,开了一大堆空间。那么就考虑优化啦。

蝴蝶操作

待填坑...

图论

最短路

Dijkstra

一个单源最短路算法。

设源点为 s,从点 i 到点 j 的图上距离为 w_{i,j},源点到点 i 的距离 为 dis_i

易得:

dis_s = 0

算法步骤如下:

  1. 找到 dis_i 最小的点 i

  2. i 为中转点更新与它连接的点 jdis_j

  3. 重复第 1 步,直到每个点都被作为中转点过为止。

参考博文

理由:

设当前中转点为 kik 所可达的未被作为中转点过的点,则有:

dis_k \le dis_i

理由显然,k 取的一定是未被作为中转点过的点中,dis_k 最小的一个。

最小生成树

树的定义:对于无向图 T,若 T 中每一个点都可以互达,且 T 只有 n - 1 条边(nT 中的节点数目),那么 T 就是一棵树。

生成树的定义:对于无向图 G,若 G'G 的子图,而且是一棵树,则称 G'G 的一棵生成树。

其中,生成树的边权之和被称为生成树的大小。

最小生成树的定义:对于图 G,在它的生成树集中,大小最小的生成树 G'_{min} 即为 G 的最小生成树。

那么,根据定义可得,对于任意的 G'_k,都有:

G'_{min} \le G'_k

接下来,我们来了解一些算法。

Kruskal

一个使用了并查集的算法。

将图中的每一条边按权值排序,按顺序尝试加入,分两种情况讨论:

  1. 该边所联通的两个节点已经可以互达,选择不加入。

  2. 否则加入这条边。

代码:

#include <bits/stdc++.h>
using namespace std;

struct Edge {
    int start;
    int to;
    long long w;
}arr[200001];

int n,m;
long long MST;
long long s[50001];

bool cmp(Edge a,Edge b) {
    return a.w < b.w;
}

long long _find(int a) {

    if (s[a] == a) return a;
    else {
        return s[a] = _find(s[a]);
    }
}

void kruskal() {
    int add = 0;
    int u,v;
    for(int i = 1; i <= m; i ++) {
        u = _find(arr[i].start);
        v = _find(arr[i].to);
        if(u != v) {
            MST += arr[i].w;
            s[u] = v;
            add ++;
            if(add == m - 1) {
                break;
            }
        }
    }
}

int main() {
    cin >> n >> m;
    for(int i = 1;i <= n;i ++){
        s[i] = i;
    }
    for(int i = 1; i <= m; i ++) {
        cin >> arr[i].start >> arr[i].to >> arr[i].w;
    }
    sort(arr + 1,arr + m + 1,cmp);
    kruskal();
    cout << MST;
    return 0;
}

理由:

设当前的边为 e_i,权值为 w_i,那么升序排序后可以保证对于任意的 i,都有:

w_i \le w_{i+1}

那么,我们可以顺序取边。

e_i 连接 uv 两点,那么如果 uv 已经同属一个并查集,即 uv 两点被联通了,那么对于构造树是无意义的,并且使树的大小变大了,不符合我们对于最小生成树的定义,所以不能取 e_i 这条边。