算法介绍:动态规划(Dynamic Programming, DP)在查找有很多重叠子问题的情况的最优解时有效。它将问题重新组合成子问题。为了避免多次解决这些子问题,它们的结果都逐渐被计算并被保存,从简单的问题直到整个问题都被解决。因此,动态规划保存递归时的结果,因而不会在解决同样的问题时花费时间。
补充:

  • 动态规划只能应用于有最优子结构的问题。最优子结构的意思是局部最优解能决定全局最优解(对有些问题这个要求并不能完全满足,故有时需要引入一定的近似)。简单地说,问题能够分解成子问题来解决。
  • 动态规划和其它遍历算法(如深/广度优先搜索)都是将原问题拆成多个子问题然后求解,他们之间最本质的区别是,动态规划保存子问题的解,避免重复计算。解决动态规划问题的关键是找到状态转移方程,这样我们可以通过计算和储存子问题的解来求解最终问题。
  • 可以对动态规划进行空间压缩,起到节省空间消耗的效果。
  • 如果题目需求的是最终状态,那么使用动态搜索比较方便;如果题目需要输出所有的路径,那么使用带有状态记录的优先搜索会比较方便。

一、一维动态规划

  • 题目一:爬楼梯
    给定n节台阶,每次可以走一步或走两步,求一共有多少种方式可以走完这些台阶。

    解答:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    class Solution {
    public:
    int climbStairs(int n) {
    vector<int> dp(n + 1, 1);
    for (int i = 2; i <= n; ++i) {
    dp[i] = dp[i - 1] + dp[i - 2];
    }
    return dp[n];
    }
    };

    解答二(空间压缩):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    class Solution {
    public:
    int climbStairs(int n) {
    if (n == 1) return 1;
    int prevPrev = 1, prev = 1, cur = 0;
    for (int i = 2; i <= n; ++i) {
    cur = prevPrev + prev;
    prevPrev = prev;
    prev = cur;
    }
    return cur;
    }
    };

    补充:典型的斐波拉契数列问题,可转化为dp[n]=dp[n-1]+dp[n-2]。

  • 题目二:打家劫舍
    假如你是一个劫匪,并且决定抢劫一条街上的房子,每个房子内的钱财数量各不相同。如果你抢了两栋相邻的房子,则会触发警报机关。求在不触发机关的情况下最多可以抢劫多少钱。

    解答:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    class Solution {
    public:
    int rob(vector<int>& nums) {
    int prevPrev = 0, prev = 0, cur = 0;
    for (int i = 0; i < nums.size(); ++i) {
    cur = max(prevPrev + nums[i], prev);
    prevPrev = prev;
    prev = cur;
    }
    return cur;
    }
    };

    提示:状态转移方程为dp[n]=max(dp[n-1], dp[n-2]+nums[n])。

  • 题目三:等差数列划分
    如果一个数列至少有三个元素,并且任意两个相邻元素之差相同,则称该数列为等差数列。例如,[1,3,5,7,9]、[7,7,7,7]和[3,-1,-5,-9]都是等差数列。
    给你一个整数数组nums,返回数组nums中所有为等差数组的子数组个数。

    解答:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    class Solution {
    public:
    int numberOfArithmeticSlices(vector<int>& nums) {
    int n = nums.size();
    int count = 0, k = 0;
    for (int i = 2; i < n; ++i) {
    if (nums[i] - nums[i - 1] == nums[i - 1] - nums[i - 2]) {
    ++k;
    count += k;
    } else k = 0;
    }
    return count;
    }
    };

    提示:状态转移方程为dp[n]=dp[n-1]+1。

