1. 程式人生 > 其它 >IDEA簡單好用外掛分享

IDEA簡單好用外掛分享

多項式全家桶

打算開 GF 這個萬惡之源了,但在此之前先把多項式的那堆板子理理清楚吧。程式碼沒有刻意卡常,而且寫成的年代不同,碼風和實現方法會有一點不一樣,板子也不會太全,之後會遇到問題會在這裡慢慢補充。

多項式乘法

給定一個 \(n\) 次多項式 \(F(x)=\sum_{i=0}^n f_ix^i\)\(m\) 次多項式 \(G(x)=\sum_{j=0}^m g_jx^j\),求出 \(F(x)\)\(G(x)\) 的卷積:

\[\sum_{i=0}^n\sum_{j=0}^m f_ig_jx^{i+j} \]

注意到多項式乘法的過程,單從卷積定義角度考慮是難以優化的,所以我們從多項式本身考慮。

多項式有兩種表示方法,點值表示法和係數表示法。其中係數表示法就是我們常用的形式:

\[F(x)=\{f_0,f_1,f_2,\cdot\cdot\cdot,f_n\} \]

其中 \(f_i\) 表示 \(x^i\) 前面的係數。這顯然可以唯一確定一個 \(n\) 次多項式。而點值表示法是這樣的:

\[F(x)=\{(x_0,F(x_0)),(x_1,F(x_1)),\cdot\cdot\cdot,(x_n,F(x_n))\} \]

注意到這 \(n+1\) 個點也能表示一個 \(n\) 次多項式,可以從高斯消元的角度考慮。

顯然我們常用的(題目輸入和要求輸出的)都是第一種表示方法,那第二種表示方法有啥用呢。對於兩個 \(n\)

次多項式 \(F(x),G(x)\),它們的卷積 \(H(x)\) 在點值表示法下能很輕易地被求出:

\[H(x)=\{(x_0,F(x_0)G(x_0)),(x_1,F(x_1)G(x_1)),\cdot\cdot\cdot,(x_n,F(x_n)G(x_n))\} \]

時間複雜度僅有 \(\mathcal{O}(n)\)。但問題是題目不認點值表示法,而暴力在兩種表示方法之間的轉化複雜度是很高的。所以我們現在的任務就是找到在兩種表示方法之間轉化的合適方法。

FFT/快速傅立葉變換

前置知識:複數。

可以做到在 \(\mathcal{O}(n\log n)\) 的時間複雜下完成點值表示法和係數表示法的相互轉化。

我們先來看從係數表示法到點值表示法,注意到這裡的瓶頸在於算 \(x^i\) 和計算 \(F(x^i)\)。這兩個問題本質上其實挺像的,都是要求尋找一個合適的 \(x\),使得 \(x^i\) 具有一些美妙的性質。

注意到 \(1,-1\) 的冪都是很好算的,但這樣僅僅夠我們算兩個點,一共可是需要 \(n+1\) 個。所以我們要更多的點,更具體地講,我們需要更多滿足 \(|\omega^k|=1\)\(\omega\)。可以發現需要引入複數了,\(i,-i\) 顯然是方程的解,除此之外,在單位圓上所有向量對應的複數都滿足:

(應該都能看出來是從 OI wiki 拿的圖吧)

嚴謹講,我們定義 \(x^n=1\)\(\mathbb{C}\) 中的解是 \(n\) 次復根。根據上圖,我們能發現這樣的解有 \(n\) 個,根據尤拉公式 \(e^{ix}=\cos x+i\sin x\),我們定義:

\[\omega_n=e^{\frac{2\pi i}{n}} \]

為單位復根,可以發現這個複數對應了把單位圓 \(n\) 等分的第一個角對應的向量。則 \(x^n=1\) 的解集能用 \(w_n\) 的冪表示:

\[\{\omega^k_n|k\in[0,n)\cap\mathbb{Z}\} \]

說了這麼多,真正要投入應用的話,我們還需要知道單位復根的一些性質。對於任意的正整數 \(n\) 和整數 \(k\),有:

\[\omega^n_n=1,w_n^k=\omega^{2k}_{2n},w_{2n}^{k+n}=-w_{2n}^k \]

均可以通過把複數看成向量,用幾何意義證明,不再贅述。而有了這些性質,就可以開始進入正片了。

FFT 的基本思路是分治,遞迴處理當 \(x=w_n^k\) 時,\(f(x)\) 的值。我們來舉個例子吧,\(8\) 項的多項式:

\[f(x)=a_0+a_1x+a_2x^2+\cdot\cdot\cdot+a_7x^7 \]

考慮奇偶分治,把式子按照下標的奇偶性分成兩坨:

\[\begin{aligned}f(x)&=(a_0+a_2x^2+a_4x^4+a_6x^6)+(a_1x^1+a_3x^3+a_5x^5+a_7x^7)\\&=(a_0+a_2x^2+a_4x^4+a_6x^6)+x(a_1+a_3x^2+a_5x^4+a_7x^6)\end{aligned} \]

分別對奇偶項建立新函式:

\[g(x)=a_0+a_2x+a_4x^2+a_6x^3,h(x)=a_1+a_3x+a_5x^2+a_7x^3 \]

則顯然有:

\[f(x)=g(x^2)+xh(x^2) \]

好了,現在就可以代入 \(\omega^k_n\) 了:

\[\begin{aligned}f(\omega^k_n)&=g((\omega^k_n)^2)+\omega^k_nh((\omega^k_n)^2)\\&=g(\omega^{2k}_{n})+\omega^k_nh(\omega^{2k}_{n})\\&=g(\omega_{\frac{n}{2}}^k)+\omega^k_nh(\omega^k_{\frac{n}{2}})\end{aligned} \]

好像還是看不出來什麼,再代入個 \(\omega^{k+\frac{n}{2}}_n\) 試試?推導過程省略,跟上面差不多,我們能得到:

\[f(\omega^{k+\frac{n}{2}}_n)=g(w_{\frac{n}{2}}^k)-\omega^k_nh(\omega^k_{\frac{n}{2}}) \]

好了,現在我們就完全能看出來了,求出 \(g(\omega_{\frac{n}{2}}^k),h(\omega^k_{\frac{n}{2}})\) 之後我們就能一次處理處兩個函式值!而處理這倆函式的過程又是一次遞迴的過程!這下找到了方向了,我們對於每個函式值,需要代入 \(n\) 個不同的值,每次會把多項式長度縮減 \(\frac{1}{2}\),所以最終複雜度就是 \(\mathcal{O}(n\log n)\)

值得注意的是,分治的時候需要保證每次分治兩邊長短相同,換句話說,需要 \(n=2^m,m\in\mathbb{N}\)。如果原多項式不滿足的話,就用 \(0\) 補就好了。

好了,現在我們能把係數表示法換成點值表示法了,愉快地把函式值相乘,然後呢?然後我們就需要把點值表示法換成係數表示法了。這一過程通常被稱為 IFFT,即逆 FFT。

考慮原本的多項式是 \(f(x)=\sum_{i=0}^{n-1} a_ix^i\),而現在我們已知 \(y_i=f(\omega_n^i),i\in[0,n)\cap\mathbb{Z}\),現在要求 \(\{a_0,a_1,\cdot\cdot\cdot,a_{n-1}\}\)。考慮設:

\[A(x)=\sum_{i=0}^{n-1}y_ix^i \]

現在我們就要在 \(A(x)\) 上面搞搞事了。

考慮將 \(\omega_n^{-i}\) 分別代入 \(A(x)\),則有:

\[\begin{aligned}A(\omega_n^{-k})&=\sum_{i=0}^{n-1}f(\omega_n^{i})\omega_n^{-ik}\\&=\sum_{i=0}^{n-1}\omega_n^{-ik}\sum_{j=0}^{n-1}a_j\omega^{ij}\\&=\sum_{i=0}^{n-1}\sum_{j=0}^{n-1}a_j\omega^{i(j-k)}\\&=\sum_{j=0}^{n-1}a_j\sum_{i=0}^{n-1}\omega^{i(j-k)}\end{aligned} \]

