当前位置: 首页 > news >正文

33.状态压缩动态规划

一、算法内容

1.简介

若元素数量比较小(不超过 20 20 20)时,想要存储每个元素取或不取的状态时, 可以借助位运算将状态压缩。 需要借助状态压缩过程的动态规划就是状态压缩 DP(很多地方会简称为状压 DP)。

取若干元素,也就是对应的位置记为 1 1 1,其余位置记为 0 0 0。例如,一共有 5 5 5 个元素 a , b , c , d , e a,b,c,d,e a,b,c,d,e ,我们分别用二进制的 1 , 10 , 100 , 1000 , 10000 1,10,100,1000,10000 1,10,100,1000,10000 表示这五个元素,则集合 { a , c , e } \{a,c,e\} {a,c,e} 可以用 ( 10101 ) 2 = 21 (10101)_2=21 (10101)2=21 来表示,而集合 { b , c , d } \{b,c,d\} {b,c,d} 可以用 ( 01110 ) 2 = 14 (01110)_2=14 (01110)2=14 表示。

对于元素个数为 n n n 的情况,其空间复杂度为 O ( 2 n ) O(2^n) O(2n) 。 如果不用状态压缩,那么我们状态需要开 5 5 5 维数组 d p [ 2 ] [ 2 ] [ 2 ] [ 2 ] [ 2 ] dp[2][2][2][2][2] dp[2][2][2][2][2] ,这样不仅使得代码的实现变的很复杂,并且当 n n n 的大小不一样的时候,状态维度也不一样,不是很容易实现。

2.子集与二进制

由于状压 DP 经常涉及到枚举子集的情况,所以我们介绍一种代码编写非常方便的枚举子集方法,也就是用二进制来模拟。这也是我们状压 DP 的基础。我们可以用二进制的一位表示集合对应某一元素的选取状态, 1 1 1 表示选取, 0 0 0 表示未选取。举个例子,我们拥有一个集合 { 0 , 1 , 2 , 3 , 4 , 5 , 6 } \{0,1,2,3,4,5,6\} {0,1,2,3,4,5,6}。那么二进制 0101101 0101101 0101101 就代表子集合 { 0 , 2 , 3 , 5 } \{0,2,3,5\} {0,2,3,5}

而在集合选取尤其是动态规划的状态转移方程中,我们需要用位运算来帮助我们实现子集的枚举。例如: i & (1 << j) 就可以用来判断 i i i 的二进制展开所代表的集合里面,是否包含第 j j j 个元素;(1 << n) - 1 就可以表示所以元素都被选取的情况,也就是全集。

由二进制展开的情况不难发现,子集的大小是指数级别的,所以数据范围一定不会很大。因此这也是一条判断是否是状压 DP 的一大方式。

二、实例分析

1. P2622 关灯问题

(1)题目大意

现有 n n n 盏灯,以及 m m m 个按钮。每个按钮可以同时控制这 n n n 盏灯,按下 i i i 按钮对于第 j j j 盏灯,具体的操作结果如下:

  • 如果 a i , j a_{i,j} ai,j 1 1 1,那么当这盏灯开了的时候,把它关上,否则不管;
  • 如果 a i , j a_{i,j} ai,j − 1 -1 1,如果这盏灯是关的,那么把它打开,否则也不管;
  • 如果 a i , j a_{i,j} ai,j 0 0 0,无论这灯是否开,都不管。

现在这些灯都是开的,给出所有开关对所有灯的控制效果,求问最少要按几下按钮才能全部关掉。

(2)题目分析

  • 本题灯只有开关两种状态,所以可以用 1 1 1 表示开灯状态, 0 0 0 表示关灯状态

  • 因此可以用一个长度为 n n n 的二进制数唯一地表示每个状态

  • 题目提供的开关灯方式即可作为一个状态转移的方式,我们以灯全开也就是 (1 << n) - 1 为起点

  • 如果我们设当前状态为 now ,则 now & (1 << j) 就可以判断 j j j 这个灯是否是打开状态。

  • 那么我们就可以对于每个状态遍历所有开关的情况。对于 a[i][j] 来说,我们有状态转移如下所示:

    • a[i][j] = 1 && (now & (1 << j))now ^= (1 << j)
    • a[i][j] == -1 && !(now & (1 << j))now |= (1 << j)
  • 我们用迷宫问题的思路,设全开为迷宫入口,全关为迷宫出口。记录每个状态的步数,即可在第一次找到出口的时候退出程序。

