五种常用算法之二:动态规划算法

动态规划算法:

基本思想:

  动态规划算法通常用于求解具有某种最优性质的问题。在这类问题中,可能会有许多可行解。每一个解都对应于一个值,我们希望找到具有最优值的解。动态规划算法与分治法类似,其基本思想也是将待求解问题分解成若干个子问题,先求解子问题,然后从这些子问题的解得到原问题的解。与分治法不同的是,适合于用动态规划求解的问题,经分解得到子问题往往不是互相独立的。若用分治法来解这类问题,则分解得到的子问题数目太多,有些子问题被重复计算了很多次。如果我们能够保存已解决的子问题的答案,而在需要时再找出已求得的答案,这样就可以避免大量的重复计算,节省时间。我们可以用一个表来记录所有已解的子问题的答案。不管该子问题以后是否被用到,只要它被计算过,就将其结果填入表中。这就是动态规划法的基本思路。具体的动态规划算法多种多样,但它们具有相同的填表格式

  动态规划算法与分治法最大的差别是:适合于用动态规划法求解的问题,经分解后得到的子问题往往不是互相独立的(即下一个子阶段的求解是建立在上一个子阶段的解的基础上,进行进一步的求解)


应用场景:

适用动态规划的问题必须满足最优化原理、无后效性和重叠性。
1.最优化原理(最优子结构性质) 最优化原理可这样阐述:一个最优化策略具有这样的性质,不论过去状态和决策如何,对前面的决策所形成的状态而言,余下的诸决策必须构成最优策略。简而言之,一个最优化策略的子策略总是最优的。一个问题满足最优化原理又称其具有最优子结构性质。

2.无后效性  将各阶段按照一定的次序排列好之后,对于某个给定的阶段状态,它以前各阶段的状态无法直接影响它未来的决策,而只能通过当前的这个状态。换句话说,每个状态都是过去历史的一个完整总结。这就是无后向性,又称为无后效性。

3.子问题的重叠性  动态规划将原来具有指数级时间复杂度的搜索算法改进成了具有多项式时间复杂度的算法。其中的关键在于解决冗余,这是动态规划算法的根本目的。动态规划实质上是一种以空间换时间的技术,它在实现的过程中,不得不存储产生过程中的各种状态,所以它的空间复杂度要大于其它的算法。


动态规划算法经典案例:

  案例一:

  有n级台阶,一个人每次上一级或者两级,问有多少种走完n级台阶的方法。

  分析:动态规划的实现的关键在于能不能准确合理的用动态规划表来抽象出 实际问题。在这个问题上,我们让f(n)表示走上n级台阶的方法数。

  那么当n为1时,f(n) = 1,n为2时,f(n) =2,就是说当台阶只有一级的时候,方法数是一种,台阶有两级的时候,方法数为2。那么当我们要走上n级台阶,必然是从n-1级台阶迈一步或者是从n-2级台阶迈两步,所以到达n级台阶的方法数必然是到达n-1级台阶的方法数加上到达n-2级台阶的方法数之和。即f(n) = f(n-1)+f(n-2),我们用dp[n]来表示动态规划表,dp[i],i>0,i<=n,表示到达i级台阶的方法数。

  

 1 public class CalculationSteps {
 2     //动态规划表,用来记录到达i级台阶的方法数
 3     public static int[] steps = new int[11];
 4
 5     public static void main(String[] args) {
 6         steps[10] = calStep(10);
 7
 8         for (int i = 0; i < steps.length; i++) {
 9             System.out.print(steps[i]+" ");
10         }
11         System.out.println();
12         System.out.println(steps[10]);
13     }
14
15     //计算到达i级台阶的方法数
16     public static int calStep(int n){
17         //如果为第一级台阶或者第二级台阶 则直接返回n
18         if(n==1||n==2){
19             return n;
20         }
21         //计算到达n-1级台阶的方法数
22         if(steps[n-1]==0){
23             steps[n-1] = calStep(n-1);
24         }
25         //计算到达n-2级台阶的方法数
26         if(steps[n-2] == 0){
27             steps[n-2] = calStep(n-2);
28         }
29         //到达第n级台阶=到达n-1级台阶+到达n-2级台阶
30         return steps[n-1]+steps[n-2];
31     }
32 }

运行结果如下:

0 1 2 3 5 8 13 21 34 55 89
89


案例2:

给定一个矩阵m,从左上角开始每次只能向右走或者向下走,最后达到右下角的位置,路径中所有数字累加起来就是路径和,返回所有路径的最小路径和,如果给定的m如下,那么路径1,3,1,0,6,1,0就是最小路径和,返回12.

