Leetcode的刷题记录

长期更新,记录贴。

303 区域和检索

给定一个整数数组 nums,求出数组从索引 i 到 j(i ≤ j)范围内元素的总和,包含 i、j 两点。实现 NumArray 类:NumArray(int[] nums),
使用数组 nums 初始化对象 。int sumRange(int i, int j) 返回数组 nums 从索引 i 到 j(i ≤ j)范围内元素的总和,包含 i、j 两点(也就是 sum(nums[i], nums[i + 1], ... , nums[j]))

求数组的一个子区域的和,暴力计算:每次调用函数进行循环求解。
前缀和:计算\(nums[i]+nums[i+1]+...+nums[j]\)时显然可以用\(sum(nums[0]~nums[j]) - sum(nums[0]~nums[i]) + nums[i]\),这样如果能记录每个元素前面的元素和,要求某个区间内的元素和就可以直接公式计算。因此,直接在初始化数组时记录一个前缀和数组即可。

class NumArray:
    def __init__(self, nums: List[int]):
        self.sums = []  # 记录每一个元素的前缀和
        self.nums = nums
        for i in range(len(nums)):
            if i == 0:
                self.sums.append(nums[0])
            else:
                self.sums.append(self.sums[-1]+nums[i])  # 每次计算到下一个元素,只需要加上该位置元素即可
    def sumRange(self, i: int, j: int) -> int:
        # 计算i到j的元素和,就可以直接调用记录的前缀和数组
        return self.sums[j] - self.sums[i] + self.nums[i]

304 二维矩阵的区域和

给定一个二维矩阵,计算其子矩形范围内元素的总和,该子矩阵的左上角为 (row1, col1) ,右下角为 (row2, col2)
矩阵
上图子矩阵左上角 (row1, col1) = (2, 1) ,右下角(row2, col2) = (4, 3),该子矩形内元素的总和为 8。
示例:

给定 matrix = [
  [3, 0, 1, 4, 2],
  [5, 6, 3, 2, 1],
  [1, 2, 0, 1, 5],
  [4, 1, 0, 1, 7],
  [1, 0, 3, 0, 5]
]

sumRegion(2, 1, 4, 3) -> 8
sumRegion(1, 1, 2, 2) -> 11
sumRegion(1, 2, 2, 4) -> 12

和303一样,显然如果要求二维数组某个对角区域和,如果能知道各个点的左上角区域(包含该点)的元素和,那么两个点的对角区域元素和就可以很方便计算。记sum(i,j)为二维数组的左上角(0,0)到点(i,j)的区域元素和,如下图,计算点A和点B的包围区域元素和。从图上可以很直观的看出,AB的区域元素和= B的左上角元素和 - 区域左下角的元素和(不含该点) - 区域右上角的元素和 + A的左上角元素和(不含该点,加它是因为这部分被减去两次)

和303问题一样,在初始化时就求出sum(i,j)即可。求前缀和的方法,画图也能直观看出,如下图要求红色点处的左上角区域元素和,可以递归调用,可以两种计算:1)该点对角点的前缀和 + 该点的所在行左边元素和 + 该点所在列的上边元素和 2)该点的左边点的前缀和 + 上边点的前缀和 - 重合区域(对角点的前缀和),即图中的区域②+区域③-区域①。
从分析来看,第一列和第一行的元素的前缀和实际就是一维的情况。然后扩展到二维,依次求出剩下点的前缀和即可。

class NumMatrix:
    def __init__(self, matrix):
        self.sum_mat = matrix  # sum_mat和原数组一样大,存放每个点的前缀和
        # 记录(0,0)到点(i,j)的区域元素和 
        if not len(matrix) == 0:
            # 对于第一行和第一列,是一维问题,直接计算
            for i in range(1,len(matrix)): # 遍历行 的 第一列元素
                # [i][0]处的元素前缀和是 
                self.sum_mat[i][0] = self.sum_mat[i-1][0] + self.sum_mat[i][0]
            for j in range(1,len(matrix[0])):  # di
                self.sum_mat[0][j] = self.sum_mat[0][j-1]  + self.sum_mat[0][j]

            for i in range(1,len(matrix)):
                for j in range(1,len(matrix[0])):
                    # matrix[i][j]表示第i行j列的元素 要求它的左上角邻域元素和  
                    # 它左边点的邻域和 + 上边点的领域和 - 左上角点的邻域和
                    self.sum_mat[i][j] = self.sum_mat[i][j-1] +  self.sum_mat[i-1][j] - self.sum_mat[i-1][j-1] + self.sum_mat[i][j]



    def sumRegion(self, row1: int, col1: int, row2: int, col2: int) -> int:
        # 求任意两点之间的邻域和 等于 
        
        if col1 == 0  and row1 == 0:
            return self.sum_mat[row2][col2]

        if col1 == 0 :
            return self.sum_mat[row2][col2] - self.sum_mat[row1-1][col2]

        if row1 == 0 :
            return self.sum_mat[row2][col2] - self.sum_mat[row2][col1-1]

        p1 = [row2,col1-1]  # 考虑col1就是第一列的情况
        left_bottom = self.sum_mat[p1[0]][p1[1]]
        right_up = self.sum_mat[row1-1][col2] 
        left_up = self.sum_mat[row1-1][col1-1]
        return self.sum_mat[row2][col2] - (left_bottom + right_up - left_up)
class Solution:
    def generateMatrix(self, n: int) -> List[List[int]]:
        i = 0  # 初始位置
        j = 0
        matrix = [[0 for _ in range(n)]  for _ in range(n)]  # 要生成的矩阵
        print(matrix[0][0])
        matrix[0][0] = 1
        print(matrix)
        while matrix[i][j] != n*n :
            # 当前的矩阵最后一个赋值元素是 n*n
            while j+1 < n and matrix[i][j+1]==0: 
            # 向右走
                matrix[i][j+1]=matrix[i][j]+1
                j = j + 1
            print("右边走完",matrix[i][j],matrix[i+1][j],i,j)
            while i+1 < n and matrix[i+1][j] == 0:
                # 向下走
                matrix[i+1][j] = matrix[i][j] + 1
                print(matrix[i+1][j])
                i = i +1
            print("下面走完",matrix[i][j])
            while j-1 >= 0 and matrix[i][j-1] ==0:
                matrix[i][j-1] = matrix[i][j] + 1
                j = j -1
            print("左边走完",matrix[i][j])
            while i-1 >= 0 and matrix[i-1][j] == 0:
                # 向上走
                matrix[i-1][j] = matrix[i][j] +1 
                i = i - 1
            print("上面走完",matrix[i][j])
            print("\n\n")
            # break
        for k in matrix:
            print(k,'\n')

动态规划

DP步骤:

  • 确定dp下标及其含义
  • 递推公式
  • 初始化
  • 遍历顺序
  • 举例推导

M X N矩阵的路径数目

LeetCode62和63题

\(M*N\)的网格,每次只能向右或向下走,左上角到右下角的路径数目。
其中62题是路径总数,63题在其基础上,部分格子存在障碍,记作1,遇到这种格子就无法前进,求路径总数

对于任意一个格子(i,j)而言,只能由两种路径抵达,即从(i-1,j)向下走一步或者(i,j-1)向右走一步,因此显然,如果用二维dp,令dp[i][j]表示到达(i,j)的路径数,那么\(dp[i][j]=dp[i-1][j] + dp[i][j-1]\)

初始化:对于第一列和第一行的所有点而言,只能一直朝着一个方向走,因此到达它的路径数都是1,即\(dp[:][0]=0,dp[0][:]=0\)
而在63题中,如果有障碍物,第一列和第一行在第一个障碍物之前的点都能抵达,之后的点都不能抵达,路径数是0。

实现,63题的代码如下。62题同理,只是无需障碍物判断,初始化时每一个格子都能初始化。循环赋值dp时每一个点都能进行计算。

class Solution {
    public int uniquePathsWithObstacles(int[][] obstacleGrid) {
        int m = obstacleGrid.length, n = obstacleGrid[0].length;
            int[][] dp = new int[m][n];

            for(int i =0;i<m;i++){  // 
                if (obstacleGrid[i][0] == 1) {  // 如果当前的格子[i][0]是1,遇到障碍物,之后就无需判断,全是0 直接结束循环
                    break;
                } 
                else {
                    dp[i][0] = 1;  //还没遇到障碍物,是1
                }
            }

            for(int j =0;j<n;j++){
                if (obstacleGrid[0][j] == 1) {
                    break;
                } 
                else {
                    dp[0][j] = 1;
                }
            }

            for(int i =1;i<m;i++){
                for(int j=1;j<n;j++){
                    if(obstacleGrid[i][j] == 0 ){  // 如果[i][j]本身是障碍物,那么不用管,到达它的方法是0,只需判断[i][j]是0的情况。
                        dp[i][j] = dp[i-1][j] + dp[i][j-1];
                    }
                }
            }
        
        return dp[m-1][n-1];
    }
}

343题 整数拆分

给定一个正整数 n,将其拆分为至少两个正整数的和,并使这些整数的乘积最大化。 返回你可以获得的最大乘积。

分析: 对于正整数i,假设dp[i] 表示它可以拆分的最大乘积,即说明存在一组数x1,x2,...xm,和为i,乘积最大。假设拆分的其中一个数是j,剩下的那部分数的和是i-j。这个因数可以拆分的最大乘积是dp[i-j], 但是如果不拆分剩下的i-j,那么可以得到另一种组合乘积i*(i-j),而i的拆分是这两种情况之一,选择较大的那个。即dp[i] = max(j* dp[i-j], j*(i-j)) ,而j可以通过依次遍历1~~i-1 进行比较。

边界情况,至少拆分两个正整数,对n=1,2可以直接确定结果。1不能拆分,因此dp[1]=0,2只能拆分为1+1=2,因此dp[2]=1*1=1, 要求dp[i],从3开始遍历,而对每一种拆分结果,从1开始遍历判断。

class Solution {
    public int integerBreak(int n) {
        // 
        // 先求任意一个数 i 分解的最大乘积  
        // (0)

        if (n==1) {return 0;}
        if (n==2) {return 1;}
        int[] dp = new int[n+1];
        dp[1] = 0;  // 将1拆分的最大乘积
        dp[2] = 1; //将2拆分的最大乘积

        for(int i=3; i <=n; i ++ ){
            // 对于每一个i 计算它可以拆分的最大积
            for (int j =1; j <=i-1; j++){
                // i可以拆分一个因数j  j可以从1到i-1进行遍历
               
                int max_num = Math.max((i-j)*j, j* dp[i-j]);   //剩下的因数是 i-j 两种情况 继续拆分它和不拆分它
                dp[i] = Math.max(dp[i], max_num); // 每次计算完要更新一次dp[i] 
            }
        }
        return dp[n];
    }
}

91解码方法

一条包含字母 A-Z 的消息通过以下映射进行了 编码 :'A' -> 1 'B' -> 2 ...'Z' -> 26
要 解码 已编码的消息,所有数字必须基于上述映射的方法,反向映射回字母(可能有多种方法)。例如,"11106" 可以映射为:

"AAJF" ,将消息分组为 (1 1 10 6)
"KJF" ,将消息分组为 (11 10 6)注意,消息不能分组为 (1 11 06) ,因为 "06" 不能映射为 "F" ,这是由于 "6" 和 "06" 在映射中并不等价。

给你一个只含数字的 非空 字符串 s ,请计算并返回 解码 方法的 总数 。

题目数据保证答案肯定是一个 32 位 的整数。

 思路: 动态规划,考虑当前字符是单独进行映射 还是与前面的字符一起映射

使用dp[i]表示前i个字符映射的方法数。假设当前遍历到s的第i个字符,它的解码方法计算分两种情况考虑:

  1. 单独将该字符映射, 如果s[i] !='0',必然可以解码,解码方法是dp[i-1], dp的含义:  s的前i个部分,可以映射的方法数 ,如果s[i]的字符可以映射,它的总方法数还是 和前一个相同。 而不是增加或是什么。记作\(num1 = dp[i-1] \quad if \quad decode(s[i])\)
  2. 如果将该字符和前一个组合一起映射,条件是 :前一个字符不是'0',因为两个字符要解码不能以0开头,只能是10~26的数字。把这种映射的解码方法数量记作 \(num2=dp[i-2] \quad if \quad decode(s[i-1]+s[i])\)

总的解码方法就应该是\(dp[i] = num1 + num2\),因为两种解码情况是不重叠的。方法1解码出来的字母必然是 \(s[0:i-1]\)解码的结果加上\(s[i]\)解码的结果,而方法2出来的是\(s[0:i-2]\)解码结果加上\(s[i-1:i]\)的解码结果。

编码实现:初始化int[] dp =new int[n],长度是数组的长度。一些特殊情况:

数组长度是0 或者第一个字符是'0',必然不能映射,返回0。 初始化dp[0]=1,因为已经排除0字符开头的情况,第一个必然可以映射。长度为1的时候也可以直接返回。

for循环遍历的时候,由于前一个组合映射,需要考虑dp[i-2]的情况,如果i=1时会越界,因此对于1单独考虑组合映射。

class Solution {
    public int numDecodings(String s) {
        char[] st = s.toCharArray(); //转成字符数组
        int n = st.length;
        if (n==0 || st[0]=='0') {return 0;} //空字符 或者以0开头
        int[] dp = new int[n];
        // dp[i]表示i及其之前的字符映射的结果
        dp[0] = 1; //s[0]必然可以映射 上面已经排除 s[0]=0的情况了
        if (n==1){return dp[0];}
        // 考虑dp[1]  =1 还是2 
     
        for(int i=1;i<n; i++){
            if (st[i]!='0'){
                dp[i] = dp[i] + dp[i-1]; //s[i]只要不是0 就可以单独映射 方法是dp[i-1]
            }
            // i= 1单独考虑 如果 0和1位的两个可以映射,方法数 +1
            if(i==1 && (st[i-1] - '0')*10 + (st[i]- '0')*1 <=26){
                dp[i] = dp[i] + 1;
            }
            //只要 s[i-1] 和s[i]组成的字符 转成的两位数小于等于26即可
            //s[i-1]是十位  s[i]是各位 分别转数字 计算这个两位数
            // i-2 要越界 因此 i= 1的时候单独考虑 
            if( i >1 &&st[i-1] !='0' && ((st[i-1] - '0')*10 + (st[i]- '0')*1) <= 26){
                dp[i] = dp[i] + dp[i-2];
            }
        }

        return dp[n-1];

    }
}

96题 不同的二叉搜索树

给定一个整数 n,求以 1 ... n 为节点组成的二叉搜索树有多少种?

二叉搜索树: 对于任意一个节点,左子树的节点值小于等于该节点,右子树的节点值大于等于该节点的值。

题中用n个数构成二叉搜索树,如果根节点是i,那么他的左子树必然只有i-1个节点, 右子树只有n-i个节点。假设\(n\)个节点的二叉树有\(G(n)\)个,用\(f(i)\)表示以\(i\)为根节点的数的种类,那么\(G(n)=f(1)+f(2) +...+f(n)\)

对于\(f(i)\),表示以\(i\)为根节点的树的数量,将这个问题拆成更小规模的问题,显然左子树有i-1个节点,而i-1个节点的二叉搜索树一共有G(i-1)个,同理右子树一共有G(n-i)种情况,那么组合起来一共有\(f(i) = G(i-1)*G(n-i)\)

\(G(n) = G(0)*G(n-1)+ G(1)*G(n-2) + G(n-1)*G(0)\)

程序实现:依次遍历计算\(f(i)\)

class Solution {
    public int numTrees(int n) {
        int[] dp = new int[n+1];
        if (n<=2)  {return n;} // 
        dp[0] = 1; // dp[i]表示 i节点的二叉搜索树一共1中
        dp[1] = 1; // dp[0] =1 ?没有
        dp[2] = 2;
        for (int i =3;i < n+1; i++){
            //轮流计算dp[i] = f(1) +++ f(i)
            // f(j) = dp[j-1]* dp[i-j]
            for (int j =1; j <=i; j++){
                dp[i] = dp[i] + dp[j-1]*dp[i-j];
            }
        }
        return dp[n];
    }
}

背包问题

0-1背包问题:有N个物品,其中第i个物品的重量是weights[i],得到的价值是values[i],每个物品只能用一次,背包最多的承受重量是W。求解装入哪些物品,可以让价值最大。

显然,假设我们选取i个物品xi装入背包,要满足的约束是sum(x_i) <=Wsum(values[i])最大。

用二维数组dp[i][j]表示从下标0到i的物品里面选择,放入容量为j的背包,获得的最大价值

dp[i][j]的值表示最大价值, 下标 i表示从前面的那堆物品选择,j表示背包的容量。有n个物品,求最大价值,显然就是dp[n-1][W]

计算dp[i][j]: 它表示 从下标0到i的选择若干个物品,那么自然想到考虑物品i,它有两种状态:被选择和不被选择。

如果物品i不被选择, 那么dp[i][j]实际上是从前i-1个里面选择若干个物品,即dp[i][j] = dp[i-1][j]

而如果物品i被选择了,考虑剩下物品的价值。 对于剩下的i-1个物品,要从里面选择若干个,但此时发生背包容量发生了变化,这时的容量变为了j-weights[i],因为物品i被选择,留给剩下物品选择的容量就减少了。而这种方法的价值是: 剩下i-1个物品选择若干个物品的价值,加上物品i的价值,即dp[i][j] = dp[i-1][j- weights[i]] + values[i]