二、二维动态规划

  • 题目一:最小路径和
    给定一个包含非负整数的mxn网格grid,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。
    示例图

    解答:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    class Solution {
    public:
    int minPathSum(vector<vector<int>>& grid) {
    int m = grid.size();
    int n = grid[0].size();
    if (m == 1 && n == 1) return grid[0][0];
    vector<vector<int>> dp(m, vector<int>(n, 0));
    dp[0][0] = grid[0][0];
    for (int i = 1; i < m; ++i) {
    dp[i][0] = dp[i - 1][0] + grid[i][0];
    }
    for (int j = 1; j < n; ++j) {
    dp[0][j] = dp[0][j - 1] + grid[0][j];
    }
    for (int i = 1; i < m; ++i) {
    for (int j = 1; j < n; ++j) {
    int minTrace = min(dp[i - 1][j], dp[i][j - 1]);
    dp[i][j] = grid[i][j] + minTrace;
    }
    }
    return dp[m - 1][n - 1];
    }
    };

    提示:状态转移方程为dp[i][j]=grid[i][j]+min(dp[i-1][j], dp[i][j-1])

  • 题目二:01矩阵
    给定一个由0和1组成的矩阵mat,请输出一个大小相同的矩阵,其中每一个格子是mat中对应位置元素到最近的0的距离。
    示例图

    解答:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    class Solution {
    public:
    vector<vector<int>> updateMatrix(vector<vector<int>> &matrix) {
    int m = matrix.size(), n = matrix[0].size();
    vector<vector<int>> dp(m, vector<int>(n, numeric_limits<int>::max() - 1));
    for (int i = 0; i < m; ++i) {
    for (int j = 0; j < n; ++j) {
    if (matrix[i][j] != 0) {
    if (i > 0) dp[i][j] = min(dp[i][j], dp[i - 1][j] + 1);
    if (j > 0) dp[i][j] = min(dp[i][j], dp[i][j - 1] + 1);
    }
    else dp[i][j] = 0;
    }
    }
    for (int i = m - 1; i >= 0; --i) {
    for (int j = n - 1; j >= 0; --j) {
    if (matrix[i][j] != 0) {
    if (i < m - 1) dp[i][j] = min(dp[i][j], dp[i + 1][j] + 1);
    if (j < n - 1) dp[i][j] = min(dp[i][j], dp[i][j + 1] + 1);
    }
    }
    }
    return dp;
    }
    };

    提示:需要左上到右下,右下到左上各扫一遍。

  • 题目三:最大正方形
    在一个由’0’和’1’组成的二维矩阵内,找到只包含’1’的最大正方形,并返回其面积。
    示例图

    解答:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    class Solution {
    public:
    int maximalSquare(vector<vector<char>>& matrix) {
    int maxSide = 0;
    int m = matrix.size(), n = matrix[0].size();
    vector<vector<int>> dp(m + 1, vector<int>(n + 1, 0));
    for (int i = 1; i <= m; ++i) {
    for (int j = 1; j <= n; ++j) {
    if (matrix[i - 1][j - 1] == '1') {
    int minNum = min({dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]});
    dp[i][j] = minNum + 1;
    }
    maxSide = max(maxSide, dp[i][j]);
    }
    }
    return maxSide * maxSide;
    }
    };

    提示:在右下角记录正方形的最大边,状态转移方程为dp[i][j]=1+min({dp[i-1][j-1], dp[i-1][j], dp[i][j-1]})。