1 3 5 9

8 1 3 4

5 0 6 1

8 8 4 0

分析:对于这个题目,假设m是m行n列的矩阵,那么我们用dp[m][n]来抽象这个问题,dp[i][j]表示的是从原点到i,j位置的最短路径和。我们首先计算第一行和第一列,直接累加即可,那么对于其他位置,要么是从它左边的位置达到,要么是从上边的位置达到,我们取左边和上边的较小值,然后加上当前的路径值,就是达到当前点的最短路径。然后从左到右,从上到下依次计算即可。

Java代码实现:

 1 /**
 2  * 给定一个矩阵m,从左上角开始每次只能向右走或者向下走
 3  * 最后达到右下角的位置,路径中所有数字累加起来就是路径和,
 4  * 返回所有路径的最小路径和
 5  */
 6 public class MinSteps {
 7
 8     public static int[][] steps=new int[4][4];
 9
10     public static void main(String[] args) {
11         int[][] arr = {{4,1,5,3},{3,2,7,7},{6,5,2,8},{8,9,4,5}};
12         steps[3][3] = minSteps(arr, 3, 3);
13         print(steps);
14     }
15
16
17     public static int minSteps(int[][] arr,int row,int col){
18         //如果为起始位置,则直接返回
19         if(row==0&&col==0){
20             steps[row][col] = arr[row][col];
21             return steps[row][col];
22         }
23
24         //计算到arr[row][col]的左面位置的值
25         if(col>=1&&steps[row][col-1]==0){
26             steps[row][col-1]=minSteps(arr, row, col-1);
27         }
28         //计算到arr[row][col]的上面位置的值
29         if(row>=1&&steps[row-1][col]==0){
30             steps[row-1][col]=minSteps(arr, row-1, col);
31         }
32         //如果为第一行,则直接加左面位置上的值
33         if(row==0&&col!=0){
34             steps[row][col] = arr[row][col]+steps[row][col-1];
35         }else if(col == 0&&row!=0){
36             //如果为第一列,则直接加上上面位置上的值
37             steps[row][col] = arr[row][col]+steps[row-1][col];
38         }else{
39             //比较到达左面位置和到达上面位置的值的大小,加上两者的最大值
40             steps[row][col] =arr[row][col]+min(steps[row][col-1],steps[row-1][col]);
41         }
42         return steps[row][col];
43     }
44
45     private static int min(int minSteps, int minSteps2) {
46         return minSteps>minSteps2?minSteps:minSteps2;
47     }
48
49
50     static void print(int[][] arr){
51         for (int i = 0; i < arr.length; i++) {
52             for (int j = 0; j < arr[i].length; j++) {
53                 System.out.println("到达arr["+i+"]["+j+"]的最大路径:"+arr[i][j]);
54             }
55         }
56     }
57 }

运行结果:

到达arr[0][0]的最大路径:4
到达arr[0][1]的最大路径:5
到达arr[0][2]的最大路径:10
到达arr[0][3]的最大路径:13
到达arr[1][0]的最大路径:7
到达arr[1][1]的最大路径:9
到达arr[1][2]的最大路径:17
到达arr[1][3]的最大路径:24
到达arr[2][0]的最大路径:13
到达arr[2][1]的最大路径:18
到达arr[2][2]的最大路径:20
到达arr[2][3]的最大路径:32
到达arr[3][0]的最大路径:21
到达arr[3][1]的最大路径:30
到达arr[3][2]的最大路径:34
到达arr[3][3]的最大路径:39

