状压dp入门

状压dp入门

什么是状态压缩dp?
状态压缩dp就是把某些用dfs会超时的递归调用过多的题目转换成用数组中的一位数表示状态。我们知道在二进制下每一位上不是0就是1,这样就可以很好的表示事物的状态,是存在还是不存在,状态压缩就是把状态压缩到一位数内,这一位数操作时都用二进制操作。

要学状态压缩dp首先要对二进制有所了解。我们下面来介绍一下二进制的相关知识。
1.’&’符号,x&y,会将两个十进制数在二进制下进行与运算,然后返回其十进制下的值。例如3(11)&2(10)=2(10)。

2.’|’符号,x|y,会将两个十进制数在二进制下进行或运算,然后返回其十进制下的值。例如3(11)|2(10)=3(11)。

3.’’符号,xy,会将两个十进制数在二进制下进行异或运算,然后返回其十进制下的值。例如3(11)^2(10)=1(01)。

4.’<<’符号,左移操作,x<<2,将x在二进制下的每一位向左移动两位,最右边用0填充,x<<2相当于让x乘以4。相应的,’>>’是右移操作,x>>1相当于给x/2,去掉x二进制下的最有一位。

这四种运算在状压dp中有着广泛的应用,常见的应用如下:

1.判断一个数字x二进制下第i位是不是等于1。

方法:if ( ( ( 1 << ( i - 1 ) ) & x ) > 0)

将1左移i-1位,相当于制造了一个只有第i位上是1,其他位上都是0的二进制数。然后与x做与运算,如果结果>0,说明x第i位上是1,反之则是0。

2.将一个数字x二进制下第i位更改成1。

方法:x = x | ( 1<<(i-1) )

证明方法与1类似,此处不再重复证明。

3.把一个数字二进制下最靠右的第一个1去掉。

方法:x=x&(x-1)

感兴趣的读者可以自行证明。

下面介绍一些状压dp的例题
[POJ3254]Corn Fields(其实就是牛吃草)
题目大意
一个矩阵里有很多格子,每个格子有两种状态,可以放牧和不可以放牧,可以放牧用1表示,否则用0表示,在这块牧场放牛,要求两个相邻的方格不能同时放牛(不包括斜着的),即牛与牛不能相邻。问有多少种放牛方案(一头牛都不放也是一种方案)
输入
1<=n<=12,1<=m<=12
输出
一个mod100000000的整数
样例输入
2 3
1 1 1
0 1 0
样例输出
9

#include <cstdio>  
#include <cstring>  
const int N = 13;  
const int M = 1<<N;  
const int mod = 100000000;  
int st[M],map[M];  ///分别存每一行的状态和给出地的状态  
int dp[N][M];  //表示在第i行状态为j时候可以放牛的种数  
bool judge1(int x)  //判断二进制有没有相邻的1  
{  
    return (x&(x<<1));  
}  
bool judge2(int i,int x)  
{  
    return (map[i]&st[x]);  
}  
int main()  
{  
    int n,m,x;  
    while(~scanf("%d%d",&n,&m))  
    {  
        memset(st,0,sizeof(st));  
        memset(map,0,sizeof(map));  
        memset(dp,0,sizeof(dp));  
        for(int i=1;i<=n;i++)  
        {  
            for(int j=1;j<=m;j++){  
                scanf("%d",&x);  
                if(x==0)  
                    map[i]+=(1<<(j-1));  
            }  

        }  
        int k=0;  
        for(int i=0;i<(1<<m);i++){  
            if(!judge1(i))  
                st[k++]=i;  
        }  
        for(int i=0;i<k;i++)  
        {  
            if(!judge2(1,i))  
                dp[1][i]=1;  
        }  
        for(int i=2;i<=n;i++)  
        {  
            for(int j=0;j<k;j++)  
            {  
                if(judge2(i,j))  //判断第i行 假如按状态j放牛的话行不行。  
                    continue;  
                for(int f=0;f<k;f++)  
                {  
                    if(judge2(i-1,f))   //剪枝 判断上一行与其状态是否满足  
                        continue;  
                    if(!(st[j]&st[f]))  
                        dp[i][j]+=dp[i-1][f];  
                }  
            }  
        }  
        int ans=0;  
        for(int i=0;i<k;i++){  
            ans+=dp[n][i];  
            ans%=mod;  
        }  
        printf("%d\n",ans);  
    }  
    return 0;  
}  

【例2】最小总代价(Vijos-1456)

题目描述:

n个人在做传递物品的游戏,编号为1-n。

游戏规则是这样的:开始时物品可以在任意一人手上,他可把物品传递给其他人中的任意一位;下一个人可以传递给未接过物品的任意一人。

即物品只能经过同一个人一次,而且每次传递过程都有一个代价;不同的人传给不同的人的代价值之间没有联系;

求当物品经过所有n个人后,整个过程的总代价是多少。

输入格式:

第一行为n,表示共有n个人(16>=n>=2);

以下为n*n的矩阵,第i+1行、第j列表示物品从编号为i的人传递到编号为j的人所花费的代价,特别的有第i+1行、第i列为-1(因为物品不能自己传给自己),其他数据均为正整数(<=10000)。

(对于50%的数据,n<=11)。

输出格式:

一个数,为最小的代价总和。

输入样例:

2

-1 9794

2724 –1

输出样例:

2724

思路: 看了题目之后我们发现,题目的数据范围很小,最多就16个人,所以我们可以用状态压缩dp,dp[state][now],state表示现在已经经手过的人物状态,now表示现在礼物在那个人手中。这样我们的状态转移方程就可以写出来了。
初始状态为:dp[1<<i][i]=0;表示一开始物品在i手中。

所求状态为:min(dp[(1<<n)-1][j]); 0<=j<n

状态转移方程是:

dp[state][now]=min(dp[pre][t]+dist[t][now]);

/*
ID:shijieyywd
PROG:Vijos-1456
LANG:c++
*/
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <vector>
 
#define MAXN 20
#define INF 0x3f3f3f3f
 
using namespace std;
 
int n;
int edges[MAXN][MAXN];
int dp[65546][MAXN];
 
int min(int a, int b) 
{
	if (a == -1) return b;
	if (b == -1) return a;
	return a < b ? a : b;
}
 
int main() {
	freopen("p1456.in", "r", stdin);
	scanf("%d", &n);
	int t;
	for (int i = 0; i < n; i++) 
	{
		for (int j = 0; j < n; j++) 
		{
			scanf("%d", &edges[i][j]);
		}
	}
	memset(dp, -1, sizeof(dp));
	for (int i = 0; i < n; i++) 
	{
		dp[1 << i][i] = 0;
	}
	int ans = -1;
	for (int i = 0; i < 1 << n; i++) 
	{
		for (int j = 0; j < n; j++) 
		{
			if (dp[i][j] != -1) 
			{
				for (int k = 0; k < n; k++) 
				{
					if (!(i & (1 << k))) 
					{
						dp[i | (1 << k)][k] = min(dp[i | (1 << k)][k], dp[i][j] + edges[j][k]);
						if ((i | (1 << k)) == (1 << n) - 1) ans = min(ans, dp[i | (1 << k)][k]);
					}
				}
			}
		}
	}
	if (ans != -1)
		printf("%d\n", ans);
	else printf("0\n");
 
	return 0;
}

大佬博客
https://blog.csdn.net/u011077606/article/details/43487421
https://www.cnblogs.com/ibilllee/p/7651971.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值