FFT,IFT,DFT,IDFT

FFT 大数乘法

不论FFT还是FFT大数乘法,网上都有”大量”的资料

但很多都看得云里雾里,不太能真正的理解

有几篇快要讲清了,但实际靠的 举例 来描述其可行性,

本篇希望从个人理解以及偏数学的角度来解释

FFT 大数乘法要解决的问题

小学生都知道,乘法就是列竖式,然后进行一个数字一个数字的乘法再相加

那么对于一个长度a长度b的两个数字相乘,进行了$O(ab)$次的运算,为了描述简便,假设 两个数字的长度都为$n=max(a,b)$,不足的补零,也就是$O(n^2)$的时间复杂度

那么如果题目是 长$50000$乘长$50000$的运算,那么用原始的乘法是无法在1s内解出的,

FFT可以做到$O(n \cdot log(n))$时间复杂度

计算逻辑总览

要计算 $a \cdot b$

且存在 函数$f:x \to X$,能使

  1. $f:x \to X$ 的时间复杂度 $\le O(n \cdot log (n))$
  2. $f^{-1}:X \to x$ 的时间复杂度 $\le O(n \cdot log (n))$, 注:是逆运算不是$-1$次方
  3. $f^{-1}(g(f(a),f(b))) = a\cdot b$, 其中$g$的时间复杂度 $\le O (n \cdot log(n))$

用文字描述,大自为

如果$a,b$可以通过$f$函数变为$A,B$
且$A,B$通过$g$的运算变为$C$
$C$再通过$f$的逆函数变为$c$
且$c == a \cdot b$的
那么原本$O(n^2)$的乘法,就可以用这样的方法替代,变为$O(n \cdot log(n))$

观察上面的式子,可以发现,如果有以下两个映射也可以实现

  1. $f:x \to X$ 的时间复杂度 $\le O(n \cdot log (n))$
  2. $g(f(a),f(b)) = a\cdot b$, 其中$g$的时间复杂度 $\le O(n \cdot log (n))$

之所以写成3条是因为我们具体用得方法, 和3条对应

所以简单的理解,为如果把$a,b$进行低时间复杂度的变形,再进行某种低时间复杂度的运算能变为$a\cdot b$,那么即可在低时间复杂度内算出$a\cdot b$

先抛开时间复杂度谈方法

$ f * g= \mathcal{F}^{-1} \{ \mathcal{F} \{ f \} \cdot \mathcal{F} \{ g \} \}$

以上就是整个FFT大数乘法的所有数学公式,实际问题是离散傅立叶变换

分别解释符号

  1. $*$ 卷积 不是乘号
  2. $\mathcal{F}$傅里叶变换
  3. $\cdot$ 乘
  4. $f,g$ 这里看作$n$元向量

卷积(连续的)

定义

${\displaystyle (f * g)(t){\stackrel {\mathrm {def} }{=}}\ \int_{-\infty }^{\infty }f(\tau )g(t-\tau ),d\tau }$

简单讲,卷积的结果$(f * g)(t)$ 等于 $f(r)\cdot g(t-r)$的积分

这里就和乘法$a \cdot b = c$ 对应上了,在乘法过程中,不考虑进位, 每一位的取值都是所有整数

结果的第$t$位的原始值 = 所有$a$的$i$位 乘上 $b$的$t-i$位的和

$c_t = \sum_{i=0}^t a_i\cdot b_{t-i}$

傅里叶变换(连续的)

也就是上面的$\mathcal{F}$

${\displaystyle {\hat {f}}(\xi )=\int_{-\infty }^{\infty }f(x)\ e^{-2\pi ix\xi },dx,}$ (定义)

逆变换

${\displaystyle f(x)=\int_{-\infty }^{\infty }{\hat {f}}(\xi )\ e^{2\pi ix\xi },d\xi ,}$ (由正向定义推导)

证明 卷积的等式

要证明 $f * g= \mathcal{F}^{-1} \{ \mathcal{F} \{ f \} \cdot \mathcal{F} \{ g \} \}$

抄写自wiki

$\mathcal{F}\{f * g\}(\nu ) $

$= \int_{\mathbb{R}^n} \{f * g\} (z) , e^{-2 \pi i z\cdot \nu}, dz$ (傅立叶定义)

$= \int_{\mathbb{R}^n} \int_{\mathbb{R}^n} f(x) g(z-x), dx, e^{-2 \pi i z\cdot \nu}, dz.$ (卷积定义)

$= \int_{\mathbb{R}^n} f(x)\left(\int_{\mathbb{R}^n} g(z-x)e^{-2 \pi i z\cdot \nu},dz\right),dx.$(积分顺序)

