状态压缩dp(规律)

状态压缩dp

将从以下几个方面讲解状态压缩dp:

  1. 状态压缩dp的专业术语(小白很难看懂的操作)
  2. 模板
  3. 利用模板做题
    1. 玉米田
    2. 小国王
    3. 蒙德里安的梦想
  4. 旅行商问题——TSP

1.状态压缩dp的专业术语(小白很难看懂的操作)

<< 、>>、|、&,这些符号是什么意思,以及用到得特殊操作

首先,要明确这些操作都是在二进制,上的操作

设d = 5,其二进制表示是101,用栗子的形式表示这些操作在干什么。

  1. <<(左移)、>>(右移)

栗一、d << 2 (d左移两位)

101 << 2 == 10100

即d << 2 = 20

所以<<(左移):二进制数左移几位,就补几个零,最后转化为十进制就是其结果

栗二、d >> 2 (d右移两位)

101 >> 2 == 1

即d >> 2 = 1

所以>>(右移):二进制数右移几位,就去除几位数,最后转化为十进制就是其结果

后面我们再加一个变量

p = 9,其二进制表示是1001

栗三 、d | p (d或p)

​ [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mpRT92KJ-1660986578935)(D:\大学四年经历\算法总结\或运算.png)]

所以|(或运算):两个二进制数,每一位上都是零才是零,也即有1便是1

栗三 、d &p (d与p)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ycZlG189-1660986578941)(D:\大学四年经历\算法总结\与运算.png)]

所以&(与运算):两个二进制数,每一位上都是1才是1,也即有0便是0

  1. 为什么要用到这些符号?也即这些符号在状态压缩dp中的作用是什么?

答:关键点就在压缩是怎么压缩的。我们用二进制来存储这些状态,为什么可以这样做呢?比如10的地方,每一个地方都有去和没去,和二进制0-1对上。所以我们要用二进制,也即需要这些符号来操作二进制。

  1. 常用操作

    1. 1 << n & 1 —— 判断第n个状态是否到过
    2. ​ S | 1 << n ——把第n个地方到过存到是S中

    ps: S——表示已经到过的

2.模板

