avatar

Junyuan Lu(陆俊元)

MPhil student at ZJU Intelligent Autonomous Systems Lab

代码随想录算法训练营第七天 | 454. 四数相加II、383. 赎金信、15. 三数之和、18. 四数之和


454. 四数相加II

  • 题目链接:click here

  • 文章讲解:click here

  • 题目描述:给定四个包含整数的数组列表 A , B , C , D ,计算有多少个元组 (i, j, k, l) ,使得 A[i] + B[j] + C[k] + D[l] = 0。

    为了使问题简单化,所有的 A, B, C, D 具有相同的长度 N,且 0 ≤ N ≤ 500 。所有整数的范围在 -2^28 到 2^28 - 1 之间,最终结果不会超过 2^31 - 1 。

  • 思路:
    只统计个数,那么就先把A+B的结果遍历存储在map里,键值对分别为{a+b的和,该和出现的个数}。
    然后再遍历 C+D,看结果的相反数是否存在于map里,若存在,则将结果增加对应键值对的‘值’。

  • 代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    class Solution
    {
    public:
    int fourSumCount(vector<int> &nums1, vector<int> &nums2, vector<int> &nums3, vector<int> &nums4)
    {
    unordered_map<int, int> um;
    for (auto &num1 : nums1)
    {
    for (auto &num2 : nums2)
    {
    int res = num1 + num2;
    if (um.find(res) != um.end())
    {
    um[res]++;
    }
    else
    {
    um[res] = 1;
    }
    }
    }

    int result = 0;

    for (auto &num3 : nums3)
    {
    for (auto &num4 : nums4)
    {
    int res = num3 + num4;
    if (um.find(-res) != um.end())
    {
    result += um[-res];
    }
    }
    }

    return result;
    }
    };
  • 讲解代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    class Solution {
    public:
    int fourSumCount(vector<int>& A, vector<int>& B, vector<int>& C, vector<int>& D) {
    unordered_map<int, int> umap; //key:a+b的数值,value:a+b数值出现的次数
    // 遍历大A和大B数组,统计两个数组元素之和,和出现的次数,放到map中
    for (int a : A) {
    for (int b : B) {
    umap[a + b]++;
    }
    }
    int count = 0; // 统计a+b+c+d = 0 出现的次数
    // 再遍历大C和大D数组,找到如果 0-(c+d) 在map中出现过的话,就把map中key对应的value也就是出现次数统计出来。
    for (int c : C) {
    for (int d : D) {
    if (umap.find(0 - (c + d)) != umap.end()) {
    count += umap[0 - (c + d)];
    }
    }
    }
    return count;
    }
    };
  • 时空复杂度分析:
    双重for :O(n2n^2) 时间复杂度
    使用unordered_map存储,至多 n2n^2 种a+b的和:O(n2n^2) 空间复杂度


383. 赎金信

  • 题目链接:click here

  • 文章讲解:click here

  • 题目描述:给定一个赎金信 (ransom) 字符串和一个杂志(magazine)字符串,判断第一个字符串 ransom 能不能由第二个字符串 magazines 里面的字符构成。如果可以构成,返回 true ;否则返回 false。

    (题目说明:为了不暴露赎金信字迹,要从杂志上搜索各个需要的字母,组成单词来表达意思。杂志字符串中的每个字符只能在赎金信字符串中使用一次。)

  • 思路:
    和昨天的异位词相似,只不过这里杂志集可以包含更多的元素,但一定要含有足够的能表达赎金信的元素。
    遍历杂志构建map,键值对分别是{char,个数}
    再遍历赎金信的char,map里查找,若无,则为false,若有,则对应键值对的值减少1,并检查值是否为负数了,也就是杂志上的该char的数量不够。

  • 讲解思路:
    可以利用char只有从a->z的26个数字的先验信息,如昨天的异位词一样使用数组。

  • 代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    class Solution
    {
    public:
    bool canConstruct(string ransomNote, string magazine)
    {
    unordered_map<char, int> um;
    for (auto c : magazine)
    {
    um[c]++;
    }

    for (auto c : ransomNote)
    {
    um[c]--;
    if (um[c] < 0)
    {
    return false;
    }
    }
    return true;
    }
    };
  • 时空复杂度分析:
    遍历string 1和2:O(n+m) 时间复杂度
    使用unordered_map存储数组2结果:O(m) 空间复杂度


