24暑假算法刷题 | Day7 | LeetCode 454. 四数之和 II,383. 赎金信,15. 三数之和,18. 四数之和


454. 四数相加 II

点此跳转题目链接

题目描述

给你四个整数数组 nums1nums2nums3nums4 ,数组长度都是 n ,请你计算有多少个元组 (i, j, k, l) 能满足:

  • 0 <= i, j, k, l < n
  • nums1[i] + nums2[j] + nums3[k] + nums4[l] == 0

示例 1:

输入:nums1 = [1,2], nums2 = [-2,-1], nums3 = [-1,2], nums4 = [0,2]
输出:2
解释:
两个元组如下:
1. (0, 0, 0, 1) -> nums1[0] + nums2[0] + nums3[0] + nums4[1] = 1 + (-2) + (-1) + 2 = 0
2. (1, 1, 0, 0) -> nums1[1] + nums2[1] + nums3[0] + nums4[0] = 2 + (-1) + (-1) + 0 = 0

示例 2:

输入:nums1 = [0], nums2 = [0], nums3 = [0], nums4 = [0]
输出:1

提示:

  • n == nums1.length
  • n == nums2.length
  • n == nums3.length
  • n == nums4.length
  • 1 <= n <= 200
  • -228 <= nums1[i], nums2[i], nums3[i], nums4[i] <= 228

题解

由于从一个数组中取出四元组,暴力求解时间复杂度高达 O ( n 4 ) O(n^4) O(n4) ,故考虑分治方法,将四元组 (a, b, c, d) 分为 a, bc, d 两部分,让这两部分求和为0即可。利用哈希表可以分别用两个双重循环实现,时间复杂度为 O ( n 2 ) O(n^2) O(n2)

代码

c++

int fourSumCount(vector<int> &nums1, vector<int> &nums2, vector<int> &nums3, vector<int> &nums4)
{
    // 思路:哈希表 + 分治
    int count = 0;
    unordered_map<int, int> sumMap; // 存储前两个数a + b各种求和结果出现的次数
    for (int a : nums1) {
        for (int b : nums2)
            sumMap[a + b]++;
    }
    // 根据后两个数的和c + d,找凑成0所需的差-(c + d)是否能由之前的a + b得出
    for (int c : nums3) {
        for (int d : nums4)
            count += sumMap[- c - d];
    }
    return count;
}

go

func fourSumCount(nums1 []int, nums2 []int, nums3 []int, nums4 []int) int {
	abSumMap := make(map[int]int)
	for _, a := range nums1 {
		for _, b := range nums2 {
			abSumMap[a+b]++
		}
	}
	count := 0
	for _, c := range nums3 {
		for _, d := range nums4 {
			count += abSumMap[-c-d]
		}
	}
	return count
}

383. 赎金信

点此跳转题目链接

题目描述

给你两个字符串:ransomNotemagazine ,判断 ransomNote 能不能由 magazine 里面的字符构成。

如果可以,返回 true ;否则返回 false

magazine 中的每个字符只能在 ransomNote 中使用一次。

示例 1:

输入:ransomNote = "a", magazine = "b"
输出:false

示例 2:

输入:ransomNote = "aa", magazine = "ab"
输出:false

示例 3:

输入:ransomNote = "aa", magazine = "aab"
输出:true

提示:

  • 1 <= ransomNote.length, magazine.length <= 105
  • ransomNotemagazine 由小写英文字母组成

题解

哈希表入门题目,先用哈希表统计 magazine 中各字符的数量,再拿去和 ransomNote 中的字符对比,看够不够用即可。

代码

c++

bool canConstruct(string ransomNote, string magazine)
{
    // 哈希表秒了
    unordered_map<char, int> charMap; // magazine 中可用的字符及其数量
    for (char c : magazine)
        charMap[c]++;
    for (char c : ransomNote) {
        if (!charMap[c])
            return false;
        charMap[c]--;
    }
    return true;
}

go

func canConstruct(ransomNote string, magazine string) bool {
	charMap := make(map[rune]int)
	for _, c := range magazine {
		charMap[c]++
	}
	for _, c := range ransomNote {
		if charMap[c] == 0 {
			return false
		}
		charMap[c]--
	}
	return true
}

15. 三数之和

点此跳转题目链接

题目描述

给你一个整数数组 nums ,判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i != ji != kj != k ,同时还满足 nums[i] + nums[j] + nums[k] == 0 。请

你返回所有和为 0 且不重复的三元组。

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

示例 1:

输入:nums = [-1,0,1,2,-1,-4]
输出:[[-1,-1,2],[-1,0,1]]
解释:
nums[0] + nums[1] + nums[2] = (-1) + 0 + 1 = 0 。
nums[1] + nums[2] + nums[4] = 0 + 1 + (-1) = 0 。
nums[0] + nums[3] + nums[4] = (-1) + 2 + (-1) = 0 。
不同的三元组是 [-1,0,1] 和 [-1,-1,2] 。
注意,输出的顺序和三元组的顺序并不重要。

示例 2:

输入:nums = [0,1,1]
输出:[]
解释:唯一可能的三元组和不为 0 。

示例 3:

输入:nums = [0,0,0]
输出:[[0,0,0]]
解释:唯一可能的三元组和为 0 。

提示:

  • 3 <= nums.length <= 3000
  • -105 <= nums[i] <= 105

题解

今天最恶心的一道题目 🙃

​ – 2024/7/10

首先好好读题,可以发现要找的三元组涉及两个 “去重” 问题:

  • 三元组内各元素在原数组中的下标不能重复(也就是说,原数组每个位置的元素只能用一次)

    这里指的是元素本身,而不是其数字值。比如数组中本来就有两个1,那么三元组中可以用两个1。

  • 三元组之间不能重复(也就是不能同时返回 (1, 2, 1)(1, 1, 2) 这样数字相同的三元组)

受到之前做过的 四数相加 II 这题影响,一开始想用类似的“哈希表+分治”思路解决,写出如下代码(c++):

vector<vector<int>> threeSum_Fail(vector<int> &nums)
{
    // 哈希表 + 分治
    vector<vector<int>> res;
    unordered_map<int, vector<pair<int, int>>> sumMap; // 前两个数 a, b 之和及其下标
    for (int i = 0; i < nums.size() - 1; i++)
    {
        for (int j = i + 1; j < nums.size(); j++)
            sumMap[nums[i] + nums[j]].emplace_back(i, j);
    }

    // 用一个哈希集合存已得到结果(三元组转为字符串),便于去重
    unordered_set<string> strSet;

    // 查找第三个数 c 凑成 0 所需的前两数之和,即 -c
    for (int i = 0; i < nums.size(); i++)
    {
        if (sumMap.find(-nums[i]) != sumMap.end())
        {
            for (pair p : sumMap[-nums[i]])
            {
                if (i == p.first || i == p.second)
                    continue; // 三元组中不能有同一下标的数字
                vector<int> vec = {nums[i], nums[p.first], nums[p.second]};
                sort(vec.begin(), vec.end());
                string str = to_string(vec[0]) + "_" + to_string(vec[1]) + "_" + to_string(vec[2]);
                if (strSet.find(str) == strSet.end())
                {
                    res.push_back(vec);
                    strSet.insert(str);
                }
            }
        }
    }
    return res;
}

这么写,首先通过 i , j 不重复的二重循环获得前两个数字之和及其下标,再从中找第三个数字凑成0需要的”另一半“。同时,将每次得到的三元组排序后转换成字符串,加入一个哈希集合,达到去重的目标。该算法通过了LeetCode中90%以上的测试样例,但在一个特殊样例上超时:几千个0组成的数组 🤡

超时原因不言而喻:没有全面而有效的剪枝策略。如果还是用哈希表解决,可以参考卡哥代码随想录的去重、剪枝思路,代码如下(c++):

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[j], c = -(a + b)
    for (int i = 0; i < nums.size(); i++) {
        // 排序之后如果第一个元素已经大于零,那么不可能凑成三元组
        if (nums[i] > 0) {
            break;
        }
        if (i > 0 && nums[i] == nums[i - 1]) { //三元组元素a去重
            continue;
        }
        unordered_set<int> set;
        for (int j = i + 1; j < nums.size(); j++) {
            if (j > i + 2
                    && nums[j] == nums[j-1]
                    && nums[j-1] == nums[j-2]) { // 三元组元素b去重
                continue;
            }
            int c = 0 - (nums[i] + nums[j]);
            if (set.find(c) != set.end()) {
                result.push_back({nums[i], nums[j], c});
                set.erase(c);// 三元组元素c去重
            } else {
                set.insert(nums[j]);
            }
        }
    }
    return result;
}

下面采用更适合这题的双指针解法。

所谓”适合“是指去重方法和整体思路更直观

在这里插入图片描述

