状态压缩技巧:动态规划的降维打击
刷题认准labuladong
东哥带你手把手撕力扣????
点击下方卡片即可搜索????
我们号之前写过十几篇动态规划文章,可以说动态规划技巧对于算法效率的提升非常可观,一般来说都能把指数级和阶乘级时间复杂度的算法优化成 O(N^2),堪称算法界的二向箔,把各路魑魅魍魉统统打成二次元。
但是,动态规划本身也是可以进行阶段性优化的,比如说我们常听说的「状态压缩」技巧,就能够把很多动态规划解法的空间复杂度进一步降低,由 O(N^2) 降低到 O(N),
能够使用状态压缩技巧的动态规划都是二维dp
问题,你看它的状态转移方程,如果计算状态dp[i][j]
需要的都是dp[i][j]
相邻的状态,那么就可以使用状态压缩技巧,将二维的dp
数组转化成一维,将空间复杂度从 O(N^2) 降低到 O(N)。
什么叫「和dp[i][j]
相邻的状态」呢,比如前文 最长回文子序列 中,最终的代码如下:
int longestPalindromeSubseq(string s) {int n = s.size();// dp 数组全部初始化为 0vector<vector<int>> dp(n, vector<int>(n, 0));// base casefor (int i = 0; i < n; i++)dp[i][i] = 1;// 反着遍历保证正确的状态转移for (int i = n - 2; i >= 0; i--) {for (int j = i + 1; j < n; j++) {// 状态转移方程if (s[i] == s[j])dp[i][j] = dp[i + 1][j - 1] + 2;elsedp[i][j] = max(dp[i + 1][j], dp[i][j - 1]);}}// 整个 s 的最长回文子串长度return dp[0][n - 1];
}
PS:我们本文不探讨如何推状态转移方程,只探讨对二维 DP 问题进行状态压缩的技巧。技巧都是通用的,所以如果你没看过前文,不明白这段代码的逻辑也无妨,完全不会阻碍你学会状态压缩。
你看我们对dp[i][j]
的更新,其实只依赖于dp[i+1][j-1], dp[i][j-1], dp[i+1][j]
这三个状态:
这就叫和dp[i][j]
相邻,反正你计算dp[i][j]
只需要这三个相邻状态,其实根本不需要那么大一个二维的 dp table 对不对?
状态压缩的核心思路就是,将二维数组「投影」到一维数组:
思路很直观,但是也有一个明显的问题,图中dp[i][j-1]
和dp[i+1][j-1]
这两个状态处在同一列,而一维数组中只能容下一个,那么当我计算dp[i][j]
时,他俩必然有一个会被另一个覆盖掉,怎么办?
这就是状态压缩的难点,下面就来分析解决这个问题,还是拿「最长回文子序列」问题举例,它的状态转移方程主要逻辑就是如下这段代码:
for (int i = n - 2; i >= 0; i--) {for (int j = i + 1; j < n; j++) {// 状态转移方程if (s[i] == s[j])dp[i][j] = dp[i + 1][j - 1] + 2;elsedp[i][j] = max(dp[i + 1][j], dp[i][j - 1]);}
}
想把二维dp
数组压缩成一维,一般来说是把第一个维度,也就是i
这个维度去掉,只剩下j
这个维度。压缩后的一维dp
数组就是之前二维dp
数组的dp[i][..]
那一行。
我们先将上述代码进行改造,直接无脑去掉i
这个维度,把dp
数组变成一维:
for (int i = n - 2; i >= 0; i--) {for (int j = i + 1; j < n; j++) {// 在这里,一维 dp 数组中的数是什么?if (s[i] == s[j])dp[j] = dp[j - 1] + 2;elsedp[j] = max(dp[j], dp[j - 1]);}
}
上述代码的一维dp
数组只能表示二维dp
数组的一行dp[i][..]
,那我怎么才能得到dp[i+1][j-1], dp[i][j-1], dp[i+1][j]
这几个必要的的值,进行状态转移呢?
在代码中注释的位置,将要进行状态转移,更新dp[j]
,那么我们要来思考两个问题:
1、在对dp[j]
赋新值之前,dp[j]
对应着二维dp
数组中的什么位置?
2、dp[j-1]
对应着二维dp
数组中的什么位置?
对于问题 1,在对dp[j]
赋新值之前,dp[j]
的值就是外层 for 循环上一次迭代算出来的值,也就是对应二维dp
数组中dp[i+1][j]
的位置。
对于问题 2,dp[j-1]
的值就是内层 for 循环上一次迭代算出来的值,也就是对应二维dp
数组中dp[i][j-1]
的位置。
那么问题已经解决了一大半了,只剩下二维dp
数组中的dp[i+1][j-1]
这个状态我们不能直接从一维dp
数组中得到:
for (int i = n - 2; i >= 0; i--) {for (int j = i + 1; j < n; j++) {if (s[i] == s[j])// dp[i][j] = dp[i+1][j-1] + 2;dp[j] = ?? + 2;else// dp[i][j] = max(dp[i+1][j], dp[i][j-1]);dp[j] = max(dp[j], dp[j - 1]);}
}
因为 for 循环遍历i
和j
的顺序为从左向右,从下向上,所以可以发现,在更新一维dp
数组的时候,dp[i+1][j-1]
会被dp[i][j-1]
覆盖掉,图中标出了这四个位置被遍历到的次序:
那么如果我们想得到dp[i+1][j-1]
,就必须在它被覆盖之前用一个临时变量temp
把它存起来,并把这个变量的值保留到计算dp[i][j]
的时候。为了达到这个目的,结合上图,我们可以这样写代码:
for (int i = n - 2; i >= 0; i--) {// 存储 dp[i+1][j-1] 的变量int pre = 0;for (int j = i + 1; j < n; j++) {int temp = dp[j];if (s[i] == s[j])// dp[i][j] = dp[i+1][j-1] + 2;dp[j] = pre + 2;elsedp[j] = max(dp[j], dp[j - 1]); // 到下一轮循环,pre 就是 dp[i+1][j-1] 了pre = temp;}
}
别小看这段代码,这是一维dp
最精妙的地方,会者不难,难者不会。为了清晰起见,我用具体的数值来拆解这个逻辑:
假设现在i = 5, j = 7
且s[5] == s[7]
,那么现在会进入下面这个逻辑对吧:
if (s[5] == s[7])// dp[5][7] = dp[i+1][j-1] + 2;dp[7] = pre + 2;
我问你这个pre
变量是什么?是内层 for 循环上一次迭代的temp
值。
那我再问你内层 for 循环上一次迭代的temp
值是什么?是dp[j-1]
也就是dp[6]
,但这是外层 for 循环上一次迭代对应的dp[6]
,也就是二维dp
数组中的dp[i+1][6] = dp[6][6]
。
也就是说,pre
变量就是dp[i+1][j-1] = dp[6][6]
,也就是我们想要的结果。
那么现在我们成功对状态转移方程进行了降维打击,算是最硬的的骨头啃掉了,但注意到我们还有 base case 要处理呀:
// 二维 dp 数组全部初始化为 0
vector<vector<int>> dp(n, vector<int>(n, 0));
// base case
for (int i = 0; i < n; i++)dp[i][i] = 1;
如何把 base case 也打成一维呢?很简单,记住,状态压缩就是投影,我们把 base case 投影到一维看看:
二维dp
数组中的 base case 全都落入了一维dp
数组,不存在冲突和覆盖,所以说我们直接这样写代码就行了:
// 一维 dp 数组全部初始化为 1
vector<int> dp(n, 1);
至此,我们把 base case 和状态转移方程都进行了降维,实际上已经写出完整代码了:
int longestPalindromeSubseq(string s) {int n = s.size();// base case:一维 dp 数组全部初始化为 1vector<int> dp(n, 1);for (int i = n - 2; i >= 0; i--) {int pre = 0;for (int j = i + 1; j < n; j++) {int temp = dp[j];// 状态转移方程if (s[i] == s[j])dp[j] = pre + 2;elsedp[j] = max(dp[j], dp[j - 1]);pre = temp;}}return dp[n - 1];
}
本文就结束了,不过状态压缩技巧再牛逼,也是基于常规动态规划思路之上的。
你也看到了,使用状态压缩技巧对二维dp
数组进行降维打击之后,解法代码的可读性变得非常差了,如果直接看这种解法,任何人都是一脸懵逼的。
算法的优化就是这么一个过程,先写出可读性很好的暴力递归算法,然后尝试运用动态规划技巧优化重叠子问题,最后尝试用状态压缩技巧优化空间复杂度。
也就是说,你最起码能够熟练运用我们前文 动态规划框架套路详解 的套路找出状态转移方程,写出一个正确的动态规划解法,然后才有可能观察状态转移的情况,分析是否可能使用状态压缩技巧来优化。
希望读者能够稳扎稳打,层层递进,对于这种比较极限的优化,不做也罢。毕竟套路存于心,走遍天下都不怕!
往期推荐 ????
数据结构和算法学习指南
我作了首诗,保你闭着眼睛也能写对二分查找
我写了套框架,把滑动窗口算法变成了默写题
BFS 算法框架套路详解
回溯算法解题框架
动态规划解题框架
_____________
学好算法全靠套路,认准 labuladong 就够了。
算法小抄即将出版,公众号后台回复关键词「pdf」下载,回复「进群」可加入刷题群。
状态压缩技巧:动态规划的降维打击相关推荐
- 状态压缩:对动态规划进行降维打击
文章目录 我们号之前写过十几篇动态规划文章,可以说动态规划技巧对于算法效率的提升非常可观,一般来说都能把指数级和阶乘级时间复杂度的算法优化成 O(N^2),堪称算法界的二向箔,把各路魑魅魍魉统统打成二 ...
- 提高篇 第五部分 动态规划 第4章 状态压缩类动态规划
例1 骑士(Sgu223) 1592:[例 1]国王 信息学奥赛一本通(C++版)在线评测系统 https://blog.csdn.net/guoyangfan_/article/details/82 ...
- HOJ-2662Pieces Assignment(状态压缩,动态规划)
Pieces Assignment Source : zhouguyue Time limit : 1 sec Memory limit : 64 M Submitted : 415, Accepte ...
- caioj1495: [视频]基于连通性状态压缩的动态规划问题:Formula 2
本来想写一天插头的,但是这题太难受(绝望)500+的代码量..我选择下午放松一下. 先ORZ一下苏大佬(yz的cdq啊%%%%%)居然把cdq论文里面的题抠出来出数据放在c站(呵呵真是个悲伤的故事不过 ...
- 【转】状态压缩动态规划
引入 首先来说说"状态压缩动态规划"这个名称,顾名思义,状态压缩动态规划这个算法包括两个特点,第一是"状态压缩",第二是"动态规划". 状 ...
- 动态规划——状态压缩dp
文章目录 概述 状态压缩 使用条件 状压dp 位运算 棋盘(基于连通性)类问题 概述 例题 蒙德里安的梦想 小国王 玉米田 炮兵阵地 集合类问题 概述 例题 最短Hamilton路径 愤怒的小鸟 总结 ...
- HDU 1693(状态压缩 插头DP)
我们引用国家队2008年陈丹琦的大作--<基于连通性状态压缩的动态规划问题>,上面对于插头.轮廓线的概念有详细的解释,不再赘述. 我们使用一个三维数组,前两维表示所在的格子,后一维表示轮廓 ...
- 状态压缩DP(大佬写的很好,转来看)
奉上大佬博客 https://blog.csdn.net/accry/article/details/6607703 动态规划本来就很抽象,状态的设定和状态的转移都不好把握,而状态压缩的动态规划解决的 ...
- LeetCode第 57 场力扣夜喵双周赛(差分数组、单调栈) and 第 251 场力扣周赛(状态压缩动规,树的序列化,树哈希,字典树)
LeetCode第 57 场力扣夜喵双周赛 离knight勋章越来越近,不过水平没有丝毫涨进 1941. 检查是否所有字符出现次数相同 题目描述 给你一个字符串 s ,如果 s 是一个 好 字符串,请 ...
最新文章
- R语言使用ggplot2包使用geom_density()函数绘制密度图(连续色彩、离散色彩、梯度色彩)实战(density plot)
- Python基础之标准库datetime 时间与日期的使用
- FASTICA独立成分分析matlab代码实现
- Fluid 0.4 新版本正式发布
- input子系统分析二
- Learning Scrapy笔记(五)- Scrapy登录网站
- 荣耀v40还会适配鸿蒙,荣耀年度旗舰V40再确认!将搭载“双芯片”:还能升级鸿蒙系统...
- 64位系统使用Access数据库文件的彻底解决方法
- C语言中多维数组的内存分配和释放(malloc与free)(转)
- Python Numpy中reshape函数参数-1的含义
- 组态软件android版,昆仑通态组态软件
- EPSON/爱普生打印机Linux打印服务器基于ARM驱动安装踩坑CUPS实现支持远程打印AirPrint
- 5gh掌上云计算认证不通过_华为云计算认证含金量高么?
- 第六届ACM省赛总结--吕云飞
- 倒计时1天 | 大势智慧2022新品发布会全面而来!
- 异常解决之——无法在Web服务器上启动调试。远程服务器返回错误:(405)
- .NET中的枚举用法浅析
- Spring 依赖注入的理解及三种注入方式
- 基于道格拉斯普克算法的轮廓点简化
- Linux内核UDP性能优化(超详细讲解)
热门文章
- mysql数据库完整实例-“汽车维修”
- 音视频5.4——两个MP3混音合成一个MP3
- 利用Excel批量修改图片名称
- 轮播移动端 html,移动端h5如何使用轮播插件swipe
- spring 自带的定时器task
- ASP.NET MVC 实现页落网资源分享网站+充值管理+后台管理(11)之支付管理及广告管理...
- 【天池】金融风控-贷款违约预测(五)—— 模型融合
- 三国演义人物词频统计-4
- 十字路口通行优先权,十字路口通行规则图解
- c语言的编写程序--最简单的算术题