例题

1. 全排列

描述
image.png
思路
对每一个位置,使用或不使用,进行回溯搜索。使用used数组来标记。
代码
Python代码:

  1. class Solution:
  2. def permute(self, nums: List[int]) -> List[List[int]]:
  3. res = []
  4. length = len(nums)
  5. used = [False for i in nums]
  6. path = []
  7. def dfs(nums, depth, path, length, used):
  8. if depth == length:
  9. res.append(path.copy())
  10. return
  11. for i in range(len(nums)):
  12. if used[i]:
  13. continue
  14. path.append(nums[i])
  15. used[i] = True
  16. dfs(nums, depth+1, path, length, used)
  17. path.pop()
  18. used[i] = False
  19. dfs(nums,0,path,length,used)
  20. return res

Java代码:

class Solution {
    public List<List<Integer>> permute(int[] nums) {
        int len = nums.length;
        List<List<Integer>> res = new ArrayList<>();
        if (len == 0) {
            return res;
        }
        Deque<Integer> path = new ArrayDeque<Integer>();
        boolean[] used = new boolean[len];
        dfs(nums, len, 0, path, used, res);
        return res;
    }

    private void dfs(int[] nums, int len, int depth, Deque<Integer> path, boolean[] used, List<List<Integer>> res) {
        if (depth == len) {
            res.add(new ArrayList<>(path));
        }

        for (int i=0; i <len; i++) {
            if (used[i]) {
                continue;
            }
            path.addLast(nums[i]);
            used[i] = true;
            dfs(nums, len, depth+1, path, used, res);
            path.removeLast();
            used[i] = false;
        }
    }
}

2. 全排列II

描述
image.png
思路
此题同上题的区别在于,存在重复元素。
有两种办法:

  • 在加入结果集时候进行去重;
  • 搜索的时候进行剪枝

如果要比较两个列表是否一样,一个很显然的办法是分别排序,然后逐个比对。既然要排序,我们可以在搜索之前就对候选数组排序,一旦发现这一支搜索下去可能搜索到重复的元素就停止搜索,这样结果集中不会包含重复元素。
排列组合 - 图3
上一次的 1 刚刚被撤销,正是因为刚被撤销,下面的搜索中还会使用到,因此会产生重复,剪掉的就应该是这样的分支

核心:

  • 数组先排序
  • 增加以下代码

    if i > 0 and nums[i] == nums[i-1] and not used[i-1]:
                      continue
    
    if (i > 0 && nums[i] == nums[i-1] && !used[i-1]) {
                  continue;
              }
    

    代码
    Python代码:

    class Solution:
      def permuteUnique(self, nums: List[int]) -> List[List[int]]:
          def dfs(path, depth, used, i):
              if depth == len(nums):
                  res.append(path.copy())
    
              for i in range(len(nums)):
                  if used[i]:
                      continue
                  if i > 0 and nums[i] == nums[i-1] and not used[i-1]:
                      continue
                  used[i] = True
                  path.append(nums[i])
                  dfs(path, depth+1, used, i)
                  used[i] = False
                  path.pop()
          nums.sort()
          res = []
          path = []
          used = [False] * len(nums)
          dfs(path, 0, used, 0)
          return res
    

    Java代码:

    class Solution {
      public List<List<Integer>> permuteUnique(int[] nums) {
          int len = nums.length;
          List<List<Integer>> res = new ArrayList<>();
          if (len == 0) {
              return res;
          }
          Deque<Integer> path = new ArrayDeque<Integer>();
          boolean[] used = new boolean[len];
          Arrays.sort(nums);
          dfs(nums, len, 0, path, used, res);
          return res;
      }
    
      private void dfs(int[] nums, int len, int depth, Deque<Integer> path, boolean[] used, List<List<Integer>> res) {
          if (depth == len) {
              res.add(new ArrayList<>(path));
          }
    
          for (int i=0; i <len; i++) {
              if (used[i]) {
                  continue;
              }
              if (i > 0 && nums[i] == nums[i-1] && !used[i-1]) {
                  continue;
              }
              path.addLast(nums[i]);
              used[i] = true;
              dfs(nums, len, depth+1, path, used, res);
              path.removeLast();
              used[i] = false;
          }
      }
    }
    

    3. lc 377. 组合总和 Ⅳ

    描述
    image.png
    思路

  • 输入数组的每个元素可以使用多次,这一点和「完全背包」问题有点像;

  • 顺序不同的序列被视作不同的组合,这一点和所有的「背包问题」都不同,与 518. 零钱兑换 II 问题不同的地方就在这一点。

常规思路大概有「回溯搜索」、「动态规划」;不需要记录搜索路径的,可以用动态规划来做。
代码
Java代码:

public class Solution {

    /**
     * 这里状态定义就是题目要求的,并不难,状态转移方程要动点脑子,也不难:
     * 状态转移方程:dp[i]= dp[i - nums[0]] + dp[i - nums[1]] + dp[i - nums[2]] + ... (当 [] 里面的数 >= 0)
     * 特别注意:dp[0] = 1,表示,如果那个硬币的面值刚刚好等于需要凑出的价值,这个就成为 1 种组合方案
     * 再举一个具体的例子:nums=[1, 3, 4], target=7;
     * dp[7] = dp[6] + dp[4] + dp[3]
     * 即:7 的组合数可以由三部分组成,1 和 dp[6],3 和 dp[4], 4 和dp[3];
     *
     * @param nums
     * @param target
     * @return
     */
    public int combinationSum4(int[] nums, int target) {
        int[] dp = new int[target + 1];
        // 这个值被其它状态参考,设置为 1 是合理的
        dp[0] = 1;

        for (int i = 1; i <= target; i++) {
            for (int num : nums) {
                if (num <= i) {
                    dp[i] += dp[i - num];
                }
            }
        }
        return dp[target];
    }
}

Python代码:

class Solution:
    def combinationSum4(self, nums: List[int], target: int) -> int:
        dp = [0] * (target + 1)

        dp[0] = 1
        for i in range(1, target+1):
            for num in nums:
                if num <= i:
                    dp[i] += dp[i - num]
        return dp[target]

4. lc 518. 零钱兑换 II

描述
image.png
思路
这道题是典型的“完全背包问题”。“完全背包问题”的特点是:背包里的物品可以无限次选取。

本题特殊的地方在于:从背包里选取的物品必须刚刚好装满需要考虑的容量,而不是小于等于就好,注意这点细微的区别。

完全背包问题是基于 0-1 背包问题的扩展。它们的不同之处在于:
0-1 背包问题:当前考虑的物品用或者不用;
完全背包问题:当前考虑的物品用或者不用,如果用,用几个。
代码
Python代码:

class Solution:
    def change(self, amount: int, coins: List[int]) -> int:
        dp = [0] * (amount + 1)
        dp[0] = 1

        for coin in coins:
            for x in range(coin, amount + 1):
                dp[x] += dp[x - coin]
        return dp[amount]

这里是遍历每一个硬币值,然后遍历钱数。

Java完全背包:

class Solution {
    public int change(int amount, int[] coins) {
        int n = coins.length;
        int[] dp = new int[amount + 1];
        dp[0] = 1;
        for (int i = 0; i < n; i++) {
            for (int j = coins[i]; j <= amount; j++) {
                dp[j] = dp[j] + dp[j - coins[i]];
            }
        }
        return dp[amount];
    }
}

5. lc 322 零钱兑换

image.png
思路

  • 动态规划
    • 每个金额,遍历硬币数,取最小的值,注意当前金币是否能达到该金额
    • 遍历每个硬币,计算能组成的amout的最小值
  • 两种方式初始化不太一样

代码
Java代码1:

public class Solution {
  public int coinChange(int[] coins, int amount) {
    int max = amount + 1;
    int[] dp = new int[amount + 1];
    Arrays.fill(dp, max);
    dp[0] = 0;
    for (int i = 1; i <= amount; i++) {
      for (int j = 0; j < coins.length; j++) {
        if (coins[j] <= i) {
          dp[i] = Math.min(dp[i], dp[i - coins[j]] + 1);
        }
      }
    }
    return dp[amount] > amount ? -1 : dp[amount];
  }
}

Java代码2:

class Solution {
    public int coinChange(int[] coins, int amount) {
        int[] dp = new int[amount+1];
        Arrays.fill(dp, -1);
        dp[0] = 0;
        for (int i = 1; i < amount + 1; i++) {
            for (int coin : coins) {
                if (i - coin >= 0 && dp[i - coin] != -1) {
                    if (dp[i] == -1 || dp[i] > dp[i - coin] + 1) {
                        dp[i] = dp[i-coin] + 1;
                    }
                }
            }
        }
        return dp[amount];
    }
}

Python代码1:

class Solution:
    def coinChange(self, coins: List[int], amount: int) -> int:
        dp = [float('inf')] * (amount + 1)
        dp[0] = 0

        for coin in coins:
            for x in range(coin, amount + 1):
                dp[x] = min(dp[x], dp[x - coin] + 1)
        return dp[amount] if dp[amount] != float('inf') else -1

Python代码2:

class Solution:
    def coinChange(self, coins: List[int], amount: int) -> int:
        n = amount
        dp = [-1 for i in range(n+1)]
        dp[0] = 0
        for i in range(1, n+1):
            for coin in coins:
                if i - coin >= 0 and dp[i-coin] != -1:
                    if dp[i] == -1 or dp[i] > dp[i-coin] + 1:
                        dp[i] = dp[i-coin] + 1
        return dp[n]

6. 组合

image.png
思路

  • 回溯

代码

class Solution:
    def combine(self, n: int, k: int) -> List[List[int]]:
        def backtrack(begin, path):
            if len(path) == k:
               res.append(path.copy())

            for i in range(begin, n+1):
                path.append(i)
                backtrack(i+1, path)
                path.pop()

        res = []
        path = []
        backtrack(1, path)
        return res

7. 组合总和II

image.png

public class Solution {

    public List<List<Integer>> combinationSum2(int[] candidates, int target) {
        int len = candidates.length;
        List<List<Integer>> res = new ArrayList<>();
        if (len == 0) {
            return res;
        }

        // 关键步骤
        Arrays.sort(candidates);

        Deque<Integer> path = new ArrayDeque<>(len);
        dfs(candidates, len, 0, target, path, res);
        return res;
    }

    /**
     * @param candidates 候选数组
     * @param len        冗余变量
     * @param begin      从候选数组的 begin 位置开始搜索
     * @param target     表示剩余,这个值一开始等于 target,基于题目中说明的"所有数字(包括目标数)都是正整数"这个条件
     * @param path       从根结点到叶子结点的路径
     * @param res
     */
    private void dfs(int[] candidates, int len, int begin, int target, Deque<Integer> path, List<List<Integer>> res) {
        if (target == 0) {
            res.add(new ArrayList<>(path));
            return;
        }
        for (int i = begin; i < len; i++) {
            // 大剪枝:减去 candidates[i] 小于 0,减去后面的 candidates[i + 1]、candidates[i + 2] 肯定也小于 0,因此用 break
            if (target - candidates[i] < 0) {
                break;
            }

            // 小剪枝:同一层相同数值的结点,从第 2 个开始,候选数更少,结果一定发生重复,因此跳过,用 continue
            if (i > begin && candidates[i] == candidates[i - 1]) {
                continue;
            }

            path.addLast(candidates[i]);
            // 调试语句 ①
            // System.out.println("递归之前 => " + path + ",剩余 = " + (target - candidates[i]));

            // 因为元素不可以重复使用,这里递归传递下去的是 i + 1 而不是 i
            dfs(candidates, len, i + 1, target - candidates[i], path, res);

            path.removeLast();
            // 调试语句 ②
            // System.out.println("递归之后 => " + path + ",剩余 = " + (target - candidates[i]));
        }
    }

    public static void main(String[] args) {
        int[] candidates = new int[]{10, 1, 2, 7, 6, 1, 5};
        int target = 8;
        Solution solution = new Solution();
        List<List<Integer>> res = solution.combinationSum2(candidates, target);
        System.out.println("输出 => " + res);
    }
}