算法小白之路-动态规划

291 阅读3分钟

前提

在平时面试提出的算法题中,动态规划是我们相对来说比较常见的。下面就跟着小编一起来快速上手动态规划类算法题,斩下面试的第一刀。

定义

首先看下动态规划在维基百科中的定义:

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)) // 补充下我们刚刚填补出来的

因此可以得到逻辑如下:

  1. 先进行下 p(i, i) 的初始化,

  2. 然后再进行遍历

  3. 当遍历到 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)

rainwatertrap.png

解题

在做这道题的时候,最开始的一个想法是,我直接按照行的方式去做,直接去算每一行能储存的水量,然后再进行相加,这样计算的复杂度为 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、遍历;(你也可以递归,不过感觉遍历要好一些,避免堆栈太深)

结尾

以上就是关于动态规划的讲解啦,从这些题可以看到,动态规划本身的实现上其实并不是很难,难的点还是在于你能不能判断出来这是一个动态规划的问题,动态规划可解。根据小编整理的思路,你可以自己找几道题小试下牛刀啦。

感谢观看。