RESTORE <- 클릭시 문제 링크로 이동합니다.
실험 데이터 복구하기
어떻게 풀까?
뭔가 문제 풀이를 어떻게 설명해드려야될지 정말 고민되는 문제였습니다!
이 문제를 풀다보면 몇가지 규칙이 보입니다.
1. 아예 포함되는 문자는 삭제해도 무방하다.
2. 아예 겹치는 것이 없는 문자는 어떻게 끼워넣어도 상관없다.
3. 겹치는 문자를 최대로 해야지 가장 짧아진다!
이 정도만 만족하면 문제가 해결되는 것을 볼 수 있습니다!
가장 짧은 문자열 길이는 어떻게 찾는것이 좋을까요?
예제 1번을 예로 들겠습니다.
geo oji jing
우선 문자열이 겹치는 것을 생각하지 않고 k개의 문자열의 순서를 나열해 봅시다.
geo oji jing
geo jing oji
등이 나올 것입니다.
그럼 이제 해줘야 할 일은 3번을 이용해서 겹치는 부분을 최대화 하는 것이 될겁니다.
geo oji jing의 경우에는 geojing가 되겠죠!
이렇게 하면 문제의 답을 꽤 쉽게 찾을 수 있게 됩니다.
어떻게 문자열 순서를 하던 결국 총 길이는 같아질 것입니다!
달라지는 곳은 바로 i - 1과 i번째의 단어가 얼마나 겹치느냐의 차이이죠!
즉, 이 문제는 가장 겹치는 부분이 많은 수열의 순서를 찾아내는 것이 됩니다!
이렇게 되면 살짝 문제가 생기는 부분이 있습니다.
바로 1번에서 말했던, 아예 포함되는 부분을 삭제해야한다는 것이죠.
예를 들어보겠습니다!
abcdef - de - cdefgh를 겹친다고 생각해봅시다!
이렇게 되면, 실제로는 abcdefgh가 정답이 되지만,
지금 적용하려는 알고리즘에 의하면 i - 2를 보지 않기 때문에 abcdef와 cdefgh가 abcdefgh로 겹쳐질 수 있다는 사실을 보기 힘들게 되죠.
즉, 포함되는 문자열이 있다면 바로 삭제 하는게 좋다는 것입니다!
이제 중복되는 부분이 어떻게 되는지 생각해봅시다.
문자열이 10개가 주어졌다고 생각해봅시다.
그리고, 현재 0번 문자열 - 1번 문자열 - 2번 문자열을 방문한 상태입니다.
그럼 이제 나머지 3,4,5,6,7,8,9번 문자열에서 만들 수 있는 가장 짧은 문자열을 만드는 것이 현재 상태에서 가장 짧은 문자열을 만드는 방법입니다!
다음으로, 1번 문자열 - 0번 문자열 - 2번 문자열을 방문한 상태라고 합시다.
위에서 했던 것처럼 또다시 3,4,5,6,7,8,9번 문자열에서 만들 수 있는 가장 짧은 문자열을 만드는 것이 현재 상태에서 가장 짧은 문자열을 만드는 방법입니다!
즉, 현재 방문한 위치들을 true와 false를 통해 k자리수의 2진법 수로 나타낸다면 비트마스킹을 통해서 중복 문제를 해결할 수 있을 것 입니다!
다만, 첫번째로 선택된 글자에 따라서 앞의 글자와 겹치는 글자가 달라지기 때문에, 선택된 인덱스도 포함해서 메모이제이션을 적용합시다!
이제 지금까지 생각했던 것을 토대로 점화식을 만들어봅시다!
restore는 총 겹져있는 글자수를 반환하고, operlap(a,b)는 a 문자열과 b 문자열이 실제로 겹치는 문자의 수라고 입니다.
다만, 이 문제는 실제로 가장 짧은 글자를 출력하는 것이 목표이기 때문에 여기서 끝나지 않습니다.
저 같은 경우에는 마찬가지로 selected와 used를 통해서 가장 겹쳐져 있는 글자수가 많은 next값이 온다면,
해당 next를 path[selected][used]에 저장했습니다.
그림으로 보면 다음과 같은 알고리즘이라고 할 수 있겠습니다.
처음에는 geo는 jing로 가야한다고 합니다.
하지만 oji가 겹치는 곳이 알고보니 더 많았고, geo는 oji로 가야한다고 수정합니다.
또한, 출력할때도 문자열을 그대로 출력하면 예제 1번의 경우에는 geoojijing가 출력되겠죠??
하지만, operlap이 얼마인지 이미 알기 때문에 그 문제도 해결할 수 있습니다!
geo와 oji가 겹치는게 1이기 때문에 1번 문자는 ge만 출력하면 됩니다! 왜냐하면 다음거에서 어짜피 나머지 1개를 출력해주기 때문이죠!
이러한 방법들을 어떻게 코드로 구현했는지 알아봅시다.
코드
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 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 | #pragma once #include<cstdio> #include<cstring> int n; char str[16][41], res[1000], tstr[16][41]; int mlen[16][1 << 15], len[16], tlen[16], order[16], temp[16]; int to[16][16], path[16][1 << 15]; int min(int i1, int i2) { return i1 < i2 ? i1 : i2; } int getLen(int idx, int visit) { if (visit == (1 << n) - 1) { path[idx][visit] = 0; return 0; } int &ret = mlen[idx][visit]; if (ret != -1) return ret; ret = 987654321; for (int next = 0; next < n; next++) { if ((1 << next) & visit) continue; //이미 방문한 인덱스 int dist = getLen(next + 1, visit | (1 << next)) - to[idx][next + 1] + len[next + 1]; if (dist < ret) { path[idx][visit] = next + 1; ret = dist; } } return ret; } //길이가 긴 순서대로 merge void mergeSort(int left, int m, int right) { int l = left, r = m + 1, k = l; while (l <= m && r <= right) { if (len[l] < len[r]) { tlen[k] = len[r]; temp[k++] = order[r++]; } else { tlen[k] = len[l]; temp[k++] = order[l++]; } } while (l <= m) { tlen[k] = len[l]; temp[k++] = order[l++]; } while (r <= right) { tlen[k] = len[r]; temp[k++] = order[r++]; } for (int i = left; i <= right; i++) { len[i] = tlen[i]; order[i] = temp[i]; } } void merge(int l, int r) { if (l < r) { int m = (l + r) / 2; merge(l, m); merge(m + 1, r); mergeSort(l, m, r); } } //경로 탐색 만약 경로면 다음 문자열과 겹치기 전 부분까지만 출력! void setRes(int idx, int visit, int &l) { int next = path[idx][visit]; for (int i = 0; i < len[idx] - to[idx][next]; i++) { res[l++] = str[idx][i]; } if(visit != (1 << n) - 1) setRes(next, visit | (1 << (next - 1)), l); } void init() { //포함되는지 검사 for (int i = 1; i <= n; i++) { for (int j = i + 1; j <= n;) { bool part = false; for (int k = 0; k <= len[i] - len[j]; k++) { bool same = true; for (int l = 0; l < len[j]; l++) { if (str[i][k + l] != str[j][l]) { same = false; break; } } if (same) { part = true; break; } } //포함된다면 해당 문자열 삭제 if (part) { for (int k = j; k < n; k++) { memcpy(str[k], str[k + 1], sizeof(str[k])); len[k] = len[k + 1]; } n--; } else { j++; } } } memset(to, 0, sizeof(to)); memset(mlen, -1, sizeof(mlen)); memset(path, 0, sizeof(path)); //겹치는 길이 검사 for (int i = 1; i <= n; i++) { for (int j = 1; j <= n; j++) { if (i == j) continue; int st = 0 > len[i] - len[j] + 1 ? 0 : len[i] - len[j] + 1; int k; for (k = st; k < len[i]; k++) { bool allsame = true; for (int l = 0; k + l < len[i]; l++) { if (str[i][k + l] != str[j][l]) { allsame = false; break; } } if (allsame) { break; } } to[i][j] = len[i] - k; } } } int main() { int tc; scanf("%d\n", &tc); while (tc--) { scanf("%d\n", &n); for (int i = 1; i <= n; i++) { order[i] = i; scanf("%s\n", str[i]); len[i] = strlen(str[i]); } merge(1, n); for (int i = 1; i <= n; i++) { memcpy(tstr[i], str[order[i]], sizeof(tstr[i])); } memcpy(str, tstr, sizeof(str)); init(); int idx = getLen(0, 0); int l = 0; setRes(0, 0, l); res[l] = 0; printf("%s\n", res); } return 0; } | cs |
여담
정말 어려웠습니다!
뭔가 알고리즘은 금방 찾았지만
1번 조건에 포함되면 삭제한다는 부분에서 삽질을 많이했네요!
처음에 문자열의 길이가 큰 것을 앞으로 오도록하니 문제가 훨씬 쉽게 풀렸습니다!
시간복잡도는 어떻게 될까요?
부분문제가 k * 2^k이고, 함수 내에서 k번의 반복을 하기 때문에 의 시간복잡도를 가지게 됩니당!!!
'공부 > 알고리즘 문제풀이' 카테고리의 다른 글
[SW Expert Academy] Code Battle! (0) | 2018.07.11 |
---|---|
[SW Expert Acade] 4616. 점프점프! 개굴이의 점핑! (0) | 2018.07.05 |
[BOJ] 1158번 조세퍼스 문제 (0) | 2018.07.02 |
[BOJ] 2161번 문제 카드1, 2164번 문제 카드2 (0) | 2018.07.02 |
[알고스팟] 웨브바짐 (2) | 2018.07.02 |