포스트

프로그래머스 옹알이 토큰 매칭 구현

실패 판정 위치 하나로 정답이 갈린다

문자열을 앞에서부터 토큰으로 갉아먹는 문제. 알고리즘이 아니라 실패 판정을 어느 루프에 두느냐 때문에 답이 어긋났다. string::compare 3인자 오버로드와 조기 실패 제거.

프로그래머스 옹알이 토큰 매칭 구현

문자열을 앞에서부터 토큰으로 갉아먹는 문제를 풀었는데, 알고리즘이 틀린 게 아니라 실패 판정을 놓는 위치가 틀려서 답이 어긋났다. 이 글에서는 옹알이 문제의 구현을 따라가며 그 지점을 이야기하려 한다 — 구현은 토큰 소비 루프와 사용 여부 플래그, 트러블슈팅은 string::compare 시그니처 오해와 조기 실패 판정 두 건이다.

문제 — 옹알이(aya, ye, woo, ma)

각 발음(“aya”, “ye”, “woo”, “ma”)을 최대 1번씩만 사용해 단어를 만들 수 있을 때, 발음 가능한 단어 개수를 구하는 문제다.

트러블슈팅 — 토큰을 다 보기 전에 실패로 빠진다

원인: match 실패 판정을 토큰 순회 for 안쪽에 둔 것. 토큰 네 개 중 첫 번째가 안 맞는 순간 실패로 확정되니, 두 번째 이후 토큰은 확인조차 되지 않는다. 여기에 string::compare의 시그니처를 잘못 알고 있어 비교 자체가 의도와 다르게 동작한 것이 겹쳤다.

증상: "wooye"처럼 앞 토큰이 “aya”가 아닌 단어들이 전부 불가 판정으로 떨어져, 정답보다 개수가 적게 나왔다.

해결: 실패 판정을 토큰 for를 완전히 빠져나온 뒤로 옮겨 if (!match)를 while 본문 끝에서 검사하게 했다. 비교는 word.compare(pos, len, token[j]) == 0 — pos부터 len글자를 token과 견주는 3인자 오버로드로 고정했다. 순회는 복사 비용이 없는 const string& 범위 기반 for로 정리했다.

기술 구현 — pos 포인터로 소비하는 토큰 매칭

고친 기본형이 아래 코드다. pos 포인터가 왼쪽부터 문자열을 소비하고, used 플래그가 각 발음의 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
int solution(vector<string> babbling) {
    int answer = 0;
    vector<string> token = {"aya", "ye", "woo", "ma"};

    // range-based for: babbling의 각 단어를 복사 없이(const reference) 순회
    for (const string& word : babbling) {
        vector<bool> used(4, false); // 각 발음은 최대 1번만 사용
        int pos = 0;                 // 현재 읽는 위치(왼쪽부터 소비)
        bool ok = true;

        // while 사용 이유: 단어 길이 끝까지 pos를 전진시키며 확인
        // (시간복잡도는 단어 길이 L에 비례, 전체 O(N * L * 4))
        while (pos < (int)word.size()) {
            bool match = false;

            for (int j = 0; j < (int)token.size(); j++) {
                int len = (int)token[j].size();

                // compare(pos, len, token[j]) == 0 :
                // word의 pos부터 len글자가 token[j]와 같으면 매칭
                if (!used[j] &&
                    pos + len <= (int)word.size() &&
                    word.compare(pos, len, token[j]) == 0) {
                    used[j] = true;
                    pos += len;      // 매칭된 길이만큼 이동
                    match = true;
                    break;
                }
            }

            // 토큰 전체를 다 확인한 뒤에도 매칭이 없을 때만 실패
            if (!match) {
                ok = false;
                break;
            }
        }

        // 카운트 증가 시점: '단어 하나' 검증이 끝난 뒤에만 증가
        if (ok && pos == (int)word.size()) {
            answer++;
        }
    }

    return answer;
}

복습하면서 유효 단어 목록과 개수를 함께 출력하는 버전도 만들어 봤다. 로직은 같고, 통과한 단어를 validWords에 모아 마지막에 출력하는 부분만 다르다.

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
int solution(vector<string> babbling) {
    int answer = 0;
    int count = 0;
    vector<string> validWords;
    vector<string> token = {"aya", "ye", "woo", "ma"};

    for (const string& word: babbling) {
        vector<bool> used(4,false);
        int pos = 0;
        bool ok = true;

        while (pos < (int)word.size()) {
            bool match = false;
            for (int j = 0; j < (int)token.size(); j++) {
                int len = (int)token[j].size();
                if (!used[j] &&
                    pos + len <= (int)word.size() &&
                    word.compare(pos, len, token[j]) == 0) {
                    used[j] = true;
                    pos += len;
                    match = true;
                    break;
                }
            }
            if (!match) {
                ok = false;
                break;
            }
        }

        if (ok && pos == (int)word.size()) {
            answer++;
            count++;
            validWords.push_back(word);
        }
    }

    for (int i = 0; i < (int)validWords.size(); i++) {
        cout << validWords[i] << endl;
    }
    cout << count << endl;

    return answer;
}

int main() {
    vector<string> test = {"yewoo", "wooye", "mayeaya", "ayawooma", "woowoo", "aya"};
    cout << solution(test) << endl;
    return 0;
}

정리하면 핵심은 pos 포인터로 문자열을 끝까지 소비하며 토큰을 매칭하는 것이고, used 배열로 각 발음의 1회 제한을 건다. 복잡도는 O(N × L × 4) 수준이라 제한 안에서 안정적이다.

핵심 요약 — 문자열 토큰 매칭은 pos 포인터로 왼쪽부터 소비하며 word.compare(pos, len, token)으로 비교하고, used 플래그로 사용 횟수 제한을 건다. 실패 판정은 토큰 전체를 확인한 뒤에만 내려야 조기 실패를 막을 수 있다.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.