算法题目 动态规划


目录

    • 斐波那契问题
        • 爬楼梯
        • 打家劫舍
        • 环形区域内打家劫舍
        • 信件错排
        • 母牛生产
    • 子序列问题
        • 和最大的连续子序列
        • 最长递增子序列
        • 一组整数对能构成的最长链
        • 最长摆动子序列
        • 等差递增连续子序列的个数
        • 最长公共子序列
    • 0-1背包问题
      • 多维背包
        • 定量的01字符可构成字符串的最多数目
      • 多重背包
        • 划分数组为相等的两部分
        • 改变一组数的正负号使它们的和为一个给定数
      • 完全背包
        • 找零钱求解最少硬币数
        • 找零钱求解可行的硬币组合数
      • 顺序完全背包
        • 找零钱求解可行的硬币组合(考虑硬币顺序)
        • 用单词列表中的单词填充字符串
    • 字符串编辑
        • 删除字符使两个字符串相同的最少删除个数
        • 编辑一个字符串使其等于另一个字符串的最少编辑次数
        • 复制粘贴字符串


斐波那契问题

爬楼梯

leetcode 70 爬楼梯(简单)
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。

每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?

注意:给定 n 是一个正整数。

示例 1:

输入: 2
输出: 2
解释: 有两种方法可以爬到楼顶。
1.1 阶 + 1 阶
2.2 阶

示例 2:

输入: 3
输出: 3
解释: 有三种方法可以爬到楼顶。

  1. 1 阶 + 1 阶 + 1 阶
  2. 1 阶 + 2 阶
  3. 2 阶 + 1 阶

题解:

class Solution {
    public int climbStairs(int n) {
        //爬第k阶台阶,可以是从第k-1阶爬上来,也可以是从第k-2阶爬上来,所以,爬上第k阶楼梯的方法为爬上第k-1阶楼梯的方法+爬上第k-2阶楼梯的方法
        //动态规划实现
        if(n==1){
            return 1;
        }
        
        int[] dp = new int[n+1];  //dp[k]为爬上第k阶楼梯的方法数
        dp[1] = 1;
        dp[2] = 2;
        
        for(int k=3; k<=n; k++){
            dp[k] = dp[k-2]+dp[k-1];
        }     
        return dp[n];        
    }
}

打家劫舍

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

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

示例 1:

输入: [1,2,3,1]
输出: 4
解释: 偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。
偷窃到的最高金额 = 1 + 3 = 4 。

示例 2:

输入: [2,7,9,3,1]
输出: 12
解释: 偷窃 1 号房屋 (金额 = 2), 偷窃 3 号房屋 (金额 = 9),接着偷窃 5 号房屋 (金额 = 1)。
偷窃到的最高金额 = 2 + 9 + 1 = 12 。

题解:

class Solution {
    public int rob(int[] nums) {
        int n = nums.length;
        if(n == 0){
            return 0;
        }
        
        //定义状态,dp[k] 为考虑盗取第[0,k]号房子所取得的最大收益
        int[] dp = new int[n];
        dp[0] = nums[0];
     
        //状态转移方程,求解dp[k], dp[k] = max( nums[k]+dp[k-2], nums[k-1]+dp[k-3]... )
        //或者,状态转移方程,求解dp[k], dp[k] = max( nums[k]+dp[k-2], nums[k-1]+dp[k-3])
        for(int k=1; k=0; i--){
                dp[k] = Math.max((nums[i]+(i>=2 ? dp[i-2]:0)), dp[k]);
            }
        }

        return dp[n-1];        
    }
}

题解2:

class Solution {
    public int rob(int[] nums) {
        int n = nums.length;
        if(n==0){
            return 0;
        }
        
        int[] dp = new int[n];
        dp[0] = nums[0];
        
        for(int k=1; k=0? dp[k-2]:0));
        }
        
        return dp[n-1];       
    }
}

环形区域内打家劫舍

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

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

示例 1:

输入: [2,3,2]
输出: 3
解释: 你不能先偷窃 1 号房屋(金额 = 2),然后偷窃 3 号房屋(金额 = 2), 因为他们是相邻的。

