454. 四数相加 II
题目描述
给你四个整数数组 nums1
、nums2
、nums3
和 nums4
,数组长度都是 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, b
和 c, 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. 赎金信
题目描述
给你两个字符串:ransomNote
和 magazine
,判断 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
ransomNote
和magazine
由小写英文字母组成
题解
哈希表入门题目,先用哈希表统计 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 != j
、i != k
且 j != 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
a
、b
、c
和d
互不相同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;
}