LeetCode 454:四数相加II

给定四个包含整数的数组列表 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 = [ 1, 2]
B = [-2,-1]
C = [-1, 2]
D = [ 0, 2]

输出:
2

解释:
两个元组如下:

  1. (0, 0, 0, 1) -> A[0] + B[0] + C[0] + D[1] = 1 + (-2) + (-1) + 2 = 0
  2. (1, 1, 0, 0) -> A[1] + B[1] + C[0] + D[0] = 2 + (-1) + (-1) + 0 = 0

一.暴力法
最直观的方法就是逐个遍历

class Solution:
    def fourSumCount(self, A: List[int], B: List[int], C: List[int], D: List[int]) -> int:
        hash_set = set()
        for ia in range(len(A)):
            for ib in range(len(B)):
                for ic in range(len(C)):
                    for j in range(len(D)):
                        if A[ia] + B[ib] + C[ic] + D[j] == 0:
                            hash_set.add((ia, ib, ic, j))
        return len(hash_set)

一般情况下,这种方法的时间复杂度是O(A×B×C×D),ABCD分别表示四个列表中的元素个数;题目假设四个列表的长度均为N,则时间复杂度为O(N^4),肯定会超时。

二、优化的暴力法(超时)
使用暴力法时第一个超时的测试样例是第20个,第20个样例中ABCD都是好多0组成的:
A = [0,0,0,0,0,0,…]
B = [0,0,0,0,0,0,…]
C = [0,0,0,0,0,0,…]
D = [0,0,0,0,0,0,…]
这是因为对于这些重复的元素,我们仍然进行了重复遍历,其实是不必要的。通过对每个列表构建哈希表,可以避免重复元素的遍历,哈希表的键是列表中的不重复元素,值是该元素在该列表中出现的次数。
假设A = [1,1,2,3,3,3,3],我们对A构建的哈希表应该是{1:2, 2:1, 3:4}
然后对每个哈希表的键进行暴力组合,如果其和为0,则将对应的值相乘,最后相加即可得结果。

import collections
class Solution:
    def fourSumCount(self, A: List[int], B: List[int], C: List[int], D: List[int]) -> int:
        hash_a = collections.defaultdict(int)
        for i in A:
            hash_a[i] += 1
        hash_b = collections.defaultdict(int)
        for i in B:
            hash_b[i] += 1
        hash_c = collections.defaultdict(int)
        for i in C:
            hash_c[i] += 1
        hash_d = collections.defaultdict(int)
        for i in D:
            hash_d[i] += 1
        
        cnt = 0
        for ka, va in hash_a.items():
            for kb, vb in hash_b.items():
                for kc, vc in hash_c.items():
                    for kd, vd in hash_d.items():
                        if ka + kb + kc + kd == 0:
                            cnt += (va*vb*vc*vd)
        return cnt

这个算法是我们在暴力法的基础上,针对重复元素很多的情况下进行的优化。我们构建四个哈希表的时间消耗为O(4N),假设A、B、C、D中分别有a、b、c、d个不重复元素,则暴力组合的时间消耗为O(a×b×c×d),这样算下来,整体的时间消耗为O(4N+a×b×c×d)。但是,我们知道a、b、c、d的值是可变的,这导致算法很不稳定。

  • 在最好的情况下,即A中的所有元素都是重复的(a=b=c=d=1),BCD也都是重复的,此时时间复杂度为O(4N)→O(N);
  • 在最坏情况下,即列表没有重复元素时(a=b=c=d=N),优化的暴力法时间复杂度仍然为O(N^4),这种方法仍然会超时。

三、哈希表/分治
我们假设从A、B、C、D中分别取了e1,e2,e3,e4,由题意可知,目标就是找到所有满足式(1)的组合数量。
e1 + e2 + e3 + e4 = 0 (1)
式(1)可以化为
e1 + e2 = -e3 - e4 (2)
由于加法的结合律,所以式(2)中的任意两个元素都可以互相交换(但是要注意变号),比如可以换成
e1 + e3 = -e2 - e4
但是无论如何改变元素的位置,我们的要求都是一样的,即等号左边应该等于等号右边。以此结论为基础,我们不妨令e1 + e2 = target,再找到满足 -e3 - e4 = target 的e3、e4即可。换言之,我们将一个大问题分成了两个子问题。

算法思路为:

  • 暴力求解A和B的所有可能的e1+e2的值,将其存到哈希表里,哈希表的键为e1+e2,值为e1+e2出现的次数。假设A=[1,2],B=[1,2],我们嵌套地遍历A和B,应该有1+1=2,1+2=3,1+2=3,2+2=4,那么构建的哈希表应该是{2:1, 3:2, 4:1}

  • 暴力求解C和D的所有可能的-e3-e4的值,与哈希表的键进行对比,如果该值在哈希表的键中,那么说明e3和e4有解,解的数量是键的值,我们将这个值添加到一个新的list中。
    仍以第1.步的例子为例,假设C=[-1,-3],D=[-2,-1],我们嵌套地遍历C和D,应该有-(-1)-(-2)=3,-(-1)-(-1)=2,-(-3)-(-2)=5,-(-3)-(-1)=4;
    对于-(-1)-(-2)=3,哈希表中键3的值为2,此时list=[2]
    对于-(-1)-(-1)=2,哈希表中键2的值为1,此时list=[2,1]
    对于-(-3)-(-2)=5,哈希表中没有键5,此时list=[2,1]
    对于-(-3)-(-1)=4,哈希表中键4的值为1,此时list=[2,1,4]

  • 对list求和,即为答案

import collections
class Solution(object):
    def fourSumCount(self, A, B, C, D):
        hash_map = collections.defaultdict(int)
        result = []
        
        for a in A:
            for b in B:
                hash_map[a+b] += 1
        
        for c in C:
            for d in D:
                if -c-d in hash_map:
                    result.append(hash_map[-c-d])
        return sum(result)

这种方法的时间复杂度是O(N×N+N×N)→O(N²),显然比暴力法的O(N^4)快了不少,而且与优化的暴力法相比非常稳定。但是如果四个列表中都存在大量重复元素,那么优化的暴力算法可能会比这种方法更好。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值