Problem
- 给定一个n(≤20)*m(≤100 000)的01矩阵,每次操作可以将一行或一列取反。
- 求最终1的最少个数。
Solution
- 前置技能:快速沃尔什变换(FWT)。
- 观察到n较小,考虑
O(2n)
O
(
2
n
)
枚举每一行选或不选。
- 不妨设f(x)表示行的操作状态为x时(我们可用一个二进制数表示状态),经过各种列操作后所得到的最少的1的个数。
- 可以
O(m)
O
(
m
)
再扫一遍所有列。但显然T飞了。
- 定义
Cj
C
j
表示有多少列的状态为j;
Ek
E
k
表示对于某一列而言,若它经过各种行操作状态变成了k,则它再经历各种列操作后最少能得到的1的个数。
- 显然,
Cj
C
j
我们对于每一列统计一下即可;而
Ek
E
k
也很好求,设状态k中有cnt个1,则
Ek=min(cnt,n−cnt)
E
k
=
m
i
n
(
c
n
t
,
n
−
c
n
t
)
(不进行/进行列操作)。
- 而且我们也可以得到一个较为显然的式子:
f(x)=∑x⊕j=kCj∗Ek
f
(
x
)
=
∑
x
⊕
j
=
k
C
j
∗
E
k
。这个式子的思路就是对于所有状态为j的列,我们都通过状态为x的行操作令其变成了
x⊕j=k
x
⊕
j
=
k
,然后再看看变成了k以后的答案。
- 可以暴力枚举j,暴力转移。但是这样的复杂度是
O(22n)
O
(
2
2
n
)
的。
- 注意到xor的特殊性:对于任何
x⊕y=z
x
⊕
y
=
z
,有
y⊕z=x
y
⊕
z
=
x
。
- 因此,上式可化为:
f(x)=∑j⊕k=xCj∗Ek
f
(
x
)
=
∑
j
⊕
k
=
x
C
j
∗
E
k
。
- 观察到这是一个卷积的形式,我们用FWT优化它。
- 时间复杂度:
O(nm+2nn)
O
(
n
m
+
2
n
n
)
。
Code
#include <bits/stdc++.h>
#define go(i,a,b) for(i=a;i<b;i++)
using namespace std;
typedef long long ll;
const int N=21,M=1e5+1,S=1<<21;
int i,j,n,m,s,tmp;
char str[M];
bool a[N][M];
ll c[S],e[S],f[S],ans;
void FWT(ll *tf)
{
for(int d=1;d<s;d<<=1)
for(int m=d<<1,i=0;i<s;i+=m)
for(int j=0;j<d;j++)
{
ll x=tf[i+j],y=tf[i+j+d];
tf[i+j]=x+y; tf[i+j+d]=x-y;
}
}
void UFWT()
{
for(int d=1;d<s;d<<=1)
for(int m=d<<1,i=0;i<s;i+=m)
for(int j=0;j<d;j++)
{
ll x=f[i+j],y=f[i+j+d];
f[i+j]=x+y>>1; f[i+j+d]=x-y>>1;
}
}
int main()
{
scanf("%d%d",&n,&m);
go(i,0,n)
{
scanf("%s",str);
go(j,0,m) a[i][j]=str[j]-48;
}
go(i,0,m)
{
s=0;
go(j,0,n) s+=a[j][i]*(1<<j);
c[s]++;
}
s=1<<n;
go(i,0,s)
{
for(tmp=i; tmp; tmp>>=1) e[i]+=tmp&1;
e[i]=min(e[i],n-e[i]);
}
FWT(c); FWT(e);
go(i,0,s) f[i]=c[i]*e[i];
UFWT();
ans=n*m;
go(i,0,s) ans=min(ans,f[i]);
printf("%lld",ans);
}