FWT 學習筆記


FWT 學習筆記

想盡量講得本質一點。

首先有一個引出問題叫做 集合冪級數

\[c_i=\sum_{j \ opt\ k=i}a_jb_k \]

其中,\(opt\) 是集合的並交補運算,而 \(i,j,k\) 也都是集合的意思

當我們把 \(i,j,k\) 看成二進制表示,那么集合中的每一個元素的選/不選對應二進制的 \(1/0\)\(opt\) 變成了 \(or,and,xor\) 的一種

所以問題變成了這樣:給定兩個長度為 \(2^n\) 的序列 \(a_i,b_i\) (不夠長用 \(0\) 補全),求出一個序列 \(c\) ,滿足 \(\forall i,c_i=\sum_{j \ opt\ k=i}a_jb_k\)

這個時候我們可以使用,FWT ,\(\text{Fast Walsh-Hadamard Transform}\) ,快速沃爾什變換。

FFT

發現 \(fwt\)\(fft\) 的英文很像,所以我們考慮,類似的思路

關於 \(fft\) ,我們有一個經典思路是:(圖源:pyb 的 ppt)

其中,我們能做到 \(O(n\log n)\)\(DFT\) 的原因在於我們有兩個重要操作叫做 奇偶分段 蝴蝶變換 ,也就是說我們把原來的東西進行分治的操作后可以 \(O(1)\) 的合並回去,\(O(n\log n)\)\(IDFT\) 在於一個 \(w\) 的性質的應用,我們不妨嘗試把這樣的思想套進 FWT

記住上圖!

FWT

現在我們有這樣的目標:找到一種變換 \(FWT\) ,使得

\[FWT(A)\times FWT(B)=FWT(C) \]

其中,\(\times\) 表示對應位相乘,然后通過 \(FWT(C)\) 復原 \(C\)

顯然,我們的 FWT 應該是對於原序列的一種線性組合,即某一項只是若干個原多項式中的若干項的若干倍的和

因為我們本來的 \(c_i\) 就是數個 \(a_jb_k\) 的和,而 FWT 是對應位相乘,如果其出現原多項式某幾項的乘積,那么顯然 GG

所以顯然有的是 \(FWT(A+B)=FWT(A)+FWT(B)\)

這里不妨約定一些記號:

$ 𝐴 = 𝑎_𝑖 ,𝐵 = 𝑏_𝑖 , 𝐶 = 𝑐_𝑖 $

$ 𝐴 + 𝐵 = 𝑎_0 + 𝑏_0, 𝑎_1 + 𝑏_1,\dots $

$ 𝐴 − 𝐵 = 𝑎_0 − 𝑏_0, 𝑎_1 − 𝑏_1,\dots$

$ 𝐴\oplus𝐵 = \sum_{𝑖⨁𝑗=0} 𝑎_𝑖 ∗ 𝑏_𝑗 , \sum_{𝑖⨁𝑗=1} 𝑎_𝑖 ∗ 𝑏_𝑗 ,\dots$

默認所有的數列都有 \(2^n\) 項,即可以理解為集合元素個數有 \(𝑛\)

$ 𝐴_0$ 代表數列 \(𝐴\) 的前 \(2^{𝑛−1}\) 項,即最高位(第一個)不選的所有集合,\(𝐴_1\) 代表數列 \(𝐴\) 中的后 \(2 ^{𝑛−1}\) ,即最高位(第一個)選的所有集合

FWTor

假設現在我們的 \(opt\) 就是 \(or\) ,下面簡記 \(A|B=\sum_{j|k=i} a_jb_k\)

下面講得可能有點小亂

FWT

總體想法是仿照 FFT ,考慮一個遞歸的形式構造出 FWT

顯然,當 \(n=0\) 的時候,\(FWT(A)=A\)

下面討論 \(n>0\) 的情況,我們首先考慮奇偶分段,假裝我們已經知道了 \(FWT(A_0),FWT(A_1)\)

於是,一個粗略的 FWT 出現了: \(FWT(A)=Merge(FWT(A_0),FWT(A_1))\)

\(Merge\) 表示直接拼接,xjb 舉一個例子:\(Merge(998,244)=998244\)

我們知道 \(FWT(C)\) 需要等於 \(FWT(A)\times FWT(B)\)

還知道 \(C_0=A_0B_0\) ,$ C_1=A_0B_1+A_1B_0+A_1 B_1$

發現上面那個顯然不靠譜,我們對應位相乘后,只會得到 \(FWT(C_0)=FWT(A_0)FWT(B_0),FWT(C_1)=FWT(A_1)FWT(B_1)\) 這樣一個錯誤。

