状压 dp
就是把一连串的状态压缩成一个长的二进制数,可以起到减省空间、简便计算等作用。
这个二进制数的每一位都代表这一位的状态。
#P313. 特殊方格棋盘
标准的模板。
我们把每一列是否防止一辆车的状态化为 0 和 1,那么整体的状态就是一个 \(n\) 位的二进制数。
比如说,\(n=4\) 时,\(01101\) 就表示第一列、第三列和第四列放。
由于一行只能放一个,我们可以得出:
抽象化地讲,对于状态 \(S\):
其中 \(i\) 表示满足 \(S\text{ and }1<<(i+1)=1\) 的所有 \(i\)。
题目上说,有的格子不能放,那我们也可以把这个状态压缩一下:
对于一个点 \((x,y)\),我们开一个数组 \(a\) 来记录
如果说点 \((2,3)\) 不能放,那么 \(a[2]=100\)。
这样我们在循环每个状态的时候就可以排除掉不能放的点,对于状态 \(S\):
就好了。
找一个数的最低一个一,使用 \(\text{lowbit}\) 就好。
记得空间要开到 \(2^n\)。
#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\),那么通过位运算可以得到合法条件:
如果这一行放了 \(k\) 个国王,那么上一行一定是 \(k-sum[s]\) 个。
其中 \(sum[s]\) 表示 \(s\) 状态时这一行有多少个国王,即二进制下 \(s\) 有多少个 \(1\)。
\(sum[s]\) 和合法 \(s\) 我们都可以预处理求出,课件上给的求 \(sum[k]\) 的方法是每次右移一位,判有多少个 \(1\),我的做法是每次 \(s-=\text{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)\text{ or }(s<<2)\text{ or }(s>>1)\text{ or }(s>>2)]\text{ and }s=0\),这个可以预处理出来,这样我们可以简化后面 \(dp\) 多重循环嵌套时的判断次数。
其次,要判断山地是否为 \(0\),我们通过读入的数据计算。
如果循环到三个状态 \(s1,s2,s3\) 都合法,那么可以转移:
\(dp[i][s1][s2]=\max(dp[i][s1][s2],dp[i-1][s2][s3]+cnt[s1])\)
其中 \(cnt[s]\) 代表状态 \(s\) 中 \(1\) 的个数,可以用 \(\text{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\) 个马,可以得到状态转移方程:
其中 \(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\) 取与(具体请看代码),那么,为什么不能,也不需要 \(s2\) 和 \(s3\) 比较?
答案是这样的:如果 \(s2\) 和 \(s3\) 比较了,就会使得一部分你要用到的 \(dp[i-1][...][...][...]\) 被干掉,即是会把原本合法的状态判非法。
我就是这个思考了半个小时。
在代码的最后,统计第 \(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\);
- \(i\) 到 \(j\) 可以走。
第一个很好判断,第二个的话可以先预处理出 \(dis[i][j]\) 表示从 \(i\) 到 \(j\) 的直线距离,再跑个 \(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=ax^2+bx\),因为其必过原点,所以只需要两点即可确定。
设找到的两点分别是 \((x_1,y_1)\) 和 \((x_2,y_2)\),可以列出:
两个未知数两个方程,可解,最终得出来的式子是这样的:
处理每条抛物线时,需要判断分母是否为零,要不然不能除。并且要判断 \(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\times 10+num[i]\),这个数模 \(d\) 就是 \(k\times 10+num[i]\) 模 \(d\) 的结果,写成式子就是:
应该是这么写的吧,同余方程已经忘了
定义 \(dp[s][k]\) 表示状态为 \(s\),当前数模 \(d=k\) 的个数。
因此我们可以得出转移方程:
对于状态 \(s\),一个数 \(num[i]\),并且 \(s\text{ and }(1<<(i-1))=0\),有:
还有细节!
比如:\(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\) 则不存在 \(2i\) 和 \(3i\),怎么搞呢?
我们把它想象成这样:
或者把它写成数字:
观察到如果矩阵中有一个数字,那么一定没有它下面和右面的数字。
这是什么??!
芝士 棋盘!
但是不可能一个棋盘就包含所有 \(\in[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
刚好是表哥生日