状压 dp

就是把一连串的状态压缩成一个长的二进制数,可以起到减省空间、简便计算等作用。

这个二进制数的每一位都代表这一位的状态。

#P313. 特殊方格棋盘

标准的模板。

我们把每一列是否防止一辆车的状态化为 0 和 1,那么整体的状态就是一个 n 位的二进制数。

比如说,n=4 时,01101 就表示第一列、第三列和第四列放。

由于一行只能放一个,我们可以得出:

dp(01101)=dp(01100)+dp(01001)+dp(00101)

抽象化地讲,对于状态 S

dp(S)+=dp(S2i)

其中 i 表示满足 S and 1<<(i+1)=1 的所有 i

题目上说,有的格子不能放,那我们也可以把这个状态压缩一下:

对于一个点 (x,y),我们开一个数组 a 来记录

a[x]=a[x] or 1<<(y1)

如果说点 (2,3) 不能放,那么 a[2]=100

这样我们在循环每个状态的时候就可以排除掉不能放的点,对于状态 S:

S=S and neg(x)

就好了。

找一个数的最低一个一,使用 lowbit 就好。

记得空间要开到 2n

#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。

在设计状态时,要考虑到 n,k 两个变量,同时也有所有的状态。

定义 dp[i][s][j] 表示第 i 行,行状态为 s,有 j 个国王时的方案和。

考虑一个状态是合法的,如果这一位放了国王,那么上一行这里和这里的左右都不能放国王,这一行这个位置的左右也不能放国王。

我们设当前状态是 s1,上一行的状态是 s2,那么通过位运算可以得到合法条件:

((s1<<1) or (s1>>1)) and s1=0

(s2 or (s2<<1) or (s2>>1)) and s1=0

如果这一行放了 k 个国王,那么上一行一定是 ksum[s] 个。

其中 sum[s] 表示 s 状态时这一行有多少个国王,即二进制下 s 有多少个 1

sum[s] 和合法 s 我们都可以预处理求出,课件上给的求 sum[k] 的方法是每次右移一位,判有多少个 1,我的做法是每次 s=lowbit(s),这样更快吧。

预处理:

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] 炮兵阵地

和上面两个题大同小异,都是棋盘式的。

设计状态:dp[i][s1][s2] 表示当前行数为第 i 行,本行状态为 s1,上一行状态为 s2

判断一个状态 s 合不合法,首先要看本行内满足 [(s<<1) or (s<<2) or (s>>1) or (s>>2)] and s=0,这个可以预处理出来,这样我们可以简化后面 dp 多重循环嵌套时的判断次数。

其次,要判断山地是否为 0,我们通过读入的数据计算。

如果循环到三个状态 s1,s2,s3 都合法,那么可以转移:

dp[i][s1][s2]=max(dp[i][s1][s2],dp[i1][s2][s3]+cnt[s1])

其中 cnt[s] 代表状态 s1 的个数,可以用 lowbit 求得。

这样就基本得解了。

因为第一行和第二行很特殊,所以需要单独处理,并且在循环第三行到第 n 行的时候要判断 n 是否等于 1

观察到每一行状态只与上一行有关,我们可以放心地滚掉一维 ~

#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 星期三

这个就很有意思了。

我们观察到每行的每个状态都与上一行和上上一行有关,并且题目要求计算总共摆 K 个马的方案数,因此本题就像是互不侵犯炮兵阵列的结合体。

定义状态 dp[i][s1][s2][j] 表示当前第 i 行,本行状态为 s1,上行状态为 s2,已经摆了 j 个马,可以得到状态转移方程:

dp[i][s1][s2][j]+=dp[i1][s2][s3][jcnt[s1]]

其中 cnt[s] 表示状态 s 含有的 1 的个数。(都是老套路)

炮兵阵列我们可以知道第一行和第二行需要预处理,因为特殊。

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]]);
}
}
}

在转移时我们要注意判断是否合法,按照题目中给出的图去做就好了。

这里给出一个思考题:

判合法时需要看上一行状态 s2 和上上一行状态 s3,并且和 s1 取与(具体请看代码),那么,为什么不能,也不需要 s2s3 比较?

答案是这样的:如果 s2s3 比较了,就会使得一部分你要用到的 dp[i1][...][...][...] 被干掉,即是会把原本合法的状态判非法。

我就是这个思考了半个小时

在代码的最后,统计第 m 行的所有方案之和即可。

以及,别别别忘了取模!

完整代码:

#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 星期日

