概念
动态规划(dynamic programming)是运筹学的一个分支,是求解决策过程(decision process)最优化的数学方法。20世纪50年代初美国数学家R.E.Bellman等人在研究多阶段决策过程(multistep decision process)的优化问题时,提出了著名的最优化原理(principle of optimality),把多阶段过程转化为一系列单阶段问题,利用各阶段之间的关系,逐个求解,创立了解决这类过程优化问题的新方法——动态规划.简单的说就是通过分阶段求解问题的结果,每一阶段的结果都依赖于上一阶段的结果。通常我们把每个阶段的结果都保存在一个数组中(或者表)。
举个栗子
给定一个数字,求该位置上的斐波那契数是多少?
我们都知道斐波那契数列的规律是指定位置上的数等于这个数的前两项之和,即。把这个公式翻译成java代码通过递归的表达方式可以表示为
public int fnc(int n){//递归表达
if(n<=1)return 1;
return fnc(n-1)+fnc(n-2);
}
通过动态规划的表达方式可以表示为
public int fnc(int n){//动态规划表示
if(n<=1)return 1;
int res = 1;
int last = 1;
int lastlast = 1;
for(int i = 2; i <= n; i++){
res = last + lastlast;
lastlast = last;
last = res;
}
return res;
}
每次循环都会将正确的结果更新到上一项和上上一项,所以最后一项的结果也会是正确的。
上面两种计算方法都会得到最后的结果,但是第二种在耗时上明显优于第一种,因为递归求解时我们需要独立计算
和
,而计算
时我们又需要计算
和
,这样一来
就重复计算了,这就是递归耗时更多的原因。通常这种情况下我们通过动态规划为每一项计算正确的值并记录下来,通过线性的时间的到最终的结果。
第二个栗子
给定一个非空字符串 s 和一个包含非空单词列表的字典 wordDict,判定 s 是否可以被空格拆分为一个或多个在字典中出现的单词。
说明:
拆分时可以重复使用字典中的单词。
你可以假设字典中没有重复的单词。
示例 1:
输入: s = "leetcode", wordDict = ["leet", "code"]
输出: true
解释: 返回 true 因为 "leetcode" 可以被拆分成 "leet code"。
示例 2:
输入: s = "applepenapple", wordDict = ["apple", "pen"]
输出: true
解释: 返回 true 因为 "applepenapple" 可以被拆分成 "apple pen apple"。
注意你可以重复使用字典中的单词。
示例 3:
输入: s = "catsandog", wordDict = ["cats", "dog", "sand", "and", "cat"]
输出: false
分析上题,假设字符串s能被拆分,那么从字符串末位到前面的某个位置构成的单词一定在字典中(例如leetcode的code存在于字典中),同理删除这个词字后依然能用前面的假设来证明从删除后剩余的字符串末位到前面的某个位置构成的单词也一定在字典中(leet也能在字典中找到)。如此反复逆向推断,如果所有假设都成立(两次推断都成立),说明字符串s的确能被拆分(返回结果true),而其中如果有一个假设不成立(示例3),那么字符串s一定不能被分割。
基于上面的这种思想,每次判断的结果都依赖上一次判断的结果,这种问题的解答过程叫动态规划。所以有
public static boolean wordBreak(String s, List<String> wordDict) {
boolean[] f = new boolean[s.length() + 1];
f[0] = true;
for(int i=1; i <= s.length(); i++){
for(int j=0; j < i; j++){
if(f[j] && wordDict.contains(s.substring(j, i))){
f[i] = true;
break;
}
}
}
return f[s.length()];
}
f是我们构建的一个表,他记录着字符串每个位置能否在基于前一次拆分的结果上本次拆分的结果。默认是false,表示不能被拆分。最后返回的是最后一个字符位置上的结果,他依赖于上一次的结果,就像分析说的样。