状压随笔
1.倒序枚举
作用:dp时用到,用来去掉后效性避免转移出错
其实跟01背包很像,一个思想
1 #include <bits/stdc++.h> 2 using namespace std; 3 int cow[1005]; 4 vector <int> a[20]; 5 int get1(int x) 6 { 7 int s=0; 8 while(x) 9 { 10 if(x&1)s++; 11 x=(x>>1); 12 } 13 return s; 14 } 15 int f[1100000]; 16 int main() 17 { 18 int n,d,k; 19 cin>>n>>d>>k; 20 for(int i=1;i<=n;i++) 21 { 22 int m; 23 scanf("%d",&m); 24 for(int j=1;j<=m;j++) 25 { 26 int num; 27 scanf("%d",&num); 28 cow[i]|=(1<<(num-1)); 29 } 30 } 31 for(int i=1;i<=n;i++)if(!cow[i])f[0]++; 32 for(int i=(1<<d)-1;i>=1;i--)//注意这里一定要倒序枚举(01背包) 33 for(int j=1;j<=n;j++) 34 { 35 f[i|cow[j]]=max(f[i|cow[j]],f[i]+1); 36 } 37 int ans=0; 38 for(int i=0;i<=(1<<d)-1;i++) 39 if(get1(i)==k)ans=max(ans,f[i]); 40 cout<<ans; 41 return 0; 42 }
2.正逆关系
比较经典的状压是先把状态压成二进制,然后分析每个状态从什么转移过来
比如放车放国王问题,炮兵阵地等等
方程通式是
f[i]=max(f[i],f[j]+k) (条件:状态ij都合法,j是i的子状态,并且转移合法)
这里按照需要可以加维
另外一种就是根据已有的状态推未知,比如愤怒的小鸟比较典型
1 #include<bits/stdc++.h> 2 using namespace std; 3 double geta(double x1,double x2,double y1,double y2) 4 { 5 double a=(x2*y1-x1*y2)/(x1*x2*(x1-x2)); 6 if(a>0)return 0; 7 return a; 8 } 9 double getb(double x1,double x2,double y1,double y2) 10 { 11 return (y1*x2*x2-y2*x1*x1)/(x1*x2*(x2-x1)); 12 } 13 bool pd(double x,double y) 14 { 15 if(x<y)swap(x,y); 16 if(x-y<=1e-9)return 1; 17 else return 0; 18 } 19 struct pig{ 20 double x,y; 21 }p[20]; 22 struct pwx{ 23 double a,b; 24 int state; 25 }an[400];int num; 26 int get1(int x) 27 { 28 int s=0; 29 while(x) 30 { 31 if(x&1)s++; 32 x=(x>>1); 33 } 34 return s; 35 } 36 vector <int> state[20]; 37 int n,m; 38 int f[1100000]; 39 int main() 40 { 41 int t; 42 cin>>t; 43 for(int i=1;i<=t;i++) 44 { 45 memset(p,0,sizeof(p)); 46 memset(an,0,sizeof(an)); 47 memset(f,0x3f,sizeof(f)); 48 for(int j=1;j<=n;j++)state[j].clear();//清空数组 49 num=0; 50 scanf("%d%d",&n,&m); 51 for(int j=1;j<=n;j++)scanf("%lf%lf",&p[j].x,&p[j].y); 52 for(int j=1;j<=n;j++) 53 for(int k=j+1;k<=n;k++) 54 { 55 double ta=geta(p[j].x,p[k].x,p[j].y,p[k].y); 56 if(ta==0)continue; 57 num++; 58 an[num].a=ta; 59 an[num].b=getb(p[j].x,p[k].x,p[j].y,p[k].y); 60 for(int l=1;l<=n;l++) 61 if(pd(an[num].a*p[l].x*p[l].x+an[num].b*p[l].x,p[l].y)) 62 an[num].state|=(1<<(l-1)); 63 } 64 for(int j=1;j<=n;j++) 65 an[++num].state|=(1<<(j-1)); 66 f[0]=0; 67 for(int j=1;j<=n;j++) 68 for(int k=1;k<=num;k++) 69 if((an[k].state>>(j-1))&1)state[j].push_back(an[k].state); 70 for(int j=0;j<=(1<<n)-1;j++) 71 { 72 for(int k=1;k<=n;k++) 73 { 74 if((j>>(k-1))&1)continue; 75 for(int p=0;p<state[k].size();p++) 76 f[j|state[k][p]]=min(f[j|state[k][p]],f[j]+1); 77 //可以从已有的扩展出dp,不一定非要按顺序 78 } 79 } 80 printf("%d\n",f[(1<<n)-1]); 81 } 82 return 0; 83 }
f[i|j]=min(f[i|j],f[i]+1) (条件:有一个候选集合j,已有状态i用来更新i|j的状态)
要学会灵活运用,枚举关系分层有序是主要第一种,集合枚举主要第二种
3.关于状压压什么
状压关键是压状态,把状态压成一个二进制数,特点是数据小,一般在20以下
这里的状态要满足可用二进制表示,即只有每一位表示的情况只有两种选择,选或不选(有时也可以用三进制),把这样的压成状态
还有关于附加维的问题,只有不能从状态中得到的信息才需要附加维维护
经常是推了半天思考怎么转移最后发现方程压根不对合适的方程与状态表示可以剩下很多时间并降低思维量
来道题 排列
这题能用stl水(爆搜),这里想状压
第一个关键点是要看出来要有一维表示余数,因为无论你怎么压状态余数肯定不能o1出来,所以要存一维
本来我想把拼出的数变成状态,每次在后边加1个1表示已经拼的位数,结果根本不可做,想下会发现这样实际上只有10种状态不靠谱,根本原因是没有解决怎么选数这个问题
其实正解是把原数字串当成状态,01分别表示这位上已选或未选,我们根本不用管已经拼出来了个啥数,既然是一位一位往后加,那么已选的数字个数就是出来的长度,管余数就行
f[i|(1<<(j-1))][(k*10+a[j])%d]+=f[i][k];
这里i是当前状态,j是挑的位数,k是余数,相当于每次往原来数后面加一个数字
初始化f[0][0]=1,转移条件是j这位的数没被选过,注意不仅仅是这一位没选过,看的是相同数字,比如1,3位上都有1,但不管j=1还是j=3加上去的都一样
所以这里要去重,用一个临时数组b,看这位上的数字有没有选过,没选才能转移
1 #include <bits/stdc++.h> 2 using namespace std; 3 int a[15],f[(1<<11)-1][1005]; 4 bool b[11]; 5 signed main() 6 { 7 ios::sync_with_stdio(false); 8 int tt; 9 cin>>tt; 10 for(int t=1;t<=tt;t++) 11 { 12 string s;int d; 13 cin>>s>>d; 14 int l=s.size(); 15 memset(a,0,sizeof(a)); 16 memset(f,0,sizeof(f)); 17 for(int i=0;i<l;i++)a[i+1]=s[i]-'0'; 18 f[0][0]=1; 19 for(int i=0;i<=(1<<l)-1;i++) 20 { 21 memset(b,0,sizeof(b)); 22 for(int j=1;j<=l;j++) 23 { 24 if(((i>>(j-1))&1)||(b[a[j]]))continue; 25 b[a[j]]=1; 26 for(int k=0;k<d;k++) 27 f[i|(1<<(j-1))][(k*10+a[j])%d]+=f[i][k]; 28 } 29 } 30 printf("%d\n",f[(1<<l)-1][0]); 31 } 32 return 0; 33 }
所以这里告诉我们:状态表示要可以通过状态表示复杂问题,一般处理起来麻烦的东西表示成状态,比如这里应该表示选数情况

浙公网安备 33010602011771号