3Sum과 4Sum은 널리 알려진 알고리즘 문제인 2Sum의 확장 버전이다.
정수 배열이 주어졌을 때, 배열에서 3개의 값을 선택해 그 합이 목표 숫자가 되는 모든 부분집합을 찾는 문제다.
주의: 해결책으로 제시된 부분집합은 중복된 세 쌍을 포함해서는 안 된다.
예를 들어, 배열이 [-1, 0, 1, 2, -1, -4]이고 목표 숫자가 0인 경우:
해결책 집합은 [[-1, 0, 1], [-1, -1, 2]]가 된다. // 배열에 있는 두 개의 -1 값은 서로 다른 값으로 간주된다.
이 알고리즘을 해결하는 데는 두 가지 주요 절차가 있다. 배열을 정렬하는 것과 중복을 피하는 것이다.
입력 배열을 정렬하면 두 가지 강력한 가정을 할 수 있다:
이 두 가지 규칙을 활용해 효율적인 알고리즘을 만들 수 있다.
배열을 미리 정렬하면 중복된 값들이 서로 인접하게 배치된다. 따라서 인접한 값을 비교하면서 중복을 건너뛰면 된다:
extension Collection where Element: Equatable {
/// 정렬된 컬렉션에서 주어진 인덱스를 고유한 요소를 가리키는 다음 인덱스로 대체한다.
///
/// - Parameter index: 컬렉션의 유효한 인덱스. `index`는 `endIndex`보다 작아야 한다.
func formUniqueIndex(after index: inout Index) {
var prev = index
repeat {
prev = index
formIndex(after: &index)
} while index < endIndex && self[prev] == self[index]
}
}
비슷한 방식으로 주어진 인덱스의 이전 고유 인덱스를 얻는 구현도 가능하다:
extension BidirectionalCollection where Element: Equatable {
/// 정렬된 컬렉션에서 주어진 인덱스를 고유한 요소를 가리키는 이전 인덱스로 대체한다.
///
/// - Parameter index: 컬렉션의 유효한 인덱스. `index`는 `startIndex`보다 커야 한다.
func formUniqueIndex(before index: inout Index) {
var prev = index
repeat {
prev = index
formIndex(before: &index)
} while index > startIndex && self[prev] == self[index]
}
}
세 숫자를 나타내기 위해 세 개의 인덱스를 추적한다. 현재 시점의 합은 array[l] + array[m] + array[r]
이다:
m -> <- r
[-4, -1, -1, 0, 1, 2]
l
이 접근법은 2Sum 알고리즘을 알고 있다면 매우 직관적이다. l
을 배열 전체를 순회하며 반복한다. 각 반복마다 l
이후의 요소에 대해 2Sum 알고리즘을 적용한다. 인덱스를 이동할 때마다 합을 확인하여 일치하는 조합을 찾는다. 다음은 해당 알고리즘이다:
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
}
정수 배열 S가 주어졌을 때, 배열에서 4개의 값을 선택해 그 합이 목표값(target)과 일치하는 모든 부분집합을 찾는 문제이다.
주의: 해결책 집합에는 중복된 4개의 값 조합(quadruplets)이 포함되어서는 안 된다.
이 문제는 주어진 배열에서 4개의 숫자를 선택해 그 합이 특정 값과 일치하는 모든 조합을 찾는 것이다. 단, 동일한 조합이 여러 번 포함되지 않도록 주의해야 한다. 예를 들어, 배열이 [1, 0, -1, 0, -2, 2]이고 목표값이 0인 경우, 가능한 조합은 다음과 같다:
이 문제를 해결하려면 배열을 정렬한 후, 두 개의 포인터를 사용해 가능한 조합을 탐색하는 방법을 고려할 수 있다. 이때 중복된 조합이 발생하지 않도록 주의해야 한다.
Foursum은 threeSum 알고리즘을 자연스럽게 확장한 것이다. threeSum에서는 세 개의 인덱스를 추적했다:
m -> <- r
[-4, -1, -1, 0, 1, 2]
l
fourSum에서는 네 개의 인덱스를 추적한다:
mr -> <- r
[-4, -1, -1, 0, 1, 2]
l ml ->
아래는 fourSum을 구현한 코드다. threeSum과 매우 유사하다는 점을 확인할 수 있다:
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
}
Swift Algorithm Club의 Kai Chen과 Kelvin Lau가 작성함