【C++】动态规划题目总结

动态规划介绍

动态规划(Dynamic Programming)简称dp,它是分治思想的延伸,通俗一点来说就是大事化小,小事化无的艺术。在将大问题化解为小问题的分治过程中,保存对这些小问题已经处理好的结果,并供后面处理更大规模的问题时直接使用这些结果。

特点

动态规划具备了以下三个特点:

  • 把原来的问题分解成了几个相似的子问题。
  • 所有的子问题都只需要解决一次。
  • 储存子问题的解。

解题思路

动态规划的本质,是对问题状态的定义状态转移方程的定义(状态以及状态之间的递推关系),动态规划问题一般从以下四个角度考虑:

  1. 明确问题
  2. 状态定义(根据问题从中抽象出来状态)
  3. 状态转移方程定义
  4. 状态初始化
  5. 返回结果

适用场景:最大值/最小值, 可不可行, 是不是,方案个数。

一. 矩阵系列


1、查找两个字符串a,b中的最长公共子串


题目连接

题目描述
查找两个字符串a,b中的最长公共子串。若有多个,输出在较短串中最先出现的那个。本题含有多组输入数据!

输入描述
输入两个字符串。

输出描述
返回重复出现的字符。

示例

输入
abcdefghijklmnop
abcsafjklmnopqrstuvw
输出
jklmnop

解题思路

  • 先创建一个二维数组保存公共子串长度,以较小串的长度+1为行数,较大串的长度+1为列数,初始值全为0。
  • 较大串的第一个字符开始在较小串中全部查找一遍,如果有该字符则更新数组对应位置的值,直到较大字符串遍历完。
  • 更新方式为count[i][j] = count[i-1][j-i]+1,在此过程中不断更新最大子串的长度max及起始位置start。

完整代码

#include <string>
#include <vector>
#include <iostream>
using namespace std;

string Find(const string& s1, const string& s2)
{
  int start = 0;
  int maxLength = 0;
  int len1 = s1.size();
  int len2 = s2.size();
  vector<vector<int>> count(len1 + 1, vector<int>(len2 + 1));
  for(int i = 1; i <= len1; ++i)
  {
    for(int j = 1; j <= len2; ++j)
    {
      // 更新当前位置
      if(s1[i-1] == s2[j-1])
      {
        count[i][j] = count[i-1][j-1] + 1;
      }

      if(count[i][j] > maxLength)
      {
        maxLength = count[i][j];
        start = i-maxLength;
      }
    }
  }
  return s1.substr(start, maxLength);
}

int main()
{
  string s1;
  string s2;
  // 设置s1为较小长度的串
  while (cin >> s1 >> s2)
  {
	  if (s2.size() < s1.size())
	  {
		  s1.swap(s2);
	  }
	  cout << Find(s1, s2) << endl;
  }
  return 0;
}

性能分析

  • 时间复杂度:O(len1 * len2)。len1、len2对对应最小、最大串的长度,动态规划需要遍历整个数组来完善数据,最终得到结果。
  • 空间复杂度:O(len1 * len2)。数组空间开销。

2、计算字符串距离


题目连接

题目描述
在这里插入图片描述
输入描述
每组用例一共2行,为输入的两个字符串

输出描述
每组用例输出一行,代表字符串的距离

示例

输入
abcdefg
abcdef
abcde
abcdf
abcde
bcdef
输出
1
1
2

解题思路
参考这张图,黑色数字的行和列分别表示,其中一个字符串相对于另一个空字符串的编辑距离,红色部分表示两个字符串均非空的情况。
在这里插入图片描述
if (i ≥ 1 且 j ≥ 1) ,对比新加的字符

  • 新加的字符如果相同,不需要任何变换则当前位置的距离等于前一位置的距离:distance(i, j) = distance(i-1, i+1)
  • 如果不相同,在之前的编辑距离计算好的基础上+1取最小值。
    distance(i, j) == min{ distance(i-1, j) + 1, distance(i, j-1) + 1, edit(i-1, j-1) + 1 }

完整代码

#include <vector>
#include <iostream>
using namespace std;

size_t StringDistance(const string& s1, const string& s2)
{
  // 1、创建出一个len1+1*len2+1大小的数组
  size_t len1 = s1.size();
  size_t len2 = s2.size();
  vector<vector<size_t>> distance(len1+1, vector<size_t>(len2+1));
  // 2、初始化最开始数据
  for(size_t i = 1; i <= len1; ++i)
  {
    distance[i][0] = i;
  }
  for(size_t j = 1; j <= len2; ++j)
  {
    distance[0][j] = j;
  }
  // 3、动态规划处理每一种情况,直到最后位置
  for(size_t i = 1; i <= len1; ++i)
  {
    for(size_t j = 1; j <= len2; ++j)
    {
      if(s1[i-1] == s2[j-1])
      {
        distance[i][j] = distance[i-1][j-1];
      }
      else 
      {
        distance[i][j] = min(min(distance[i-1][j]+1, distance[i][j-1]+1), distance[i-1][j-1]+1);
      }
    }
  }
  return distance[len1][len2];
}

int main()
{
  string s1;
  string s2;
  while(cin>>s1>>s2)
  {
    cout<<StringDistance(s1, s2)<<endl;
  }
  return 0;
}                    

性能分析

  • 时间复杂度:O(len1 * len2)。
  • 空间复杂度:O(len1 * len2)。

3、年终奖


题目连接

题目描述
小东所在公司要发年终奖,而小东恰好获得了最高福利,他要在公司年会上参与一个抽奖游戏,游戏在一个6*6的棋盘上进行,上面放着36个价值不等的礼物,每个小的棋盘上面放置着一个礼物,他需要从左上角开始游戏,每次只能向下或者向右移动一步,到达右下角停止,一路上的格子里的礼物小东都能拿到,请设计一个算法使小东拿到价值最高的礼物。

给定一个6*6的矩阵board,其中每个元素为对应格子的礼物价值,左上角为[0,0],请返回能获得的最大价值,保证每个礼物价值大于100小于1000。

解题思路
题目规定只能往下走或者往右走,那么当前位置肯定是从上面或者左边过来的,即当前能拿取礼物的最大价值就是max(上面位置的最大礼物价值,左面位置的最大礼物价值)+当前位置礼物价值。

  • 先处理最上一排和最左一列的数据。
  • 根据状态方程处理中间位置数据:board[i][j] += max(board[i-1][j], board[i][j-1]);

完整代码

class Bonus {
public:
    int getMost(vector<vector<int> > board) 
    {
        // 1、先处理最上一排和最左一列位置的数据
        for(int i = 1; i < 6; ++i)
        {
            board[0][i] += board[0][i-1];
            board[i][0] += board[i-1][0];
        }
        // 2、处理中间位置的数据
        for(int i = 1; i < 6; ++i)
        {
            for(int j = 1; j < 6; ++j)
            {
                board[i][j] += max(board[i-1][j], board[i][j-1]);
            }
        }
        // 3、最终右下角的值就是最大礼物价值
        return board[5][5];
    }
};

性能分析

  • 时间复杂度:O(n)。需要处理每一个位置的元素。
  • 空间复杂度:O(1)。

4、编辑距离


题目连接

解题思路

  • 问题:求word1到word2的编辑距离。
  • 状态定义:dp[i][j] — word1的[0, i-1]区间字符到word2的[0, j-1]区间字符的编辑距离。
  • 状态转移方程:dp[i][j] = 下面三种情况取最小
    • 增加一个字符:dp[i-1][j] + 1
    • 删除一个字符:dp[i][j-1] + 1
    • 替换一个字符:
      • 如果word1[i-1] == word2[j-1]的话,dp[i-1][j-1]
      • 否则,dp[i-1][j-1] + 1
  • 状态初始化
    • dp[i][0] = i,表示逐个删除字符使word1变成word2(空串)
    • dp[0][j] = j,表示逐个插入字符使word1(空串)变成word2
  • 返回结果:dp[word1.size()][word2.size()]

完整代码

class Solution {
public:
    int minDistance(string word1, string word2) 
    {
        int row = word1.size();
        int col = word2.size();
        vector<vector<int>> distances(row+1, vector<int>(col+1));
        // 1、状态初始化
        for(int i = 0; i <= row; ++i)
        {
            distances[i][0] = i;
        }
        for(int j = 0; j <= col; ++j)
        {
            distances[0][j] = j;
        }
        // 2、状态转移
        for(int i = 1; i <= row; ++i)
        {
            for(int j = 1; j <= col; ++j)
            {
                distances[i][j] = min(distances[i-1][j], distances[i][j-1]) + 1;
                if(word1[i-1] == word2[j-1])
                {
                    distances[i][j] = min(distances[i][j], distances[i-1][j-1]);
                }
                else 
                {
                    distances[i][j] = min(distances[i][j], distances[i-1][j-1] + 1);
                }
            }
        }
        // 3、返回结果
        return distances[row][col];
    }
};

