Develop Study/Algorithm
(25.03.24) n개에서 k 고르기 : 재귀 백트래킹 VS 비트마스크
OO 사 코딩 테스트를 진행하면서, 기억에 남는 문제를 복기를 해서 알고리즘을 확인했다.
최대한 많은 조합을 만드는 문제는 많은 알고리즘에서 발생을 했지만,
만약 그 조합 중에 K개 만큼 특정 갯수가 변수로 주어진 경우에는, 불필요하게 모든 조합을 구하고, 그리고 거기서 k개의 요소를 가진 경우의 수를 모두 고르는 방식은 굉장히 비효율 적이다.
따라서 재귀 트백트래킹을 사용할 수 있지만, 이 보다 효율적인 메모리를 사용해서 재귀를 사용하지도 않고도, 순서 -> 비트로 사용하는 비트마스크 방식을 찾아 이를 적용하고 학습했다.
[음료수 맛 조합 문제]
여러 종류의 음료수가 주어집니다. 각 음료수는 여러 가지 맛을 가지고 있습니다. 4가지 맛이 있을 경우 음료수의 맛은 이진수(1101, 1000, etc) 로 주어집니다. 주어진 음료수들 중에서 k개의 음료수를 골라서 그 음료수들을 섞었을 때 만들 수 있는 새로운 맛의 개수가 최대 몇 개인지를 구하시오.
- 1101 과 1000 음료수를 섞는다면 1101 맛으로 나옴 - 음료수들을 섞는 방식은 각 음료수의 맛을 비트 OR 연산을 통해 결합하는 방식
- 음료수 중 `k`개를 선택해서 그들의 비트 OR 연산 결과로 만들어질 수 있는 새로운 맛의 최대 개수
재귀 백트래킹
import java.util.*;
public class DrinkFlavor {
static Set<Integer> resultFlavors = new HashSet<>();
public static int maxFlavors(List<Integer> drinks, int k) {
// 음료수의 개수
resultFlavors.clear(); // 결과 집합 초기화
// 백트래킹을 통해 k개의 음료수 조합을 구하고 맛 계산
findCombinations(drinks, k, 0, 0, 0);
return resultFlavors.size(); // 가능한 맛의 개수 반환
}
// 음료수와 k개의 조합을 구하는 재귀하는 메서드
public static void findCombinations(List<Integer> drinks, int k, int start, int current, int count) {
if (count == k) {
resultFlavors.add(current);
return;
}
for (int i = start; i < drinks.size(); i++) {
// 비트 OR 연산
findCombinations(drinks, k, i + 1, current | drinks.get(i), count + 1);
}
}
// 예시
public static void main(String[] args) {
List<Integer> drinks = Arrays.asList(101, 110, 111, 100, 101);
int k = 2;
int result = maxFlavors(drinks, k);
System.out.println("최대 가능한 맛의 개수: " + result);
}
}
- n개중 k 를 고르기 위해 재귀와 백트래킹 : 이미 선택된 경우 다시 돌아가기 를 통해서 최대 조합수를 구하는 알고리즘(가장 기본적인)
- 주어진 값들이 이미 이진법 형태를 띄고 있기 때문에 , findCombinations 내에서 비트 OR 연산 을 진행
- 주어진 값이 이진법이 아니라면 그냥 내가 임의로 비교하는 어떤 Method를 활용할 수 있음
비트연산자
& | 대응되는 비트가 모두 1이면 1을 반환함. (비트 AND 연산) |
** | ** |
^ | 대응되는 비트가 서로 다르면 1을 반환함. (비트 XOR 연산) |
~ | 비트를 1이면 0으로, 0이면 1로 반전 시킴. (비트 NOT 연산, 1의 보수) |
<< | 명시된 수만큼 비트들을 전부 왼쪽으로 이동시킴. (left shift 연산) |
>> | 부호를 유지하면서 지정한 수만큼 비트를 전부 오른쪽으로 이동시킴. (right shift 연산) |
>>> | 지정한 수만큼 비트를 전부 오른쪽으로 이동 시키며, 새로운 비트는 전부 0이 됨. |
- 비트 OR 연산을 활용해서 구할 수 있음
비트연산자로 쓸 수 없는 n개 값 중 k개를 고르는 모든 조합 구하기
import java.util.*;
public class Combination {
// 조합 리스트를 저장할 리스트
static List<List<Integer>> result = new ArrayList<>();
// 조합을 구하는 메서드
public static void findCombinations(List<Integer> arr, int k, int start, List<Integer> current) {
// K개가 되면 결과에 추가
if (current.size() == k) {
result.add(new ArrayList<>(current));
return;
}
// 재귀적으로 원소를 하나씩 선택
for (int i = start; i < arr.size(); i++) {
current.add(arr.get(i));
findCombinations(arr, k, i + 1, current);
// 여기서 백트래킹 사이즈 줄이고 그 사이에 다른거 넣기
current.remove(current.size() - 1);
}
}
// 예시
public static void main(String[] args) {
List<Integer> arr = Arrays.asList(1, 2, 3, 4, 5);
int k = 3;
findCombinations(arr, k, 0, new ArrayList<>());
System.out.println("모든 조합: " + result);
// 모든 조합: [[1, 2, 3], [1, 2, 4], [1, 2, 5], [1, 3, 4], [1, 3, 5], [1, 4, 5], [2, 3, 4], [2, 3, 5], [2, 4, 5], [3, 4, 5]]
}
}
비트 마스크 활용
- 전체적인 플로우는 k개를 골라야 할 때, 1 << n 를 사용을 해서 이진법으로 n 자리 11111… 를 만들기 → 1의 갯수가 k개인 것 찾기 → 해당 값의 1인 부분 (1 << i) 를 활용해서 그 i번째 값을 리스트에 넣기 → 모든 리스트를 최종 리스트에 담아서 반환
import java.util.*;
public class DrinkFlavor {
// 주어진 음료수들로 만들 수 있는 최대 맛의 개수를 구하는 메서드
public static int maxFlavors(List<Integer> drinks, int k) {
Set<Integer> resultFlavors = new HashSet<>();
int n = drinks.size();
// 가능한 모든 조합을 비트마스크로 계산
for (int mask = 1; mask < (1 << n); mask++) {
// 이진법으로 변환했을 때의, 1의 갯수 = 선택한 수
int count = Integer.bitCount(mask);
// k개 음료수가 선택된 경우
if (count == k) {
int flavor = 0;
for (int i = 0; i < n; i++) {
if ((mask & (1 << i)) != 0) {
// 비트 OR 연산 필요에 따라 다른 메서드
flavor |= drinks.get(i);
}
}
resultFlavors.add(flavor);
}
}
return resultFlavors.size();
}
public static void main(String[] args) {
List<Integer> drinks = Arrays.asList(101, 110, 111, 100, 101);
int k = 2;
int result = maxFlavors(drinks, k);
System.out.println("최대 가능한 맛의 개수: " + result);
}
}
- mask 라는 비트마스크를 사용해서, 재귀가 아닌 음료수 선택여부를 바로 확인할 수 있음
- mask < (1 << n) : 왼쪽 쉬프트 연산자(left shift operator) 활용
- 2^n을 계산 하는 연산 : 즉, n이 3이라면, 0001 부터 쉬프트패서 1000을 한 8이랑 똑같이 사용
- n개의 음료수 종류에 대해서 선택한다/안한다 두개의 경우의 수를 보기 때문에 사용
- mask < (1 << n) : 왼쪽 쉬프트 연산자(left shift operator) 활용
- Integer.**bitCount**(mask)
- mask는 int
- mask → 이진법으로 바꾸고 → 그 중에 비트 즉 1의 갯수를 반환하는 메서드
- if ((mask & (1 << i)) != 0)
- AND 연산자를 사용해서, i번째 해당하는 숫자 즉, 두번째는 0100 (1 << i) 과비교
- 둘다 같은 자리에 1이 있어야 1을 반환하기 때문에 = 그 자리가 1인지 확인 하는 조건 = 해당 순서의 음료수(drinks)가 선택이 되었는지 확인
- flavor |= drinks.get(i);
- 단순하게 a |= b는 a = a | b 라고 생각하면 편함 a에 덮어 씌우기
- drinks.get(i) 자체가 이진법이기 때문에 계속 비트를 flavor에 덮어 씌우면서 위의 자리가 1인 값들을 골라서 비교하고 덮는 것을 반복
비트 연산자 쓸수 없는 n개 중 k개의 조합 구하기
import java.util.*;
public class CombinationWithBitmask {
static List<List<Integer>> result = new ArrayList<>();
public static void findCombinations(List<Integer> arr, int k) {
int n = arr.size();
// 가능한 모든 비트마스크를 확인
for (int mask = 0; mask < (1 << n); mask++) {
List<Integer> combination = new ArrayList<>();
// 음료수와 똑같이 진행
int count = Integer.bitCount(mask);
// 선택된 비트들에 대해 조합을 구함
if (count == k) {
for (int i = 0; i < n; i++) {
if ((mask & (1 << i)) != 0) { // 해당 비트가 1이면 선택된 값
// 여기서 바리에이션 가능
combination.add(arr.get(i));
}
}
result.add(combination); // 결과에 추가
}
}
}
public static void main(String[] args) {
List<Integer> arr = Arrays.asList(1, 2, 3, 4, 5);
int k = 3;
// k개의 조합을 구함
findCombinations(arr, k);
System.out.println("모든 조합: " + result);
// 모든 조합: [[1, 2, 3], [1, 2, 4], [1, 2, 5], [1, 3, 4], [1, 3, 5], [1, 4, 5], [2, 3, 4], [2, 3, 5], [2, 4, 5], [3, 4, 5]]
}
}
- 위와 똑같이 진행할 수 있고, Dynamic Programming 의 Memoization 같지만 전혀 다름
- 오히려 DP에서 위를 활용할 수 있는 방법으로 접근을 해야함
- DP는 최적의 값을 찾기 위한 목적이지만 비트마스크는 모든 경우의 수에 초점이 있음
- DFS 와 같은 모든 경우를 확인하지만 개념은 다름
- 어쩔 수 없이 모든 조합을 다 확인해봐야만 하는 경우에 효율적으로 사용할 수 있음
- 부분집합의 연산을 통해서 그 결과 값이 같은 경우도 고려해야하는 등, 모두 확인이 필요
- 위의 if 조건문을 없애면 모든 부분집합을 구할 수 있음
- 가능한 모든 경우를 다 확인해야하는 문제에서 매우 유용
굉장히 단순하지만, 코딩테스트, 알고리즘에서는 전혀 알 수 없었던 방법을 찾아낸 것이기 때문에, 매우 빠른게 메모리를 효율적으로 사용하면서, 확인할 수 있을 것이다.
단, 이러한 알고리즘은 어쩔 수 없이 모든 조합을 다 확인해야하는 경우(중복 값을 고려 하거나, 순서를 상관해야하 하는 등) 에만 쓰일 수 있기 때문에 가급적이면 모든 문제에 적용하지는 않는 것으로 해야할 것이다.
'Develop Study > Algorithm' 카테고리의 다른 글
(25.03.27) 비트 마스크의 활용 : DP 심화 (0) | 2025.03.27 |
---|---|
(25.03.25) 비트 마스크의 활용 : DP (0) | 2025.03.25 |
(25.01.28) DFS 알고리즘의 시간 부족 Time Limit Exceeded 해결 : 자료구조의 활용 (1) | 2025.01.28 |
(25.01.25) DFS 알고리즘의 시간 부족 Time Limit Exceeded 해결 (0) | 2025.01.25 |
(25.01.15) BFS 알고리즘의 메모리 부족 Memory Limit 해결 (0) | 2025.01.15 |