本文主要目的为向初学者介绍动态规划的思想。
什么是动态规划?
答:动态规划(dynamic programming)是运筹学的一个分支,是求解决策过程(decision process)最优化的数学方法。(来源于百度百科)
动态规划的基本思想是什么?
答:动态规划算法通常用于求解具有某种最优性质的问题。在这类问题中,可能会有许多可行解。每一个解都对应于一个值,我们希望找到具有最优值的解。
如果我们能够保存已解决的子问题的答案,而在需要时再找出已求得的答案,这样就可以避免大量的重复计算,节省时间。我们可以用一个表来记录所有已解的子问题的答案。不管该子问题以后是否被用到,只要它被计算过,就将其结果填入表中。这就是动态规划法的基本思路。(来源于百度百科)
看完了动态规划的基本思想,你会发现自己还是不会,接下来是一道例题:
下题为leetcode第279题:完全平方数
给定正整数 n,找到若干个完全平方数(比如 1, 4, 9, 16, …)使得它们的和等于 n。你需要让组成和的完全平方数的个数最少。
示例 1:
输入: n = 12
输出: 3
解释: 12 = 4 + 4 + 4.
示例 2:
输入: n = 13
输出: 2
解释: 13 = 4 + 9.
这道题的解法有很多,我们先试着用普通递归的思路解一下: 假设f(n)为结果
易知f(1)=1;f(2)=2;f(3)=3;
当n=某一个数的平方时,易知f(n)=1;
当n=6时,f(6)的情况有2种——分别是f(1)+f(6-1)和f(4)+f(6-4),即f(1)+f(5)和f(4)+f(2)。那么f(6)的值便是这两者的最小值,即min( f(1)+f(5) , f(4)+f(2) );
情况1:f(1)的值=1,那f(5)的值等于多少呢?我们可以继续往下推:f(5)的情况有2种:f(1)+f(4)和f(4)+f(1);
情况1.1:f(1)=1,f(4)=1;那么f(5)=2;
情况1.2:f(5)=2;
最终f(5)的值为情况1.1和1.2中的最小值,即f(5)=2;
那么情况1的结构便是f(6)=3;
同理可计算出情况2的结果
…(此处省略)
接下来我们开始写代码:
class Solution {
public int numSquares(int n) {
if(n==1 || n==2 || n==3) return n;
int temp = (int) Math.sqrt(n);
if(temp*temp == n) return 1;
int min = Integer.MAX_VALUE;
for(int i=1;i*i <= n;i++){
min = Math.min(min, numSquares(n-i*i)+1);
}
return min;
}
}
这个代码是正确的,只不过时间复杂度太高(指数级别)。我们在leetcode上提交一下发现超时:
那应该如何改进我们的代码呢?
在普通递归中我们会发现,有很多值都被重复计算过很多次,我们可以使用记忆化搜索来改善上面的普通递归,使得这些值只被计算一次就被保存下来,大大降低时间复杂度。代码如下:
class Solution {
static int[] arr;
public int numSquares(int n) {
arr = new int[n+1];//创建一个数组以保存计算出来的f(n)
for(int i=0;i<n+1;i++)//初值为-1
arr[i] = -1;
return f(n);
}
public static int f(int n){
if(arr[n] != -1) return arr[n];//当arr[n]已经被赋过值,便返回arr[n]。
if(n==1 || n==2 || n==3) return n;
int temp = (int) Math.sqrt(n);
if(temp*temp == n) return 1;
int min = Integer.MAX_VALUE;
for(int i=1;i*i <= n;i++){
min = Math.min(min, f(n-i*i)+1);
}
arr[n] = min;
return arr[n];
}
}
接下来,我们在leetcode上提交一下。
成功通过,这个记忆化搜索已经是很好的方法了,那还有没有更简洁、更快的方法呢?答案是有,那便是动态规划。
上面两种递归方法都是通过“自顶向下”的思维来解决问题,而动态规划恰恰相反,它是通过“自底向上”来解决的,认清这一点很重要。 代码如下:
class Solution {
public int numSquares(int n) {
int[] arr = new int[n+1];//依然是设置数组来保存计算出的值
arr[1] = 1; //f(1)当然是1了
for(int i=2;i<n+1;i++){//自底向上,依次计算出f(n)的值,并保存在arr[n]中
int min = Integer.MAX_VALUE;
for(int j=1;j*j <= i;j++)
min = Math.min( min,arr[i-j*j]);
arr[i] = min + 1;
}
return arr[n];
}
}
试着提交一下,果然比记忆化搜索更快:
看完例题后,请再返回最顶部阅读一下动态规划的基本思想,相信会加深对动态规划的理解。