性能分析

  • 时间复杂度:O(mn)。
  • 空间复杂度:O(mn)。

二. 最大子序和系列

从动态规划角度讲,最大子数组和是以一类较简单的 DP 问题,但它的状态设计比较经典,同时也是很多问题的基础组件。


1、最大子序和


题目连接

题目描述
给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
在这里插入图片描述
解题思路
题目已经保证了至少有一个元素,我们定义两个变量,一开始它们的值都是nums[0]。

  • cur:记录当前下标i位置的最大连续子数组和(注意要加上nums[i])。
  • ret:记录最大连续子数组和。

从下标为1的位置开始遍历根据状态转移方程更新cur和ret。

  • 状态转移方程:cur = num[i] + max(cur, 0);

完整代码

class Solution {
public:
    int maxSubArray(vector<int>& nums) 
    {
        int ret = nums[0];
        int cur = nums[0];
        for(int i = 1; i < nums.size(); ++i)
        {
            cur = nums[i] + max(cur, 0);
            if(cur > ret)
            {
                ret = cur;
            }
        }
        return ret;
    }
};

性能分析

  • 时间复杂度:O(n)。
  • 空间复杂度:O(1)。

2、乘积最大子数组


题目连接

题目描述
给你一个整数数组nums,请你找出数组中乘积最大的连续子数组(该子数组中至少包含一个数字),并返回该子数组所对应的乘积。
在这里插入图片描述

解题思路
参考最大子序和的思路,当前位置的乘积最大子数组 dp[i] = max(dp[i - 1] * nums[i], nums[i]),但是对于乘法而言存在负负得正的情况,所以不能简单的就这样套用。

考虑当前位置的元素cur:

  • 如果cur是个正数,我们希望以它前一个位置结尾的某个段的积也是个正数,并且希望它尽可能地大。
  • 如果cur是个负数,那么我们希望以它前一个位置结尾的某个段的积也是个负数,这样就可以负负得正,并且我们希望这个积尽可能「负得更多」,即尽可能小。

对于上面的两种情况我们维护两个状态方程,以保证得到最终正确的结果:
在这里插入图片描述
完整代码

class Solution {
public:
    int maxProduct(vector<int>& nums) 
    {
        int ret = nums[0];
        int curMax = nums[0];
        int curMin = nums[0];
        for(int i = 1; i < nums.size(); ++i)
        {
            int mx = curMax;
            int mn = curMin;
            curMax = max(nums[i], max(nums[i] * mx, nums[i] * mn));
            curMin = min(nums[i], min(nums[i] * mx, nums[i] * mn));
            ret = max(curMax, ret);
        }
        return ret;
    }
};

性能分析

  • 时间复杂度:O(n)。只需遍历一次数组即可。
  • 空间复杂度:O(1)。

3、环形子数组的最大和


题目连接

题目描述
在这里插入图片描述
在这里插入图片描述
解题思路
该题目是最大连续子序和的变种。分两种情况分析:

  • 最大连续子序和在中间,和平时一样的解法。
  • 最大连续子序和跨越头尾,此时从两边出发往中间靠拢的和是最大,那就说明中间的和是最小,我们只需找中间和最小的,用数组的总和减去这个最小的就是我们要的结果。

完整代码

class Solution {
public:
    int maxSubarraySumCircular(vector<int>& nums) 
    {
        int sum = nums[0];
        int curMax = nums[0];
        int curMin = nums[0];
        int allMax = nums[0];
        int allMin = nums[0];
        // 1、正常计算该数组的最大子数组和
        for(size_t i = 1; i < nums.size(); ++i)
        {
            sum += nums[i];
            curMax = max(nums[i], curMax + nums[i]);
            allMax = max(allMax, curMax);
        }
        // 2、正常计算该数组的最小子数组和
        // 循环终止条件是“i < nums.size() - 1”的原因:
        // 如果最大连续子数组和跨越了头尾,根据ret = sum - allMin
        // allMin一定不会包括最后一个因素,这个元素一定是在ret中的,当然不减一也可以通过。
        for(size_t i = 1; i < nums.size() - 1; ++i)
        {
            curMin = min(nums[i], curMin + nums[i]);
            allMin = min(allMin, curMin);
        }
        // 3、看是中间的连续子数组大还是贯穿头尾的连续子数组大
        return max(allMax, sum == allMin? sum : sum - allMin);
    }
};

性能分析

  • 时间复杂度:O(n)。一共遍历两次数组。
  • 空间复杂度:O(1)。

三. 最长递增子序列(LIS)系列

最长递增子序列(Longest increasing subsequence, LIS)是个经典的动态规划问题,很多单串的题目的状态设计都是启发自这道题来设计的。


1、最长递增子序列


题目描述

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

子序列是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。

在这里插入图片描述
在这里插入图片描述
解题思路
创建整型数组count用来记录到当前下标位置的最长递增子序列的长度,一开始都只算自己,所以都初始化为1。

  1. 遍历[0, i-1]区间的nums元素,找到小于nums[i]的元素。
  2. 通过状态转移方程:count[ i ] = max(count[ i ], counnt[ j ]+1);来更新count[ i ]。
  3. 最后取counnt中最大值就是最终结果。

完整代码

class Solution {
public:
    int lengthOfLIS(vector<int>& nums) 
    {
        vector<int> count(nums.size(), 1);
        for(size_t i = 1; i < nums.size(); ++i)
        {
            for(int j = 0; j < i; ++j)
            {
                if(nums[j] < nums[i])
                {
                    count[i] = max(count[i], count[j]+1);
                }
            }
        }
        return *max_element(count.begin(), count.end());
    }
};

性能分析

  • 时间复杂度:O(n^2)。两层for循环。
  • 空间复杂度:O(n)。处理一个count[ i ],需要用到前面[0, i-1]区间的所有数据,所以要创建一个长度为n的count数组。

2、俄罗斯套娃信封问题


题目连接

题目描述
给你一个二维整数数组 envelopes ,其中 envelopes[i] = [wi, hi] ,表示第 i 个信封的宽度和高度。

当另一个信封的宽度和高度都比这个信封大的时候,这个信封就可以放进另一个信封里,如同俄罗斯套娃一样。

请计算 最多能有多少个 信封能组成一组“俄罗斯套娃”信封(即可以把一个信封放到另一个信封里面)。

注意:不允许旋转信封。

在这里插入图片描述
在这里插入图片描述
解题思路

  1. 对envelopes的每一个元素进行排序:对宽度升序,并且在宽度相同时对高度降序,这样做的目的有两个:
    • 保证迭代排序后的envelopes时,宽度是非递减的(注意:相邻的两个envelope可能宽度相同)
    • 在相邻两个envelope宽度相同的情况时,两者的高度是非递增的,比如(3, 5), (3, 3), 不会出现“相同高度,后者套前者”的情况。
  2. 随后我们就可以忽略宽度,求出以高度为基准的最长严格递增子序列,其长度即为答案。

完整代码

// sort会把传入的每一个元素解引用之后再传入MyCompare中作比较
// 所以MyCompare的参数类型为vector<int>
// 如果返回true,则让e1排到e2之前
// 如果返回false,则让e1排到e2之后
// 如果要把MyCompare定义到类内部的话需要加static修饰,这样sort中调用时不会因为没有this指针而报错
// 也可以把MyCompare定义到类外
int MyCompare(const vector<int>& e1, const vector<int>& e2)
{
    if(e1[0] < e2[0])
    {
        return 1;
    }
    else if(e1[0]==e2[0] && e1[1]>e2[1])
    {
        return 1;
    }
    return 0;
}

class Solution 
{
public:
    int maxEnvelopes(vector<vector<int>>& envelopes) 
    {
        sort(envelopes.begin(), envelopes.end(), MyCompare);
        vector<int> count(envelopes.size(), 1);
        for(int i = 1; i < envelopes.size(); ++i)
        {
            for(int j = 0; j < i; ++j)
            {
                if(envelopes[j][1] < envelopes[i][1])
                {
                    count[i] = max(count[i], count[j]+1);
                }
            }
        }
        return *max_element(count.begin(), count.end());
    }
};

性能分析

  • 时间复杂度:动态规划需要的时间复杂度为 O(n^2)。
  • 空间复杂度:数组count的开销。

3、最长递增子序列的个数


题目连接

题目描述
给定一个未排序的整数数组,找到最长递增子序列的个数。
在这里插入图片描述
解题思路
这题是最长递增子序列的进阶版本,最原题的基础上再维护一个cnt数组用来统计当前位置的最长递增序列的个数。

该题的用到的两个状态转移方程说明:

  • dp[i]:代表第个i位置上的最长递增子序列的长度。
  • cnt[i]:代表第i个位置上的最长递增子序列的个数。

j < inums[j] < nums[i] 时:

  • 如果dp[j] + 1 > dp[i],更新i位置的最长递增子序列长度dp[i] = dp[j] + 1,并重置cnt[i] = cnt[j]。
  • 否则如果dp[j] + 1 == dp[i]的话,更新cnt[i] += cnt[j]。

完整代码

class Solution {
public:
    int findNumberOfLIS(vector<int>& nums) 
    {
        int ret = 1;
        int maxLen = 1;
        vector<int> dp(nums.size(), 1);
        vector<int> cnt(nums.size(), 1);
        for(size_t i = 1; i < nums.size(); ++i)
        {
            for(size_t j = 0; j < i; ++j)
            {
                if(nums[j] < nums[i])
                {
                    if(dp[j] + 1 > dp[i])
                    {
                        dp[i] = dp[j] + 1;
                        cnt[i] = cnt[j];// 重置计数
                    }
                    else if(dp[j] + 1 == dp[i])
                    {
                        cnt[i] += cnt[j];// 更新计数
                    }
                }
            }
            if(dp[i] > maxLen)
            {
                maxLen = dp[i];
                ret = cnt[i];
            }
            else if(dp[i] == maxLen)
            {
                ret += cnt[i];
            }
        }
        return ret;
    }
};

性能分析

  • 时间复杂度:O(n^2)。处理当前位置的状态转移方程时需要参考前面i-1个已处理好了的数据。
  • 空间复杂度:O(n)。

四. Fibonacci系列


1、斐波那契数列


题目连接

题目描述:大家都知道斐波那契数列,现在要求输入一个整数n,请你输出斐波那契数列的第n项(从0开始,第0项为0)。

解题思路

  • 状态定义:F(n),第n个斐波那契数的值
  • 状态转移方程:F(n) = F(n-1) + F(n-2)
  • 状态初始化:F(0) = 0,F(1) = 1
  • 返回结果:F(n)

完整代码

class Solution 
{
public:
    int Fibonacci(int n) 
    {
        if(n == 0)
            return 0;
        if(n == 1)
            return 1;
        
        return Fibonacci(n-1) + Fibonacci(n-2);
    }
};
  • 时间复杂度:O(2^n),递归次数是公比为2的等比数列之和。
  • 空间复杂度:O(2^n)。

优化版本一
上面的递归的方法时间复杂度为O(2^n),随着n的增大呈现指数增长,效率低下,这是因为在递归过程中有大量的重复计算,当输入比较大时,可能导致栈溢出使用。

下面循环迭代的方法来减少递归不可避免的重复计算:

class Solution {
public:
    int Fibonacci(int n) 
    {
        if(n == 0)
            return 0;
        if(n == 1)
            return 1;
        // 申请一个数组,保存子问题的解
        vector<int> Fib(n+1);
        Fib[0] = 0;
        Fib[1] = 1;
        for(int i = 2; i <= n; ++i)
        {
            Fib[i] = Fib[i-1] + Fib[i-2];
        }
        return Fib[n];
    }
};
  • 时间复杂度:O(n)。
  • 空间复杂度:O(n)。

最终优化版本
上述解法的空间复杂度为O(n),其实Fib(n)只与它相邻的前两项有关,所以没有必要保存所有子问题的解,只需要保存两个子问题的解就可以。

下面方法的空间复杂度将优化为O(1):

class Solution {
public:
    int Fibonacci(int n) 
    {
        if(n == 0)
            return 0;
        if(n == 1 || n == 2)
            return 1;
        
        int fn1 = 1;
        int fn2 = 1;
        int fn = 0;
        for(int i = 3; i <= n; ++i)
        {
            fn = fn1 + fn2;
            fn2 = fn1;
            fn1 = fn;
        }
        return fn;
    }
};
  • 时间复杂度:O(n)。
  • 空间复杂度:O(1)。

五. 路径系列


1、三角矩阵最短路径和


题目连接

题目描述
给定一个三角形 triangle ,找出自顶向下的最小路径和。

每一步只能移动到下一行中相邻的结点上。相邻的结点 在这里指的是 下标 与 上一层结点下标 相同或者等于 上一层结点下标 + 1 的两个结点。也就是说,如果正位于当前行的下标 i ,那么下一步可以移动到下一行的下标 i 或 i + 1 。

方法一:自顶向下求解

  • 问题: 找出自顶向下的最小路径和。
  • 状态定义:dp[i][j] — 自顶到[i][j]位置的最小路径和。
  • 状态转移方程:
    • 当0<j<i时:dp[i][j] = min(dp[i-1][i] + dp[i-1][j-1]) + array[i][j]
    • 当j==0时:dp[i][j] = dp[i-1][j] + array[i][j]
    • 当i==j时:dp[i][j] = dp[i-1][j-1] + array[i][j]
  • 状态初始化:dp[0][0] = array[0][0]
  • 返回结果:min(dp[ROW-1][j])
class Solution
{
public:
    int minimumTotal(vector<vector<int>>& triangle)
    {
        // 计算出三角矩阵的最大行数
        size_t row = triangle.size();
        // 1、动态规划算出所有位置的最小路径和
        for (size_t i = 1; i < row; ++i)
        {
            for (size_t j = 0; j <= i; ++j)// 不要用j <= triangle[i].size()
            {
                if (j == 0)
                    triangle[i][j] += triangle[i - 1][j];
                else if (j == i)
                    triangle[i][j] += triangle[i - 1][j - 1];
                else
                    triangle[i][j] += min(triangle[i - 1][j], triangle[i - 1][j - 1]);
            }
        }
        // 2、拿到自顶向下的最小路径和
        int minSum = triangle[row - 1][0];
        for (size_t j = 1; j < row; ++j)// 不要用j <= triangle[row-1].size()
        {
            minSum = min(minSum, triangle[row - 1][j]);
        }

        return minSum;
    }
};

方法二:自底向上求解
从倒数第二行开始往上求解,不用考虑边界元素:

  • 状态定义:dp[i][j] — 自底到[i][j]位置的最小路径和。
  • 状态转移方程:dp[i][j] = min(dp[i+1][j], dp[i+1][j+1]) + array[i][j];
  • 态初始化:dp[ROW.size()-1][j] = array[ROW.size()-1][j];
  • 返回结果:dp[0][0]
class Solution 
{
public:
    int minimumTotal(vector<vector<int>>& triangle) 
    {
        size_t row = triangle.size();
        for(int i = row-2; i >= 0; --i)
        {
            for(int j = 0; j <= i; ++j)
            {
                triangle[i][j] += min(triangle[i+1][j], triangle[i+1][j+1]);
            }
        }
        return triangle[0][0];
    }
};

性能分析
两种方法效率上没有差别,不过方法二的代码写出来更加简洁。

  • 时间复杂度:O(n)。其中n代表三角矩阵中元素的个数。
  • 空间复杂度:O(n)。一般不能修改原数组的数据,所以拷贝原二维矩阵需要n的空间。

2、方形矩阵路径数目


题目连接

解题思路

  • 问题:求起点到终点的路径的数目。
  • 状态定义:dp[i][j] — 从起点到(i, j)位置的路径的数目。
  • 状态转移方程:dp[i][j] = dp[i-1][j] + dp[i][j-1]
  • 状态初始化:dp[0][j] = dp[i][0] = 1
  • 返回结果:dp[m-1][n-1]

完整代码

class Solution 
{
public:
    int uniquePaths(int m, int n) 
    {
        vector<vector<int>> dp(m, vector<int>(n, 0));
        // 1、状态初始化
        for(size_t i = 0; i < m; ++i)
        {
            dp[i][0] = 1;
        }
        for(size_t j = 1; j < n; ++j)
        {
            dp[0][j] = 1;
        }
        // 2、动态规划处理中间的所有dp[i][j]
        for(size_t i = 1; i < m; ++i)
        {
            for(size_t j = 1; j < n; ++j)
            {
                dp[i][j] = dp[i-1][j] + dp[i][j-1];
            }
        }
        // 3、返回最终结果
        return dp[m-1][n-1];
    }
};

性能分析

  • 时间复杂度:O(mn)。
  • 空间复杂度:O(mn)。

3、方形矩阵最短路径和

题目连接

解题思路

  • 问题:左上角到右下角的最短路径和。
  • 状态定义:dp[i][j] — (0, 0)到(i, j)的最短路径和。
  • 状态转移方程:dp[i][j] = min(dp[i-1][j], dp[i][j-1])+array[i][j]
  • 状态初始化:dp[0][0] = array[0][0], dp[0][j] = dp[0][j-1]+array[0][j], dp[i][0] = dp[i-1][0]+array[i][0]
  • 返回结果:dp[row-1][col-1]

完整代码

class Solution 
{
public:
    int minPathSum(vector<vector<int> >& grid) 
    {
        size_t row = grid.size();
        size_t col = grid[0].size();
        // 1、状态初始化
        for(size_t j = 1; j < col; ++j)
        {
            grid[0][j] = grid[0][j-1] + grid[0][j];
        }
        for(size_t i = 1; i < row; ++i)
        {
            grid[i][0] = grid[i-1][0] + grid[i][0];
        }
        // 2、中间范围的(i, j)数据计算
        for(size_t i = 1; i < row; ++i)
        {
            for(size_t j = 1; j < col; ++j)
            {
                grid[i][j] = min(grid[i-1][j], grid[i][j-1]) + grid[i][j];
            }
        }
        // 3、返回终点结果
        return grid[row-1][col-1];
    }
};

性能分析

  • 时间复杂度:O(mn)。
  • 空间复杂度:O(mn)。

六. 背包问题


1、01背包


题目连接

解题思路

  • 问题:最多能装入背包的总价值是多大。
  • 状态定义:dp[i][j] — (dp中的的i从1开始,对应到数组就是下标为i-1的那个物品),前i个物品(包括第i个)放到容量为j的背包中,最大价值是多少。
  • 状态转移方程:
    • w[i] <= j时,dp[i][j] = max(dp[i-1][j], dp[i-1][j-w[i]] + v[i]);
    • w[i] > j时,dp[i][j] = dp[i-1][j];
  • 状态初始化:dp[0][j] = dp[i][0] = 0
  • 返回结果:dp[i][j]

方法一:二维数组

class Solution {
public:
    int backPackII(int m, vector<int> &a, vector<int> &v) 
    {
        // 1、状态初始化
        int n = a.size();
        vector<vector<int>> dp(n+1, vector<int>(m+1));
        // 2、利用状态转移方程计算出所有位置的dp[i][j]的值
        for(int i = 1; i <= n; ++i)
        {
            for(int j = 1; j <= m; ++j)
            {
                if(a[i-1] <= j)
                {
                    dp[i][j] = max(dp[i-1][j], dp[i-1][j-a[i-1]] + v[i-1]);
                }
                else
                {
                    dp[i][j] = dp[i-1][j];
                }
            }
        }
        // 3、返回结果
        return dp[n][m];
      }
};  
  • 时间复杂度:O(nm)
  • 空间复杂度:O(nm)。

方法二:一维数组

class Solution 
{
public:
    int backPackII(int m, vector<int>& a, vector<int>& v)
    {
   	    // 1、状态初始化
   	    int n = a.size();
        vector<int> dp(m + 1);
		// 2、利用状态转移方程计算出所有位置的dp[i][j]的值
        for (int i = 1; i <= n; ++i)
        {
            for (int j = m; j >= 1; --j)
            {
                if (a[i - 1] <= j)
                {
                    dp[j] = max(dp[j], dp[j - a[i - 1]] + v[i - 1]);
                }
            }
        }
		// 3、返回结果
        return dp[m];
    }
};  
  • 时间复杂度:O(nm)
  • 空间复杂度:O(m)。

七. 待分类


1、拆分词句


题目连接

题目描述
给定一个字符串s和一组单词dict,判断s是否可以用空格分割成一个单词序列,使得单词序列中所有的单词都是dict中的单词(序列可以包含一个或多个单词)。
例如:
给定s=“nowcode”;
dict=[“now”, “code”]
返回true,因为"nowcode"可以被分割成"now code"

解题思路

  • 问题:判断字符串是否能被分割成一个单词序列
  • 状态定义:dp[i] — 前i个字符能否被分割
  • 状态转移方程:dp[i] = (j<i && dp[j] && [j+1, i]存在于dict中)
  • 状态初始化:dp[0] = true
  • 返回结果:dp[s.size()]

完整代码

class Solution 
{
public:
    bool wordBreak(string s, unordered_set<string> &dict) 
    {
        vector<bool> canBreak(s.size()+1, false);
        canBreak[0] = true;
        for(size_t i = 1; i <= s.size(); ++i)
        {
            for(size_t j = 0; j < i; ++j)
            {
                if(canBreak[j] && (dict.find(s.substr(j, i-j)) != dict.end()))
                {
                    canBreak[i] = true;
                    break;
                }
            }
        }
        return canBreak[s.size()];
    }
};

性能分析

  • 时间复杂度:O(n^2)。
  • 空间复杂度:O(n^2)。

2、分割回文串


题目连接

解题思路

  • 问题:把字符串s分割成回文子串的最小分割次数。
  • 状态定义:dp[i] — i从1开始,把前面的i个字符分割成回文子串的最小分割次数。
  • 状态转移方程:
    • [0, i-1]直接是回文串,dp[i] = 0
    • 否则:(j < i && [j+1, i-1]是回文串) —> 更新dp[i] = min(dp[i], dp[j]+1)
  • 状态初始化:dp[i] = i-1
  • 返回结果:dp[s.size()]

完整代码

class Solution {
public:
    int minCut(string s) 
    {
        // 1、状态初始化
        int len = s.size();
        vector<int> minCut(len+1);
        for(int i = 0; i <= len; ++i)
        {
            minCut[i] = i-1;
        }
        // 2、状态转移
        for(int i = 2; i <= len; ++i)
        {
            if(IsPal(s, 0, i-1))
            {
                minCut[i] = 0;
                continue;
            }
            else 
            {
                for(int j = 1; j < i; ++j)
                {
                    if(IsPal(s, j, i-1))
                    {
                        minCut[i] = min(minCut[i], minCut[j]+1);
                    }
                }
            }
        }
        // 3、返回结果
        return minCut[len];
    }
    
private:
    // 判断字符区间是否回文
    bool IsPal(const string& s, int start, int end)
    {
        while(start < end)
        {
            if(s[start++] != s[end--])
            {
                return false;
            }
        }
        return true;
    }
};

性能分析

  • 时间复杂度:O(n^3),n为字符串长度。在第二次for循环内每次都有判断是否回文的逻辑,所以实际上是3层for循环。
  • 空间复杂度:O(n)。必须要保存前i个字符的最小分割结果。

优化时间复杂度为O(n^2)
上面的代码在第二层for循环中每次需要判断一段区间内的字符串是否回文,这样导致时间复杂度为O(n^3),现在我们考虑用空间换时间的方法来优化时间复杂度为O(n ^2)。具体办法是使用一个二维数组来存储[i, j]范围内字符串的回文情况,到时候直接查找matrix[i][j]的值就知道是否回文了,矩阵的设计思路如下,也是用动态规划的思想:

  • 目的:二维矩阵存储所有区间范围内的字符串回文情况。
  • 状态定义:dp[i][j] = [i, j]范围内的字符串是否回文。
  • 状态转移方程:dp[i][j] = (s[i] == s[j] && dp[i+1][j-1] == true)
  • 状态初始化:
    • i == j --> dp[i][j] = true
    • i+1 == j --> dp[i][j] = (s[i] == s[j] ? true : false)

PS:观察状态转移方程,在计算dp[i][j]时需要事先知道dp[i+1][j-1]的结果,所以我们从最后一行开始动态规划,又因为该矩阵的目的是存储字符串s所有区间范围内的字符串回文情况,所以只需计算矩阵主对角线上半部分的值即可:
在这里插入图片描述
最终代码如下:

class Solution {
public:
    int minCut(string s) 
    {
        // 1、状态初始化
        int len = s.size();
        vector<int> minCut(len+1);
        for(int i = 0; i <= len; ++i)
        {
            minCut[i] = i-1;
        }
        // 2、状态转移
        vector<vector<bool>> matrix = GerMatrix(s);
        for(int i = 2; i <= len; ++i)
        {
            if(matrix[0][i-1])
            {
                minCut[i] = 0;
                continue;
            }
            else 
            {
                for(int j = 1; j < i; ++j)
                {
                    if(matrix[j][i-1])
                    {
                        minCut[i] = min(minCut[i], minCut[j]+1);
                    }
                }
            }
        }
        // 3、返回结果
        return minCut[len];
    }
    
private:
    vector<vector<bool>> GerMatrix(const string& s)
    {
        int n = s.size();
        vector<vector<bool>> matrix(n, vector<bool>(n, false));
        for(int i = n-1; i >= 0; --i)
        {
            for(int j = i; j < n; ++j)
            {
                if(i == j)
                    matrix[i][j] = true;
                else if(i+1 == j && s[i] == s[j])
                    matrix[i][j] = true;
                else if(s[i] == s[j] && matrix[i+1][j-1])
                    matrix[i][j] = true;
            }
        }
        return matrix;
    }
};
  • 3
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值