problem
有一个 n n n 行 m m m 列的表格,每个元素都是 0 / 1 0/1 0/1,每次操作可以选择一行或一列,把 0 / 1 0/1 0/1 翻转,即把 0 0 0 换为 1 1 1,把 1 1 1 换为 0 0 0。请问经过若干次操作后,表格中最少有多少个 1 1 1。
数据范围: 1 ≤ n ≤ 20 1\le n\le20 1≤n≤20, 1 ≤ m ≤ 1 0 5 1\le m\le10^5 1≤m≤105。
solution
看到这个 n n n 的范围很小,我们有这样一个暴力:枚举行的翻转情况,然后枚举列贪心计算答案。
但是这样的复杂度是 O ( 2 n m ) O(2^nm) O(2nm),不能通过本题,考虑怎么优化。
我们用 f k f_k fk 表示翻转行集合 k k k 时的最优答案, a i a_i ai 表示有多少状态为 i i i 的列, b j b_j bj 表示状态为 j j j 时的最优答案(就是翻和不翻取 min \min min)。
其中 i , j , k i,j,k i,j,k 都是二进制数, k k k 表示一个集合。
那么有这样的转移:
f k = ∑ k ⨁ i = j a i b j f_k=\sum_{k⨁i=j}a_ib_j fk=k⨁i=j∑aibj
意思就是,翻转行集合 k k k 时, i i i 状态变成了 j j j 状态,此时的贡献就是 a i b j a_ib_j aibj。
然后由于异或对称差的性质, k ⨁ i = j ⇔ k = i ⨁ j k⨁i=j\Leftrightarrow k=i⨁j k⨁i=j⇔k=i⨁j,所以有:
f k = ∑ i ⨁ j = k a i b j f_k=\sum_{i⨁j=k}a_ib_j fk=i⨁j=k∑aibj
然后就可以愉快的 FWT 啦。
注意一下,虽然答案是不超过 n × m n\times m n×m 的,但是在处理过程中可能会爆 int,要开 long long。
code
#include<cstdio>
#include<cstring>
#include<algorithm>
#define M 100005
#define S 1<<20
#define ll long long
using namespace std;
int n,m,num[M];
ll A[1<<20],B[1<<20];
char s[M];
void FWT(ll *f,int lim,int type){
for(int mid=1;mid<lim;mid<<=1){
for(int i=0;i<lim;i+=(mid<<1)){
for(int j=0;j<mid;++j){
ll p0=f[i+j],p1=f[i+j+mid];
f[i+j]=p0+p1,f[i+j+mid]=p0-p1;
}
}
}
if(type==-1) for(int i=0;i<lim;++i) f[i]/=lim;
}
int main(){
scanf("%d%d",&n,&m);
int status=1<<n;
for(int i=1;i<=n;++i){
scanf("%s",s+1);
for(int j=1;j<=m;++j) num[j]=(num[j]<<1)+s[j]-'0';
}
for(int i=1;i<=m;++i) A[num[i]]++;
for(int i=0;i<status;++i) B[i]=B[i>>1]+(i&1);
for(int i=0;i<status;++i) B[i]=min(B[i],n-B[i]);
FWT(A,status,1),FWT(B,status,1);
for(int i=0;i<status;++i) A[i]*=B[i];
FWT(A,status,-1);
ll ans=n*m;
for(int i=0;i<status;++i) ans=min(ans,A[i]);
printf("%lld\n",ans);
return 0;
}