也就是說后半部分還需要有 \(FWT(A_0)\) 的信息甩進去,所以:(這里不妨將 FWT 當成一個神秘的抽象函數)

\[FWT(A)= \begin{cases} Merge(FWT(A_0),FWT(A_0)+FWT(A_1))&,n>0\\ \quad \quad\quad\quad\quad\quad\quad\quad A&,n=0 \end{cases} \]

(回顧 \(+\) 的記號:$ 𝐴 + 𝐵 = 𝑎_0 + 𝑏_0, 𝑎_1 + 𝑏_1,\dots $)

可是這樣還是有問題呀,這樣對應位相乘,好像會多一個 \(FWT(A_0)FWT(B_0)\) 項的貢獻出現在 \(FWT(C_1)\)

但值得注意的是, \(FWT(C)\) 並不一定需要就是 \(C\) 了,我們不妨將這個問題交給 \(IFWT\) 處理,

至少我們這里保證了信息是完全的,並且符合我們心目中奇偶分段和蝴蝶變換的要求

也就是說像這樣給出來后,IFWT 有操作空間,並且 FWT 時間的確是 \(n\log n\)

當然,我們還需要保證 \(FWT(A)\times FWT(B)=FWT(C)\)

這里采用類似數學歸納法的方式證明,思路來自 pyb 的 ppt ,

即假設已知 \(FWT(A_0)\times FWT(B_0)=FWT(C_0),FWT(A_0)FWT(B_1)+FWT(A_1)FWT(B_0),FWT(A_1)FWT(B_1)=FWT(C_1)\) ,那么

\[\begin{align} FWT(A)\times FWT(B)&=Merge(FWT(A_0),FWT(A_0+A_1))\times Merge(FWT(B_0),FWT(B_0+B_1))\\ &=Merge(FWT(A_0)FWT(B_0),FWT(A_0)FWT(B_0)+FWT(A_0)FWT(B_1)+FWT(A_1)FWT(B_0),FWT(A_1)FWT(B_1))\\ &=Merge(FWT(A_0B_0),FWT(A_0B_0+A_0B_1+A_1B_0+A_1B_1))\\ &=FWT(Merge(A_0B_0,A_0B_1+A_1B_0+A_1B_1))\\ &=FWT(C) \end{align} \]

小心 \(Merge\) 的括號打的位置

第一個等號:按 FWT 定義展開

第二個等號:按 \(\times\) 定義對應位相乘

第三個等號:利用歸納法得到的結論

第四個等號:把 \(A_0B_0\) 理解成 \(A^{\prime}_0\) , \(A_0B_1+A_1B_0+A_1B_1\) 理解成 \(A^{\prime}_1\)

第五個等號:前文在定義 Merge 的后面一點點所述:

還知道 \(C_0=A_0B_0\) ,$ C_1=A_0B_1+A_1B_0+A_1 B_1$

所以我們歸納證明了 \(FWT(A)\times FWT(B)=FWT(C)\)

IFWT

(小劇場)

FWT: 至高無上的 IFWT !吾交給汝一個使命:干掉 $FWT(A) \times FWT(B) $ 之后出現在 \(FWT(C_1)\) 中的 \(FWT(A_0B_0)\) 項!主不需要它!

IFWT:(心里mmp:彩筆 FWT)哈哈哈!看我容斥!

(突然發現自己好智障)

直接給出來:

\[IFWT(A)=Merge(IFWT(A_0),IFWT(A_1-A_0)) \]

其中傳入的 \(A\) 是經歷了 \(FWT\) 的產物,小證一手:

\(FWT(A)\times FWT(B)=FWT(C_0)+FWT(C_0+C_1)\)

所以:

\(IFWT(FWT(C_0)+FWT(C_0+C_1))=Merge(IFWT(FWT(C_0)),IFWT(C_0+C_1-C_0))=Merge(IFWT(FWT(C_0)),IFWT(FWT(C_1)))=C\)

FWT 的本質

我們考慮 FWT 的代碼怎么寫,這里再嫖一張 pyb 的 ppt

發現 \(FWT\) 做的事情就是不斷地讓箭頭的起點加到箭頭的終點,按照紅綠藍的順序一層一層地加,可以有代碼:

void FWT(ll *A){
	for(int i=2;i<=N;i<<=1)         //i 線段長度
		for(int p=i>>1,j=0;j<N;j+=i)//j 哪一個部分
			for(int k=j;k<j+p;++k)  //k 嫖取信息
				A[k+p]+=A[k];
}

我們仔細觀察這些箭頭究竟都干了什么:

對於 \(111\) 而言,它嫖走了 \(110,101,011\) 的信息

對於 \(110\) 而言,它嫖走了 \(101,010\) 的信息

