状压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\}\)

下面是一道例题
image

考虑这样一个棋盘
image
image
我们只需要考虑横着放的方案数,横着放完了,剩下的空填满竖着放的即可.
假设我们当前考虑到了箭头所指的第 \(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.相邻两列的国王不能攻击到
image
如果前一列的状态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;
}
posted @ 2022-10-22 17:53  LZH_03  阅读(26)  评论(0编辑  收藏  举报