两种状态只可能有一个,因此选择较大的值,dp[i][j] = max( dp[i-1][j] , dp[i-1][j- weights[i]] + values[i] )`

初始化

二维数组初始化的边界通常是第一行和第一列。如果j=0,表示背包容量为0,那无论怎么选,背包的物品价值和都是0,因为不能放入背包。而如果i=0,即只有一个物品放入背包,对于不同的容量j, 放入这个容量背包获取的价值是固定的,如果容量比物品容量小,不能放入,价值是0, 如果容量比物品重量大,可以放入,价值始终是固定的,是该物品的价值。即

\[dp[i][0] = 0 \quad ,容量为0,不能放入,价值是0 \\ dp[0][j] = \begin{cases} 0 & if \quad j < weights[0] \\ values[0] & if \quad j >= weights[0] \end{cases} \]

数组的其他元素初始化,因为遍历过程涉及到比较,如果价值都是正数,那么可以初始化为0,但如果有负数,需要初始化为负无穷,否则在取较大值的时候有可能被初始值覆盖掉。

416 分割等和子集

给你一个 只包含正整数非空 数组 nums 。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。

输入:nums = [1,5,11,5]
输出:true
解释:数组可以分割成 [1, 5, 5] 和 [11] 

题目解读: 可以把题目看成,对于一个非空数组,从中选取一部分元素,让这部分的和与剩下的元素和相等。显然,如果数组的所有元素和是 2*target,那么要做的是从中选择若干个元素,使其和为target

转换成背包问题,有一堆物品,它的重量nums[i],选择一些放入背包,背包的容量是target,要求刚好放满。

dp[i][j]表示背包的容量为j,从0-i的物品选择中获得的价值,本题价值和容量相等,即,寻找一个dp[i][target] = target

class Solution:
    def canPartition(self, nums: List[int]) -> bool:
        n = len(nums)
        if n < 2 : return False
        target = sum(nums)
        if target %2 ==1 : return False # 和是奇数,显然也不能分成两个相等子集
        
        target = target // 2  # 比如target是100  那么 背包容量最大只用考虑101
        #dp = [ [ 0 for _ in range(target+1) ] for i in range(n) ]
        dp = [[0]* (target+1) for _ in range(n)]
        # dp[i][0] = 0
        # print(len(dp), len(dp[0]),n)
        for i in range(n):
            dp[i][0] = 0
        for j in range(0,target+1):
            dp[0][j] = 0 if j < nums[0] else nums[0]
            
        for j in range(1,target+1):
            # 对于每一种背包容量
            for i in range(1,n):
                # 考虑当前的物品能不能放进去 当前的物品重量小于等于容量j时才考虑
                if nums[i] <= j :
                	dp[i][j] = max(dp[i-1][j] , dp[i-1][j- nums[i]] + nums[i])
                else:
                    dp[i][j] =  dp[i-1][j]
        return dp[n-1][target] == target

滚动数组 0-1背包的二维遍历压缩

dp[i][j]进行二维遍历效率太低,可以压缩到1维,使用dp[j]表示从物品中选取若干个的最大价值,递推公式如下,分别对应着选择当前物品和不选择当前物品的情况。 \(dp[j] = max(dp[j] , dp[j-weights[i]] + values[i])\)

一维背包dp[j]的计算依赖于values[i],因此从前往后遍历计算,会出现背包重复利用的问题,对j的遍历要从最大的开始,倒序遍历。外层是物品,内层是容量的遍历。

class Solution:
    def canPartition(self, nums: List[int]) -> bool:
        n = len(nums)
        if n < 2 : return False
        target = sum(nums)
        if target %2 ==1 : return False # 和是奇数,显然也不能分成两个相等子集
        
        target = target // 2  # 比如target是100  那么 背包容量最大只用考虑100
        dp = [0]* (target+1)
        
        for i in range(n):
            j = target
            while j >= nums[i]:
                dp[j] = max(dp[j], dp[j-nums[i]] + nums[i])
                j = j -1
        return dp[target] == target

1049 最后一块石头的重量

有一堆石头,每块石头的重量都是正整数。

每一回合,从中选出任意两块石头,然后将它们一起粉碎。假设石头的重量分别为 x 和 y,且 x <= y。那么粉碎的可能结果如下:

如果 x == y,那么两块石头都会被完全粉碎;如果 x != y,那么重量为 x 的石头将会完全粉碎,而重量为 y 的石头新重量为 y-x。最后,最多只会剩下一块石头。返回此石头最小的可能重量。如果没有石头剩下,就返回 0。

可以将问题拆解为: 分成两堆,然后两堆相撞,剩下的石头重量最小。即416的变体,石头总重量是sum, 要求分成两堆,尽可能接近sum/2,那么相撞后的结果会更小。

dp[j]表示容量为j的情况下的最大价值,j是最接近sum/2的,把j的最大值记作target, 那么dp[target]即表示分出的一堆,它的价值(重量最大),两堆都是这种分法,两堆的差是 sum - 2* dp[target]

class Solution:
    def lastStoneWeightII(self, stones: List[int]) -> int:
        n = len(stones)
        if n == 2: return abs(stones[0]- stones[1])
        sum_ = sum(stones)  # 所有的价值和 
        sum2 = sum_
        sum_ = sum_ //2  # 如果和是个奇数,比如 13, 结果就是 6
        dp = [0]*(sum_ +1)  # dp初始化为0  最大下标可以达到 sum_
        dp[0] = 0
        for i in range(n): # 遍历物品
            for j in range(sum_, stones[i]-1, -1):
                # j要从 最大值遍历到 当前物品的重量
                dp[j] = max(dp[j],  dp[j-stones[i] ] + stones[i])
       return sum2 - dp[sum_] *2

494 目标和

给定一个非负整数数组,a1, a2, ..., an, 和一个目标数,S。现在你有两个符号 + 和 -。对于数组中的任意一个整数,你都可以从 + 或 -中选择一个符号添加在前面。

返回可以使最终数组和为目标数 S 的所有添加符号的方法数。

可以将问题分解为: 一个非负整数数组, 将它分为两组, 一组用加号, 一组用减号。记加号那组的整数的和是left,减号那组的数的和是 right,显然 left + right = sum是固定的,是整个数组的和。现在要求的是left - right = S,而 S已知,带入left - right = left - (sum - left) = S, 化简得到 2*left = sum + S,即要找到一组数,它们的和为left,且满足2*left = sum + S,显然,如果sum + S是奇数,必然不可能存在这样一组数,问题的结果是0。

问题转换为: 在数组中寻找一些数,它们的和是 (sum + s) /2

dp[j]表示,填满一个容量为j的背包的方法数,这里的容量就是找到的数的和。最终就是 dp[target],找一组数,和为target。dp[0] = 1, 背包容量为0 ,装满它只有一种方法,就是装 0件物品。

要求的是对容量j的组装方法,因此每次更新dp[j]不是选较大值,而是相加,因为是两种不同的方法(对当前物品选和不选)。

class Solution:
    def findTargetSumWays(self, nums: List[int], target: int) -> int:
        sum_ = sum(nums)  # 数组和
        if (sum_ + target) %2 == 1 : return 0  #如果是 奇数 不可能存在这样的组合
        target = (sum_ + target ) //2  # 要找的一组数 和为target
        dp = [0]*(target + 1)  
        dp[0] = 1 
        for i in range(len(nums)):
            # 对每一个物品
            for j in range( target+1, nums[i]-1, -1):
                # 对每一种 背包容量  倒着遍历
                dp[j] = dp[j] + dp[j - nums[i]] 
        return dp[target]
        

474 1和0

给你一个二进制字符串数组 strs 和两个整数 m 和 n 。请你找出并返回 strs 的最大子集的大小,该子集中 最多 有 m 个 0 和 n 个 1 。如果 x 的所有元素也是 y 的元素,集合 x 是集合 y 的 子集 。

输入:strs = ["10", "0001", "111001", "1", "0"], m = 5, n = 3

输出:4
解释:最多有 5 个 0 和 3 个 1 的最大子集是 {"10","0001","1","0"} ,因此答案是 4 。
其他满足题意但较小的子集包括 {"0001","1"} 和 {"10","1","0"} 。{"111001"} 不满足题意,因为它含 4 个 1 ,大于 n 的值 3 。

背包有两个维度·mn,不同长度的字符串就是物品。

dp[i][j]表示最多有i个0和j个1的最大子集的大小。

对于第k个物品,计算 dp[i][j]的公式:

dp[i][j] = max( 1 + dp[i - cost_zero[k]][j - cost_one[k]] , dp[i][j]) if i >= cost_zero[k] and j >= cost_one[k]

class Solution:
    def findMaxForm(self, strs: List[str], m: int, n: int) -> int:
        dp =[[0 for _ in range(n+1)] for _ in range(m+1)]  # dp[m][n]  m表示0的个数 n表示1的个数
        for string in strs:
            # 对每一个字符串
            num_0 = string.count("0")
            num_1 = len(string) - num_0   # 分别计算 0 和 1的数量 
            for i in range(m,num_0-1,-1):
                for j in range(n,num_1-1, -1):
                    dp[i][j] = max(dp[i][j], dp[i-num_0][j-num_1] + 1)  # dp[i-num_0][j-num_1] + 1 表示当前的这个字符串被选中,剩下的是dp[i-num_0][j-num_1],表示剩下那部分的子集大小,加上当前的1个,就是最终的
        return dp[m][n]

完全背包

完全背包问题中,每种物品有无数个,01背包的逻辑如下,倒序遍历背包容量是为了防止重复,因为每种物品被添加一次。完全背包物品可以添加多次,那么就可以从小到大遍历。

for i in range(n): # 遍历物品
    for j in range(m, nums[i] - 1, -1): # 倒序遍历背包容量
        dp[j] = max(dp[j],  dp[j - nums[i] ] + values[i])  # 当前的容量j  对于物品 可以选择放和不放

对于完全背包,ij的顺序无所谓,物品和背包容量的遍历顺序不会影响结果。不过一般物品在外层循环,背包容量在内层循环。

518 零钱兑换2

给定不同面额的硬币和一个总金额。写出函数来计算可以凑成总金额的硬币组合数。假设每一种面额的硬币有无限个。

输入: amount = 5, coins = [1, 2, 5]
输出: 4
解释: 有四种方式可以凑成总金额:
5=5
5=2+2+1
5=2+1+1+1
5=1+1+1+1+1

class Solution:
    def change(self, amount: int, coins: List[int]) -> int:
        n = len(coins)  # 物品数量
        dp = [0] *(amount + 1)
        for i in range(n):
            
            for j in range(coins[i], amount + 1):
                dp[j] = dp[j - coins[i]] + dp[j]  # 求组合数
        return dp[amount] 

01 背包问题:
最基本的背包问题就是 01 背包问题:一共有 N 件物品,第 i(i 从 1 开始)件物品的重量为 w[i],价值为 v[i]。在总重量不超过背包承载上限 W 的情况下,能够装入背包的最大价值是多少?

完全背包问题:
完全背包与 01 背包不同就是每种物品可以有无限多个:一共有 N 种物品,每种物品有无限多个,第 i(i 从 1 开始)种物品的重量为 w[i],价值为 v[i]。在总重量不超过背包承载上限 W 的情况下,能够装入背包的最大价值是多少?
可见 01 背包问题与完全背包问题主要区别就是物品是否可以重复选取。

背包问题具备的特征:
是否可以根据一个 target(直接给出或间接求出),target 可以是数字也可以是字符串,再给定一个数组 arrs,问:能否使用 arrs 中的元素做各种排列组合得到 target。

背包问题解法:
01 背包问题:
如果是 01 背包,即数组中的元素不可重复使用,外循环遍历 arrs,内循环遍历 target,且内循环倒序:

完全背包问题:
(1)如果是完全背包,即数组中的元素可重复使用并且不考虑元素之间顺序,arrs 放在外循环(保证 arrs 按顺序),target在内循环。且内循环正序。
(2)如果组合问题需考虑元素之间的顺序,需将 target 放在外循环,将 arrs 放在内循环,且内循环正序。

0-1背包问题 : 用dp[j]表示,目标target为j的情况。遍历的时候,物品在外面,target在里面循环,并且内循环是倒序的。

完全背包问题, 如果是纯完全背包,顺序无所谓。纯完全背包,是能否凑成目标。

遍历的顺序,与要求的问题有关。

组合:求方法数,这种不管物品的顺序,即(1,5)和(5,1)是同一种情况。而排列要关心顺序,(1,5)和 (5,1)是两种不同的排列。求排列的数量,要把 target在外面, 物品放在里面,这样对于同一个j的计算,里面遍历物品,就考虑到了顺序,(1,5)和(5,1)被认为不同的方法。

而求组合数,不考虑物品的顺序, 即要把(1,5)和(5,1)看做一种情况, 就必须物品在外层,目标target在里面。

求装满背包有几种方法,或者类似的刚好为j的方法数量,递推公式都是dp[j] = dp[j] + dp[j - nums[i]] ,分别表示 当前选择和不选该物品的方法。

求最大价值,或者最小花费这种问题,递推公式都是dp[j] = max(dp[j], dp[j - nums[i]] + values[i]),分别表示选择该物品的价值,与不选该物品的价值,结果是二者的较大者。或者花费选二者较小值。

377组合数量

给你一个由 不同 整数组成的数组 nums ,和一个目标整数 target 。请你从 nums 中找出并返回总和为 target 的元素组合的个数。

输入:nums = [1,2,3], target = 4
输出:7
解释:
所有可能的组合为:
(1, 1, 1, 1)
(1, 1, 2)
(1, 2, 1)
(1, 3)
(2, 1, 1)
(2, 2)
(3, 1)
请注意,顺序不同的序列被视作不同的组合。

虽然题目是求组合,但认为顺序不同的序列是不同的方法,因此实际是个排列问题。动归时候,先遍历目标,后遍历物品。

class Solution {
    public int combinationSum4(int[] nums, int target) {
        Arrays.sort(nums);
        int n = nums.length;
        int[] dp = new int[target +1]; //dp[i]表示 凑成i 有这么多种方法
        dp[0] = 1;
        
        for (int j = 0; j < target + 1; j++){
            for (int i = 0; i < n; i++){
                if (nums[i] <= j ){  // 只有整数比 要组成的和小的情况下才需要进行判断。 因此事先排好序,如果当前比j大,后面的肯定更大,当前j的计算可以直接break
                    dp[j] = dp[j] + dp[j - nums[i]];
                }
                else{
                    break;
                }
            }
        }
        return dp[target];
    } 
}

322 最小的零钱数量

给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1。你可以认为每种硬币的数量是无限的。

和之前的问题类似,定义dp[j]表示凑成j的金额的最小数量, 要求的最小的, 对于当前遍历的硬币(物品),也是由选和不选两种情况,不选,硬币个数是 dp[j],选了凑成的数量是 dp[j - nums[i] ] + 1 , 加一是因为当前选择的硬币数量,总数量等于剩下j-nums[i]金额的数量加上当前这个硬币。结果选择二者较小值。

初始化: 由于是选择较小值,因此初始化给一个较大的值,但dp[0]=0,因为凑成0元需要的硬币数量是0,而对于其他,可以直接用最大整数表示。

遍历顺序:完全背包问题,求得是组合,不用管顺序,物品在外面,目标在里面。

class Solution {
    public int coinChange(int[] coins, int amount) {
        
        int[] dp = new int[amount + 1] ;  // dp[i]表示凑到 i 需要的最少硬币数量
        int n = coins.length;  // n个 硬币
        // for (int i =0; i <n; i++){
        //     dp[i] = Max
        // }
        Arrays.fill(dp, Integer.MAX_VALUE);
        dp[0] = 0;
        for(int i = 0; i < n; i ++ ){
            for (int j = coins[i]; j < amount + 1; j ++ ){
                dp[j] = Math.min(dp[j], dp[j -coins[i] ] + 1);
            }
        }

        if(dp[amount] == Integer.MAX_VALUE){
            return -1;
        }
        else {return dp[amount];}
        
    }
}

279 完全平方数

给定正整数 n,找到若干个完全平方数(比如 1, 4, 9, 16, ...)使得它们的和等于 n。你需要让组成和的完全平方数的个数最少。

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

完全平方数 是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,1、4、9 和 16 都是完全平方数,而 3 和 11 不是。

遍历顺序: 只需要管,组成的完全平方和的个数,与顺序无关。物品和背包的顺序都无所谓。

class Solution {
    public int numSquares(int n) {
        int[] dp = new int[n+1];  // dp[i] 表示 找到一组数的和为i ,最少的数的情况 
        Arrays.fill(dp,Integer.MAX_VALUE);   //求最小值 因此要初始化为很大的数
        dp[0] = 0;  
        for (int i =1; i*i <=n; i++){
            for (int j =1; j <=n; j++){
                if (j >= i*i){
                    dp[j] = Math.min(dp[j],  dp[j - i*i] + 1);  // 求最小,选择两种情况下的较小者
                }
            }
        }

        return dp[n];
    }
}

139 单词拆分

给定一个非空字符串 s 和一个包含非空单词的列表 wordDict,判定 s 是否可以被空格拆分为一个或多个在字典中出现的单词。

说明 : 拆分时可以重复使用字典中的单词。 你可以假设字典中没有重复的单词。

输入: s = "leetcode", wordDict = ["leet", "code"]
输出: true
解释: 返回 true 因为 "leetcode" 可以被拆分成 "leet code"。

假设用dp[i]表示前i个字符串能否被拆分,即s[0:i-1]这个子串是否能被拆分。

子串能被拆分的条件: 依次遍历s[0: i-1] ,当前遍历到 j,把这个子串分为两部分,即s[0:j]s[j : i-1] ,只需要分别判断两个部分能否被拆分,当且仅当两个部分都能被拆分的情况下, dp[i]才为真,同时,在循环过程中,找到一个情况。就可以结束,进入下一个i的判断。

而对于这两个部分,前者s[0:j]实际就是dp[j],后者只需判断有没有在字典出现过即可。

class Solution:
    def wordBreak(self, s: str, wordDict: List[str]) -> bool:
        n = len(s)
        dp = [False] * (len(s) + 1)
        dp[0] =True  # 要求的是 dp[n]   dp[i] 表示 前i个字符可以被拆分

        for i in range(1,n+1):
            #  i从1 到 
            for j in range(0, i+1):
                # j 把 前i个分成两部分  
                tmp = s[j:i]  # 这个是后面那串
                if (tmp in wordDict) and dp[j]:
                    dp[i] = True
                    break 

        return dp[n]

背包递推公式:

能否装满(或者最多装多少个) dp[j] = max( dp[j], dp[j - nums[i]] + nums[i]) 416和1049题

装满背包有多少种方法 dp[j] = dp[j] + dp[j - nums[i]] 494 518 377题

装满背包的最大价值 dp[j] = max(dp[j], dp[j- nums[i]] + values[i]) 474 题

装满背包所有物品的最小个数 dp[j] = min(dp[j], dp[j - nums[i]] + 1) 322和279题

遍历顺序:

0-1背包问题: 二维数组先遍历物品或先遍历背包都可以,第二层循环从小到大。 一维则要先物品后背包,而里面循环是从大到小。

完全背包问题:求组合数,与顺序无关,外层物品内层背包。

求排列数,外层背包,内层物品。

740 删除能获得的最大点数

给你一个整数数组 nums ,你可以对它进行一些操作。

每次操作中,选择任意一个 nums[i] ,删除它并获得 nums[i] 的点数。之后,你必须删除每个等于 nums[i] - 1 或 nums[i] + 1 的元素。

开始你拥有 0 个点数。返回你能通过这些操作获得的最大点数。

对任意一个数x,如果x被选择了,那么必须删除每个 等于 x-1或者x+1的数,即把全部x-1x+1去除。这样,下一次还可以继续选择x,因为已经没有x的相邻值了,就可以把x全部选完,获得的点数是x*count(x),其中count(x)x的出现点数。

因此,首先对数组统计每个元素的出现次数。之后只需对这些不重复的key进行遍历。

dp[i]表示从第一个到keys[i]可以获得的最大点数。可以获得的最大点数。如果有nums中一共n个不重复元素(即keys的大小),最终要求的是dp[n-1]

依次遍历keys(即这些不重复的元素),假设当前遍历到keys[i],对于这个值有两种情况。如果他和前一个是相邻的,二者就不能同时选择。

keys[i] == keys[i-1] + 1的情况,因为keys经过排序,左边的必然比当前小。如果二者相邻,两个数不能同时选择,最终dp[i]是各选一个的较大值。即:如果不选择keys[i]这个元素之前已经获得的最大点数是dp[i-1];如果选择keys[i]这个元素,之前可以获得最大点数是0~ i-2区间取得的,是dp[i-2],而选择keys[i],点数可以增加keys[i] \* count[keys[i]] 。两种情况取较大值,dp[i] = max(dp[i-1], dp[i-2] + keys[i] \* count[keys[i])

keys[i] != keys[i-1] + 1的情况,这说明前一个和当前的都可以被选择,在keys[i]之前获得的点数是dp[i-] ,而选择keys[i],点数可以增加keys[i] * count[keys[i]],因此这种情况的点数是dp[i-1] + keys[i] \* count[keys[i]]

初始化:由于递推公式涉及到i-2,因此要初始化dp[0]dp[1] ,其中dp[0]=keys[0]\*count[keys[0]],表示只选择第一个元素时可以获得的点数,显然只有一种情况。而dp[1]要看前后的大小。和递推是一样的。

class Solution:
    def deleteAndEarn(self, nums: List[int]) -> int:
        from collections import OrderedDict,Counter
        map_dict = OrderedDict()
        nums.sort()  # 数组排序
        for num in nums:
            if num in map_dict.keys():
                map_dict[num] += 1 
            else:
                map_dict[num] = 1
        keys = map_dict.keys()
        keys = sorted(keys)
        n = len(keys)
        dp = [0]*(n)  # dp[i]表示 [0:i]可以获得的最大点数
        dp[0] = keys[0] * map_dict[keys[0]]
        if n == 1: return dp[0]
        if keys[0] == keys[1] - 1:
            dp[1] = max(dp[0], keys[1] * map_dict[keys[1]])
        else:
            dp[1] = dp[0] +  keys[1] * map_dict[keys[1]]

        # print(keys)
        # print(map_dict)
        for i in range(2,n):
            if keys[i-1] == keys[i] - 1:
                # 如果当前的和前一个数是相邻的 那么选择不选当前 是dp[i-1]
                # 选择当前 获得是dp[i-2] + 当前的点数
                #print(dp[i-1],  map_dict[i] , keys[i])
                dp[i] = max(dp[i-1],  dp[i-2] + map_dict[keys[i]] * keys[i])
            else:
                dp[i] = dp[i-1] + keys[i] * map_dict[keys[i]]

        return dp[n-1]

打家劫舍系列

198 打家劫舍1

你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。

给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。

dp[i]表示前i个房间能偷到的最大收益。不触动报警装置,即相邻的房屋不能同时选择。假设当前遍历到nums[i],那么对于这个房间,有选择和不选择两种情况。选择nums[i],那么不能选择nums[i-1],取得的收益是dp[i-2] + nums[i] ,而不选择nums[i],取得的收益是dp[i-1],因此二者选较大的那个,dp[i] = max(dp[i-1], dp[i-2] + nums[i])

初始化:由于涉及i-2,因此要初始化i=0,1两种情况,dp[0] = nums[0],dp[1] = max(nums[0], nums[1])

class Solution {
    public int rob(int[] nums) {
        int n = nums.length;
        if (n==1) {return nums[0]; }
        if (n==2) {return Math.max(nums[0], nums[1]);} 

        int dp[] = new int[n]; //dp[i]表示0-i能偷到的最大值
        dp[0] = nums[0];
        dp[1] = Math.max(nums[0], nums[1]);
        for(int i=2 ; i<n ;i++){
            dp[i] = Math.max(dp[i-2] + nums[i], dp[i-1]);
        }
        return dp[n-1];
    }
}

213 打家劫舍2 首尾相连

在198的基础上,房间是首尾相连的,不能同时选择两个相邻房间。

可以将分为不相连的情况: 如果选第一个房子,可以偷窃的房屋是0-i-2这些序号,而不选第一个,可以偷窃的房屋是1-i-1的序号,从而将问题变为198的情况。

因此,只需要看这两种,哪种能获得更大的金额即可。

class Solution {
    public int rob(int[] nums) {
        int n = nums.length;  
        return Math.max(rob_max(nums,0,n-2) ,rob_max(nums,1,n-1));
    }
    // 定义rob_max函数,功能是 从 start到 end区间能取得的最大值
    public int rob_max(int[] nums, int start, int end){
        // 从start到end偷窃的最大金额
        int[] dp = new int[nums.length] ; // dp[i]

        dp[start] = nums[start];  // 区间获得第一个
        dp[start+1] = Math.max(nums[start],nums[start+1]);
        for(int i = start+2; i <= end; i++){
            dp[i] = Math.max(dp[i-1], dp[i-2] + nums[i]);
        }
        return dp[end];

    }
}

337 打家劫舍3 二叉树

在上次打劫完一条街道之后和一圈房屋后,小偷又发现了一个新的可行窃的地区。这个地区只有一个入口,我们称之为“根”。 除了“根”之外,每栋房子有且只有一个“父“房子与之相连。一番侦察之后,聪明的小偷意识到“这个地方的所有房屋的排列类似于一棵二叉树”。 如果两个直接相连的房子在同一天晚上被打劫,房屋将自动报警。

计算在不触动警报的情况下,小偷一晚能够盗取的最高金额。

输入: [3,2,3,null,3,null,1]
     3
    / \
   2   3
    \   \ 
     3   1
输出: 7 
解释: 小偷一晚能够盗取的最高金额 = 3 + 3 + 1 = 7.

和213不同的是,这次是不能选择相邻的两个节点,即不能在选择父节点的时候,选择它的两个子节点。

对于任意一个节点i,假设用dp[i]表示这个节点及其子树能打劫到的最大值。 分析: 对于这个节点而言,有两种选择:打劫和不打劫。比较简单是不打劫,如果不打劫,两个子节点都可以选,获得的收益是dp[ i.left ] + dp[i.right] ,表示左边子树的收益和右边子树的收益和。而如果打劫这个节点,获得的收益是dp[i] + dp[i.left.left] + dp[i.left.right] + dp[i.right.left ] + dp[i.right.right] ,表示选取节点i的收益和i的孙子节点的收益,孙子 一共四个,即左节点的左右节点和右节点的左右节点。

另外,如果直接使用会超时,因为涉及大量重复运算。比如在根节点,第一种方式的时候,递归调用计算root.left.left,而在第二种情况,有可能又会计算到这个节点,造成大量冗余,因此可以使用一个map记录已经访问过的节点的最大收益,从而加速。

class Solution:
    meme = {}
    def rob(self, root: TreeNode) -> int:
        # rob表示 打劫节点root 可以获取的收益
        if not root: return 0
        
        if root in Solution.meme.keys():
            return Solution.meme[root]
        # 对于节点 都有两种情况
        # 情况1  表示选择这个节点 那么不能选择儿子节点,但是可以选择孙子节点
        gain_1 = root.val   
        if  root.left:  # 如果左孩子不空 计算左边子树可能的收益
            gain_1 = gain_1 + self.rob(root.left.left) + self.rob(root.left.right)
        if root.right:
            gain_1 = gain_1 + self.rob(root.right.left) + self.rob(root.right.right)
        
        # 第二种情况 不选择这个节点,那么和是左右儿子节点的收益和
        gain_2 = self.rob(root.left) + self.rob(root.right)
        
        # 计算完,把当前节点的最大收益记录下来
        Solution.meme[root] = max(gain_1, gain_2)
        return max(gain_1, gain_2)

股票交易

121 买卖股票的最佳时机

给定一个数组 prices ,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。

你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。

返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0 。

输入:[7,1,5,3,6,4]
输出:5
解释:在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。
     注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格;同时,你不能在买入前卖出股票。

image

求获得的最大利润,即在第i天买入,第j天卖出,利润nums[j] - nums[i]的最大值。显然两次遍历即可,i从第一天到最后一天,ji+1到最后一天。

方法2 : 假设在第i天卖出, 那么必然在第0 - i-1天中的某天买入。现在卖出的价格是nums[i],是固定的,要让利润最大化,显然应该在0 - i-1这个区间的最低价格那一天买入。 这样第i天卖出的利润就最大了,遍历i,寻找哪一天卖出的利润最大即可。

在计算之前,要预先找到第i天之前的最小值,即买入的合适时间。

class Solution:
    def maxProfit(self, prices: List[int]) -> int:
        min_price = [prices[0]] # 第一个是0  表示在第0天之前没有可以买入的
        n = len(prices)
        for i in range(1,n):
            if prices[i] < min_price[-1] :
                min_price.append(prices[i])
            else:
                min_price.append(min_price[-1])
        # min_prices 记录了 第i个位置之前的最小值  min_price[i]  表示 i这个位置之前的最小值
        max_val = 0
        for i in range(1,n):
            if (prices[i] - min_price[i]) > max_val:
                max_val = prices[i] - min_price[i]
        return max_val

简化代码。一边遍历一边寻找最小值,然后记录最大收益。

class Solution:
    def maxProfit(self, prices: List[int]) -> int:
        
        n = len(prices)
        min_price = prices[0]  # 最小的价值 可以选择在这一天卖出
        # min_prices 记录了 第i个位置之前的最小值  min_price[i]  表示 i这个位置之前的最小值
        max_val = 0
        for i in range(1,n):
            # 在i这天卖出,卖出的价格是 prices[i]  在之前的区间里最低的买入价格 
            min_price = min(min_price, prices[i-1])
            max_val = max(max_val, prices[i] - min_price )
        return max_val

动态规划,对于第i天,总有两种状态,持有股票和不持有股票,分别用01表示。持有的意思是 当天手上有股票, 而不是当天买入,可能在之前的某一天买入股票。

i天如果持有股票, dp[i][0]由两个状态变化而来,前一天没有股票,今天买入,和 前一天手上就有了股票。

dp[i][0] = max (dp[i-1][0] , -prices[i]) dp[i-1][0]表示前一天就有股票,而-prices[i]表示前一天还没有股票,而今天买入,那手上的钱是 -prices[i]。 手上的钱一开始是0,如果买入,显然变为负数,因此这里是-prices[i]

dp[i][1]的推导,它表示第i天手上没有股票的状态,可能是 还没有买入, 或者 在第i天卖出。

dp[i][1] = max(dp[i-1][1] , prices[i] + dp[i-1][0])

初始化: dp[0][0]表示最开始持有股票,显然是买入了,dp[0][0] = -prices[0],而dp[0][1]=0,表示第0天还没有买入,手上的钱是0

class Solution:
    def maxProfit(self, prices: List[int]) -> int:
        n = len(prices)
        dp = [[0,0] for _ in range(n)]
        dp[0][0] = -prices[0]
        dp[0][1] = 0
        for i in range(1,n):
            dp[i][0] = max (dp[i-1][0] ,  -prices[i])
            dp[i][1] = max(dp[i-1][1] ,  prices[i] + dp[i-1][0])  # 不持有的话
  
        return max(dp[n-1])  # 最后一天,持有的状态钱多还是不持有的钱多。 实际上必然是dp[n-1][1]更大

122 股票问题 多笔交易

给定一个数组 prices ,其中 prices[i] 是一支给定股票第 i 天的价格。

设计一个算法来计算你所能获取的最大利润。你可以尽可能地完成更多的交易(多次买卖一支股票)。

注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。

输入: prices = [7,1,5,3,6,4]
输出: 7
解释: 在第 2 天(股票价格 = 1)的时候买入,在第 3 天(股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。
     随后,在第 4 天(股票价格 = 3)的时候买入,在第 5 天(股票价格 = 6)的时候卖出, 这笔交易所能获得利润 = 6-3 = 3 。

在121的基础上,可以进行多次交易。

类似的,用dp[i][0]表示第i天持有股票的利润,dp[i][1]表示第i天不持有股票的利润。

但是递推公式和121有所不同。

如果第i天持有股票: 可以是之前就有股票了, 或者在第i天进行买入。

  • 之前就有股票,那么当天不卖出,利润是 dp[i-1][0]

  • 之前没有股票,当前买入,利润会减少 prices[i], 但是原来的钱不是0(在121中买入之前的钱是0),因为有可能在第i天之前已经完成一次交易,因此这种情况,总的利润是 -prices[i] + dp[i-1][1] ,在121中,这个情况下的dp[i-1][1]必然是0

如果第i天不持有股票,也是两种情况: 之前没有股票,或者之前有,第i天卖出去

  • 之前没有股票,当天也没有买入,利润是 dp[i-1][1]
  • 之前有股票,当天卖出,卖出的钱是 prices[i],总的利润是 prices[i] + dp[i-1][0]
class Solution:
    def maxProfit(self, prices: List[int]) -> int:
        n = len(prices)
        dp = [[0,0] for _ in range(n)]
        dp[0][0] = -prices[0]
        dp[0][1] = 0
        for i in range(1,n):
            dp[i][0] = max (dp[i-1][0] ,  -prices[i] + dp[i-1][1])  # 当天有股票
            dp[i][1] = max(dp[i-1][1] ,  prices[i] + dp[i-1][0])  # 不持有的话
  
        return max(dp[n-1])  # 最后一天,持有的状态钱多还是不持有的钱多。 实际上必然是dp[n-1][1]更大

123 股票问题 最多两次交易

给定一个数组,它的第 i 个元素是一支给定的股票在第 i 天的价格。设计一个算法来计算你所能获取的最大利润。你最多可以完成 两笔 交易。注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。

相比122的多次交易,如果限制了交易次数,最多两次,更加复杂,可以交易一次,或者交易两次。

对于第i天而言,它的状态就不是持有或者不持有两种情况,分为 五种:,用dp[i][j]表示第i天的钱。

  • dp[i][0] 表示没有进行操作
  • dp[i][1] 表示第一次买入的状态,但并不是值第i天买入,而是指此时处于第一次买入的状态,可能是i或者前面某一天进行了买入。
  • dp[i][2]表示第一次卖出
  • dp[i][3]表示第二次买入
  • dp[i][4] 表示第二次卖出

对于每一种分析状态变化,dp[i][0] = dp[i-1][0] ,第i天没有任何操作,钱和前一天相同。

dp[i][1] : 表示第i天的状态是第一次买入。可以分为 是在 第i天买入 和之前的某一天买入。如果是前者,第i天买入,之前的值是 dp[i-1][0] ,现在买入,剩的钱是dp[i-1][0] - prices[i]。 如果是后者,dp[i][1] = dp[i-1][1],选择二者的较大值,即有dp[i][1] = max( dp[i-1][0] - prices[i], dp[i-1][1])

同理,dp[i][2]表示当前天处于第一次卖出的状态,也可以分为当天卖出和之前某一天卖出。 如果是第i天卖出, 那么在之前处于的状态是 dp[i-1][1] ,卖出的钱是prices[i],获得的钱是 dp[i-1][1] + prices[i], 如果是之前就卖出,当前处于第一次卖出状态,说明当天没有操作,在之前卖出的钱是dp[i-1][2] ,同样取较大值,dp[i][2] = max( dp[i-1][1] + prices[i], dp[i-1][2])

dp[i][3] 表示当前处于第二次买入的状态,如果是第i天是第二次买入, 之前的状态必然是第一次卖出是dp[i-1][2] ,买入了prices[i],剩下的钱是 dp[i-1][2] - prices[i] ,而如果是之前某一天是第二次买入, 现在没有进行操作,剩余的钱是dp[i-1][3],因此有 dp[i][3] = max( dp[i-1][2] - prices[i] , dp[i-1][3] )

最后,dp[i][4] = max( dp[i-1][3] + prices[i] , dp[i-1][4] )

初始化: 第0天没有操作,钱仍然是 0, 即dp[0][0] = 0,如果第0天是第一次买入,利润是dp[0][1] = -prices[0] ,表示负数。

但如果第0天是第一次卖出,最大情况是dp[i][2] = 0

第0天是第二次买入, 手上的钱是dp[0][3] = -prices[0]

第二次卖出,最大的钱是dp[0][4] = 0

class Solution:
    def maxProfit(self, prices: List[int]) -> int:
        n = len(prices)
        dp = [[0,0,0,0,0] for _ in range(n)]
        dp[0][0] = 0  # 第一天没有操作
        dp[0][1] = -prices[0]  # 第一天买入
        dp[0][2] = 0 # 第一天卖出的利润 最大是0
        dp[0][3] = -prices[0]  # 第一天 第二次买入
        dp[0][4] = 0
        for i in range(1,n):
            dp[i][0] =dp[i-1][0] # 当天没有操作,和前一天的情况相同
            dp[i][1] = max( dp[i-1][0] - prices[i],  dp[i-1][1])  # 当天的状态是 第一次买入
            dp[i][2] = max( dp[i-1][1] + prices[i],  dp[i-1][2])  # 当天的状态是 第一次卖出
            dp[i][3] =  max( dp[i-1][2] - prices[i] , dp[i-1][3] )  # 当天的状态是第二次买入
            dp[i][4] = max( dp[i-1][3] + prices[i] , dp[i-1][4] )  # 当天处于的状态是第二次卖出
  
        return max(dp[n-1])  # 最后一天,持有的状态钱多还是不持有的钱多。 实际上必然是dp[n-1][1]更大

188 股票问题 最多k次交易

给定一个整数数组 prices ,它的第 i 个元素 prices[i] 是一支给定的股票在第 i 天的价格。

设计一个算法来计算你所能获取的最大利润。你最多可以完成 k 笔交易。

思路和123问题一样,如果可以进行k次交易,那么在第i天的时候,可能处于的状态有 2*k+1种,其中0表示不操作,其余2*k中分别表示第j次买入和卖出(\(j \in [1,k]\))。仍然用dp[i][j]表示当前处于状态j下面的最大利润。

  • dp[i][0] 不操作,利润显然和上一次相同, dp[i][0] = dp[i-1][0]
  • dp[i][1] 第一次买入,
  • dp[i][2] 第一次卖出
  • ……..
  • dp[i][2*k-1]k次买入
  • dp[i][2*k]k次卖出

规律是,奇数下标对应的都是买入,偶数下标对应的都是卖出。

递推公式: 如果是第j次买入, 即推导dp[i][2*j -1] ,它的情况是 : 当前天i是第j次买入, 或者 在i天之前已经是 j次买入的状态。 前者的钱是 dp[i-1][2*(j-1)] - prices[i] , 表示上一次处于的状态(第 j-1次卖出)的钱,和当前买入的钱(负数的prices[i]) 。后者表明之前已经处于j次买入的状态,即dp[i-1][2*j -1]

那么有 dp[i][2*j - 1 ] = max( dp[i-1][2*(j-1) ] - prices[i], dp[i-1][2*j -1] )

而如果是第j次卖出,两种情况: 当天i进行卖出,和 在i之前已经卖出。 如果是当天进行卖出,i之前的状态就是是 j次买入。 之前进行了买入,状态和i-1相同

dp[i][2*j] = max( dp[i-1][2*j - 1] + prices[i] , dp[i-1][2*j] )

初始化: 第0天的时候,不进行操作 dp[0][0] =0,而无论是进行第几次买入,钱都是dp[0][2*j -1] = -prices[0],而无论是第几次卖出,仍然是0。

class Solution:
    def maxProfit(self, k: int, prices: List[int]) -> int:
        n = len(prices)
        if  n == 0: return 0
        dp = [[0]*(2*k+1) for _ in range(n)]

        for j in range(1,k+1):
            dp[0][2*j - 1] = -prices[0]  # 对于奇数项,买入操作,初始化都是 -prices[0]

        for i in range(1,n):
            dp[i][0] = dp[i-1][0] # 当天没有操作,和前一天的情况相同
            for j in range(1,k+1):
                dp[i][2*j - 1 ]  =  max( dp[i-1][2*(j-1) ] - prices[i],   dp[i-1][2*j -1] )
                dp[i][2*j]   =  max( dp[i-1][2*j - 1] + prices[i] ,  dp[i-1][2*j] )
  
        return max(dp[n-1])  # 最后一天,持有的状态钱多还是不持有的钱多。 实际上必然是dp[n-1][1]更大

309 股票问题 含冷冻期1天

给定一个整数数组,其中第 i 个元素代表了第 i 天的股票价格 。

设计一个算法计算出最大利润。在满足以下约束条件下,你可以尽可能地完成更多的交易(多次买卖一支股票):

你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。

卖出股票后,你无法在第二天买入股票 (即冷冻期为 1 天)。 冷冻期是指卖出的后一天,前一天卖出,今天的状态就是冷冻期

对于第i天,可以处在的状态是:

  • 我们目前持有一支股票,对应的「累计最大收益」记为dp[i][0]

  • 我们目前不持有任何股票,并且处于冷冻期中,对应的「累计最大收益」记为 dp[i][1]

  • 我们目前不持有任何股票,并且不处于冷冻期中,对应的「累计最大收益」记为 dp[i][2]

这里的「处于冷冻期」指的是在第 i天结束之后的状态。也就是说:如果第 i结束之后处于冷冻期,那么第i+1天无法买入股票。

  • 对于第一种情况,要达到这种情况,可以分为: 在i之前就已经买了股票,或者在i天才买股票。 dp[i][0] = max(dp[i-1][0], dp[i-1][2] - prices[i]) ,如果是i 天购入,那么i-1天必然是处于2状态,因为1状态的下一天不能买股票。
  • 对于dp[i][1],只有一种情况,在当前i天卖出了股票,能够卖股票,前一天必然要有股票在,因此利润是dp[i][1] = dp[i-1][0] + prices[i] ,前一天处于的状态是0,当天卖出增加了prices[i]
  • 对于dp[i][2], 表明当天没有股票,也没有冷冻期。没有任何操作,那么在i-1是卖出状态。 但无论i-1是1或者2的状态都不会影响,因此dp[i][2] = max(dp[i-1][2] , dp[i-1][1])
class Solution:
    def maxProfit(self, prices: List[int]) -> int:
        n = len(prices)
        dp = [[0,0,0] for _ in range(n)]
        dp[0][0] = -prices[0]  # 0表示当天买入
        dp[0][1] = 0   
        dp[0][2] = 0
        for i in range(1,n):
            dp[i][0] = max(dp[i-1][0],  dp[i-1][2] - prices[i]) # 第i天有股票,分为第i天买入 和之前买入 
            p[i][1] = dp[i-1][0] + prices[i]  # 表示当前处于冷冻期,, 即 在i-1天卖出 ,i-1天有股票
            dp[i][2] = max(dp[i-1][2] , dp[i-1][1])
  
        return max(dp[n-1])  # 最后一天

714 股票问题 含手续费

给定一个整数数组 prices,其中第 i 个元素代表了第 i 天的股票价格 ;非负整数 fee 代表了交易股票的手续费用。

你可以无限次地完成交易,但是你每笔交易都需要付手续费。如果你已经购买了一个股票,在卖出它之前你就不能再继续购买股票了。返回获得利润的最大值。注意:这里的一笔交易指买入持有并卖出股票的整个过程,每笔交易你只需要为支付一次手续费。

和122问题完全一致,只是在卖出的时候,增加的收益不是prices[i],而是prices[i] - fee,减去扣除的手续费才是获得的收益。

class Solution:
    def maxProfit(self, prices: List[int], fee: int) -> int:
        n = len(prices)
        dp = [[0,0] for _ in range(n)]
        dp[0][0] = -prices[0]
        dp[0][1] = 0
        for i in range(1,n):
            dp[i][0] = max (dp[i-1][0] ,  -prices[i] + dp[i-1][1])  # 当天有股票
            dp[i][1] = max(dp[i-1][1] ,  prices[i] + dp[i-1][0] -fee)  # 不持有的话 要减去手续费
  
        return max(dp[n-1])  

数组 子序列 和字符串

300 最长递增子序列

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

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

输入:nums = [10,9,2,5,3,7,101,18]
输出:4
解释:最长递增子序列是 [2,3,7,101],因此长度为 4 。

子序列,不要求连续。因此自然想到,依次遍历数组,当前的数,如果能加入到之前的最长子序列中,那么子序列长度就+1了。

dp[i]表示从第一个元素到第i个元素区间的最长子序列的长度。在位置i的地方,我们考虑前一次的最长子序列,在0-i的位置,找到那个j,让nums[i]加入以nums[j]为末尾的子序列,长度最大。即dp[i] = max(dp[j]) + 1

class Solution:
    def lengthOfLIS(self, nums: List[int]) -> int:
        n = len(nums)
        dp = [1] *n  # 任意一个位置 至少的子序列长度是1 就是只选择第 i 个元素
        res = 1
        for i in range(1,n):
            for j in range(0,i):  
                if nums[j] < nums[i]: # 如果nums[i] 比j位置大,可以选择加入j这个子序列,
                    dp[i] = max(dp[i], dp[j] + 1) # 然后看是加入j这个子序列的长度大 还是保持原样
            if res < dp[i]:  res = dp[i]
        return res

674 最长连续递增子序列

给定一个未经排序的整数数组,找到最长且 连续递增的子序列,并返回该序列的长度。

连续递增的子序列 可以由两个下标 l 和 r(l < r)确定,如果对于每个 l <= i < r,都有 nums[i] < nums[i + 1] ,那么子序列 [nums[l], nums[l + 1], ..., nums[r - 1], nums[r]] 就是连续递增子序列。

在300题的基础上,增加连续,那么只需要判断当前的nums[i]和前一个字符能否凑成一个更长的递增子序列。dp[i]表示以nums[i]结尾的数构成的连续递增子序列,它的计算方式是:如果nums[i] > nums[i-1],那么nums[i]可以加入前一个的子序列中,长度加1,即dp[i] = dp[i-1] + 1 ,否则dp[i] =1,表示nums[i]单独一个数组成子序列。而300题,nums[i]的前一个位置不是在nums[i-1],因此要在0 - i遍历,寻找这个j,可以让它和nums[i]构成更长的一个子序列。

class Solution:
    def findLengthOfLCIS(self, nums: List[int]) -> int:
        n = len(nums)
        if n<=1 : return n  # 1时 最长的是1 n=0 没有
        dp = [1] * n # dp[i]表示以当前 nums[i]结尾的最长连续递增序列
        res = 1
        for i in range(1,n):
            if nums[i] > nums[i-1]:
                dp[i] = dp[i-1] + 1
            if res < dp[i]:
                res = dp[i]
        return res

718 最长重复子数组

给两个整数数组 A 和 B ,返回两个数组中公共的、长度最长的子数组的长度。

输入:
A: [1,2,3,2,1]
B: [3,2,1,4,7]
输出:3
解释:
长度最长的公共子数组是 [3, 2, 1] 。

题目要求的公共子数组是连续的几个数。二维dp,令dp[i][j]表示以nums1[i]nums2[j]结尾的子数组长度(包含当前值)。

假设当前遍历到ij位置,这两个位置的元素相等,那么看前一个位置,dp[i-1][j-1],他表示前面的那部分子数组的长度,现在长度加1,因此是dp[i][j] = dp[i-1][j-1] + 1

初始化全部值是0,但是在边界,如果也用这个递推公式就会有问题。比如dp[0][2] = 1,而如果nums1[0] == nums2[3],那么dp[0][3] = 1 + 1 =2,但是实际上,dp[0][3] =1,因为nums1[0]只有一个元素,最长的相同部分也只能是1。因此边界要单独考虑。

但可以通过将dp设置为m+1,n+1个元素来解决这个问题。当前位置i 从1开始遍历。遍历到m+1,而要比较的对象是nums1[i-1]nums2[j-1] ,这样,即使有i=1 or j=1的情况, dp[i][j] = dp[i-1][j-1] + 1仍然成立,比如i=1 ,那么dp[i][j]计算的时候,右边那个对应的每一个 dp[0][j-1]都是0, 因为初始化的时候dp=0,而后续计算没有管这些值。

class Solution:
    def findLength(self, nums1: List[int], nums2: List[int]) -> int:
        m = len(nums1)
        n = len(nums2)
        dp = [[0]*n   for _ in range(m)]  #dp[m][n]   dp[i][j] 表示以nums1[i]结尾 nums2[j]结尾的满足子数组的长度
        res = 0
        for i in range(m):
            for j in range(n):
                if nums1[i] == nums2[j]:
                    if i==0 or j ==0 :   # 如果是i或者j为0的情况, 不适用 
                        dp[i][j] = 1  
                    else:
                        dp[i][j] = dp[i-1][j-1] + 1
                if res < dp[i][j]:  # 保留最大的那个值
                    res = dp[i][j]

        return res
    
 
# 另一种写法
class Solution:
    def findLength(self, nums1: List[int], nums2: List[int]) -> int:
        m = len(nums1)
        n = len(nums2)
        dp = [[0]*(n+1)   for _ in range(m+1)]  #dp[m][n]   dp[i][j] 表示以nums1[i]结尾 nums2[j]结尾的满足子数组的长度
        res = 0
        for i in range(1,m+1):
            for j in range(1,n+1):
                if nums1[i-1] == nums2[j-1]:  # 这里比较的是 nums[i-1] 
                    dp[i][j] = dp[i-1][j-1] + 1
                if res < dp[i][j]:
                    res = dp[i][j]

        return res

1143 字符串的最长公共序列

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

一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。

例如,"ace" 是 "abcde" 的子序列,但 "aec" 不是 "abcde" 的子序列。 两个字符串的 公共子序列 是这两个字符串所共同拥有的子序列。

输入:text1 = "abcde", text2 = "ace" 
输出:3  
解释:最长公共子序列是 "ace" ,它的长度为 3 。

子序列,不要求连续,和300题类似不连续,但方法和718相同,718要求连续,因此每次和前一个比较,而不连续就要在前面所有的部分寻找最大的那个。

class Solution:
    def longestCommonSubsequence(self, text1: str, text2: str) -> int:
        m = len(text1)
        n = len(text2)
        dp = [[0] * (n+1) for _ in range(m+1) ]  # dp[i][j]表示 到第i j位置之前的最长的子序列的长度
        
        for i in range(1,m+1):
            for j in range(1,n+1):
                if text1[i-1] == text2[j-1]:
                    dp[i][j] = dp[i-1][j-1] + 1
                else:
                    # 但是  如果字符不相同   dp[i-1][j] 或者 dp[i][j-1]这两个分别表示 i 或者j前移一位 和另一个的公共子序列部分  718为什么不这样寻找?因为dp[i][j] 只能通过dp[i-1][j-1]达到,是连续数组 
                    dp[i][j] = max(dp[i-1][j], dp[i][j-1])
        return dp[m][n]
         
                

1035 不相交的线

我们在两条独立的水平线上按给定的顺序写下 A 和 B 中的整数。

现在,我们可以绘制一些连接两个数字 A[i] 和 B[j] 的直线,只要 A[i] == B[j],且我们绘制的直线不与任何其他连线(非水平线)相交。以这种方法绘制线条,并返回我们可以绘制的最大连线数。

输入:A = [1,4,2], B = [1,2,4]
输出:2
解释:
我们可以画出两条不交叉的线,1和1相连 4和4相连或者 2和2相连。但是4-4和2-2不能同时,因为要交叉
我们无法画出第三条不相交的直线,因为从 A[1]=4 到 B[2]=4 的直线将与从 A[2]=2 到 B[1]=2 的直线相交。

实际上,要让两个数组,那些相同的数依次连起来,且不交叉,就是求一个最长的公共序列。最长公共序列就是:不连续,各个值相同的对应序列。代码和1143代码完全一致。

53 最大子序和

给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。

输入:nums = [-2,1,-3,4,-1,2,1,-5,4]
输出:6
解释:连续子数组 [4,-1,2,1] 的和最大,为 6 。

和之前的子序列长度问题一样,只是这次计算的不是长度,而是和。仍然以dp[i]表示以nums[i]结尾的连续子数组的最大和。遍历到nums[i],如果当前值和前一个的子数组和加起来更大,那就更新,否则就选择nums[i]单独作为结果。

class Solution:
    def maxSubArray(self, nums: List[int]) -> int:
        n = len(nums)
        dp = [0]*n
        dp[0] = nums[0]  # dp[0] 到第一个元素的最大子数组,只有一种情况,就是第一个元素
        for i in range(n):
            if nums[i] + dp[i] > nums[i]:  # 与nums[i]进行比较 
                dp[i] = dp[i-1] + nums[i]
        return max(dp)

392 判断是否子序列

给定字符串 s 和 t ,判断 s 是否为 t 的子序列。

字符串的一个子序列是原始字符串删除一些(也可以不删除)字符而不改变剩余字符相对位置形成的新字符串。(例如,"ace"是"abcde"的一个子序列,而"aec"不是)。

输入:s = "abc", t = "ahbgdc"
输出:true

一个很直观的解法是:两个指针i,j遍历两个字符串,在t中找到一个s的字符,就把i后移,直到找完t的所有字符,如果i已经走完s,说明是子序列,否则不是。或者还没走完t,而s已经走完,也说明是子序列。

class Solution:
    def isSubsequence(self, s: str, t: str) -> bool:
        i = 0  # i遍历s
        j = 0 # j遍历t
        m = len(s)
        n = len(t)
        if m == 0: return True  # s是空串,是任何字符串的子序列 
        while j < t:
            if t[j] == s[i]:  # 当前遍历到t[j] 判断和s[i]是不是相同 是就判断下一个s[i+1]
                i = i + 1
                if i== m :
                    return True
            j = j +1
        return False

动态规划的求解:

直接进行最长子序列的判断即可,如果st的子序列,那么他们的最长公共子序列长度一定等于s的长度。

class Solution:
    def isSubsequence(self, text1: str, text2: str) -> bool:
        m = len(text1)
        n = len(text2)
        dp = [[0] * (n+1) for _ in range(m+1) ]  # dp[i][j]表示 到第i j位置之前的最长的子序列的长度
        
        for i in range(1,m+1):
            for j in range(1,n+1):
                if text1[i-1] == text2[j-1]:
                    dp[i][j] = dp[i-1][j-1] + 1
                else:
                    # 但是  如果字符不相同   dp[i-1][j] 或者 dp[i][j-1]这两个分别表示 i 或者j前移一位 和另一个的公共子序列部分  718为什么不这样寻找?因为dp[i][j] 只能通过dp[i-1][j-1]达到,是连续数组 
                    dp[i][j] = max(dp[i-1][j], dp[i][j-1])
        if dp[m][n] == m:  # 判断最长公共子序列的长度是不是m
            return True
        else:
            return False
         

115 子序列的出现次数

给定一个字符串 s 和一个字符串 t ,计算在 s 的子序列中 t 出现的个数。

输入:s = "rabbbit", t = "rabbit"
输出:3
解释:
如下图所示, 有 3 种可以从 s 中得到 "rabbit" 的方案。
(上箭头符号 ^ 表示选取的字母)
rabbbit
^^^^ ^^
rabbbit
^^ ^^^^
rabbbit
^^^ ^^^

仍然是动态规划,用dp[i][j]表示的是各自对应长度上 ,子序列的出现个数。

从1开始遍历,考虑位置前面的部分。如果s[i-1]=t[j-1] , dp[i][j]表示的是 t[0] --- t[j-1]这个子串在 s[0]---s[i-1]里面的出现次数,现在已经有最后一个字符对应上了。那么考虑剩下的,一种情况是dp[i-1][j-1] , 但还有一种情况,dp[i-1][j],这是两种不同的情况。前者,表示使用这个匹配上的s[i]==t[j],考虑剩下的字符关系,后者则是不管这个,直接在s[0]--s[i-2]里面进行全部t的匹配。

(下图来自https://leetcode-cn.com/problems/distinct-subsequences/solution/shou-hua-tu-jie-xiang-jie-liang-chong-ji-4r2y/)

img

class Solution:
    def numDistinct(self, s: str, t: str) -> int:
        m = len(s)  # s中 t的出现次数 那么 m一定要比t大
        n = len(t)
        if m < n : return 0
        dp = [[0]*(n+1) for _ in range(m+1)]
        # dp[i][0]  表示 空串的出现次数, 都是1
        for i in range(m+1):
            dp[i][0] = 1
        # dp[0][j] 表示 t中的那部分 在 空串s中的出现次数 肯定是0
        for i in range(1,m+1):
            for j in range(1,n+1):
                if s[i-1] == t[j-1] :
                    dp[i][j] = dp[i-1][j-1] + dp[i-1][j]  #  
                else:
                    dp[i][j] = dp[i-1][j]
         return dp[m][n]

583 两字符串变相同的最小删除步数

给定两个单词 word1word2,找到使得 word1word2 相同所需的最小步数,每步可以删除任意一个字符串中的一个字符。

输入: "sea", "eat"
输出: 2
解释: 第一步将"sea"变为"ea",第二步将"eat"变为"ea"

dp[i][j] 表示word1[0]到word1[i-1](包含它)word2[0]到word2[j-1](包含他)变为相同所需要的最小步数。

从1开始遍历,当前遍历到dp[i][j], 如果word1[i-1] == word2[j-1]的话,前面一步的步数是dp[i-1][j-1],这一步不需要操作,因此相同的时候dp[i][j] = dp[i-1][j-1]

如果两个不同,三种情况: 如果删除word1[i-1],总的操作步数:dp[i-1][j] + 1 ,dp[i-1][j]表示word1[0: i-2]变成word2[0: j-1]的最小步数。因为word1[i-1]被删除,那就只能看word前面的部分,变为对应的字符word2需要的步数,因此是dp[i-1][j]

情况2: 如果删除word2[j-1],总的步数是dp[i][j-1] + 1

情况3:如果两个都删除,总的步数是dp[i-1][j-1] + 2

最终选择的是三个情况的最小值。

初始化: dp[0][j],表示word1是空的,word2要删除多少步变为和word1相同,步数就是j ,同理,dp[i][0]= i,将word1变成空,删除的步数是它的长度。

class Solution:
    def minDistance(self, word1: str, word2: str) -> int:
        m = len(word1)
        n = len(word2)
        dp = [[0]*(n+1) for _ in range(m+1)]
        for i in range(m+1):
            dp[i][0] = i
        for j in range(n+1):
            dp[0][j] = j
        for i in range(1,m+1):
            for j in range(1,n+1):
                if word1[i-1] == word2[j-1]:
                    dp[i][j] = dp[i-1][j-1]
                else:
                    dp[i][j] = min( dp[i-1][j]+1,  dp[i][j-1] +1,  dp[i-1][j-1] + 2)
        return dp[m][n]

72 编辑距离 两个单词变为相同的操作数

给你两个单词 word1 和 word2,请你计算出将 word1 转换成 word2 所使用的最少操作数 。

你可以对一个单词进行如下三种操作:

插入一个字符,删除一个字符,替换一个字符

按照题目,实际上的操作只有三种,两个字符串A,B。

对单词 A 删除一个字符和对单词 B 插入一个字符是等价的。例如当单词 A 为 doge,单词 B 为 dog 时,我们既可以删除单词 A 的最后一个字符 e,得到相同的 dog,也可以在单词 B 末尾添加一个字符 e,得到相同的 doge;

同理,对单词 B 删除一个字符和对单词 A 插入一个字符也是等价的;

对单词 A 替换一个字符和对单词 B 替换一个字符是等价的。例如当单词 A 为 bat,单词 B 为 cat 时,我们修改单词 A 的第一个字母 b -> c,和修改单词 B 的第一个字母 c -> b 是等价的。

因此总共的操作是: 对A进行删除,对B进行删除,对A进行替换。

输入:word1 = "horse", word2 = "ros"
输出:3
解释:
horse -> rorse (将 'h' 替换为 'r')
rorse -> rose (删除 'r')
rose -> ros (删除 'e')

dp[i][j] 仍表示0 - i-1位置的word1,到0 - j-1位置的word2的最小编辑距离。当前遍历到i,j,如果word1[i-1] = word2[j-1] ,在上一次的基础上,现在不需要进行操作。因此最小距离还是dp[i][j] = dp[i-1][j-1]

而如果不等,就要考虑三种情况选最小值。

对word1进行删除一个字符: 那么需要考虑的是dp[i-1][j]的步数加上当前的步数1, 即dp[i-1][j] + 1

对word2删除一个字符,总的步数是dp[i][j-1] + 1

如果对word1进行替换,最少的操作数是直接替换word1[i-1],替换到与word2[j-1]相同,这样前面的步数是dp[i-1][j-1],总的步数加1,即dp[i-1][j-1] + 1

初始化:分析和583相同。事实上,编辑距离和583的情况基本相同,只有在三种情况分析的最后一步递推公式不同,583需要删除两个元素,是两步。而72只需要替换,是一步。

class Solution:
    def minDistance(self, word1: str, word2: str) -> int:
        m = len(word1)
        n = len(word2)
        dp = [[0]*(n+1) for _ in range(m+1)]
        for i in range(m+1):
            dp[i][0] = i
        for j in range(n+1):
            dp[0][j] = j
        for i in range(1,m+1):
            for j in range(1,n+1):
                if word1[i-1] == word2[j-1]:
                    dp[i][j] = dp[i-1][j-1]
                else:
                    dp[i][j] = min( dp[i-1][j]+1,  dp[i][j-1] +1,  dp[i-1][j-1] + 1)
        return dp[m][n]

647 计算回文子串的个数

给定一个字符串,你的任务是计算这个字符串中有多少个回文子串。

具有不同开始位置或结束位置的子串,即使是由相同的字符组成,也会被视作不同的子串。

输入:"abc"
输出:3
解释:三个回文子串: "a", "b", "c"

dp[i][j]表示字符串中str[i] --- str[j]的这部分的子串是不是回文串。dp[i][j] = True表示是回文串。

dp[i][j]的计算: 如果s[i] != s[j],肯定不是回文串。dp[i][j] =False

但如果s[i] == s[j],然后看dp[i+1][j-1]是不是回文串,即中间的那部分子串是否回文串。 如果j=i+1,只有两个字符,且相等就肯定是回文串。

初始化: 全部为false

遍历顺序:dp[i][j]dp[i+1][j-1]有关。就是:在计算i之前,i+1要被计算出来,而j-1要先在j之前计算。因此i要倒序遍历,j要正序遍历,

class Solution:
    def countSubstrings(self, s: str) -> int:
        n = len(s)
        cnt = 0
        dp = [[False] * n for _ in range(n)]

        # i==j 回文串 , 或者 j =i +1 且两个相等 回文串 
        # j >= i+2 看dp[i+1][j-1]  因为只考虑j>=i的情况,因此j可以从i开始遍历  但i要从 n-1倒序遍历
        for i in range(n-1,-1,-1):
            for j in range(i,n,1):
                if s[i] == s[j] :
                    if j == i +1 or i == j:  # 相等的时候,单个字符或者两个字符都是回文串。
                        dp[i][j] = True 
                        cnt = cnt + 1  # 计数+1
                    else:
                        dp[i][j] = dp[i+1][j-1]  # 两个以上的子串,就看内部的了。   
                        if dp[i][j]:
                            cnt =cnt + 1  # 内部看完,如果是回文,计数+1
             
        return cnt

双指针法:中心扩散,遍历每一个字符,向两边扩散,直到扩散到不是回文串为止,依次记下每次扩散找到的回文串个数。但是扩散的时候,可以选择一个字符作为回文串的中心,也可以选择两个字符作为回文串的中心,两个字符作为中心,这两个字符是相同的情况下才可以哈。

中心扩散的方法速度比动态规划更快,

class Solution:
    def countSubstrings(self, s: str) -> int:
        n = len(s)
        cnt = 0  # 计数

        for i in range(n):
            # 遍历每一个字符
            left = i
            right = i
            while left >= 0 and right < n and s[left] == s[right]:  # 以一个字符为回文串的中心left和right分别往两边走,直到两个指针的字符不同
                # 没走一次 就找到一个回文串 计数+1
                left = left - 1
                right = right + 1
                cnt = cnt + 1   #
            left = i
            right = i+1
            while left >= 0 and right <n  and s[left] == s[right]:
                left = left -1
                right = right + 1
                cnt = cnt + 1

        return cnt

5 最长回文子串

方法和647一模一样,采用动态规划或者中心扩散,每次找到回文子串,判断长度是否更大,更大就更新。

class Solution:
    def longestPalindrome(self, s: str) -> str:
        n = len(s)
        cnt = 0  # 计数
        ans = ""  # 记录结果

        for i in range(n):
            # 遍历每一个字符   
            left = i
            right = i
            while left >= 0 and right < n and s[left] == s[right]:
                left = left - 1
                right = right + 1
            # 每次while循环结束表明当前的left和right其实并不是回文子串,
            # 它的上一次才是,因此恢复一下, 它内部的那个是以 i为中心的最长的一个回文子串
            left = left +1
            right = right - 1

            if right - left + 1 > len(ans):
                ans = s[left : right + 1]
            
            left = i
            right = i+1   # 以相邻两个字符为中心的回文串
            while left >= 0 and right <n  and s[left] == s[right]:
                left = left -1
                right = right + 1
            
            left = left +1   # 和上面相同,也是恢复一下
            right = right - 1
            if right - left + 1 > len(ans):  # 长度更长就保留当前结果
                ans = s[left : right + 1]

        return ans

516 最长回文子序列

给定一个字符串 s ,找到其中最长的回文子序列,并返回该序列的长度。可以假设 s 的最大长度为 1000

"bbbab"
输出 4
一个可能的最长回文子序列为 "bbbb"。

回文子序列不要求连续。

dp[i][j]表示ij部分的最长回文子序列长度。

如果s[i] == s[j],那么只需看内部的回文子序列的长度,而内部是dp[i+1][j-1],因此总长度是dp[i+1][j-1] + 2

如果不相等,可以考虑选择其中一个和内部能否组成一个回文子序列。选择s[i],长度就是dp[i][j-1],就是说选择ij-1这部分的最长子序列。

而选择s[j]加入,长度就是 dp[i+1][j] ,选择二者较大的那个。

初始化: i=j的时候,长度都是1,即单个字符本身,

class Solution:
    def countSubstrings(self, s: str) -> int:
        n = len(s)
        cnt = 0
        dp = [[0] * n for _ in range(n)]

        # i==j 回文串 , 或者 j =i +1 且两个相等 回文串 
        # j >= i+2 看dp[i+1][j-1]  因为只考虑j>=i的情况,因此j可以从i开始遍历  但i要从 n-1倒序遍历
        for i in range(n-1,-1,-1):
            for j in range(i,n,1):
                if i == j
                if s[i] == s[j] :
                    if j == i +1 or i == j:  # 相等的时候,单个字符或者两个字符都是回文串。
                        dp[i][j] = True 
                        cnt = cnt + 1  # 计数+1
                    else:
                        dp[i][j] = dp[i+1][j-1]  # 两个以上的子串,就看内部的了。   
                        if dp[i][j]:
                            cnt =cnt + 1  # 内部看完,如果是回文,计数+1
             
        return cnt

深度优先搜索DFS

岛屿系列问题

通用解法

https://leetcode-cn.com/problems/number-of-islands/solution/dao-yu-lei-wen-ti-de-tong-yong-jie-fa-dfs-bian-li-/

腾讯音乐笔试

n*m的棋盘格状土地上盘踞着三个国家的若干股势力,上下左右相邻的属于同一个国家的土地被认为是同一股势力。现在想知道,土地上总共有多少股势力?

输入: 
4 4
1122
1222
3111
3333
输出: 4 一共有四块连通的区域
第一块:
11
1
第二块
  22
 222
第3块
 111
第四块
3
3333
import sys
def dfs(grid, r, c,kind=1):
    grid[r][c] = 0  # r,c这个格子遍历过, 将其置0
    nr, nc = len(grid), len(grid[0])
    for x, y in [(r - 1, c), (r + 1, c), (r, c - 1), (r, c + 1)]:
        if 0 <= x < nr and 0 <= y < nc and grid[x][y] == kind:
            dfs(grid, x, y,kind)

while True:
    try:
        n, m = list(map(int, sys.stdin.readline().strip().split()))  # n和m的岛屿
        grid = [ [0]*m for _ in range(n)] 
        for i in range(n):
            #line = list(map(int, sys.stdin.readline().strip().split()))
            line = list(map(int, list(sys.stdin.readline().strip())))
            grid[i] = line
        ans = 0
        for r in range(n):
            for c in range(m):
                if grid[r][c] == 1:
                    ans = ans + 1
                    dfs(grid, r,c,1)
                if grid[r][c] == 2:
                    ans = ans + 1
                    dfs(grid, r,c,2)
                    
                if grid[r][c] == 3:
                    ans = ans + 1
                    dfs(grid, r,c,3)
        print(ans)
    except:
        break

17 电话号码的组合

给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。

给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。

输入:digits = "23"
输出:["ad","ae","af","bd","be","bf","cd","ce","cf"]
class Solution:
    def letterCombinations(self, digits: str) -> List[str]:
        phoneMap = {
            "2": "abc",
            "3": "def",
            "4": "ghi",
            "5": "jkl",
            "6": "mno",
            "7": "pqrs",
            "8": "tuv",
            "9": "wxyz",
            }
        n = len(digits)
        ans = []
        strs = []
        if n==0 : return ans

        def dfs(digits, index):
            # 从index开始搜索遍历
            if index == len(digits) :
                # 搜到了最后一个
                ans.append("".join(strs))
                return 
            else:
                num = digits[index]  # 当前的数字
                tm = phoneMap[num]  # 对应的字符集
                for var in tm:
                    strs.append(var)
                    dfs(digits, index +1)
                    strs.pop()
        
        dfs(digits, 0)
        return ans

回溯算法

    def findItinerary(self, tickets):
        path = [] # 记录路径
        # res = [] # 多条路径,记录结果
        def backtrack(cur):

            if # 结束条件:
                # res.append(path[:])
                return True

            for 某节点 in 当前节点可以有的选择:
                # 去掉不合适选择
                path.append(某节点)  # 做选择
                if backtrack(某节点):  # 进入下一层决策树
                    return True
                path.pop()  # 取消选择

            return False
        backtrack(cur)
        return path

posted @ 2021-03-06 22:08  木易123  阅读(160)  评论(0编辑  收藏  举报