状压 dp
就是把一连串的状态压缩成一个长的二进制数,可以起到减省空间、简便计算等作用。
这个二进制数的每一位都代表这一位的状态。
#P313. 特殊方格棋盘
标准的模板。
我们把每一列是否防止一辆车的状态化为 0 和 1,那么整体的状态就是一个 位的二进制数。
比如说, 时, 就表示第一列、第三列和第四列放。
由于一行只能放一个,我们可以得出:
抽象化地讲,对于状态 :
其中 表示满足 的所有 。
题目上说,有的格子不能放,那我们也可以把这个状态压缩一下:
对于一个点 ,我们开一个数组 来记录
如果说点 不能放,那么 。
这样我们在循环每个状态的时候就可以排除掉不能放的点,对于状态 :
就好了。
找一个数的最低一个一,使用 就好。
记得空间要开到 。
#include<bits/stdc++.h> using namespace std; int n,m; long long a[(1<<20)+1]; long long dp[(1<<20)+1]; inline int lowbit(int x) { return x&(-x); } int main() { cin>>n>>m; int mx=(1<<n)-1; for(int i=1;i<=m;i++) { int x,y;cin>>x>>y; a[x]|=(1<<(y-1)); } dp[0]=1; for(int i=1;i<=mx;i++) { int num=i,cnt=0; for(;num;num-=lowbit(num)) ++cnt; num=i&(~a[cnt]); while(num) { dp[i]+=dp[i^lowbit(num)]; num-=lowbit(num); } } cout<<dp[mx]; }
P1896 [SCOI2005] 互不侵犯
标准的状压 dp。
在设计状态时,要考虑到 两个变量,同时也有所有的状态。
定义 表示第 行,行状态为 ,有 个国王时的方案和。
考虑一个状态是合法的,如果这一位放了国王,那么上一行这里和这里的左右都不能放国王,这一行这个位置的左右也不能放国王。
我们设当前状态是 ,上一行的状态是 ,那么通过位运算可以得到合法条件:
如果这一行放了 个国王,那么上一行一定是 个。
其中 表示 状态时这一行有多少个国王,即二进制下 有多少个 。
和合法 我们都可以预处理求出,课件上给的求 的方法是每次右移一位,判有多少个 ,我的做法是每次 ,这样更快吧。
预处理:
for(int i=0;i<=(1<<n)-1;i++) { int s=i; int tot=0; while(s) { tot++; s-=lowbit(s); }s=i; // cout<<s<<" "<<tot<<endl; cnt[s]=tot; if((((s<<1)|(s>>1))&s)==0) legal[++num]=s; }
核心 dp:
dp[0][0][0]=1; for(int i=1;i<=n;i++)\\循环每一行 { for(int j=1;j<=num;j++)\\循环当前所有合法状态 { int s1=legal[j]; for(int o=1;o<=num;o++)\\循环可转移的上一行合法状态 { int s2=legal[o]; if(((s2|(s2<<1)|(s2>>1))&s1)==0) { for(int k=0;k<=mx;k++) { if(k-cnt[s1]>=0) dp[i][s1][k]+=dp[i-1][s2][k-cnt[s1]]; } } } } }
P2704 [NOI2001] 炮兵阵地
和上面两个题大同小异,都是棋盘式的。
设计状态: 表示当前行数为第 行,本行状态为 ,上一行状态为 。
判断一个状态 合不合法,首先要看本行内满足 ,这个可以预处理出来,这样我们可以简化后面 多重循环嵌套时的判断次数。
其次,要判断山地是否为 ,我们通过读入的数据计算。
如果循环到三个状态 都合法,那么可以转移:
其中 代表状态 中 的个数,可以用 求得。
这样就基本得解了。
因为第一行和第二行很特殊,所以需要单独处理,并且在循环第三行到第 行的时候要判断 是否等于 。
观察到每一行状态只与上一行有关,我们可以放心地滚掉一维 ~
#include<bits/stdc++.h> #define int short using namespace std; int n,m; int a[101]; int dp[2][(1<<10)+1][(1<<10)+1]; int r=1; int legal[(1<<10)+1]; int num; int cnt[(1<<10)+1]; inline int max(int x,int y) { return x>y?x:y; } inline int lowbit(int x) { return x&-x; } signed main() { cin>>n>>m; getchar(); for(int i=1;i<=n;i++) { for(int j=1;j<=m;j++) { char c;cin>>c; // cout<<c<<" "; if(c=='H') { a[i]|=(1<<(j-1)); } }//cout<<endl; } // dp[0][0]=1; for(int i=0;i<(1<<m);i++) { int s=i; if((((s<<2)|(s<<1)|(s>>1)|(s>>2))&s)==0) { legal[++num]=s; // cout<<i<<" "<<cnt[i]<<endl; // for(int j=1;j<=n;j++)dp[j][i]=1; } for(int tmp=s;tmp;tmp-=lowbit(tmp))cnt[s]++; } // for(int i=1;i<=num;i++) cout<<legal[i]<<" "<<cnt[legal[i]]<<endl; int ans=0; for(int i=1;i<=num;i++) { int s=legal[i]; if((s&a[1])==0) dp[r][s][0]=cnt[s],ans=max(ans,cnt[s]); } r^=1; if(n>1) { for(int i=1;i<=num;i++) { int s1=legal[i]; if((s1&a[2])==0) for(int j=1;j<=num;j++) { int s2=legal[j]; if((s2&s1)==0&&(s2&a[1])==0) { dp[r][s1][s2]=max(dp[r][s1][s2],dp[r^1][s2][0]+cnt[s1]); } } } for(int i=3;i<=n;i++) { r^=1; for(int j=1;j<=num;j++) { int s1=legal[j]; if((s1&a[i])==0) for(int k=1;k<=num;k++) { int s2=legal[k]; if((s2&a[i-1])==0) for(int o=1;o<=num;o++) { int s3=legal[o]; if((s3&a[i-2])==0) { if(((s3|s2)&s1)==0) dp[r][s1][s2]=max(dp[r][s1][s2],dp[r^1][s2][s3]+cnt[s1]); } } } } } } for(int i=1;i<=num;i++) { int s1=legal[i]; if((s1&a[n])==0) for(int j=1;j<=num;j++) { int s2=legal[j]; if((s2&a[n-1])==0&&(s2&s1)==0) { // cout<<dp[r][s1][s2]<<" "; ans=max(ans,dp[r][s1][s2]); } } } cout<<ans; return 0; }
P8756 [蓝桥杯 2021 省 AB2] 国际象棋
2024-04-03 18:17:54 星期三
这个就很有意思了。
我们观察到每行的每个状态都与上一行和上上一行有关,并且题目要求计算总共摆 个马的方案数,因此本题就像是互不侵犯和炮兵阵列的结合体。
定义状态 表示当前第 行,本行状态为 ,上行状态为 ,已经摆了 个马,可以得到状态转移方程:
其中 表示状态 含有的 的个数。(都是老套路)
由炮兵阵列我们可以知道第一行和第二行需要预处理,因为特殊。
for(int i=0;i<(1<<n);i++) dp[1][i][0][cnt[i]]=1; for(int s1=0;s1<(1<<n);s1++) { for(int s2=0;s2<(1<<n);s2++) { if((s1&(s2<<2))|(s1&(s2>>2))) continue; for(int j=0;j<=k;j++) { if(j-cnt[s1]>=0) dp[2][s1][s2][j]+=(dp[1][s2][0][j-cnt[s1]]); } } }
在转移时我们要注意判断是否合法,按照题目中给出的图去做就好了。
这里给出一个思考题:
判合法时需要看上一行状态 和上上一行状态 ,并且和 取与(具体请看代码),那么,为什么不能,也不需要 和 比较?
答案是这样的:如果 和 比较了,就会使得一部分你要用到的 被干掉,即是会把原本合法的状态判非法。
我就是这个思考了半个小时。
在代码的最后,统计第 行的所有方案之和即可。
以及,别别别忘了取模!
完整代码:
#include<bits/stdc++.h> using namespace std; int n,m,k; const int mod=1e9+7; int cnt[(1<<6)+1]; int dp[101][(1<<6)+1][(1<<6)+1][21]; inline int lowbit(int x) { return x&-x; } int main() { cin>>n>>m>>k; for(int s=0;s<(1<<n);s++) { for(int tmp=s;tmp;tmp-=lowbit(tmp))//计算 cnt 数组,lowbit 每次会找到这个数最低一个 1。 ++cnt[s]; } for(int i=0;i<(1<<n);i++) dp[1][i][0][cnt[i]]=1;//第一行处理 if(m>1){//防止被卡 for(int s1=0;s1<(1<<n);s1++) { for(int s2=0;s2<(1<<n);s2++) { if((s1&(s2<<2))|(s1&(s2>>2))) continue; for(int j=0;j<=k;j++) { if(j-cnt[s1]>=0) dp[2][s1][s2][j]+=(dp[1][s2][0][j-cnt[s1]]);//第二行处理 } } } if(m>2){//防止被卡 for(int i=3;i<=m;i++) { for(int s1=0;s1<(1<<n);s1++) { for(int s2=0;s2<(1<<n);s2++) { if((s1&(s2<<2))|(s1&(s2>>2))) continue;//判断上一行是否合法。 for(int s3=0;s3<(1<<n);s3++) { // if((s2&(s3<<1))|(s2&(s3>>1)))continue; 这里就是上面说的那个思考题,删掉注释只有 5 分。 if((s1&(s3<<1))|(s1&(s3>>1))) continue;//判断上上一行是否合法。 for(int j=0;j<=k;j++) { if(j-cnt[s1]>=0)//防止 RE。 dp[i][s1][s2][j]+=(dp[i-1][s2][s3][j-cnt[s1]]); dp[i][s1][s2][j]%=mod; } } } } } }} long long ans=0; for(int s1=0;s1<(1<<n);s1++) { for(int s2=0;s2<(1<<n);s2++) { if((s1&(s2<<2))|(s1&(s2>>2))) continue; ans+=dp[m][s1][s2][k]; ans%=mod; } } cout<<ans; }
也可以加一个滚动数组优化时空,能减少 30MB 空间和 200ms 时间。
P5005 中国象棋 - 摆上马
2024-04-05 11:00:58 星期五
此题和上一道题几乎一样,去掉第四维、修改判断非法的语句即可。
但是难点在判断非法上。
这个题会有蹩马腿的事情,这就会导致一个问题:
- 如果一匹马的左边有马,那么它攻击不到上一行左边的马;
- 如果一匹马的右边有马,那么它攻击不到上一行右边的马;
- 如果一匹马的上面有马,那么它攻击不到上上一行的所有马。
因此,答案会比不考虑蹩马腿的答案要多。
加上判断就行。
for(int i=3;i<=m;i++) { r^=1; memset(dp[r],0,sizeof(dp[r])); for(int s1=0;s1<(1<<n);s1++) { for(int s2=0;s2<(1<<n);s2++) { if((((s1&(~(s1>>1)))&(s2>>2))||((s1&(~(s1<<1)))&(s2<<2))||(((s2&(~(s2>>1)))&(s1>>2)))||((s2&(~(s2<<1)))&(s1<<2)))) continue; //奇妙的判断 for(int s3=0;s3<(1<<n);s3++) { if((((s1&(~s2))&(s3>>1))||((s1&(~s2))&(s3<<1))||((s3&(~s2))&(s1>>1))||((s3&(~s2))&(s1<<1)))) continue; dp[r][s1][s2]+=(dp[r^1][s2][s3]); dp[r][s1][s2]%=mod; } } } }
P8733 [蓝桥杯 2020 国 C] 补给
2024-03-31 16:01:36 星期日
这是状压的非传统棋盘的表现形式。
注意到末态必须是经过所有的村庄,并且最大的 只有 ,那我们可以直接状压。
设 表示现在的状态是 ,并且现在在第 个村庄,要到第 个村庄,我们需要满足的条件是:
- 中不包含 ;
- 到 可以走。
第一个很好判断,第二个的话可以先预处理出 表示从 到 的直线距离,再跑个 全源最短路找到能走的最短路径,然后就能直接跑状压 了。
for(int k=1;k<=n;k++)//Floyd 全源最短路 for(int i=1;i<=n;i++) for(int j=1;j<=n;j++) dis[i][j]=min(dis[i][j],dis[i][k]+dis[k][j]); memset(dp,0x7f,sizeof(dp)); dp[1][1]=0; for(int s=0;s<(1<<n);s++) { for(int i=1;i<=n;i++) { if(s&(1<<(i-1))==0) continue;//如果 i 不在在 s 里面就跳过 for(int j=1;j<=n;j++) { if((s^(1<<(i-1))>>(j-1))&1)// dp[s][i]=min(dp[s][i],dp[s^(1<<(i-1))][j]+dis[j][i]); } } }
考虑走完全程之后仍需要回到出发点,再最后计算答案时还要加上 。
P1433 吃奶酪
和上面一个题一模一样,把起点 当成虚拟原点就好。
P1171 售货员的难题
和上面两道题一样,不需要跑 Floyd。
P2831 [NOIP2016 提高组] 愤怒的小鸟
这个就和上面的所有题很不一样了。
观察题目:我们先要找到所有能攻击的抛物线,并且最大化抛物线打到的猪的个数。
所以,需要设一个 init
函数预处理,遍历所有的猪的坐标对。
为什么是两个?我们观察题目给出的抛物线模板 ,因为其必过原点,所以只需要两点即可确定。
设找到的两点分别是 和 ,可以列出:
两个未知数两个方程,可解,最终得出来的式子是这样的:
处理每条抛物线时,需要判断分母是否为零,要不然不能除。并且要判断 的正负。
然后对于每个抛物线,遍历全部的猪,看有没有在这条线上,然后定义一个状态 ,如果一个点在抛物线上,就把 中这个位置标位 ,以此初始化 数组。
因为或运算的性质,不需要考虑判重的情况(无法用言语表述了……)。
然后定义 表示状态状态 下最小需要的鸟的个,对于每个状态,循环一每组猪对转移即可。
#include<bits/stdc++.h> using namespace std; int t; int n,m; const int N=19; struct node{ double x,y; }a[N]; int dp[(1<<N)]; const double minn=1e-8; int b[N][N]; void calc(int i,int j,double &x,double &y) { double x1=a[j].x*a[j].x*a[i].x-a[i].x*a[i].x*a[j].x; if(fabs(x1)<minn)return ; x=(a[j].y*a[i].x-a[i].y*a[j].x)/(x1); x1=a[j].x*a[j].x-a[i].x*a[j].x; if(fabs(x1)<minn)return ; y=(a[i].y/a[i].x)-(a[j].y*a[i].x-a[i].y*a[j].x)/(x1); } int cnt; void init() { for(int i=1;i<=n;i++) { for(int j=1;j<=n;j++) { // cout<<i<<" "<<j<<endl; if(fabs(a[i].x-a[j].x)<minn) continue; double x=0,y=0; calc(i,j,x,y); bool flag=0; if(x>0) continue; // cout<<x<<" "<<y<<endl; int s=0; for(int k=1;k<=n;k++) { double x1=a[k].x,y2=a[k].y; if(fabs(x1*x1*x+x1*y-y2)<minn) { s|=(1<<(k-1)); dp[s]=dp[1<<(k-1)]=1; } } b[i][j]=b[j][i]=s; } } } int main() { cin>>t; while(t--) { memset(b,0,sizeof(b)); cnt=0; cin>>n>>m; memset(dp,0x7f,sizeof(dp)); dp[0]=0; for(int i=1;i<=n;i++) { cin>>a[i].x>>a[i].y; } init(); for(int s=0;s<(1<<n);s++) { for(int i=1;i<=n;i++) { if((s&(1<<(i-1)))!=0) continue;\\如果这只猪在此状态里似了,直接跳过,避免不必要的循环 for(int j=1;j<=i;j++) { if((s&(1<<(j-1)))!=0) continue; dp[s|b[i][j]]=min(dp[s|b[i][j]],dp[s]+1); } dp[s|(1<<(i-1))]=min(dp[s|(1<<(i-1))],dp[s]+1); } } cout<<dp[(1<<n)-1]<<endl; } }
P4163 [SCOI2007] 排列
2024-04-03 17:19:25 星期三
这个题就很玄学了。
有 个数组成排列,问能被 整除的数的个数。
当选数的状态是 ,组成的数字是 时,考虑其被 整除的余数是 ,如果再加一个数 , 会变成 ,这个数模 就是 模 的结果,写成式子就是:
应该是这么写的吧,同余方程已经忘了
定义 表示状态为 ,当前数模 的个数。
因此我们可以得出转移方程:
对于状态 ,一个数 ,并且 ,有:
还有细节!
比如: 这个组合,显然在循环的时候会把 分别多计算 次,因此我们在答案最后要除以每个数字出现个数的阶乘。
for(int i=0;i<=12;i++) { if(f[cnt[i]]) dp[(1<<len)-1][0]/=f[cnt[i]]; }
#P333. [Usaco2005 Open]Disease Manangement 疾病管理
2024-04-06 22:06:03 星期六
好像不用 就可以。
我们先处理出每只牛对于的病的状态 ,然后预处理每个状态的 的个数,都是老套路。
然后对于一个状态 ,如果一头牛的病是 的子集,那么这个状态对应的牛的个数就要加一,统计即可。
注意状态是以 为准,而不是 。
#P335. [Sdoi2009]Bill的挑战
神奇的小题。
玄学。。
#include<bits/stdc++.h> using namespace std; int t,n,d; int a[51][51]; int dp[51][(1<<15)+1]; const int mod=1000003; char s[16][51]; int main() { cin>>t; while(t--) { cin>>n>>d; int len; memset(dp,0,sizeof(dp)); memset(a,0,sizeof(a)); for(int i=0;i<n;i++)scanf("%s",s[i]); len=strlen(s[0]); for(int i=0;i<len;i++) { for(int j=0;j<26;j++) { for(int k=0;k<n;k++) { if(s[k][i]==j+'a'||s[k][i]=='?') a[i][j]|=(1<<k); } } } dp[0][(1<<n)-1]=1; for(int i=0;i<len;i++) { for(int j=0;j<(1<<n);j++) { if(dp[i][j]) { for(char k='a';k<='z';k++) { dp[i+1][j&a[i][k-'a']]=(dp[i][j]+dp[i+1][j&a[i][k-'a']])%mod; } } } } long long ans=0; for(int i=0;i<(1<<n);i++) { int tot=0; for(int j=0;j<n;j++) { if(i&(1<<j)) ++tot; } if(tot==d) ans=(ans+dp[len][i])%mod; } cout<<ans<<endl; } }
P3226 [HNOI2012] 集合选数
最离谱的状压了(神秘构造)。
在看到怎么状压之前甚至不知道怎么状压。。
观察到:要求每个集合如果存在 则不存在 和 ,怎么搞呢?
我们把它想象成这样:
或者把它写成数字:
观察到如果矩阵中有一个数字,那么一定没有它下面和右面的数字。
这是什么??!
芝士 棋盘!
但是不可能一个棋盘就包含所有 的数字,我们可以开多个这样的棋盘和一个 数组实现全覆盖。
最终答案是每个棋盘格的答案乘积(别忘了取模)。
核心代码:
inline void init(int x) { for(int i=1;i<=11;i++) { if(i==1) a[i][1]=x; else a[i][1]=a[i-1][1]*3; if(a[i][1]>n) break; vis[a[i][1]]=1;lin[i]=1,ed=i; for(int j=2;j<=18;j++) { a[i][j]=a[i][j-1]*2; if(a[i][j]>n) break; lin[i]=j; vis[a[i][j]]=1; } lim[i]=(1<<lin[i])-1; } } int dp[51][(1<<18)]; long long res; inline void solve(int x) { res=0; for(int i=0;i<=lim[1];i++) dp[1][i]=g[i]; for(int i=2;i<=ed;i++) { for(int j=0;j<=lim[i];j++) { if(!g[j])continue; dp[i][j]=0; for(int k=0;k<=lim[i-1];k++) { if(k&j||!g[k])continue; dp[i][j]+=dp[i-1][k],dp[i][j]%=mod; } } } for(int i=0;i<=lim[ed];i++) { res+=dp[ed][i];res%=mod; } }
事实上,状压到这里要告一段落了。还有 道题单里的题没写,先溜了 ~~
2024.04.10
刚好是表哥生日
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!