三、分割问题

  • 题目一:完全平方数
    给你一个整数n,返回和为n的完全平方数的最少数量。

    解答:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    class Solution {
    public:
    int numSquares(int n) {
    vector<int> dp(n + 1, 0);
    for (int i = 1; i <= n; ++i) {
    int minNum = 10001;
    for (int j = 1; j * j <= i; ++j) {
    minNum = min(minNum, dp[i - j * j]);
    }
    dp[i] = minNum + 1;
    }
    return dp[n];
    }
    };

    提示:位置只依赖于i-j^2,因为这样才符合完全平方数分割。

  • 题目二:解码方法
    已知字母 A-Z 可以表示成数字 1-26。给定一个数字串,求有多少种不同的字符串等价于这个数字串。

    解答:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    class Solution {
    public:
    int numDecodings(string s) {
    if (s[0] == '0') return 0;
    int n = s.length();
    if (n == 1) return 1;
    vector<int> dp(n, 1);
    if (s[0] == '1' || s[0] == '2' && s[1] <= '6') ++dp[1];
    if (s[1] == '0') dp[1]--;
    for (int i = 2; i < n; ++i) {
    if (s[i] == '0') {
    if (s[i - 1] > '2' || s[i - 1] == '0')
    return 0;
    else dp[i] = dp[i - 2];
    }
    else {
    if (s[i - 1] == '1' || s[i - 1] == '2' && s[i] <= '6')
    dp[i] = dp[i - 1] + dp[i - 2];
    else dp[i] = dp[i - 1];
    }
    }
    return dp[n - 1];
    }
    };

    提示:注意状态转移方程不止一种。

  • 题目三:单词拆分
    给你一个字符串s和一个字符串列表wordDict作为字典。如果可以利用字典中出现的一个或多个单词(可以重复使用)拼接出s则返回true。

    解答:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    class Solution {
    public:
    bool wordBreak(string s, vector<string>& wordDict) {
    int size = s.length();
    vector<bool> dp(size + 1, false);
    dp[0] = true;
    for (int i = 1; i <= size; ++i) {
    for (const string& word : wordDict) {
    int length = word.length();
    if (i >= length && s.substr(i - length, length) == word)
    dp[i] = dp[i - length];
    if (dp[i]) break;
    }
    }
    return dp[size];
    }
    };

    提示:和完全平方数差不多,位置只依赖于i-word.length。

  • 题目四:填充书架
    给定一个数组,每个元素代表一本书的厚度和高度。问对于一个固定宽度的书架,如果按照数组中书的顺序从左到右、从上到下摆放,最小总高度是多少。
    示例图

    解答:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    class Solution {
    public:
    int minHeightShelves(vector<vector<int>>& books, int shelfWidth) {
    int n = books.size();
    vector<int> dp(n + 1, 0);
    for (int i = 1; i <= n; ++i) {
    int w = books[i - 1][0], h = books[i - 1][1];
    dp[i] = dp[i - 1] + h;
    for (int j = i - 1; j > 0; --j) {
    int prevW = books[j - 1][0], prevH = books[j - 1][1];
    w += prevW;
    if (w > shelfWidth) break;
    h = max(prevH, h);
    dp[i] = min(dp[i], h + dp[j - 1]);
    }
    }
    return dp[n];
    }
    };

    提示:摆放只有两种情况,摆在前一本书后面或者直接摆在下一层。

  • 题目五:组合总和 Ⅳ
    给定一个不重复数字的数组和一个目标数,求加起来是目标数的所有排列的总数量。(虽然这道题叫做Combination Sum,但是不同顺序的组合会被当作不同答案,因此本质上是排列)

    解答:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    class Solution {
    public:
    int combinationSum4(vector<int>& nums, int target) {
    vector<int> dp(target + 1, 0);
    dp[0] = 1;
    for (int i = 1; i <= target; ++i) {
    for (int num : nums) {
    if (num <= i) {
    if (dp[i] > __INT_MAX__ - dp[i - num]) break;
    dp[i] += dp[i - num];
    }
    }
    }
    return dp[target];
    }
    };

    提示:和完全平方数也是差不多的,这里为了全用int来完成时间上会稍稍多一点,可以换成double类型去掉多的if判断。

四、子序列问题

  • 题目一:最长递增子序列
    给你一个整数数组nums,找到其中最长严格递增子序列的长度。

    解答:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    class Solution {
    public:
    int lengthOfLIS(vector<int>& nums) {
    int n = nums.size();
    vector<int> dp(n, 0);
    int res = 1;
    for (int i = 0; i < n; ++i) {
    int maxTemp = 0;
    for (int j = i; j >= 0; --j) {
    if (nums[i] > nums[j])
    maxTemp = max(maxTemp, dp[j]);
    }
    dp[i] = maxTemp + 1;
    res = max(dp[i], res);
    }
    return res;
    }
    };

    提示:这道题很简单,但是动态规划并不是最优解。

  • 题目二:最长公共子序列
    给定两个字符串text1和text2,返回这两个字符串的最长公共子序列的长度。如果不存在公共子序列,返回0。

    解答:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    class Solution {
    public:
    int longestCommonSubsequence(string text1, string text2) {
    int len1 = text1.length();
    int len2 = text2.length();
    vector<vector<int>> dp(len1 + 1, vector<int>(len2 + 1, 0));
    for (int i = 1; i <= len1; ++i) {
    for (int j = 1; j <= len2; ++j) {
    if (text1[i - 1] == text2[j - 1])
    dp[i][j] = dp[i - 1][j - 1] + 1;
    else
    dp[i][j] = max(dp[i][j - 1], dp[i - 1][j]);
    }
    }
    return dp[len1][len2];
    }
    };

    提示:使用二维dp来记录,dp[i][j]记录的是text1.length<=1,text2.length<=j时最大的公共子长度。

五、背包问题

  • 算法解释:背包问题(knapsack problem)是一种组合优化的NP完全问题:有n个物品和载重为w的背包,每个物品都有自己的重量weight和价值value,求拿哪些物品可以使得背包所装下物品的总价值最大。如果限定每种物品只能选择0个或1个,则问题称为0-1背包问题(0-1 knapsack);如果不限定每种物品的数量,则问题称为无界背包问题或完全背包问题(unbounded knapsack)。我们可以用动态规划来解决背包问题。以0-1背包问题为例。

  • 0-1背包问题:

    • 分析:我们可以定义一个二维数组dp存储最大价值,其中dp[i][j]表示前i件物品重量不超过j的情况下能达到的最大价值。在我们遍历到第i件物品时,在当前背包总载重为j的情况下,如果我们不将物品i放入背包,那么dp[i][j]=dp[i-1][j],即前i个物品的最大价值等于只取前i-1个物品时的最大价值;如果我们将物品i放入背包,假设第i件物品重量为weight,价值为value,那么我们得到dp[i][j]=dp[i-1][j-weight]+value。我们只需在遍历过程中对这两种情况取最大值即可,总时间复杂度和空间复杂度都为O(nw)。
    • 状态转移矩阵示例:
      示例
    • 代码:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      int knapsack(vector<int> weights, vector<int> values, int n, int w) {
      vector<vector<int>> dp(n + 1, vector<int>(w + 1, 0));
      for (int i = 1; i <= n; ++i) {
      int weight = weights[i - 1], value = values[i - 1];
      for (int j = 1; j <= w; ++j) {
      if (j >= weight) {
      dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight] + value);
      } else {
      dp[i][j] = dp[i - 1][j];
      }
      }
      }
      return dp[n][w];
      }
    • 代码2(空间复杂度为O(w)):
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      int knapsack(vector<int> weights, vector<int> values, int n, int w) {
      vector<int> dp(w + 1, 0);
      for (int i = 1; i <= n; ++i) {
      int weight = weights[i - 1], value = values[i - 1];
      for (int j = w; j >= weight; --j) {
      dp[j] = max(dp[j], dp[j - weight] + value);
      }
      }
      return dp[w];
      }
  • 无界背包问题

    • 分析:在完全背包问题中,一个物品可以拿多次。如图上半部分所示,假设我们遍历到物品i=2,且其重量为weight=2,价值为value=3;对于背包载重j=5,最多只能装下2个该物品。那么我们的状态转移方程就变成了dp[2][5]=max(dp[1][5], dp[1][3]+3, dp[1][1]+6)。如果采用这种方法,假设背包载重无穷大而物体的重量无穷小,我们这里的比较次数也会趋近于无穷大,远超O(nw)的时间复杂度。怎么解决这个问题呢?我们发现在dp[2][3]的时候我们其实已经考虑了dp[1][3]和dp[2][1]的情况,而在时dp[2][1]也已经考虑了dp[1][1]的情况。因此,如图下半部分所示,对于拿多个物品的情况,我们只需考虑dp[2][3]即可,即dp[2][5]=max(dp[1][5], dp[2][3]+3)。这样,我们就得到了完全背包问题的状态转移方程:dp[i][j]=max(dp[i-1][j], dp[i][j-w]+v),其与0-1背包问题的差别仅仅是把状态转移方程中的第二个i-1变成了i。
    • 状态转移矩阵示例:
      示例
    • 代码:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      int knapsack(vector<int> weights, vector<int> values, int n, int w) {
      vector<vector<int>> dp(n + 1, vector<int>(w + 1, 0));
      for (int i = 1; i <= n; ++i) {
      int weight = weights[i - 1], value = values[i - 1];
      for (int j = 1; j <= w; ++j) {
      if (j >= weight) {
      dp[i][j] = max(dp[i - 1][j], dp[i][j - weight] + value);
      } else {
      dp[i][j] = dp[i - 1][j];
      }
      }
      }
      return dp[n][w];
      }
    • 代码2(空间复杂度为O(w)):
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      int knapsack(vector<int> weights, vector<int> values, int n, int w) {
      vector<int> dp(w + 1, 0);
      for (int i = 1; i <= n; ++i) {
      int weight = weights[i - 1], value = values[i - 1];
      for (int j = weight; j <= w; ++j) {
      dp[j] = max(dp[j], dp[j - weight] + value);
      }
      }
      return dp[w];
      }
  • 题目一:分割等和子集
    给你一个只包含正整数的非空数组nums。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。

    解答:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    class Solution {
    public:
    bool canPartition(vector<int>& nums) {
    int sum = accumulate(nums.begin(), nums.end(), 0);
    if (sum % 2 == 1) return false;
    int n = nums.size();
    int target = sum / 2;
    vector<int> dp(target + 1, 0);
    for (int i = 1; i <= n; ++i) {
    int weight = nums[i - 1];
    for (int j = target; j >= weight; --j) {
    dp[j] = max(dp[j], dp[j - weight] + weight);
    }
    }
    return dp[target] == target;
    }
    };

    提示:注意这道题没有value且要知道背包的容量为sum/2。

  • 题目二:一和零
    给定m个数字0和n个数字1,以及一些由0-1构成的字符串,求利用这些数字最多可以构成多少个给定的字符串,字符串只可以构成一次。

    解答:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    class Solution {
    public:
    int findMaxForm(vector<string>& strs, int m, int n) {
    int len = strs.size();
    vector<int> weights0(len, 0);
    vector<int> weights1(len, 0);
    for (int i = 0; i < len; ++i) {
    for (char c : strs[i]) {
    if (c == '0') ++weights0[i];
    else ++weights1[i];
    }
    }
    vector<vector<int>> dp(m + 1, vector<int>(n + 1, 0));
    for (int i = 0; i < len; ++i) {
    for (int j = m; j >= weights0[i]; --j) {
    for (int k = n; k >= weights1[i]; --k) {
    dp[j][k] = max(dp[j][k], dp[j - weights0[i]][k - weights1[i]] + 1);
    }
    }
    }
    return dp[m][n];
    }
    };

    提示:本质上是一个双重背包。

  • 题目三:零钱交换
    给定一些硬币的面额,求最少可以用多少颗硬币组成给定的金额,硬币可以重复使用。

    解答:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    class Solution {
    public:
    int coinChange(vector<int>& coins, int amount) {
    vector<int> dp(amount + 1, 10001);
    dp[0] = 0;
    for (int i = 0; i < coins.size(); ++i) {
    for (int j = coins[i]; j <= amount; ++j) {
    dp[j] = min(dp[j], dp[j - coins[i]] + 1);
    }
    }
    return dp[amount] == 10001 ? -1 : dp[amount];
    }
    };

    提示:很典型的无限背包问题。

