1. 程式人生 > 實用技巧 >多項式全家桶——Part.2 多項式位運算

多項式全家桶——Part.2 多項式位運算

此划水文全為結論、板子,證明還得看大爺證明。

我們可以用FFT、NTT計算多項式乘法:

\[C(k)=\sum_{i+j=k}A(i)*B(j) \]

但是計算不了這種玩意:

\[C(k)=\sum_{i|j=k}A(i)*B(j) \]

或者這種東東:

\[C(k)=\sum_{i\&j=k}A(i)*B(j) \]

或者這種:

\[C(k)=\sum_{i\ xor\ j=k}A(i)*B(j) \]

所以一個演算法應著時代的需求誕生了:

多項式位運算

前言

(WTF) FWT是一個神奇的演算法,它的名字叫做“快速沃爾什變換”。
雖然不知道百度百科寫的是smg,但是網上部落格已經足夠多以至於讓人學懂了。
話說我以前還不知不解地打過這個板子。現在學感覺當時浪費了這麼好的一個演算法。

或運算

過程

多項式或運算是用來快速做這條式子的:

\[C(i)=\sum_{i=k|j}A(j)*B(k) \]

其具體思想其實和FFT差不多,首先先把多項式A轉化為點值表達,然後再快速運算。
然後再轉回插值。這樣完成一個演算法流程。然鵝我們如何快速DFT這個醜陋的式子呢?
拿出流程圖:

一樣三部曲:

如何找出類似於FFT的點值乘法

我們這裡並不考慮去什麼點值插值,我們只需要一些簡單的數論變換即可得到我們所要求的東東。
同樣的思路,考慮把A轉化成另外一個東東,假設叫做\(FWT(A)\),其滿足可以快速求出\(FWT(C)=FWT(A)*FWT(B)\)。算出來之後還可以快速轉化回\(A\)

,也就是\(IFWT\)逆變換。

首先我們觀察柿子:\(i|j=k\),我們發現,由於是或運算,看到或運算就應該拆位(這個套路真的好用)。
那麼拆完位就可以得到一個結論:如果將k拆位完後,得到1位置集合。那麼i的1位置集合與j的1位置集合即為k的1位置集合的子集。
得到這個結論後,我們即可構造:

\[FWT(A)=\sum_{i=i|j}A(j) \]

這條柿子的意義即為:j的1的位置集合為i的1的位置集合的子集。
(其實為什麼這樣構造,理由是要去進行一波反演得到的。這裡就8推了,記結論)
那麼如果把\(FWT(A)\)\(FWT(B)\)乘起來會變成什麼呢?

\[FWT(A)*FWT(B)=\sum_{i=i|j}A(j)*\sum_{i=i|k}B(k)\\=\sum_{i=i|j}\sum_{i=i|k}A(j)*B(k)\\=\sum_{i=i|(j|k)}A(j)*B(k)\\=FWT(C) \]

那麼就有方向了。如果現在已經得到一個新的東東\(FWT(A)\),那麼答案就可以在\(O(n)\)時間內求出。
問題是如何快速求出\(FWT(A)\)

如何找出FWT正變換

同樣考慮像FFT一樣分治求之。

那麼遞迴可能會慢。怎麼辦?能不能像FFT一樣把下標取出掘其規律?
那就試試。(逝世)

還記得這張神圖把。
考慮設a0表示當前分治到的當前位為0的序列,a1表示當前分治到的當前位為1的序列。
再回顧\(FWT(A)\)的定義:

\[FWT(A)=\sum_{i=i|j}A(j) \]

那麼可以知道在對應位置的a0永遠是a1的子集。
所以可以寫成這個式子:

\[FWT(A)=(FWT(A0),FWT(A1+A0)) \]

其中\(A1+A0\)表示對應位置相加。

於是我們的FWT正變換就完成了。

如何找出FWT逆變換(IFWT)

這個直接理解還挺簡單的。我們發現,由於FWT是求子集和,現在要求的是當前值。
所以把子集減去即可。
於是寫成這個式子:

\[FWT(A)=(FWT(A0),FWT(A1-A0)) \]

