经典考题7 - Two Sum系列

336 阅读6分钟

2-Sum系列解法:

2-Sum:

  • hashmap/set(O(N)
    • 只可以找确定的target,速度快,也便于统计target出现次数
    • 不便于去重
  • 双指针
    • 便于去重
    • 可以用来找相近答案
    • 可以集合iterator处理tree类 (ie:LC653)
      • 可以节省空间(O(N) -> O(h)

3-Sum / 4-Sum:

  • 双指针(本质上都可以简化成2-Sum)
    • “k-sum(k >= 3)系列”都建议使用“双指针”

K-Sum:

  • 递归

2-Sum

170. 两数之和 III - 数据结构设计(Easy)

image.png

Solu:HashMap

ie:比如find(2),但是迄今只有一次add(1),那么虽然key = 2 - 1 = 1存在,但是出现频率至少要≥ 2

Code:

class TwoSum:
    
    def __init__(self):
        self.dic = {}
    
    def add(self, number: int) -> None:
        self.dic[number] = self.dic.get(number, 0) + 1
    
    def find(self, value: int) -> bool:
        for num in self.dic:
            if value - num in self.dic and (value - num != num or self.dic[num] > 1):
                return True
        return False


653. 两数之和 IV - 输入 BST(Easy)

image.png

Solu 1:双指针 O(N)

  • BST的中序遍历是一个sorted array
  • 对一个sorted array用双指针在O(N)时间内找2-Sum

Code 1:

class Solution:
    def findTarget(self, root: Optional[TreeNode], k: int) -> bool:
        def inOrder(node) -> List[int]:
            if not node:
                return []
            return inOrder(node.left) + [node.val] + inOrder(node.right)
        
        def find(nums, target) -> bool:
            left, right = 0, len(nums) - 1
            while left < right:
                x = nums[left] + nums[right]
                if x == target:
                    return True
                elif x < target:
                    left += 1
                else:
                    right -= 1
            return False
        
        nums = inOrder(root)
        return find(nums, k)

Solu 2:空间优化O(N)->O(h):stack构建BST iterator ❤️

详见173. 二叉搜索树迭代器

  • 正向和反向分别建立一个iterator,空间复杂度O(h),时间复杂度O(N)
    • left++ -> 正向iterate
    • right-- -> 反向iterate

Code 2:

class BSTIterator:

    def __init__(self, root: TreeNode, forward: bool):
        self.stack = []
        self.forward = forward
        if forward:
            self.pushAllLeft(root)
        else:
            self.pushAllRight(root)
    
    def next(self) -> int:
        node = self.stack.pop()
        if self.forward:
            self.pushAllLeft(node.right)
        else:
            self.pushAllRight(node.left)
        return node.val
    
    def hasNext(self) -> bool:
        return len(self.stack) > 0
    
    def pushAllLeft(self, root):
        while root:
            self.stack.append(root)
            root = root.left
    
    def pushAllRight(self, root):
        while root:
            self.stack.append(root)
            root = root.right
    
    def peek(self) -> int:
        return self.stack[-1].val


class Solution:
    def findTarget(self, root: Optional[TreeNode], k: int) -> bool:
        l, r = BSTIterator(root, True), BSTIterator(root, False)
        while l.hasNext() and r.hasNext() and l.peek() < r.peek():
            x = l.peek() + r.peek()
            if x == k:
                return True
            elif x < k:
                l.next()  # 变大
            else:
                r.next()  # 变小
        return False


3-Sum

套路:第一个指针i切开,右侧arraynums[i+1:]双指针做2-sum

15. 三数之和(Medium)

Solu:双指针

3次去重:

  • 2-Sum前:对当前基准nums[i]去重
  • 2-Sum时:对nums[left]nums[right]分别去重

2次剪枝:

  • if 当前max = nums[i] + nums[-1] + nums[-2] < 0, then nums[i]太小,直接跳到下一轮
  • if 当前min = nums[i] + nums[i + 1] + nums[i + 2] > 0, then nums[i]太大,后面的都不用算了

Code:

class Solution:
    def threeSum(self, nums: List[int]) -> List[List[int]]:
        nums.sort()
        res = []
        for i in range(len(nums) - 2):
            if nums[i] + nums[-1] + nums[-2] < 0:  # 剪枝
                continue
            if nums[i] + nums[i + 1] + nums[i + 2] > 0:  # 剪枝
                break
            if i > 0 and nums[i] == nums[i - 1]:  # 去重nums[i]
                continue
            # 2-Sum
            l, r = i + 1, len(nums) - 1
            while l < r:
                total = nums[i] + nums[l] + nums[r]
                if total == 0:
                    res.append([nums[i], nums[l], nums[r]])
                    l += 1
                    r -= 1
                    while l < r and nums[l] == nums[l - 1]:  # 去重nums[l]
                        l += 1
                    while l < r and nums[r] == nums[r + 1]:  # 去重nums[r]
                        r -= 1
                elif total > 0:
                    r -= 1
                else:
                    l += 1
        return res


16. 最接近的三数之和(Medium)

image.png

Solu:双指针

2次剪枝同上,略

只要当前sum != target,即使abs(sum - target) < abs(cur_closest - target),也要继续尝试令sum逼近target

Code:

class Solution:
    def threeSumClosest(self, nums: List[int], target: int) -> int:
        nums.sort()
        closest = nums[0] + nums[1] + nums[2]
        for i in range(len(nums) - 2):
            if i > 0 and nums[i] == nums[i - 1]:  # skip duplicates
                continue
            if nums[i] + nums[i + 1] + nums[i + 2] > target:  # 剪枝
                if nums[i] + nums[i + 1] + nums[i + 2] - target < abs(closest - target):
                    closest = nums[i] + nums[i + 1] + nums[i + 2]
                break
            if nums[i] + nums[-1] + nums[-2] < target:  # 剪枝
                if target - (nums[i] + nums[-1] + nums[-2]) < abs(closest - target):
                    closest = nums[i] + nums[-1] + nums[-2]
                continue
            l, r = i + 1, len(nums) - 1
            while l < r:
                total = nums[i] + nums[l] + nums[r]
                if total == target:
                    return target
                else:
                    if abs(total - target) < abs(closest - target):
                        closest = total
                    # 继续尝试逼近target
                    if total > target:
                        r -= 1
                    else:
                        l += 1
        return closest


259. 较小的三数之和(Medium)

image.png

Solu:双指针

  • 如果nums[i]+nums[l]+nums[r] < target,说明对于任意j满足l+1 ≤ j ≤ r,都有nums[i]+nums[l]+nums[j] < target。这样的j共有r - (l+1) + 1 = r - l

2次剪枝:

  • if min = nums[i] + nums[i + 1] + nums[i + 2] >= target,那么后面的也没必要算了,必定不满足条件
  • if max = nums[i] + nums[-1] + nums[-2] < target,那么只要对于当前nums[i],只要在subArray nums[i+1:]中任选两个元素,都可以满足3-sum < target

Code:

class Solution:
    def threeSumSmaller(self, nums: List[int], target: int) -> int:
        nums.sort()
        count = 0
        for i in range(len(nums) - 2):
            if nums[i] + nums[i + 1] + nums[i + 2] >= target:  # 剪枝
                break
            if nums[i] + nums[-1] + nums[-2] < target:  # 剪枝
                length = len(nums) - i - 1
                count += length * (length - 1) // 2
                continue
            l, r = i + 1, len(nums) - 1
            while l < r:
                total = nums[i] + nums[l] + nums[r]
                if total < target:
                    count += r - l
                    l += 1
                else:
                    r -= 1
        return count


923. 三数之和的多种可能(Medium)

image.png

Solu 1:HashMap + 3Sum传统做法

  • 对于每个arr[i],在subarray arr[i+1 : ]中统计2-sum == target - arr[i]的个数

Code 1:

class Solution:
    def threeSumMulti(self, arr: List[int], target: int) -> int:
        def twoSum(start, end, k):
            dic = {}
            count = 0
            for i in range(start, end):
                count += dic.get(k - arr[i], 0)
                dic[arr[i]] = dic.get(arr[i], 0) + 1
            return count
        
        mod = 10 ** 9 + 7
        res = 0
        arr.sort()
        for i in range(len(arr)):
            if i < len(arr) - 2 and arr[i] + arr[i + 1] + arr[i + 2] > target:  # prune
                break
            if arr[i] + arr[-1] + arr[-2] < target:  # prune
                continue
            res = (res + twoSum(i + 1, len(arr), target - arr[i])) % mod  # count
        return res

Solu 2:

  • 统计对于当前arr[i],在subarray arr[ : i]2-sum = target - arr[i]出现的次数
  • 为了下一轮loop的统计,更新当前arr[j] + arr[i] (0 ≤ j < i)出现的次数

Code 2:

class Solution:
    def threeSumMulti(self, arr: List[int], target: int) -> int:
        mod = 10 ** 9 + 7
        count = 0
        dic = {}
        for i in range(len(arr)):
            count = (count + dic.get(target - arr[i], 0)) % mod  # count
            for j in range(i):  # update dict
                dic[arr[i] + arr[j]] = dic.get(arr[i] + arr[j], 0) + 1
        return count

Solu 3:hashMap + 组合 ❤️

  • 先统计每个numarr中出现的frequency
  • 任选三个数字,以下三种case时值得考虑的(避免重复统计):
    • num1 == num2 == num3 -> C(#num1, 3)
    • num1 == num2 != num3 -> C(#num1, 2) * #num3
    • num1 < num2 < num3 -> #num1 * #num2 * #num3

Code 3:

class Solution:
    def threeSumMulti(self, arr: List[int], target: int) -> int:
        mod = 10 ** 9 + 7
        count = 0
        dic = collections.Counter(arr)
        for num1 in dic.keys():
            for num2 in dic.keys():
                num3 = target - num1 - num2
                cnt1, cnt2, cnt3 = dic[num1], dic[num2], dic[num3]
                if num1 == num2 == num3:  # 如果三个数相等,则相当于cnt1中任选三个
                    count += cnt1 * (cnt1 - 1) * (cnt1 - 2) // 6
                elif num1 == num2:  # 如果num1==num2,则相当于cnt1中任选两个
                    count += cnt1 * (cnt1 - 1) // 2 * cnt3
                elif num1 < num2 < num3:
                    count += cnt1 * cnt2 * cnt3
                count %= mod
        return count


4-Sum

18. 四数之和(Medium)

image.png

Solu:

  • 和3-sum的思路完全一致,不过现在是暴力枚举前两位数字nums[i]nums[j],然后在subarray nums[j+1 : ]上用“双指针”做2-sum
  • 剪枝的思路也和3-sum一致,略

Code:

class Solution:
    def fourSum(self, nums: List[int], target: int) -> List[List[int]]:
        nums.sort()
        res = []
        for i in range(len(nums) - 3):
            if i > 0 and nums[i] == nums[i - 1]:  # 去重
                continue
            if sum(nums[i:i + 4]) > target:  # 剪枝
                break
            if sum(nums[-3:]) < target - nums[i]:  # 剪枝
                continue
            for j in range(i + 1, len(nums) - 2):
                if j > i + 1 and nums[j] == nums[j - 1]:  # 剪枝
                    continue
                if sum(nums[j:j + 3]) > target - nums[i]:  # 剪枝
                    break
                if sum(nums[-2:]) < target - nums[i] - nums[j]:  # 去重
                    continue
                # 2-sum
                l, r = j + 1, len(nums) - 1
                while l < r:
                    total = nums[i] + nums[j] + nums[l] + nums[r]
                    if total == target:
                        res.append([nums[i], nums[j], nums[l], nums[r]])
                        l += 1
                        r -= 1
                        while l < r and nums[l] == nums[l - 1]:
                            l += 1
                        while l < r and nums[r] == nums[r + 1]:
                            r -= 1
                    elif total < target:
                        l += 1
                    else:
                        r -= 1
        return res


454. 四数相加 II(Medium)

image.png

Solu:

  • 对于∀x in nums1∀y in nums2,统计所有x+y出现的频率
  • 对于∀m in nums3∀n in nums4,因为满足x+y+m+n=0,所以找x+y=-(m+n)出现的频率

Code:

class Solution:
    def fourSumCount(self, nums1: List[int], nums2: List[int], nums3: List[int], nums4: List[int]) -> int:
        dic = {}
        for x in nums1:
            for y in nums2:
                dic[x + y] = dic.get(x + y, 0) + 1
        res = 0
        for m in nums3:
            for n in nums4:
                res += dic.get(-m - n, 0)
        return res


K-Sum

  • 如果只要求k-sum == target的数量(不要求内部具体是哪些数字),那么可以转化为01背包问题
  • 如果要求列出具体是有哪些数字,则DFS暴力法

89 · K数之和(Hard)

image.png

Solu 1:DP - 01背包

转化为01背包问题

  • dp[i][j][k] = #前i个元素里选j个数使得sum == k的方案
  • dp[i][j][k] = dp[i-1][j-1][k-A[i]] (if k ≥ A[i]) + dp[i-1][j][k]

Code 1:滚动数组优化

class Solution:
    def kSum(self, A, k, target):
        n = len(A)
        dp = [[0] * (target + 1) for _ in range(k + 1)]
        dp[0][0] = 1
        
        for i in range(n):
            for j in range(k, 0, -1):
                for w in range(target, A[i] - 1, -1):
                    dp[j][w] += dp[j - 1][w - A[i]]
        
        return dp[k][target]

Solu 2:DFS记忆化递归

  • (start_idx, 还需要添加多少个数字, 还剩多少才能达到target)标识一个状态
  • 把问题DFS不断规约(K Sum -> K-1 Sum),直至k == 0或者变为2-Sum

Code 2:

class Solution:
    
    def __init__(self):
        self.memo = {}
    
    def kSum(self, A, k, target):
        A.sort()
        return self.dfs(A, k, target, 0)
    
    def dfs(self, A, k, remain, idx) -> int:
        if k == 0:
            return 1 if remain == 0 else 0
        if remain < 0:  # pruning
            return 0
        key = (idx, k, remain)
        if key in self.memo:
            return self.memo[key]
        res = 0
        for i in range(idx, len(A)):
            res += self.dfs(A, k - 1, remain - A[i], i + 1)
        self.memo[key] = res
        return res

或者DFS将K-Sum不断削弱至2-Sum

class Solution:
    
    def __init__(self):
        self.memo = {}
    
    def kSum(self, A, k, target):
        A.sort()
        if k < 2:
            return A.count(target)
        return self.dfs(A, k, target, 0)
    
    def dfs(self, A, k, remain, idx) -> int:
        if remain < 0 or A[-1] * k < remain:  # pruning
            return 0
        if k == 2:
            return self.twoSum(A, remain, idx)
        key = (idx, k, remain)
        if key in self.memo:
            return self.memo[key]
        res = 0
        for i in range(idx, len(A)):
            res += self.dfs(A, k - 1, remain - A[i], i + 1)
        self.memo[key] = res
        return res
    
    def twoSum(self, nums, target, idx):
        count = 0
        l, r = idx, len(nums) - 1
        while l < r:
            total = nums[l] + nums[r]
            if total == target:
                count += 1
                l += 1
                r -= 1
            elif total < target:
                l += 1
            else:
                r -= 1
        return count



89 · K数之和(Meidum)

image.png

Solu 1:DFS回溯

  • 类似于在做subset的展开,暴力展开所有解

Code 1:

class Solution:
    
    def __init__(self):
        self.ans = []
    
    def kSumII(self, A, k, target):
        A.sort()
        self.dfs(A, k, target, 0, [])
        return self.ans
    
    def dfs(self, A, k, remain, idx, path):
        if k == 0:
            if remain == 0:
                self.ans.append(path[:])
            return
        if remain < 0:  # 剪枝
            return
        for i in range(idx, len(A)):
            path.append(A[i])
            self.dfs(A, k - 1, remain - A[i], i + 1, path)
            path.pop()

Solu 2:DFS递归(不回溯)

  • DFS递归地去约化问题为(K Sum -> K-1 Sum),直至问题被削弱为2-sum
    • PS:需要单独处理k == 1的case
  • 剪枝 x2:如果最小的数 * k > target 或者 最大的数 * k < target,可以提前停止

Code 2:

class Solution:
    
    def kSumII(self, A, k, target):
        A.sort()
        if k < 2:
            return [[target]] if target in A else []
        return self.dfs(A, k, target, 0, [])
    
    def dfs(self, A, k, remain, idx, path):
        res = []
        if idx == len(A):
            return res
        if k * A[idx] > remain or k * A[-1] < remain:  # pruning
            return res
        if k == 2:
            return self.twoSum(A, remain, idx)
        for i in range(idx, len(A)):
            for path in self.dfs(A, k - 1, remain - A[i], i + 1, path + [A[i]]):
                res.append([A[i]] + path)
        return res
    
    def twoSum(self, nums, target, idx):
        res = []
        l, r = idx, len(nums) - 1
        while l < r:
            total = nums[l] + nums[r]
            if total == target:
                res.append([nums[l], nums[r]])
                l += 1
                r -= 1
            elif total < target:
                l += 1
            else:
                r -= 1
        return res


Reference: