클릭시 이동합니다!
웨브바짐
어떻게 풀까?
가장 중요한 것은 '수를 어떻게 만들 것인가?' 를 정하는 것입니다.
각 자릿수의 순열을 이용해서 만든다고 생각해봅시다.
예제 입력의 422를 예로 들겠습니다.
그렇다면, 인덱스 순서대로
0 1 2 = 422
0 2 1 = 422
1 0 2 = 242
1 2 0 = 224
2 0 1 = 242
2 1 0 = 224
실제로 만들어지는 수는 422, 242, 224 세가지 이지만, 순열을 이용해서 만들면 6가지의 경우의 수가 생기기 때문에 중복해서 세는 경우가 생깁니다.
이를 위해서는 우선 e의 각 자릿수를 정렬해서 만든 배열을 이용하면 됩니다.
그림으로 설명하겠습니다.
입력을 432242 라고 하겠습니다.
위 그림처럼, 정렬을 하고 나면 각 자리에서 가장 앞에있는 수만을 다음 수로 선택하면 됩니다!
가장 앞에있는 수를 알아내는 방법은
1. 앞에 아무것도 없다 (i = 0)
2. 앞의 인덱스랑 값이 다르다.
입니다.
그리고, 맨 앞의 2를 선택했다고 가정합시다.
위 그림처럼, 만약 맨 앞의 2가 이미 쓰였다면, 가장 앞에 있는 2는 그 뒤의 2가 될 것입니다.
이렇게, 맨 앞의 수를 갱신해주면서 선택 가능한 수를 선택하도록 하면, 중복되는 수는 나오지 않을 것 입니다!
이제 수를 하나하나 만드는 방법을 정했으니, 중복되는 부분을 통해 메모이제이션을 적용하는 단게로 가봅시다.
문제에서 요구하는 것은 다음과 같습니다.
1. 항상 원래 수보다 작아야 한다.
2. 만들어진 가격이 m의 배수여야한다.
우선, 항상 원래의 수보다 작기 위해서는 어떻게 해야하는지 생각해 봅시다.
입력은 213이라고 가정하겠습니다.
위 그림처럼, 만약 선택했던 수들이 원래 수보다 작지 않다면 원래 수보다 큰 값을 선택해서는 안됩니다.
만약, 원래 수보다 작은것이 확정되어있다면, 선택할 수 있는 수는 나머지 모든 수와 같아집니다.
원래 수보다 작으려면, 가장 왼쪽에 있는 자리가 한 번이라도 원래 수보다 작으면 되는 것입니다!
따라서, 원래보다 작은 것을 선택한 적이 있었는지를 함수의 인자로 추가해주고, 메모이제이션에도 이를 알기 위한 공간을 만듭시다.
다음으로는 만들어진 가격이 m의 배수여야한다는 부분입니다.
이를 위해서는 우선 나눗셈이 어떻게 계산되었는지를 다시 한 번 돌이켜볼 필요가 있습니다.
가장 중요한 것은 위 부분입니다! 처음에 1과 3을 선택한 다음 이 수를 6으로 나눈 나머지 1뒤에 2를 붙여서 계산하는 것을 볼 수 있습니다!
만약 13245가 31245였으면 어떨까요?
13 % 6 = 1이고 31 % 6 = 1이기 때문에 나머지 진행 상황은 똑같을 것 입니다!
즉, 선택한 인덱스와 그 나머지가 같다면, 그 뒤에 부분은 중복 계산을 한다는 것을 알 수 있습니다!!
인덱스를 선택한다는 것은 인덱스마다 true와 false를 이용한다는 것으로 생각할 수 있습니다.
즉, 비트 마스킹을 하는 것이 편하다는 것입니다!
메모이제이션을 저장하는 배열은 따라서,
memoi[나머지][선택한 인덱스들][2(원래 수 보다 더 작을 경우와 아닐 경우)] 를 이용하면 구할 수 있습니다!
참고로, memoi에는 선택한 인덱스들이 원래 수보다 작은경우 or 아닐 경우, 나머지가 X라고 할 경우에 나누어 떨어지는 개수가 들어갑니다.
코드
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 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 | #pragma once #include<cstdio> #include<cstring> using namespace std; int div[20][1 << 15][2] , len, m; //e : 입력, digit : 정렬된 e char e[16], digit[16], temp[16]; const int MOD = 1000000007; int getLen() { int i; for (i = 0; e[i]; i++); return i; } void mergeSort(int left, int m, int right) { int l = left, r = m + 1, k = left; while (l <= m && r <= right) { if (digit[l] < digit[r]) temp[k++] = digit[l++]; else temp[k++] = digit[r++]; } while (l <= m) temp[k++] = digit[l++]; while (r <= right) temp[k++] = digit[r++]; for (int i = left; i <= right; i++) digit[i] = temp[i]; } void merge(int left, int right) { if (left < right) { int m = (left + right) / 2; merge(left, m); merge(m + 1, right); mergeSort(left, m, right); } } int setDiv(int index, int mod, int selected, bool lower) { if(index == len){ return lower && (mod == 0); } int &ret = div[mod][selected][lower]; if (ret != -1) return ret; ret = 0; for (int next = 0; next < len; next++) { if (selected & (1 << next)) continue; //이미 선택된 인덱스 금지 if (next != 0 && digit[next - 1] == digit[next] && ((selected & (1 << (next-1))) == 0)) continue; // 중복 선택 금지 if (!lower && e[index] < digit[next]) continue; // 이미 작지 않은 상태인데 더 큰거 선택 금지 int ns = selected | (1 << next); int nl = lower || (e[index] > digit[next]); int nm = (mod * 10 + digit[next] - '0') % m; ret += setDiv(index + 1, nm, ns, nl); ret %= MOD; } return ret; } int main() { int tc; scanf("%d\n", &tc); while (tc--) { scanf("%s %d\n", e, &m); memset(div, -1, sizeof(div)); len = getLen(); memcpy(digit, e, sizeof(e)); merge(0, len -1); int res = setDiv(0, 0, 0, 0); printf("%d\n", res); } return 0; } | cs |
여담
이번 문제는 결국 풀지 못했습니다!
앞의 수들을 나머지로만 저장하면 된다는 사실을 깨닫지 못했기 때문에 메모이제이션의 메모리가 너무너무 커져버렸어요.
DP의 매력이 바로 저것처럼 사소한걸 알기만 한다면 문제는 금방 풀린다는 것 같습니다.
물론, 사소한걸 놓친다면 풀 수 없게 되죠...
시간 복잡도는 어떻게 될까요?
e의 자리수가 n이라고 할 떄,
부분 문제의 개수가 m * 2^n이고, 함수에서 n번 반복하는 반복문이 있기때문에
이 됩니다.
'공부 > 알고리즘 문제풀이' 카테고리의 다른 글
[BOJ] 1158번 조세퍼스 문제 (0) | 2018.07.02 |
---|---|
[BOJ] 2161번 문제 카드1, 2164번 문제 카드2 (0) | 2018.07.02 |
[SW Expert Academy] 괄호 짝짓기 (0) | 2018.07.01 |
[SW Expert Academy] 길찾기 (0) | 2018.07.01 |
[알고스팟] 드래곤 커브 (0) | 2018.07.01 |