'1'과 '2'로만 이루어진 문자열 S가 있다고 상상해 보세요. 이 문자열은 특별한 성질 때문에 마법 문자열(Magical String)이라고 불리는데, 그 비결은 문자 '1'과 '2'가 연속해서 등장하는 횟수를 순서대로 이어 붙였을 때 그 결과가 놀랍게도 문자열 S 자신이 된다는 점입니다.
마법 문자열 S의 첫 부분은 다음과 같습니다.
S = "1221121221221121122……"
S에서 연속된 같은 문자들을 그룹으로 묶어 보면 다음과 같습니다.
1 | 22 | 11 | 2 | 1 | 22 | 1 | 22 | 11 | 2 | 11 | 22 ……
각 그룹의 길이, 즉 '1' 또는 '2'가 연속해서 나타난 횟수를 차례대로 적어 보면 다음과 같습니다.
1, 2, 2, 1, 1, 2, 1, 2, 2, 1, 2, 2 ……
흥미롭게도 이 수열은 원래 문자열 S와 정확히 일치합니다. 바로 이러한 자기 참조적(self-referential) 성질이 이 문자열을 "마법"이라고 부르는 이유입니다.
문제 정의
정수 N이 주어졌을 때, 마법 문자열 S의 처음 N개 문자 가운데 '1'의 개수를 구하는 것이 목표입니다.
예시: 입력이 6이라면 출력은 3입니다. 마법 문자열의 처음 6개 문자는 "122112"이며, 여기에는 '1'이 세 개 포함되어 있기 때문입니다.
해결 접근 방식
이 문제는 두 포인터(two pointer) 기법을 활용하면 효율적으로 해결할 수 있습니다. 한 포인터(head)는 "다음 문자를 몇 번 반복할지"를 알려 주는 값을 가리키고, 다른 포인터(tail)는 새 문자를 채워 넣을 위치를 가리킵니다. 즉, 배열의 앞부분을 읽으면서 뒷부분을 동시에 채워 나가는 방식입니다.
구체적인 알고리즘 단계는 다음과 같습니다.
- n ≤ 0이면 0을 반환하고, n ≤ 3이면 1을 반환합니다.
- ret := 1로 초기화하고, 크기가 n인 배열 arr을 준비합니다.
- arr[0] := 1, arr[1] := 2, arr[2] := 2로 초기값을 설정합니다.
- head := 2, tail := 3, num := 1로 초기화합니다.
- tail < n인 동안 다음을 반복합니다.
- i를 0부터 arr[head] − 1까지 반복하면서:
- arr[tail] := num을 대입합니다.
- num이 1이고 tail < n이면 ret을 1 증가시킵니다.
- tail을 1 증가시킵니다.
- tail ≥ n이 되면 내부 반복문을 종료합니다.
- num = num XOR 3으로 갱신합니다. (XOR 3은 값이 1 ↔ 2로 전환되는 효과가 있습니다.)
- head를 1 증가시킵니다.
- i를 0부터 arr[head] − 1까지 반복하면서:
- 최종적으로 ret을 반환합니다.
C++ 구현 예제
#include <bits/stdc++.h>
using namespace std;
class Solution {
public:
int magicalString(int n) {
if(n <= 0) return 0;
if(n <= 3) return 1;
int ret = 1;
vector <int> arr(n);
arr[0] = 1;
arr[1] = 2;
arr[2] = 2;
int head = 2;
int tail = 3;
int num = 1;
while(tail < n){
for(int i = 0; i < arr[head]; i++){
arr[tail] = num;
if(num == 1 && tail < n) ret++;
tail++;
if(tail >= n) break;
}
num ^= 3;
head++;
}
return ret;
}
};
main(){
Solution ob;
cout << (ob.magicalString(6));
}
입력
6
출력
3
동작 원리 살펴보기
이 알고리즘의 핵심은 이미 확정된 배열의 앞부분(arr[head])을 "반복 횟수 지시자"로 활용해 뒷부분(arr[tail])을 채워 나간다는 점입니다. 문자열의 접두사가 곧 생성 규칙이 되는 셈이므로, 별도의 규칙을 따로 저장하지 않고도 필요한 길이만큼 마법 문자열을 생성할 수 있습니다.
또한 num ^= 3 연산은 1과 2를 번갈아 전환하는 우아한 기법입니다. 1 XOR 3 = 2이고, 2 XOR 3 = 1이기 때문에 조건문 없이 간단히 값을 뒤집을 수 있습니다.
성능 면에서 이 알고리즘의 시간 복잡도는 O(n), 공간 복잡도 역시 O(n)으로, 문자열을 직접 하나씩 검증하는 방식보다 훨씬 효율적입니다.