这是状压的非传统棋盘的表现形式。

注意到末态必须是经过所有的村庄,并且最大的 N 只有 20,那我们可以直接状压。

dp[s][i] 表示现在的状态是 s,并且现在在第 i 个村庄,要到第 j 个村庄,我们需要满足的条件是:

  • s 中不包含 j
  • ij 可以走。

第一个很好判断,第二个的话可以先预处理出 dis[i][j] 表示从 ij 的直线距离,再跑个 Floyd 全源最短路找到能走的最短路径,然后就能直接跑状压 dp 了。

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]);
}
}
}

考虑走完全程之后仍需要回到出发点,再最后计算答案时还要加上 dis[i][1]

P1433 吃奶酪

和上面一个题一模一样,把起点 (0,0) 当成虚拟原点就好。

P1171 售货员的难题

和上面两道题一样,不需要跑 Floyd。

P2831 [NOIP2016 提高组] 愤怒的小鸟

这个就和上面的所有题很不一样了。

观察题目:我们先要找到所有能攻击的抛物线,并且最大化抛物线打到的猪的个数。

所以,需要设一个 init 函数预处理,遍历所有的猪的坐标对。

为什么是两个?我们观察题目给出的抛物线模板 y=ax2+bx,因为其必过原点,所以只需要两点即可确定。

设找到的两点分别是 (x1,y1)(x2,y2),可以列出:

{y1=ax12+bx1y2=ax22+bx2

两个未知数两个方程,可解,最终得出来的式子是这样的:

{a=y2x1y1x2x22x1x12x2b=y1x1y2x1y1x2x2x2x1x2

处理每条抛物线时,需要判断分母是否为零,要不然不能除。并且要判断 a 的正负。

然后对于每个抛物线,遍历全部的猪,看有没有在这条线上,然后定义一个状态 s,如果一个点在抛物线上,就把 s 中这个位置标位 1,以此初始化 dp 数组。

因为或运算的性质,不需要考虑判重的情况(无法用言语表述了……)。

然后定义 dp[s] 表示状态状态 s 下最小需要的鸟的个,对于每个状态,循环一每组猪对转移即可。

dp[s|b[i][j]]=min(dp[s|b[i][j]],dp[s]+1)

#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 星期三

这个题就很玄学了。

n 个数组成排列,问能被 d 整除的数的个数。

当选数的状态是 s,组成的数字是 a 时,考虑其被 d 整除的余数是 k,如果再加一个数 num[i]a 会变成 a×10+num[i],这个数模 d 就是 k×10+num[i]d 的结果,写成式子就是:

a×10+num[i]k×10+num[i](modd)

应该是这么写的吧,同余方程已经忘了

定义 dp[s][k] 表示状态为 s,当前数模 d=k 的个数。

因此我们可以得出转移方程:

对于状态 s,一个数 num[i],并且 s and (1<<(i1))=0,有:

dp[s|(1<<(i1))][(k×10+num[i])%d]+=dp[s][k]

还有细节!

比如:001122 这个组合,显然在循环的时候会把 0,1,2 分别多计算 2 次,因此我们在答案最后要除以每个数字出现个数的阶乘。

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 星期六

好像不用 dp 就可以。

我们先处理出每只牛对于的病的状态 a[i],然后预处理每个状态的 1 的个数,都是老套路。

然后对于一个状态 s,如果一头牛的病是 s 的子集,那么这个状态对应的牛的个数就要加一,统计即可。

注意状态是以 d 为准,而不是 n

#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] 集合选数

最离谱的状压了(神秘构造)。

在看到怎么状压之前甚至不知道怎么状压。。

观察到:要求每个集合如果存在 i 则不存在 2i3i,怎么搞呢?

我们把它想象成这样:

[i2i4i8i3i6i12i24i9i18i36i72i]

或者把它写成数字:

[12483612249183672]

观察到如果矩阵中有一个数字,那么一定没有它下面和右面的数字。

这是什么??!

芝士 棋盘

但是不可能一个棋盘就包含所有 [1,n] 的数字,我们可以开多个这样的棋盘和一个 vis 数组实现全覆盖。

最终答案是每个棋盘格的答案乘积(别忘了取模)。

核心代码:

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;
}
}



事实上,状压到这里要告一段落了。还有 3 道题单里的题没写,先溜了 ~~

2024.04.10

刚好是表哥生日

END


posted @   ccjjxx  阅读(32)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!
点击右上角即可分享
微信分享提示