[Problem] 비트 연산자를 이용한 부분집합 구하기
비트 연산자를 이용한 부분집합 구하기
문제
다음과 같은 집합이 있습니다.
data = {1, 10, 3, -3, -10}
이 집합의 부분 집합 중, 요소의 합이 0인 부분 집합을 구하는 문제입니다.
정답부터 써보자면
[3, -3], [10, -10], [10, 3, -3, -10]
이렇게 3개의 부분집합 입니다.
그럼 프로그래밍으론 이 문제를 어떻게 해결할 수 있을까요?
모든 부분집합을 구해서 요소의 합을 구해야 겠죠.
부분집합을 구하려면
data[0]을 포함할 때, 포함안할 때.
data[1]을 포함할 때, 포함안할 때.
…
그럼 정말 직관적으로 코딩을 한다면 다음과 같이 작성할 수 있습니다.
int[] data = {1, 10, 3, -3, -10}
int sum = 0;
// containCheck[n]==0 이면 data[n]값 미포함
// containCheck[n]==1 이면 data[n]값 포함
// 만약 containCheck가 {1, 1, 0, 0, 0} 이라면,
// 이때 만들어지는 부분집합은 {1, 10}
int[] containCheck = {0, 0, 0, 0, 0}
for(int a=0; a<2; a++){
containCheck[0] = a;
for(int b=0; b<2; b++){
containCheck[1] = b;
for(int c=0; c<2; c++){
containCheck[2] = c;
for(int d=0; d<2; d++){
containCheck[3] = d;
for(int e=0; e<2; e++){
containCheck[4] = e;
//부분 집합의 요소의 합 구하기
for(int f=0; f<5; f++){
sum += data[f]*containCheck[f];
}
//부분 집합의 요소의 합이 0일 때, 해당 집합 출력하기
if(sum==0){
for(inf g=0; g<5; g++){
System.out.print((data[g]*containCheck[g]) + " ");
}
System.out.println();
}
}
}
}
}
}
집합 요소의 개수가 5개 이므로 5단 for문을 써야합니다.
물론 각각의 for문당 2번씩밖에 안도므로, 실제로 돌아가는 연산은 얼마 안되지만
만약 집합의 요소 개수가 100개라면?
100단 for문을 쓰는건 쫌 아닌거 같습니다.
비트 연산자 활용하기
어차피 요소의 합이 0이 되는 부분집합을 구하려면
모든 부분집합을 탐색해봐야합니다.
data의 부분집합의 개수는 \(2^5\) 입니다.
그런데 이렇게 센 부분집합에는 공집합도 포함되어 있습니다.
공집합에는 아무 요소도 없으므로 이를 빼고 생각해주어야겠죠.
즉, 우리가 탐색해야 할 부분집합의 개수는 \(2^5-1\)개 입니다.
그 부분집합들은 1번 부분집합부터 \(2^5-1\)번 부분집합까지 미리 구해놨다고 가정하겠습니다.
다른 방식으로 표현 하자면 우리는
1번 부분집합 ~ \(2^5-1\)번 부분집합까지 요소의 합을 검사해야한다는 것이고,
이를 2진법으로 표현 하자면
00001번 부분집합 ~ 11111번 부분집합까지 검사를 해야합니다.
이때 다섯자리 2진법 숫자(\(X_0 X_1 X_2 X_3 X_4\))를 이렇게 생각합니다.
\(X_i\)가 0이면 data[i]를 부분집합에 안포함 시킨다는 것이고,
\(X_i\)가 1이몀 data[i]를 부분집합에 포함 시키는 겁니다.
즉, 00001번 부분집합은 실제로 {-10}이 되는 것이고
01001번 부분집합은 {10, -10}이 되는 것입니다.
이렇게 생각하고 00001~11111 까지 for문을 돌리면
한번의 for문으로 모든 부분집합을 검사할 수 있을 것입니다.
이렇게 for문을 쓰려면 비트 연산자를 알아야합니다.
여기서는 «와 &를 씁니다.
public class BitOperation {
public static void main(String[] ar){
int a = 1;
a = a << 2;
System.out.println("a : " + a);
System.out.println("a : " + Integer.toBinaryString(a));
int b = 6;
System.out.println("b : " + Integer.toBinaryString(b));
int c = a & b;
System.out.println("c : " + Integer.toBinaryString(c));
// a : 4
// a : 100
// b : 110
// c : 100
}
}
«는 비트를 해당 숫자만큼 왼쪽으로 밀고, 새로 생겨나는 부분은 0으로 채웁니다.
a«2는 1«2와 같은데 이진법으로 표현하면
001 을 왼쪽으로 2번 밀어서 100이 된것입니다.
즉, b = 1 « a 를 한다면
\(b=1*2^a\)이 되는 것이죠. 즉, 2의 거듭제곱 값을 만들 수 있습니다.
&는 둘다 1일때만 1을 리턴하는 연산자입니다.
a&b를 보면 100&110으로 두 숫자 모두 0번째 자리만 둘다 1이고 나머지는 둘다 1이 아닙니다.
그래서 연산결과는 0번째 자리만 1이 되고 1번째, 2번째는 0이 되는 100이 됩니다.
이 연산자를 이용한 예제를 살펴보겠습니다.
import java.util.Random;
import java.util.ArrayList;
public class BitOpertaionSubset {
public static void main(String[] ar){
BitOpertaionSubset ex = new BitOpertaionSubset();
Random rand = new Random();
int arrayLength = 5;
int[] intArray = new int[arrayLength];
int temp;
boolean dupCheck = false;
for(int index=0; index<arrayLength; index++){
do{
temp = rand.nextInt(21) - 10;
//중복된 숫자 있는지 체크
dupCheck = false;
for(int index2=0; index2<index; index2++){
if(intArray[index2] == temp){
dupCheck = true;
break;
}
}
}while(dupCheck);
intArray[index] = temp;
}
System.out.print("intArray => ");
ex.printArray(intArray);
ex.calcSumZero(intArray, arrayLength);
}
public void calcSumZero(int[] inputArray, int inputArrayLength){
int sum;
ArrayList<Integer> subset = new ArrayList();
int numSubset = 1 << inputArrayLength;
for(int i=1; i<numSubset; i++){
sum = 0;
subset.clear();
for(int j=0; j<inputArrayLength; j++){
if((i & (1 << j)) != 0){
sum += inputArray[inputArrayLength-1-j];
subset.add(inputArray[inputArrayLength-1-j]);
}
}
if(sum == 0){
System.out.println(subset);
}
}
}
public void printArray(int[] array){
for(int item: array){
System.out.print(item + " ");
}
System.out.println();
}
}
//intArray => -9 6 5 9 3
//[9, -9]
//[3, 6, -9]
-10~10 사이의 랜덤 숫자로
크기 5개의 배열을 만들고 있습니다.
알고리즘은 calcSumZero()를 보면 됩니다.
numSubset을 보면 비트연산자를 이용해 부분집합의 갯수를 구했고,
이를 for문에서 사용하고 있습니다.
또한 두번째 for문을 보면 (i & (1 « j)) != 0 이 나오는데요,
i = 01001 일때를 예로 들어보겠습니다.
그럼 data[1]과 data[4]값을 부분집합의 요소로 포함시킨다는 건데
이를 어떻게 확인할까요?
j값은 0~4이고 만약
j==1이라면
1 « j 는 1 « 1이 되서 00010이 되는 것이고
i & (1 « j) 은 01001 & 00010 으로 00000되죠
결과값의 모든값이 0이므로 아무일도 일어나지 않습니다.
j==3라면
1 « j 는 1 « 3가 되서 01000이 되는 것이고
i & ( 1 « j) 는 01001 & 01000 으로 01000이 됩니다.
결과값의 1번째 자리수가 1이므로 data[1]이 부분집합에 포함됨을 알 수 있습니다.
즉, 01001에서 첫번째 자리에 1이 있다는 것을 알아낸 셈이죠.
그리고 j==3일 때 1번째 요소가 들어가는, 즉 3+1==4 임을 감안해서
sum += inputArray[inputArrayLength-1-j];
subset.add(inputArray[inputArrayLength-1-j]);
와 같이 조정해서 요소값을 얻어내었습니다.
이런식으로 01001에 1번째, 4번째자리에 1이 있는지 알아내고,
이에 해당하는 부분집합의 합을 구해서 0이 되는지를 확인하는 방식입니다.