\(S(\omega_n^a)=\sum_{i=0}^{n-1}(\omega_n^{ai})\),則當 \(a\equiv0\pmod{n}\) 時,顯然 \(S(\omega_n^a)=n\)(因為 \(\omega_n^a=1\) 嘛)。

而當 \(a\not \equiv0\pmod{n}\) 時,考慮經典 trick 錯位相減:

\[\begin{aligned}S(\omega_n^a)&=\sum_{i=0}^{n-1}(\omega_n^{ai})\\\omega_n^{a}S(\omega_n^a)&=\sum_{i=1}^{n}(\omega_n^{ai})\\\therefore S(\omega_n^a)&=\dfrac{\omega_n^{an}-\omega_n^{a0}}{\omega_n^a-1}=0\end{aligned} \]

綜上,我們有:

\[S(\omega_n^a)=\begin{cases}n&a\equiv0\pmod{n}\\0&a\not \equiv0\pmod{n}\end{cases} \]

帶回原式:

\[A(\omega_n^{-k})=\sum_{j=0}^{n-1}a_jS(\omega_n^{j-k})=a_kn \]

非常神奇,如果令 \(b_k=\omega_n^{-k}\),則 \(A\) 的點值表示法就是:

\[\{(b_0,a_0n),(b_1,a_1n),\cdot\cdot\cdot,(b_{n-1},a_{n-1}n)\} \]

綜上,我們取單位根為其倒數,然後對 \(A\) 做一遍上述的 FFT 過程,再除以 \(n\) 就得到了係數表示法。

遞迴版實現:\(\tt code\)

好的現在您寫完了遞迴版,非常愉快地交了模板題,非常愉快地獲得了 \(\tt 100pts\),但這一切快樂都在您開啟提交記錄,按照執行時間排序後結束了,“他們的為啥跑的這麼快??”

直覺告訴我們是遞迴的鍋。遞迴帶來了大常數,讓本就因為實數運算常數不小的 FFT 雪上加霜。所以我們要考慮把它變成非遞迴版本。

考慮模擬演算法中遞迴分治的過程:

\[\{0,1,2,3,4,5,6,7\}\\\{0,2,4,6\}\{1,3,5,7\}\\\{0,4\}\{2,6\}\{1,5\}\{3,7\}\\\{0\}\{4\}\{2\}\{6\}\{1\}\{5\}\{3\}\{7\} \]

分完了。看不出來什麼?考慮把分治前的數和分治後的數都變成二進位制表示:

\[\{000,001,010,011,100,101,110,111\}\\\{000,100,010,110,001,101,011,111\} \]

這下明顯了吧,分治前後的數二進位制位是反過來的!那我們只需要提前把數交換到對應的位置,然後模擬遞迴的過程就好了。

現在的問題是怎麼把二進位制位給反過來。\(\mathcal{O}(n\log n)\) 固然是一種方法,不過我們能做到 \(\mathcal{O}(n)\)。考慮遞推,設 \(rev_x\) 表示 \(x\) 的二進位制位反過來的結果,顯然有 \(rev_0=0\)。而對於一個數 \(x\),如果除去最高位不算,\(rev_x=\left\lfloor\dfrac{rev_{\lfloor\frac{x}{2}\rfloor}}{2}\right\rfloor\),也就是把 \(x\) 右移一位的數反轉一下再右移一位。而對於最高位,它取決於 \(x\) 的奇偶性,所以有:

\[rev_x=\left\lfloor\dfrac{rev_{\lfloor\frac{x}{2}\rfloor}}{2}\right\rfloor+(x\bmod{2})2^{k-1} \]

其中 \(k\) 是二進位制表示下的位數。

非遞迴版實現:\(\tt code\)

NTT/快速數論變換

前置知識:原根

剛剛我們看到了 FFT,這裡還有一種思路類似的 NTT,是 FFT 在數論基礎上的實現。由於 FFT 涉及到大量的實數運算,丟精度,速度慢就成為了不可避免的問題。而 NTT 是實現在數論基礎上的,運算都是整數,準確度和速度都會更快,但時間複雜度依然是 \(\mathcal{O}(n\log n)\)

