有的DP题,某一部分的状态只有两种,选或不选。
开数组记录,代价太大,转移不方便。
状态压缩意为,用 “0/1“ 表示 “选/不选“ 。
把状态表示为二进制整数。
There are 10 kinds of people in the world, who knows binary and who doesn't.
用位运算判断条件并转移状态。
hdu 6149 Valley Numer II
用f[i][j]表示选到前i个点,状态为j的答案。
枚举其他两个高点。
转移之前判断之前是否用过,以及高低点之间是否有连边。
所以用邻接矩阵表示连边比较方便。
1 #include<cstdio> 2 #include<cstring> 3 #include<algorithm> 4 using namespace std; 5 6 int t; 7 int n,m,k; 8 int c[35][35]; 9 int h[35]; 10 int v[35]; 11 int f[35][(1<<16)+1]; 12 int ans; 13 14 int main() 15 { 16 scanf("%d",&t); 17 while(t--) 18 { 19 memset(f,0,sizeof(f)); 20 memset(c,0,sizeof(c)); 21 memset(h,0,sizeof(h)); 22 memset(v,0,sizeof(v)); 23 scanf("%d%d%d",&n,&m,&k); 24 for(int i=1;i<=m;i++) 25 { 26 int q,w; 27 scanf("%d%d",&q,&w); 28 c[q][w]=1; 29 c[w][q]=1; 30 } 31 for(int i=0;i<k;i++) 32 { 33 scanf("%d",&h[i]); 34 v[h[i]]=1; 35 } 36 int st=(1<<k); 37 ans=0; 38 for(int i=1;i<=n;i++) 39 { 40 for(int j=0;j<st;j++)f[i][j]=f[i-1][j]; 41 if(v[i])continue; 42 for(int j=0;j<st;j++) 43 { 44 for(int q1=0;q1<k;q1++) 45 { 46 if(j&(1<<q1))continue; 47 if(!c[i][h[q1]])continue; 48 for(int q2=0;q2<q1;q2++) 49 { 50 if(j&(1<<q2))continue; 51 if(!c[i][h[q2]])continue; 52 f[i][(j|(1<<q1))|(1<<q2)]=max(f[i][(j|(1<<q1))|(1<<q2)],f[i-1][j]+1); 53 } 54 } 55 } 56 } 57 for(int j=0;j<st;j++)ans=max(ans,f[n][j]); 58 printf("%d ",ans); 59 } 60 return 0; 61 }
CodeForces 895C Square Subsets
一道数学思想浓重的状压DP。
因为1~70只有19个质数(用pr[]储存每个质数),先预处理出每个数的质因子组成c。
c[i]的第k位表示i这个数含有几个pr[k]因子。含有奇数个,那一位就是1,,否则为零。
f[i][j]表示选到第i个数,此时所有选的数的积的质因子组成为j的时候的方案数。
那个“组成”与之前预处理的表示方法类似,每一个二进制位代表一个质因子的情况,0为偶数个,1为奇数个。
读入的时候记录每个数被读进来的次数。
如果读入的数据中含有x这个数,就对x进行一次计算。
具体来说,x可以选奇数个也可以选偶数个。
偶数个的情况状态不变。奇数个的情况,状态异或上x这个数的组成c[x]。
两个加一起,乘上方案数,取个模即可。
最后因为要求是平方数,所以所有质因子都有偶数个,状态为零。
输出f[nw][0]。
1 #include<cstdio> 2 #include<cstring> 3 #include<algorithm> 4 #define mod 1000000007 5 #define ll long long 6 using namespace std; 7 8 int n; 9 int h[75]; 10 int c[75]; 11 int pr[25]={2,3,5,7,11,13,17,19,23,29,31,37,41,43,47,53,59,61,67,71}; 12 ll f[2][1<<20]; 13 ll pow[100005]; 14 int main() 15 { 16 scanf("%d",&n); 17 for(int i=1;i<=n;i++) 18 { 19 int t; 20 scanf("%d",&t); 21 h[t]++; 22 } 23 for(int i=1;i<=70;i++) 24 { 25 int t=i; 26 int p=0; 27 while(t>1) 28 { 29 while(t%pr[p]==0) 30 { 31 t/=pr[p]; 32 c[i]^=(1<<p); 33 } 34 p++; 35 } 36 } 37 pow[0]=1; 38 for(int i=1;i<=n;i++)pow[i]=(pow[i-1]<<1)%mod; 39 f[0][0]=1; 40 int nw=0; 41 for(int i=1;i<=70;i++) 42 { 43 if(!h[i])continue; 44 nw^=1; 45 for(int j=0;j<(1<<19);j++) 46 { 47 f[nw][j]=(f[nw^1][j^c[i]]+f[nw^1][j])%mod*pow[h[i]-1]%mod; 48 } 49 } 50 printf("%d",f[nw][0]-1); 51 return 0; 52 }
CodeForces 482C Game with Strings
一道期望+状压DP......
摧残大脑的推导与状态转移。
强荐zhx大佬的题解,写的非常非常明白。zhx大佬的题解传送门
作为蒟蒻我就只能大致解释解释代码了QwQ
f[i]代表问了状态为i的问题时,距离确定字符串(终点结果)的期望次数。
显然,f[一个能确定字符串的问法]=0。
接下来的难点在于如何判断每种问法不能确定哪些字符串。
设dbt[i]代表问法为i时,不能区分的字符串有哪些。
用二进制数表示,每一位代表一个字符串,1代表不能确定,0反之。
num[i]代表问法为i时,不能区分的串的个数,也就是dbt[i]中1的个数。
接下来玄学公式和位运算转移状态,zhx大佬的题解里写的很详细,本蒟蒻在此不做赘述。
1 #include<cstdio> 2 #include<cstring> 3 #include<algorithm> 4 #define ll long long 5 using namespace std; 6 7 int n,m; 8 char s[55][25]; 9 ll dbt[1<<20]; 10 int num[1<<20]; 11 double f[1<<20]; 12 13 int main() 14 { 15 scanf("%d",&n); 16 for(int i=0;i<n;i++) 17 { 18 scanf("%s",s[i]); 19 } 20 if(n==1){printf("0.000000000");return 0;} 21 m=strlen(s[1]); 22 for(int i=0;i<n;i++) 23 { 24 for(int j=i+1;j<n;j++) 25 { 26 ll st=0; 27 for(int k=0;k<m;k++) 28 { 29 if(s[i][k]==s[j][k])st|=(1ll<<k); 30 } 31 dbt[st]|=(1ll<<i); 32 dbt[st]|=(1ll<<j); 33 } 34 } 35 for(int i=(1<<m)-1;i>=1;i--) 36 { 37 for(int j=0;j<m;j++) 38 { 39 if(i&(1<<j))dbt[i^(1<<j)]|=dbt[i]; 40 } 41 } 42 for(int i=0;i<(1<<m);i++) 43 { 44 for(int j=0;j<n;j++) 45 { 46 if(dbt[i]&(1ll<<j))num[i]++; 47 } 48 } 49 f[(1<<m)-1]=0.00; 50 for(int i=(1<<m)-2;i>=0;i--) 51 { 52 if(!num[i]){f[i]=0.00;continue;} 53 int tot=m; 54 for(int j=0;j<m;j++) 55 { 56 if(i&(1<<j))tot--; 57 } 58 for(int j=0;j<m;j++) 59 { 60 if(i&(1<<j))continue; 61 f[i]+=f[i|(1<<j)]*1.0/(double)(tot)*(double)(num[i|(1<<j)])/(double)(num[i]); 62 } 63 f[i]+=1.00; 64 } 65 printf("%.9lf",f[0]); 66 return 0; 67 }
hdu 6125 Free from square
由于对于一个合法的答案,sqrt(500)后的素数最多只会用到一个,所以只对前8个素数状压后01背包。
f(S, k)表示素数状态为S时由k个自然数组成的方法有几种。
再对后面的素数分别01背包。
1 #include<cstdio> 2 #include<cstring> 3 #include<algorithm> 4 #define mod 1000000007 5 #define ll long long 6 using namespace std; 7 8 int t,n,K; 9 int num[1<<8]; 10 bool pr[505]; 11 ll f[1<<8][505]; 12 ll ans; 13 14 int main() 15 { 16 num[0]=1; 17 int k=0; 18 for(int i=2;i<505;i++) 19 { 20 if(!pr[i]) 21 { 22 if(k<8)num[1<<k]=i,k++; 23 for(int j=i<<1;j<505;j+=i)pr[j]=1; 24 } 25 } 26 for(int i=1;i<(1<<8);i++) 27 { 28 int j=(i-1)&i; 29 num[i]=num[j]*num[i^j]; 30 if(num[i]>505)num[i]=505; 31 } 32 scanf("%d",&t); 33 while(t--) 34 { 35 scanf("%d%d",&n,&K); 36 memset(f,0,sizeof(f)); 37 f[0][0]=1; 38 for(int i=0;i<(1<<8);i++) 39 { 40 if(num[i]<=n) 41 { 42 int ss =((1<<8)-1)^i; 43 for(int k=K;k>=1;k--) 44 for(int j=ss;;j=(j-1)&ss) 45 { 46 f[i|j][k]=(f[i|j][k]+f[j][k-1])%mod; 47 if(j==0)break; 48 } 49 } 50 } 51 for(int i=23;i<=n;i++) 52 { 53 if(pr[i])continue; 54 for(int k=K;k>0;k--) 55 { 56 for(int j=0;j<(1<<8);j++) 57 { 58 if(num[j]*i<=n) 59 { 60 int ss=((1<<8)-1)^j; 61 for(int s=ss;;s=(s-1)&ss) 62 { 63 f[j|s][k]=(f[s][k-1]+f[j|s][k])%mod; 64 if(s==0)break; 65 } 66 } 67 } 68 } 69 } 70 ans=0; 71 for(int i=0;i<(1<<8);i++) 72 for(int k=K;k>=1;k--) 73 ans=(ans+f[i][k])%mod; 74 printf("%lld ",ans); 75 } 76 return 0; 77 }
CodeForces 449D Jzzhu and Numbers
传说中的官方题解:
(如果图看起来长宽比不对,查看原图即可)
想法不是很简单。
脑洞大,真的想不出来。
看了题解瞎打了一会,cf测,过不了test5。
盯着code看了半个小时,发现是a数组开小了,少打了个0。
话说a数组本来也没什么用是吧。
1 #include<cstdio> 2 #include<cstring> 3 #include<algorithm> 4 #define ll long long 5 #define mod 1000000007 6 using namespace std; 7 8 int n; 9 int a[1000005]; 10 int f[1<<20][20]; 11 12 ll ksm(int B,int p) 13 { 14 ll ret=1; 15 ll b=B; 16 while(p) 17 { 18 if(p&1)ret=(ret*b)%mod; 19 b=(b*b)%mod; 20 p>>=1; 21 } 22 return ret; 23 } 24 25 int cnt(int x) 26 { 27 int ret=0; 28 while(x)x-=(x&(-x)),ret++; 29 return (ret&1)?-1:1; 30 } 31 32 int main() 33 { 34 scanf("%d",&n); 35 for(int i=1;i<=n;i++)scanf("%d",&a[i]),f[a[i]][0]++; 36 for(int i=1;i<=20;i++) 37 { 38 for(int j=0;j<(1<<20);j++) 39 { 40 if(1&(j>>(i-1)))f[j][i]=f[j][i-1]; 41 else f[j][i]=f[j][i-1]+f[j+(1<<(i-1))][i-1]; 42 } 43 } 44 ll ans=0; 45 for(int i=0;i<(1<<20);i++) 46 { 47 ll tmp=((ksm(2,f[i][20])-1)*cnt(i)%mod+mod)%mod; 48 ans=(ans+tmp+mod)%mod; 49 } 50 printf("%I64d",ans); 51 return 0; 52 }
hdu 5713 K个联通块
跟前面某些题相比还是比较友善的。
状态不是很难想,转移也是显然,就是中间的步骤比较多,倒起来有些复杂。
首先算出num[i]表示选点为i状态时,i中这些点相互连边的方案数。
具体做法是找出lowbit代表的那个点,跟前面的点一一计算累加到答案上。
再算出f[i][p]代表所选点集为i时,构成p个联通块的方案数。
第一步要算出f[i][1]。
利用容斥原理,用所有的方法减去不合法的方法。
再利用f[i][1]算出f[i][2...k]。
最后的答案为f[所有点都选((1<<n)-1)][k]。
注意取模。
模数很奇葩。
1 #include<cstdio> 2 #include<cstring> 3 #include<algorithm> 4 #define ll long long 5 #define mod 1000000009 6 using namespace std; 7 8 int t,n,m,k; 9 int e[20][20],num[1<<14]; 10 ll f[1<<14][20]; 11 12 int count(int nw,int p) 13 { 14 int ret=0; 15 for(int i=1;i<=n;i++) 16 { 17 if((1<<(i-1))!=p)continue; 18 p=i; 19 break; 20 } 21 for(int i=1;i<=n;i++) 22 { 23 if(nw&(1<<(i-1)))ret+=e[i][p]; 24 } 25 return ret; 26 } 27 28 int main() 29 { 30 scanf("%d",&t); 31 for(int cs=1;cs<=t;cs++) 32 { 33 memset(e,0,sizeof(e)); 34 memset(num,0,sizeof(num)); 35 memset(f,0,sizeof(f)); 36 scanf("%d%d%d",&n,&m,&k); 37 for(int i=1;i<=m;i++) 38 { 39 int v1,v2; 40 scanf("%d%d",&v1,&v2); 41 e[v1][v2]=e[v2][v1]=1; 42 } 43 for(int i=1;i<(1<<n);i++) 44 { 45 int lb=i&(-i); 46 num[i]=num[i^lb]+count(i,lb); 47 } 48 for(int i=1;i<(1<<n);i++) 49 { 50 int lb=i&(-i); 51 ll no=0; 52 for(int j=i^lb;j;j=((j-1)&(i^lb))) 53 { 54 no=(no+f[i^j][1]*1ll%mod*(1ll<<num[j])%mod)%mod; 55 } 56 f[i][1]=(1ll*(1ll<<num[i])%mod-no)%mod; 57 } 58 for(int i=1;i<(1<<n);i++) 59 { 60 for(int j=2;j<=k;j++) 61 { 62 int lb=i&(-i); 63 for(int h=i^lb;h;h=((h-1)&(i^lb))) 64 { 65 f[i][j]=(f[i][j]+f[h][j-1]*f[i^h][1]%mod)%mod; 66 } 67 } 68 } 69 printf("Case #%d: %lld ",cs,(f[(1<<n)-1][k]%mod+mod)%mod); 70 } 71 return 0; 72 }