목차
비트마스킹(Bit Masking)
- 비트마스킹은 일종의 기술입니다.
- 이진수 표현을 활용해 값을 저장하거나 연산하는 기법으로 빠른 연산이 가능합니다.
- 비트 연산자 종류
- and(&) : 둘 다 1이여야지 1 / 나머지 0
- or(|) : 둘 중에 한 개라도 1이 있다면 1 / 둘다 0일 경우 0
- xor(^) : 둘 중에 한개만 1일 경우 1 / 둘다 1이거나 0일 경우 0
- not(~) : 1인 것은 0으로 0인것은 1로 변환
- 왼쪽 쉬프트(<<) : A<<n 은 A를 2**n로 곱한것
- 오른쪽 쉬프트(>>) : A>>n 은 A를 2**n으로 나눈것
- 비트 연산자의 활용 예시
- 특정 mask에 n번째 비트값이 1인지 확인하기
- 1<<3을 예시로 들면 1000 이 됩니다. 즉 1000과 & 연산을 할때 mask의 3번째 비트가 1이여야지 1000과 &을 하면 0이 되지 않습니다. (1<<3 이면 0이 3개라고 생각하기)
- 3번째 비트라고 하는 이유는 비트는 0번째부터 시작합니다.
-
if mask & (1 << n): print("n번째 비트는 1입니다.") else: print("n번째 비트는 0입니다.")
- 1<<3을 예시로 들면 1000 이 됩니다. 즉 1000과 & 연산을 할때 mask의 3번째 비트가 1이여야지 1000과 &을 하면 0이 되지 않습니다. (1<<3 이면 0이 3개라고 생각하기)
- k번째 비트 키고 싶을때
-
mask = mask|(1<<k)
-
- k번째 비트 끄고 싶을때
-
mask = mask&~(1<<k)
-
- 모든 비트가 1인지 확인하고 싶을때
- 비트가 5자리라고 하면 (1<<5)-1과 비교하면 됩니다.
-
mask==(1<<n)-1
- 모든 비트가 0인지 확인하기
-
mask==0
-
- 특정 mask에 n번째 비트값이 1인지 확인하기
- 그렇다면 어떤 문제에서 비트연산자가 사용될까요?
- 가장 대표적인 예시로는 비트 단위로 k번째 물건이 선택되었나/안되었나, k번째 도시를 방문했나/안했나 정도를 표현할때 사용됩니다.
- 원래는 visited 를 [] 리스트 안에 넣어서 체크를 진행했는데 이것을 이진법을 사용해서 0000 처럼 표현이 가능합니다. k번째노드 방문처리 -> (1<<k) 이런식으로 처리하게 되면 메모리 측면에서 효율적이게 됩니다. 또한 계산적인 부분에서도 쉬프트 연산자로 빠르게 수행이 가능하므로 효율적입니다.
- 모든 노드를 방문했는지 체크하고 싶을 경우 (1<<n)-1 하고 비교해보면 됩니다.
- 현재 위치에 대해서 여태까지 방문한 도시들에 대한 최소 비용을 구할때도 유용합니다.
- dp[mask][현재위치] 이런식으로도 사용이 가능합니다.
- 방문 순서에 상관없이, 방문했던곳이 동일하고 현재 위치가 동일하다면 어떤것이 더 빠른지 dp[mask][현재위치] 이런식으로 확인이 가능합니다.
- visited를 사용하게 되면 위와 같은 dp는 사용할 수 없습니다.
- 하지만, dp를 이용할 경우 mask가 2진수 이기때문에 엄청나게 커질수가 있습니다. 따라서 이렇게 사용할 경우는 전체 n개가 작을 경우에만 사용이 가능합니다. (n이 20이하정도일경우)
- 가장 대표적인 예시로는 비트 단위로 k번째 물건이 선택되었나/안되었나, k번째 도시를 방문했나/안했나 정도를 표현할때 사용됩니다.
예시
- [백준 2098] 외판원 순회
- 어느 한 도시에서 출발해 N개의 도시를 모두 거쳐 다시 원래의 도시로 돌아오는 순회 여행 경로를 계획하려고 할때, (단 한번 갔던 도시로는 다시 갈 수 없습니다.) 가장 적은 비용을 들이는 여행 계획을 세우는 방법을 세우는 문제입니다.
- n개의 도시를 가장 적은 비용이 들도록 나열하는 문제입니다. 즉 최대한 도시 사이의 거리가 짧은 것들로만 다녀야합니다.
- 여기서 dp는 dp[현재방문한도시비트][현재위치] 에서 모든 도시를 방문하게된다면 그때 방문할 도시들의 거리의 최소 비용이 됩니다.
- 재귀의 제일 안쪽은 dp[11111111][현재위치] 에서 출발지점인 0으로 돌아오는 경우가 될것입니다.
- 따라서 만약에 내가 탐구해서 들어가다가 dp[현재방문한도시비트][현재위치] 값이 None이 아니라면 그 값을 그대로 가져오면 됩니다. 이미 탐구해서 들어간 뒤 해당 지점(현재방문한도시비트,현재비트)에서 출발지점까지 돌아가는 최소비용을 구해놓은 값이기때문입니다.
- 결국 dp[1][0] 은 현재방문한도시비트가 1이고(0번째도시면 0번째비트가1), 현재 0번째 도시에 있는 상태인데 여기서 다시 출발점으로 돌아올 최소비용을 의미합니다.
- 외판원 순회는 0번째에서 출발해서 0으로 돌아오나, 1번째에서 출발해서 1으로 돌아오나 최소비용은 모두 동일할 것입니다. (어차피 원을 형성하는 것이기때문에, 최소거리로 연결되는 원을 찾는 것)
- 출발지점과 최종목적지가 정해져 있는경우 비트마스킹을 이용하여 (현재까지방문한mask, 현재위치)에서 최종목적지까지 도달하기 위한 최소 거리를 dp[현재까지방문한mask][현재위치] 로 표현가능합니다.)
- 코드
-
n = int(input()) w = [list(map(int, input().split())) for _ in range(n)] dp = [[None for _ in range(n)] for _ in range(1 << n)] def find(bit, now_point): if dp[bit][now_point] != None: return dp[bit][now_point] if bit == (1 << n) - 1: if w[now_point][0] == 0: # 돌아가는 길이 없는 경우 return float("inf") else: return w[now_point][0] temp = float("inf") for next in range(n): if bit & (1 << next) == 0 and w[now_point][next] != 0: temp = min(temp, find(bit | (1 << next), next) + w[now_point][next]) dp[bit][now_point] = temp return dp[bit][now_point] print(find(1, 0))
-
'Algorithm > Concepts' 카테고리의 다른 글
위상 정렬(Topological Sort) (0) | 2024.03.21 |
---|---|
느리게 갱신되는 세그먼트 트리(Lazy Segment Tree) (0) | 2024.03.20 |
구간 트리(Segment Tree) (0) | 2024.03.20 |
최소 스패닝 트리(Minimum Spanning Tree, MST) (0) | 2024.03.20 |
Union-Find (0) | 2024.03.20 |