這倆的基本思路都差不多,而我們的關鍵就是在數論領域中找出一個東西來代替單位復根。我們發現,對於質數 \(p=qn+1(n=2^m)\),它的原根 \(g\) 恰好就滿足剛剛所說的性質:

\[g^{qn}\equiv1\pmod{p} \]

而如果我們把 \(g^q\) 記作 \(g_{n}\),會發現它也有類似的性質,如:

\[g_{n}^n\equiv1\pmod{p},g_{n}^{\frac{n}{2}}\equiv-1\pmod{p} \]

然後就結束了,只需要把 FFT 裡的單位復根扣掉換成原根就行了。常見的質數原根:

\[p=998,244,353=7\times17\times2^{23}+1,g=3 \]

其餘的質數可以參考求原根的方法考場現求。

實現:\(\tt code\)

分治 FFT/NTT

給出序列 \(g_{1\sim n}\),求出序列 \(f_{0\sim n}\),其中:

\[f_0=1,f_i=\sum_{j=1}^i f_{i-j}g_j \]

答案對 \(998,244,353\) 取模。

能看到很明顯的卷積特徵,但很遺憾,我們不能直接進行一個積的卷,因為對於 \(f_i\) 來說,它的值是依賴以前求出來的值的,換句話說,我們要求線上的卷積。(這也是為什麼這個演算法在國外有時也被稱為線上卷積)

考慮參考 \(\rm cdq\) 分治的思路,遞迴處理區間。先處理左區間,然後處理左區間對有區間的貢獻,最後處理右區間,這樣能保證每次卷積需要的值都已經被求出了。

具體來講,對於當前區間 \([l,r)\),我們先遞迴求出 \([l,mid)\)\(f\) 的值,然後將 \(f_{l\sim mid-1}\)\(g_{0\sim r-l-1}\) 給捲起來,這樣能得到左邊對右邊 \(f_{mid,r-1}\) 的貢獻。之後遞迴處理右區間 \([mid,r)\) 即可。時間複雜度:

\[T(n)=T\left(\dfrac{n}{2}\right)+\mathcal{O}(n\log n)=\mathcal{O}(n\log n) \]

實現:\(\tt code\)

注意卷積之前要補 \(0\) 清空而不是不管!

MTT

任意模數 NTT,不會。

多項式牛頓迭代

給出多項式 \(g(x)\),已知存在一個多項式 \(f(x)\),滿足:

\[g(f(x))\equiv0\pmod{x^n} \]

求出模 \(x^n\) 意義下的 \(f(x)\)

考慮倍增的思路。首先考慮邊界,對於 \(n=1\) 的情況,我們需要單獨求出 \([x^0]g(f(x))=0\) 的解。然後對於其餘的 \(n\),我們考慮先遞迴計算 \(\left\lceil\dfrac{n}{2}\right\rceil\) 的情況,假設得到在模 \(x^{\lceil\frac{n}{2}\rceil}\) 意義下的解是 \(f_0(x)\),則現在我們的目標就是要用 \(f_0(x)\) 表示 \(f(x)\)

考慮將 \(g(f(x))\)\(f_0(x)\) 處泰勒展開,則原同餘方程可化為:

\[\sum_{i\ge 0}\dfrac{g^{(i)}(f_0(x))}{i!}(f(x)-f_0(x))^i\equiv0\pmod{x^n} \]

其中 \(g^{(i)}\) 表示 \(g\)\(i\) 階導。注意到 \(f(x)-f_0(x)\) 這個式子的最低非 \(0\) 項次數最低是 \(\lceil\frac{n}{2}\rceil\),因為在這之前它們都一樣,所以有:

\[\forall i\ge 2:(f(x)-f_0(x))^i\equiv0\pmod{x^n} \]

這樣,原同餘方程又能化為:

\[g(f_0(x))+g'(f_0(x))(f(x)-f_0(x))\equiv0\pmod{x^n} \]

簡單的代數變化:

\[f(x)\equiv f_0(x)-\dfrac{g(f_0(x))}{g'(f_0(x))}\pmod{x^n} \]

這下式子有了,至於怎麼求,怎麼用,且聽下文分解。

多項式求逆

給定一個多項式 \(f(x)\),求出一個多項式 \(g(x)\) 滿足:

\[f(x)g(x)\equiv1\pmod{x^n} \]

係數對 \(998,244,353\) 取模。

考慮套剛剛的多項式牛頓迭代。為了避免跟上文的符號矛盾,我們記待求逆的函式為 \(h(x)\),求逆結果函式為 \(f(x)\),則有:

\[g(f(x))=\dfrac{1}{f(x)}-h(x)\equiv0\pmod{x^n} \]

套牛頓迭代的式子有:

\[\begin{aligned}f(x)&\equiv f_0(x)-\dfrac{\frac{1}{f_0(x)}-h(x)}{-\frac{1}{f^2_0(x)}}\pmod{x^n}\\&\equiv f_0(x)(2-f_0(x)h(x))\pmod{x^n}\end{aligned} \]

注意這裡求導的時候把 \(h(x)\) 當成常數來做了,然後就結束了,時間複雜度:

\[T(n)=T\left(\dfrac{n}{2}\right)+\mathcal{O}(n\log n)=\mathcal{O}(n\log n) \]

實現:\(\tt code\)

注意,在實現牛頓迭代的時候,類似分治 FFT,卷積之前一定要記得清空不用的!

多項式開根

給出一個 \(n-1\) 次多項式 \(A(x)\),找出一個模 \(x^n\) 意義下的多項式 \(B(x)\) 使得

\[B^2(x)\equiv A(x)\pmod{x^n} \]

係數對 \(998,244,353\) 取模。

依然考慮牛頓迭代。類似求逆的推導過程,我們依然記 \(h(x)\) 為待開根函式,\(f(x)\) 為答案,則有:

\[g(f(x))=f^2(x)-h(x)\equiv0\pmod{x^n} \]

還是套牛頓迭代的式子:

\[\begin{aligned}f(x)&\equiv f_0(x)-\dfrac{f_0^2(x)-h(x)}{2f_0(x)}\pmod{x^n}\\&\equiv\dfrac{f_0^2(x)+h(x)}{2f_0(x)}\end{aligned} \]

多項式求逆就可以做不帶餘數的除法了。類似求逆,時間複雜度 \(\mathcal{O}(n\log n)\)

實現:\(\tt code\)

多項式求導/積分

這個比較簡單了,主要是一些公式。

首先由於求導和積分的線性性:

\[\begin{aligned}(f(x)+g(x))'&=f'(x)+g'(x)\\(cf(x))'&=cf'(x)\\\int(f(x)+g(x))&=\int f(x)+\int g(x)\\\int cf(x)&=c\int f(x)\end{aligned} \]

所以對於多項式 \(F(x)=\sum_{i=0}^{n-1}a_ix^i\),它的導數和積分分別為:

\[\begin{aligned}F'(x)=\sum_{i=1}^{n-1}a_iix^{i-1}\\\int F(x)=\sum_{i=0}^{n-2}\dfrac{a_ix^{i+1}}{i+1}\end{aligned} \]

可以 \(\mathcal{O}(n)\) 求解。

還有一些比較常用的求導公式:

\[\begin{aligned}(F(x)G(x))'&=F'(x)G(x)+F(x)G'(x)\\\left(\dfrac{F(x)}{G(x)}\right)'&=\dfrac{F'(x)G(x)-F(x)G'(x)}{G^2(x)}\\(G(F(x)))'&=G'(F(x))F'(x)\end{aligned} \]

更多的東西比如積分公式啊,更多的求導公式啊,常見的函式的積分導數啊,建議大概學一下微積分。

多項式帶餘除法

給出一個 \(n\) 次多項式 \(F(x)\) 和一個 \(m\) 次多項式 \(G(x)\),求出多項式 \(Q(x),R(x)\) 滿足以下條件:

  • \(Q(x)\) 次數為 \(n-m\)\(R(x)\) 次數小於 \(m\)
  • \(F(x)=Q(x)G(x)+R(x)\)

係數對 \(998,244,353\) 取模。

剛剛其實我們也做過分數運算(在牛頓迭代那一塊),但這裡不一樣的是,題目還要求求出 \(R(x)\),即餘數。這不太好辦,所以考慮消去 \(R(x)\) 的影響。

有個很妙的思想 (不知道咋想到的) ,考慮構造多項式 \(F^R(x)\)

\[F^R(x)=x^nF\left(\dfrac{1}{x}\right) \]

容易想到 \(F^R(x)\) 的實質其實就把係數反轉一下。順著這個思路,將帶餘除法的等式兩邊同時乘上 \(x^n\) 並把函式裡的自變數都改為 \(\frac{1}{x}\) 有“

\[\begin{aligned}x^nF\left(\frac{1}{x}\right)&=x^{n-m}Q\left(\frac{1}{x}\right)x^{m}G\left(\frac{1}{x}\right)+x^{n-m+1}x^{m-1}R\left(\frac{1}{x}\right)\\F^R(x)&=Q^R(x)G^R(x)+x^{n-m+1}R^R(x)\end{aligned} \]

誒,這個式子就特殊了,注意到只有 \(R^R(x)\) 這一項前面有一個 \(x^{n-m+1}\),那我們只需要把上式放在模 \(x^{n-m+1}\) 意義下不就把 \(R^R(x)\) 幹掉了嗎,即:

\[F^R(x)\equiv Q^R(x)G^R(x)\pmod{x^{n-m+1}} \]

問題是,這會不會對 \(Q^R(x)\) 造成影響導致我們求出的值不精確呢?顯然不會,因為 \(Q^R(x)\) 的次數僅有 \(n-m\),小於模數的 \(n-m+1\) 次,所以 \(Q^R(x)\) 並不會受到影響。

這樣求個逆就能把 \(Q(x)\) 求出來了。求出來之後再反代回去就有 \(R(x)\) 了。時間複雜度 \(\mathcal{O}(n\log n)\)

實現:\(\tt code\)

多項式 ln

給出 \(n-1\) 次多項式 \(A(x)\),求一個模 \(x^n\) 意義下的多項式 \(B(x)\) 滿足:

\[B(x)\equiv \ln A(x)\pmod{x^n} \]

係數對 \(998,244,353\) 取模。

考慮設 \(G(x)=\ln x\),則原題相當於求 \(G(F(x))\),考慮給這玩意求個導:

\[(G(F(x)))'=G'(F(x))F'(x) \]

由於,

\[(\ln x)'=\dfrac{1}{x} \]

則上式為:

\[(G(F(x)))'=\dfrac{F'(x)}{F(x)} \]

這樣我們就能求出帶求多項式的導數了,之後只需要積分回來即可:

\[\int \dfrac{F'(x)}{F(x)} \]

這樣直接求導+求逆+積分這題就做完了。時間複雜度 \(\mathcal{O}(n\log n)\)

實現:\(\tt code\)

多項式 Exp

給出 \(n-1\) 次多項式 \(A(x)\),求一個模 \(x^n\) 意義下的多項式 \(B(x)\) 滿足:

\[B(x)\equiv e^{A(x)}\pmod{x^n} \]

係數對 \(998,244,353\) 取模。

考慮多項式牛頓迭代法。記 \(A(x)\)\(h(x)\)\(B(x)\)\(f(x)\),則有:

\[g(f(x))=\ln f(x)-h(x)\equiv0\pmod{x^n} \]

套式子:

\[\begin{aligned}f(x)&=f_0(x)-\dfrac{\ln f_0(x)-h(x)}{\frac{1}{f_0(x)}}\\&=f_0(x)(1-\ln f_0(x)+h(x))\end{aligned} \]

求個 \(\ln\),再加加減減後跟其他的乘起來就完事了。時間複雜度 \(\mathcal{O}(n\log n)\)

實現:\(\tt code\)