Loading [MathJax]/extensions/TeX/newcommand.js
\newcommand{\ord}[1]{\mathcal{O}\left(#1\right)} \newcommand{\abs}[1]{\lvert #1 \rvert} \newcommand{\floor}[1]{\lfloor #1 \rfloor} \newcommand{\ceil}[1]{\lceil #1 \rceil} \newcommand{\opord}{\operatorname{\mathcal{O}}} \newcommand{\argmax}{\operatorname{arg\,max}} \newcommand{\str}[1]{\texttt{"#1"}}

2024年12月24日 星期二

[The radix-2 Cooley-Tukey FFT / FNTT Algorithm] 庫利-圖基 快速 (傅立葉/數論) 變換演算法

離散捲積 (Discrete Convolution)

給定兩個數列 A = (a_0, a_1, \dots a_{n-1}),\ B = (b_0, b_1, \dots, b_{m-1})
求兩數列的離散捲積 C = (c_0, c_1, \dots, c_{n+m-2}),其中 c_k = \sum_{i + j = k}a_ib_j

我們可以將數列轉換成多項式: 

\begin{align} A(x)=a_0+a_1x+a_2x^2+\dots+a_{n-1}x^{n-1}\\B(x)=b_0+b_1x+b_2x^2+\dots+b_{m-1}x^{m-1}\end{align}

這樣一來,c_i = (A * B)(x)x^i 項的係數,如果用最 naive 的做法,總共要花 O(n\times m) 的時間。
這裡的目標是要在 O((n+m)\log (n+m)) 的時間算出 C

多項式的表示法

係數表示法 Coefficient Representation

對於一個 n-1 次多項式 F(x)=a_0+a_1x+a_2x^2+\dots+a_{n-1}x^{n-1}
我們可以用 Coefficient Representation 來表示他:

F(x) := [a_0, a_1, \dots, a_{n - 1}]

點值表示法 Point-value Representation

除此之外,令 x_0, x_1, \dots, x_{n-1}n 個不同的數字,
我們也能用這些點在 F 中的取值來表示他

y_i = F(x_i),\ i = 0, 1, \dots, n-1
F(x):= [y_0, y_1, \dots, y_{n - 1}]

這種表示法又叫做 Point-value Representation

新的思路

給定 Coefficient Representation,我們現在只會 O(n\times m) 來做多項式乘法。
那如果換成 Point-value Representation 呢?

C(x_i) = A(x_i) \times B(x_i),~i = 0, 1, \dots, n+m-2

我們只要能抓 n+m-1 個不同數字的取值,最後一個對一個再相乘起來就好了!只需要 O(n+m) 的時間。

我們可以把計算多項式乘法的任務轉換成:

  1. 選擇 n+m-1 個不同的數字 X=(x_0, x_1, \dots, x_{n+m-2})
  2. 將原本是 Coefficient Representation 的多項式 A,B 轉為 Point-value Representation
  3. O(n+m) 的時間計算 C(x_i) = A(x_i) \times B(x_i),得到用 Point-value Representation 表示的多項式 C
  4. 將多項式 C 轉換回 Coefficient Representation

只要好好的選擇 X,就可以用分治法 (divide and conquer) 加速步驟 2, 4。

圖片與文字內容皆參考自 NTHUCPP FFT 單元
圖片與文字內容皆參考自 NTHUCPP FFT 單元

X 的選擇

n=2^r,r\ge 0 的時候,假設有個 \omega(n) 函數有以下性質:

  1. \omega(n)^0, \omega(n)^1,...,\omega(n)^{n-1} 皆為不同數值
  2. \omega(n)^n=1
  3. \omega(n)^{\frac{n}{2}}=-1,其實條件 1, 2 同時滿足的話這點會自動成立
  4. \omega(n)^2=\omega(\frac{n}{2})

X=(x_0, x_1, \dots, x_{n-1}),~x_i=\omega(n)^i 則原本是 Coefficient Representation 的多項式 F(x) := [a_0, a_1, \dots, a_{n - 1}] 其 Point-value Representation F(x) := [y_0, y_1, \dots, y_{n - 1}],~y_i=F(x_i)

透過 \omega(n) 函數的性質可以利用分治法在 O(n \log n) 的時間遞迴求出來。

n 不是 2 的冪次,我們可以找到一個 n'=2^r,n'>n,將 a_n,a_{n+1},...,a_{n'-1} 都設為 0,則 F'(x)=a_0+a_1x+...+a_{n'-1}x^{n'-1} 就能滿足使用分治法的條件。

分治法 (divide and conquer) 求 Point-value Representation

假設有個函數 DC(F, n) 輸入一個 n-1 次多項式 F(x) 的係數表示法,回傳 [F(\omega(n)^0), F(\omega(n)^1),..., F(\omega(n)^{n-1})]

\begin{align} G(x)=a_0+a_2x+a_4x^2+...+a_{n-2}x^{\frac{n}{2}-1}\\ H(x)=a_1+a_3x+a_5x^2+...+a_{n-1}x^{\frac{n}{2}-1} \end{align}F 的係數得到 H,G 的係數只需要 O(n) 的時間。

我們可以把 F(x)G(x)H(x) 表示:

F(x)=G(x^2)+x\times H(x^2) 透過 DC 函數可以遞迴得到 \begin{align} DC(H,n/2)&=[H(\omega(n/2)^0), H(\omega(n/2)^1),..., H(\omega(n/2)^{n/2-1})]\\ DC(G,n/2)&=[G(\omega(n/2)^0), G(\omega(n/2)^1),..., G(\omega(n/2)^{n/2-1})] \end{align}

對於 0\le k<\frac{n}{2},透過性質 2, 3, 4 可以知道:

\begin{align} F(\omega(n)^k)&=G(\omega(n)^{2k})+\omega(n)^k\times H(\omega(n)^{2k})\\ &=G(\omega(n/2)^k)+\omega(n)^k\times H(\omega(n/2)^k)\\ \\ F(\omega(n)^{\frac{n}{2}+k})&=G(\omega(n)^{n+2k})+\omega(n)^{\frac{n}{2}+k}\times H(\omega(n)^{n+2k})\\ &=G(\omega(n)^{2k})-\omega(n)^k\times H(\omega(n)^{2k}) \\ &=G(\omega(n/2)^k)-\omega(n)^k\times H(\omega(n/2)^k) \end{align}

這樣有了 DC(H,n/2),DC(G,n/2) 就可以在 O(n) 的時間做出 DC(F,n) 的結果。得到遞迴的時間複雜度 T(n)=O(n) + 2T(n/2) + O(n) = O(n\log n)

舉例來說 n=8

F(x)=a_0+a_1x+a_2x^2+...+a_7x^7

\begin{align} G(x)=a_0+a_2x+a_4x^2+a_6x^3\\ H(x)=a_1+a_3x+a_5x^2+a_7x^3 \end{align}

想要用遞迴方法求出 F(\omega(8)^0), F(\omega(8)^1),..., F(\omega(8)^7)

首先可以遞迴求出

\begin{align} G(\omega(4)^0), G(\omega(4)^1), G(\omega(4)^2), G(\omega(4)^3)\\ H(\omega(4)^0), H(\omega(4)^1), H(\omega(4)^2), H(\omega(4)^3) \end{align}

接著可以在 O(n) 得到:

  • 用加的
    • F(\omega(8)^0)=G(\omega(4)^0)+\omega(8)^0\times H(\omega(4)^0)
    • F(\omega(8)^1)=G(\omega(4)^1)+\omega(8)^1\times H(\omega(4)^1)
    • F(\omega(8)^2)=G(\omega(4)^2)+\omega(8)^2\times H(\omega(4)^2)
    • F(\omega(8)^3)=G(\omega(4)^3)+\omega(8)^3\times H(\omega(4)^3)
  • 用減的
    • F(\omega(8)^4)=G(\omega(4)^0)-\omega(8)^0\times H(\omega(4)^0)
    • F(\omega(8)^5)=G(\omega(4)^1)-\omega(8)^1\times H(\omega(4)^1)
    • F(\omega(8)^6)=G(\omega(4)^2)-\omega(8)^2\times H(\omega(4)^2)
    • F(\omega(8)^7)=G(\omega(4)^3)-\omega(8)^3\times H(\omega(4)^3)

程式碼的部分等講完逆變換後在介紹。 

逆變換

(y_0, y_1, \dots, y_{n - 1}),~y_i=F(x_i),令多項式 Z(x)=y_0+y_1x+y_2x^2+y_{n-1}x^{n-1},也就是將 F(x) 的 Point-value Representation 作為多項式 Z(x) 的 Coefficient Representation。

\omega(n)^k 帶入 Z(x) 可以發現

\begin{align} Z(\omega(n)^k)&=\sum_{i=0}^{n-1} F(\omega(n)^i)\omega(n)^{ik} \\ &=\sum_{i=0}^{n-1} \left(\left(\sum_{j=0}^{n-1} a_j\omega(n)^{ij}\right)\omega(n)^{ik}\right)\\ &=\sum_{j=0}^{n-1}a_j\left(\sum_{i=0}^{n-1} \left(\omega(n)^{j+k}\right)^i\right) \end{align}

這裡等比數列的和只有兩種可能

\sum_{i=0}^{n-1} (\omega(n)^{j+k})^i = \left\{ \begin{aligned} &n&,&~~~j+k\equiv 0\ (mod\ n) \\ &\frac{\omega(n)^{n(j+k)}-1}{\omega(n)^{j+k} -1} = 0&, &~~~\text{else} \end{aligned} \right.

因此得到結論

  1. Z(\omega(n)^0)=a_0\times n
  2. Z(\omega(n)^k)=a_{n-k}\times n, ~~~0<k<n

這表示我們可以將 y_0\sim y_{n-1} 使用同樣的分治法輕鬆地在 O(n \log n) 得到原本多項式 F(x) 的係數 a_0\sim a_{n-1}

遞迴版本程式碼

#include <algorithm>
#include <cassert>
#include <cstddef>
template <typename T, typename Policy>
class CooleyTukeyAlgorithmRecursive {
public:
using policy = Policy;
using vector_type = typename Policy::vector_type;
private:
// Input: 係數表示法 F(x) := [f[0], f[1], ..., f[n-1]]
// Output: 點值表示法 F(x) := [fY[0], fY[1], ..., fY[n-1]], fY[i] = F(w(n)^i)
auto divide_and_conquer(vector_type f) {
size_t n = f.size();
if (n <= 1) return f;
vector_type g(n / 2), h(n / 2);
for (size_t i = 0; i < n; ++i) { // 根據奇偶分類
if (i % 2 == 0)
g[i / 2] = f[i];
else
h[i / 2] = f[i];
}
auto gY = divide_and_conquer(g); // 得到 gY[i] = G(w(n/2)^i)
auto hY = divide_and_conquer(h); // 得到 hY[i] = H(w(n/2)^i)
vector_type fY(n);
auto wn = Policy::omega(n), wk = Policy::one();
for (size_t k = 0; k < n / 2; ++k) {
auto u = gY[k], t = Policy::mul(wk, hY[k]);
fY[k] = Policy::add(u, t);
fY[k + n / 2] = Policy::sub(u, t);
wk = Policy::mul(wk, wn);
}
return fY; // 得到 fY[i] = F(w(n)^i)
}
public:
auto run(const vector_type& in, bool is_inv) {
size_t N = in.size();
assert((N & (N - 1)) == 0 && Policy::check(N)); // N 必須是 2 的冪次
auto out = divide_and_conquer(in);
if (is_inv) { // 逆變換
std::reverse(out.begin() + 1, out.end());
auto inv_N = Policy::inverse(N);
for (size_t i = 0; i < N; ++i) out[i] = Policy::mul(out[i], inv_N);
}
return out;
}
};

由於我們還不知道 \omega(n) 究竟是個怎樣的函數,實作使用 template 的方式,使用者要將與 \omega(n) 有關的操作寫成 class 後填入 `Policy` 這個欄位。


\omega(n) 的選擇

可以觀察到 \omega(n)^k 有非常明顯的循環性質,這在一般人常見的實數領域中很少見,有這種性質的東西經常出現在:

  1. 複數運算的單位根
  2. 同餘運算下的有限體 (finite field)

快速傅立葉變換 (Fast Fourier Transform, FFT)

\omega(n)=e^{i\frac{2\pi}{n}}。透過 Euler's formula 可以知道 e^{i\frac{2\pi}{n}}=\cos(\frac{2\pi}{n})+i\sin(\frac{2\pi}{n})

這樣 \omega(n) 的數學含意就是複數的 n單位根

  1. \omega(n)^0, \omega(n)^1,...,\omega(n)^{n-1} 的值皆不相同
  2. \omega(n)^n=e^{i\times 2\pi}=1
  3. \omega(n)^{\frac{n}{2}}=e^{i\pi}=-1
  4. \omega(n)^2=e^{i\frac{2\times 2\pi}{n}}=e^{i\frac{2\pi}{n/2}}=\omega(\frac{n}{2})

複數以及 `exp` 函數都是 C++ STL 有提供的東西:

#include <cmath>
#include <complex>
#include <vector>
template <typename T, typename ComplexTy = std::complex<T>>
struct FFT_Policy {
using vector_type = std::vector<ComplexTy>;
static constexpr T pi = std::acos((T)-1);
static bool check(size_t N) { return true; }
static auto one() { return ComplexTy(1, 0); }
static auto omega(size_t N) {
return std::exp(ComplexTy(0, 2 * pi / N));
}
static auto inverse(T value) { return T(1) / value; }
static auto add(ComplexTy a, ComplexTy b) { return a + b; }
static auto sub(ComplexTy a, ComplexTy b) { return a - b; }
static auto mul(ComplexTy a, ComplexTy b) { return a * b; }
};
view raw FFT_policy.cpp hosted with ❤ by GitHub

不過使用 FFT 計算多項式乘法會產生浮點數誤差,因此有些人會考慮使用待會會介紹的 FNTT

快速數論變換 (Fast Number-Theoretic Transform, FNTT)

\omega(n)=g^{\frac{P-1}{n}}\mod P,這裡的 P 是滿足某性質的質數且 g\mod P 的原根。因此首先我們要來認識什麼是原根。

什麼是原根

假設 g, m 互質, 使得 g^d \equiv 1\ (mod\ m) 成立的最小正整數 d 定義為 \delta_m(g)

根據歐拉定理 \delta_m(g)|\phi(m),若 \delta_m(g) = \phi(m) ,則稱 g\mod m 的原根 (primitive root)。

如果 m 是個質數,則最小的 g 通常是個很小的數字 (g\ll P^{5/\log\log P} by Least Prime Primitive Roots),zerojudge 上剛好有一題 [b435. 尋找原根]

對於任意質數 P>2 其原根 g 有一些直觀的性質:

  1. \phi(P)=P-1,~g^{\phi(P)}\equiv g^{P-1}\equiv 1\ (mod\ P),這其實就是費馬小定理
  2. g^1,...,g^{P-2},g^{P-1}\mod P 的結果皆不相同,這是原根本來的性質
  3. g^{(P-1)/2}\equiv -1\ (mod\ P) ,由性質 1,2 可以得到

如何選擇質數 P

P-1 可以被 n 整除,則所有 \omega(n) 的性質都能滿足(所有運算皆是同餘運算):

  1. \omega(n)^0, \omega(n)^1,...,\omega(n)^{n-1} 的值皆不相同
  2. \omega(n)^n=g^{\frac{P-1}{n}n}=g^{P-1}= 1
  3. \omega(n)^{\frac{n}{2}}=g^{(P-1)/2}=-1
  4. \omega(n)^2=g^{\frac{2(P-1)}{n}}=g^{\frac{P-1}{n/2}}=\omega(\frac{n}{2})

為了滿足 P-1 可以被 n 整除,因為 n 是 2 的冪次,FNTT 需要一個特殊構造的質數 P=r\times 2^k+1,~2^k\ge n,已經有中國人整理出一些常用的質數:

P=998244353=7\times 17\times 2^{23}+1 是個經常被使用的質數,其原根 g=3

這樣我們就可以輕鬆地根據定義寫出 FNTT 的實作:

// It is recommended that T at least be long long
#include <vector>
template <typename T, T P, T G>
struct NTT_Policy {
using vector_type = std::vector<T>;
static T pow_mod(T n, T k, T m) {
T ans = 1;
for (n %= m; k; k >>= 1) {
if (k & 1) ans = ans * n % m;
n = n * n % m;
}
return ans;
}
static bool check(size_t N) { return N <= 1 || P % N == 1; }
static auto one() { return T(1); }
static auto omega(size_t N) {
return pow_mod(G, (P - 1) / N, P);
}
static auto inverse(T value) { return pow_mod(value, P - 2, P); }
static auto add(T a, T b) { return (a + b) % P; }
static auto sub(T a, T b) { return ((a - b) % P + P) % P; }
static auto mul(T a, T b) { return a * b % P; }
};
view raw NTT_policy.cpp hosted with ❤ by GitHub

注意 FNTT 的所有運算皆是同餘運算,也就是說 FNTT 的計算多項式乘法的結果是原本的數字\mod P 的值,因此若需要得到精確的結果需要用不同質數執行多次 FNTT 使用中國剩餘定理將結果合併。

假設有個 n-1 次多項式要和一個 m-1 次多項式做乘法,這兩個多項式的所有係數皆小於一個正整數 q

那麼這樣任何多項式係數的範圍就是 [0,q-1],係數兩兩相乘不會超過 (q-1)^2,一共最多 \min(n,m) 項相加,不會超過 \min(n,m)\times(q-1)^2

我們可以選 k 個可以進行 FNTT 的不同質數使得以下條件成立:

\prod_{i=1}^{k}p_i>\min(n,m)\times(q-1)^2

這樣分別使用這些質數執行 FNTT 後再使用中國剩餘定理將結果合併就可以得到完全精確的係數,但要注意計算範圍可能會超過 `long long`,甚至有可能會需要 `__int128_t`。

非遞迴版 Cooley-Tukey Algorithm

我們將係數遞迴的狀況畫出來,注意到葉節點係數的順序會是 (0, 4, 2, 6, 1, 5, 3, 7)

觀察這棵樹,由上往下的第 i 次分層時,是按照其 index 在第 i 個 bit 的奇偶分兩邊的,並且第 i 次分層會決定其最後位子的第 \log_2 n - i - 1 個 bit。

可以推論出,index i 的換置後的位子就會將是 i 的 binary representation 給 reverse。

Reverse Bit 的方法

遞推建表法,建立 O(n) 大小表,總時間複雜度也是 O(n)

auto get_bit_reverse_table(size_t N){
std::vector<size_t> table(N, 0);
for(size_t i = 1; i < N; ++i){
table[i] = table[i >> 1] >> 1;
if(i & 1) table[i] += N >> 1;
}
return table;
}

直接換置法,一次反轉一個數字 n,只要 O(1) 空間,但時間複雜度是 O(\log\log n)

#include <type_traits>
template <typename T>
inline T reverse_bits(T n) {
using unsigned_T = typename std::make_unsigned<T>::type;
unsigned_T v = (unsigned_T)n;
v = ((v & 0xAAAAAAAAAAAAAAAA) >> 1) | ((v & 0x5555555555555555) << 1);
v = ((v & 0xCCCCCCCCCCCCCCCC) >> 2) | ((v & 0x3333333333333333) << 2);
v = ((v & 0xF0F0F0F0F0F0F0F0) >> 4) | ((v & 0x0F0F0F0F0F0F0F0F) << 4);
if constexpr (sizeof(T) == 1) return v;
v = ((v & 0xFF00FF00FF00FF00) >> 8) | ((v & 0x00FF00FF00FF00FF) << 8);
if constexpr (sizeof(T) == 2) return v;
v = ((v & 0xFFFF0000FFFF0000) >> 16) | ((v & 0x0000FFFF0000FFFF) << 16);
if constexpr (sizeof(T) <= 4)
return v;
else
v = ((v & 0xFFFFFFFF00000000) >> 32) | ((v & 0x00000000FFFFFFFF) << 32);
return (T)v;
}

如果 index i 的位置是 j,那麼 index j 的位置也會是 i

想要節省空間的話,可以考慮用直接換置法 in-place 進行換置:

template<typename VectorTy>
void displacement(VectorTy &V){
size_t N = V.size();
for(int i = 0; i < N; ++i){
size_t rev_i = reverse_bits(i) >> (sizeof(i) * 8 - N);
if(i < rev_i) std::swap(V[i], V[rev_i]);
}
}
view raw in_place.cpp hosted with ❤ by GitHub

蝶形網路 Butterfly Diagram

我們一開始就把係數的順序透過 bit reverse 換置,可以寫出非遞迴版本的程式碼:

#include <algorithm>
#include <cassert>
#include <cstddef>
template <typename T, typename Policy>
class CooleyTukeyAlgorithm {
size_t reverse_bits_len(size_t N, size_t len) {
return ::reverse_bits(N) >> (sizeof(N) * 8 - len);
}
public:
using policy = Policy;
using vector_type = typename Policy::vector_type;
auto run(const vector_type& in, bool is_inv) {
size_t N = in.size();
assert((N & (N - 1)) == 0 && Policy::check(N));
vector_type out(N);
for (size_t i = 0; i < N; ++i)
out[reverse_bits_len(i, std::__lg(N))] = in[i];
for (size_t step = 2; step <= N; step *= 2) {
auto wn = Policy::omega(step), wk = Policy::one();
const size_t helf_step = step / 2;
for (size_t i = 0; i < helf_step; ++i) {
for (size_t k = i; k < N; k += step) {
size_t j = k + helf_step;
auto u = out[k], t = Policy::mul(wk, out[j]);
out[k] = Policy::add(u, t);
out[j] = Policy::sub(u, t);
}
wk = Policy::mul(wk, wn);
}
}
if (is_inv) {
std::reverse(out.begin() + 1, out.end());
auto inv_N = Policy::inverse(N);
for (size_t i = 0; i < N; ++i) out[i] = Policy::mul(out[i], inv_N);
}
return out;
}
};

將計算流程畫成圖形,可以看到有很多長得像蝴蝶的形狀,因此被稱之為蝶形網路:

離散捲積程式碼

template <typename AlgorithmTy>
auto convolution(typename AlgorithmTy::vector_type A,
typename AlgorithmTy::vector_type B) {
using Policy = typename AlgorithmTy::policy;
using vector_type = typename AlgorithmTy::vector_type;
if (A.empty() || B.empty()) return vector_type{};
size_t C_size = A.size() + B.size() - 1, N = C_size;
while (N & (N - 1)) ++N;
A.resize(N), B.resize(N);
A = AlgorithmTy().run(A, false), B = AlgorithmTy().run(B, false);
vector_type C(N);
for (size_t i = 0; i < N; ++i) C[i] = Policy::mul(A[i], B[i]);
C = AlgorithmTy().run(C, true);
C.resize(C_size);
return C;
}
view raw convolution.cpp hosted with ❤ by GitHub

測試程式碼

#include <initializer_list>
#include <iostream>
template <typename ValueTy>
auto naiveMethod(std::vector<ValueTy> A, std::vector<ValueTy> B) {
if (A.empty() || B.empty()) return std::vector<ValueTy>{};
std::vector<ValueTy> C(A.size() + B.size() - 1);
for (size_t i = 0; i < A.size(); ++i) {
for (size_t j = 0; j < B.size(); ++j) {
C[i + j] += A[i] * B[j];
}
}
return C;
}
template <typename AlgorithmTy>
void test(typename AlgorithmTy::vector_type A,
typename AlgorithmTy::vector_type B) {
auto Res = convolution<AlgorithmTy>(A, B);
for (auto x : Res) std::cout << x << ' ';
std::cout << std::endl;
}
int main() {
std::cout << std::fixed;
std::cout.precision(1);
using NTT =
CooleyTukeyAlgorithm<long long,
NTT_Policy<long long, (1 << 23) * 7 * 17 + 1, 3>>;
test<NTT>({1, 2, 3, 4}, {5, 6, 7, 8, 9});
using FFT = CooleyTukeyAlgorithm<double, FFT_Policy<double>>;
test<FFT>({1, 2, 3, 4}, {5, 6, 7, 8, 9});
auto C = naiveMethod<long long>({1, 2, 3, 4}, {5, 6, 7, 8, 9});
for (auto x : C) std::cout << x << ' ';
std::cout << std::endl;
return 0;
}
view raw test.cpp hosted with ❤ by GitHub

Output:

5 16 34 60 70 70 59 36
(5.0,0.0) (16.0,0.0) (34.0,0.0) (60.0,-0.0) (70.0,-0.0) (70.0,-0.0) (59.0,-0.0) (36.0,0.0)
5 16 34 60 70 70 59 36 


2021年12月18日 星期六

[Counting Sort, Radix Sort] 計數排序, 基數排序

 Counting Sort是一種效率很高的排序方式,複雜度為\ord{n+k},其中k是Bucket的大小,由此可知僅限於整數且數字範圍不能太大。根據觀察在很多應用中會有對物件以編號大小進行排序的行為,在這方面應該能做到很大的加速。

另外一個問題是Counting Sort雖然簡單,很多人甚至可以自己想到實作方法,但這也導致了標準的作法常常被人忽略。因此這裡就來給大家展示標準的Counting sort:

#include <algorithm>
#include <numeric>
template <typename InputIter, typename OutputIter, typename BucketIter,
typename KeyFunction>
void counting_sort(InputIter First, InputIter Last, BucketIter BucketFirst,
BucketIter BucketLast, OutputIter OutputFirst,
KeyFunction Key) {
std::fill(BucketFirst, BucketLast, 0);
for (auto Iter = First; Iter != Last; ++Iter)
++(*(BucketFirst + Key(*Iter)));
std::partial_sum(BucketFirst, BucketLast, BucketFirst);
do {
--Last;
*(OutputFirst + --(*(BucketFirst + Key(*Last)))) = std::move(*Last);
} while (Last != First);
}

參數的解釋如下:

  • First, Last:
    和std::sort的定義一樣,需要排序的範圍,注意不一定要是random access iterator。
  • BucketFirst, BucketLast:
    Counting Sort正統的實作方式會有一個正整數陣列作為Bucket,考量到各種應用所以這裡接傳Bucket的範圍進來能做的優化會比較多,必須要是random access iterator。
  • OutputFirst:
    Counting Sort的output是直接將input存到另一個陣列中,因此OutputFirst指的是Output陣列的開頭,必須要是random access iteator,且要注意output的空間是足夠的。這邊將input存進output時是用std::move的方式,如果想要保留原本input的話可以將其拿掉。
  • Key:
    這是一個函數,Key(x)必須要回傳一個0~(BucketLast-BucketFirst-1)的正整數作為被排序物件x的關鍵值。
有了Counting sort,Radix Sort就比較好解釋了。首先正統的Counting sort是stable sort,所以Key值相同的東西排序前後的先後順序是不變的。因此可以透過多次的Counting Sort來完成一些原本Counting Sort無法完成的事情。
以整數(int, -2147483648~2147483647)排序為例,可以先針對第十億位做為Key進行排序,接著再對第一億位做為Key、第一千萬位做為Key...直到十位數、個位數作為Key,最後再以正負號最為Key進行排序,這樣就可以完成一般範圍的整數排序。
實際上一般不會這樣用,通常是用在有多個Key值的情況,以下面的程式碼來說,可以自行執行看看花費的時間有多少:

#include "CountingSort.hpp"
#include <cassert>
#include <chrono>
#include <iostream>
#include <random>
#include <vector>
using namespace std;
int main() {
vector<pair<int, int>> V1;
mt19937 MT(7122);
const int N = 10000000, A = 1000, B = 10000;
for (int i = 0; i < N; ++i) {
V1.emplace_back(MT() % A, MT() % B);
}
decltype(V1) V2 = V1;
auto Begin = chrono::high_resolution_clock::now();
sort(V2.begin(), V2.end());
auto End = chrono::high_resolution_clock::now();
cout << "std::sort: "
<< chrono::duration_cast<chrono::milliseconds>(End - Begin).count()
<< "ms\n";
Begin = chrono::high_resolution_clock::now();
decltype(V2) Tmp(V1.size());
vector<int> Bucket(max(A, B), 0);
counting_sort(V1.begin(), V1.end(), Bucket.begin(), Bucket.begin() + B,
Tmp.begin(), [&](const auto &P) -> int { return P.second; });
counting_sort(Tmp.begin(), Tmp.end(), Bucket.begin(), Bucket.begin() + A,
V1.begin(), [&](const auto &P) -> int { return P.first; });
End = chrono::high_resolution_clock::now();
cout << "radix_sort: "
<< chrono::duration_cast<chrono::milliseconds>(End - Begin).count()
<< "ms\n";
assert(V1 == V2);
return 0;
}
view raw main.cpp hosted with ❤ by GitHub

2021年12月13日 星期一

[Discretize Relabeling] 離散化器

離散化是演算法競賽常用的操作,在各種實際問題上也能看到其應用。最基本的情況,是對於n個可排序的元素,製造一個map使得它們可以和自己的名次一一對應,但通常的應用中這n個元素確定之後就不太會有增減的動作,因此可以存到vector中排序去除重複的部分,搜索的部分就用二分搜尋來取代。

#include <algorithm>
#include <stdexcept>
#include <vector>
template <typename T, typename Alloc = std::allocator<T>>
class Discretizer : private std::vector<T, Alloc> {
void build() {
std::sort(std::vector<T, Alloc>::begin(), std::vector<T, Alloc>::end());
std::vector<T, Alloc>::erase(std::unique(std::vector<T, Alloc>::begin(),
std::vector<T, Alloc>::end()),
std::vector<T, Alloc>::end());
}
public:
using const_iterator = typename std::vector<T, Alloc>::const_iterator;
using const_reverse_iterator =
typename std::vector<T, Alloc>::const_reverse_iterator;
using const_reference = typename std::vector<T, Alloc>::const_reference;
using size_type = typename std::vector<T, Alloc>::size_type;
Discretizer() = default;
template <class InputIterator>
Discretizer(InputIterator first, InputIterator last,
const Alloc &alloc = Alloc())
: std::vector<T, Alloc>(first, last, alloc) {
build();
}
Discretizer(const typename std::vector<T, Alloc> &x)
: std::vector<T, Alloc>(x) {
build();
}
Discretizer(const typename std::vector<T, Alloc> &x, const Alloc &alloc)
: std::vector<T, Alloc>(x, alloc) {
build();
}
Discretizer(typename std::vector<T, Alloc> &&x)
: std::vector<T, Alloc>(std::move(x)) {
build();
}
Discretizer(typename std::vector<T, Alloc> &&x, const Alloc &alloc)
: std::vector<T, Alloc>(std::move(x), alloc) {
build();
}
Discretizer(std::initializer_list<T> il, const Alloc &alloc = Alloc())
: std::vector<T, Alloc>(il, alloc) {
build();
}
Discretizer(const Discretizer &x) : std::vector<T, Alloc>(x) {}
Discretizer(Discretizer &&x) : std::vector<T, Alloc>(std::move(x)) {}
Discretizer(Discretizer &&x, const Alloc &alloc)
: std::vector<T, Alloc>(std::move(x), alloc) {}
Discretizer &operator=(const Discretizer &x) {
std::vector<T, Alloc>::operator=(x);
return *this;
}
Discretizer &operator=(Discretizer &&x) {
std::vector<T, Alloc>::operator=(std::move(x));
return *this;
}
const_iterator begin() const noexcept {
return std::vector<T, Alloc>::begin();
}
const_iterator end() const noexcept { return std::vector<T, Alloc>::end(); }
const_reverse_iterator rbegin() const noexcept {
return std::vector<T, Alloc>::rbegin();
}
const_reverse_iterator rend() const noexcept {
return std::vector<T, Alloc>::rend();
}
size_type size() const noexcept { return std::vector<T, Alloc>::size(); }
size_type capacity() const noexcept {
return std::vector<T, Alloc>::capacity();
}
bool empty() const noexcept { return std::vector<T, Alloc>::empty(); }
void shrink_to_fit() { std::vector<T, Alloc>::shrink_to_fit(); }
const_reference operator[](size_type n) const {
return std::vector<T, Alloc>::operator[](n);
}
const_reference at(size_type n) const { return std::vector<T, Alloc>::at(n); }
const_reference front() const { return std::vector<T, Alloc>::front(); }
const_reference back() const { return std::vector<T, Alloc>::back(); }
void pop_back() { std::vector<T, Alloc>::pop_back(); }
void clear() noexcept { std::vector<T, Alloc>::clear(); }
void swap(Discretizer<T, Alloc> &x) { std::vector<T, Alloc>::swap(x); }
const_iterator lower_bound(const_reference x) const {
return std::lower_bound(begin(), end(), x);
}
const_iterator upper_bound(const_reference x) const {
return std::upper_bound(begin(), end(), x);
}
size_type getIndex(const_reference x) const {
auto Iter = lower_bound(x);
if (Iter == end() || *Iter != x)
throw std::out_of_range("value not exist.");
return Iter - begin();
}
};
view raw Discretizer.h hosted with ❤ by GitHub
#include <iostream>
#include "Discretizer.h"
int main() {
std::vector<int> V{3, 1, 9, 2, 2, 7, 3, 100};
Discretizer<int> D(std::move(V));
for (auto e : D)
std::cout << e << ' ';
std::cout << '\n';
std::cout << D.getIndex(7) << '\n';
std::cout << D.getIndex(100) << '\n';
std::cout << D.upper_bound(9) - D.lower_bound(2) << '\n';
return 0;
}
view raw main.cpp hosted with ❤ by GitHub

2021年8月13日 星期五

[Multiple line segment intersection] Bentley–Ottmann 演算法

 基本上這個問題就是給你一些線段(格式通常為兩個端點),你要找出這些線段的交點。直觀的做法兩兩進行計算會花上\ord{n^2}的時間,但大多數的情況下交點不會很多。為了解決這個問題,修改自Shamos–Hoey演算法的Bentley–Ottmann演算法可以在\ord{(n+k)\log n}的時間內找出所有交點,其中k是交點數量。

這裡附上實作時需要用到的基本資料結構:

template <typename T> struct point {
T x, y;
point() {}
point(const T &x, const T &y) : x(x), y(y) {}
point operator+(const point &b) const { return point(x + b.x, y + b.y); }
point operator-(const point &b) const { return point(x - b.x, y - b.y); }
point operator*(const T &b) const { return point(x * b, y * b); }
bool operator==(const point &b) const { return x == b.x && y == b.y; }
T dot(const point &b) const { return x * b.x + y * b.y; }
T cross(const point &b) const { return x * b.y - y * b.x; }
};
template <typename T> struct line {
line() {}
point<T> p1, p2;
T a, b, c; // ax+by+c=0
line(const point<T> &p1, const point<T> &p2) : p1(p1), p2(p2) {}
void pton() {
a = p1.y - p2.y;
b = p2.x - p1.x;
c = -a * p1.x - b * p1.y;
}
T ori(const point<T> &p) const { return (p2 - p1).cross(p - p1); }
T btw(const point<T> &p) const { return (p1 - p).dot(p2 - p); }
int seg_intersect(const line &l) const {
// -1: Infinitude of intersections
// 0: No intersection
// 1: One intersection
// 2: Collinear and intersect at p1
// 3: Collinear and intersect at p2
T c1 = ori(l.p1), c2 = ori(l.p2);
T c3 = l.ori(p1), c4 = l.ori(p2);
if (c1 == 0 && c2 == 0) {
bool b1 = btw(l.p1) >= 0, b2 = btw(l.p2) >= 0;
T a3 = l.btw(p1), a4 = l.btw(p2);
if (b1 && b2 && a3 == 0 && a4 >= 0)
return 2;
if (b1 && b2 && a3 >= 0 && a4 == 0)
return 3;
if (b1 && b2 && a3 >= 0 && a4 >= 0)
return 0;
return -1;
} else if (c1 * c2 <= 0 && c3 * c4 <= 0)
return 1;
return 0;
}
point<T> line_intersection(const line &l) const {
point<T> a = p2 - p1, b = l.p2 - l.p1, s = l.p1 - p1;
// if(a.cross(b)==0) return INF;
return p1 + a * (s.cross(b) / a.cross(b));
}
};
template <typename T> using segment = line<T>;
view raw Basic.cpp hosted with ❤ by GitHub

演算法使用掃描線進行。掃描線是一條垂直線從左邊掃到右邊(有些實作是水平線從上面掃到下面),並且在遇到事件點的時候進行相關處理。

線段的兩端點以及交點都作為事件點被紀錄在最終結果中。對於每個事件點P,我們會計算三個集合:

  • U集合:所有以P為起始點的線段集合
  • C集合:所有包含P的線段集合
  • L集合:所有以P為結束點的線段集合
當然要先保證每條線段的起始點移動會在結束點的左方,只要得到線段後稍微判斷一下就可以做到了。每個事件點找出這三個集合後就可以很容易的判斷相交資訊,但要注意的是會有以下的退化情形:
  • 線段退化成點:這種情況該點的U和L都會包含該線段。
  • 兩線段重合:只有重合處的兩端點會被紀錄為事件點,可以根據UCL判斷出是否線段重合
  • 垂直線段:排序點和線段時如果x一樣就按照y來比較
最後是掃描線的資料結構,需要一棵平衡的BST根據當前掃描線和各個線段切點的y值進行排序,但這件事是可以用STL做到的!我們把當前事件點傳進比較函數裡面進行計算,因為在任何一個時刻BST中的資料都是根據當前的比較函數由小排到大的,應該不算undefined behavior。另外該演算法的浮點數誤差很大,建議使用時套上處理誤差的模板或是直接用分數計算:
#include <algorithm>
#include <map>
#include <set>
#include <vector>
template <typename T> struct SegmentIntersection {
struct PointInfo {
std::vector<const segment<T> *> U, C, L;
// U: the set of segments start at the point.
// C: the set of segments contain the point.
// L: the set of segments end at the point.
};
private:
struct PointCmp {
bool operator()(const point<T> &a, const point<T> &b) const {
return (a.x < b.x) || (a.x == b.x && a.y < b.y);
}
};
struct LineCmp {
const point<T> &P;
LineCmp(const point<T> &P) : P(P) {}
T getY(const segment<T> *s) const {
if (s->b == 0)
return P.y;
return (s->a * P.x + s->c) / -s->b;
}
bool operator()(const line<T> *a, const line<T> *b) const {
return getY(a) < getY(b);
}
};
const std::vector<segment<T>> Segs;
std::map<point<T>, PointInfo, PointCmp> PointInfoMap;
std::set<point<T>, PointCmp> Queue;
point<T> Current;
std::multiset<const segment<T> *, LineCmp> BST;
std::vector<segment<T>> initSegs(std::vector<segment<T>> &&Segs) {
for (auto &S : Segs) {
if (!PointCmp()(S.p1, S.p2))
std::swap(S.p1, S.p2);
S.pton();
}
return Segs;
}
void init() {
for (auto &S : Segs) {
PointInfoMap[S.p1].U.emplace_back(&S);
PointInfoMap[S.p2].L.emplace_back(&S);
Queue.emplace(S.p1);
Queue.emplace(S.p2);
}
}
void FindNewEvent(const segment<T> *A, const segment<T> *B) {
auto Type = A->seg_intersect(*B);
if (Type <= 0)
return;
point<T> P;
if (Type == 2)
P = A->p1;
else if (Type == 3)
P = A->p2;
else
P = A->line_intersection(*B);
if (PointCmp()(Current, P))
Queue.emplace(P);
}
void HandleEventPoint() {
auto &Info = PointInfoMap[Current];
segment<T> Tmp(Current, Current);
auto LBound = BST.lower_bound(&Tmp);
auto UBound = BST.upper_bound(&Tmp);
std::copy_if(
LBound, UBound, std::back_inserter(Info.C),
[&](const segment<T> *S) -> bool { return !(S->p2 == Current); });
BST.erase(LBound, UBound);
auto UC = Info.U;
UC.insert(UC.end(), Info.C.begin(), Info.C.end());
UC.erase(std::remove_if(
UC.begin(), UC.end(),
[&](const segment<T> *S) -> bool { return S->p1 == S->p2; }),
UC.end());
std::sort(UC.begin(), UC.end(),
[&](const segment<T> *A, const segment<T> *B) -> bool {
return (A->p2 - Current).cross(B->p2 - Current) > 0;
});
if (UC.empty()) {
if (UBound != BST.end() && UBound != BST.begin()) {
auto Sr = *UBound;
auto Sl = *(--UBound);
FindNewEvent(Sl, Sr);
}
} else {
if (UBound != BST.end()) {
auto Sr = *UBound;
FindNewEvent(UC.back(), Sr);
}
if (UBound != BST.begin()) {
auto Sl = *(--UBound);
FindNewEvent(Sl, UC.front());
}
}
for (auto S : UC)
BST.emplace(S);
}
public:
SegmentIntersection(std::vector<segment<T>> Segs)
: Segs(initSegs(std::move(Segs))), BST(Current) {
init();
while (Queue.size()) {
auto It = Queue.begin();
Current = *It;
Queue.erase(It);
HandleEventPoint();
}
}
const std::vector<segment<T>> getSegments() const { return Segs; }
const std::map<point<T>, PointInfo, PointCmp> &getPointInfoMap() const {
return PointInfoMap;
}
};

最後是測試的部分,以下圖做為測試範例:

將該圖轉換成我們接受的input如下:
10
-2 7 2 0
-2 7 -2 0
-2 6 2 5
-2 6 2 2
-2 4 2 7
-2 4 2 2
-2 4 4 1
-2 0 2 2
0 1 0 1
0 3 4 1

最後附上測試程式碼,需要的話可以自己執行看看:

#include <cmath>
#include <iostream>
const double EPS = 1e-9;
struct Double {
double d;
Double(double d = 0) : d(d) {}
Double operator-() const { return -d; }
Double operator+(const Double &b) const { return d + b.d; }
Double operator-(const Double &b) const { return d - b.d; }
Double operator*(const Double &b) const { return d * b.d; }
Double operator/(const Double &b) const { return d / b.d; }
Double operator+=(const Double &b) { return d += b.d; }
Double operator-=(const Double &b) { return d -= b.d; }
Double operator*=(const Double &b) { return d *= b.d; }
Double operator/=(const Double &b) { return d /= b.d; }
bool operator<(const Double &b) const { return d - b.d < -EPS; }
bool operator>(const Double &b) const { return d - b.d > EPS; }
bool operator==(const Double &b) const { return fabs(d - b.d) <= EPS; }
bool operator!=(const Double &b) const { return fabs(d - b.d) > EPS; }
bool operator<=(const Double &b) const { return d - b.d <= EPS; }
bool operator>=(const Double &b) const { return d - b.d >= -EPS; }
friend std::ostream &operator<<(std::ostream &os, const Double &db) {
return os << db.d;
}
friend std::istream &operator>>(std::istream &is, Double &db) {
return is >> db.d;
}
};
void getResult(const SegmentIntersection<Double> &SI) {
std::cout << R"(-----------------Info---------------------
U: the set of segments start at the point.
C: the set of segments contain the point.
L: the set of segments end at the point.
------------------------------------------
)";
for (auto p : SI.getPointInfoMap()) {
std::cout << "(" << p.first.x << ',' << p.first.y << "):\n";
std::cout << " U:\n";
for (auto s : p.second.U) {
std::cout << " (" << s->p1.x << ',' << s->p1.y << ") (" << s->p2.x
<< ',' << s->p2.y << ")\n";
}
std::cout << " C:\n";
for (auto s : p.second.C) {
std::cout << " (" << s->p1.x << ',' << s->p1.y << ") (" << s->p2.x
<< ',' << s->p2.y << ")\n";
}
std::cout << " L:\n";
for (auto s : p.second.L) {
std::cout << " (" << s->p1.x << ',' << s->p1.y << ") (" << s->p2.x
<< ',' << s->p2.y << ")\n";
}
std::cout << '\n';
}
}
int main() {
int n;
std::cin >> n;
std::vector<segment<Double>> segments;
while (n--) {
point<Double> a, b;
std::cin >> a.x >> a.y >> b.x >> b.y;
segments.emplace_back(a, b);
}
SegmentIntersection<Double> SI(segments);
getResult(SI);
return 0;
}
view raw main.cpp hosted with ❤ by GitHub


2019年8月1日 星期四

[ Minimum Spanning Tree, kruskal, prim ] 最小生成樹經典演算法

以前覺得這應該是很簡單的東西,但我發現網路上使用priority_queue的prim演算法相關程式碼我覺得寫不好,我就把我自己的放上來。這裡順便也放上kruskal的程式碼。

prim \ord{\left(\abs{V}+\abs{E}\right)\log{\abs{V}}}:
template<typename T>
class prim{
static const int MAXN=100005;
struct edge{
int u, v;
T cost;
edge(){}
edge(int u,int v,const T &cost):u(u),v(v),cost(cost){}
bool operator< (const edge&b)const{
return cost > b.cost;
}
};
int n;// 1-base
vector<edge> E;
vector<int> G[MAXN];
bool vis[MAXN];
T build(int x){
T ans = 0;
priority_queue<edge> q;
for(auto i: G[x]) q.push(E[i]);
vis[x] = true;
while(q.size()){
edge e = q.top(); q.pop();
if(vis[e.v]) continue;
ans += e.cost;
vis[e.v] = true;
for(auto i:G[e.v]) if(!vis[E[i].v]){
q.push(E[i]);
}
}
return ans;
}
public:
void init(int _n){
n = _n;
for(int i=1; i<=n; ++i) G[i].clear();
}
void addEdge(int u, int v, const T &cost){
G[u].emplace_back(E.size());
E.emplace_back(u, v, cost);
G[v].emplace_back(E.size());
E.emplace_back(v, u, cost);
}
pair<T,int> solve(){
T ans = 0;
int component = 0;
for(int i=1; i<=n; ++i) vis[i] = false;
for(int i=1; i<=n; ++i) if(!vis[i]){
ans += build(i);
++component;
}
return {ans, component};
}
};
view raw prim.cpp hosted with ❤ by GitHub
kruskal \ord{\abs{V}+\abs{E}\log{\abs{E}}}:
template<typename T>
class kruskal{
static const int MAXN=100005;
int n; // 1-base
tuple<T,int,int> edge;
int pa[MAXN];
int find(int x){
if(x==pa[x]) return x;
return pa[x] = find(pa[x]);
}
public:
void init(int _n){
n = _n;
}
void addEdge(int u,int v,const T &cost){
edge.emplace_back(cost, u, v);
}
pair<T,int> solve(){
T ans = 0;
int component = n;
for(int i=1; i<=n; ++i) pa[i] = i;
sort(edge.begin(), edge.end());
for(const auto &e: edge){
int u = find(get<1>(e)), v = find(get<2>(e));
if(u==v) continue;
pa[u] = v;
ans += get<0>(e);
}
return {ans, component};
}
};
view raw kruskal.cpp hosted with ❤ by GitHub

2019年1月29日 星期二

[ Delaunay Triangulation ] Delaunay 三角剖分

我高中的時候在morris的網站第一次看到這個東西,那個時候就很想試著寫出來看看,但那時候英文很不好所以不會找資料就沒有成功。直到今年的NPSC,聽說台大出了一題找Voronoi Diagram的題目沒有人寫出來,於是我就想挑戰看看,寒假開始之後就認真研讀相關資料,終於在前幾天完成了隨機增量法以及分治法的模板。

先來說說甚麼是三角剖分 Triangulation 吧!假設平面上散布很多不重複的點,現在用一些線段將這些點連成許多的三角形,滿足所有三角形的內部不會有其他點,而且三角形的數量越多越好,這就是一個三角剖分。

Delaunay Triangulation是一種特殊的三角剖分,滿足所有的三角形其外接圓中不會出現其他的點,這樣的性質會使得所有三角形中最小的角盡量大,而且最重要的是它是 Voronoi Diagram 的對偶圖,Voronoi Diagram 直接求的話很容易有浮點誤差而且程式也不好寫,因此從Delaunay Triangulation直接轉換過去會更加方便。

還有一個很重要的點,透過Delaunay Triangulation 的邊集合可以找出平面歐幾里得距離最小生成樹。

這裡我先給出點的資料結構,方便接下來的講解:
template<typename T>
struct point{
T x,y;
point(){}
point(const T&x,const T&y):x(x),y(y){}
point operator+(const point &b)const{
return point(x+b.x,y+b.y);
}
point operator-(const point &b)const{
return point(x-b.x,y-b.y);
}
point operator*(const T &b)const{
return point(x*b,y*b);
}
point operator/(const T &b)const{
return point(x/b,y/b);
}
bool operator==(const point &b)const{
return x==b.x&&y==b.y;
}
T dot(const point &b)const{
return x*b.x+y*b.y;
}
T cross(const point &b)const{
return x*b.y-y*b.x;
}
T abs2()const{//向量長度的平方
return dot(*this);
}
};
view raw point2D.cpp hosted with ❤ by GitHub

一般來說找出 Delaunay Triangulation 的其中一個操作是要判斷一個點是不是在一個三角形的外接圓內,但直接去求外接圓圓心會有浮點誤差,因此有一個作法是將所有點投影到三維的拋物線曲面中,也就是下圖的碗狀曲面,會發現把一個圓投影上去之後,該圓會落在一個平面上,圓內的點會在平面下面,圓外的點會在平面上面,這樣就可以利用一些處理三維幾何的技術就可以避免浮點誤差的判斷點是否在外接圓內了。

範例程式碼中點在圓內輸出1,圓外輸出-1,圓上輸出0:
template<class T>
struct point3D{
T x, y, z;
point3D(){}
point3D(const T &x, const T &y, const T &z):
x(x), y(y), z(z) {}
point3D(const point<T> &p){
x=p.x, y=p.y, z=x*x+y*y;
}
point3D operator-(const point3D &b)const{
return point3D(x-b.x, y-b.y, z-b.z);
}
T dot(const point3D &b){
return x*b.x+y*b.y+z*b.z;
}
point3D cross(const point3D &b)const{
return point3D(y*b.z-z*b.y, z*b.x-x*b.z, x*b.y-y*b.x);
}
};
template<class T>
int inCircle(const point<T> &a, point<T> b, point<T> c, const point<T> &p){
if((b-a).cross(c-a)<0) swap(b, c);
point3D<T> a3(a), b3(b), c3(c), p3(p);
b3=b3-a3, c3=c3-a3, p3=p3-a3;
T res = p3.dot(b3.cross(c3));
return res<0 ? 1 : (res>0 ? -1 : 0);
}
view raw inCircle.cpp hosted with ❤ by GitHub

當然實作時並不會真正的寫一個三維點的資料結構,這樣程式碼會比較長。

Delaunay Triangulation 我所知道的有兩種,分別是隨機增量法以及分治法。隨機增量法再點隨機加入的時候複雜度是\ord{n\log n},分治法則是穩定\ord{n\log n},但我的實作分治法的效能是隨機增量法的三倍快。由於兩種做法都已經有詳細教學了這裡就不用文字敘述。

  1. 隨機增量法教學在《Computational Geometry - Algorithms and Applications》書中有提到,該書有中文版
  2. 分治法教學請點該網站連結
這裡附上程式碼,順帶一提morris的分治法好像在某些測資會因為三點共線爛掉

隨機增量法:
template<class T>
class Delaunay{
typedef point<T> PT;
struct face{
int a, b, c, tag;
vector<int> DAG;
face(int a,int b,int c):a(a),b(b),c(c),tag(0){}
};
struct edge{
int v, fid, pre, next;
edge(int v,int fid,int pre,int next):
v(v),fid(fid),pre(pre),next(next){}
};
vector<PT> S;
vector<face> F;
vector<edge> E;
int onLeft(int a, int b, int p){
if(a<3&&b<3) return (a+1)%3==b;
PT ba = S[b]-S[a], pa = S[p]-S[a];
if(a<3) ba = S[a]*-1, pa = S[p]-S[b];
if(b<3) ba = S[b];
if(p<3) pa = S[p];
T res = ba.cross(pa);
return res>0 ? 1 : (res<0 ? -1 : 0);
}
int inTriangle(int p, int fid){
int a = E[F[fid].a].v, b = E[F[fid].b].v;
int c = E[F[fid].c].v, ab = onLeft(a,b,p);
int bc = onLeft(b,c,p), ca = onLeft(c,a,p);
if(!ab&&bc*ca>=0) return F[fid].b;
if(!bc&&ab*ca>=0) return F[fid].c;
if(!ca&&ab*bc>=0) return F[fid].a;
return ab==bc&&bc==ca ? F[fid].a : -1;
}
int timeTag;
int pointIn(int p, int fid = 0){
if(F[fid].tag==timeTag) return -1;
F[fid].tag = timeTag;
int eid = inTriangle(p, fid);
if(eid<0||F[fid].DAG.empty()) return eid;
for(int v:F[fid].DAG){
int res = pointIn(p, v);
if(res>=0) return res;
}
return -2; // error!
}
T det3(T a11,T a12,T a13,T a21,T a22,T a23,T a31,T a32,T a33){
return a11*(a22*a33-a32*a23)-a12*(a21*a33-a31*a23)+a13*(a21*a32-a31*a22);
}
int inCircle(const PT &a, const PT &b, const PT &c, const PT &p){
T as = a.abs2(), bs = b.abs2(), cs = c.abs2(), ps = p.abs2();
T res = a.x * det3(b.y,bs,1,c.y,cs,1,p.y,ps,1)
-a.y * det3(b.x,bs,1,c.x,cs,1,p.x,ps,1)
+as * det3(b.x,b.y,1,c.x,c.y,1,p.x,p.y,1)
-det3(b.x,b.y,bs,c.x,c.y,cs,p.x,p.y,ps);
return res<0 ? 1 : (res>0 ? -1 : 0);
}
void addFlipFace(int pid, int eid){
F[E[eid].fid].DAG.emplace_back(F.size());
F[E[eid^1].fid].DAG.emplace_back(F.size());
F.emplace_back(E[eid].next, E.size(), E[eid^1].pre);
int a = F.back().a, c = F.back().c;
E.emplace_back(pid, F.size()-1, a, c);
E[a].pre = c;
E[a].next = E[c].pre = F.back().b;
E[a].fid = E[c].fid = F.size()-1;
E[c].next = a;
}
void legalizeEdge(int pid,int eid){
if(E[eid^1].fid<0) return;
int i=E[eid].v, j=E[eid^1].v, k=E[E[eid^1].next].v;
if(i>2&&j>2&&k<3) return;
bool notLegal;
if(i<3) notLegal = onLeft(pid,j,k)==1;
else if(j<3) notLegal = onLeft(i,pid,k)==1;
else notLegal = inCircle(S[pid], S[i], S[j], S[k])==1;
if(notLegal){
addFlipFace(k, eid);
addFlipFace(pid, eid^1);
E[eid].fid = E[eid^1].fid = -3;
legalizeEdge(pid, E[eid^1].pre);
legalizeEdge(pid, E[eid^1].next);
}
}
public:
void init(){
S = { PT(-1,-1), PT(1,0), PT(0,1) };
F = { face(0,2,4) };
E = { edge(1,0,4,2), edge(0,-1,3,5), edge(2,0,0,4),
edge(1,-1,5,1), edge(0,0,2,0), edge(2,-1,1,3) };
timeTag = 0;
}
void add(const PT& p){
S.emplace_back(p);
int eid = (++timeTag, pointIn(S.size()-1));
static vector<int> fe; fe.clear();
fe.emplace_back(E[eid].next);
fe.emplace_back(E[eid].pre);
if(!onLeft(E[eid^1].v, E[eid].v, S.size()-1)){
fe.emplace_back(E[eid^1].next);
fe.emplace_back(E[eid^1].pre);
E[eid].fid = E[eid^1].fid = -4;
}else fe.emplace_back(eid);
int fn = fe.size(), pid = S.size()-1;
for(int e:fe){
F[E[e].fid].DAG.emplace_back(F.size());
E[e].fid = F.size();
E[e].next = E.size();
F.emplace_back(e,E.size(),0);
E.emplace_back(pid,F.size()-1,e,0);
E.emplace_back(E[e].v,0,0,0);
}
for(int i=0,j=fn-1; i<fn; j=i++){
int last = F[E[fe[j]].fid].b^1;
F[E[fe[i]].fid].c = last;
E[fe[i]].pre = last;
E[E[fe[i]].next].next = last;
E[last].pre = E[fe[i]].next;
E[last].next = fe[i];
E[last].fid = E[fe[i]].fid;
}
for(int e:fe) legalizeEdge(pid, e);
}
vector<pair<int,int>> getEdge(){
vector<pair<int,int>> res;
for(const auto &f:F){
if(f.DAG.empty()){
int a = E[f.a].v,b = E[f.b].v,c = E[f.c].v;
if(a>2&&b>2) res.emplace_back(a-3, b-3);
if(b>2&&c>2) res.emplace_back(b-3, c-3);
if(c>2&&a>2) res.emplace_back(c-3, a-3);
}
}
return res;
}
};

分治法:
template<class T>
class Delaunay{
struct PT:public point<T>{
int g[2];
PT(const point<T> &p):
point<T>(p){ g[0]=g[1]=-1; }
};
static bool cmp(const PT &a,const PT &b){
return a.x<b.x||(a.x==b.x&&a.y<b.y);
}
struct edge{
int v,g[2];
edge(int v,int g0,int g1):
v(v){g[0]=g0,g[1]=g1;}
};
vector<PT> S;
vector<edge> E;
bool convex(int &from,int to,T LR){
for(int i=0;i<2;++i){
int c = E[S[from].g[i]].v;
auto A=S[from]-S[to], B=S[c]-S[to];
T v = A.cross(B)*LR;
if(v>0||(v==0&&B.abs2()<A.abs2()))
return from = c, true;
}
return false;
}
void addEdge(int v,int g0,int g1){
E.emplace_back(v,g0,g1);
E[E.back().g[0]].g[1] = E.size()-1;
E[E.back().g[1]].g[0] = E.size()-1;
}
void climb(int &p, int e, int n, int nl, int nr, int LR){
for(int i=E[e].g[LR]; (S[nr]-S[nl]).cross(S[E[i].v]-S[n])>0;){
if(inCircle(S[E[i].v],S[nl],S[nr],S[E[E[i].g[LR]].v])>=0)
{ p = i; break; }
for(int j=0;j<4;++j)
E[E[i^j/2].g[j%2^1]].g[j%2] = E[i^j/2].g[j%2];
int j=i; i=E[i].g[LR];
E[j].g[0]=E[j].g[1]=E[j^1].g[0]=E[j^1].g[1]=-1;
}
}
T det3(T a11,T a12,T a13,T a21,T a22,T a23,T a31,T a32,T a33){
return a11*(a22*a33-a32*a23)-a12*(a21*a33-a31*a23)+a13*(a21*a32-a31*a22);
}
int inCircle(const PT &a, const PT &b, const PT &c, const PT &p){
T as = a.abs2(), bs = b.abs2(), cs = c.abs2(), ps = p.abs2();
T res = a.x * det3(b.y,bs,1,c.y,cs,1,p.y,ps,1)
-a.y * det3(b.x,bs,1,c.x,cs,1,p.x,ps,1)
+as * det3(b.x,b.y,1,c.x,c.y,1,p.x,p.y,1)
-det3(b.x,b.y,bs,c.x,c.y,cs,p.x,p.y,ps);
return res<0 ? 1 : (res>0 ? -1 : 0);
}
void divide(int l, int r){
if(l>=r)return;
if(l+1==r){
int A=S[l].g[0]=S[l].g[1]=E.size();
E.emplace_back(r,A,A);
int B=S[r].g[0]=S[r].g[1]=E.size();
E.emplace_back(l,B,B);
return;
}
int mid = (l+r)/2;
divide(l,mid), divide(mid+1, r);
int nl = mid, nr = mid+1;
for(;;){
if(convex(nl,nr,1)) continue;
if(S[nr].g[0]!=-1&&convex(nr,nl,-1)) continue;
break;
}
addEdge(nr,S[nl].g[0],S[nl].g[1]);
S[nl].g[1] = E.size()-1;
if(S[nr].g[0]==-1){
addEdge(nl,E.size(),E.size());
S[nr].g[1] = E.size()-1;
}else addEdge(nl,S[nr].g[0],S[nr].g[1]);
S[nr].g[0] = E.size()-1;
int cl = nl, cr = nr;
for(;;){
int pl=-1, pr=-1, side;
climb(pl,E.size()-2,nl,nl,nr,1);
climb(pr,E.size()-1,nr,nl,nr,0);
if(pl==-1&&pr==-1) break;
if(pl==-1||pr==-1) side = pl==-1;
else side=inCircle(S[E[pl].v],S[nl],S[nr],S[E[pr].v])<=0;
if(side){
nr = E[pr].v;
addEdge(nr,E.size()-2,E[E.size()-2].g[1]);
addEdge(nl,E[pr^1].g[0],pr^1);
}else{
nl = E[pl].v;
addEdge(nr,pl^1,E[pl^1].g[1]);
addEdge(nl,E[E.size()-2].g[0],E.size()-2);
}
}
if(cl==nl&&cr==nr) return;//Collinearity
S[nl].g[0] = E.size()-2;
S[nr].g[1] = E.size()-1;
}
public:
void solve(const vector<point<T>> &P){
S.clear(), E.clear();
for(const auto &p:P) S.emplace_back(p);
sort(S.begin(),S.end(),cmp);
divide(0,int(S.size())-1);
}
vector<pair<int,int>> getEdge(){
vector<pair<int,int>> res;
for(size_t i=0;i<E.size();i+=2)
if(E[i].g[0]!=-1)
res.emplace_back(E[i].v,E[i^1].v);
return res;
}
};

2018年12月31日 星期一

[ Cyclic Longest Common Subsequence ] 環狀最長共同子序列

相信會看我發的文的人應該多少都知道LCS(最長共同子序列)是什麼吧?對於這樣的問題很早以前就有人提出\ord{nm}的方法了。
現在如果將問題中的字串改成頭尾相接環狀的字串,我們稱其最長共同子序列為CLCS,例如:
設A,B為頭尾相接環狀字串
A = "aabaa" , B = "aaabb"
CLCS(A,B) = "aaab"
像這樣的問題要怎麼解呢?剛好codeforces上面就有一題:http://codeforces.com/gym/100203/problem/B
為了解決這一題,查閱了不少奇怪的文章,最後給我找到了這篇論文:Solving Cyclic Longest Common Subsequence in Quadratic Time
對於CLCS提出了\ord{nm}的演算法,在這裡附上該演算法的實作:
#include<cstring>
#include<algorithm>
using std::max;
const int MAXN = 1505;
enum traceType{LEFT,DIAG,UP};
int dp[MAXN*2][MAXN], pa[MAXN*2][MAXN];
char AA[MAXN*2];
void LCS(const char *a, const char *b, int m, int n){
for(int i=1; i<=m; ++i)
for(int j=1; j<=n; ++j){
if(a[i-1]==b[j-1]) dp[i][j]=dp[i-1][j-1]+1;
else dp[i][j]=max(dp[i][j-1], dp[i-1][j]);
if(dp[i][j]==dp[i][j-1]) pa[i][j]=LEFT;
else if(a[i-1]==b[j-1]) pa[i][j]=DIAG;
else pa[i][j]=UP;
}
}
int trace(int m, int n){
int res = 0;
while(m&&n){
if(pa[m][n]==LEFT) --n;
else if(pa[m][n]==UP) --m;
else --m, --n, ++res;
}
return res;
}
void reRoot(int root,int m, int n){
int i=root, j=1;
while(j<=n&&pa[i][j]!=DIAG) ++j;
if(j>n) return;
pa[i][j] = LEFT;
while(i<m&&j<n){
if(pa[i+1][j]==UP) pa[++i][j]=LEFT;
else if(pa[i+1][j+1]==DIAG)
pa[++i][++j]=LEFT;
else ++j;
}
while(i<m&&pa[++i][j]==UP) pa[i][j]=LEFT;
}
int CLCS(const char *a, const char *b){
int m=strlen(a), n=strlen(b);
strcpy(AA,a); strcpy(AA+m,a);
LCS(AA,b,m*2,n);
int ans = dp[m][n];
for(int i=1; i<m; ++i){
reRoot(i,m*2,n);
ans=max(ans,trace(m+i,n));
}
return ans;
}
view raw reRoot.cpp hosted with ❤ by GitHub

作法大致上是將b字串變成原來的兩倍,然後做一般的LCS,但在過程中要注意更新答案的順序。接著修改DP來源表格,每次將來源表格的第一個row刪掉(reRoot函數),然後計算當前的答案。

當我AC這一題後,我看到了一個非常驚人的作法,也是用DP複雜度是\ord{nm},但是我目前還無法完全理解為何這樣是可行的,而且這樣的做法我也不知道如何回溯找出一組解,只能知道CLCS的長度,希望路過的大大們能提供想法:
#include<cstring>
#include<algorithm>
const int MAXN=1505;
int h[MAXN][MAXN*2], v[MAXN][MAXN*2];
char B[MAXN*2];
int CLCS(const char *a, const char *b){
int m = strlen(a), n = strlen(b);
strcpy(B,b); strcpy(B+n,b);
for(int j=0; j<n*2; ++j) h[0][j] = j+1;
for(int i=0; i<m; ++i){
for(int j=0; j<n*2; ++j){
if(a[i]==B[j]){
h[i+1][j] = v[i][j];
v[i][j+1] = h[i][j];
}else{
h[i+1][j] = std::max(h[i][j], v[i][j]);
v[i][j+1] = std::min(h[i][j], v[i][j]);
}
}
}
int ans=0;
for(int i=0; i<n; ++i){
int sum = 0;
for(int j=0; j<n; ++j)
if (h[m][j+i] <= i) ++sum;
ans = std::max(ans,sum);
}
return ans;
}
view raw HV.cpp hosted with ❤ by GitHub