题目描述(中等难度)

40. Combination Sum II - 图1

上一道题非常像了,区别在于这里给的数组中有重复的数字,每个数字只能使用一次,然后同样是给出所有和等于 target 的情况。

解法一 回溯法

只需要在上题的基础上改一下就行了。直接看代码吧。

  1. public List<List<Integer>> combinationSum2(int[] candidates, int target) {
  2. List<List<Integer>> ans = new ArrayList<>();
  3. getAnswer(ans, new ArrayList<>(), candidates, target, 0);
  4. /*************修改的地方*******************/
  5. // 如果是 Input: candidates = [2,5,2,1,2], target = 5,
  6. // 输出会出现 [2 2 1] [2 1 2] 这样的情况,所以要去重
  7. return removeDuplicate(ans);
  8. /****************************************/
  9. }
  10. private void getAnswer(List<List<Integer>> ans, ArrayList<Integer> temp, int[] candidates, int target, int start) {
  11. if (target == 0) {
  12. ans.add(new ArrayList<Integer>(temp));
  13. } else if (target < 0) {
  14. return;
  15. } else {
  16. for (int i = start; i < candidates.length; i++) {
  17. temp.add(candidates[i]);
  18. /*************修改的地方*******************/
  19. //i -> i + 1 ,因为每个数字只能用一次,所以下次遍历的时候不从自己开始
  20. getAnswer(ans, temp, candidates, target - candidates[i], i + 1);
  21. /****************************************/
  22. temp.remove(temp.size() - 1);
  23. }
  24. }
  25. }
  26. private List<List<Integer>> removeDuplicate(List<List<Integer>> list) {
  27. Map<String, String> ans = new HashMap<String, String>();
  28. for (int i = 0; i < list.size(); i++) {
  29. List<Integer> l = list.get(i);
  30. Collections.sort(l);
  31. String key = "";
  32. for (int j = 0; j < l.size() - 1; j++) {
  33. key = key + l.get(j) + ",";
  34. }
  35. key = key + l.get(l.size() - 1);
  36. ans.put(key, "");
  37. }
  38. List<List<Integer>> ans_list = new ArrayList<List<Integer>>();
  39. for (String k : ans.keySet()) {
  40. String[] l = k.split(",");
  41. List<Integer> temp = new ArrayList<Integer>();
  42. for (int i = 0; i < l.length; i++) {
  43. int c = Integer.parseInt(l[i]);
  44. temp.add(c);
  45. }
  46. ans_list.add(temp);
  47. }
  48. return ans_list;
  49. }

时间复杂度:

空间复杂度:

看到这里),想法很棒,为了解决重复的情况,我们可以先把数组先排序,这样就好说了。

  1. public List<List<Integer>> combinationSum2(int[] candidates, int target) {
  2. List<List<Integer>> ans = new ArrayList<>();
  3. Arrays.sort(candidates); //排序
  4. getAnswer(ans, new ArrayList<>(), candidates, target, 0);
  5. return ans;
  6. }
  7. private void getAnswer(List<List<Integer>> ans, ArrayList<Integer> temp, int[] candidates, int target, int start) {
  8. if (target == 0) {
  9. ans.add(new ArrayList<Integer>(temp));
  10. } else if (target < 0) {
  11. return;
  12. } else {
  13. for (int i = start; i < candidates.length; i++) {
  14. //跳过重复的数字
  15. if(i > start && candidates[i] == candidates[i-1]) continue;
  16. temp.add(candidates[i]);
  17. /*************修改的地方*******************/
  18. //i -> i + 1 ,因为每个数字只能用一次,所以下次遍历的时候不从自己开始
  19. getAnswer(ans, temp, candidates, target - candidates[i], i + 1);
  20. /****************************************/
  21. temp.remove(temp.size() - 1);
  22. }
  23. }
  24. }

解法二 动态规划

怎么去更改上题的算法满足本题,暂时没想到,只想到就是再写个函数对答案再过滤一次。先记录给定的 nums 中的每个数字出现的次数,然后判断每个 list 的数字出现的次数是不是满足小于等于给定的 nums 中的每个数字出现的次数,不满足的话就剔除掉。如果大家有直接改之前算法的好办法可以告诉我,谢谢了。