六、股票问题

  • 题目一:买卖股票的最佳时机
    给定一段时间内每天某只股票的固定价格,已知你只可以买卖各一次,求最大的收益。

    解答:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    class Solution {
    public:
    int maxProfit(vector<int>& prices) {
    int res = 0;
    int minSale = prices[0];
    for (int i = 1; i < prices.size(); ++i) {
    if (prices[i] < minSale) minSale = prices[i];
    else res = max(res, prices[i] - minSale);
    }
    return res;
    }
    };

    提示:简单题,使用贪心思想即可。

  • 题目二:买卖股票的最佳时机 IV
    给定一段时间内每天某只股票的固定价格,已知你只可以买卖各k次,且每次只能拥有一支股票,求最大的收益。

    解答:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    class Solution {
    public:
    int maxProfit(int k, vector<int>& prices) {
    vector<int> buy(k + 1, -1001), sell(k + 1, 0);
    for (int i = 0; i < prices.size(); ++i) {
    for (int j = 1; j <= k; ++j) {
    buy[j] = max(buy[j], sell[j - 1] - prices[i]);
    sell[j] = max(sell[j], buy[j] + prices[i]);
    }
    }
    return sell[k];
    }
    };

    提示:需要用两个数组来维护状态,思考一下状态转换的关系。

  • 题目三:买卖股票的最佳时机含冷冻期
    给定一段时间内每天某只股票的固定价格,已知每次卖出之后必须冷却一天,且每次只能拥有一支股票,求最大的收益。

    解答:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    class Solution {
    public:
    int maxProfit(vector<int> &prices) {
    int n = prices.size();
    vector<int> buy(n), sell(n), s1(n), s2(n);
    s1[0] = buy[0] = -prices[0];
    sell[0] = s2[0] = 0;
    for (int i = 1; i < n; ++i) {
    buy[i] = s2[i - 1] - prices[i];
    s1[i] = max(buy[i - 1], s1[i - 1]);
    sell[i] = max(buy[i - 1], s1[i - 1]) + prices[i];
    s2[i] = max(s2[i - 1], sell[i - 1]);
    }
    return max(sell[n - 1], s2[n - 1]);
    }
    };

    提示:考虑如下状态转换图,这道题还有二维dp的做法,二维dp的做法也可以被压缩成一维dp的做法。
    状态转换图