(3)正解程序

#include <iostream>
#include <queue>using namespace std;
typedef long long ll;
struct node
{ll status;ll step;
};
ll n, m;
ll a[110][20];
bool vis[2010];
ll bfs()
{queue<node> q;q.push((node) {(1 << n) - 1, 0});vis[(1 << n) - 1] = true;while(!q.empty()){node u = q.front();q.pop();if(u.status == 0)return u.step;for(ll i = 0; i < m; i++){ll now = u.status;for(ll j = 0; j < n; j++)if((a[i][j] == 1 && (now & (1 << j))) || (a[i][j] == -1 && !(now & (1 << j))))now ^= (1 << j);if(!vis[now]){q.push((node){now, u.step + 1});vis[now] = true;}}}return -1;
}
int main()
{cin >> n >> m;for(ll i = 0; i < m; i++)for(ll j = 0;j < n; j++)cin >> a[i][j];cout << bfs() << endl;return 0;
}

2. P1441 砝码称重

(1)题目大意

现有 n n n 个砝码,重量分别为 a i a_i ai,在去掉 m m m 个砝码后,问最多能称量出多少不同的重量(不包括 0 0 0)。砝码只能放在其中一边。

(2)题目分析

  • 设我们当前选择的砝码集合是 T T T,它能称出的重量的集合为 d p dp dp
  • 我们设 M M M 对应的二进制表示为 ( m t , m t − 1 , . . . , m 0 ) 2 (m_t,m_{t-1},...,m_0)_2 (mt,mt1,...,m0)2,其中 m i = 1 m_i=1 mi=1 表示 i i i 这个重量是可以被称出来的;
  • 那么再添加一个砝码之后,能称出的重量就是 dp |= (dp << w[i])

(3)正解代码

#include <iostream>
#include <cstdio>
#include <bitset>using namespace std;
typedef int ll;
const ll maxn = 30;
ll n, m, a[maxn];
ll my_count(ll x)
{ll res = 0;while(x){res += (x & 1);x >>= 1;}return res;
}
int main()
{scanf("%d%d", &n, &m);m = n - m;for(ll i = 0; i < n; i++)scanf("%d", &a[i]);ll ans = 0;for(ll i = 1; i < (1 << n); i++){if(my_count(i) != m)continue;bitset<2010> dp;dp[0] = 1;for(ll j = 0; j <= n - 1; ++j)if(i & (1 << j))dp = dp | dp << a[j];ans = max(ans, (ll)dp.count());}printf("%d\n", ans - 1);return 0;
}

3. P1896 互不侵犯

(1)题目大意

N × N N\times N N×N 的棋盘里面放 K K K 个国王,使他们互不攻击,共有多少种摆放方案。国王能攻击到它上下左右,以及左上左下右上右下八个方向上附近的各一个格子,共 8 8 8 个格子。

(2)题目分析

  • 有由题目意思可以知道,每个格子只有能摆放和不能拜访两种状态,所以我们可以用状态压缩的方式来表示整个棋盘

  • 现在考虑应该用哪些内容来完整的表示棋盘。因为国王会影响上下行,所以我们可以考虑从上往下遍历状态,这样就只需要考虑每一行的上一行是否会发生冲突即可。

  • 考虑每一行的合法状态,因为对于同一行来说,不能有两个连续的格子存在国王,那也就是说对于状态 i i i 来说,需要满足 i & (i << 1) == 0

  • 设总共有 M a x Max Max 种合法状态,如果我们设当前行的状态为 k k k,上一行的状态为 x x x。那么它们应该满足以下关系:(suit[k] & suit[x]) == 0 && ((suit[k] << 1) & suit[x]) == 0 && ((suit[k] >> 1) & suit[x]) == 0

  • 因此设 d p [ i ] [ j ] [ k ] dp[i][j][k] dp[i][j][k] 表示在前 i i i 行中已经放置 j j j 个国王,第 i i i 行的状态为第 k k k 种合法状态时,题目的合法方案数。

  • 如果我们设置前 i − 1 i-1 i1 行的第 i − 1 i-1 i1 行为第 x x x 种合法状态,则我们有状态转移方程:
    d p [ i ] [ j ] [ k ] = ∑ x = 1 M a x d p [ i − 1 ] [ j − n u m [ k ] ] [ x ] ; dp[i][j][k]=\sum_{x=1}^{Max}dp[i - 1][j - num[k]][x]; dp[i][j][k]=x=1Maxdp[i1][jnum[k]][x];

    其中 n u m [ k ] num[k] num[k] 表示状态 k k k 的国王数量。