此外,要注意一点就是上题中,给定的 nums 没有重复的,而这题中是有重复的。为了使得和之前一样,所以我们在算法中都得加上

  1. if (i > 0 && nums[i] == nums[i - 1]) {
  2. continue;
  3. }

跳过重复的数字,不然是不能 AC 的,至于原因下边分析下。

  1. public List<List<Integer>> combinationSum2(int[] nums, int target) {
  2. List<List<List<Integer>>> ans = new ArrayList<>(); //opt 数组
  3. Arrays.sort(nums);// 将数组有序,这样可以提现结束循环
  4. for (int sum = 0; sum <= target; sum++) { // 从 0 到 target 求出每一个 opt
  5. List<List<Integer>> ans_sum = new ArrayList<List<Integer>>();
  6. for (int i = 0; i < nums.length; i++) { //遍历 nums
  7. /*******************修改的地方********************/
  8. if (i > 0 && nums[i] == nums[i - 1]) {
  9. continue;
  10. }
  11. /***********************************************/
  12. if (nums[i] == sum) {
  13. List<Integer> temp = new ArrayList<Integer>();
  14. temp.add(nums[i]);
  15. ans_sum.add(temp);
  16. } else if (nums[i] < sum) {
  17. List<List<Integer>> ans_sub = ans.get(sum - nums[i]);
  18. //每一个加上 nums[i]
  19. for (int j = 0; j < ans_sub.size(); j++) {
  20. List<Integer> temp = new ArrayList<Integer>(ans_sub.get(j));
  21. temp.add(nums[i]);
  22. ans_sum.add(temp);
  23. }
  24. } else {
  25. break;
  26. }
  27. }
  28. ans.add(sum, ans_sum);
  29. }
  30. return remove(removeDuplicate(ans.get(target)),nums);
  31. }
  32. private List<List<Integer>> removeDuplicate(List<List<Integer>> list) {
  33. Map<String, String> ans = new HashMap<String, String>();
  34. for (int i = 0; i < list.size(); i++) {
  35. List<Integer> l = list.get(i);
  36. Collections.sort(l);
  37. String key = "";
  38. //[ 2 3 4 ] 转为 "2,3,4"
  39. for (int j = 0; j < l.size() - 1; j++) {
  40. key = key + l.get(j) + ",";
  41. }
  42. key = key + l.get(l.size() - 1);
  43. ans.put(key, "");
  44. }
  45. //根据逗号还原 List
  46. List<List<Integer>> ans_list = new ArrayList<List<Integer>>();
  47. for (String k : ans.keySet()) {
  48. String[] l = k.split(",");
  49. List<Integer> temp = new ArrayList<Integer>();
  50. for (int i = 0; i < l.length; i++) {
  51. int c = Integer.parseInt(l[i]);
  52. temp.add(c);
  53. }
  54. ans_list.add(temp);
  55. }
  56. return ans_list;
  57. }
  58. //过滤不满足答案的情况
  59. private List<List<Integer>> remove(List<List<Integer>> list, int[] nums) {
  60. HashMap<Integer, Integer> nh = new HashMap<Integer, Integer>();
  61. List<List<Integer>> ans = new ArrayList<List<Integer>>(list);
  62. //记录每个数字出现的次数
  63. for (int n : nums) {
  64. int s = nh.getOrDefault(n, 0);
  65. nh.put(n, s + 1);
  66. }
  67. for (int i = 0; i < list.size(); i++) {
  68. List<Integer> l = list.get(i);
  69. HashMap<Integer, Integer> temp = new HashMap<Integer, Integer>();
  70. //记录每个 list 中数字出现的次数
  71. for (int n : l) {
  72. int s = temp.getOrDefault(n, 0);
  73. temp.put(n, s + 1);
  74. }
  75. for (int n : l) {
  76. //进行比较
  77. if (temp.get(n) > nh.get(n)) {
  78. ans.remove(l);
  79. break;
  80. }
  81. }
  82. }
  83. return ans;
  84. }

如果不加跳过重复的数字的话,下边的样例不会通过。

40. Combination Sum II - 图2

这是因为我们求 opt 的时候每个列表的数量在以指数级增加,在上一个 opt 的基础上,每一个列表都增加 5 个 列表。

