1. 程式人生 > 實用技巧 >【題解】LOJ #6488 數表【FWT】

【題解】LOJ #6488 數表【FWT】

題目連結

題意

\(n\times m\) 的表格,一次操作可以將一行或一列在模 \(4\) 意義下加 \(1\),問任意次操作後表格內數之和的最小值。\(n\leq 10,m\leq 10^4\)

題解

行上的操作確定後,每一列都容易確定最優解。

\(c(i)\) 為列上狀態為 \(i\) 的列的個數,\(f(i)\) 為當一列為 \(i\) 時,做列操作後的最小和。二者做減法卷積便能得到每種行操作下的最優答案。

\(4\) 進位制的高維迴圈卷積類似於異或卷積,但在迴圈內要做長為 \(4\) DFT(暴力即可)

減法卷積在翻轉系數時怎麼做都能過,但大多數寫法得到的結果順序其實是亂的。

#include<bits/stdc++.h>
using namespace std;
const int N=21;
int f[1<<N],c[1<<N],rt=86583718,mod=998244353,inv4=(mod*3ll+1)/4;
int a[11][10001];

int p[4],q[4];
void dft(int tp){
    int r=1;
    for(int i=0;i<4;i++){
        q[i]=0;
        for(int j=3;j>=0;j--)
            q[i]=(q[i]*1ll*r+p[j])%mod;
        r=r*1ll*rt%mod;
    }
    if(tp==-1){
        swap(q[1],q[3]);
        for(int i=0;i<4;i++)q[i]=q[i]*1ll*inv4%mod;
    }
}
void fwt(int *a,int lim,int tp){
    for(int i=1;i<lim;i<<=2){
        for(int j=0;j<lim;j+=i<<2){
            for(int k=0;k<i;k++){
                // cerr<<"| "<<j+k<<" "<<i<<endl;
                for(int l=0;l<4;l++)p[l]=a[i*l+j+k];
                dft(tp);
                for(int l=0;l<4;l++)a[i*l+j+k]=q[l];
            }
        }
    }
}
int main(){
    int n,m;
    scanf("%d%d",&n,&m);
    for(int i=0;i<n;i++)for(int j=0;j<m;j++)scanf("%d",a[i]+j);
    for(int i=0;i<m;i++){
        int s=0;
        for(int j=0;j<n;j++)
            s=(s<<2)|((4-a[j][i])&3);
        c[s]++;
        // if(s)c[(1<<(n*2))-s]++;
        // else c[0]++;
    }
    for(int i=0;i<(1<<n*2);i++){
        int mn=0x7f7f7f7f;
        for(int j=0;j<4;j++){
            int s=0;
            for(int k=0;k<n;k++)
                s+=((i>>(k*2))+j)&3;
            mn=min(s,mn);
        }
        f[i]=mn;
    }
    // for(int i=0;i<1<<(n*2);i++)cerr<<"! "<<c[i];cerr<<endl;
    fwt(f,1<<(n*2),1);
    fwt(c,1<<(n*2),1);
    // for(int i=0;i<1<<(n*2);i++)cerr<<"! "<<c[i];cerr<<endl;
    for(int i=0;i<1<<(n*2);i++)f[i]=f[i]*1ll*c[i]%mod;
    fwt(f,1<<(n*2),-1);
    int ans=0x7f7f7f7f;
    for(int i=0;i<1<<(n*2);i++)ans=min(ans,f[i]);
    cout<<ans;
}