案例3:最长公共子序列问题

  最长公共子序列问题是要找到两个字符串间的最长公共子序列。假设有两个字符串sudjxidjs和xidjxidpolkj,其中djxidj就是他们的最长公共子序列。许多问题都可以看成是公共子序列的变形。例如语音识别问题就可以看成最长公共子序列问题。

  假设两个字符串分别为A=a1a2..am,B=b1b2..bn,则m为A的长度,n为B的长度。那么他们的最长公共子序列分为两种情况。

  1、am=bn,这时他们的公共子序列一定为的长度F(m,n)=F(m-1,n-1)+am

  2、am≠bn,这时他们的公共子序列一定为的长度F(m,n)=Max(F(m-1,n),F(m,n-1))

  

  1 /**
  2  * 求两个字符串之间的最长子序列
  3  */
  4 public class MaxCommonStr {
  5     // 数组用来存储两个字符串的最长公共子序列
  6     public static String[][] result = new String[10][15];
  7
  8     public static void main(String[] args) {
  9         String strA = "sudjxidjs";
 10         String strB = "xidjxidpolkj";
 11         System.out.println(maxCommonStr(strA, strB));
 12         // System.out.println(strA.charAt(strA.length()-1));
 13     }
 14
 15     /**
 16      * 获取两个字符串的最大公共子序列
 17      *
 18      * @param strA
 19      * @param strB
 20      * @return
 21      */
 22     public static String maxCommonStr(String strA, String strB) {
 23         // 分别获取两个字符串的长度
 24         int lenA = strA.length();
 25         int lenB = strB.length();
 26
 27         // 如果字符串strA的长度为1,那么如果strB包含字符串strA,则公共子序列为strA,否则为null
 28         if (lenA == 1) {
 29             if (strB.contains(strA)) {
 30                 result[lenA - 1][lenA - 1] = strA;
 31             } else {
 32                 result[lenA - 1][lenA - 1] = "";
 33             }
 34             return result[lenA - 1][lenA - 1];
 35         }
 36
 37         // 如果字符串strB的长度为1,那么如果strA包含字符串strB,则公共子序列为strB,否则为null
 38         if (lenB == 1) {
 39             if (strA.contains(strB)) {
 40                 result[lenA - 1][lenA - 1] = strB;
 41             } else {
 42                 result[lenA - 1][lenA - 1] = "";
 43             }
 44             return result[lenA - 1][lenA - 1];
 45         }
 46
 47         // 如果字符串strA的最后一位和strB的最后一位相同的话,
 48         if (strA.charAt(lenA - 1) == strB.charAt(lenB - 1)) {
 49             //先判断数组result[lenA - 2][lenB - 2] == null,这样可以减少一些重复运算
 50             if (result[lenA - 2][lenB - 2] == null) {
 51                 //求strA和strB都去除最后一位剩余字符串的最大公共子序列f
 52                 result[lenA - 2][lenB - 2] = maxCommonStr(strLenSub(strA), strLenSub(strB)) ;
 53             }
 54             //strA和strB的最大公共子序列就是他们各去除最后一位剩余字符串的最大公共子序列+strA或者strB的最后一位
 55             result[lenA-1][lenB-1] = result[lenA - 2][lenB - 2]+ strA.charAt(lenA - 1);
 56         } else {
 57             //否则
 58             if (result[lenA - 2][lenB-1] == null) {
 59                 //计算strA去除最后一位后和strB的最大子序列
 60                 result[lenA - 2][lenB-1] = maxCommonStr(strLenSub(strA), strB);
 61             }
 62             if (result[lenA-1][lenB - 2] == null) {
 63                 //计算strB去除最后一位后和strA的最大子序列
 64                 result[lenA-1][lenB - 2] = maxCommonStr(strA, strLenSub(strB));
 65             }
 66             //等于result[lenA - 2][lenB-1]和result[lenA-1][lenB - 2]中的最大数
 67             result[lenA-1][lenB-1] = max(result[lenA - 2][lenB-1], result[lenA-1][lenB - 2]);
 68         }
 69         return result[lenA-1][lenB-1];
 70     }
 71
 72     /**
 73      * 使字符串去除最后一位,返回该新的字符串
 74      * @param str
 75      * @return
 76      */
 77     public static String strLenSub(String str) {
 78         return str.substring(0, str.length() - 1);
 79     }
 80
 81     /**
 82      * 比较两个字符串长度,返回最长字符串 当两个字符串长度相等时,返回任意字符串
 83      *
 84      * @param strA
 85      * @param strB
 86      * @return
 87      */
 88     public static String max(String strA, String strB) {
 89         if (strA == null && strB == null) {
 90             return "";
 91         } else if (strA == null) {
 92             return strB;
 93         } else if (strB == null) {
 94             return strA;
 95         }
 96         if (strA.length() > strB.length()) {
 97             return strA;
 98         } else {
 99             return strB;
100         }
101     }
102 }

运行结果:

djxidj

写在最后:
  此篇随笔仅用来记录我的学习内容,如有错误,欢迎指正。谢谢!!!

时间: 2024-10-07 04:17:43

五种常用算法之二:动态规划算法的相关文章

五种常用的算法设计技巧之二:分治算法

一,介绍 分治算法主要包含两个步骤:分.治.分,就是递归地将原问题分解成小问题:治则是:在解决了各个小问题之后(各个击破之后)合并小问题的解,从而得到整个问题的解 二,分治递归表达式 分治算法一般都可以写出一个递归表达式:比如经典的归并排序的递归表达式:T(N)=2T(N/2)+O(N) T(N)代表整个原问题,采用了分治解决方案后,它可以表示成: ①分解成了两个规模只有原来一半(N/2)的子问题:T(N/2) ②当解决完这两个子问题T(N/2)之后,再合并这两个子问题需要的代价是 O(N) 递

PHP V5 的五种常用设计模式

设计模式 一书将设计模式引入软件社区,该书的作者是 Erich Gamma.Richard Helm.Ralph Johnson 和 John Vlissides Design(俗称 "四人帮").所介绍的设计模式背后的核心概念非常简单.经过多年的软件开发实践,Gamma 等人发现了某些具有固定设计的模式,就像建筑师设计房子和建筑物一样,可以为浴室的位置或厨房的构造方式开发模板.使用这些模板或者说设计模式 意味着可以更快地设计更好的建筑物.同样的概念也适用于软件. 设计模式不仅代表着更

五种常用的图片格式及其是否有数据压缩的总结

五种常用的图片格式及其是否有数据压缩的总结 声明:引用请注明出处http://blog.csdn.net/lg1259156776/ 说明:本文主要介绍五种最常见和最常用的图像格式:BMP,PNG,JPEG,JPEG200,以及GIF.在进行图像处理相关应用之前第一步首先是能够读取这些图像文件,虽然很多开发工具支持库比如OpenCV等已经帮助节省了这些工作的麻烦,便利的同时也使得开发人员不再熟悉这些基本的图像格式.本文的作用就在于将这五种常用的图像格式进行分条叙述,方便查阅. 内容借鉴主流图片格

java线程池和五种常用线程池的策略使用与解析

java线程池和五种常用线程池策略使用与解析 一.线程池 关于为什么要使用线程池久不赘述了,首先看一下java中作为线程池Executor底层实现类的ThredPoolExecutor的构造函数 public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory th

最短路径算法之二——Dijkstra算法

Dijkstra算法 Dijkstra算法主要特点是以起始点为中心向外层层扩展,直到扩展到终点为止. 注意该算法要求图中不存在负权边. 首先我们来定义一个二维数组Edge[MAXN][MAXN]来存储图的信息. 这个图的Edge数组初始化以后为 我们还需要用一个一维数组dis来存储1号顶点到其余各个顶点的初始路程,如下. 这个dis数组中存的是最短路的估计值. 通过Dijkstra算法来松弛后,dis存的为从初始点到各点的精确值(最短路径)了. Dijkstra算法实现如下(以HDU1548为例

java线程池与五种常用线程池策略使用与解析

背景:面试中会要求对5中线程池作分析.所以要熟知线程池的运行细节,如CachedThreadPool会引发oom吗? java线程池与五种常用线程池策略使用与解析 可选择的阻塞队列BlockingQueue详解 首先看一下新任务进入时线程池的执行策略: 如果运行的线程少于corePoolSize,则 Executor始终首选添加新的线程,而不进行排队.(如果当前运行的线程小于corePoolSize,则任务根本不会存入queue中,而是直接运行) 如果运行的线程大于等于 corePoolSize

五种常用的C/C++编译器对64位整型的支持

变量定义 输出方式 gcc(mingw32) g++(mingw32) gcc(linux i386) g++(linux i386) MicrosoftVisual C++ 6.0 long long "%lld" 错误 错误 正确 正确 无法编译 long long "%I64d" 正确 正确 错误 错误 无法编译 __int64 "%lld" 错误 错误 无法编译 无法编译 错误 __int64 "%I64d" 正确 正

《程序设计与算法(二)算法基础》《第五周 分治》求排列的逆序数 11

011:求排列的逆序数 查看 提交 统计 提问 总时间限制:  1000ms 内存限制:  65536kB 描述 在Internet上的搜索引擎经常需要对信息进行比较,比如可以通过某个人对一些事物的排名来估计他(或她)对各种不同信息的兴趣,从而实现个性化的服务. 对于不同的排名结果可以用逆序来评价它们之间的差异.考虑1,2,…,n的排列i1,i2,…,in,如果其中存在j,k,满足 j < k 且 ij > ik, 那么就称(ij,ik)是这个排列的一个逆序. 一个排列含有逆序的个数称为这个排

两种常用的C语言排序算法

1. 要求输入10个整数,从大到小排序输出 输入:2 0 3 -4 8 9 5 1 7 6 输出:9 8 7 6 5 3 2 1 0 -4 解决方法:选择排序法 实现代码如下: #include <stdio.h> int main(int argc, const char * argv[]) { int num[10],i,j,k,l,temp; //用一个数组保存输入的数据 for(i=0;i<=9;i++) { scanf("%d",&num[i]);