专题7:动态规划 记忆化搜索
1. 什么是动态规划 ?
这是算法导论对动态规划的定义:
动态规划( dynamic programming,常简称为 dp ) 与分治方法相似,都是通过组合子问题的解来求解原问题( 在这里,“programming”指的是一种表格法,并非编写计算机程序 )。分治方法将问题划分为互不相交的子问题,递归地求解子问题,再将它们的解组合起来,求出原问题的解。与之相反,动态规划应用于子问题重叠的情况,即不同的子问题具有公共的子子问题(子问题的求解是递归进行的,将其划分为更小的子子问题)。在这种情况下,分治算法会做许多不必要的工作,它会反复地求解那些公共子子问题。而动态规划算法对每个子子问题只求解一次,将其解保存在一个表格中,从而无需每次求解一个子子问题时都重新计算,避免了这种不必要的计算工作。
动态规划方法通常用来求解最优化问题(optimization problem)。这类问题可以有很多可行解,每个解都有一个值,我们希望寻找具有最优值(最小值或最大值)的解。我们称这样的解为问题的一个最优解(an optimal solution),而不是最优解(the optimal solution),因为可能有多个解都达到最优值。
2. 使用动态规划有什么条件么?
Of coures. 动态规划求解的问题需满足 :
1)最优子结构:问题的最优解由相关子问题的最优解组合而成,而这些子问题可以独立求解;
2)无后效性:确定当前子问题最优解不受未确定最优解的其他子问题的影响。
3. 如何用动态规划解决问题?
我们通常按如下4个步骤来设计一个动态规划算法:
1)状态定义(最优子结构 、无后效性);
2)确定状态转移方程 ;
3)状态初始化;
4)确定求解方向(通常采用自底向上的方法),求解;
5)利用计算出的信息构造一个最优解(如果需要的话)。
(记忆化搜索是个代替dp的好方法,那么什么是记忆化搜索呢? 记忆化搜索本身就是一个搜索,就是一个分治,只不过它把解决过的子问题的答案记录下来,以后再遇到这个子问题时可以直接给出答案从而避免再次求解,这正解决了分治存在的重复工作问题)
4. 举例
eg1:现在有一段长度为n的木材,要将其切割后售出,木材长度与价格的对应关系如下表,问如何切割能获得最大收益。
分治:
int n, maxGet;
int price[11] = {0,1,5,8,9,10,17,17,20,24,30};void dfs(int cur,int rm) // cur:当前已切割木材的价值; reamin:等待切割的木材长度
{if(rm == 0) maxGet = max(maxGet, cur);for(int i=1; i<=rm; i++) dfs(cur+price[i], rm-i);
}
int main()
{cin >> n;maxGet = -1;dfs(0,n);cout << "最优收益为: " << maxGet;
}
画出求解结构树可以发现,求解过程中很多子问题会重复求解。这种方法会枚举种可能,结合求解结构树可以计算时间复杂度为O()。
记忆化搜索:(自顶向下)
int n, maxGet[11]; // maxGet[k] 切割长度为k的木材最大收益
int price[11] = {0,1,5,8,9,10,17,17,20,24,30};int dfsMemory(int rm) // cur:当前已切割木材的价值; reamin:等待切割的木材长度
{if(maxGet[rm] != -1) // 此子问题已解决过 return maxGet[rm];if(rm == 0)return 0;for(int i=1; i<=rm; i++) maxGet[rm] = max(maxGet[rm], price[i]+dfs(rm-i)); return maxGet[rm];
}
int main()
{cin >> n;memset(maxGet, -1, sizeof maxGet);maxGet[n] = dfs(n);cout << "最优收益为: " << maxGet[n] << endl;
}
dp:(自底向上)
最优子结构:maxGet[i] = maxGet[i-k] + maxGet[k]
int price[11] = {0,1,5,8,9,10,17,17,20,24,30};int dp(int n)
{int maxGet[11]; // maxGet[k] 切割长度为k的木材最大收益 memset(maxGet, -1, sizeof maxGet);maxGet[0] = 0;// dpfor(int i=1; i<=n; i++) {for(int j=1; j<=i; j++)maxGet[i] = max(maxGet[i], price[j]+maxGet[i-j]);}return maxGet[n];
}
int main()
{int n;cin >> n;cout << "最优收益为: " << dp(n) << endl;
}
时间复杂度O(2n)
5. 动态规化相对于普通分治的优势
dp有利于解决重复子问题,可以避免重复计算
习题1
LeetCode1143. 最长公共子序列
最优化问题,考虑使用dp.
1)定义状态(最优子结构)
毫无疑问状态必然为字符串1和字符串2的最长公共子序列长度,以 dp[i][j] 表示 字符串1[0:i-1] 和 字符串2[0:i-1] 的最长公共子序列长度,求解目标状态: dp[text1.len][text2.len].
那么是否满足最优子结构?其实验证最优子结构的过程就是寻找状态转移方程的过程;
2)确定状态转移方程
与dp[i][j]相邻的状态有 dp[i-1][j-1]、dp[i-1][j]、dp[i][j-1],
当 text1[i] 和 text2[j] 相同时,dp[i][j] = 1 + dp[i-1][j-1] ;
当 text1[i] 和 text2[j] 不相同时呢?这时可以忽略 text1[i] 和 text2[j]同时存在,dp[i][j] = max(dp[i-1][j],dp[i][j-1]).
3)状态初始化
存在i-1, j-1。 则i,j最小均为1 (求解某一状态需要用到其上方、左侧、左上方的状态),则初始状态为 dp[0][k1] 、dp[k2][0]。
4)确定求解方向
根据状态转移方程知:从上到下,从左到右
代码:
class Solution {
public:int longestCommonSubsequence(string text1, string text2) {int len1 = text1.length(), len2 = text2.length();int dp[len1+2][len2+2];for(int i=0; i<=len1; i++)dp[i][0] = 0;for(int j=0; j<=len2; j++)dp[0][j] = 0;for(int i=1; i<=len1; i++) {for(int j=1; j<=len2; j++) {if(text1[i-1] == text2[j-1])dp[i][j] = 1+dp[i-1][j-1];elsedp[i][j] = max(dp[i-1][j], dp[i][j-1]);}}return dp[len1][len2];}
};
习题2
LeetCode62. 不同路径
1)定义状态
dp[i][j] 表示从 (i,j) 处到达网格右下角,求解目标状态: dp[1][1]
2)状态转移方程
只能向右、向下走, ∴ dp[i][j] = dp[i+1][j] + dp[i][j+1];
3)初始状态
最后一列只能向下走,最后一行只能向右走,∴ dp[k][n] = 1 , dp[m][k] = 1
4)求解方向
从右到左,从下到上
dp代码:
class Solution {
public:int uniquePaths(int m, int n) {int dp[m+1][n+1]; // 下标从1开始for(int i=1; i<=m; i++) dp[i][n] = 1;for(int j=1; j<=n; j++) dp[m][j] = 1;for(int i=m-1; i>=1; i--) for(int j=n-1; j>=1; j--)dp[i][j] = dp[i+1][j] + dp[i][j+1];return dp[1][1];}
};
记忆化搜索代码:
class Solution {
public:int dp[101][101]; // 下标从1开始int dfs(int m, int n, int i, int j) {if(dp[i][j] != 0)return dp[i][j];if(i == m && j == n)return dp[i][j] = 1;if(i+1 <= m)dp[i][j] += dfs(m, n, i+1, j);if(j+1 <= n)dp[i][j] += dfs(m, n, i, j+1);return dp[i][j];}int uniquePaths(int m, int n) {memset(dp, 0, sizeof dp);return dfs(m,n,1,1);}
};
习题3
LeetCode63. 不同路径 II
和习题2基本一样,多加一个判断障碍的if语句即可。
dp代码:
class Solution {
public:int uniquePathsWithObstacles(vector<vector<int> >& obs) {int m = obs.size()-1, n = obs[0].size()-1;long long int dp[m+1][n+1]; //下标从0开始,[0:m-1]memset(dp,0,sizeof dp);for(int i=m; i>=0; i--) { // 状态初始化最后一行if(obs[i][n] == 1) break;else dp[i][n] = 1; }for(int j=n; j>=0; j--) { //状态初始化最后一列if(obs[m][j] == 1) break;else dp[m][j] = 1; }for(int i=m-1; i>=0; i--) {for(int j=n-1; j>=0; j--) {if(obs[i][j] == 1) continue;dp[i][j] = dp[i+1][j] + dp[i][j+1];}}return dp[0][0]; }
};
分析习题2和习题3的dp代码可以发现,两道题的时间复杂度和空间复杂度均为O(mn), 然而根据状态转移方程发现,每次确定一个状态时仅仅用到其本身和其后方或下方最多三个状态(如果后方或下方有状态),
如图,确定红色的位置的状态,只会用到这两个黄色位置的状态。结合我们的状态转移方程和求解方向,
for(int i=m-1; i>=0; i--) {for(int j=n-1; j>=0; j--) {if(obs[i][j] == 1) continue;dp[i][j] = dp[i+1][j] + dp[i][j+1];}
}
可以发现,求解第i行时,只需用到第i行和第i+1行的状态,第i+k(k>1)行的状态不会且之后不会再使用。
因此我们可以使用滚动数组,将空间复杂度降低到O(2n).
代码:
class Solution {
public:int uniquePathsWithObstacles(vector<vector<int> >& obs) {int m = obs.size()-1, n = obs[0].size()-1;long long int dp[2][n+1]; //下标从0开始,[0:m/n]memset(dp,0,sizeof dp);for(int j=n; j>=0; j--) { //状态初始化最后一行 if(obs[m][j] == 1) break;else dp[m%2][j] = 1; }for(int i=m-1; i>=0; i--) {if(obs[i][n] == 1) dp[i%2][n] = 0;else dp[i%2][n] = dp[(i+1)%2][n];for(int j=n-1; j>=0; j--) {if(obs[i][j] == 1) dp[i%2][j] = 0;else dp[i%2][j] = dp[(i+1)%2][j] + dp[i%2][j+1];}}return dp[0][0]; }
};
习题4
LeetCode5. 最长回文子串
方法一:枚举,从中心向外“探测”,代码
class Solution {
public:string longestPalindrome(string s) {int maxLen=1, len=s.length(), l=0, r=0;for(int i=0; i<len; i++) {int k = min(i,len-i-1), t = 1; for(int j=1; j<=k; j++) { // 以s[i]为中心,奇数长度if(s[i-j] != s[i+j]) break;t += 2;if(t > maxLen) {maxLen = t;l = i-j;r = i+j;}}if(s[i] == s[i+1]) { // 以s[i]和s[i+1]为中心,偶数长度t = 2;if(t > maxLen) {maxLen = t;l = i;r = i+1;}k = min(i, len-i-2);for(int j=1; j<=k; j++) {if(s[i-j] != s[i+1+j]) break;t += 2;if(t > maxLen) {maxLen = t;l = i-j;r = i+1+j;}}}}string ans;for(int i=l; i<=r; i++)ans += s[i];return ans;}
};
方法二:dp方法
1)定义状态
(bool) isPl[i][j] 表示 s[i:j] 是否为回文串;
2)状态转移方程
s[i:j] 为回文串 充分必要条件: s[i+1:j-1] 为回文串且 s[i] == s[j]; 则isPl[i:j] = ( isPl[i+1:j-1] & (s[i] == s[j] ) ).
3)初始状态
单个字符串为回文,两个字符组成的字符串是否为回文取决于两个这个字符是否相等;
即:isPl[k][k] = true, isPl[k][k+1] = ( s[k] == s[k+1] );
4)求解方向
根据状态转移方程,求解方向为“从内向外”,从短字符串到长字符串;
5)构造最优解
记录最长字符串的左右下标即可。
代码:
class Solution {
public:string longestPalindrome(string s) {int maxLen=1, len=s.length(), l=0, r=0;bool isPl[len][len];memset(isPl, false, sizeof isPl);for(int L=0; L<len; L++) { // L 当前验证字符串的长度-1for(int i=0; i<len-L; i++) {int j = i+L;if(L == 0) isPl[i][j] = true;else if(L == 1) isPl[i][j] = (s[i] == s[j]);else isPl[i][j] = (isPl[i+1][j-1] & (s[i] == s[j]) );if(isPl[i][j] && (j-i+1 > maxLen) ) {maxLen = j-i+1;l = i;r = j;} }}string ans = "";for(int i=l;i<=r;i++)ans += s[i];return ans; }
};
习题5
LeetCode120. 三角形最小路径和
自底向上
class Solution {
public:int minimumTotal(vector<vector<int> >& tri) {int h = tri.size(); for(int i=h-2; i>=0; i--) { for(int j=i; j>=0; j--) tri[i][j] = tri[i][j] + min(tri[i+1][j], tri[i+1][j+1]); }return tri[0][0];}
};
198. 打家劫舍 - 力扣(LeetCode) (leetcode-cn.com)
专题7:动态规划 记忆化搜索相关推荐
- [Leetcode][第312题][JAVA][戳气球][动态规划][记忆化搜索]
[问题描述][困难] [解答思路] 1. 记忆化搜索 时间复杂度:O(n^3) 空间复杂度:O(n^2) class Solution {public int[][] rec;public int[] ...
- BZOJ_P2461 [BeiJing2011]符环(动态规划/记忆化搜索)
BZOJ传送门 Time Limit: 20 Sec Memory Limit: 128 MB Submit: 113 Solved: 59 [Submit][Status][Discuss] Des ...
- 记忆化搜索 Memorization Search
记忆化搜索 Memorization Search 什么是记忆化搜索 记忆化搜索函数的三个特点 记忆化搜索 vs 动态规划 三种适用于DP的场景 三种不适用于DP的场景 Examples: Leetc ...
- 动态规划:记忆化搜索
记忆化搜索和递推都是为了解决子问题重复计算而产生的 虽然动态规划的转移图是一个DAG,但是一个状态可以被其他的状态复用多次 因此为了提高动态规划的效率,记忆化搜索便产生了 但是有时候,状态转移图是不容 ...
- [蓝桥杯]算法提高 第二点五个不高兴的小明(记忆化搜索||动态规划)
问题描述 有一条长为n的走廊,小明站在走廊的一端,每次可以跳过不超过p格,每格都有一个权值wi. 小明要从一端跳到另一端,不能回跳,正好跳t次,请问他跳过的方格的权值和最大是多少? 输入格式 输入的第 ...
- 【BZOJ1048】分割矩阵(记忆化搜索,动态规划)
[BZOJ1048]分割矩阵(记忆化搜索,动态规划) 题面 BZOJ 洛谷 题解 一个很简单的\(dp\),写成记忆化搜索的形式的挺不错的. #include<iostream> #inc ...
- [Leetcode][第322题][JAVA][零钱兑换][回溯][记忆化搜索][动态规划]
[问题描述][中等] [解答思路] 1. 递归(超时) class Solution {int res = Integer.MAX_VALUE;public int coinChange(int[] ...
- 记忆化搜索=搜索的形式+动态规划的思想(来自百度百科)
记忆化搜索=搜索的形式+动态规划的思想 记忆化搜索:算法上依然是搜索的流程,但是搜索到的一些解用动态规划的那种思想和模式作一些保存. 一般说来,动态规划总要遍历所有的状态,而搜索可以排除一些无效状态. ...
- 递归 dfs 记忆化搜索 动态规划
今天做洛谷P1434 [SHOI2002]滑雪 的时候仔细想了想记忆化搜索 现在总结一下 为了描述问题的某一状态,必须用到该状态的上一状态,而描述上一状态,又必须用到上一状态的上一状态--这种用自已来 ...
最新文章
- 把变量赋值给寄存器_用C语言对DSP的寄存器进行操作?
- TX2安装中文输入法
- JDBC的CRUD操作之PreparedStatement的查询操作
- Windows与Linux的主要区别
- DDD as Code:如何用代码诠释领域驱动设计?
- WPF圆角按钮与触发颜色变化
- 我的worktools集合们
- GT决赛第二次讨论会议
- 零基础掌握区块链关键概念
- 深入理解HTTP协议—HTTP协议详解(真的很经典)
- md5解密 python_python写一个md5解密器示例
- 烽火通信力推SDN技术在网络中实现
- 圆柱体积怎么算立方公式_圆柱体积怎么算立方 高为3米则此圆柱的体积为27
- 服务器虚拟机装nas,nas虚拟主机(nas为什么要装虚拟机)
- 几个重要的电子元器件网站
- KDA的新宠儿,金贝KD6,更大算力,探索无限可能
- Windbg 2进程线程结构分析
- 区块链技术在三角债清收领域的应用思考
- E2GameboyAX正式版
- matlab牛顿环gif,牛顿环干涉实验的 Matlab模拟
热门文章
- 学习随记三十一——递归实现二叉查找树
- 从零手写pm-cli脚手架,统一阿里拍卖源码架构
- 2019中国机器人大赛窄足机器人赛后总结
- ftp服务器 修改文件,ftp服务器修改文件属性的权限
- Centos在NAT模式下的设置
- AutoIt 在线中文文档、开发工具 (GUI 图形开发工具)
- android原生农场壁纸,Android 6.0高清壁纸下载-Android 6.0原生壁纸高清免费打包下载-东坡下载...
- 计算机匹配函数,匹配函数VLOOKUP使用方法
- Vue中常用的提示信息:
- linux qt fscanf,fscanf QT小部件C++