後話
話說其實FWT可以有多種方法來看。
網上有的說這其實是FMT(快速莫比烏斯變換)(其實這玩意我真的沒搞懂它和反演有什麼區別),我一開始看Vfleaking的反演覺得是個子集反演用分治來做。
然鵝比較簡單的理解就是從位運算的意義上理解,可以推柿子來比較嚴謹地去證明,當然也可以我這樣直接理解地去推結論。
一個問題有多種角度去看,有時是一件好事,有時卻是一件壞事,因為可能會莫衷一是,一開始學會陷入理解的困境。
不管怎樣,還是挺好玩的。就好像3blue1brown讓我學會的一個道理,去發現數學的美,而不是去死板地認為這個就是一個工具。
誒我tm怎麼莫名其妙就放了怎麼多沒用的屁話

結論

\[FWT(A)=(FWT(A0),FWT(A1+A0)) \]

\[IFWT(A)=(IFWT(A0),IFWT(A1-A0)) \]

程式碼

void orfwt(int a[],int inv)
{
	long long op,oq;
	for (int len=2;len<=m;len<<=1)
	{
		int mid=len/2;
		for (int j=0;j<mid;j++)
		{
			for (int k=j;k<m;k+=len)
			{
				op=inv*a[k];
				oq=a[k+mid];
				op=(op+oq+mo)%mo;
				a[k+mid]=op;
			}
		}
	}
}

和運算

過程

這個過程和或運算的過程其實長得基本一樣,就不再推一遍了。

結論

\[FWT(A)=(FWT(A0+A1),FWT(A1)) \]

\[IFWT(A)=(IFWT(A0-A1),IFWT(A1)) \]

程式碼
void andfwt(int a[],int inv)
{
	long long op,oq;
	for (int len=2;len<=m;len<<=1)
	{
		int mid=len/2;
		for (int j=0;j<mid;j++)
		{
			for (int k=j;k<m;k+=len)
			{
				op=a[k];
				oq=inv*a[k+mid];
				op=(op+oq+mo)%mo;
				a[k]=op;
			}
		}
	}
}

異或運算

過程

這個其實和上面的思路差不多,但是構造的東東還是很不同的。

如何找出類似於FFT的點值乘法

考慮設一個函式\(f(x)\)表示當前\(x\)在二進位制表示下1的數量的奇偶性。
則有:

\[FWT(A)=\sum_{f(i\&j)=0}A(j)-\sum_{f(i\&j)=1}A(j) \]

這玩意當然滿足\(FWT(C)=FWT(A)*FWT(B)\)
理由就是把這條柿子帶進去,然後再瞎換一下即可得到。
當然,在推柿子的時候可能要用到一個結論:\(f(i\&j)\ xor\ f(i\&k)=f(i\&(j\ xor\ k))\)
這裡8推了。

如何找出FWT正變換
考慮如何求\(FWT(A)\)
觀察柿子:

\[FWT(A)=\sum_{f(i\&j)=0}A(j)-\sum_{f(i\&j)=1}A(j) \]

由於是位運算,那麼繼續套路,拆位。
考慮當前第i位,這一位就有4種情況:

  • 0 xor 0=0,奇偶性不會改變。
  • 0 xor 1=1,奇偶性不會改變。
  • 1 xor 0=1,奇偶性不會改變。
  • 1 xor 1=0,這時奇偶性改變。

這意味著什麼呢?
假如把\(FWT(A)\)取個絕對值(當然這樣計算最後的答案是不會改變的),那麼就可以把奇偶性改變都看做是減去貢獻,反之則為加上貢獻。

那麼正變換就得到了:

\[FWT(A)=(FWT(A0+A1),FWT(A0-A1)) \]

如何找出FWT逆變換(IFWT)

逆變換一樣簡單,把上面的計算貢獻反過來即可。

\[IFWT(A)=(\frac{IFWT(A0+A1)}2,\frac{IFWT(A0-A1)}2) \]

結論

\[FWT(A)=(FWT(A0+A1),FWT(A0-A1)) \]

\[IFWT(A)=(\frac{IFWT(A0+A1)}2,\frac{IFWT(A0-A1)}2) \]

程式碼
void xorfwt(int a[],int inv)
{
	long long op,oq,kk;
	for (int len=2;len<=m;len<<=1)
	{
		int mid=len/2;
		for (int j=0;j<mid;j++)
		{
			for (int k=j;k<m;k+=len)
			{
				if (inv==1)
				{
					op=a[k];
					oq=a[k+mid];
					kk=(op+oq+mo)%mo;a[k]=kk;
					kk=(op-oq+mo)%mo;a[k+mid]=kk;
				}
				else
				{
					op=a[k];
					oq=a[k+mid];
					kk=(op+oq+mo)%mo*inv2%mo;a[k]=kk;
					kk=(op-oq+mo)%mo*inv2%mo;a[k+mid]=kk;
				}
			}
		}
	}
}

總程式碼

#include <iostream>
#include <cstdio>
#include <cmath>
#include <cstring>
using namespace std;
const long long mo=998244353;
const long long inv2=499122177;

int n,m,a[131072],b[131072],ja[131072],jb[131072];

void orfwt(int a[],int inv)
{
	long long op,oq;
	for (int len=2;len<=m;len<<=1)
	{
		int mid=len/2;
		for (int j=0;j<mid;j++)
		{
			for (int k=j;k<m;k+=len)
			{
				op=inv*a[k];
				oq=a[k+mid];
				op=(op+oq+mo)%mo;
				a[k+mid]=op;
			}
		}
	}
}

void andfwt(int a[],int inv)
{
	long long op,oq;
	for (int len=2;len<=m;len<<=1)
	{
		int mid=len/2;
		for (int j=0;j<mid;j++)
		{
			for (int k=j;k<m;k+=len)
			{
				op=a[k];
				oq=inv*a[k+mid];
				op=(op+oq+mo)%mo;
				a[k]=op;
			}
		}
	}
}

void xorfwt(int a[],int inv)
{
	long long op,oq,kk;
	for (int len=2;len<=m;len<<=1)
	{
		int mid=len/2;
		for (int j=0;j<mid;j++)
		{
			for (int k=j;k<m;k+=len)
			{
				if (inv==1)
				{
					op=a[k];
					oq=a[k+mid];
					kk=(op+oq+mo)%mo;a[k]=kk;
					kk=(op-oq+mo)%mo;a[k+mid]=kk;
				}
				else
				{
					op=a[k];
					oq=a[k+mid];
					kk=(op+oq+mo)%mo*inv2%mo;a[k]=kk;
					kk=(op-oq+mo)%mo*inv2%mo;a[k+mid]=kk;
				}
			}
		}
	}
}

void solve(int a[],int b[],int ki)
{
	if (ki==1) orfwt(a,1),orfwt(b,1);
	else if (ki==2) andfwt(a,1),andfwt(b,1);
	else xorfwt(a,1),xorfwt(b,1);
	long long op,oq;
	for (int i=0;i<m;i++)
	{
		op=a[i];
		oq=b[i];
		op=op*oq%mo;
		a[i]=op;
	}
	if (ki==1) orfwt(a,-1);
	else if (ki==2) andfwt(a,-1);
	else xorfwt(a,-1);
	for (int i=0;i<m;i++)
	{
		printf("%d ",a[i]);
	}
	printf("\n");
}

int main()
{
	scanf("%d",&n);
	m=1<<n;
	for (int i=0;i<m;i++)
	{
		scanf("%d",&ja[i]);
	}
	for (int i=0;i<m;i++)
	{
		scanf("%d",&jb[i]);
	}
	for (int i=1;i<=3;i++)
	{
		memcpy(a,ja,sizeof(a));
		memcpy(b,jb,sizeof(b));
		solve(a,b,i);
	}
}

學習資料:
http://oi-wiki.com/math/poly/fwt/
https://blog.csdn.net/zhouyuheng2003/article/details/85950280
https://blog.csdn.net/hzj1054689699/article/details/83340154
https://www.cnblogs.com/cjyyb/p/9065615.html
http://blog.leanote.com/post/rockdu/TX20
https://blog.csdn.net/zhouyuheng2003/article/details/84728063
https://zhuanlan.zhihu.com/p/41867199
https://www.cnblogs.com/wjyyy/p/FWT.html