15. 三数之和

  • 题目链接:click here

  • 文章讲解:click here

  • 题目描述:给你一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?请你找出所有满足条件且不重复的三元组。

    注意: 答案中不可以包含重复的三元组。

  • 思路:
    我一开始按照昨天的二数之和的方法,将三数之和写为:遍历第一个数,然后使用二数之和的方法查找符合要求的第二、三个数。
    然后再用 set 的方法来去除重复的结果。
    但是这种做法通不过力扣的第310个测试案例[几百个0的数组],超时。

    所以肯定需要一些预剪枝的操作来加快无谓的查找。
    如何跳过?才能不漏?

    讲解提供了关键操作,先排序,再遍历时,碰到相同元素即可跳过。

    讲解中还提供了排序后的二数之和的新方法:双指针法

    原理和二分法是一样的,即,左右指针分别向中间移动,若当前二数之和大于target,则说明右值较大,需要左移右指针;若小于target,则右移左指针,相等,则意味着找到了符合条件的两数,协同最开始的第一个数,构成了符合条件的三数。

    其次,这里也就可以和二分法一样进行剪枝,若左右指针移动前后,值不变,则可继续移动。

    总的来说,要求结果里不能重复时,先排序可能会降低后续算法的复杂度。

    • 代码:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      class Solution
      {
      public:
      vector<vector<int>> threeSum(vector<int> &nums)
      {
      std::sort(nums.begin(), nums.end());
      vector<vector<int>> res;

      int n = nums.size();
      int left = 0;
      int right = n - 1;
      for (int i = 0; i < n - 2; i++)
      {
      if (nums[i] > 0)
      {
      break;
      }

      if (i > 0 && nums[i] == nums[i - 1]) // 移动过了,和自己之前的值比,是否没发生变化
      {
      continue;
      }

      int target = -nums[i];

      left = i + 1;
      right = n - 1;
      int last_left = 1e6, last_right = 1e6;
      while (left < right)
      {
      if (last_left == nums[left])
      {
      left++;
      continue;
      }

      if (last_right == nums[right])
      {
      right--;
      continue;
      }

      if (nums[left] + nums[right] > target)
      {
      last_right = nums[right];
      right--;
      }
      else if (nums[left] + nums[right] < target)
      {
      last_left = nums[left];
      left++;
      }
      else
      {
      last_right = nums[right];
      last_left = nums[left];

      res.push_back({nums[i], nums[left], nums[right]});

      right--;
      left++;
      }
      }
      }
      return res;
      }
      };
    • 讲解代码:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      class Solution {
      public:
      vector<vector<int>> threeSum(vector<int>& nums) {
      vector<vector<int>> result;
      sort(nums.begin(), nums.end());
      // 找出a + b + c = 0
      // a = nums[i], b = nums[left], c = nums[right]
      for (int i = 0; i < nums.size(); i++) {
      // 排序之后如果第一个元素已经大于零,那么无论如何组合都不可能凑成三元组,直接返回结果就可以了
      if (nums[i] > 0) {
      return result;
      }
      // 错误去重a方法,将会漏掉-1,-1,2 这种情况
      /*
      if (nums[i] == nums[i + 1]) {
      continue;
      }
      */
      // 正确去重a方法
      if (i > 0 && nums[i] == nums[i - 1]) {
      continue;
      }
      int left = i + 1;
      int right = nums.size() - 1;
      while (right > left) {
      // 去重复逻辑如果放在这里,0,0,0 的情况,可能直接导致 right<=left 了,从而漏掉了 0,0,0 这种三元组
      /*
      while (right > left && nums[right] == nums[right - 1]) right--;
      while (right > left && nums[left] == nums[left + 1]) left++;
      */
      if (nums[i] + nums[left] + nums[right] > 0) right--;
      else if (nums[i] + nums[left] + nums[right] < 0) left++;
      else {
      result.push_back(vector<int>{nums[i], nums[left], nums[right]});
      // 去重逻辑应该放在找到一个三元组之后,对b 和 c去重
      while (right > left && nums[right] == nums[right - 1]) right--;
      while (right > left && nums[left] == nums[left + 1]) left++;

      // 找到答案时,双指针同时收缩
      right--;
      left++;
      }
      }

      }
      return result;
      }
      };
    • 时间复杂度:O(n2n^2),第一个数遍历了数组 + 二数之和遍历了剩余的元素

    • 空间复杂度:O(1),一些临时存储


18. 四数之和

  • 题目链接:click here

  • 文章讲解:click here

  • 题目描述:题意:给定一个包含 n 个整数的数组 nums 和一个目标值 target,判断 nums 中是否存在四个元素 a,b,c 和 d ,使得 a + b + c + d 的值与 target 相等?找出所有满足条件且不重复的四元组。

    注意:答案中不可以包含重复的四元组。

  • 思路:
    遍历第一个数,剩余的三数,直接套用上题的接口找对应的target,微微修改一些逻辑即可。
    比如,剪枝的时候,要跳过,不光光是当前值 > target,还要求当前值 > 0。
    三数之和之所以可以直接 target > 0 就剪枝,是因为恰巧 target = 0.

  • 代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    class Solution
    {
    public:
    vector<vector<int>> fourSum(vector<int> &nums, int target)
    {
    std::sort(nums.begin(), nums.end());
    vector<vector<int>> res;

    int n = nums.size();
    int last_size1 = 0;
    for (int i = 0; i < n - 3; i++)
    {
    // 剪枝处理
    if (nums[i] > target && nums[i] >= 0) {
    break; // 这里使用break,统一通过最后的return返回
    }

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

    long target_of_three = target - nums[i];
    threeSum(nums, i + 1, target_of_three, res);
    for (int j = last_size1; j < res.size(); j++)
    {
    res[j].push_back(nums[i]);
    }
    last_size1 = res.size();
    }

    return res;
    }

    void threeSum(vector<int> &nums, int start_idx, long target, vector<vector<int>> &res)
    {
    int n = nums.size();
    int last_size = res.size();
    for (int i = start_idx; i < n - 2; i++)
    {
    // 剪枝处理
    if (nums[i] > target && nums[i] >= 0) {
    break; // 这里使用break,统一通过最后的return返回
    }

    if (i > start_idx && nums[i] == nums[i - 1])
    {
    continue;
    }

    long target_of_two = target - nums[i];
    twoSum(nums, i + 1, target_of_two, res);
    for (int j = last_size; j < res.size(); j++)
    {
    res[j].push_back(nums[i]);
    }
    last_size = res.size();
    }
    }

    void twoSum(vector<int> &nums, int start_idx, long target, vector<vector<int>> &res)
    {
    int n = nums.size();
    int left = start_idx;
    int right = n - 1;

    int last_left = 0, last_right = 0;
    while (left < right)
    {
    if (left > start_idx && last_left == nums[left])
    {
    left++;
    continue;
    }

    if (right < n - 1 && last_right == nums[right])
    {
    right--;
    continue;
    }

    if ((long)nums[left] + nums[right] > target)
    {
    last_right = nums[right];
    right--;
    }
    else if ((long)nums[left] + nums[right] < target)
    {
    last_left = nums[left];
    left++;
    }
    else
    {
    last_right = nums[right];
    last_left = nums[left];

    res.push_back({nums[left], nums[right]});

    right--;
    left++;
    }
    }
    }
    };
  • 时间复杂度:O(n3n^3) 相当于三重for循环

  • 空间复杂度:O(1) 一些临时变量


今日收获

  • 个人博客更新。若Blog有误或是有其他想交流的,左侧有联系方式。

  • 复习:
    今日核心思想:若要求结果中元素不能重复,可以考虑先排序,再搜索,既可以确保不重复,又能提供更强的剪枝依据,加快计算。

    四数相加II:只统计满足情况的数量,直接遍历AB->构建map->遍历CD->查找map,即可。
    赎金信:规模有限的比对问题,可以先用有限大小的数组保存其中一个词条的字频,再遍历另一个词条核对字频
    字频符合要求则比对成功。昨天异位词的要求是“词频完全一致”,这里的要求是“杂志词频>=赎金信词频”。
    三数之和:核心是去重和剪枝,以及排序后的二数之和的双指针写法,原理和二分法是一样的。
    四数之和:在三数之和的基础上写就比较简单了。

  • 耗时:3h