码迷,mamicode.com
首页 > 其他好文 > 详细

数据结构——动态规划

时间:2017-09-05 10:00:48      阅读:122      评论:0      收藏:0      [点我收藏+]

标签:题目   splay   递归   cli   img   ...   class   pre   没有   

前言

“动态规划”在大一时,就知道了这个词,当时觉得好难好高大上,从此心生畏惧,闻词色变,心理阴影一直留存到现在。

在校招时,也多次被问到动态规划相关的题目。

本篇从一道经典动态规划题目说起,慢慢展开。

从题目讲起

【换钱的方法数】

给定数组 arr,arr 中所有的值都为正数且不重复。每个值代表一种面值的货币,每种面值的货币都可以使用任意张,再给定一个整数 aim 代表要找的钱数,求换钱有多少种方法。

【举例】

arr = [5, 10, 25, 1],aim = 15

则组成 15 元的方法共有 6 种,即 1 张 10 元和 1 张 5元、1 张 10 元 5 张 1 元、3 张 5 元、2张 5 元和 5 张 1 元、1 张 5 元和 10 张 1 元、 15 张 1 元。

【解析】

这道题目足够的经典,经典到校招时面试官面我第一个问题就是这个 = =。

之所以经典,是因为这道题通过暴力递归、记忆搜索和动态规划这 3 种方法均可以解决,同时这 3 种方法也是趋向最优解的过程。

通常情况下,可以通过暴力递归解决的问题都可以通过,暴力递归 -> 记忆搜索 -> 动态规划,这样的优化轨迹来进行优化。

从暴力递归到动态规划

暴力递归方法:

如果 arr[5, 10, 25, 1],aim = 100,分析如下:

  • 取 0 张 5 元,让剩下的 arr[1...N-1](即 10、25、1 元)种货币,组成 100 元,求其方法数,结果记为 r1;
  • 取 1 张 5 元,让剩下的 arr[1...N-1] 种货币,组成 100-5 元,求其方法数,结果记为 r2;
  • 取 2 张 5 元,让剩下的 arr[1...N-1] 种货币,组成 90 元,求其方法数,结果记为 r3;
  • ...
  • 取 20 张 5 元,让剩下的 arr[1...N-1] 中货币,组成 0 元,求其方法数,结果记为 r21;

那么 r0 + r1 + r2 + r3 + ... + r21 即是总的方法数。

在上面的分析中,“让剩下的 arr[..],组成。。。,求其方法数”,这句话其实就表示一个递归的过程。

可定义递归方法 solve(int index, int[] arr, int aim),其中 index 表示用 arr[index...N-1] 种货币,组成 aim 元的方法数。源码如下:

技术分享
 1     private int process(int[] arr, int aim) {
 2         if (arr == null || arr.length == 0 || aim < 0)
 3             return 0;
 4         return solve(0, arr, aim);
 5     }
 6 
 7     private int solve(int index, int[] arr, int aim) {
 8         int res = 0;
 9         if (index == arr.length) {
10             res = aim == 0 ? 1 : 0;
11         } else {
12             for (int i = 0; arr[index] * i <= aim; i++) {
13                 res += solve(index + 1, arr, aim - arr[index] * i);
14             }
15         }
16         return res;
17     }
暴力递归方法

在暴力递归的过程中,其中有很多都是不必要的重复计算,比如当计算过 0 张 5 元加 1 张 10 元后,需要进行 arr[25, 1],aim = 90 的递归,同样的,当计算过 2 张 5 元加 0 张 10 元后,需要进行的也是 arr[25, 1],aim = 90 的递归,显然这两次的递归方法入参是相同的,是完全不必要的计算。

如何减少不必要的计算?因为重复调用的递归方法入参是相同的(因为 arr 为全局变量,所以可以忽略,即只考虑 index 和 aim),所以将第一次调用的返回值保存起来,当下次调用递归方法时,首先判断该入参所对应的结果是否已经保存,如果存在对应的结果,则直接获取,不再进行递归计算。

记忆搜索方法:

为了保存入参对应的结果,这里通过二维数组来保存,即:dp[index][aim]。因为 dp[][] 为整数二维数组,所以其初始值为 0,这里我们就要区分是否进行过递归计算,如果入参对应的值在 dp 中没有保存,则进行递归计算,如果计算的结果为 0,则保存至 dp 中的值为 -1,以此来区分没有进行过递归计算和计算结果为0这两种情况。源码如下:

技术分享
 1     private int process(int[] arr, int aim) {
 2         if (arr == null || arr.length == 0 || aim < 0)
 3             return 0;
 4         int[][] dp = new int[arr.length + 1][aim + 1];
 5         return solve(0, arr, aim, dp);
 6     }
 7 
 8     private int solve(int index, int[] arr, int aim, int[][] dp) {
 9         int res = 0;
10         if (index == arr.length) {
11             res = aim == 0 ? 1 : 0;
12         } else {
13             int dpV;
14             for (int i = 0; arr[index] * i <= aim; i++) {
15                 dpV = dp[index + 1][aim - arr[index] * i];
16                 if (dpV != 0) {
17                     res += dpV == -1 ? 0 : dpV;
18                 } else {
19                     res += solve(index + 1, arr, aim - arr[index] * i, dp);
20                 }
21             }
22         }
23         dp[index][aim] = res == 0 ? -1 : res;
24         return res;
25     }
记忆搜索方法

其实记忆搜索方法可以说是一种特别的动态规划方法,下面看经典的动态规划解决方法。

动态规划方法:

如果 arr 长度为 N,则生成行数为 N,列数为 aim + 1 的矩阵 dp。

 

数据结构——动态规划

标签:题目   splay   递归   cli   img   ...   class   pre   没有   

原文地址:http://www.cnblogs.com/zhengbin/p/7476713.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!