如图所示(图源:代码随想录 ),首先将数组升序排列,然后定义3个有序的指针 i , left , right ,其中

  • i 是最外层循环指针,逐个遍历数组中的元素
  • left 每次初始化为 i 的下一个位置,然后可向右移动
  • right 每次初始化为数组的最后一个位置,然后可向左移动

可以看出,虽然用了三指针,核心还是后面的两个经典的左右双指针。由于限定这些指针不相遇,所以自然解决了下标去重的问题。然后还需要考虑三元组 a, b, c 之间去重的问题,这可以通过灵活移动指针解决:由于三个指针总是有序的,所以每个指针都不应该“连续指向相同的数”。其他细节去重方法见代码注释。

比如, i 这次指向了-1,若下次又指向-1,相当于考虑了两次 a = -1 的三元组,显然没必要。

代码(C++)

vector<vector<int>> threeSum(vector<int> &nums)
{
    vector<vector<int>> res;
    sort(nums.begin(), nums.end());

    // 目标三元组为 (a, b, c)
    for (int i = 0; i < nums.size(); i++)
    {
        if (nums[i] > 0) break; // 升序排列后,第一个数a仍大于0,则三元组之和必大于0了
        if (i > 0 && nums[i] == nums[i - 1]) continue; // 对a去重

        int left = i + 1, right = nums.size() - 1;
        while (left < right)
        {
            int curSum = nums[i] + nums[left] + nums[right];
            if (curSum == 0)
            {
                res.emplace_back(vector<int>{nums[i], nums[left], nums[right]});

                // 对left和right去重
                while (left < right && nums[left] == nums[left + 1]) left++;
                while (left < right && nums[right] == nums[right - 1]) right--;

                // 双指针向中间移动,进行下一轮
                left++, right--;
            }
            // 没找到则根据情况移动左右指针
            else if (curSum < 0) left++;
            else right--;
        }
    }
    return res;
}

细节深入可参见代码随想录 (programmercarl.com)


18. 四数之和

点此跳转题目链接

题目描述

给你一个由 n 个整数组成的数组 nums ,和一个目标值 target 。请你找出并返回满足下述全部条件且不重复的四元组 [nums[a], nums[b], nums[c], nums[d]] (若两个四元组元素一一对应,则认为两个四元组重复):

  • 0 <= a, b, c, d < n
  • abcd 互不相同
  • nums[a] + nums[b] + nums[c] + nums[d] == target

你可以按 任意顺序 返回答案 。

示例 1:

输入:nums = [1,0,-1,0,-2,2], target = 0
输出:[[-2,-1,1,2],[-2,0,0,2],[-1,0,0,1]]

示例 2:

输入:nums = [2,2,2,2,2], target = 8
输出:[[2,2,2,2]]

提示:

  • 1 <= nums.length <= 200
  • -109 <= nums[i] <= 109
  • -109 <= target <= 109

题解

在做过15. 三数之和后,发现这题采用同样的思路解决,只不过在最外层添加一层遍历即可。三数之和的算法参见我的笔记Carl的讲解

代码(C++)

vector<vector<int>> fourSum(vector<int> &nums, int target)
{
    // 思路:双指针,在三数求和的基础上在外套一层循环即可
    // 记四元组为(a, b, c, d)
    // 分别对应代码中的(nums[i], nums[j], nums[left], nums[right])
    vector<vector<int>> res;
    sort(nums.begin(), nums.end());
    for (int i = 0; i < nums.size(); i++)
    {
        if (nums[i] > target && nums[i] >= 0)
            break; // 剪枝
        if (i > 0 && nums[i - 1] == nums[i])
            continue; // 对a去重

        // 下面基本就是三数求和
        for (int j = i + 1; j < nums.size(); j++)
        {
            if (nums[i] + nums[j] > target && nums[i] + nums[j] >= 0)
                break; // 剪枝
            if (j > i + 1 && nums[j - 1] == nums[j])
                continue; // 对b去重

            int left = j + 1, right = nums.size() - 1;
            while (left < right)
            {
                long long curSum = static_cast<long long>(nums[i]) + static_cast<long long>(nums[j])
                                    + static_cast<long long>(nums[left]) + static_cast<long long>(nums[right]);
                if (curSum == target)
                {
                    res.emplace_back(vector<int>{nums[i], nums[j], nums[left], nums[right]});
                    while (left < right && nums[left] == nums[left + 1])
                        left++; // 对c去重
                    while (left < right && nums[right] == nums[right - 1])
                        right--; // 对d去重
                    left++, right--;
                }
                else if (curSum < target)
                    left++;
                else
                    right--;
            }
        }
    }
    return res;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值