Leetcode 算法之动态规划 —— Java 题解

340 阅读15分钟

所谓动态规划(Dynamic Programming),即为求解决策过程中最优化的过程,通常用于求解具有某种最优性质的问题。

例如:

  • 解决某种问题需要的最少步骤
  • 一个数组划分成具有某种特征的子数组时,能划分出来的最大数量
  • 在一定条件下,某种事物所能取到的最大数量,如背包问题

对于这类问题,往往有许多可行解,但我们的目标是找出最优解。

动态规划的基本思想是,将问题分解成若干个子问题,并求解这些子问题。同时,这些子问题并不是相互独立的,部分子问题的解可以在求解其他子问题时利用到,因此,我们可以保存某些子问题的最优解,在需要时利用这些子问题的解辅助求解当前子问题。

对于动态规划类问题,我们往往需要定义一个dp数组,用来存放各个子问题的解,然后找到某一种规律或者说公式,这条公式用来让我们结合dp数组求解子问题。

70. 爬楼梯 - 简单

假设你正在爬楼梯。需要 n 阶你才能到达楼顶。

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

示例:

输入:n = 3

输出:3

解释:有三种方法可以爬到楼顶。

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

题解:

考虑到达第 n 阶阶梯:

  1. 在到达第 n-1 阶阶梯时,只有 1 种方法即爬一阶阶梯到达第 n 阶。
  2. 在到达第 n-2 阶阶梯时,有 2 种方法可以到达第 n 阶,爬一阶阶梯到达第 n-1 阶后再爬一阶到达第 n 阶,即情况 1;或者爬二阶阶梯到达第 n 阶

综上,想要到达第 n 阶阶梯时,可以考虑到达第 n-2 阶阶梯的方法数 a 和到达第 n-1 阶阶梯的方法数 ba+b的和即为到达第 n 阶阶梯的方法数。

声明一个长度为 n+1dp 数组,初始化 dp[0] = dp[1] = 1,即不爬阶梯或者爬一层阶梯只有一种方法,此后不断遍历,到达第 i 层阶梯的方法数即为 dp[i] = dp[i-2]+dp[i-1]

💡 也可以对这个过程进行优化,注意到每次遍历第 i 层阶梯时,只需考虑前两层阶梯的方法数,之前遍历的阶梯已经不需要考虑了,因此可以将空间复杂度 O(n) 优化为 O(1)

代码:

public int climbStairs(int n) {
    if (n <= 3) {
        return n;
    }
    int[] dp = new int[n+1];
    dp[0] = dp[1] = 1;
    for (int i = 2; i <= n; i++) {
        dp[i] = dp[i-2] + dp[i-1];
    }
    return dp[n];
}

// 优化
 public int climbStairs(int n) {
     if (n <= 3) {
         return n;
     }
     // pre2 对应 dp[i-2] pre1 对应 dp[i-1]
     int pre2 = 1, pre1 = 2;
     int cur = 3;
     for (int i = 3; i <= n; i++) {
         cur = pre2 + pre1;
         pre2 = pre1;
         pre1 = cur;
     }
     return cur;
 }

413. 等差数列划分 - 中等

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

例如,[1,3,5,7,9]、[7,7,7,7] 和 [3,-1,-5,-9] 都是等差数列。 给你一个整数数组 nums ,返回数组 nums 中所有为等差数组的 子数组 个数。

子数组 是数组中的一个连续序列。

示例:

输入:nums = [1,2,3,4]

输出:3

解释:nums 中有三个子等差数组:[1, 2, 3]、[2, 3, 4] 和 [1,2,3,4] 自身。

题解:

从 nums 数组后面往前遍历,如果某个元素与它后面的两个元素构成等差数列,那么以该元素为等差数列第一个元素且长度为 3 的子数组的个数即为 1,同时,考虑后面更后面的元素能否和该子数组构成等差数列。

例如:[1,2,3,4,5,6]

  • 4 构成的等差数组个数为 1
  • 3 构成的等差数组个数为 2,即在 [4,5,6]的基础上加入了 3
  • 2 构成的等差数组个数为 3,......

最后,求和各个元素所能构成的等差数组的个数,即为该等差数组中符合题意的子数组个数。

代码:

public int numberOfArithmeticSlices(int[] nums) {
    int n = nums.length;
    if (n < 3) {
        return 0;
    }
    int[] dp = new int[n];
    for (int i = n-3; i >= 0; i--) {
        if (nums[i]-nums[i+1] == nums[i+1]-nums[i+2]) {
            dp[i] = dp[i+1]+1;
        }
    }
    int res = 0;
    for (int i = 0; i < n; i++) {
        res += dp[i];
    }
    return res;
}

// 优化,将空间复杂度优化为 O(1)
public int numberOfArithmeticSlices(int[] nums) {
    int n = nums.length;
    if (n < 3) {
        return 0;
    }
    // next 表示当前遍历元素的下一个元素构成的等差数组个数
    int next = 0;
    // res 是返回结果
    int res = 0;
    for (int i = n-3; i >= 0; i--) {
        if (nums[i]-nums[i+1] == nums[i+1]-nums[i+2]) {
            // 当前元素所能构成的等差数组个数
            int cur = 1 + next;
            next = cur;
            res += cur;
        } else {
            next = 0;
        }
    }

    return res;
}

198. 打家劫舍 - 中等

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

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

示例:

输入:[2,7,9,3,1]

输出:12

解释:偷窃 1 号房屋 (金额 = 2), 偷窃 3 号房屋 (金额 = 9),接着偷窃 5 号房屋 (金额 = 1)。

偷窃到的最高金额 = 2 + 9 + 1 = 12 。

题解:

使用一个一维数组 dp[] 表示当偷窃第 i 个房屋时,包含前面房屋所能偷窃到的最大金额。

当房屋数量为 0 时,所能偷窃的最大金额为 0,当房屋数量为 1 时,所能偷窃的最大金额为 nums[0],因此初始化 dp[0]=0,dp[1]=nums[0]

具体的“偷窃”思路为,当遍历到某个房屋时,我们可以选择:

  1. 偷窃这个房屋,那么所能偷窃的金额数为偷窃到往前数第二座房屋时所能偷窃的最大金额加上当前房屋的金额
  2. 不偷窃这个房屋,那么所能偷窃的金额为偷窃到上一座房屋时所能偷窃的最大金额

比较两个金额,取最大值,即为偷窃到当前房屋时所能偷窃的最大金额

代码:

 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+1];
     dp[1] = nums[0];
     for (int i = 2; i <= n; i++) {
         dp[i] = Math.max(dp[i-1], dp[i-2]+nums[i-1]);
     }
     return dp[n];
 }

// ========== 空间优化
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+1];
            dp[1] = nums[0];*/
    int pre2 = 0, pre1 = nums[0];
    int cur = pre1;
    for (int i = 2; i <= n; i++) {
        cur = Math.max(pre2+nums[i-1], pre1);
        pre2 = pre1;
        pre1 = cur;
    }
    return cur;
}

64. 最小路径和 - 中等

给定一个包含非负整数的 m x n 网格 grid ,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。

**说明:**每次只能向下或者向右移动一步。

示例:

minpath.jpg

输入:grid = [

[1,3,1],

[1,5,1],

[4,2,1]]

输出:7

解释:因为路径 1→3→1→1→1 的总和最小。

题解:

我们只能往下走或者往右走。

我们声明一个二维数组 dp[m][n],记录走到某个单元格时,最小路径和为多少。

因此,我们遍历网格 grid ,遍历到某个单元格时,根据其上面的单元格和左边的单元格,就可以知道走到当前单元格的最小路径和是多少。

代码:

class Solution {
    public int minPathSum(int[][] grid) {
        int m = grid.length, n = grid[0].length;
        int[][] dp = new int[m][n];
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (i == 0 && j == 0) {			// 左上角第一个格子
                    dp[i][j] = grid[i][j];
                }else if (i == 0) {				// 遍历到第一行并且不是第一个格子,当前格子只能是从左边格子走过来
                    dp[i][j] = dp[i][j-1] + grid[i][j];
                }else if (j == 0) {				// 遍历到第一列并且不是第一个格子,当前格子只能是从上边格子走过来
                    dp[i][j] = dp[i-1][j] + grid[i][j];	
                }else {							// 其它情况,可能是从上边走过来,也可能是从左边走过来
                    dp[i][j] = Math.min(dp[i-1][j], dp[i][j-1]) + grid[i][j];
                }

            }

        }
        return dp[m-1][n-1];
    }
}

542. 01 矩阵 - 中等

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

两个相邻元素间的距离为 1 。

示例:

1626667205-xFxIeK-image-16620202124204.png

输入:mat = [[0,0,0],[0,1,0],[1,1,1]]

输出:[[0,0,0],[0,1,0],[1,2,1]]

题解:

声明一个二维数组 dp[m][n],记录矩阵中每个元素距离最近的 0 的距离。

首先,遍历矩阵 mat ,我们从左上角逐个遍历,即从左遍历第一行元素,再从左遍历第二行元素......

  1. 如果当前遍历元素 mat[i][j] 为 0,那么该元素距离最近的 0 的距离为 0,即 dp[i][j] = 0
  2. 如果不为 0,那么当前遍历元素距离需要这么考虑:
    1. 如果当前元素的上边或者左边是 0,那么其距离最近的 0 的距离为 1
    2. 如果当前元素的上边或者左边不是 0,那么其距离最近的 0 即为上边的或者左边的 1 距离 0 的距离的最小值 + 1,即 dp[i][j] = min(dp[i-1][j], dp[i][j-1]) + 1

此时,我们已经考虑了每一个单元格与左边或者上边的最近的 0 的距离,还没有考虑其右边或者下边最近的 0 的距离。

因此,只需要从右下角再遍历一遍,考虑下右边或者下边的情况即可。

代码:

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

        // 初始化 dp 数组,未知情况下,每个元素距离最近的 0 的距离最大。
        for (int i = 0; i < m; i++) {
            Arrays.fill(dp[i], Integer.MAX_VALUE-1);
        }

        // 从左上角开始遍历
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {

                if (mat[i][j] == 0) {	// 当前元素为 0,距离最近 0 的距离为 0
                    dp[i][j] = 0;
                }else {					// 当前元素不为 0
                    if (i > 0) {
                        dp[i][j] = Math.min(dp[i-1][j]+1, dp[i][j]);
                    }
                    if (j > 0) {
                        dp[i][j] = Math.min(dp[i][j-1]+1, dp[i][j]);
                    }
                }
            }
        }

        // 从右下角开始遍历
        for (int i = m-1; i >= 0; i--) {
            for (int j = n-1; j >= 0; j--) {
                if (mat[i][j] == 1) {
                    if (i < m-1) {
                        dp[i][j] = Math.min(dp[i][j], dp[i+1][j]+1);
                    }
                    if (j < n-1) {
                        dp[i][j] = Math.min(dp[i][j], dp[i][j+1]+1);
                    }
                }
            }
        }
        return dp;
    }
}

221. 最大正方形 - 中等

在一个由 '0''1' 组成的二维矩阵内,找到只包含 '1' 的最大正方形,并返回其面积。

示例:

max1grid.jpg

输入:matrix = [

["1","0","1","0","0"],

["1","0","1","1","1"],

["1","1","1","1","1"],

["1","0","0","1","0"]]

输出:4

题解:

从一个正方形的右下角开始看,某个正方形的边长,取决于其上方的、左上角的、左边的正方形的边长的最小值。

基于此,构建一个二维数组 dp[][] 记录每一个方格处所能构成的最大正方形的长度。

对二维矩阵进行遍历,如果遇到元素 1 ,那么查看其上方、左上角、左边正方形的边长,取最小值,即为当前正方形的最大边长。

遍历完毕后,遍历二维数组 dp[][],取的最大边长值 maxLen,取平方后即为题目答案。

代码:

public int maximalSquare(char[][] matrix) {
    int m = matrix.length, n = matrix[0].length;
    int[][] dp = new int[m+1][n+1];

    for (int i = 1; i <= m; i++) {
        for (int j = 1; j <= n; j++) {
            if (matrix[i][j] == '1') {
                dp[i][j] = Math.min(dp[i-1][j], Math.min(dp[i-1][j-1], dp[i][j-1]))+1;
            }
        }
    }

    int maxLen = 0;
    for (int i = 1; i <= m; i++) {
        for (int j = 1; j <= n; j++) {
            maxLen = Math.max(maxLen, dp[i][j]);
        }
    }
    return maxLen*maxLen;
}

279. 完全平方数 - 中等

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

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

示例:

输入:n = 12

输出:3

解释:12 = 4 + 4 + 4

题解:

求和为 n 的完全平方数的最少数量,假设 k 是一个完全平方数,并且和为 m = n-k 的完全平方数的最少数量我们已知为 y,那么和为 n 的完全平方数的最少数量我们就可以知道了,即为 y+1

因此,这道题利用动态规划求解,从 1 开始,直到 n,我们求出和为每一个数字的完全平方数的最少数量,找出每一个数字的最优解,同时这个最优解还能被后面的数字利用。

定义一个一维数组dp[]dp[i] 代表和为数字 i 的完全平方数的最少数量。除 dp[0] = 0,每个元素初始值为Integer.MAX_VALUE,方便后续比较取得最少数量。

从 1 开始遍历,直到遍历到数字 n。当前遍历数字 i 减去一个完全平方数 k 后的那个数字的结果 dp[i-k]+1,即为和为数字 i 的完全平方数的数量,后面可能还有更少的数量,因此继续检索其他完全平方数的情况。

代码:

public int numSquares(int n) {
    int[] dp = new int[n+1];
    Arrays.fill(dp, Integer.MAX_VALUE);
    dp[0] = 0;

    for (int i = 1; i <= n; i++) {
        for (int j = 1; j*j <= i; j++) {
            dp[i] = Math.min(dp[i], dp[i-j*j]+1);
        }
    }

    return dp[n];
}

139. 单词拆分 - 中等

给你一个字符串 s 和一个字符串列表 wordDict 作为字典。请你判断是否可以利用字典中出现的单词拼接出 s 。

注意:不要求字典中出现的单词全部都使用,并且字典中的单词可以重复使用。

示例:

输入: s = "applepenapple", wordDict = ["apple", "pen"]

输出: true

解释: 返回 true 因为 "applepenapple" 可以由 "apple" "pen" "apple" 拼接成。

注意,你可以重复使用字典中的单词。

题解:

遍历字符串 s,确定字符串 s 的子串 p 是否能够有字典中的单词组成,不断扩大子串 p,我们可以利用到前面的结果,确定当前子串是否能够由字典中的单词组成。

例如子串 [0, j),如果子串 [0,i) 能够由字典中的单词组成,并且子串[i,j)也在单词列表中,那么子串 [0,j)就能由字典中的单词组成。

具体思路为:

  1. 声明一个一维数组 dp[],其中 dp[i] 表示字符串 s 的子串 [0,i) 可否由字典中的单词组成
  2. 遍历字符串,遍历长度为 i 的子串是否能由字典中的单词组成
  3. 遍历子串,慢慢扩大子串的前半部分,缩小子串的后半部分。如果子串的**前半部分能够由字典中的单词组成,并且后半部分**也在字典中,那么当前子串能够由字典中的单词组成,标记 dp[i]

❗ 还有优化的空间,如果子串的后半部分的长度比字典中长度最小的单词还小,那么停止遍历该子串。

❗ 如果子串的**后半部分**比字典中长度最大的单词还长,那么缩小子串的后半部分。

代码:

public boolean wordBreak(String s, List<String> wordDict) {
    int n = s.length();
    boolean[] dp = new boolean[n+1];
    dp[0] = true;

    Set<String> set = new HashSet<>(wordDict);
	
    // 遍历子串,扩大子串长度
    for (int i = 1; i <= n; i++) {
        // 遍历子串的前半部分是否能由字典中的单词组成,以及后半部分是否在字典中
        for (int j = 0; j < i; j++) {
            if (dp[j] && set.contains(s.substring(j, i))) {
                dp[i] = true;
            }
        }
    }

    return dp[n];
}

// 优化
public boolean wordBreak(String s, List<String> wordDict) {
     int n = s.length();
     boolean[] dp = new boolean[n+1];
     dp[0] = true;

     Set<String> set = new HashSet<>();
     int maxLen = 0, minLen = n;
     for (String word : wordDict) {
         maxLen = Math.max(word.length(), maxLen);
         minLen = Math.min(word.length(), minLen);
         set.add(word);
     }

    // 遍历子串,扩大子串长度
     for (int i = 1; i <= n; i++) {
         // 遍历子串的前半部分是否能由字典中的单词组成,以及后半部分是否在字典中
         for (int j = 0; j < i; j++) {
             // 后半部分不可能出现在字典中,缩小后半部分
             if (i-j > maxLen) {
                 continue;
             }
             // 后半部分不可能出现在字典中,并且再缩小也不会出现在字典中,退出遍历
             if (i-j < minLen) {
                 break;
             }

             if (dp[j] && set.contains(s.substring(j, i))) {
                 dp[i] = true;
             }
         }
     }

     return dp[n];
 }

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[],其中 dp[i] 表示子数组 [0,i] 的最长递增子序列的长度
  2. 遍历数组的子数组,长度 i 从 0 不断扩大到 nums.length
  3. 遍历子数组中的每一个元素 nums[j],如果最后一个元素 nums[i] 大于当前遍历元素 nums[i],那么在不考虑最后一个元素的情况下,最大子序列长度即为子数组[0,j] 的最大子序列长度,即 dp[j]
  4. 子数组遍历结束,考虑上最后一个元素,当前子数组的最大递增子序列长度加一。

代码:

public int lengthOfLIS(int[] nums) {
    int n = nums.length;
    // dp[i] 表示数组 nums [0,i] 的子数组,最长的递增子序列
    int[] dp = new int[n];
    int res = 0;
    // 求解子数组 [0,i] 的最长递增子序列
    for (int i = 0; i < n; i++) {
        // 遍历子数组中的每一个元素,如果子数组的最后一个元素大于当前遍历元素 nums[j]
        // 那么当前子数组排除掉【最后一个元素】的最长递增子序列长度即为 子数组 [0,j] 的最长递增子序列的长度
        for (int j = 0; j < i; j++) {
            if (nums[i] > nums[j]) {
                dp[i] = Math.max(dp[i], dp[j]);
            }
        }
        // 当前子树组的长度确定,同时更新最大递增子序列长度
        dp[i] += 1;
        res = Math.max(res, dp[i]);
    }

    return res;
}

1143. 最长公共子序列 - 中等

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

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

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

示例:

输入:text1 = "abcde", text2 = "ace"

输出:3

解释:最长公共子序列是 "ace" ,它的长度为 3 。

题解:

分解子问题,分别将两个字符串的长度减到最小,然后不断扩大字符串的长度去寻找最长公共子序列。

当字符串 1 即 text1 的子串 1 长度为 i,字符串 2 即 text2 的子串 2 长度为 j 时,如果两个字符串的最后一个字符相等,那么其最长公共子序列长度为子串 1 长度为 i-1, 子串 2 长度为 j-1 时的最长公共子序列长度加一。

如果不相等,那么其最长公共子序列长度取决于以下两个值的最大值:

  1. 子串 1 长度为 i,子串 2 长度为 j-1 的公共子序列长度 l1
  2. 子串 1 长度为 i-1,子串 2 长度为 j 的公共子序列长度 l2

可以画一个表格辅助理解,以示例来说(行为 text1 的长度,列为 text2 的长度,表格的值为公共子序列长度):

长度0123
00000
10111
20111
30122
40122
50123

代码:

public int longestCommonSubsequence(String text1, String text2) {
    int m = text1.length(), n = text2.length();

    // 定义 dp[][] 数组,其中 dp[i][j] 表示字符串 text1 长度为 i,text2 长度为 j 时
    // 两个字符串的最大公共子序列长度
    int[][] dp = new int[m+1][n+1];

    // 遍历字符串
    for (int i = 1; i <= m; i++) {
        for (int j = 1; j <= n; j++) {
            // 如果当前两个子串的最后一个字符相等,那么结果为不考虑这两个字符时,即两个子串长度都减一时的结果 加一
            if (text1.charAt(i-1) == text2.charAt(j-1)) {
                dp[i][j] = dp[i-1][j-1]+1;
            } else {
                dp[i][j] = Math.max(dp[i][j-1], dp[i-1][j]);
            }
        }
    }
    return dp[m][n];
}

416. 分割等和子集 - 中等

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

示例:

输入:nums = [1,5,11,5]

输出:true

解释:数组可以分割成 [1, 5, 5] 和 [11] 。

题解:

要分割成等和子集,首先数组求和必须是偶数,否则无法等和分割。

数组求和 sum 若为偶数,取其一半,即为等和子集的元素和 target

分解成子问题,即 [0,target] 这个区间内,每个数值是否可以由数组中的元素求和得到,同时,后续还可以利用前面的结果求出当前结果。

声明一个二维数组 dp[][],其中 dp[i][j] 表示数组中第 [0,i] 个元素可否求和出 j

遍历数组元素:

  • 如果当前遍历元素大于等于求和 j,那么利用之前求和结果以及之前求和 j-当前元素 的结果判断是否可以求和 j,即 dp[i][j] = dp[i-1][j] | dp[i-1][j-nums[i-1]]
  • 如果当前遍历元素小于求和 j,那么可否求和 j 取决之前遍历元素可否求和出 j,即 dp[i][j] = dp[i-1][j]

例如,对于数组 [1,2,5] 可否分割等和子集,即求数组中的元素是否可以求和出 4

  • 当不取任何元素时,可以求和出零。
  • 画出一个表格(行表示数组元素,列表示求和),查看遍历到每个元素时,可否求和出某个和 j
01234
0✔️
1 (1)✔️✔️
2 (2)✔️✔️✔️✔️
3 (5)✔️✔️✔️✔️✔️

代码:

public boolean canPartition(int[] nums) {
    // 求和,如果不为偶数,无法分割
    int sum = 0;
    for (int num : nums) {
        sum += num;
    }
    if ((sum & 1) == 1) {
        return false;
    }
    // 确定分割子集的和
    int target = sum/2;
    int n = nums.length;
    // 声明 dp[][],dp[i][j] 表示遍历到第 i 个数字时,是否可以求和为 j
    boolean[][] dp = new boolean[n+1][target+1];
    dp[0][0] = true;

    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= target; j++) {
            if (j < nums[i-1]) {
                // 如果当前数字大于求和数值,那么遍历到当前数字时,可否求和 j,取决于之前的数字可否求和 j
                dp[i][j] = dp[i-1][j];
            } else {
                // 当前数字小于等于求和 j ,那么遍历到当前数字时
                // 之前遍历的数字可否求和 j,以及可否求和 j 减去当前数字
                dp[i][j] = dp[i-1][j] | dp[i-1][j-nums[i-1]];
            }
        }
    }

    return dp[n][target];
}