(3)正解代码

#include <iostream>
#include <algorithm>
#include <cstdio>using namespace std;
typedef long long ll;
ll n, K;
ll suit[1 << 10], Max = 0;
ll num[1 << 10];
ll dp[100][100][1 << 10];
void prework()
{for(ll i = 0; i <= (1 << n) - 1; i++)if((i & (i << 1)) == 0)suit[++Max] = i;for(ll i = 1; i <= Max; i++){ll tmp = suit[i];while(tmp){num[i] += (temp & 1);tmp >>= 1;}}
}
int main()
{scanf("%lld%lld", &n, &K);prework();dp[0][0][1] = 1;for(ll i = 1; i <= n; i++)for(ll j = 0; j <= K; j++)for(ll k = 1; k <= Max; k++)for(ll x = 1; x <= Max; x++)if(j - num[k] >= 0 && (suit[k] & suit[x]) == 0 && ((suit[k] << 1) & suit[x]) == 0 && ((suit[k] >> 1) & suit[x]) == 0)dp[i][j][k] += dp[i - 1][j - num[k]][x];ll ans = 0;for(ll i = 1; i <= Max; i++)ans += dp[n][K][i];printf("%lld", ans);return 0;
}

三、作业

1.绿题

P1441 砝码称重

P1879 [USACO06NOV] Corn Fields G

2.蓝题

P1896 [SCOI2005] 互不侵犯

P2704 [NOI2001] 炮兵阵地

P2831 [NOIP 2016 提高组] 愤怒的小鸟

P3959 [NOIP2017 提高组] 宝藏

相关文章:

  • 当JIT遇见K8s
  • Go 1.24 中的弱指针包 weak 使用介绍
  • 顶点着色器和片元着色器染色+表面体着色器染色
  • 《企业级 Java EE 架构设计精深实践》内容详解
  • 监听退出事件
  • 系统架构设计(三):质量属性
  • 扩展和自定义 asammdf 库:满足特定需求的解决方案
  • 如何创建一个C#项目(基于VS2022版)
  • 前端面试 HTML篇
  • 从像素到驾驶决策:Python与OpenCV赋能自动驾驶图像识别
  • PotPlayer,强大的高清视频播放器
  • MySQL 联合查询教程
  • STM32的开发环境介绍
  • C++如何设计线程池(thread pool)来提高线程的复用率,减少线程创建和销毁的开销
  • Vue3的内置组件 -实现过渡动画 TransitionGroup
  • 实现从一个微信小程序跳转到另一个微信小程序
  • 文本预处理(NLTK)
  • 达芬奇模板 15组现代简洁文字标题动画 Modern Titles v2.0 DR
  • 在Spring Boot项目中实现Word转PDF并预览
  • 路由交换网络专题 | 第八章 | GVRP配置 | 端口安全 | 端口隔离 | Mux-VLAN | Hybrid
  • 马上评|“AI神医宇宙”欺诈,连演员都不请了
  • 国家发改委:我国能源进口来源多元,企业减少甚至停止自美能源进口对国内能源供应没有影响
  • 知名计算机专家、浙江大学教授张森逝世
  • 广西给出最后期限:6月30日之前主动交代问题可从宽处理
  • 时代邻里:拟收购成都合达联行科技剩余20%股权
  • 成都一季度GDP为5930.3亿元,同比增长6%