포스트

프로그래머스 12926 - 시저 암호 (Lv.1)

핵심 접근 — 대소문자별 기준점을 빼 0..25 상대 좌표로 옮기고 % 26

프로그래머스 12926 - 시저 암호 (Lv.1)

출처: https://school.programmers.co.kr/learn/courses/30/lessons/12926

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
// 프로그래머스 12926 - 시저 암호 (Lv.1)
// https://school.programmers.co.kr/learn/courses/30/lessons/12926

// 문제 설명
// 문장의 각 알파벳을 n만큼 뒤로 밀어 암호화한다. "AB"를 1만큼 밀면 "BC",
// "z"를 1만큼 밀면 "a"가 된다. 문자열 s와 거리 n이 주어질 때 암호문을 반환하라.

// 제약 조건
// 공백은 밀지 않고 그대로 둔다. s는 소문자, 대문자, 공백으로만 구성된다.
// s의 길이 <= 8,000, n은 1 이상 25 이하의 자연수 (대문자는 대문자로, 소문자는 소문자로)

// Example
// Input : s = "AB", n = 1        Output: "BC"
// Input : s = "z",  n = 1        Output: "a"
// Input : s = "a B z", n = 4     Output: "e F d"

// 접근 — 대소문자별 기준점을 빼서 0..25 상대 위치로 바꾸고 % 26
// 'z' + 1이 '{'가 되는 것을 막으려면 알파벳 구간을 0부터 시작하는 좌표로 옮겨야 한다.
// 1) 대문자면 기준점 'A', 소문자면 'a'를 잡는다.
// 2) c - base로 0..25 상대 위치를 얻고 n을 더한 뒤 % 26으로 감아준다.
// 3) 다시 base를 더해 원래 구간(대문자/소문자)으로 되돌린다 — 두 구간을 한 식으로 처리.
// 공백은 어느 구간에도 없으므로 건너뛴다.
// 시간 O(n), 공간 O(1) (입력 문자열을 제자리 변환)

#include <string>
#include <cctype>

using namespace std;

string solution(string s, int n) {
    for (char& c : s) {                                         // 참조로 받아 제자리 변환
        if (c == ' ') continue;                                 // 공백은 밀지 않는다

        char base = isupper(static_cast<unsigned char>(c)) ? 'A' : 'a';   // 구간 기준점
        c = base + (c - base + n) % 26;                          // 0..25로 옮겨 밀고 감은 뒤 복귀
    }

    return s;
}

정리

  • 이 문제의 전부는 'z' + 1'a'가 아니라 '{'라는 사실이다. ASCII에서 소문자 구간은 97~122로 끝나고 그 뒤에 {가 붙어 있어서, 그냥 더하면 알파벳 밖으로 튀어나간다. 감아주려면 나머지 연산이 필요하고, 나머지 연산은 0에서 시작하는 좌표계에서만 의미가 맞는다.
  • 그래서 base를 빼는 것이 필수 단계다. c - base로 0..25 상대 위치를 얻고 → + n% 26+ base로 복귀. (c + n) % 26처럼 base를 빼지 않고 나머지를 취하면 122가 아니라 아무 값이나 나온다('z'면 122 % 26 = 18 = '\x12'). 좌표 이동 → 연산 → 역이동이 한 세트다.
  • 대문자와 소문자를 두 갈래로 복사해 쓰지 않는 이유도 여기 있다. 차이는 base값 하나뿐이라 기준점만 변수로 뽑으면 동일한 한 식으로 두 구간을 처리한다. if (isupper) {...} else {...}로 갈라 쓰면 같은 산술이 두 번 나오고, 한쪽만 고치는 실수가 생긴다.
  • 공백은 continue로 건너뛴다. 공백(32)은 두 알파벳 구간 어디에도 없으므로, 밀어버리면 엉뚱한 기호가 되고 isupper가 false라 소문자 기준점이 잡혀 더 어긋난다. “바꾸지 않을 문자”를 먼저 걸러내는 가드가 대소문자 판정보다 앞에 와야 하는 순서.
  • 제약이 1 <= n <= 25(c - base + n)이 항상 음수가 아니고 최대 50이라 % 26 한 번으로 충분하다. 만약 왼쪽으로 미는(복호화) 변형이면 음수 나머지가 구현체 정의로 음수가 되므로 ((x % 26) + 26) % 26 형태가 필요해진다. 시저 암호를 일반화할 때 첫 번째로 깨지는 지점.
  • 길이 8,000에 O(n) 한 번 순회, for (char& c : s)로 제자리 변환이라 공간 O(1). 12930 이상한 문자 만들기와 골격이 같은 계열이지만 — 12930은 위치의 홀짝으로 대소문자를 바꾸고 공백을 카운터 리셋 신호로 쓰는 반면, 여기서는 값을 모듈러로 밀고 공백은 그냥 건너뛴다. c - 'a' 인덱스 변환이 룩업(5622 다이얼)·집합 비트(1987 알파벳)에 이어 좌표 이동으로 쓰이는 세 번째 용도.
  • 검증: 예제 3개(“BC”, “a”, “e F d”)와 랩어라운드 경계("Zz Aa", n=25 → "Yy Zz") 통과 (MSVC /std:c++17 컴파일·실행).

핵심 요약'z' + 1'a'가 아니라 '{'이므로, 대소문자별 기준점을 빼서 0..25 상대 좌표로 옮긴 뒤 % 26으로 감고 다시 기준점을 더한다. base를 뽑아내면 대문자·소문자를 한 식으로 처리하고, 공백 가드를 대소문자 판정보다 먼저 둔다.

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