这里讨论是n x m的方格上的题(这里默认n是行数,m是列数)

  1. 枚举状态(一行或者一列),用二进制存储。用到得变量

    • s[maxl]——存储每行满足要求的状态
    • cnt——充当指针,用来记录满足要求的状态的位置

    ps: 这样做的目的是减少遍历的次数,提高效率。不这样做就要把所有状态来遍历。

  2. 接下来就是题目的特殊要求,玉米田的n x m的图上做文章,蒙德里安的梦想,一列上只能放偶数个0(竖着放),以及小国王,放的多少会限制。

  3. 接下来是标准的三重for循环(分成两个部分),

    • 第一重for循环,前面保存的状态是竖着放的状态(遍历n),这里就是n,反之m 意思就是,如果前面存的是一行的状态,那么我们就遍历这些行。这里如果遍历n行,我们要遍历n + 1次。因为最后输出dp好表示

    • 第二行以及第三行for循环,遍历出第i或第i - 1行的合法状态,(这里遍历两重是因为题目中说,上下不能同时种(也即同时为1),如果说题目牵扯到三行,我们这部分就是三重for循环了

    例一玉米田(和图(n x m)结合)

    玉米田

    ​ 农夫约翰的土地由n*m个小方格组成,现在他要在土地里种植玉米。相邻的土地不能同时种植玉米,也就是说种植玉米的所有方格之间都不会有公共边缘。而且,部分土地是不育的,无法种植。

    输入格式

    第1行包含两个整数n和m。1sn,ms12。
    第2…n+1行:每行包含m个整数o或1,1表示该块土地肥沃,o表示该块土地不育。

    输出格式

    输出总种植方案对10000o000取模后的值。

    样例

    输入

    2 3
    1 1 0
    0 1 1

    输出

    8

    代码如下

    #include <bits/stdc++.h>
    using namespace std;
    
    const int maxl = 15, mod = 1e9;
    
    int n, m, mp[maxl][maxl], cnt = 0, s[1 << maxl], g[maxl], dp[maxl][maxl];
    
    int main(){
    	cin >> n >> m;
    	for (int i = 0; i < n; i++)//把能不能种,用二进制存储一行的状态(与后面对应)
    	    for (int j = 0; j < m; j++){
    			int x;
    			cin >> x;
    			g[i] = (g[i] << 1)+ x;
    		}
    	
    	for (int i = 0; i < 1 << m; i++){//一行为状态(有m列,也即一行有1 >> m的状态)
    		if(!(i & i >> 1)){
    			s[cnt++] = i;
    		}
    	}
    	
    	memset(dp, 0, sizeof(dp));//dp初始化
    	dp[0][0] = 1;//什么都没有也算一种方案
        
    	for (int i = 0; i <= n; i++)//枚举行数
    	    for (int a = 0; a < cnt; a++)//枚举牵扯到的状态
    	        for (int b = 0; b < cnt; b++){
    				if (!(s[a] & s[b]) && ((s[a] & g[i]) == s[a])){
    					dp[i + 1][a] += dp[i][b];
    				}
    			}
    			cout << dp[n + 1][0];//表示第n+1行,0个状态
    			return 0;
    	    
    }
    

    例三小国王(限制次数)

    小国王

    在n×n的棋盘上放k个国王,国王可攻击相邻的8个格子,求使他们无法互相攻击的方案总数。

    输入格式

    共一行,包含两个整数n和 k。1≤ns10,0≤ k≤n2

    输出格式

    共一行,表示方案总数,若不能够放置则输出0。

    输入样例

    3 2

    输出样例

    16


    代码如下

    #include <bits/stdc++.h>
    using namespace std;
    
    const int maxl = 10, N = 100;
    
    int n, k, s[1 << maxl], num[maxl], cnt = 0, dp[maxl][N][1 << maxl];
    
    int main(){
    	cin >> n >> k;
    	
    	memset(num, 0, sizeof(num));
    	memset(dp, 0, sizeof(dp));
    	dp[0][0][0] = 1;
    	
    	for (int i = 0; i < 1 << n; i++){
    		if (!(i & i >> 1)){
    			s[cnt++] = i;
    			for (int j = 0; j < n; j++){//(遍历得出一行可以放多少国王)
    				num[i] += i >> j & 1;
    			}
    		}
    	}
    	
    	for (int i = 0; i <= n; i++)
    	    for (int j = 0; j <= k; j++)(像多重背包,枚举k就成,其他都很相似)
    	        for (int a = 0; a < cnt; a++)
    	            for (int b = 0; b < cnt; b++){
    	            	int c = num[s[a]];
    					if ((j >= c) && !(s[a] & s[b]) && !((s[a] >> 1) & s[b]) && !((s[a] << 1) & s[b])){
    						dp[i + 1][j][a] += dp[i][j - c][b];
    					}
    				}
    				cout << dp[n + 1][k][0];
    				return 0;
    }
    

    例三蒙德里安的梦想(一列状态存储)

    蒙德里安的梦想

    ​ 求把 N×MN×M 的棋盘分割成若干个 1×21×2 的长方形,有多少种方案。

    例如当 N=2,M=4N=2,M=4 时,共有 55 种方案。当 N=2,M=3N=2,M=3 时,共有 33 种方案。

    如下图所示:

    2411_1.jpg

    数据范围

    1≤N,M≤111≤N,M≤11

    输入样例:
    4 11
    
    输出样例:
    51205
    

    代码如下

    #include <bits/stdc++.h>
    using namespace std;
    
    const int maxl = 115;
    int n, m, dp[maxl][maxl];
    bool st[maxl];//判断这个状态合不合法——是不是偶数个零 (偶数个0是合法状态) 
    
    int main(){	
        cin >> n >> m;
    	memset(dp, 0, sizeof(0));
    	dp[0][0] = 1;//什么都不放也是一种状态(一般题目提到方法等字眼,dp[0][0] = 1;) 
    	
    	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++;
    			}
    		}
    		if (cnt & 1){
    			st[i] = false;
    		}
    	}
    	
    	for (int i = 0; i <= m; i++)
    	    for (int j = 0; j < i << n; j++)//遍历第i行的状态 
    		    for (int k = 0; k < 1 << n; k++){//遍历第i - 1行的状态
    		    	if ((j & k) == 0 && st[j | k])//没有1重合,有偶数个0 
    		    	dp[i][j] += dp[i - 1][k];
    			} 
    			cout << dp[m][0] << endl;//到m列, 摆放第0个状态 
    	return 0;
    }
    

