3Sum and 4Sum

3Sum and 4Sum are extensions of a popular algorithm question, the 2Sum.

3Sum

Given an array of integers, find all subsets of the array with 3 values where the 3 values sum up to a target number.

Note: The solution subsets must not contain duplicate triplets.

For example, given the array [-1, 0, 1, 2, -1, -4], and the target 0:
The solution set is: [[-1, 0, 1], [-1, -1, 2]] // The two -1 values in the array are considered to be distinct

There are 2 key procedures in solving this algorithm. Sorting the array, and avoiding duplicates.

Sorting

Sorting your input array allows for powerful assumptions:

You'll make use of these two rules to create an efficient algorithm.

Avoiding Duplicates

Since you pre-sort the array, duplicates will be adjacent to each other. You just need to skip over duplicates by comparing adjacent values:

extension Collection where Element: Equatable {
  
  /// In a sorted collection, replaces the given index with a successor mapping to a unique element.
  ///
  /// - Parameter index: A valid index of the collection. `index` must be less than `endIndex`
  func formUniqueIndex(after index: inout Index) {
    var prev = index
    repeat {
      prev = index
      formIndex(after: &index)
    } while index < endIndex && self[prev] == self[index]
  }
}

A similar implementation is used to get the unique index before a given index:

extension BidirectionalCollection where Element: Equatable {
  
  /// In a sorted collection, replaces the given index with a predecessor that maps to a unique element.
  ///
  /// - Parameter index: A valid index of the collection. `index` must be greater than `startIndex`.
  func formUniqueIndex(before index: inout Index) {
    var prev = index
    repeat {
      prev = index
      formIndex(before: &index)
    } while index > startIndex && self[prev] == self[index]
  }
}

Assembling the Subsets

You'll keep track of 3 indices to represent the 3 numbers. The sum at any given moment is array[l] + array[m] + array[r]:

      m ->      <- r
[-4, -1, -1, 0, 1, 2]
  l   

The premise is quite straightforward (given that you're familiar with 2Sum). You'll iterate l through the array. For every iteration, you also apply the 2Sum algorithm to elements after l. You'll check the sum every time you moving the indices to check if you found match. Here's the algorithm:

func threeSum<T: BidirectionalCollection>(_ collection: T, target: T.Element) -> [[T.Element]] where T.Element: Numeric & Comparable {
  let sorted = collection.sorted()
  var ret: [[T.Element]] = []
  var l = sorted.startIndex
  
  ThreeSum: while l < sorted.endIndex { defer { sorted.formUniqueIndex(after: &l) }
    var m = sorted.index(after: l)
    var r = sorted.index(before: sorted.endIndex)
    
    TwoSum: while m < r && r < sorted.endIndex { 
      let sum = sorted[l] + sorted[m] + sorted[r]
      if sum == target {
        ret.append([sorted[l], sorted[m], sorted[r]])
        sorted.formUniqueIndex(after: &m)
        sorted.formUniqueIndex(before: &r)
      } else if sum < target {
        sorted.formUniqueIndex(after: &m)
      } else {
        sorted.formUniqueIndex(before: &r)
      }
    }
  }
  
  return ret
}

4Sum

Given an array S of n integers, find all subsets of the array with 4 values where the 4 values sum up to a target number.

Note: The solution set must not contain duplicate quadruplets.

Solution

Foursum is a very straightforward extension to the threeSum algorithm. In threeSum, you kept track of 3 indices:

      m ->      <- r
[-4, -1, -1, 0, 1, 2]
  l   

For fourSum, you'll keep track of 4:

         mr ->  <- r
[-4, -1, -1, 0, 1, 2]
  l  ml -> 

Here's the code for it (notice it is very similar to 3Sum):

func fourSum<T: BidirectionalCollection>(_ collection: T, target: T.Element) -> [[T.Element]] where T.Element: Numeric & Comparable {
  let sorted = collection.sorted()
  var ret: [[T.Element]] = []
  
  var l = sorted.startIndex
  FourSum: while l < sorted.endIndex { defer { sorted.formUniqueIndex(after: &l) }
    var ml = sorted.index(after: l)
    
    ThreeSum: while ml < sorted.endIndex { defer { sorted.formUniqueIndex(after: &ml) }
      var mr = sorted.index(after: ml)
      var r = sorted.index(before: sorted.endIndex)
      
      TwoSum: while mr < r && r < sorted.endIndex {
        let sum = sorted[l] + sorted[ml] + sorted[mr] + sorted[r]
        if sum == target {
          ret.append([sorted[l], sorted[ml], sorted[mr], sorted[r]])
          sorted.formUniqueIndex(after: &mr)
          sorted.formUniqueIndex(before: &r)
        } else if sum < target {
          sorted.formUniqueIndex(after: &mr)
        } else {
          sorted.formUniqueIndex(before: &r)
        }
      }
    }
  }
  return ret
}

Written for the Swift Algorithm Club by Kai Chen and Kelvin Lau