动态规划(Dynamic Programming,DP)是运筹学的一个分支,是求解决策过程最优化的过程。20世纪50年代初,美国数学家贝尔曼(R.Bellman)等人在研究多阶段决策过程的优化问题时,提出了著名的最优化原理,从而创立了动态规划。算法
咱们把要解决的一个大问题转换成若干个规模较小的同类型问题,当咱们求解出这些小问题的答案,大问题便不攻自破。这就是动态规划。框架
看一个很经典的介绍 DP 的问题:
“How should i explain Dynamic Programming to a 4-year-old?“
writes down "1+1+1+1+1+1+1+1 =" on a sheet of paper
"What's that equal to?"
counting "Eight!"
writes down another "1+" on the left
"What about that?"
quickly "Nine!"
"How'd you know it was nine so fast?"
"You just added one more"
"So you didn't need to recount because you remembered there were eight! Dynamic Programming is just a fancy way to say 'remembering stuff to save time later'"函数
这个估计你们都能看懂,就不解释了。动态规划其实就是把要解决的一个大问题转换成若干个规模较小的同类型问题。那这里的关键在于小问题的答案,能够进行重复使用,好比经典的爬楼梯问题。优化
这种思想的本质是:一个规模较大的问题(能够用两三个参数表示),经过若干规模较小的问题的结果来获得的(一般会寻求到一些特殊的计算逻辑,如求最值等)ui
咱们通常看到的状态转移方程,基本相似下面的公式(注:i、j、k 都是在定义DP方程中用到的参数。opt 指代特殊的计算逻辑,大多数状况下为 max 或 min。func 指代逻辑函数):3d
动态规划是一个求最值的过程,既然是要求最值,核心问题是什么呢?求解动态规划的核心问题是穷举。由于要求最值,确定要把全部可行的答案穷举出来,而后在其中找最值。code
首先,动态规划的穷举有点特别,由于这类问题存在“重叠子问题”,若是暴力穷举的话效率会极其低下,因此须要“备忘录”或者“DP table”来优化穷举过程,避免没必要要的计算。
并且,动态规划问题必定会具有“最优子结构”,才能经过子问题的最值获得原问题的最值。
最后,虽然动态规划的核心思想就是穷举求最值,可是问题能够变幻无穷,穷举全部可行解其实并非一件容易的事,只有列出正确的“状态转移方程”才能正确地穷举。blog
总体框架递归
斐波那契数列不算动态规划,可是解决问题的思路与动态规划很像,再加上你们上学的时候基本都接触过斐波那契数列,经过它来理解动态规划就很不错了。rem
斐波那契数列的数学形式就是递归的,写成代码就是这样:
int fib(int N) { if (N == 1 || N == 2) return 1; return fib(N - 1) + fib(N - 2); }
这个递归,相信有很多人能看出问题,子问题被不断计算,以N=20为例
fib(20) = fib(19) + fib(18) = fib(18) + fib(17) + fib(18)
写到这里,已经发现,fib(18)已经被计算屡次,效率很低下。
因此引入带备忘录的递归算法,把每次计算的子结果的值进行存储,后面就不须要重复计算了。整改以后的代码
int fib(int N) { int[] dp = int int[N]; // 最小子问题 dp[1] = dp[2] = 1; for (int i = 3; i <= N; i++) { // 状态转移方程 dp[i] = dp[i - 1] + dp[i - 2]; } return dp[N]; }
问题:给定一个字符串 s,找到 s 中最长的回文子串。你能够假设 s 的最大长度为 1000。
输入: "babad"
输出: "bab"
注意: "aba" 也是一个有效答案。
思路:对于一个子串而言,若是它是回文串,而且长度大于2,那么将它首尾的两个字母去除以后,它仍然是个回文串。例如对于字符串“ababa”,若是咱们已经知道“bab” 是回文串,那么“ababa” 必定是回文串,这是由于它的首尾两个字母都是“a”。
因而获得咱们的状态转移方程:
dp[i][j] 表示i到j之间的字符串是不是回文串
dp[i][j] = dp[i+1][j-1] and (s[i] eq s[j])
最小子问题:当s[i] eq s[j],子串长度是2或3,不须要检查子串是否回文串,即j-i<=2
public String longestPalindrome(String s) { if (s == null || s.length() <= 1) { return s; } int len = s.length(); int maxLen = 1; int left = 0; int right = 0; boolean[][] dp = new boolean[len][len]; char[] chars = s.toCharArray(); // 若是i从0开始,那么对应abba这样的字符串,bb这个子串在遍历过程当中无法被当作子问题进行存储 for (int i=len-2; i>=0; i--) { for (int j=i+1; j<len; j++) { if (chars[i] == chars[j]) { if (j-i <= 2) { // 最小字问题 if (j-i+1 > maxLen) { maxLen = j-i+1; left = i; right = j; } dp[i][j] = true; }else if (dp[i+1][j-1]) { // 子问题 if (j-i+1 > maxLen) { maxLen = j-i+1; left = i; right = j; } dp[i][j] = true; } } } } return s.substring(left, right + 1); }