opt [ 1 ] = [ [ 1 ],[ 1 ],[ 1 ],[ 1 ],[ 1 ] ] 数量是 5,

opt [ 2 ] = [

​ [ 1,1 ], [ 1,1 ],[ 1,1 ],[ 1,1 ],[ 1,1 ],

​ [ 1,1 ], [ 1,1 ],[ 1,1 ],[ 1,1 ],[ 1,1 ]

​ [ 1,1 ], [ 1,1 ],[ 1,1 ],[ 1,1 ],[ 1,1 ],

​ [ 1,1 ], [ 1,1 ],[ 1,1 ],[ 1,1 ],[ 1,1 ],

​ [ 1,1 ], [ 1,1 ],[ 1,1 ],[ 1,1 ],[ 1,1 ],,

​ ] 数量是 5 * 5。

opt [ 3 ] 数量是 5 * 5 * 5。

到了 opt [ 9 ] 就是 5 的 9 次方,数量是 1953125 内存爆炸了。

另一个算法也可以改一下

  1. public List<List<Integer>> combinationSum2(int[] nums, int target) {
  2. List<List<List<Integer>>> ans = new ArrayList<>();
  3. Arrays.sort(nums);
  4. if (nums[0] > target) {
  5. return new ArrayList<List<Integer>>();
  6. }
  7. for (int i = 0; i <= target; i++) {
  8. List<List<Integer>> ans_i = new ArrayList<List<Integer>>();
  9. ans.add(i, ans_i);
  10. }
  11. for (int i = 0; i < nums.length; i++) {
  12. /*******************修改的地方********************/
  13. if (i > 0 && nums[i] == nums[i - 1]) {
  14. continue;
  15. }
  16. /***********************************************/
  17. for (int sum = nums[i]; sum <= target; sum++) {
  18. List<List<Integer>> ans_sum = ans.get(sum);
  19. List<List<Integer>> ans_sub = ans.get(sum - nums[i]);
  20. if (sum == nums[i]) {
  21. ArrayList<Integer> temp = new ArrayList<Integer>();
  22. temp.add(nums[i]);
  23. ans_sum.add(temp);
  24. }
  25. if (ans_sub.size() > 0) {
  26. for (int j = 0; j < ans_sub.size(); j++) {
  27. ArrayList<Integer> temp = new ArrayList<Integer>(ans_sub.get(j));
  28. temp.add(nums[i]);
  29. ans_sum.add(temp);
  30. }
  31. }
  32. }
  33. }
  34. return remove(ans.get(target), nums);
  35. }
  36. private List<List<Integer>> remove(List<List<Integer>> list, int[] nums) {
  37. HashMap<Integer, Integer> nh = new HashMap<Integer, Integer>();
  38. List<List<Integer>> ans = new ArrayList<List<Integer>>(list);
  39. for (int n : nums) {
  40. int s = nh.getOrDefault(n, 0);
  41. nh.put(n, s + 1);
  42. }
  43. for (int i = 0; i < list.size(); i++) {
  44. List<Integer> l = list.get(i);
  45. HashMap<Integer, Integer> temp = new HashMap<Integer, Integer>();
  46. for (int n : l) {
  47. int s = temp.getOrDefault(n, 0);
  48. temp.put(n, s + 1);
  49. }
  50. for (int n : l) {
  51. if (temp.get(n) > nh.get(n)) {
  52. ans.remove(l);
  53. break;
  54. }
  55. }
  56. }
  57. return ans;
  58. }

如果不加跳过重复的数字的话,下边的样例不会通过

40. Combination Sum II - 图3

会发现出现了很多重复的结果,就是因为没有跳过重复的 1。在求 opt [ 1 ] 的时候就变成了 [ [ 1 ],[ 1 ] ] 这样子,由于后边求的时候都是直接在原来每一个列表里加数字,所有后边都是加了两次了。

和上题很像,基本上改一改就好了。排序的来排除重复的情况也很妙。还有就是改算法的时候,要考虑到题的要求的变化之处。

windliang wechat

添加好友一起进步~

如果觉得有帮助的话,可以点击 这里 给一个 star 哦 ^^

如果想系统的学习数据结构和算法,强烈推荐一个我之前学过的课程,可以点击 这里 查看详情