变种——旅行商问题

给定一个n个顶点组成的带权有权有向图的矩阵d(I,j)(INF表示没有边)。要求从顶点0出发,经过每个顶点恰好一次后再回到顶点0,问经过的边的总权重的最小值是多少?

限制条件

输入

n = 5(如下图所示)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-I2X7I2r7-1660986578945)(D:\大学四年经历\算法总结\TSP——旅行商问题.png)]

输出

22(0 -> 3 -> 4 -> 1 -> 2 -> 0)


先用记忆化搜索解题,这个比较容易理解

/*输入数据
5 8
0 1 3
0 3 4
1 2 5
2 0 4
2 3 5
3 4 3
4 0 7
4 1 6
*/
#include <bits/stdc++.h>
using namespace std;

const int maxl = 15, INF = 1e9;

int n, mp[maxl][maxl], dp[1 << maxl][maxl], d, a, b, c;

int rec(int S, int v){//已访问过的节点集合为S,当前位置为v 
	if (dp[S][v] >= 0)//记忆化搜索,到过直接返回,相应的值就好了 
	   return dp[S][v];
	   
	if (S == (1 << n) - 1 && v == 0)//把下标从0~n-1全部访问,并且当前位置为0 
	   return dp[S][v] = 0;
	
	int res = INF;
	for (int i = 0; i < n; i++){//一个数组遍历全部情况,且用下标存其结果,进行剪枝(也即记忆化搜索) 
		if (!(S >> i & 1)){
			res = min(res, rec(S | 1 << i, i) + mp[v][i]);
		}
	}
	return dp[S][v] = res;
}
int main(){
	cin >> n >> d;//n ——顶点,d ——边数 
	for (int i = 0; i < n; i++)
	    for (int j = 0; j < n; j++)
	        mp[i][j] = INF;
	        
	for (int i = 0; i < d; i++){
		cin >> a >> b >> c;
		mp[a][b] = c;
	}
	   
	
//	cout <<'\t';	//把图打印出来 
//	for (int i = 0; i < n; i++){
//			cout << i << '\t';
//	}
//	cout << endl;
//	for (int i = 0; i < n; i++){
//		cout <<  i << '\t';
//	    for (int j = 0; j < n; j++){
//          if(mp[i][j] != INF)
//			cout << mp[i][j] << '\t';
//          else
//          cout << "INF" << '\t';
//		}
//		cout << endl;
//	}
	
	memset(dp, -1, sizeof(dp));
	cout << rec(0, 0);//访问过0节点,且当前位置为0 
}

状态压缩dp

#include <bits/stdc++.h>
using namespace std;

const int maxl = 15, INF = 1e9;

int n, mp[maxl][maxl], dp[1 << maxl][maxl], d, a, b, c;

void rec(){
	dp[(1 << n) - 1][0] = 0;
	
	for (int S = (1 << n) - 2; S >= 0; S--)//遍历状态
	    for (int i = 0; i < n; i++)
	        for (int j = 0; j < n; j++){
				if (!(S >> j & 1))
				dp[S][i] = min(dp[S][i], dp[S | 1 << j][j] + mp[i][j]);
			}

}
int main(){
	cin >> n >> d;//n ——顶点,d ——边数 
	for (int i = 0; i < n; i++)
	    for (int j = 0; j < n; j++){
			mp[i][j] = INF;
		}
	        
	for (int i = 0; i < d; i++){
		cin >> a >> b >> c;
		mp[a][b] = c;
	}
	   
	
//	cout <<'\t';	//把图打印出来 
//	for (int i = 0; i < n; i++){
//			cout << i << '\t';
//	}
//	cout << endl;
//	for (int i = 0; i < n; i++){
//		cout <<  i << '\t';
//	    for (int j = 0; j < n; j++){
//          if(mp[i][j] != INF)
//			cout << mp[i][j] << '\t';
//          else
//          cout << "INF" << '\t';
//		}
//		cout << endl;
//	}
    for (int S = 0; S < 1 << n; S++)//一个 1 << n行,n列,用fill() 初始化 
    fill(dp[S], dp[S] + n, INF);
    
	rec();
	cout << dp[0][0];//访问过0节点,且当前位置为0 
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值