状态压缩dp
将从以下几个方面讲解状态压缩dp:
- 状态压缩dp的专业术语(小白很难看懂的操作)
- 模板
- 利用模板做题
- 玉米田
- 小国王
- 蒙德里安的梦想
- 旅行商问题——TSP
1.状态压缩dp的专业术语(小白很难看懂的操作)
<< 、>>、|、&,这些符号是什么意思,以及用到得特殊操作
首先,要明确这些操作都是在二进制,上的操作
设d = 5,其二进制表示是101,用栗子的形式表示这些操作在干什么。
- <<(左移)、>>(右移)
栗一、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
- 为什么要用到这些符号?也即这些符号在状态压缩dp中的作用是什么?
答:关键点就在压缩是怎么压缩的。我们用二进制来存储这些状态,为什么可以这样做呢?比如10的地方,每一个地方都有去和没去,和二进制0-1对上。所以我们要用二进制,也即需要这些符号来操作二进制。
常用操作
- 1 << n & 1 —— 判断第n个状态是否到过
- S | 1 << n ——把第n个地方到过存到是S中
ps: S——表示已经到过的
2.模板
这里讨论是n x m的方格上的题(这里默认n是行数,m是列数)
-
枚举状态(一行或者一列),用二进制存储。用到得变量
- s[maxl]——存储每行满足要求的状态
- cnt——充当指针,用来记录满足要求的状态的位置
ps: 这样做的目的是减少遍历的次数,提高效率。不这样做就要把所有状态来遍历。
-
接下来就是题目的特殊要求,玉米田的n x m的图上做文章,蒙德里安的梦想,一列上只能放偶数个0(竖着放),以及小国王,放的多少会限制。
-
接下来是标准的三重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 种方案。
如下图所示:
数据范围
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
}