go 分金幣,case和字元用法
多項式全家桶
打算開 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\)
時間複雜度僅有 \(\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^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\)