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:动态规划 记忆化搜索相关推荐

  1. [Leetcode][第312题][JAVA][戳气球][动态规划][记忆化搜索]

    [问题描述][困难] [解答思路] 1. 记忆化搜索 时间复杂度:O(n^3) 空间复杂度:O(n^2) class Solution {public int[][] rec;public int[] ...

  2. BZOJ_P2461 [BeiJing2011]符环(动态规划/记忆化搜索)

    BZOJ传送门 Time Limit: 20 Sec Memory Limit: 128 MB Submit: 113 Solved: 59 [Submit][Status][Discuss] Des ...

  3. 记忆化搜索 Memorization Search

    记忆化搜索 Memorization Search 什么是记忆化搜索 记忆化搜索函数的三个特点 记忆化搜索 vs 动态规划 三种适用于DP的场景 三种不适用于DP的场景 Examples: Leetc ...

  4. 动态规划:记忆化搜索

    记忆化搜索和递推都是为了解决子问题重复计算而产生的 虽然动态规划的转移图是一个DAG,但是一个状态可以被其他的状态复用多次 因此为了提高动态规划的效率,记忆化搜索便产生了 但是有时候,状态转移图是不容 ...

  5. [蓝桥杯]算法提高 第二点五个不高兴的小明(记忆化搜索||动态规划)

    问题描述 有一条长为n的走廊,小明站在走廊的一端,每次可以跳过不超过p格,每格都有一个权值wi. 小明要从一端跳到另一端,不能回跳,正好跳t次,请问他跳过的方格的权值和最大是多少? 输入格式 输入的第 ...

  6. 【BZOJ1048】分割矩阵(记忆化搜索,动态规划)

    [BZOJ1048]分割矩阵(记忆化搜索,动态规划) 题面 BZOJ 洛谷 题解 一个很简单的\(dp\),写成记忆化搜索的形式的挺不错的. #include<iostream> #inc ...

  7. [Leetcode][第322题][JAVA][零钱兑换][回溯][记忆化搜索][动态规划]

    [问题描述][中等] [解答思路] 1. 递归(超时) class Solution {int res = Integer.MAX_VALUE;public int coinChange(int[] ...

  8. 记忆化搜索=搜索的形式+动态规划的思想(来自百度百科)

    记忆化搜索=搜索的形式+动态规划的思想 记忆化搜索:算法上依然是搜索的流程,但是搜索到的一些解用动态规划的那种思想和模式作一些保存. 一般说来,动态规划总要遍历所有的状态,而搜索可以排除一些无效状态. ...

  9. 递归 dfs 记忆化搜索 动态规划

    今天做洛谷P1434 [SHOI2002]滑雪 的时候仔细想了想记忆化搜索 现在总结一下 为了描述问题的某一状态,必须用到该状态的上一状态,而描述上一状态,又必须用到上一状态的上一状态--这种用自已来 ...

最新文章

  1. 把变量赋值给寄存器_用C语言对DSP的寄存器进行操作?
  2. TX2安装中文输入法
  3. JDBC的CRUD操作之PreparedStatement的查询操作
  4. Windows与Linux的主要区别
  5. DDD as Code:如何用代码诠释领域驱动设计?
  6. WPF圆角按钮与触发颜色变化
  7. 我的worktools集合们
  8. GT决赛第二次讨论会议
  9. 零基础掌握区块链关键概念
  10. 深入理解HTTP协议—HTTP协议详解(真的很经典)
  11. md5解密 python_python写一个md5解密器示例
  12. 烽火通信力推SDN技术在网络中实现
  13. 圆柱体积怎么算立方公式_圆柱体积怎么算立方 高为3米则此圆柱的体积为27
  14. 服务器虚拟机装nas,nas虚拟主机(nas为什么要装虚拟机)
  15. 几个重要的电子元器件网站
  16. KDA的新宠儿,金贝KD6,更大算力,探索无限可能
  17. Windbg 2进程线程结构分析
  18. 区块链技术在三角债清收领域的应用思考
  19. E2GameboyAX正式版
  20. matlab牛顿环gif,牛顿环干涉实验的 Matlab模拟

热门文章

  1. 学习随记三十一——递归实现二叉查找树
  2. 从零手写pm-cli脚手架,统一阿里拍卖源码架构
  3. 2019中国机器人大赛窄足机器人赛后总结
  4. ftp服务器 修改文件,ftp服务器修改文件属性的权限
  5. Centos在NAT模式下的设置
  6. AutoIt 在线中文文档、开发工具 (GUI 图形开发工具)
  7. android原生农场壁纸,Android 6.0高清壁纸下载-Android 6.0原生壁纸高清免费打包下载-东坡下载...
  8. 计算机匹配函数,匹配函数VLOOKUP使用方法
  9. Vue中常用的提示信息:
  10. linux qt fscanf,fscanf QT小部件C++