从子集和问题到and卷积
枚举子集:O ( 3 n ) O(3^{n}) O ( 3 n )
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 int u=15 ; for (int s = u; s; s = (s - 1 ) & u) { b=s ; cout<<b<<endl; }
其实这类算法更多的是解决子集和 (sum of subset) 问题,因此也叫 SOS DP。
也就是对于i从1-n我们希望求出s u m [ i ] = ∑ j ⊆ i a [ j ] sum[i] = \sum_{j \subseteq i} a[j] s u m [ i ] = ∑ j ⊆ i a [ j ]
一个平凡的做法是直接枚举子集:O ( 3 n ) O(3^{n}) O ( 3 n )
1 2 3 4 5 for (int i = 0 ; i < n; i++) { sum[i] = a[0 ]; for (int j = i; j > 0 ; j = (j - 1 ) & i) sum[i] += a[j]; }
引入高维前缀和可以优化到O ( 2 n n ) O(2^{n}n) O ( 2 n n )
对于高位前缀和如果容斥计算,不仅麻烦而且复杂度不优
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 for (int j = 1 ; j <= m; j++) for (int k = 1 ; k <= l; k++) for (int i = 1 ; i <= n; i++) { line_sum[i][j][k] = line_sum[i - 1 ][j][k] + a[i][j][k]; } for (int k = 1 ; k <= l; k++) for (int i = 1 ; i <= n; i++) for (int j = 1 ; j <= m; j++) { face_sum[i][j][k] = face_sum[i][j - 1 ][k] + line_sum[i][j][k]; } for (int i = 1 ; i <= n; i++) for (int j = 1 ; j <= m; j++) for (int k = 1 ; k <= l; k++) { sum[i][j][k] = sum[i][j][k - 1 ] + face_sum[i][j][k]; }
注意到这里其实对于每个三重循环,我们可以任意排列 i,j,k 循环的位置。仔细观察,我们其实是在按照维度依次做前缀和(每次处理了前 p个维度的前缀和)。
这里我们发现复杂度从原本的容斥O ( 2 D Π i = 1 D N i ) O(2^D \Pi_{i = 1}^{D} N_i) O ( 2 D Π i = 1 D N i ) 下降到了 O ( D Π i = 1 D N i ) O(D \Pi_{i = 1}^{D} N_i) O ( D Π i = 1 D N i ) 。当维数 D很大时,这个优化将会非常显著。
高维和和子集和的关系
这里我们注意到,实际上子集和可以按照位数 D 分为 D 个维度(每个维度只有 0,10,1 两个取值),而实际上每个子集和对应了一个 D 维超立方的前缀和。
这里我们可以完全的将求前缀和的方式搬到这里求子集和。
只是细节需要注意,任意一维 被−1 的位置可以假想对应了一个 0 的值,于是我们发现我们实质上在处理第 p 维时,只需要注意那些该维为 1 的位置。这里我们用 d p [ i ] [ d ] dp[i][d] d p [ i ] [ d ] 表示超立方体已经处理了下标 0∼d维度时的前缀和。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 for (int i = 0 ; i < (1 << D); i++) { if (i & 1 ) { dp[i][0 ] = a[i] + a[i ^ (1 << d)]; } else { dp[i][0 ] = a[i]; } } for (int d = 1 ; d < D; d++) { for (int i = 0 ; i < (1 << D); i++) { if (i & (1 << d)) { dp[i][d] = dp[i][d - 1 ] + dp[i ^ (1 << d)][d]; } else { dp[i][d] = dp[i][d - 1 ]; } } }
我们发现可以采用滚动数组省去一维
1 2 3 4 5 6 7 8 9 10 for (int i = 0 ; i < (1 << D); i++) { dp[i] = a[i]; } for (int d = 0 ; d < D; d++) { for (int i = 0 ; i < (1 << D); i++) { if (i & (1 << d)) { dp[i] += dp[i ^ (1 << d)]; } } }
当我们知道了子集和,我们可以倒推出本身的权值。
1 2 3 4 5 for(int i=n-1;~i;i--) { for(int j=0;j<1<<n;j++) if(j&(1<<i)) { sum[j]-=sum[j^(1<<i)]; } }
超集和s u m [ i ] = ∑ j ⊇ i a [ j ] sum[i] = \sum_{j \supseteq i} a[j] s u m [ i ] = ∑ j ⊇ i a [ j ]
1 2 3 4 5 6 7 8 9 10 for (int i = 0 ; i < (1 << D); i++) { dp[i] = a[i]; } for (int d = 0 ; d < D; d++) { for (int i = 0 ; i < (1 << D); i++) { if (!(i & (1 << d))) { dp[i] += dp[i ^ (1 << d)]; } } }
当我们知道了超集和,我们可以倒推出本身的权值。
1 2 3 4 5 for(int i=n-1;~i;i--) { for(int j=(1<<n)-1;~j;j--) if(j&(1<<i)) { sum[j^(1<<i)]-=sum[j];//疑似博客写错,这里应该是减号 } }
题意:给定一个数组 a n a_n a n ,求有几对数字 满足( a i , a j ) (a_i,a_j) ( a i , a j ) 满足C a j a i C^{a_i}_{a_j} C a j a i 为奇数
Sol:结论题:C m n C^{n}_{m} C m n 为奇数的条件是 n & m = m
也就是对于C(n,k),若n&k == k 则C(n,k)为奇数,否则为偶数
严谨证明:考虑卢卡斯定理的推论,尝试证明mod2意义下为1
C n m ≡ C [ n / p ] [ m / p ] × C n m o d p m m o d p ( m o d p ) \text{C}_n^m\equiv \text{C}_{[n/p]}^{[m/p]}\times\text{C}_{n\bmod p}^{m\bmod p} \pmod p C n m ≡ C [ n / p ] [ m / p ] × C n m o d p m m o d p ( m o d p )
将C m n C^{n}_{m} C m n 中的 n 和 m表示成 p 进制数:
$n=\sum\limits_{i=0}^k a_i\cdot p^i \left(a_k \ne 0 \right) $
m = ∑ i = 0 k b i ⋅ p i m=\sum\limits_{i=0}^k b_i\cdot p^i m = i = 0 ∑ k b i ⋅ p i
C n m ≡ ∏ i = 0 k C a i b i ( m o d p ) \text{C}_n^m \equiv \prod\limits_{i=0}^k\text{C}_{a_i}^{b_i} \pmod p C n m ≡ i = 0 ∏ k C a i b i ( m o d p )
取p=2的时候,也就是对n和m的二进制拆分,我们希望为奇数,也就是必须全为1,也就是a[i]为0的位不参与计算,就是只计算二进制有效位。此时无论b[i]在这位为0或1结果,本次计算都是1,多个1相乘还是1,也就证明了计算结果是奇数
综上所述,原问题等价于在数组中找到有多少对包含关系
由于题目既不要求自环又不要求偏序,所以我们需要先把所有数存下来,再去dp。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 const int len=__lg(N);void solve () { cin>>n; vector<int >dp (N); ll ans=0 ; for (int i=1 ;i<=n;i++){ cin>>a[i]; dp[a[i]]++; } for (int i=0 ;i<=len;i++){ for (int j=1 ;j<=1000000 ;j++){ if ((j>>i)&1 )dp[j]+=dp[j^(1 <<i)]; } } for (int i=1 ;i<=n;i++)ans+=dp[a[i]]; cout<<ans<<endl; }
SOSDP学习笔记 | c4Lnn 的个人博客
题意:给定一个数组,对于每个a i a_{i} a i 找到a j a_{j} a j 满足a i & a j = 0 a_{i}\&a_{j}=0 a i & a j = 0 .如果有多个j满足要求,输出最小满足要求的j(cf上题目没要求,但可以加强)
Sol:考虑对于二进制位的取值情况,原问题等价于找~a[i]的子集,我们只需要正常做一遍sosdp。然后考虑统计答案。又由于设置最小,我们考虑对于dp的时候使用min运算维护值域对应的最小下标,答案是要输出元素
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 const int len=__lg(M)+1 ;const int N =(1 <<len)+5 ;int a[N];int dp[N];void solve () { memset (dp,0x3f ,sizeof dp); cin>>n; for (int i=1 ;i<=n;i++){ cin>>a[i]; dp[a[i]]=i; } for (int i=0 ;i<=len;i++){ for (int j=0 ;j<(1 <<len);j++){ if ((j>>i)&1 ){ dp[j]=min (dp[j],dp[j^(1 <<i)]); } } } for (int i=1 ;i<=n;i++){ int x=(1 <<len)-1 -a[i];; if (dp[x]==inf)cout<<-1 <<" " ; else cout<<a[dp[x]]<<" " ; } }
Codeforces Round 225 (Div. 1) E
题意:字符集为小写字母前24个。共有2 24 2^{24} 2 2 4 的子集,给定若干长度为3的单词。对于每个子集,我们认为当前子集里的元素是元音,每个单词至少含1个元音才合法,回答在当前子集条件下有多少正确的单词?
Sol:将单词映射成二进制位,我们只关心当前单词有哪几个元素,不用关心重复元素。假设集合为s,当前单词为a i a_{i} a i ,我们需要统计有多少个i满足a i & s ! = 0 a_{i}\&s!=0 a i & s ! = 0 ,这样正着考虑复杂度太高,我们考虑反面,a i & s = 0 a_{i}\&s=0 a i & s = 0 的i的数量num,答案用n-num得到。而与起来等于0在上题已经被解决,我们只需要正常sosdp,在统计答案的时候找~a[i]的子集的数量。
bug1没想清楚到底两个循环的边界
bug2:一开始没处理高维前缀和,导致统计答案的时候导致多加了一层循环
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 int dp[N];void solve () { cin>>n; for (int i=1 ;i<=n;i++){ string s;cin>>s; int x=0 ; for (int j=0 ;j<3 ;j++){ int u=s[j]-'a' ; x|=1 <<u; } dp[x]++; } for (int i=0 ;i<len;i++){ for (int j=0 ;j<(1 <<len);j++){ if ((j>>i)&1 )dp[j]+=dp[j^(1 <<i)]; } } int ans=0 ; for (int j=0 ;j<N-3 ;j++){ int x=(1 <<len)-1 -j; ans^=((n-dp[x])*(n-dp[x])); } cout<<ans<<endl; }
题意:r i = ∑ i & ( a ∣ b ∣ c ) = i f a g b h c r_i=\sum_{i\&(a|b|c)=i}{f_ag_bh_c} r i = ∑ i & ( a ∣ b ∣ c ) = i f a g b h c ,求∑ 0 2 n − 1 r i ( 1 ≤ n ≤ 20 ) \sum_0^{2^n-1}{r_i}(1\le n \le 20) ∑ 0 2 n − 1 r i ( 1 ≤ n ≤ 2 0 ) 。
我的理解:首先考虑无法直接r i r_{i} r i 一个一个计算,我们考虑从可能贡献的值的出现次数入手,对于一个数,它贡献的次数,就是它作为别人超集的个数,就是它的子集个数,也就是它的二进制位为1的个数。那么接下来考虑哪些数可能作为值域,显然我们不可能暴力枚举。对于这种位运算的希望求某一点的单值,一般都是先求高维前缀和,再倒着差分减回去得到单点值,但这样做的时候复杂度时间复杂度就大大降低了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 int n, m;int a[N];int f[N],g[N],h[N];int sum[N];int cal (int x) { return 1LL <<__builtin_popcountll(x); } void solve () { cin>>n; for (int i=0 ;i<(1 <<n);i++)cin>>f[i]; for (int i=0 ;i<(1 <<n);i++)cin>>g[i]; for (int i=0 ;i<(1 <<n);i++)cin>>h[i]; for (int i=0 ;i<n;i++){ for (int j=0 ;j<(1 <<n);j++){ if ((j>>i)&1 ){ f[j]=(f[j]+f[j^(1 <<i)])%mod; g[j]=(g[j]+g[j^(1 <<i)])%mod; h[j]=(h[j]+h[j^(1 <<i)])%mod; } } } for (int i=0 ;i<(1 <<n);i++)sum[i]=f[i]*g[i]%mod*h[i]%mod; for (int i=n-1 ;i>=0 ;i--){ for (int j=0 ;j<(1 <<n);j++){ if ((j>>i)&1 )sum[j]=(sum[j]+mod-sum[j^(1 <<i)])%mod; } } int ans=0 ; for (int i=0 ;i<(1 <<n);i++){ ans+=sum[i]*cal (i);ans%=mod; } cout<<ans<<endl; }
Codeforces F. Bits And Pieces(位运算) - Cold_Chair - 博客园 (cnblogs.com)
题意:给定 n n n 个数的数组 d d d ,找到 $i\lt j\lt k $ 的 i , j , k i,j,k i , j , k ,使得 d i ∣ ( d j & d k ) d_i|(d_j \& d_k) d i ∣ ( d j & d k ) 最大
Sol:对于这样的多循环变量,要么就是直接枚举一维,其他方法解决其他维。还有就是直接换角度,算贡献和次数。
位运算的比较基本的题。
考虑枚举i,然后二进制位从大到小考虑, 对于第𝑤位,如果a i [ w ] a_{i}[w] a i [ w ] =1,那么对𝑗、𝑘并没有什么限制。
如果a i [ w ] a_{i}[w] a i [ w ] =0,那么我们希望( a j & a k ) [ w ] = 1 (a_{j}\&a_{k})[w]=1 ( a j & a k ) [ w ] = 1 ,结合前面的限制 ,就是给定x,问有没有x ∈ 𝑎 [ 𝑗 ] & 𝑎 [ 𝑘 ] ( 𝑖 < 𝑗 < 𝑘 ) x∈𝑎[𝑗]\&𝑎[𝑘](𝑖<𝑗<𝑘) x ∈ a [ j ] & a [ k ] ( i < j < k ) ,这个x需要满足前面高位为1限制还要满足当前位为1的限制。再考虑需要满足j,k严格大于i,我们需要满足对于一个集合的超集的最大位置和次大位置(在数组中的下标靠后)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 int n, m;int a[N];const int N = 1 <<21 ;const int len=21 ;int f[N+5 ];int g[N+5 ];void add (int val,int pos) { if (f[pos]<val){ g[pos]=f[pos]; f[pos]=val; } else if (g[pos]<val)g[pos]=val; } void solve () { cin>>n; for (int i=1 ;i<=n;i++){ cin>>a[i]; add (i,a[i]); } for (int i=0 ;i<len;i++){ for (int j=0 ;j<(N);j++){ if (((j>>i)&1 )==0 ){ add (f[j^(1 <<i)],j); add (g[j^(1 <<i)],j); } } } int ans=0 ; for (int i=1 ;i<=n-2 ;i++){ int x=0 ; for (int j=len-1 ;j>=0 ;j--){ if ((a[i]>>j)&1 )continue ; x+=1 <<j; if (g[x]<=i)x-=1 <<j; } ans=max (ans,a[i]|x); } cout<<ans<<endl; }