状压DP
我认为所谓的状压DP,就是把每一种情况用位运算压缩起来,然后暴力枚举前一种情况的每一种状态,用以转移到当前状态.由于把状态压缩了,所以判断起来的时间效率大大提高.当然这只限于 状态比较少的情况,状态比较多的话会存不开.
现在给你 \(n\) 个空间中的点( \(n\) 为偶数),问两两配对后,使得每个点恰好在一个点对中,所有点对中两点的距离之和应尽量小,最小距离是多少?
容易想到分成多个阶段,求前 \(i\) 个点能组成的最短距离.而前 \(i\) 个点组成的点对的最小距离和和前 \(i-1\) 个点组成的方案数,如果 \(i\) 与 \(j\) (\(j<i\))配对,那我们需要前 \(i-1\) 个点中,不包含 \(j\) 的点对的最小距离和,可以发现前面没有任何一种状态能表示出来,因此我们考虑增加维度,把状态分得更细致.用 \(dp[i][S]\) 表示:前 \(i\) 个点中,用集合 \(S\) 中的点两两配对的最小距离和.而数组的下标我们需要一个整数,如何表示一个集合的子集?方法是把他转化成二进制,即所谓的状态压缩.
\(d[i][S]=min\{|P_iP_j|+d[i-1][S-{j}-{i}] ,j\in S\}\)
下面是一道例题
考虑这样一个棋盘
我们只需要考虑横着放的方案数,横着放完了,剩下的空填满竖着放的即可.
假设我们当前考虑到了箭头所指的第 \(i\) 列
那么第 \(i-1\) 列如果伸过来一个木块,就记为1,如果没有伸过来一个木块,就记为0.
那么图示的情况的状态可以表示为10100,他可以用一个整数(32位二进制)表示出来.不妨记为 \(j\)
显然,只有在0有连续的偶数个时我们才能放竖着的.如果当前的状态是有奇数个0的,我们可以直接pass
这可以通过预处理来实现,(预先暴力枚举所有可能出现的情况:00000,00001,00010,....).存到一个 \(st\) 数组里,只需要通过判断 \(st[j]==true\) 即可快速判断当前枚举的状态是不是符合题意的.
好了,现在假设我们枚举的状态都是能竖着放木块的.
我们还要考虑一点:两个列是不能连着放横着的木块的
我们暴力枚举第 \(i\) 列的所有状态和第 \(i-1\) 列的所有状态.
由于我们之前的特殊的存储方式,如果两个状态的相同的位置都是\(1\),就说明这两列都在这个位置放了木块,这种情况我们也要过滤,所以如果第\(i-1\)列的状态 \(k\) 和第\(i\)列的状态 \(j\) 按位与的结果不是0,说明存在两列的某一位置都放了横着的木块,此时需要pass掉.
我们总共需要两个维度来存储状态
\(dp[i][j]\)表示前 \(i-1\) 列已经确定,第\(i-1\) 列状态为\(j\)时的方案总数
初始时其他均为0,dp[0][0]=1,也就是第-1列什么都不放,方案数为1
状态计算代码过程
dp[0][0]=1;
for(int i=1;i<=m;i++)//列数是0~m-1,还需再计算一下dp[m][0],因为第m-1列不能放任何横着的木块,因此是0.之后的状态就没有意义了,可以忽略
{
for(int j=0;j<1<<n;j++)//暴力枚举第i列的状态j
{
for(int k=0;k<1<<n;k++)//暴力枚举第i-1列的状态k
{
if(j&k||!st[j|k])continue;
dp[i][j]+=dp[i-1][k];
}
}
}
当然,此时dp[m][0]是答案.
但是也可以用下面这种方法求出答案,循环就只需要从1m变成1m-1
long long ans=0;
for(int i=0;i<1<<n;i++)
{
if(st[i])
ans+=dp[m-1][i];
}
完整代码
点击查看代码
#include<iostream>
#include<stdio.h>
#include<string.h>
using namespace std;
const int N=1e6+10;
bool st[N];
long long dp[110][1<<12];
int n,m;
int main()
{
while(cin>>n>>m,n||m)
{
memset(dp,0,sizeof dp);
for(int i=0;i<1<<n;i++)
{
int cnt=0;
st[i]=true;
for(int j=0;j<n;j++)
{
if(i>>j&1)
{
if(cnt&1)
{
st[i]=false;
break;
}
else cnt=0;
}
else cnt++;
}
if(cnt&1)st[i]=false;
}
dp[0][0]=1;
for(int i=1;i<m;i++)
{
for(int j=0;j<1<<n;j++)
{
for(int k=0;k<1<<n;k++)
{
if(j&k||!st[j|k])continue;
dp[i][j]+=dp[i-1][k];
}
}
}
long long ans=0;
for(int i=0;i<1<<n;i++)
{
if(st[i])
ans+=dp[m-1][i];
}
printf("%lld\n",ans);
//printf("%lld\n",dp[m][0]);
}
}
例题2 国王
要满足题意的状态
1.相邻两列的国王不能攻击到
如果前一列的状态k的某一个位置有国王.
那本列的状态j与k中的1相邻的三个位置均不能是国王
即
\((j<<1)\&k==0\) \((j<<1)\&k==0\) 且 \(j\&k==0\)
2.同一列中相邻两个位置不能是国王.
即\((j>>1)\&j==0\) \((j<<1)\&j==0\)
一本通上的题暴力枚举情况被卡了一下,只有提前把合法的情况存到一个数组里,从数组里枚举情况才能A
洛谷上的直接枚举所有情况就OK了.还是建议把合法情况存到数组里然后从数组中枚举情况,这样能使复杂度降低不少.
点击查看洛谷代码
#include<stdio.h>
#include<iostream>
#include<cstdlib>
#include<string.h>
#include<algorithm>
#include<math.h>
using namespace std;
typedef long long LL;
const int N=11;
int st[1<<11];
long long int dp[N][1<<11][N*N];
int n,K;
int cnt[1<<11];
int main()
{
scanf("%d%d",&n,&K);
for(int i=0;i<1<<n;i++)
{
int f=0;
st[i]=1;
if(((i<<1)|(i>>1))&i)
{
st[i]=0;
}
int t=i;
int c=0;
while(t)
{
if(t&1)c++;
t>>=1;
}
cnt[i]=c;
}
dp[0][0][0]=1;
long long ans=0;
for(int i=1;i<=n;i++)
{
for(int all=0;all<=K;all++)
{
for(int j=0;j<1<<n;j++)
{
for(int k=0;k<1<<n;k++)
{
if(j&k||st[j]==0||(j&((k>>1)|k<<1)))continue;
if(cnt[j]+cnt[k]<=all)
dp[i][j][all]+=dp[i-1][k][all-cnt[j]];
}
}
}
}
for(int i=0;i<1<<n;i++)
{
ans+=dp[n][i][K];
}
printf("%lld\n",ans);
return 0;
}
点击查看一本通代码
#include<stdio.h>
#include<iostream>
#include<cstdlib>
#include<string.h>
#include<algorithm>
#include<math.h>
using namespace std;
typedef long long LL;
const int N=11;
int st[1<<11];
long long int dp[N][1<<11][N*N];
int n,K;
int cnt[1<<11];
int num[N<<11];
int idx;
int main()
{
//freopen("uva.txt","r",stdin);
scanf("%d%d",&n,&K);
for(int i=0;i<1<<n;i++)
{
int f=0;
st[i]=1;
if(((i<<1)|(i>>1))&i)
{
st[i]=0;
}
int t=i;
int c=0;
while(t)
{
if(t&1)c++;
t>>=1;
}
cnt[i]=c;
if(st[i])num[++idx]=i;
}
dp[0][0][0]=1;
long long ans=0;
for(int i=1;i<=n;i++)
{
{
for(int j=1;j<=idx;j++)
{
for(int k=1;k<=idx;k++)
{
for(int all=0;all<=K;all++)
{
int a=num[j],b=num[k];
if(a&b||st[a]==0||(a&((b>>1)|b<<1)))continue;
if(cnt[a]+cnt[b]<=all)
dp[i][a][all]+=dp[i-1][b][all-cnt[a]];
}
}
}
}
}
for(int i=0;i<1<<n;i++)
{
ans+=dp[n][i][K];
}
printf("%lld\n",ans);
return 0;
}