비트마스크

Saemi Min·2023년 6월 21일
0

Data Structure & Algorithm

목록 보기
17/17

비트마스크를 이용한 집합 구현

비트마스크를 이용한 집합 구현은 가장 대표적이고, 중요한 사례이다. "하나의 bit가 하나의 원소"를 의미하게 된다.

bit가 켜져 있으면 해당 원소가 집합에 포함되어 있다는 의미이고, 꺼져 있으면 포함되어 있지 않다는 의미이다.

따라서 N비트 정수 변수라면 N개의 원소를 갖는 집합의 부분집합들을 모두 표현할 수 있게 된다.

원래는 N개의 boolean 원소를 갖는 배열을 선언해야 했지만, 비트마스크를 이용하면 정수 하나로 표현이 가능하기 때문에 사용하는 메모리의 크기가 많이 줄어드는 장점이 있다.

이제 비트연산자를 통해서 집합을 어떻게 효율적으로 다룰 수 있는지 알아보자.

우선, A라는 변수를 집합이라고 가정하고, 집합의 총원소의 개수를 10개라고 가정하겠다. (0번째 ~ 9번째 원소)

연산 사용 예시

  • 공집합과 꽉 찬 집합 구하기 A = 0; / A = (1 << 10) - 1;
  • 원소 추가 A |= (1 << k);
  • 원소 삭제 A &= ~(1 << k);
  • 원소의 포함 여부 확인 if(A & (1 << k))
  • 원소의 토글(toggle) A ^= (1 << k);
  • 두 집합에 대해서 연산 A | B → A와 B의 합집합
  • A & B → A와 B의 교집합
  • A & (~B) → A에서 B를 뺀 차집합
  • A ^ B → A와 B중 하나에만 포함된 원소들의 집합
    • 집합의 크기 구하기 int bitCount(int A){
      if(A == 0) return 0;
      return A%2 + bitCount(A / 2);
      }

내장 명령어

gcc/g++ → __builtin_popcount(A)
visual C++ → __
popcnt(A)
Java → Integer.bitCount(A)
최소 원소 찾기 int first = A & (-A);
최소 원소 지우기 A &= (A - 1);
모든 부분 집합 순회하기 for (int subset = A ; subset ; subset = ((subset - 1) & A)){ }

1) 공집합과 꽉 찬 집합 구하기

  • 기본적으로 공집합은 bit가 모두 꺼진 상황이기 때문에 상수 0이 공집합을 표현한다.

반대로 꽉 찬 집합은 bit가 모두 켜진 상황이기 때문에 1111111111(2) 의 값을 가져야 하는데, 이는 (1<<10) - 1 과 동일하다. 1<<10은 10000000000(2) 이므로 1을 빼면 10개의 bit가 모두 켜진 수를 얻을 수 있다.

2) 원소 추가

  • A 집합에 특정 원소를 추가하는 방법이다. 원소에 해당하는 bit만 켜야 하기 때문에 해당 bit를 항상 1로 만드는 연산이 필요하다. 따라서 OR 연산을 이용한다.

이미 A에 원소가 포함되어 있는 경우에는 아무런 변화가 없게 된다.

3) 원소 삭제

  • A 집합에 포함된 특정 원소를 삭제하는 방법이다. 원소에 해당하는 bit가 꺼져야 하기 때문에 해당 bit를 항상 0으로 만드는 연산이 필요하다.

따라서 A -= (1<<k); 로 작성하면 된다. 하지만 이 경우는 A에 반드시 k번째 원소가 포함되어 있는 경우에만 가능하다. 만약 포함되어 있지 않은 경우에는 다른 원소의 포함 여부까지 바꿔버리기 때문이다.

그러므로 A에 k번째 원소의 포함 여부와 상관없이 해당 bit를 끄기 위해서는 AND연산을 이용해야 한다.

1<<k는 k번째 bit만 켜진 상태이며, 여기에 NOT을 씌우면 k번째 bit만 꺼진 상태가 된다.

그러므로 AND 연산을 적용하면 k번째 bit만 0이 되고 나머지 bit는 변함이 없다.

4) 원소의 포함 여부 확인

  • A 집합에 특정 원소가 포함되어 있는지 확인하는 방법이다. k번째 원소가 포함되어 있는지 확인하고 싶다면, k번째 bit가 켜져 있는지만 확인하면 된다.

5) 원소의 토글

  • A 집합에 해당 원소가 빠져있는 경우에는 추가하고, 들어있는 경우에는 삭제하는 방법이다. XOR 연산을 이용한다.

6) 두 집합에 대해 연산하기

  • 두 집합을 A와 B라고 한다면 비트연산자들을 통해서 A와 B의 교집합, 합집합, 차집합 등을 구할 수 있다.

7) 집합의 크기 구하기

  • 집합에 포함된 원소의 크기를 구한다면 A에서 켜진 bit의 수를 구하면 될 것이다. 직접 모든 비트를 확인하면서 개수를 체크할 수도 있고, 내장 명령어를 이용할 수도 있다.

8) 최소 원소 찾기

  • 집합에 포함된 가장 작은 원소 (index가 가장 작은 원소)를 찾는 방법이다. 켜져 있는 bit 중에서 가장 오른쪽에 있는 bit를 찾는 것이다. 비트마스크 뿐만 아니라 펜윅 트리 (Fenwick Tree)에서도 사용되는 기법이다.

컴퓨터는 2의 보수를 이용하여 음수를 표현하기 때문에 -A를 표현하기 위해서 ~A + 1을 이용한다.

A에서 가장 오른쪽에 켜진 bit의 인덱스를 k라고 한다면, k보다 오른쪽에 있는 모든 bit는 0이다.

따라서 NOT 연산을 적용한 ~A는 k번째 bit는 0이고, 오른쪽의 모든 bit는 1이 된다.

여기서 ~A에 1을 더해주게 되면 k번째보다 오른쪽에 있는 bit는 모두 0이 되고, k번째 bit는 1이 된다. k번째 bit보다 왼쪽에 있는 bit는 아무런 변화가 없다.

따라서 -A와 A를 AND 시키면 k번째 bit만 켜진 상태로 남게 된다.

9) 최소 원소 지우기

  • 가장 오른쪽에 켜져 있는 bit를 지우고 싶다면 A-1과 AND시키면 된다. A에서 1을 빼주게 되면 가장 오른쪽에 있던 bit는 0이 되고 그보다 오른쪽에 있는 모든 bit들이 1이 되기 때문이다.

10) 모든 부분 집합 순회하기

  • A의 모든 부분 집합을 탐색하는 방법이다.

위의 설명들을 토대로 코드에 직접 예시를 넣어보면 쉽게 이해가 갈 것이다.

참고 블로그

profile
I believe in myself.

0개의 댓글