前提
在平时面试提出的算法题中,动态规划是我们相对来说比较常见的。下面就跟着小编一起来快速上手动态规划类算法题,斩下面试的第一刀。
定义
首先看下动态规划在维基百科中的定义:
dynamic programming is a method for solving a complex problem by breaking it down into a collection of simpler subproblems.
就是将一个复杂的问题简化为一系列简单的小问题。 那么,满足可以通过动态规划处理的问题,一般需要什么条件呢?
特点
核心思想:拆分子问题,记住过往,减少重复计算。
分析
我们可以通过leetcode 上的题目来进行分析(leetcode 上原题),看三道题 入门版,进阶版,终极版,从而彻底掌握动态规划的解法。
入门版
题目
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
解题
假设当前你想要到达 m 阶的楼梯,那么你的上一步所处的位置(根据题意),只能是从 (m-1) 或者 (m-2) 阶梯,如果我们用函数方法 f(x) 表示达到x所有的方法,那么可以得到这个关系
f(m) = f(m-1) + f(m-2) ,考虑到边界的情况,需要再增加一个情况 f(1) = 1 ; f(2) = 2;
所以得到以下关系:
f(1) = 1;
f(2) = 2;
f(m) = f(m-1) + f(m-2);
因此可以很简单的得到代码如下:
// dp[n-2] + dp[n-1]
int[] dp = new int[n];
int pre = 0;
int ppre = 0;
int max = 0;
for (int i = 0; i < n; i++) {
if (i == 1) {
pre = 1;
ppre = 0;
} else if (i == 2) {
pre = 1;
ppre = 1;
}
max = pre + ppre;
ppre = pre;
pre = max;
}
return max;
进阶版
题目
上面的例子比较简单,现在用一道相对没那么明显题目,同样也是leetcode 上的原题:
给你一个字符串 s,找到 s 中最长的回文子串。如果字符串的反序与原始字符串相同,则该字符串称为回文字符串。如果是s = "babad", 那么最长的回文子串是 "bab"。
解题
我们用方法 P(i,j) 来表示从字符串的下标 i 到字符串的下标 j 组成的字符串是否为回文,1表示为回文,0表示不是。
那么就可以得到
p(i, j) = p(i + 1, j -1) && (S(i) == S(j))
然后针对一些边界情况进行下处理。
p(i, i) = 1 // 单个字符串,肯定是符合回文的标准的
p(i, i + 1) = (S(i) == S(i+1))// 如果是两个相同的字符串那么就是回文
p(i, j) = p(i + 1, j -1) && (S(i) == S(j)) // 补充下我们刚刚填补出来的
因此可以得到逻辑如下:
-
先进行下 p(i, i) 的初始化,
-
然后再进行遍历
-
当遍历到 i, i+1 的时候就可以得到值,然后一步步往外推
public String longestPalindrome(String s) {
/**
* 递归方程如下:dp[i,j] 表示 i 到 j 直接的最大回文字串
* if (dp[i+1, j-1] != false) 那么 如果 s[i] == s[j] : dp[i,j] = s[i] + dp[i+1, j-1] + s[j];如果 s[i] != s[j] , 那么 dp[i,j] = false;
* else dp[i,j] = false
*/
int length = s.length();
// 如果为空的时候的特殊处理
if (length == 0) {
return "";
}
Integer[][] dp = new Integer[length][length];
int maxLength = 1;
int maxLeft = 0;
for (int i = 0; i < length; i ++) {
dp[i][i] = 1;
}
for (int len = 2; len <= length; len ++) {
for (int i = 0; i < length; i ++) {
int j = i + len - 1;
if (j >= length) {
break;
}
if (i == j) {
dp[i][j] = 1;
} else if (s.charAt(i) == s.charAt(j)) {
if (j - i == 1) {
// 就相邻的话
dp[i][j] = 2;
} else {
dp[i][j] = dp[i+1][j-1] > 0 ? dp[i+1][j-1] + 2 : 0;
}
} else {
// 如果不相等的话,不是回文
dp[i][j] = 0;
}
// 如果这里的回文大于最大的,那么
if ((dp[i][j]) > 0 && (maxLength < (j - i + 1))) {
maxLeft = i;
maxLength = j - i + 1;
}
}
}
return s.substring(maxLeft, maxLength + maxLeft);
}
终极版
leetcode 的经典动态规划题目: 接雨水
题目
给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。可以看下图(图片来自于leetcode)
解题
在做这道题的时候,最开始的一个想法是,我直接按照行的方式去做,直接去算每一行能储存的水量,然后再进行相加,这样计算的复杂度为 O(m*n),但是这个在leetcode 上不能AC,会超时。所以只能重新思考一个。
如果从每一行来算不行的话,可以计算下每一列。
针对每一列可以存储的水的数量,可以先得到该列左边最高的列x,以及该列右边最高的列y,那么该列能存储的水就是 |x - y| * 1,依次遍历即可。并且如果下一列仍然上述x,y所在的列的中间,只需要更新下左边列的最高是否需要变化,不需要的话,那就可以直接使用,这样就可以用到之前计算过的结果了。
简化来说,针对 第 m列,找到左边最高的列的高是x,下标为i(即h(i) = x),右边最高的列的高是y,下标是j (h(j) = y),那么
S(m) = Math.min(h(i), h(j)] - h(m)
另外,边界情况的考虑
m = 0 或者 m = length -1 的时候, S(m) = 0; h(m) = ori[m] // ori数组为原来传入的数组,ori[m] 表示下标为m的位置有几个柱子
所以综上可以得到代码如下:
逻辑如下:
1.先从左到右遍历一次,得到每一列中包含本身的左边最大列的下标;
2.再从右向左遍历一次,得到每一列中包含本身的右边最大列的下标;
3.然后从头开始遍历进行累加,利用上面的公式进行计算;
public int solution(int[] height) {
int n = height.length;
if (n == 0) {
return 0;
}
int[] leftMax = new int[n];
leftMax[0] = height[0];
for (int i = 1; i < n; ++i) {
leftMax[i] = Math.max(leftMax[i - 1], height[i]);
}
int[] rightMax = new int[n];
rightMax[n - 1] = height[n - 1];
for (int i = n - 2; i >= 0; --i) {
rightMax[i] = Math.max(rightMax[i + 1], height[i]);
}
int ans = 0;
for (int i = 0; i < n; ++i) {
ans += Math.min(leftMax[i], rightMax[i]) - height[i];
}
return ans;
}
总结
分析套路
从上面三道题的结论来看,一般要让你去使用动态规划的题,不会都像入门题那样明显,需要去分析,看是否满足条件。
1、你能很容易的想到穷举可以解决;
2、能根据当前某个状态,判断出得到该状态下的值所依赖的前几个值,必须是前几个值。即我可以用遍历的方式从当前一遍遍的递推下去;
3、有一个边界,这个边界是我们递推下去的起始位置,不然就没完没了了;
4、可以写出一个状态转移方程。
写代码的套路:
1、先得到状态转移方程;
2、初始化;
3、遍历;(你也可以递归,不过感觉遍历要好一些,避免堆栈太深)
结尾
以上就是关于动态规划的讲解啦,从这些题可以看到,动态规划本身的实现上其实并不是很难,难的点还是在于你能不能判断出来这是一个动态规划的问题,动态规划可解。根据小编整理的思路,你可以自己找几道题小试下牛刀啦。
感谢观看。