$= \int_{\mathbb{R}^n} f(x) \left( \int_{\mathbb{R}^n} g(y) e^{-2 \pi i (y+x)\cdot\nu},dy \right) ,dx$,(换元$z=x+y$)

$= \int_{\mathbb{R}^n} f(x)e^{-2\pi i x\cdot \nu} \left( \int_{\mathbb{R}^n} g(y) e^{-2 \pi i y\cdot\nu},dy \right) ,dx$ (提取与$y$无关乘法因式)

$= \int_{\mathbb{R}^n} f(x)e^{-2\pi i x\cdot \nu},dx \int_{\mathbb{R}^n} g(y) e^{-2 \pi i y\cdot\nu},dy.$

$= {\mathcal {F}}\{f\}(\nu ) \cdot {\mathcal {F}}\{f\}(\nu)$

得证

小总结

至此,我们有了

  1. 乘法 中按位 表示 和卷积对应
  2. 上面要找的f,和傅里叶变换对应,$f^{-1}$和傅里叶逆变换对应
  3. 有 $ f * g $ 等式

所以

乘法按位表示 $\to$ 卷积 $\to$ ((函数的傅里叶变换)的 积)的傅里叶逆变换

当然这里证明的是连续的傅立叶和卷积,而我们下面用的是离散傅立叶(讲道理还是要单独证明的,这里并没有证明

接下来谈一谈,如何实现,并且能在时间复杂度内

先处理傅里叶变换的部分,首先离散傅里叶变换DFT的定义为 $x \to X$

$X_{k}=\sum _ {n=0}^{N-1}x _ {n}e^{-\frac{2 \pi i}{N}kn}\qquad k=0,\dots ,N-1.$

IDFT

$X_{k}=\frac{1}{N} \sum _ {n=0}^{N-1}x _ {n}e^{\frac{2 \pi i}{N}kn}\qquad k=0,\dots ,N-1.$

注: 有的地方系数不是$1$和$\frac{1}{N}$,是两个 $\frac{1}{\sqrt{N}}$, 本质上没有区别,也不影响后面的推导的核心内容

其中$i,\pi,e$为意义,单位虚数,元周率,自然对数, 这里主要是复平面的单位向量的知识点

$N$取$2^{\lceil log_2n \rceil}$ 也就是长度取到2的幂次, 不足的补零

把这个式子用矩阵表示$X = Wx$,发现其实是一个”系数矩阵$W$”

${\displaystyle W=\left(\omega^{jk}\right)_ {j,k=0,\ldots ,N-1}}$

${\displaystyle W={\begin{bmatrix}
1&1&1&1&\cdots &1\
1&\omega &\omega ^{2}&\omega ^{3}&\cdots &\omega ^{N-1}\
1&\omega ^{2}&\omega ^{4}&\omega ^{6}&\cdots &\omega ^{2(N-1)}\
1&\omega ^{3}&\omega ^{6}&\omega ^{9}&\cdots &\omega ^{3(N-1)}\
\vdots &\vdots &\vdots &\vdots &\ddots &\vdots \
1&\omega ^{N-1}&\omega ^{2(N-1)}&\omega ^{3(N-1)}&\cdots &\omega ^{(N-1)(N-1)}
\end{bmatrix}},{\displaystyle \omega =e^{-\frac{2\pi i}{N}}}}$

目标是求向量$X$,虽然求这个矩阵需要$O(n^2)$的时间,但可以不求矩阵,直接求向量$X$

希望能$O(n\cdot log(n))$求出列向量$X$

行列按照$0$-index作为下标, 那么有

  • 矩阵的 $上一列_ i \cdot \omega^i = 下一列_ i$

  • 在偶数列向量中,列上$i$和$i+\frac{n}{2}$互为相反数, 因为$\omega^{\frac{N}{2}} = -1$ (或见youtube

因此有递推式

$f(x,N,idx)$ = 初始向量为$x$, 大小为$N$(即要乘上$N$阶矩阵), 结果的第idx 个的值

对于 $idx < \frac{N}{2}$

$odd(x)$ 表示 $x$的奇数位置上的值构成的长度$\frac{N}{2}$的向量

$even(x)$ 表示 $x$的偶数位置上的值构成的长度$\frac{N}{2}$的向量

$f(x,N,idx) = f(even(x),\frac{N}{2},idx) + w^k \cdot f(odd(x),\frac{N}{2},idx)$

$f(x,N,idx+\frac{N}{2}) = f(even(x),\frac{N}{2},idx) - w^k \cdot f(odd(x),\frac{N}{2},idx)$

函数是折半的,复杂度显然

至此傅里叶变换可以在$O(n\cdot log(n))$时间复杂度内实现


为了帮助理解上面这个递归式, 这里举个例子, 比如$N=8, idx = 2$ 求$f(x,8,2)$

$X_2 = 1\cdot x_0 + w^2\cdot x_1 + w^4 \cdot x_2 + w^6 \cdot x_3 + w^8 \cdot x_4 + w^{10}\cdot x_5 + w^{12} \cdot x_6 + w^{14} \cdot x_7 $

$X_{2+\frac{8}{2}} = X_{6} = 1\cdot x_0 + w^6\cdot x_1 + w^{12} \cdot x_2 + w^{18} \cdot x_3 + w^{24} \cdot x_4 + w^{30}\cdot x_5 + w^{36} \cdot x_6 + w^{42} \cdot x_7 $

注意到在$N=8$时,$w^4 = -1$, 所以

$X_{2+4} = 1\cdot x_0 - w^2\cdot x_1 + w^4 \cdot x_2 - w^6 \cdot x_3 + w^8 \cdot x_4 - w^{10}\cdot x_5 + w^{12} \cdot x_6 - w^{14} \cdot x_7 $

这不就是

$X_2 = (w^0 \cdot x_0 + w^4 \cdot x_2 + w^8 \cdot x_4 + w^{12} \cdot x_6) + w^2 (w^0 \cdot x_1 + w^4 \cdot x_3 + w^8\cdot x_5 + w^{12} \cdot x_7 ) $

$X_{2+4} = (w^0 \cdot x_0 + w^4 \cdot x_2 + w^8 \cdot x_4 + w^{12} \cdot x_6) - w^2 (w^0 \cdot x_1 + w^4 \cdot x_3 + w^8\cdot x_5 + w^{12} \cdot x_7 ) $

注意到$w_{2N}^{2k} = w_{N}^k$

而其中的 $w^0 \cdot x_0 + w^4 \cdot x_2 + w^8 \cdot x_4 + w^{12} \cdot x_6$ 正是$x$的偶数项 在 $N=4$ 时的$X_2$, 即$f(even(x), 4, 2)$

$w^0 \cdot x_1 + w^4 \cdot x_3 + w^8\cdot x_5 + w^{12} \cdot x_7 $ 正是$x$的奇数项 在 $N=4$ 时的$X_2$,即$f(odd(x), 4, 2)$


那么$g$也就是其中的 按位乘 也就是简单的$O(n)$

最后 傅里叶逆变换, 可以注意到上面的公式只在 系数上多了一个负号,所以同理可以在$O(n \cdot log (n))$时间复杂度内实现

递归+原地

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#include <bits/stdc++.h>
using namespace std;

const double pi = acos(-1.0);

void FFT(complex<double> * A, int n /* size */, int flag /* 1:FFT,-1:IFFT*/) {
if (n == 1) return;
// assert((n & (n-1)) == 0); 一定要是2的幂次
// e^(i*x) = cos(x)+i*sin(x)
complex<double> Wm(cos(2 * pi / n), -sin(2 * pi / n) * flag);
vector<complex<double> > tmp(n);
int u = 0;
for (int k = 1; k < n; k += 2, u++) tmp[u] = A[k]; // 奇数列
for (int k = 0; k < n; k += 2) A[k / 2] = A[k]; // 偶数列
for (int k = u, i = 0; k < n && i < u; k++, i++) A[k] = tmp[i];
FFT(A , n / 2, flag);
FFT(A + n / 2, n / 2, flag);
complex<double> W(1, 0); // 运算中是 Wm 的i 次方
for (int i = 0; i < n / 2 ; i++){
int j = n / 2 + i;
auto U = A[i];
auto T = W * A[j];
A[i] = U + T;
A[j] = U - T;
W *= Wm;
}
}

int main(){
// 123*456 = 56088
const int SIZE = 8; // 一定要是2的幂次
complex<double> * a = new complex<double>[SIZE]{3,2,1,0,0,0,0,0};
complex<double> * b = new complex<double>[SIZE]{6,5,4,0,0,0,0,0};
FFT(a, SIZE, 1);
FFT(b, SIZE, 1);
complex<double> * c = new complex<double>[SIZE]{0,0,0,0,0,0,0,0};
for(int i = 0;i < SIZE;i++) c[i] = a[i] * b[i];
FFT(c, SIZE, -1);
for(int i = 0;i < SIZE;i++) c[i] /= SIZE;
// print
for(int i = 0;i < SIZE;i++) printf("(%.3lf,%.3lf)", c[i].real(), c[i].imag());
printf("\n");
for(int i = 0;i < SIZE-1;i++) { // 进位
c[i+1] += int(c[i].real() / 10);
c[i] -= int(c[i].real() / 10) * 10;
}
for(int i = 0;i < SIZE;i++) printf("(%d)", int(c[i].real()));
printf("\n");
return 0;
}

递推+原地

注意,以下代码其中 for m 的部分应该是从2 开始直到n,仅仅是看上去简便,这里处理的时候 把所有对应位置都乘上了2,所以对应cos,sin等等与m相关的都变了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
#include <bits/stdc++.h>
using namespace std;

#define rep(i,a,n) for (int i=a;i < n;i++)
const double pi = acos(-1.0);

// bit 翻转
int rev(int x, int len) {
int ans = 0;
while(len -- ){
ans <<= 1;
ans |= x & 1;
x >>= 1;
}
return ans;
}

int getlog2(int n){
return 31 - __builtin_clz(n);
}

void FFT(vector<complex<double> > &A, int flag /* 1:FFT,-1:IFFT */) {
int n = A.size();
if(n == 1) return ;
assert((n & (n-1)) == 0); // 2 的幂次
int lgn = getlog2(n);
// 相当于 上面 多次递归每个位置放到的最终位置, 直接计算位置, 而神奇的是刚好 位置 = 原位置的按照长度lgn的bit翻转
rep(i, 0, n) {
int j = rev(i, lgn);
if (j > i) swap(A[i], A[j]);
}
// 逻辑和递归里一样了, 区别是 这里不像递归里下标连续, 需要计算下标, 好在还是顺序的
rep(pwr, 0, lgn){
int m = 1 << pwr;
complex<double> Wm(cos(pi / m), -sin(pi / m) * flag);
for (int k = 0; k < n; k += (m<<1)) {
complex<double> W(1, 0);
rep(j, 0, m) {
auto U = A[k + j];
auto T = W * A[k + j + m];
A[k + j] = U + T;
A[k + j + m] = U - T;
W *= Wm;
}
}
}
}

int main(){
// 123*456 = 56088
const int SIZE = 8; // 一定要是2的幂次
auto a = vector<complex<double> >{3,2,1,0,0,0,0,0};
auto b = vector<complex<double> >{6,5,4,0,0,0,0,0};
FFT(a, 1);
FFT(b, 1);
auto c = vector<complex<double> >(8,0);
for(int i = 0;i < SIZE;i++) c[i] = a[i] * b[i];
FFT(c,-1);
for(int i = 0;i < SIZE;i++) c[i] /= SIZE;
// print
for(int i = 0;i < SIZE;i++) printf("(%.3lf,%.3lf)", c[i].real(), c[i].imag());
printf("\n");
for(int i = 0;i < SIZE-1;i++) { // 进位
c[i+1] += int(c[i].real() / 10);
c[i] -= int(c[i].real() / 10) * 10;
}
for(int i = 0;i < SIZE;i++) printf("(%d)", int(c[i].real()));
printf("\n");
return 0;
}

实例代码

CF R488 Div1 E ACCEPTED

相关延伸

有没有觉得double这种东西用起来就感觉很危险,那么也的确有叫做NTT https://en.wikipedia.org/wiki/Discrete_Fourier_transform_(general)#Number-theoretic_transform的方法来 实现大数乘法,但没有精度的问题需要担心.

另外建议 PI取 const double pi = acos(-1.0); 而不要手动输入

比如下面的R488 的第43个点 就因为我前面采用手动输入一直过不了

练习题目

CF EDU57 G

CF R488 Div1 E

参考

Convolution

Convolution theorem

Multiplication

Fourier transform

DFT

FFT

DFT matrix

YouTube - The Fast Fourier Transform Algorithm

模逆元/乘法逆元

众所周知,如同线段树/FFT一样,乘法逆元是CF的入门必会算法,然而我只能凭空写线段树

每次遇到需要用乘法逆元,我都是快速搜一个函数代码,复制进来用

之前也反复学懂了三四遍,始终没记住(毕竟用的频率不算高)

以下大自整理总结一下

什么是乘法逆元

已知a和p求b, 其中gcd(a,p)=1

$a^{ -1 } \equiv b{\pmod {n}}$

求法

费马小定理

当p是质数时

$a^{p-2} \mod p $

扩展欧几里得

既解$\gcd=1$的扩展欧几里得方程, 已知$(a,p)$求一个$(b,k)$

$a\cdot b+p\cdot k = 1$, 其中$k$为某未知整数

把正常计算$\gcd$的过程的细节 写出来int gcd(int a,int b){return b==0?a:gcd(b,a%b);}

$a \cdot b+p\cdot k = 1$,

初始为$(a,p)$

把$(d_i,d_{i+1})$ 变为 $(d_{i+1},d_{i+2})$

$d_1 = a + p\cdot c_1$ 分解 式子1 ($c_1 = - a / p, d_1 = a \mod p$)

$d_2 = p + d_1\cdot c_2$ 分解 式子2($c_2 = - p / d_1, d_2 = p \mod d_1$)

$d_3 = d_1 + d_2\cdot c_3$ 分解 式子3

直到$d_i == 0$, 也就是$d_{i+1} = d_{i-1} + d_2 \cdot c_{i+1}$

$gcd(a,p) = gcd(a,p) \cdot 1 + 0 cdot 0$

在算法实现上也就是再分解, 然后反过来把式子i带入式子i+1

$d_n = d_{n-2} + d_{n-1} \cdot c_n$

$d_n = d_{n-2} + (d_{n-3}+d_{n-2} \cdot c_{n-1}) \cdot c_n$

然后我们只需要记录的是 $d_{n-2}, d_{n-1}$的系数 $gcd(a,p) = d_{n-2} \cdot k_0 + d_{n-1} \cdot k_1$

$gcd(a,p) = d_{n-2} \cdot k_0 + (d_{n-3} \cdot 1 + d_{n-2} \cdot c_{n-1}) \cdot k_1$ , 这个 $c_i = - a_i/b_i$ 在回溯过程中 可以得到

$gcd(a,p) = d_{n-3} \cdot k_1 + d_{n-2} \cdot (k_0 - k_1 \cdot a_i / b_i )$

如此展开到最初的a和p即可

1
2
3
4
5
6
7
// 返回的 [x,y,z] 满足 a*x+b*y=z
tuple<int,int,int> exgcd(int a,int b){
if(b==0)
return {1,0,a};
auto [x,y,z] = exgcd(b,a%b);
return {y,x-(a/b)*y,z};
}

以上代码需要C++17

要注意的是如果 原来的数据有负数,零,需要在外部处理

扩展欧几里得 的矩阵理解

${\begin{pmatrix} a & p \\ 1 & 0 \\ 0 & 1 \end{pmatrix} } {\begin{pmatrix}x \\ y\end{pmatrix}} \Rightarrow {\begin{pmatrix}{a \cdot x + b \cdot y = gcd(x,y) } \\ x \\ y \end{pmatrix}}$

所以可以建立 2*3矩阵,然后对它做列变换,直到 得到gcd(a,p), 这时的 列向量上的 第二第三个值就是(x,y)

批量求 $< p $ 的 模逆元

方法1

妇孺皆知,质数的欧拉筛法,又 模逆元 有 可乘性,

所以 如果 a=p1*p2,那么 a的模逆元 = p1的模逆元*p2的模逆元 mod p

只需要 O(p+ exgcd平均时间 * ~n/ln n )

方法2

线性递推

$x = p/a$ (整除)

$y = p \mod a < a$

有 $a \cdot x + y = p$

所以 $y = - a \cdot x (\bmod p)$

两边同时乘 逆元 $inv_a \cdot inv_y$

$inv_a \cdot inv_y \cdot y = - a \cdot inv_a \cdot inv_y \cdot x (\bmod p)$

即 $inv_a = - x \cdot inv_y = - (p/a) \cdot inv_{p \bmod a} (\bmod p)$

注意到$ y = p \bmod a < a$,所以 在计算$inv_a$时,$inv_y$ 已经计算好了

又注意处理负号,在mod p的意义下

$- (p/a) \cdot inv_{p \bmod a} = (p - (p/a)) \cdot inv_{p \bmod a} (\bmod p)$

综上

1
2
3
inv[1] = 1;
rep(i,2,p)
inv[i] = ((p-p/i)*inv[p%i])%p;

例题

CF EDU57 F

参考

wikipedia

baidu百科

USACO 5.1.1

简述

给你n(0<=n<=10000)个平面上点,求凸包的周长

坐标范围(-1'000'000<=x,y<=1'000'000)

解法

官网介绍的方法

关于此题

实现和介绍方法的差异

首先USACO介绍的方法选取的一个中间的点,然后对周围的点排序,

关于这个,我这里实现是选取y最小(同理可以选取任何一个斜率上的边界点(因为它必定属于凸包(或位于凸包边上 共线(也是属于啦))))

这就会导致所用的排序不太相同[我这样也可以用角度直接比]

因为是某斜率边界点,那么 剩余的其它点必定在0至180度内,可以直接用叉乘比较,因此又需要注意,0和180度的比较上为了避免对称点的 不定偏序关系,需要选取 所选斜率上边界点上的边界点

有点拗口,具体举例,如果我选择y最小的点,假设有3个

A(-4,0),B(0,0),C(4,0),如果我选取B(0,0),作为比较的点的话,那么根据叉乘来排序,A(-4,0)C(4,0)之间的左右关系是可颠倒的BA×BC=0=BC×BA

所以这种情况,建议选择y最小时,x最小或x最大的.[不过幸运的是USACO数据 向来很弱,没有加处理仅仅取边界点也能AC]

而对于USACO介绍这种方法选中心点的时候,注意不应该用叉乘来作为比较函数,因为可能存在石头剪刀布的情况,(上面的0和180度也会发生)

举例有4个点A(-1,0),B(1,1),C(0,-1),O(0,0),当我们采取USACO介绍的方法进行排序时,假设选取O为中心的比较点,那么会有 剩下3个点 都在某另一个点左侧,也就没有偏序关系,无法排序,这种情况该用,计算角度进行排序


在选比较的点/中心点时,USACO还需要解决,绕了一圈以后,处理最初的点是否属于凸包的问题,而选取某一斜率边界点的话,不会有这个问题,因为该点一定在凸包边界上

实现

选取某斜率下的边界点(简单起见就y最小)

以该点为基础,排序(下面代码用逆时针排序),如果有多点共线,选取距离近的

for排序后的数组

  • 逐个尝试push加入点

  • 如果当前点已经有>1个点,且栈顶的两个点A=p[i-1],B=p[i],与新点Q,满足AB 不在 AQ右侧,(依赖 前面的排序和 距离从近到远),那么去掉栈顶也就是B,重新尝试push该Q

最后求周长就没啥说的了..


一点点正确性怀疑[都是被cf逼的],因为USACO要求的是 精度到小数点后2位,所以我有点想知道,这么多浮点数运算 损失了多少23333

代码

(没有处理上面说的0和180可能存在的问题,没有HACK的感觉真轻松,向CF日常200+测试数据低头)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
/*
ID: 10157591
PROG: fc
LANG: C++
*/
#include <bits/stdc++.h>
#define rep(i,a,n) for (int i=a;i<n;i++)
#define per(i,a,n) for (int i=n-1;i>=a;i--)

using namespace std;

const string filename = "fc";

void usefile(){
freopen((filename+".in").c_str(),"r",stdin);
freopen((filename+".out").c_str(),"w",stdout);
}

int n;

pair<double,double> base = {0,0};
pair<double,double>p[10010];

// 右手定则, v1在v2右侧 为正, v1在v2左侧为负
double cross(const pair<double,double>& v1,const pair<double,double>& v2){
return v1.first * v2.second - v1.second * v2.first;
}

pair<double,double> operator - (const pair<double,double>& p1,const pair<double,double>& p2){
return {p1.first-p2.first,p1.second-p2.second};
}

double abs(const pair<double,double> & p){
return sqrt(p.first*p.first + p.second*p.second);
}

// 逆时针 ,共线从近到远
bool cmp(const pair<double,double>& p1,const pair<double,double>& p2){
double c = cross(p1-base,p2-base);
if(c != 0) return c>0;
return abs(p1-base) < abs(p2-base);
}

pair<double,double> ans[10010];

int main(){
usefile();
cin>>n;
if(n < 2){
cout<<0<<endl;
return 0;
}
rep(i,0,n){
scanf("%lf %lf",&p[i].first,&p[i].second);
}
base = p[0];
rep(i,0,n){
if(base.second > p[i].second){
base = p[i];
}
}
sort(p,p+n,cmp);
ans[0] = p[0];
int cnt = 1;
rep(i,1,n){
while(cnt>1 && cross(ans[cnt-1]-ans[cnt-2],p[i]-ans[cnt-1]) <= 0){ // 新加的点应该在最后一条射线左侧
cnt--;
}
ans[cnt++] = p[i];
}
double output = 0;
rep(i,0,cnt){
output+=abs(ans[(i+1)%cnt]-ans[(i)%cnt]);
}
printf("%.2lf\n",output);

return 0;
}

CF #526 Div1 C Max Mex

大意

给一个n个点的树,树上的点上带有值,这些值为0到n-1的排列

Q个询问:

询问1.(参数点的坐标) 交换两个点上的值

询问2.(无参数) 在树上找一条简单路径,使该简单路径上MEX最大,输出该最大值

(2<=n<=200'000)

(1<=q<=200'000)

MEX: 返回集合中最小未出现的自然数 如MEX({0,1,2,4,8,16}) = 3

解法

线段树维护[v1 -> vn] 这一段是否可能 在一条简单路径上,如果可能,那么记录这条简单路径的端点坐标

保存和更新,利用线段树更新的log级别,和LCA 来进行树上线段的合并


关于合并 举例解释:

如果我们的线段树要合并[l1->r1][l2->r2] , 其中l2=r1+1

通过递归处理先得到

[l1->r1]这一段值,对应在树上的简单路径的端点坐标为vl1vr1

[l2->r2]这一段值,对应在树上的简单路径的端点坐标为vl2vr2

注意到 前序遍历 和 后续遍历的特征,如果 点A是B的祖先,那么前序遍历A<B, 且后序遍历A>B

那么有如果这4个端点,在从根到某个叶子的一条链上(当且仅当),则这四个端点的 前序遍历的偏序关系 正好 和后续遍历相反.

所以 我们可以通过对这4个点排序, 前序最小值对应的点 是否和 后续最大值对应的点 相同.如果相同,那么这4个点在一个链上返回[找到的最深点,最潜点] O(1)

以上是 存在从根下来的链经过4个端点.

下面考虑是否存在一个跨某节点连接 该节点两个子链的简单路径,经过4个端点

(假设dfs的子节点枚举从左向右,便于描述)

注意到,如果有祖先关系,那么前序的最大值为深度最深的点,如果是非祖先关系,那么 前序的最大值为最右侧的点,也就有了 前序最大是最右侧最深的点(假设叫RV)

对称的,后序遍历最小为最左侧最深的一个点.(假设叫LV)

那么 实际要考察的就是四个端点是否都在从LV到RV的这样一条简单路径上,

那么也就是LCA+判断点是否在链上,走一波O(log 树的深度)

如果失败,则用记录不可合并[any way -1也好 额外字段也好]


线段树更新,就日常更新就好了

查询,0是必定可以进链的

那么 尝试把0 和 (0-n/2) 左半合并(因为链合并有幂等性 不用考虑0和0自己冲突)

如果可以合并,就把合并后的和右半合并

如果不行,则 合并目标 = 合并目标的左半; O(log n)

相关知识

线段树(建立&更新&查询),老生常谈了[CF 入门必会],[相对于后缀数组,相对更难写,但能维护更多的状态]

合并树上的链(LCA,dfs 前序 后序遍历 特点,求一个点是否在一个树中的一条链上(类似LCA))

LCA, 求树上点的最近公共祖先:(基于fa[点i][幂次k] = 点i的2^k祖先),实现O(log(树高的))的时间复杂度

代码

code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
#include <bits/stdc++.h>

using namespace std;

typedef long long ll;
#define ten5 100000+10
#define MOD 1000000007
#define rep(i, a, n) for (int i=a;i<n;i++)
#define iif(c, t, f) ((c)?(t):(f))
#define per(i, a, n) for (int i=n-1;i>=a;i--)
#define pb push_back
#define mp make_pair
#define fi first
#define se second

const int N = 200010;
int n;
struct vertex {
int dep;
int v;
int _in;
int _out;
} i2v[N];
int v2i[N];

const int Log = 22;
int fa[N][Log + 1]; // fa[0][any] = 0 的性质很好
vector<int> child[N];

pair<int, int> T[N << 2];

void dfs(int i, int dep, int &idx) {
i2v[i].dep = dep;
i2v[i]._in = idx++;
for (auto item:child[i])
dfs(item, dep + 1, idx);
i2v[i]._out = idx++;
}

int Jump(int x, int d) {
if (d < 0)
return x;
per(i, 0, Log + 1){
if ((d >> i) & 1)
x = fa[x][i];
}
return x;
}

int LCA(int l, int r) {
int d = i2v[l].dep - i2v[r].dep;
if (d < 0) {
swap(l, r);
d = -d;
}
l = Jump(l, d);
if (l == r)
return l;
per(i, 0, Log + 1){
if (fa[l][i] != fa[r][i]) {
l = fa[l][i];
r = fa[r][i];
}
}
return fa[l][0];
}


bool onpath(int idx, int l_idx, int r_idx) {
int anc = LCA(l_idx, r_idx);
if (i2v[anc].dep > i2v[idx].dep)
return false;
return
Jump(l_idx, i2v[l_idx].dep - i2v[idx].dep) == idx ||
Jump(r_idx, i2v[r_idx].dep - i2v[idx].dep) == idx;
}

pair<int, int> mergeT(const pair<int, int> &v1, const pair<int, int> &v2) {
int arr[] = {v1.fi, v1.se, v2.fi, v2.se};
rep(i, 0, 4){
if (arr[i] == -1)
return {-1, -1};
}
int l_leaf = arr[0];
rep(i, 0, 4) {
if (i2v[arr[i]]._out < i2v[l_leaf]._out)
l_leaf = arr[i];
}

int r_leaf = arr[0];
rep(i, 0, 4) {
if (i2v[arr[i]]._in > i2v[r_leaf]._in)
r_leaf = arr[i];
}

if (l_leaf == r_leaf) { // 所有点在从根向下的一条链上
int rt = arr[0];
rep(i, 0, 4) {
if (i2v[arr[i]].dep < i2v[rt].dep)
rt = arr[i];
}
return {l_leaf, rt};
} else { // 链跨过了某节点连接某节点的两个子链
rep(i, 0, 4) {
if (!onpath(arr[i], l_leaf, r_leaf))
return {-1,-1};
}
}
return {l_leaf, r_leaf};
}

pair<int, int> initSeg(int o, int l, int r) {
if (l == r)
return T[o] = {v2i[l], v2i[r]};
int mid = (l + r) / 2;
return T[o] = mergeT(
initSeg(o << 1, l, mid),
initSeg(o << 1 | 1, mid + 1, r));
}

void updateSeg(int o, int l, int r, const int &val) {
if (l == r) {
T[o] = {v2i[val], v2i[val]};
return;
}
int mid = (l + r) / 2;
if (val <= mid)
updateSeg(o << 1, l, mid, val);
else
updateSeg(o << 1 | 1, mid + 1, r, val);
T[o] = mergeT(T[o << 1], T[o << 1 | 1]);
}

int query(int o, int l, int r, const pair<int, int> &now) {
if (l == r)
return mergeT(now, T[o]).fi == -1 ? l : l + 1;
int mid = (l + r) / 2;
auto tmp = mergeT(now, T[o << 1]);
if (tmp.fi == -1)
return query(o << 1, l, mid, now);
else
return query(o << 1 | 1, mid + 1, r, tmp);
}

int main() {
cin >> n;
rep(i, 1, n + 1) {
scanf("%d", &i2v[i].v);
v2i[i2v[i].v] = i;
}
rep(i, 2, n + 1) {
scanf("%d", &fa[i][0]);
child[fa[i][0]].pb(i);
}
rep(j, 1, Log + 1) {
rep(i, 1, n + 1) {
fa[i][j] = fa[fa[i][j - 1]][j - 1];
}
}
int idx = 1;
dfs(1, 1, idx);

initSeg(1, 0, n - 1);

int Q;
cin >> Q;
while (Q--) {
int t;
scanf("%d", &t);
if (t == 1) {
int i, j;
scanf("%d %d", &i, &j);
swap(v2i[i2v[i].v], v2i[i2v[j].v]);
swap(i2v[i].v, i2v[j].v);
updateSeg(1, 0, n - 1, i2v[i].v);
updateSeg(1, 0, n - 1, i2v[j].v);
} else {
pair<int, int> st = {v2i[0], v2i[0]};
printf("%d\n", query(1, 0, n - 1, st));
}
}
return 0;
}

USACO4.4.1

输入

给你n(2<=n<=32)个点,m(0<=m<=1'000)条边,可包含重边,每条边权重c(0<=c<=2'000'000)

求/输出

最大流

让最小割被割的边数量最小,求该最小值

求在该最小值下,求字典序最小的最小割

解法

忽略0边[如果不忽略,可能在后面的问产生问题]

第一问最大流:最大流就增广路径

第二问:因为要求,让最小割的被割边的数量也最小,在进行最大流后,对所有满载且原始边不为0的边容量改为1,残余改为inf,再进行一次求最大流

第三问:在第二问基础上枚举删边,检查可能性,标记边.在原图上采取枚举删边,

关于此题

有看到这样的解法来处理二三问,把所有边按容量降序排列依次删边尝试,据说能过USACO,然而有反例[USACO数据真弱2333]

1
2
3
4
5
6
3 5
1 2 1
1 2 1
1 2 4
2 3 3
2 3 3

很明显总流量是6,要让最小割的数量最小是点2到点3 的两条流量为3的边


据说,有人的方案是把边容量乘1001再加上边index来做为新容量,从而最小边取%1001,最大流取/1001


测试数据里似乎没有c=0的时候


第二问的操作inf,必要性,简单的反例解释

1
2
3
4
5
6
7
8
4 7
1 2 7
2 3 7
1 3 2
3 4 2
3 4 2
3 4 2
3 4 2

明显1到3的流量可以达到9,而3到4的流量只有8

所以最小割一定是3到4,但如果耗尽改为1的话,且1到2,2到3都被耗尽,则有1到2,2到3都改为流量1,进行做第二问

如果不把1到3的2 改为inf,则会 认为最少的割边数量为3,而不是4


同样第二问结果并不能直接用于第三问,进行尝试删边,样例

1
2
3
4
5
6
4 5
1 2 4
2 3 2
2 4 2
1 3 100
3 4 2

最小割应该为 2->4 和3->4,如果1->2耗尽,在第二问变更后的图做尝试删边操作,则会发现1->2会影响流量,认为1->2属于割边

代码

code

0%