状态压缩dp练习题(C/C++)

1. 核心总结

  应对问题: 对于递推动态规划类型的问题,我们通常会分析其中一个比较常见的状态例如f(i),然后我们会尝试去从同样的状态f(i - 1) 或者 f(i - 2)等等当中去完美的推导出此时的一个状态,其方案数往往是几倍的f(i - 1) + 几倍的f(i - 2) 来能完美的表达出f(i)的一种状态表示。
 问题来了,假如使用一般的方法推导的时候发现其f(i)的一个状态表示不仅仅和f(i - 1)的一个数量有关系,而且它还和f(i - 1)结果当中不同的状态有联系这时候我们应该如何处理这个问题呢?
在这里插入图片描述

 分析过程: 此类问题的关键通常都是需要将状态的边界信息同时保存下来(这时候我们往往借用二进制或其他进制来表示)
在这里插入图片描述

 通用处理:
在这里插入图片描述

2. 状态dp习题

 题目链接: 坐标搜寻
 题目分析:
  我们假设 s 为图的子集我们用二进制来表示,当相应二进制位置为1认为走过此点
  状态表示 dp[s][j]:表示从(0, 0)出发走过集合 s 当中的所有点最终到达点 j 的方案数 ( j 包含在 s中)
  状态转移 : 依次枚举 s 当中所有的点 k , dp[s][j] = min(dp[s - j][k] + dist(k, j ))
  结果: 枚举每个终点方案取最小值即可.
 代码解析:

#include<cstdio>
#include<iostream>
#include<algorithm>
#include<cstring>
#include<cmath>
using namespace std;
typedef long long LL;
const int N = 18;
double dp[1 << N][N];
double x[N], y[N];
double dist(int i, int j){
    
    
	return sqrt((x[i] - x[j]) * (x[i] -x[j]) + (y[i] - y[j]) * (y[i] - y[j]) );
}
int main(){
    
    
  memset(dp, 0x7f, sizeof(dp));

	int n;
	cin >> n;
	for(int i = 1; i <= n; i ++ ) cin >> x[i] >> y[i];
	x[0] = y[0] = 0; // 起点
	n ++; // 包括起点一共 n + 1 个点
	
	dp[1][0] = 0; // 初始状态只包含 0 这个点 
	for(int s = 1; s < (1 << n); s ++ )
	for(int j = 0; j < n; j ++ )
	for(int k = 0; k < n; k ++ ){
    
    
		if((s >> j & 1) && ((s ^ (1 << j)) >> k & 1) ) // 保证 j 存在, s - j 即 k 也存在 
		dp[s][j] = min(dp[s][j], dp[s ^ (1 << j)][k] + dist(k, j));
	}	
	
	double ans = 0x7f7f7f7f;
	for(int i = 0; i < n; i ++ ) ans = min(ans, dp[(1 << n) - 1][i]);
	printf("%.2lf", ans);
	return 0;	
}

 题目链接: 糖果
 题目分析:
  使用二进制来表示每一袋糖果的口味
  状态表示 dp[s]:表示得到状态为 s 的糖果种类所选取的最小糖果袋数
  状态转移 : 依次枚举每个二进制状态判断当我们选取这袋糖果后是否可以优化选取这袋糖果后的一个状态.  s : 为枚举状态.   s | a[i] : 为选取这袋糖果后可以以到达的一个状态。
  dp[s | a[i] ] = min (dp[s | a[i] ], dp[s] + 1);
  结果: dp[dp[(1 << m) - 1]
 代码解析:

#include<cstdio>
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int N = 110, M = 1 << 21;
int dp[M], a[N];
int main(){
    
    
    memset(dp, 0x3f, sizeof(dp));

    int n, m, k;
    scanf("%d%d%d", &n, &m, &k);
    for(int i = 0; i < n; i ++ )
        for(int j = 0; j < k; j ++ ){
    
    
            int x;
            scanf("%d", &x);
            a[i] |= (1 << (x - 1)); // 表示每袋子糖果的口味 
        }

    dp[0] = 0;
    for(int i = 0; i < n; i ++ )
        for(int j = 0; j < (1 << m); j ++ ){
    
    
            int k = j | a[i]; // 由 j 状态可以推导出来的 k 状态 
            dp[k] = min(dp[k], dp[j] + 1);  // 判断能否优化 k 状态 
        }
    
    if(dp[(1 << m) - 1] == 0x3f3f3f3f) printf("-1");
    else printf("%d",dp[(1 << m) - 1]);
    return 0;
}

 题目链接: 回路计数
 题目分析:
  我们假设 s 为图的子集我们用二进制来表示,当相应二进制位置为1认为走过此点
  状态表示 dp[s][j]:表示从(0)出发走过集合 s 当中的所有点最终到达点 j 的方案数 ( j 包含在 s中)
  状态转移 : 依次枚举 s 当中所有的点 k , 判断 k 是否可以到达 j 点 dp[s][j] = min(dp[s - j][k] + dist(k, j ))
  结果: 将dp[(1 << n) - 1][所有最终点] 方案求和即可
 代码解析:

#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
typedef long long LL;
const int N = 22;
LL dp[1 << N][N], n = 21;
bool g[N][N]; // 是否可以联通
LL dfs(int state, int pos){
    
    
    if(dp[state][pos] != -1) return dp[state][pos];

    LL res = 0;
    for(int i = 1; i < n; i ++ ){
    
    
        if(((state >> i) & 1) == 0) continue;
        if(g[i][pos]){
    
    
            dp[state - (1 << pos)][i] = dfs(state - (1 << pos), i);
            res += dp[state - (1 << pos)][i];
        }
    }

    return dp[state][pos] = res; // 一定要存储状态优化dp
}

int main(){
    
    
    memset(dp, -1, sizeof(dp));

    for(int i = 1; i <= n; i ++ )
    for(int j = 1; j <= n; j ++ ){
    
    
        if(__gcd(i, j) == 1) g[i - 1][j - 1] = g[j - 1][i - 1] = true;
    }

    for(int i = 1; i < n; i ++ ) dp[(1 << i) + 1][i] = 1; // 初始化

    LL sum = 0;
    for(int i = 1; i < n; i ++ ){
    
    
        dp[(1 << n) - 1][i] = dfs((1 << n) - 1, i);
        sum += dp[(1 << n) - 1][i];
    }

    printf("%lld", sum); // 881012367360
    return 0;
}

 题目链接: 积木画
 题目分析:
  使用二进制来表示最后一列的状态四种状态(00, 01, 10, 11)
  状态表示 dp[i][j]:表示已经确定前(i - 1)列状态,并且第 i 列状态方案为 j 的一个方案数量. 若一个方块同时占用两列,我们认为其是在左边那列操作完成确定的结果。
  状态转移 : 依次对 j 的 四种状态进行分析,看其可以推导出的下一层四种状态有哪些。
  例如 dp[i][0] -> dp[i + 1][0], dp[i + 1][1], dp[i + 1][2],dp[i + 1][3]四种状态
  结果: dp[n + 1][0]
 代码解析:

#include <iostream>
using namespace std;
const int N = 10000010, mod = 1000000007;
int dp[N][4];
int g[4][4] = {
    
     // 上面方块为01, 下面方块突出为 10
    {
    
    1, 1, 1, 1}, // 00 可以推导出下面一层的哪些状态
    {
    
    0, 0, 1, 1}, // 01 可以推导出下面一层的哪些状态
    {
    
    0, 1, 0, 1}, // 10 可以推导出下面一层的哪些状态
    {
    
    1, 0, 0, 0} // 11 可以推导出下面一层的哪些状态
};
int main()
{
    
    
    int n;
    cin >> n;

    dp[1][0] = 1;
    for(int i = 1; i <= n; i ++ )
    for(int j = 0; j < 4; j ++ )
    for(int k = 0; k < 4; k ++ ){
    
     // k 枚举上一层状态
        dp[i + 1][j] = (dp[i + 1][j] + dp[i][k] * g[k][j]) % mod;
    }

    cout << dp[n + 1][0];
    return 0;
}

猜你喜欢

转载自blog.csdn.net/weixin_51566349/article/details/129097970