티스토리 뷰
#include <stdio.h>
#include <stdlib.h>
int arr[40],n,k,mid,leftCnt,rightCnt,leftPoint,rightPoint,tmp,Lvalue,Rvalue;
long long ans = 0,LCnt,RCnt;
int cmp(const void *a, const void *b) {
return *(int*)a - *(int*)b;
}
void calSub(int subCnt, int offset, int save[]) { //두개로 쪼갠 수열에서, 부분수열의 합 구하기
int gray, prevGray=0, diff, changeBitIndex; //gray code (비트마스크)
int sum=0; //부분수열의 합
save[0] = sum; //공집합 추가
for(int i=1;i<subCnt;i++) { //부분수열에서 만들수 있는 합을 계산하고 추가합니다.
//gray code : 일반적인 비스마스크보다 좀 더 빠른 연산입니다.
gray = i^i>>1;
diff = gray ^ prevGray; //어떤 하나의 비트가 바뀌었는지를 구하기 위한 처리입니다.
changeBitIndex = __builtin_ctz(diff); //비트가 1인 곳의 index를 반환합니다.
//변한 비트가 1로 변했다면, 그 부분에 대한 arr[]의 값을 sum에 추가, 반대 라면 빼줍니다.
if (gray >> changeBitIndex & 1) sum += arr[changeBitIndex+offset];
else sum -= arr[changeBitIndex+offset];
//부분수열에서 만들 수 있는 합을 담는 배열에 저장합니다.
save[i] = sum;
//이전 gray를 갱신합니다.
prevGray = gray;
}
return;
}
int main(void) {
scanf("%d %d",&n,&k);
for(int i=0;i<n;i++)scanf("%d",&arr[i]);
mid = (n+1)/2;
leftCnt = 1<<mid; rightCnt = 1<<n-mid; //둘 로 쪼개서 나온 부분수열의 모든 합의 개수 + 1 구하기
leftPoint = 0; rightPoint = rightCnt-1; //나중에 아래서 두포인터 쓸 때 사용할 변수
//부분수열의 모든 합을 담을 배열 동적할당
int * left = (int*) malloc(sizeof(int)*leftCnt);
int * right = (int*) malloc(sizeof(int)*rightCnt);
//각 각 부분수열의 모든합 계산
calSub(leftCnt,0,left);
calSub(rightCnt,mid,right);
//각 각 부분수열의 모든합 오름차순 정렬
qsort(left,leftCnt,4,cmp);
qsort(right,rightCnt,4,cmp);
//두 포인터
//왼쪽수열은 작은값부터, 오른쪽 수열은 큰 값부터
while(leftPoint<leftCnt && rightPoint>=0) { //어느 한쪽의 포인터가 범위를 넘어가면 종료
tmp = left[leftPoint] + right[rightPoint]; //두 수열의 합들을 합칩니다.
if(tmp < k) ++leftPoint; //찾고자 하는 값보다 작다면 왼쪽 수열의 포인터를 오른쪽으로
else if(tmp > k) --rightPoint; //더 크다면 오른쪽 수열의 포인터를 왼쪽으로
else { //찾았다면, 중복 체크를 합니다.
//부분수열의 합의 값이 갔더라도 이를 만드는 방법이 여러가지인 경우가 있기 때문입니다.
//ex) 1 2 3 4 라면 5 는 1,4 2,3 이렇게 2번 만들어 졌습니다. (위에서)
//중복 개수를 구하기 시작합니다.
LCnt=0;RCnt=0;
//먼저 비교에 쓸 값을 미리 저장합니다.
Lvalue = left[leftPoint];
Rvalue = right[rightPoint];
//반복문을 이용하여 포인터를 1씩 증가시키거나(왼쪽수열), 1씩 감소시키면서(오른쪽수열)
//중복인지 아닌지를 확인합니다.
do {++LCnt; ++leftPoint;}while(leftPoint<leftCnt && left[leftPoint] == Lvalue);
do {++RCnt; --rightPoint;}while(rightPoint>=0 && right[rightPoint] == Rvalue);
//결과를 ans에 누적합니다.
ans += LCnt * RCnt;
}
}
//아래 코드가 필요한 이유!
//처음 수열들의 모든합을 구할 때, 공집합 0 을 추가했었습니다.
//공집합을 추가한 이유는 두 포인터는 왼쪽 오른쪽 의 합을 더해서 구하는 원리이나
//굳이 찾고자 하는 값이 왼쪽 또는 오른쪽 수열의 부분합 자체로 존재할 수도 있기 때문입니다.
//이를 두포인터에서 한번에 구하고자 공집합을 추가했었습니다.
//그러나 찾고자 하는 값이 0 인 경우, 두포인터는 공집합 또한 수열에서 만든 0이라고 판단합니다.
//ex) 1 2 1 2 이라는 수열이 있다면 이를 1 | 1 로 쪼개고 처음에 공집합을 추가하니
// 0 1 2 3 | 0 1 2 3 이라는 왼쪽, 오른쪽 수열의 부분합을 담은 배열이 생성
// 찾고자 하는 값이 0 이므로 1 2 1 2 수열에서는 절대로 불가하나, 공집합을 추가했다보니
//두포인터는 0,0 을 더하여 1개가 가능하다고 판단합니다.
//이를 해결하기위해 찾고자 하는 값이 0 이라면 1을 빼줍니다.
if(k==0) --ans;
printf("%lld", ans);
free(left); free(right);
return 0;
}
풀이 : Meet in the Middle (MITM), gray code, 비트마스크
주된 설명은 코드 주석에 나와 있습니다.
최대 2^40 의 경우를 브루트 포스로 구하기엔 너무나 많은 시간이 소비됩니다. 그러나 이를 두개로 가르면 2^20, 2^20 + 정렬 + 두포인터 ... 이므로 브루트포스보다 더 빨리 해결이 가능하게 됩니다.
이에 대한 대표적인 알고리즘이 MITM 입니다.
1. 수열을 중간에서 2개로 쪼갠다.
2. 각각의 수열에서 만들수 있는 부분합들을 구한다. 이를 각 각 정렬한다.
3. 두포인터를 이용하여 각각의 수열에서 얻은 부분합들을 서로 더해서 비교한다.
이런 과정을 거칩니다.
물론 찾고자 하는 값이 각각의 수열에 있는 부분합들을 더할 필요가 없는 경우(수열 쪼개서 구한 부분합이 찾고자 하는 값) 도 존재하니, 2 과정에서 공집합 (값이 0인) 을 추가해줍니다! 이를 추가하면 간편하게 찾고자 하는 값이 각각의 수열에 있는 부분합들을 더할 필요가 없는 경우 를 해결할 수 있습니다.
'c언어 > BAEKJOON' 카테고리의 다른 글
| c언어 2087번 암호문 (백준) (0) | 2025.08.21 |
|---|---|
| c언어 1450번 냅색문제 (백준) (0) | 2025.08.21 |
| c언어 1562번 계단 수 (백준) (0) | 2025.08.18 |
| c언어 1311번 할 일 정하기 1 (백준) (0) | 2025.08.17 |
| c언어 33528번 Alphabetic Shift (백준) (0) | 2025.06.08 |
공지사항
최근에 올라온 글
최근에 달린 댓글
- Total
- Today
- Yesterday
링크
TAG
- 그래프
- BFS
- union
- Krustal
- java
- PASCAL
- 최소 스패닝 트리
- C언어
- 문자열
- 기하학
- 구현
- 1835
- 백준
- DP
- 그리디
- DFS
- 정렬
- 덱
- 브루트포스
- 누적 합
- 1835번
- 오프라인 쿼리
- find
- Lazy Propagation
- 누적합
- Segment Tree
- 세그먼트 트리
- 스택
- C++
- XOR
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | ||||
| 4 | 5 | 6 | 7 | 8 | 9 | 10 |
| 11 | 12 | 13 | 14 | 15 | 16 | 17 |
| 18 | 19 | 20 | 21 | 22 | 23 | 24 |
| 25 | 26 | 27 | 28 | 29 | 30 | 31 |
글 보관함