七、练习

  • 题目一:打家劫舍 II
    你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都围成一圈,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。

    解答:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    class Solution {
    public:
    int rob(vector<int>& nums) {
    int n = nums.size();
    int prev = 0, pevPrev = 0, cur1 = 0;
    for (int i = 0; i < n - 1; ++i) {
    cur1 = max(pevPrev + nums[i], prev);
    pevPrev = prev;
    prev = cur1;
    }
    prev = 0, pevPrev = 0;
    int cur2 = 0;
    for (int i = 1; i < n; ++i) {
    cur2 = max(pevPrev + nums[i], prev);
    pevPrev = prev;
    prev = cur2;
    }
    return max(cur1, cur2);
    }
    };

    提示:处理一下首尾,分开算一遍即可。

  • 题目二:最大子数组和
    给你一个整数数组nums,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。

    解答:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    class Solution {
    public int maxSubArray(int[] nums) {
    int res = nums[0];
    int pre = nums[0];
    for (int i = 1; i < nums.length; i ++) {
    pre = Math.max(pre + nums[i], nums[i]);
    res = Math.max(pre, res);
    }
    return res;
    }
    }

    提示:要么加入当前的,要么重新从当前的重新开始。

  • 题目三:整数拆分
    给定一个正整数n,将其拆分为k个正整数的和(k>=2),并使这些整数的乘积最大化。

    解答:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    class Solution {
    public:
    int integerBreak(int n) {
    if (n <= 3) return n - 1;
    vector<int> dp(n + 1, 1);
    for (int i = 1; i <= n; ++i) {
    dp[i] = i;
    }
    for (int i = 4; i <= n; ++i) {
    for (int j = 2; j < i; ++j) {
    dp[i] = max(dp[i], j * dp[i - j]);
    }
    }
    return dp[n];
    }
    };

    提示:分割类型的问题。

  • 题目四:两个字符串的删除操作
    给定两个单词word1和word2,返回使得word1和word2相同所需的最小步数。每步可以删除任意一个字符串中的一个字符。

    解答:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    class Solution {
    public:
    int minDistance(string word1, string word2) {
    int m = word1.length(), n = word2.length();
    vector<vector<int>> dp(m + 1, vector<int>(n + 1, 0));
    for (int i = 1; i <= m; ++i) {
    dp[i][0] = i;
    for (int j = 1; j <= n; ++j) {
    dp[0][j] = j;
    if (word1[i - 1] == word2[j - 1]) dp[i][j] = dp[i - 1][j - 1];
    else dp[i][j] = min(dp[i - 1][j], dp[i][j - 1]) + 1;
    }
    }
    return dp[m][n];
    }
    };

    提示:最长公共子序列的变种题,注意i和j为0时的赋值。

  • 题目五:正则表达式匹配
    给你一个字符串s和一个字符规律p,请你来实现一个支持’.’和’*’的正则表达式匹配。

    • ‘.’匹配任意单个字符。
    • ‘*’匹配零个或多个前面的那一个元素。

    解答:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    class Solution {
    public:
    bool isMatch(string s, string p) {
    int m = s.length(), n = p.length();
    vector<vector<bool>> dp(m + 1, vector<bool>(n + 1, false));
    dp[0][0] = true;
    for (int i = 0; i <= m; ++i) {
    for (int j = 1; j <= n; ++j) {
    if (p[j - 1] == '*') {
    dp[i][j] = dp[i][j] || dp[i][j - 2];
    if (i > 0 && (p[j - 2] == s[i - 1] || p[j - 2] == '.'))
    dp[i][j] = dp[i][j] || dp[i - 1][j];
    } else {
    if (i > 0 && (p[j - 1] == s[i - 1] || p[j - 1] == '.')) {
    dp[i][j] = dp[i][j] || dp[i - 1][j - 1];
    }
    }
    }
    }
    return dp[m][n];
    }
    };

    提示:非常麻烦的题,i=0是为了处理p的开头两个就有’*‘的情况,而当p[j]=’*‘时,本质上只会有两种情况:匹配s末尾的一个字符,将该字符扔掉,而该组合还可以继续进行匹配;不匹配字符,将该组合扔掉,不再进行匹配。如果仍然不太理解的话,可以考虑用for循环处理。

  • 题目六:目标和
    给你一个非负整数数组nums和一个整数target。向数组中的每个整数前添加’+’或’-‘,然后串联起所有整数,可以构造一个表达式(例如’+2-1’)。
    返回可以通过上述方法构造的、运算结果等于target的不同表达式的数目。

    解答:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    class Solution {
    public:
    int findTargetSumWays(vector<int>& nums, int target) {
    int sum = accumulate(nums.begin(), nums.end(), 0);
    int diff = sum - target;
    if (diff < 0 || diff % 2 != 0) return 0;
    int neg = (sum - target) / 2;
    vector<int> dp(neg + 1, 0);
    dp[0] = 1;
    for (int i = 0; i < nums.size(); ++i) {
    for (int j = neg; j >= nums[i]; --j) {
    dp[j] = dp[j] + dp[j - nums[i]];
    }
    }
    return dp[neg];
    }
    };

    提示:本质上是一个0-1背包问题,设有neg个数字取负,则有sum-neg-neg=target,剩下的就是选取元素之和为neg的元素排列个数了。

  • 题目七:买卖股票的最佳时机含手续费
    给定一个整数数组prices,其中prices[i]表示第i天的股票价格;整数fee代表了交易股票的手续费用。
    你可以无限次地完成交易,但是你每笔交易都需要付手续费。如果你已经购买了一个股票,在卖出它之前你就不能再继续购买股票了。
    返回获得利润的最大值。

    解答:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    class Solution {
    public:
    int maxProfit(vector<int> &prices, int fee) {
    int n = prices.size();
    vector<int> dp(3);
    dp[0] = 0, dp[1] = -prices[0], dp[2] = 0;
    for (int i = 1; i < n; ++i) {
    dp[0] = max(dp[0], dp[2]);
    dp[1] = max(dp[1], max(dp[0] - prices[i], dp[2] - prices[i]));
    dp[2] = max(dp[2], dp[1] + prices[i] - fee);
    }
    return max(dp[0], dp[2]);
    }
    };

    提示:三个状态:未持有股票未售卖;持有股票;持有股票刚售卖。