완전 탐색을 쌈싸먹기 전, 관련 개념을 알아보자 🧐
비트 마스크
bit 연산을 이용해 부분 집합을 나타내는 방법
집합을 배열의 인덱스로 표현할 수 있기 때문에 상태 다이나믹을 할 때 자주 사용한다.
STL의 bitset을 이용해서 더 쉽게 사용할 수 있다!
종류
- and (&) : 둘 다 1일 때 1
- or (|) : 둘 중 하나라도 1이면 1
- not (~) : 0이면 1, 1이면 0
- xor (^) : 같으면 0, 다르면 1
비트 연산
- 두 수 A와 B를 비트 연산 하는 경우, 가장 뒤의 자리부터 하나씩 연산을 수행한다.
- not 연산의 경우
- 자료형에 따라 결과 달라진다. 비트가 자료형에 따라 달라지니까!
- 8비트 자료형, 32비트 자료형, unsigned, signed...
- shift left (<<), shift right (>>)
- A << B : A를 왼쪽으로 B비트만큼 민다. A * 2^B와 동일
- A >> B : A를 오른쪽으로 B비트만큼 민다. A / 2^B와 동일
- (A+B)/2 = (A+B) >> 1
비트 마스크
- 정수로 집합(S)을 나타낼 수 있다.
- {1, 3, 4, 5, 9] = 570 = 2^1 + 2^3 + 2^4 + 2^5 + 2^9 = 1000111010
- 어떤 수(i)의 포함 여부를 확인하기 위해 & 연산을 이용한다.
- S & (1 << i)
- 0이 포함되어 있는지 검사
- 570 & 2^0 = 570 & (1<<0) = 1000111010 & 1 = 0
- 1이 포함되어 있는지 검사
- 570 & 2^1 = 570 & (1<<1) = 1000111010 & 10 = 10 = 2
- 2가 포함되어 있는지 검사
- 570 & 2^2 = 570 & (1<<2) = 1000111010 & 100 = 000 = 0
- 어떤 수(i)를 새로 추가하기 위해 | 연산을 이용한다.
- S | (1 << i)
- 2 추가하기
- 570 | 2^2 = 570 | (1<<2) = 1000111010 | 100 = 1000111110 = 574
- 만약 이미 존재하는 수를 새로 추가한다면? 그대로!
- 1 추가하기
- 570 | 2^1 = 570 | (1<<1) = 1000111010 | 10 = 1000111010 = 570
- 어떤 수(i)를 제거하기 위해 ~ 연산을 이용한다.
- S & ~(1 << i)
- 1 제거하기
- 570 & ~2^1 = 570 & ~(1<<1) = 570 & ~(10) = 1000111010 & ~(01) = 1000111010 & 1111111101 = 1000111000 = 568
- 어떤 수(i)를 토글하기 위해 ^ 연산을 이용한다.
- 전체 집합 : (1<<N) - 1
- 공집합 : 0
문제 예제 (1) : 백준 집합
https://www.acmicpc.net/problem/11723
내 풀이
- 고냥 저항없이 위에서 공부한대로 코드를 짰다.
- 그랬더니 시간 초과~~~😇
- 입력은 버퍼로 받았으니 아마 출력하는 부분이 문제겠지 싶어서 StringBuilder를 사용하도록 코드를 수정해줬다.
- 수정해주니 통과했다. 굿굿 앞으론 스트링빌더를 걍 습관처럼 써줘야지
순열
1~N까지로 이루어진 수열.
크기는 항상 N이며, 겹치는 숫자가 존재하지 않아야 한다.
다음 순열
순열을 사전순으로 나열했을 때, 사전순으로 다음에 오는 순열과 이전에 오는 순열을 찾는 방법
다음 순열 찾는 법 (O(n))
예시 순열 : 7 2 3 6 5 4 1
- A[i-1] < A[i] 를 만족하는 가장 큰 i를 찾는다.
- 가장 큰 i니까 오른쪽에서 부터 체크!
- 즉 순열의 마지막 수에서 끝나는 가장 긴 감소 수열 찾기.
- A[i-1] = 3, A[i] = 6. i = 3
- 감소수열은 6 5 4 1
- j >= i 면서 A[j] > A[i-1]을 만족하는 가장 큰 j를 찾는다.
- 가장 큰 j니까 오른쪽에서 찾아야!
- A[j] = 4. j = 5
- A[i-1]과 A[j]를 swap한다
- A[i]부터 순열을 뒤집는다.
다음 순열에서 제일 첫 번째 순열은 오름차순, 제일 마지막 순열은 내림차순이다.
따라서 다음 순열을 구할 때, 특정 인덱스에서 사전 순 다음 값의 조건은
- 현재 값보다 크면서
- 현재 값 다음 인덱스의 수들 중 제일 작은 값이어야 한다
을 충족해야 한다.
문제 예제 (2) : 백준 다음 순열
https://www.acmicpc.net/problem/10972
내 풀이
- 이번에도 저항없이... 위에서 공부한 다음 순열 구하는 방식을 그대로 코드로 구현했다.
- System.out을 이용한 경우
- StringBuilder를 이용한 경우
문제 예제 (3) : 백준 이전 순열
https://www.acmicpc.net/problem/10973
내 풀이
- 다음 순열 구했던거랑 반대로 구하면 된다.
- 맞았당ㅎ.ㅎ
문제 예제 (4) : 백준 모든 순열
https://www.acmicpc.net/problem/10974
내 풀이
- 다음 순열 구했던걸 응용하면 된다.
- 맞았다
문제 예제 (5) : 백준 순열의 순서
https://www.acmicpc.net/problem/1722
내 풀이
- 두번째 입력의 첫번째 수가 1이면 k번째 순열을, 2면 뒤이어 입력한 순열이 몇 번째 순열인지 찾으면 된다.
- 완전 아무 생각 없이 풀면, 위에서 풀었던 다음 순열을 응용하면 된다.
- 순서 변수 하나 만들어서, 다음 순열 함수가 호출될 때마다 카운트하는 방식.
- 근데 이게 좋은 풀이일까? 🤔
- 아아아아아주 오래 전 공부했던 확통 지식을 되살려보면, 순열의 개수는 팩토리얼로 구했다.
- ex. 1~N으로 이루어진 순열의 크기는 N!
- 요건 각 자리에 올 수 있는 숫자의 개수로 만든 공식이다.
- N= 5일 때, 맨 처음에 올 수 있는 수는 1, 2, 3, 4, 5니까 5, 두번째 올 수 있는 수는 앞에 나온 수 제외하고 전부니까 5, ... 하는 식으로.
- 요걸 잘 응용하면 되지 않을까?
- 왜냐하면 문제 조건에서 순열의 순서는 사전순 (오름차순)이라고 했으니까!
- 예를 들어 N=3일 때 { 1 2 3 } 의 순서를 찾아보자.
- 각 자리수에 올 수 있는 첫번째 숫자들이 배치되어 있으니, 0 x 0 x 0 = 0. 첫번째 순열을 고려해서 +1해주면 k = 1.
- 이번엔 { 3 2 1 }
- 첫번째 자리수에 3은 세번째로 올 수 있는 수다. 따라서 2! x 2 = 4
- 두번째 자리수에 2는 두번째로 올 수 있는 수다. 따라서 1
- 세번째 자리수에 1은 처음으로 올 수 있는 수다. 따라서 0
- 4 + 1 + 1(첫번째) = 6
- 실제로 순열을 전부 나열해보면 아래와 같으므로 6번째 순열이 맞다.
1 2 3
1 3 2
2 1 3
2 3 1
3 1 2
3 2 1
- 옹 일단 순서는 구할 수 있겠다! 그럼 이제 순서가 주어졌을 때 순열을 찾는 방법을 고민해보자.
- 위에서 생각한 순서 찾는 로직을 응용하면 된다. 주어진 숫자가 전체 수 N에서 몇 번째 순서에 위치해있는지를 알면 쏘~~~이지