對於 \(101\) 而言,它嫖走了 \(100,001\) 的信息

對於 \(100\) 而言,它飄走了 \(000\) 的信息

你發現了嗎?也就是說,每個位置獲取的是它二進制少掉一個 \(1\) 后的位置的信息

而在獲取這些少掉了 \(1\) 的位置的信息之前,這些少掉了 \(1\) 的位置也獲取了它們所需要的信息,所以,我們剛剛不是說 \(FWT\) 可以看成一個“抽象函數”嗎,它的“解析式”其實長這樣:

\[FWT(A)_i=\sum_{j\subseteq i}a_j \]

\(FWT(A)_i\) 表示 \(FWT(A)\) 的第 \(i\) 項,這里之所以用 \(\subseteq\) 是因為 FWT 本身其實就是在解決“集合冪級數”。

可以帶入 \(FWT(A)\times FWT(B)\) 驗證一下看是不是就是 \(FWT(C)\)

所以,我們也可以從另外一個角度去證明 FWT 的時間復雜度,即是 \(\sum_{0}^{1<<n}popcount(i)=n2^{n-1}\)

上面給出 \(FWT\) 的代碼,其實很容易寫出 \(IFWT\) 的代碼:

void IFWT(ll *A){
	for(int i=2;i<=N;i<<=1)         
		for(int p=i>>1,j=0;j<N;j+=i)
			for(int k=j;k<j+p;++k)
				A[k+p]-=A[k];//唯一不同點
}

所以,我們容易把它整合起來:

void FWT(ll *A,int op){
	for(int i=2;i<=N;i<<=1)         //i 線段長度
		for(int p=i>>1,j=0;j<N;j+=i)//j 哪一個部分
			for(int k=j;k<j+p;++k)  //k 嫖取信息
				A[k+p]+=op*A[k];
}

后續部分直到下一個黑標題,都有點東西寫錯了,看一樂就好

非常值得一提的是,既然 FWT 又名莫比烏斯變換,我們不妨探究一下他和莫比烏斯反演的關系

我們考慮這個高維前綴和的形式:\(F(S)=\sum _{T\subseteq S} f(T)\)

我們說,與之對應的高維差分:\(f(S)=\sum _{T\subseteq S} (-1)^{|S \backslash T|}F(T)\) 就是莫比烏斯反演

因為我們的高維前綴和實際上解決的是這樣的問題:對於兩個不同的組合對象,從對象 1 中選出 \(S\)

的方案數為 \(f(S)\) ,從對象 2 中選出 \(S\) 的方案書是 \(g(S)\) ,我們想要計算的是 \((f*g)(S)=\sum _{T_1\cup T_2 \subseteq S}f(T_1)g(T_2)\) 的值

發現實際上能產生貢獻的 \((T_1,T_2)\) 需要滿足如下條件:

  • \(T_1\subseteq S,T_2 \subseteq S\)
  • \(\forall x\in S , s.t. x\in T_1 \cup x\in T_2\)

發現第一個條件是比較喜歡的,因為實際上它意味着所有在 \(T\) 中的元素都在 \(S\) 中,限制比較強烈

而第二個條件涉及到一個 \(or\) 的問題,比較麻煩,我們考慮把這個條件容斥掉

條件的否定是: \(\exist x\in S ,s.t. x\notin T_1 \and x\notin T_2\)

考慮枚舉 \(S^{\prime }\) 使得 \(\exist x\in S^{\prime}\) 滿足上述條件,考慮此時 \(T_1,T_2\) 滿足的條件,有

\[\begin{align*} (f*g)(S)&=\sum_{S^{\prime}\in S} (-1)^{|S\backslash S^{\prime}|}\sum f(\subseteq S^{\prime}) \sum g(\subseteq S^{\prime})\\ &= \sum _{T\in S} (-1)^{|S\backslash T|}F(S)· G(S) \end{align*} \]

\(F(S),G(S)\) 就是高維前綴和

所以:並卷積 \((f∗g)(S)=∑_{T_1∪T_2=S}f(T_1 )g(T_2 )\) 的高維前綴和為 \(F⋅G\)

\(f∗g\)\(F⋅G\) 的高維差分

那莫比烏斯反演和上面這些有什么關系呢

莫比烏斯反演:

若 $g=1*f $ ,構造函數 \(\mu\) ,使得 \(1*\mu = \in\) ,有 \(f=\mu *g\)

寫開:\(g(n)=\sum _{d|n} f(d)\)

考慮使用多元組來表示一個數,對於一個 \(n=\prod p_i^{a_i}\) ,我們使用 \((a_1,a_2,\dots ,a_r)\) 來表達