示例 2:

输入: [1,2,3,1]
输出: 4
解释: 你可以先偷窃 1 号房屋(金额 = 1),然后偷窃 3 号房屋(金额 = 3)。
偷窃到的最高金额 = 1 + 3 = 4 。

题解:

class Solution {
    public int rob(int[] nums) {
        int n = nums.length;
        if(n==0){
            return 0;
        }
        
        if(n == 1){
            return nums[0];
        }
        
        // 为了避免头尾同时被选到的情况,分别对去头和去尾的nums作打家劫舍,选择最优结果
        int res1 = subRob(nums, 0, n-2);
        int res2 = subRob(nums, 1, n-1);
        
        return res1>res2? res1:res2;
    }
    
    // 对nums的[l,r]区间进行打家劫舍,l<=r
    private int subRob(int[] nums, int l, int r){
        int[] dp = new int[r-l+1];
        dp[0] = nums[l];
        
        for(int k=l+1; k<=r; k++){
            dp[k-l] = Math.max(dp[k-l-1], nums[k]+(k-l-2>=0? dp[k-l-2]:0));
        }
        
        return dp[r-l];
    }
}

信件错排

题目描述: 有 N 个 信 和 信封,它们被打乱,求错误装信方式的数量。

题解: 定义一个数组 dp 存储错误方式数量,dp[i] 表示前 i 个信和信封的错误方式数量。假设第 i 个信装到第 j 个信封里面,而第 j 个信装到第 k 个信封里面。根据 i 和 k 是否相等,有两种情况:

  • i == k,交换 i 和 k 的信后,它们的信和信封在正确的位置,但是其余 i-2 封信有 dp[i-2] 种错误装信的方式。由于 j 有 i-1 种取值,因此共有 (i-1)*dp[i-2] 种错误装信方式。
  • i != k,交换 i 和 j 的信后,第 i 个信和信封在正确的位置,其余 i-1 封信有 dp[i-1] 种错误装信方式。由于 j 有 i-1 种取值,因此共有 (i-1)*dp[i-1] 种错误装信方式。

综上所述,错误装信数量方式数量为:

d p [ i ] = ( i − 1 ) ∗ d p [ i − 1 ] + ( i − 1 ) ∗ d p [ i − 2 ] dp[i] = (i-1)*dp[i-1]+(i-1)*dp[i-2] dp[i]=(i1)dp[i1]+(i1)dp[i2]
这就是著名的 排错公式


母牛生产

题目描述: 假设农场中成熟的母牛每年都会生 1 头小母牛,并且永远不会死。第一年有 1 只小母牛,从第二年开始,母牛开始生小母牛。每只小母牛 3 年之后成熟又可以生小母牛。给定整数 N,求 N 年后牛的数量。

题解: 第 i 年成熟的牛的数量为:
d p [ i ] = d p [ i − 1 ] + d p [ i − 3 ] dp[i] = dp[i-1] + dp[i-3] dp[i]=dp[i1]+dp[i3]


子序列问题

子序列分连续子序列和不连续子序列两种。

对于连续子序列,当前状态一般仅由其相邻前一种状态决定。

对于非连续子序列,当前状态可能会由其前面所有状态中的一种决定,需要对前面所有状态遍历一次取最优。

注意,定义状态时,“以第k个元素结尾” 和 “考虑到第k个元素” 的区别,


和最大的连续子序列

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

示例:

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

进阶:

  • 如果你已经实现复杂度为 O(n) 的解法,尝试使用更为精妙的分治法求解。

题解:

class Solution {
    public int maxSubArray(int[] nums) {
      int n = nums.length;
      if(n==0){
        return 0;
      }
      
      // dp[k] 表示以第k个元素结尾的连续子序列的最大和
      int[] dp = new int[n];
      dp[0] = nums[0];
      
      // dp[k] = max(dp[k-1]+nums[k], nums[k]);
      int max = nums[0];
      for(int k=1; k

最长递增子序列

leetcode 300 最长上升子序列(中等)
给定一个无序的整数数组,找到其中最长上升子序列的长度。

示例:

输入: [10,9,2,5,3,7,101,18]
输出: 4
解释: 最长的上升子序列是 [2,3,7,101],它的长度是 4。

说明:

  • 可能会有多种最长上升子序列的组合,你只需要输出对应的长度即可。
  • 你能将算法的时间复杂度降低到 O(n log n) 吗?

题解:
时间复杂度为O(n^2)

class Solution {
    public int lengthOfLIS(int[] nums) {
      int n = nums.length;
      if(n == 0){
        return 0;
      }
      
      // dp[k] 表示以第k个元素结尾的上升子序列的长度
      int[] dp = new int[n];
      dp[0] = 1;
      
      // dp[k] = 1;
      // dp[k] = max(dp[i]+1, dp[k]) (0<=inums[i]
      int max = 1;
      for(int k=1; k=0; i--){
          if(nums[k]>nums[i]){
            dp[k] = Math.max((dp[i]+1), dp[k]);
          }
        }
        
        max = Math.max(max, dp[k]);
      }
       
      return max;
    }
}

一组整数对能构成的最长链

leetcode 646 最长数对链(中等)
给出 n 个数对。 在每一个数对中,第一个数字总是比第二个数字小。

现在,我们定义一种跟随关系,当且仅当 b < c 时,数对(c, d) 才可以跟在 (a, b) 后面。我们用这种形式来构造一个数对链。

给定一个对数集合,找出能够形成的最长数对链的长度。你不需要用到所有的数对,你可以以任何顺序选择其中的一些数对来构造。

示例 :

输入: [[1,2], [2,3], [3,4]]
输出: 2
解释: 最长的数对链是 [1,2] -> [3,4]

注意:
给出数对的个数在 [1, 1000] 范围内。

题解:

class Solution {
    public int findLongestChain(int[][] pairs) {
      if(pairs.length==0 || pairs[0].length==0){
        return 0;
      }
      
      int n = pairs.length;
      Arrays.sort(pairs, (o1, o2)->(o1[0]-o2[0]));
      
      // dp[k] 表示以第k个元素结尾的数对链的唱的
      int[] dp = new int[n];
      dp[0] = 1;
      
      // dp[k]=1
      // dp[k] = max(dp[k], dp[i]+1) (0<=ipairs[i]
      int max = 1;
      for(int k=1; kpairs[i][1]){
            dp[k] = Math.max(dp[k], dp[i]+1);
          }
        }
        
        max = Math.max(max, dp[k]);
      }
      
      return max;       
    }
}

最长摆动子序列

leetcode 376 摆动序列(中等)
如果连续数字之间的差严格地在正数和负数之间交替,则数字序列称为摆动序列。第一个差(如果存在的话)可能是正数或负数。少于两个元素的序列也是摆动序列。

例如, [1,7,4,9,2,5] 是一个摆动序列,因为差值 (6,-3,5,-7,3) 是正负交替出现的。相反, [1,4,7,2,5] 和 [1,7,4,5,5] 不是摆动序列,第一个序列是因为它的前两个差值都是正数,第二个序列是因为它的最后一个差值为零。

给定一个整数序列,返回作为摆动序列的最长子序列的长度。 通过从原始序列中删除一些(也可以不删除)元素来获得子序列,剩下的元素保持其原始顺序。

示例 1:

输入: [1,7,4,9,2,5]
输出: 6
解释: 整个序列均为摆动序列。

示例 2:

输入: [1,17,5,10,13,15,10,5,16,8]
输出: 7
解释: 这个序列包含几个长度为 7 摆动序列,其中一个可为[1,17,10,13,10,16,8]。

示例 3:

输入: [1,2,3,4,5,6,7,8,9]
输出: 2

进阶:
你能否用 O(n) 时间复杂度完成此题?

题解:
时间复杂度为O(n)

class Solution {
    public int wiggleMaxLength(int[] nums) {
      int n = nums.length;
      if(n == 0){
        return 0;
      }
      
      int up = 1, down = 1;
      for(int i=1; inums[i-1]){
          up = down+1;
        }else if (nums[i]

等差递增连续子序列的个数

413 等差数列划分(中等)
如果一个数列至少有三个元素,并且任意两个相邻元素之差相同,则称该数列为等差数列。

例如,以下数列为等差数列:

1, 3, 5, 7, 9
7, 7, 7, 7
3, -1, -5, -9

以下数列不是等差数列。

1, 1, 2, 5, 7

数组 A 包含 N 个数,且索引从0开始。数组 A 的一个子数组划分为数组 (P, Q),P 与 Q 是整数且满足 0<=P

如果满足以下条件,则称子数组(P, Q)为等差数组:
元素 A[P], A[p + 1], …, A[Q - 1], A[Q] 是等差的。并且 P + 1 < Q 。
函数要返回数组 A 中所有为等差数组的子数组个数。

示例:

A = [1, 2, 3, 4]
返回: 3, A 中有三个子等差数组: [1, 2, 3], [2, 3, 4] 以及自身 [1, 2, 3, 4]。

题解:
dp[i] 表示以 A[i] 为结尾的等差递增子区间的个数。

当 A[i] - A[i-1] == A[i-1] - A[i-2],那么 [A[i-2], A[i-1], A[i]] 构成一个等差递增子区间。而且在以 A[i-1] 为结尾的递增子区间的后面再加上一个 A[i],一样可以构成新的递增子区间。

dp[2] = 1
   [0, 1, 2]
dp[3] = dp[2] + 1 = 2
   [0, 1, 2, 3], // [0, 1, 2] 之后加一个 3
   [1, 2, 3] // 新的递增子区间
dp[4] = dp[3] + 1 = 3
   [0, 1, 2, 3, 4], // [0, 1, 2, 3] 之后加一个 4
   [1, 2, 3, 4], // [1, 2, 3] 之后加一个 4
   [2, 3, 4] // 新的递增子区间

综上,在 A[i] - A[i-1] == A[i-1] - A[i-2] 时,dp[i] = dp[i-1] + 1。

因为递增子区间不一定以最后一个元素为结尾,可以是任意一个元素结尾,因此需要返回 dp 数组累加的结果。

class Solution {
    public int numberOfArithmeticSlices(int[] A) {
      int n = A.length;
      if(n<3){
        return 0;
      }
      
      // dp[k] 表示以第k个元素结尾的等差子序列的个数
      int[] dp = new int[n];
      
      // 以第k个元素结尾的等差子序列的个数总是比以第k-1个元素结尾的等差子序列多1
      // dp[k] = dp[k-1]+1
      // 总的等差子序列的个数为 dp[2]+dp[3]+...dp[n-1]
      int sum = 0;
      for(int k=2; k

最长公共子序列

对于两个子序列 S1 和 S2,找出它们最长的公共子序列。

定义一个二维数组 dp 用来存储最长公共子序列的长度,其中 dp[i][j] 表示 S1 的前 i 个字符与 S2 的前 j 个字符最长公共子序列的长度。考虑 S1i 与 S2j 值是否相等,分为两种情况:

  • 当 S1i==S2j 时,那么就能在 S1 的前 i-1 个字符与 S2 的前 j-1 个字符最长公共子序列的基础上再加上 S1i 这个值,最长公共子序列长度加 1,即 dp[i][j] = dp[i-1][j-1] + 1。
  • 当 S1i != S2j 时,此时最长公共子序列为 S1 的前 i-1 个字符和 S2 的前 j 个字符最长公共子序列,或者 S1 的前 i 个字符和 S2 的前 j-1 个字符最长公共子序列,取它们的最大者,即 dp[i][j] = max{ dp[i-1][j], dp[i][j-1] }。

对于长度为 N 的序列 S1 和长度为 M 的序列 S2,dp[N][M] 就是序列 S1 和序列 S2 的最长公共子序列长度。

与最长递增子序列相比,最长公共子序列有以下不同点:

  • 针对的是两个序列,求它们的最长公共子序列。
  • 在最长递增子序列中,dp[i] 表示以 Si 为结尾的最长递增子序列长度,子序列必须包含 Si ;在最长公共子序列中,dp[i][j] 表示 S1 中前 i 个字符与 S2 中前 j 个字符的最长公共子序列长度,不一定包含 S1i 和 S2j。
  • 在求最终解时,最长公共子序列中 dp[N][M] 就是最终解,而最长递增子序列中 dp[N] 不是最终解,因为以 SN 为结尾的最长递增子序列不一定是整个序列最长递增子序列,需要遍历一遍 dp 数组找到最大者。
public int lengthOfLCS(int[] nums1, int[] nums2) {
    int n1 = nums1.length, n2 = nums2.length;
    int[][] dp = new int[n1 + 1][n2 + 1];
    for (int i = 1; i <= n1; i++) {
        for (int j = 1; j <= n2; j++) {
            if (nums1[i - 1] == nums2[j - 1]) {
                dp[i][j] = dp[i - 1][j - 1] + 1;
            } else {
                dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
            }
        }
    }
    return dp[n1][n2];
}

0-1背包问题

背包问题需要将物品和背包抽象出来,通过对物品和背包容量的双重循环来解决填满背包时最优解

0-1背包问题可以分出:

  • 多维背包:物品不仅有体积,还有重量,且都需要占用背包空间,往往需要多维数组解决
  • 多重背包:物品数目固定
  • 完全背包:物品数目无限
  • 带有顺序的完全背包:物品数目无限,且物品填充背包需考虑顺序,往往将遍历物品放到内层循环
  • 其它:物品之间相互依赖或相互约束

多维背包


定量的01字符可构成字符串的最多数目

leetcode 474 一和零(中等)
在计算机界中,我们总是追求用有限的资源获取最大的收益。

现在,假设你分别支配着 m 个 0 和 n 个 1。另外,还有一个仅包含 0 和 1 字符串的数组。

你的任务是使用给定的 m 个 0 和 n 个 1 ,找到能拼出存在于数组中的字符串的最大数量。每个 0 和 1 至多被使用一次。

注意:

  • 给定 0 和 1 的数量都不会超过 100。
  • 给定字符串数组的长度不会超过 600。

示例 1:

输入: Array = {“10”, “0001”, “111001”, “1”, “0”}, m = 5, n = 3
输出: 4
解释: 总共 4 个字符串可以通过 5 个 0 和 3 个 1 拼出,即 “10”,“0001”,“1”,“0” 。

示例 2:

输入: Array = {“10”, “0”, “1”}, m = 1, n = 1
输出: 2
解释: 你可以拼出 “10”,但之后就没有剩余数字了。更好的选择是拼出 “0” 和 “1” 。

题解:

class Solution {
    public int findMaxForm(String[] strs, int m, int n) {
        if(strs.length == 0){
            return 0;
        }
        
        // dp[k][l] 表示背包中存放m个0,n个1时,可从字符串列表中选取的字符串的最大个数; 
        int[][] dp = new int[m+1][n+1];
        dp[0][0] = 0;
        
        for(int i=0; i=zeros; k--){
                for(int l=n; l>=ones; l--){
                    dp[k][l] = Math.max(dp[k][l], dp[k-zeros][l-ones]+1);
                }
            }            
        }
        
        return dp[m][n];
    }
}

多重背包

多重背包物品的个数固定,需要从一系列物品中选出一部分在满足背包容量的限制的情况下求得最优解

往往将物品遍历作为外层循环,背包容量作为内层遍历,且由大到小遍历以防止出现物品重用的情况


划分数组为相等的两部分

leetcode 416 分割等和子集(中等)
给定一个只包含正整数的非空数组。是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。

注意:
每个数组中的元素不会超过 100
数组的大小不会超过 200

示例 1:

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

示例 2:

输入: [1, 2, 3, 5]
输出: false
解释: 数组不能分割成两个元素和相等的子集.

题解:
抽象为是否可以从数组 nums 中选出一部分数字恰好可以填满容量为一个固定值的背包

class Solution {
    public boolean canPartition(int[] nums) {
        int n = nums.length;
        if(n==0){
            return false;
        }
        
        // 前期判断
        int sum = 0;
        for(int i=0; i=nums[i]; k--){
                dp[k] = dp[k] || dp[k-nums[i]];
            }
        }
        
        return dp[cap];        
    }
}

改变一组数的正负号使它们的和为一个给定数

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

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

示例 1:

输入: nums: [1, 1, 1, 1, 1], S: 3
输出: 5
解释:

-1+1+1+1+1 = 3
+1-1+1+1+1 = 3
+1+1-1+1+1 = 3
+1+1+1-1+1 = 3
+1+1+1+1-1 = 3
一共有5种方法让最终目标和为3。

注意:

  • 数组的长度不会超过20,并且数组中的值全为正数。
  • 初始的数组的和不会超过1000。
  • 保证返回的最终结果为32位整数。

题解:

可以将这组数看成两部分,P 和 N,其中 P 使用正号,N 使用负号,有以下推导:

sum (P ) - sum(N) = target
sum(P ) + sum(N) + sum( P) - sum(N) = target + sum( P) + sum(N)
2 * sum( P) = target + sum(nums)

即可抽象为 从数组nums选出一部分数字,恰好可以填满容量为sum(P )的背包的方法数

class Solution {
    public int findTargetSumWays(int[] nums, int S) {
        int n = nums.length;
        if(n==0){
            return 0;
        }
        
        // 初步判断
        int sum = 0;
        for(int i=0; i=nums[i]; k--){
                dp[k] = dp[k] + dp[k-nums[i]];
            }
        }
        
        return dp[cap];
    }
}

完全背包

完全背包中每中物品的个数是无限的,选出一定种类一定数目的物品在满足背包容量限制的情况下寻求最优解

往往将物品遍历作为外层循环,背包容量作为内层循环,容量由小到大遍历以实现固定一种物品时的物品重用


找零钱求解最少硬币数

leetcode 322 零钱兑换(中等)
给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1。

示例 1:

输入: coins = [1, 2, 5], amount = 11
输出: 3
解释: 11 = 5 + 5 + 1

示例 2:

输入: coins = [2], amount = 3
输出: -1

说明:

  • 你可以认为每种硬币的数量是无限的。

题解:

class Solution {
    public int coinChange(int[] coins, int amount) {
        int n = coins.length;
        if(n==0 || amount==0){
            return 0;
        }
        
        // dp[i] 表示用可重用的物品恰好装满容量为k的背包所需物品的最少数目
        int[] dp = new int[amount+1];

       // 背包的容量从当前coin的值开始, 
       // 此时若后面遇到dp[k]==0 则说明之前的coin无法填充容量为k的背包,只能用当前coin的值去填充
       // 若遇到dp[k-coin]==0 说明当前coin无法填充容量为k的背包(当前的)
        for(int coin: coins){
            for(int k=coin; k<=amount; k++){
                if(k==coin){
                    dp[k] = 1;
                }else if(dp[k]==0 && dp[k-coin]!=0){  //此处,dp[k] 指上一层的dp[k], dp[k-coin] 指当前层的值 
                    dp[k] = dp[k-coin]+1;
                }else if(dp[k-coin]!=0){    
                    dp[k] = Math.min(dp[k-coin]+1, dp[k]);
                }
            }
        }
        
        return dp[amount] == 0? -1: dp[amount];      
    }
}

找零钱求解可行的硬币组合数

leetcode 518 零钱兑换 II(中等)
给定不同面额的硬币和一个总金额。写出函数来计算可以凑成总金额的硬币组合数。假设每一种面额的硬币有无限个。

示例 1:

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

示例 2:

输入: amount = 3, coins = [2]
输出: 0
解释: 只用面额2的硬币不能凑成总金额3。

示例 3:

输入: amount = 10, coins = [10]
输出: 1

注意:

你可以假设:

  • 0 <= amount (总金额) <= 5000
  • 1 <= coin (硬币面额) <= 5000
  • 硬币种类不超过 500 种
  • 结果符合 32 位符号整数

题解:

class Solution {
    public int change(int amount, int[] coins) {
        
        // dp[k] 表示用coins装满容量为k的背包的方法数
        // 填满容量为0的背包只有1种方法
        int[] dp = new int[amount+1];
        dp[0] = 1; 
        
        for(int coin: coins){
            for(int k=coin; k<=amount; k++){  // 完全背包,容量由小到大遍历
                dp[k] = dp[k] + dp[k-coin];
            }
        }
        
        return dp[amount];     
    }
}

顺序完全背包

顺序完全背包中物品的个数无限(物品可重用),同时物品在背包中的位置有要求,同一批满足背包容量限制的物品的不同摆放位置属于不同解

往往将物品遍历作为内层循环已实现物品的顺序性要求


找零钱求解可行的硬币组合(考虑硬币顺序)

leetcode 377 组合综合 IV(中等)
给定一个由正整数组成且不存在重复数字的数组,找出和为给定目标正整数的组合的个数。

示例:

nums = [1, 2, 3]
target = 4
所有可能的组合为:
(1, 1, 1, 1)
(1, 1, 2)
(1, 2, 1)
(1, 3)
(2, 1, 1)
(2, 2)
(3, 1)

请注意,顺序不同的序列被视作不同的组合。

因此输出为 7。

题解:

class Solution {
    public int combinationSum4(int[] nums, int target) {
        int n = nums.length;
        if(n==0){
            return 0;
        }
        
        // dp[k] 表示用nums填充满容量为K的背包的方法数,不同顺序视为不同方法
        int[] dp = new int[target+1];
        dp[0] = 1;
      
        for(int k=1; k<=target; k++){  // 顺序完全背包,背包容量在外层遍历
            for(int num: nums){
                if(num<=k){     // 判断当前物品是否可以放入背包末端
                    dp[k] = dp[k]+dp[k-num];
                }
            }
        }
        
        return dp[target];
    }
}

用单词列表中的单词填充字符串

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

说明:

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

示例 1:

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

示例 2:

输入: s = “applepenapple”, wordDict = [“apple”, “pen”]
输出: true
解释: 返回 true 因为 “applepenapple” 可以被拆分成 “apple pen apple”。
注意你可以重复使用字典中的单词。

示例 3:

输入: s = “catsandog”, wordDict = [“cats”, “dog”, “sand”, “and”, “cat”]
输出: false

题解:

class Solution {
    public boolean wordBreak(String s, List wordDict) {
        int n=s.length();
        
        // 动态规划, 将 s 看作背包, wordDict 中的单词看作物品
        // 填充时,单词放入的顺序会影响最终结果,且每个单词无限量,属于顺序完全背包问题
        
        // dp[k] 表示是否可以使用给定的单词填充容量为k的背包
        boolean[] dp = new boolean[n+1];
        dp[0] = true;
        
        for(int k=1; k<=n; k++){
            for(String word: wordDict){
                int l = word.length();
                if(l<=k && word.equals(s.substring(k-l, k))){  // 判断当前物品是否可以放入当前背包的末端
                    dp[k] = dp[k] || dp[k-l];
                }
            }
        }
        
        return dp[n];
    }
}

字符串编辑

对一个或者两个字符串,通过增加,删除,替换等操作,使它们最终达到一种要求,求解最优操作次数或最优修改字符数

删除字符使两个字符串相同的最少删除个数

leetcode 583 两个字符串的删除操作(中等)
给定两个单词 word1 和 word2,找到使得 word1 和 word2 相同所需的最小步数,每步可以删除任意一个字符串中的一个字符。

示例 1:

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

说明:

  • 给定单词的长度不超过500。
  • 给定单词中的字符只含有小写字母。

题解:
最终相等的结果一定是两个字符串的最长公共子序列,所以可以转化为求解两个字符串的最长公共子序列

class Solution {
    public int minDistance(String word1, String word2) {
        int m = word1.length(), n = word2.length();
        
        // dp[k][l] 表示考虑到word1的第k个字符 word2的第l个字符时,公共子序列的长度
        // word1 word2 字符的起始位从1开始,故 dp[0][0] = 0, dp[0][i] = 0, dp[j][0] = 0
        int[][] dp = new int[m+1][n+1];
        
        for(int i=1; i<=m; i++){
            for(int j=1; j<=n; j++){
                if(word1.charAt(i-1) == word2.charAt(j-1)){
                    dp[i][j] = dp[i-1][j-1] + 1;
                }else{
                    dp[i][j] = Math.max(dp[i-1][j], dp[i][j-1]);
                }
            }
        }
        
        int len = dp[m][n];
        
        return m+n-2*len;        
    }
}

编辑一个字符串使其等于另一个字符串的最少编辑次数

leetcode 72 编辑距离(困难)
给定两个单词 word1 和 word2,计算出将 word1 转换成 word2 所使用的最少操作数 。

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

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

示例 1:

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

示例 2:

输入: word1 = “intention”, word2 = “execution”
输出: 5
解释:
intention -> inention (删除 ‘t’)
inention -> enention (将 ‘i’ 替换为 ‘e’)
enention -> exention (将 ‘n’ 替换为 ‘x’)
exention -> exection (将 ‘n’ 替换为 ‘c’)
exection -> execution (插入 ‘u’)

题解:

class Solution {
    public int minDistance(String word1, String word2) {
        int m = word1.length(), n = word2.length();
        
        // dp[k][l]表示将word1的前k个字符编辑成word2的前l个字符所需的最少操作次数
        int[][] dp = new int[m+1][n+1];
        
        // 初始化 dp[k][0] = k 表示将k个字符编辑成空字符串,则需要删除k个字符,即需要k次操作
        // 同理,dp[0][l] = l 表示将空字符串编辑成k个字符,则需要增加k个字符,即需要k次操作
        for(int i=0; i<=m; i++){
            dp[i][0] = i;
        }
        for(int j=0; j<=n; j++){
            dp[0][j] = j;
        }
        
        for(int i=1; i<=m; i++){
            for(int j=1; j<=n; j++){
                if(word1.charAt(i-1) == word2.charAt(j-1)){
                    // 相等时,不需要任何操作
                    dp[i][j] = dp[i-1][j-1];
                }else{
                    // 在修改,增加,删除的操作中选一种
                    // dp[i][j] = dp[i-1][j-1] + 1, 表示修改当前word1的第i个字符,使其等于word2的第j个字符
                    // dp[i][j] = dp[i-1][j] + 1, 由于当前word1[i] != word2[j], 而word1[i-1] = word2[j],所以可以删除word1[1]
                    // dp[i][j] = dp[i][j-1] + 1, 表示在word1的第i个字符后新增一个等于word2[j]的字符
                    dp[i][j] = Math.min(dp[i-1][j-1], Math.min(dp[i-1][j], dp[i][j-1])) + 1; 
                }
            }
        }
              
        return dp[m][n];
    }
}

复制粘贴字符串

leetcode 650 只有两个键的键盘(中等)
最初在一个记事本上只有一个字符 ‘A’。你每次可以对这个记事本进行两种操作:

Copy All (复制全部) : 你可以复制这个记事本中的所有字符(部分的复制是不允许的)。
Paste (粘贴) : 你可以粘贴你上一次复制的字符。
给定一个数字 n 。你需要使用最少的操作次数,在记事本中打印出恰好 n 个 ‘A’。输出能够打印出 n 个 ‘A’ 的最少操作次数。

示例 1:

输入: 3
输出: 3
解释:
最初, 我们只有一个字符 ‘A’。
第 1 步, 我们使用 Copy All 操作。
第 2 步, 我们使用 Paste 操作来获得 ‘AA’。
第 3 步, 我们使用 Paste 操作来获得 ‘AAA’。

说明:

  • n 的取值范围是 [1, 1000] 。

题解:
n>1时 其实就是将n分解为m个数字的乘积 且m个数字的和最小 ,即把一个数分解为n个质数的和 ,从小到大的去试探

class Solution {
    public int minSteps(int n) {
        int res = 0;
        for(int k=2; k<=n; k++){
            while(n%k == 0){
                res+=k;
                n/=k;
            }
        }
        
        return res;
    }
}

参考:
CyC2018 leetcode算法总结

你可能感兴趣的:(算法基础)