重定義 \(\subseteq\) 符號表示對於兩個二元組 \((a_i) ,(b_i)\) ,若 \(\forall i, a_i\le b_i\) ,那么 \((a_i)\subseteq (b_i)\)

那么,可以表示成: \(g(S)=\sum _{T\in S} f(T)\) ,其實就是高維前綴和

所以對應的反演形式,也就是 \(f(n)=\sum _{d|n} \mu (\frac nd)g(d)\) ,可以看做是高維差分 \(f(S)=\sum _{T\subseteq S} (-1)^{|S\backslash T|}g(T)\) ,而對應的 \(\mu(n/d)=(-1)^{|S|-|T|}\)

所以我們得以了解 \(\mu\) 的構造思路

想象現在我們在做二維差分 ,\(a(i,j)=s(i,j)-s(i-1,j)-s(i,j-1)+s(i-1,j-1)\) ,系數都長成這樣:

同理,考慮在剛剛所提到的表示法之下的系數,以 \(x=2^i 3^j\) 為例子

\(a(x)=s(x)-s(x/2)-s(x/3)+s(x/6)\) ,對應的 \(\mu(1)=1,\mu (2)=-1,\mu(6)=1,\mu(合數)=0\)

由此:

\[\begin{equation} \mu(N)= \left\{ \begin{array}{lr} 0,&\exist i\in[1,m],c_i>1\\ 1,&m\equiv 0(\bmod 2),\forall i\in[1,m],c_i=1\\ -1,&m\equiv 1(\bmod 2),\forall i\in[1,m],c_i=1\\ \end{array} \right. \end{equation} \]

FWTand

懶得寫啦!所以直接擺式子

\[FWT(A)=\begin{cases} Merge(FWT(A_0+A_1),FWT(A_1))&,n>0\\ \quad\quad\quad\quad\quad\quad\quad A&,n=0 \end{cases}\\ IFWT(A)=(IFWT(A_0-A_1),IFWT(A_1))\\ FWT(A)_i=\sum_{i\subseteq j}a_j \]

注意到從某種意義上來說,\(and\)\(or\) 是類似的,以為

\(0|0=1,0|1=1,1|0=1,1|1=1\)

\(0\&0=0,0\&1=0,1\&0=0,1\&1=1\)

void FWT(ll *A,int opt){
	for(int i=2;i<=N;i<<=1)
		for(int p=i>>1,j=0;j<N;j+=i)
			for(int k=j;k<j+p;++k)
				A[k]+=A[k+p]*opt;
}

FWTxor

其實 FWTxor 的推導過程才和 FFT 是最像的,怎么說呢?

顯然有 \(C_0=A_0B_0+A_1B_1\),\(C_1=A_0B_1+A_1B_0\)

FFT 的蝴蝶變換叫做:

\[F(\omega_n^k)=F_1(\omega_{\frac n2}^k)+\omega_n^kA_2(\omega_{\frac n2}^k)\\ F(\omega_n^{k+\frac n2})=F_1(\omega_{\frac n2}^k)-\omega_n^kA_2(\omega_{\frac n2}^k) \]

FWTxor 直接套用:

\[FWT(A)=\begin{cases} Merge(FWT(A_0+A_1),FWT(A_0-A_1))&,n>0\\ \quad\quad\quad\quad\quad\quad\quad A&,n=0 \end{cases} \]

然后你發現 \(FWT(A)\times FWT(B)=Merge(FWT(A_0B_0+A_1B_1+A_0B_1+A_1B_0),FWT(A_0B_0+A_1B_1-A_0B_1-A_1B_0))\)

然后你發現前半部分多了 \(A_0B_1+A_1B_0\) ,后面部分需要多了 \(A_0B_0+A_1B_1\) 而且還需要變個號

所以你靈光一現,如此構造出了 IFWT:

\(IFWT(A)=Merge(IFWT(\dfrac{A_0+A_1}2),IFWT(\dfrac{A_0-A_1}2))\)

然后你發現好像是解決了

void fwtxor(int *f,int op){
    for(int i=2;i<=len;i<<=1)
        for(int p=i>>1,j=0;j<len;j+=i)
            for(int k=j;k<j+p;++k){
                int x=f[k],y=f[k+p];
                f[k]=(x+y)%mod,f[k+p]=(x-y+mod)%mod;
                if(op==-1)(f[k]*=inv2)%=mod,(f[k+p]*=inv2)%=mod;
            }
}

(細心的讀者可能發現這份代碼和上面的數組不一樣,而且取了模,但其實是一樣的,只是從不同題里面 copy 出來的)

然后你開始探究 FWTxor 的本質(“解析式”)

然后你懵逼了

然后你上網看了一下:

\[FWT(A)_i=\sum_{j=0}^n(-1)^{|j\cap i|}a_j \]

然后你嘗試探究如何從 FWT 的遞推式得到它,

然后你發現你想不明白,但是模擬一下它就是對的,於是你有上網一通亂找,你找到了 pyb‘s ppt 的參考文獻

當你看完了巨佬的文章,你開始嘗試自己推導:

\[c_i=\sum_{j\oplus k=i}a_jb_k=\sum_{j\subseteq i}\sum_{k\subseteq i}[i\oplus j\oplus k=0]a_jb_k \]

這個時候你又知道這樣一個式子:

\[\frac{1}{2^n}\sum_{T\subseteq U}(-1)^{|W\cap T|}=1\Leftrightarrow W=\empty \]

其中 \(U\) 代表某一個大小為 \(2^n\) 的全集,\(W\) 是隨便自己給定了一個集合

它的正確性是很顯然的,當 \(W\) 不為空的時候,顯然會出現一些 \(-1\) ,從而使得這個 \(\sum\) 的值小於 \(2^n\)

當且僅當 \(W\) 為空集的時候,才會使得這個 \(\sum=2^n\)

然后,你利用這個式子,進行你的推導

\[\sum_{j\subseteq i}\sum_{k\subseteq i}[i\oplus j\oplus k=0]a_jb_k=\sum_{j\subseteq i}\sum_{k\subseteq i}\frac1{2^n}\sum_{T\subseteq U}(-1)^{|(i\oplus j\oplus k)\cap T|}a_jb_k \]

你發現你的推導還需要一點東西,所以你來證明這個:

\[|T\cap\oplus_{i=1}^x s_i|\equiv \sum|T\cap s_i|\pmod2 \]

其中 \(s_i\) 是某個集合,\(T\) 是某個集合。

首先,我們來證明 \(x=2\) 的情況,即:\(|i\cap (j\oplus k)|\equiv|i\cap j|+|i\cap k|\pmod 2\)

(注意,下面沒有采用原博客所使用的方法)

我們直接把集合看成二進制(反正上面的文章也一直把集合和二進制在混用qwq) ,下面簡記 \(popcount\to ppc\)

即證明 \(ppc(i\&(j\oplus k))\equiv ppc(i\&j)+ppc(i\&k)\pmod 2\)

(突然發現由於寫了太多,導致上面出現了奇怪的人稱變化)

\(z=i\&(j\oplus k),x=i\&j,y=i\&k\) ,接下來分類討論,對於二進制每一位,都有:

  1. \(x\) 當前位為 \(0\)\(y\) 當前位為 \(0\) 。那么要么是 \(i\) 沒有當前位,要么是 \(j,k\) 均沒有當前位,所以 \(z\) 當前位也是 \(0\)
  2. \(x\) 當前位為 \(0\)\(y\) 當前位為 \(1\) 。那么肯定是 \(j\) 沒有當前位,\(i,k\) 均有當前位,所以 \(z\) 當前位也是 \(1\)
  3. \(x\) 當前位為 \(1\)\(y\) 當前位為 \(0\) 。那么肯定是 \(k\) 沒有當前位,\(i,j\) 均有當前位,所以 \(z\) 當前位也是 \(1\)
  4. \(x\) 當前位為 \(1\)\(y\) 當前位為 \(1\) 。那么肯定是 \(i,j,k\) 均有當前位,\(z\) 當前位為 \(0\) ,在 \(mod\ 2\) 意義下依舊滿足

所以現在我們已經證明了 \(x=2\) 的情況,容易發現 \(n>2\) 的情況可以看成 \(n=2\) 的情況,解決

所以我們現在繼續我們的推式子大業:

\[\begin{align} \sum_{j\subseteq i}\sum_{k\subseteq i}\frac1{2^n}\sum_{T\subseteq U}(-1)^{|(i\oplus j\oplus k)\cap T|}a_jb_k&=\sum_{j\subseteq i}\sum_{k\subseteq i}\frac1{2^n}\sum_{T\subseteq U}(-1)^{|T\cap i|+|T\cap j|+|T\cap k|}a_jb_k\\ &=\frac1{2^n}\sum_{j\subseteq i}\sum_{k\subseteq i}\sum_{T\subseteq U}(-1)^{|T\cap i|}(-1)^{|T\cap j|}a_j(-1)^{|T\cap k|b}b_k \end{align} \]

順理成章的,我們知道了:

\[FWT(A)_i=\sum_{j=0}^n(-1)^{|j\cap i|}a_j \]

!!!

「CF662C」 Binary Table

CF662C Binary Table - 洛谷 | 計算機科學教育新生態 (luogu.com.cn)

延續做一道黑題寫一篇題解的傳統,我們直接把題解寫在這里面

Statement

有一個 \(n\)\(m\) 列的表格,每個元素都是 \(0/1\) ,每次操作可以選擇一行或一列,把 \(0/1\) 翻轉,即把 \(0\) 換為 \(1\) ,把 \(1\) 換為 \(0\) 。請問經過若干次操作后,表格中最少有多少個 \(1\)

\(n\leq 20,m\leq 10^5\)

Solution

注意到 \(𝑛\) 很小

容易得到暴力算法:暴力枚舉第 \(𝑖\) 是否翻轉,這樣每一行的狀態就已確定,對於每一次完整的行翻轉后,取每一列 \(0/1\) 個數較小的貢獻即可。(因為每一列也可以翻轉)

時間復雜度是 \(𝑂(𝑚 ⋅ 2^𝑛 )\) ,我們考慮優化掉這個 \(m\)

顯然行的操作我們可以狀壓為 \(𝑥\),(二進制為 \(1\) 的位表示那一行要翻轉)

每一列的的狀態值也可以壓縮:

  1. \(𝐴[𝑖]\) 表示在原表中, \(𝑖\) 這個狀態數量
  2. \(𝐵[𝑖]\) 表示 \(𝑖\) 這個狀態最小 \(1\) 的個數(即 \(\min(popcount(i), n-ppc(i)\))

當確定了行的操作為 \(x\) ,那么所有狀態為 \(i\) 的列的貢獻為:\(A[i]B[i\oplus x]\)

\(j=i\oplus x\) ,那么 \(x=i\oplus j\)

所以 \(ans_x=\sum_{i\oplus j=x}A[i]B[j]\) ,直接上 FWT 即可

復雜度:\(O(\max(n2^m,nm))\)

Code

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = (1<<21);
const int M = 1e5+5;

char s[25][M];
int a[N],b[N];
int n,m,ans=1e18;

void fwtxor(int *f,int op){
    for(int i=2;i<=(1<<n);i<<=1)
        for(int p=i>>1,j=0;j<(1<<n);j+=i)
            for(int k=j;k<j+p;++k){
                int x=f[k],y=f[k+p];
                f[k]=x+y,f[k+p]=x-y;
                if(op==-1)f[k]/=2,f[k+p]/=2;
            }
}

signed main(){ 
    scanf("%lld %lld",&n,&m);
    for(int i=1;i<=n;++i)scanf("%s",s[i]+1);
    for(int i=1,k;k=0,i<=m;++i,a[k]++)
        for(int j=1;j<=n;++j)k=k<<1|(s[j][i]-'0');
    for(int i=0,t;i<(1<<n);++i)
        t=__builtin_popcount(i),b[i]=min(t,n-t);
    fwtxor(a,1),fwtxor(b,1);
    for(int i=0;i<(1<<n);++i)a[i]*=b[i];
    fwtxor(a,-1);
    for(int i=0;i<(1<<n);++i)ans=min(ans,a[i]);
    printf("%lld\n",ans);
    return 0;
}

子集卷積

寫不動了!!

https://www.luogu.com.cn/problem/P6097

所謂子集卷積,聽着很高級,其實也就那樣(就最基本的而言),好像有叫做集合占位冪級數什么的

它用來處理求出這樣一個 \(c\)

\[c_i=\sum_{\begin{align}j\ |\ k=i \\ j\ \&\ k=0\end{align}}a_jb_k \]

我們很容易處理第一個限制 \(i \cap j=k\),直接 FWTand 即可。

對於第二個限制 \(i \cap j=\varnothing\Leftrightarrow |i|+|j|=|i\cap j|\),所以我們不妨再開一維記錄集合中的元素個數

也就是說設 \(f_{i,j} = a_j[|j|=i],g_{i,j}=b_j[|j|=i]\),把他們卷起來,\(h_{i,j}=\sum_{k=0}^i \sum_{l|r=j}f_{k,l}g_{i-k,r}\)

最后的答案即為: \(c_i=h_{|i|,i}\) ,因為 \(popcount(i)+popcount(j)\ge popcount(i|j)\) ,所以相等的時候取到的就是正確的值

(即 \(|S|+|T|=|S|T|+|S\&T|\) )

#include<bits/stdc++.h>
using namespace std;
const int N = 1<<21;
const int mod = 1e9+9;

int read(){
	int s=0,w=1;char ch=getchar();
	while(!isdigit(ch)){if(ch=='-')w=-1;ch=getchar();}
	while(isdigit(ch))s=s*10+ch-'0',ch=getchar();
	return s*w;
}

int a[21][N],b[21][N],c[21][N];
int n,len;

void fwt(int *f,int op){
	for(int i=2;i<=len;i<<=1)
		for(int p=i>>1,j=0;j<len;j+=i)
			for(int k=j;k<j+p;++k)
				(f[k+p]+=op*f[k])%=mod; 
} 

signed main(){
	n=read(),len=1<<n;
	for(int i=0;i<len;++i)a[__builtin_popcount(i)][i]=read();
	for(int i=0;i<len;++i)b[__builtin_popcount(i)][i]=read();
	for(int i=0;i<=n;++i)fwt(a[i],1),fwt(b[i],1);
	for(int i=0;i<=n;++i)
		for(int j=0;j<=i;++j)
			for(int k=0;k<len;++k)
				(c[i][k]+=(long long)a[j][k]*b[i-j][k]%mod)%=mod;
	for(int i=0;i<=n;++i)fwt(c[i],-1);
	for(int i=0;i<len;++i)printf("%d ",(c[__builtin_popcount(i)][i]+mod)%mod);
	return 0;
} 

所謂集合占位冪級數,是這樣的形式:

\[f_Sg_T[S\&T=\varnothing]\to h_{S|T}\\ \sum_Sf_Sx^S\to \sum_S f_Sz^{|S|} x^S\\ fz^ax^S ·gz^bx^T\to fg\ z^{a+b}x^{S|T}\\ \]

擴展:https://www.luogu.com.cn/blog/user7035/zi-ji-juan-ji-ji-ji-gao-ji-yun-suan (我不會,等我長大后再學習)

[WC2018] 州區划分

WC2018 州區划分 - 洛谷 | 計算機科學教育新生態 (luogu.com.cn)

延續做一道黑題寫一篇題解的傳統,我們直接把題解寫在這里面

Statement

由於原題面已經概括得很好了,所以這里直接貼圖:

\(n\leq 21,m\leq n^2/2,p\le 2 ,w_i\le 100\)

Solution

容易考慮狀壓 DP,設 \(f[i][j]\) 表示划分了 \(i\) 個州,選出的城市的集合為 \(j\) 的貢獻,那么

\[f[i][j]=\sum_{k\subseteq j}f[i-1][k](\dfrac{sum[k]}{sum[j]})chk[k] \]

其中,\(sum[s]\) 表示集合 \(s\)\(w\) 的和,\(chk[s]\) 表示集合 \(s\) 是否可以獨立成州

所謂獨立成州的條件其實可以轉化為 圖不連通/存在奇度數點,我們容易在 \(O(2^nm)\) 左右的時間暴力預處理出 \(sum,chk\)

\(g[k]=sum[k]chk[k]\) ,那么:

\[\begin{align} f[i][j]&=\frac1{sum[j]}\sum_{k\subseteq j}f[i-1][k]g[k]\\ &=\frac1{sum[j]}\sum_{s\cup t=j \&\& s\cap t=\emptyset}f[i-1][s]g[t] \end{align} \]

發現后面的那一坨其實就是子集卷積,所以復雜度 \(O(n^22^n)\) 解決

Code

#include<bits/stdc++.h>
#define int long long
#define ppc(x) __builtin_popcount(x)
using namespace std;
const int mod = 998244353;
const int N = 22;

char buf[1<<23],*p1=buf,*p2=buf;
#define getchar() (p1==p2&&(p2=(p1=buf)+fread(buf,1,1<<21,stdin),p1==p2)?EOF:*p1++)
int read(){
    int s=0,w=1; char ch=getchar();  
    while(!isdigit(ch)){if(ch=='-')w=-1;ch=getchar();}
    while(isdigit(ch))s=s*10+(ch^48),ch=getchar();
    return s*w;
}

int u[N*N],v[N*N],w[N],fa[N],deg[N],inv[(1<<N)+5];
int f[N][(1<<N)+5],g[N][(1<<N)+5];
int n,m,p;
bool vis[N];

void input(){
    n=read(),m=read(),p=read();
    for(int i=1;i<=m;++i)u[i]=read(),v[i]=read();
    for(int i=1;i<=n;++i)w[i]=read();
}
int find(int x){return x==fa[x]?x:fa[x]=find(fa[x]);}
bool merge(int u,int v){
    u=find(u),v=find(v);
    return u==v?0:fa[u]=v;//////////////
}
int ksm(int a,int b){
    int res=1;
    while(b){
        if(b&1)res=res*a%mod;
        a=a*a%mod,b>>=1;
    }
    return res;
}
void preparation(){
    for(int i=0,sum,cnt,flag;sum=cnt=flag=0,i<(1<<n);++i){
        for(int j=1;j<=n;++j)fa[j]=j,vis[j]=false,deg[j]=0;
        for(int j=0;j<n;++j)if(i&(1<<j))vis[j+1]=true,sum+=w[j+1],cnt++;
        for(int j=1;j<=m;++j)if(vis[u[j]]&&vis[v[j]])
            merge(u[j],v[j])&&(cnt--,1),deg[u[j]]++,deg[v[j]]++;
        flag|=cnt!=1,cnt=0;
        for(int j=1;j<=n;++j)cnt+=(deg[j]&1);
        flag|=cnt!=0,g[ppc(i)][i]=flag*ksm(sum,p);
        inv[i]=ksm(ksm(sum,mod-2),p);
    }
}
void fwtor(int *f,int op){
    for(int i=2;i<=(1<<n);i<<=1)
        for(int j=0,p=i>>1;j<(1<<n);j+=i)
            for(int k=j;k<j+p;++k)(f[k+p]+=(op*f[k]+mod)%mod)%=mod;
}
void work(){
    for(int i=0;i<=n;++i)fwtor(g[i],1);
    f[0][0]=1,fwtor(f[0],1);
    for(int i=1;i<=n;++i){
        for(int j=0;j<i;++j)//j!=i
            for(int k=0;k<(1<<n);++k)
                (f[i][k]+=f[j][k]*g[i-j][k]%mod)%=mod;
        fwtor(f[i],-1);
        for(int j=0;j<(1<<n);++j)
            f[i][j]=(ppc(j)==i)*(f[i][j]*inv[j]%mod);
        if(i^n)fwtor(f[i],1);
    }
}
void output(){
    printf("%lld\n",f[n][(1<<n)-1]);
}

signed main(){
    input();
    preparation();
    work();
    output();
    return 0;
}

「CF1034E」Little C Loves 3 III

Statement

給定 \(n\) 和長度為 \(2^n\) 的數列 \(a_{0},a_{1}...a_{2^n-1}\)\(b_{0},b_1...b_{2^n-1}\),保證每個元素的值屬於$ [0,3]$

生成序列 \(c\),對於 \(c_i\),有:

\(c_i=\sum_{j|k=i,j\&k=0} a_j\times b_k\)

\(c_{0},c_1...c_{2^n-1}\),答案對 \(4\) 取模。

\(n\le 21\),時限 \(\rm 1s\)

Solution

顯然,暴力 \(O(n^22^n)\) 的子集卷積不可過,考慮利用 \(\bmod 4\) 的性質

普通的,子集卷積為了取到正確的值,采用的方法可以理解為 DP 多設一維狀態 \({|S|}\) ,使得我們加入了 \(z^{|S|}\) 這一項

這里,我們讓 \(a_S=a_S\times 4^{|S|},b_T=b_T\times 4^{|T|}\) (鬼知道是怎么想到的!)

用 FWT 求出 \(c\) 后,\(x^{|S|}\) 項系數對 \(4^{|S|+1}\) 取模,再除以一個 \(4{|S|}\) 即可

解釋:

集合占位冪級數是這樣的形式: \(\sum_S (f_S z^{|S|}x^S+\sum_{i>|S|}\sigma_{i,S}z^ix^S)\) 其中 \(z\) 的取值是任意的, $\sigma $ 一般都取 \(\rm 0\)\(\sigma\) 意義是在於可能會在 \(S\) 位置瞎 jb 轉上一些不計入答案的貢獻

而當我們把 \(z\) 取到 \(\rm 4\)\(\bmod 4^{|S|+1}\) 干掉了后面那個 \(\sum\) ,除 \(4^{|S|}\) 相當於還原

於是這樣快樂 FWT 即可,\(O(n2^n)\)

Code

#include<bits/stdc++.h>
#define int long long
using namespace std;

char s[1<<21|5],t[1<<21|5];
int f[1<<21|5],g[1<<21|5];
int n;

void fwt(int *f,int op){
    for(int i=2;i<=(1<<n);i<<=1)
        for(int p=i>>1,j=0;j<(1<<n);j+=i)
            for(int k=j;k<j+p;++k)f[k+p]+=op*f[k];
}

signed main(){
    scanf("%lld%s%s",&n,s,t);
    for(int i=0;i<(1<<n);++i)f[i]=(s[i]&15ll)<<(__builtin_popcount(i)<<1);fwt(f,1);
    for(int i=0;i<(1<<n);++i)g[i]=(t[i]&15ll)<<(__builtin_popcount(i)<<1);fwt(g,1);
    for(int i=0;i<(1<<n);++i)f[i]*=g[i]; fwt(f,-1);
    for(int i=0;i<(1<<n);++i)putchar(f[i]>>(__builtin_popcount(i)<<1)&3|48);
    return 0;
}

完結撒花!!!!✿✿ヽ(°▽°)ノ✿


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM