1학년 / 2학기 / C++ Exercise / 4강

Exercise 04 · 모듈러 거듭제곱과 최소 변환

Exercise 04의 개념, 문제 조건, 알고리즘, 정답 코드와 실행 흐름을 정리합니다.

Exercise 02 · Exercise 03 · Exercise 04 · Exercise 05

출처: CPP Programming Exercise 04.pdf. 정답은 Solution 04 PDF의 코드를 사용하고 해설도 해당 구현에 맞췄습니다. 정답 코드는 접어서 확인할 수 있습니다.

핵심 개념

1. 함수의 구성과 값 전달

int addOne(int value) {  // int: 반환형, value: 매개변수
    return value + 1;
}
// addOne(5)에서 5는 인자이며, 반환값은 6입니다.
개념동작
값 전달복사본 사용. 함수 안에서 b를 바꿔도 호출자의 변수는 유지
void 함수반환값이 없으며 return;으로 종료 가능
함수 선언정의가 호출보다 뒤에 있다면 호출 전에 선언 필요

2. 재귀가 끝나려면

재귀에는 다음 두 조건이 함께 필요합니다.

  1. 기저 조건: 더 호출하지 않고 종료하는 경우
  2. 입력 변화: 호출할수록 기저 조건에 가까워지는 변화
거듭제곱의 지수: 10 → 5 → 2 → 1 → 0
                                   기저 조건에서 반환

3. 이름의 범위와 변수 관련 키워드

블록 안의 이름은 해당 범위에서 사용합니다. 내부에 같은 이름을 선언하면 외부 이름이 가려질 수 있습니다.

문법기억할 의미
지역 static호출 사이에도 값 유지
extern다른 곳의 정의를 참조하는 선언
auto초기값으로 자료형 추론
const초기화 후 변경 제한
네임스페이스같은 이름의 충돌을 구분. std::가 대표적인 예

4. 수학 함수와 정확한 정수 계산

<cmath>에는 ceil, floor, fabs, sqrt, log 등이 있습니다.

예시결과방향
ceil(-2.3)-2입력 이상인 가장 작은 정수 값
floor(-2.3)-3입력 이하인 가장 큰 정수 값

Exercise 04

Quiz 1 - 모듈러 거듭제곱

난이도: 상

Problem (Original)

CPP Programming Exercise 04.pdf, slide 20

Quiz 1: Modular Power

Problem. Given integers a, b, and m, print the remainder when a^b is divided by m. For this problem, define 0^0 = 1.

Input. Three integers a b m on one line.

Output. The remainder, followed by a newline.

Constraints. 0 ≤ a, b ≤ 10⁹, 2 ≤ m ≤ 10⁹

SampleInputOutput
12 10 100024
20 0 71
33 13 1000323
문제 한국어 번역 보기

문제. 정수 a, b, m이 주어질 때 a^b를 m으로 나눈 나머지를 출력합니다. 이 문제에서는 0^0 = 1로 정의합니다.

입력. 한 줄에 세 정수 a b m이 주어집니다.

출력. 나머지를 출력하고 줄을 바꿉니다.

제약 조건. 0 ≤ a, b ≤ 10⁹, 2 ≤ m ≤ 10⁹

예제 입력과 출력은 위 표와 같습니다.

개념과 예제

알고리즘: 절반 거듭제곱을 한 번 계산하기

P(b) = a^b % m이라고 두면 P(0) = 1입니다. 양의 지수에서 half = P(b / 2)를 한 번 계산합니다. 짝수 지수 b = 2k이면 a^b = (a^k)², 홀수 지수 b = 2k + 1이면 a^b = (a^k)² × a이므로 half를 제곱하고 홀수일 때만 a를 한 번 더 곱합니다.

매번 곱한 직후 % m을 적용합니다. (x × y) % m = ((x % m) × (y % m)) % m이므로 중간에 나머지만 저장해도 최종 답은 같습니다. x = qm + r, y = pm + s로 놓고 곱하면 rs를 제외한 모든 항에 m이 포함되어 나머지가 0이 됩니다.

정답 코드 · Solution 04

출처: CPP Programming Exercise Solution 04.pdf, PDF 3쪽(슬라이드 23). 제공된 재귀 구현을 사용합니다.

정답 코드 보기정답 코드 숨기기
exercise04_quiz1.cpp
#include <iostream>
long long powerMod(long long a, long long b,
                   long long m) {
    if (b == 0) {
        return 1;
    }
    long long half = powerMod(a, b / 2, m);
    long long result = half * half % m;
    if (b % 2 != 0) {
        result = result * (a % m) % m;
    }
    return result;
}
int main() {
    long long a = 0, b = 0, m = 0;
    std::cin >> a >> b >> m;
    std::cout << powerMod(a, b, m) << '\n';
    return 0;
}

코드 해설과 재귀 호출 추적

  1. b == 0이면 즉시 1을 반환합니다. 문제의 0^0 = 1 조건도 여기서 처리합니다.
  2. powerMod(a, b / 2, m)가 끝날 때까지 현재 호출은 기다립니다. 각 호출의 b, half, result는 별개의 지역 변수입니다.
  3. 반환된 half를 두 번 곱한 뒤 나머지를 구합니다. 같은 함수를 두 번 호출하지 않고 결과를 재사용하는 것이 중요합니다.
  4. b가 홀수라면 a % m을 추가로 곱하고 다시 나머지를 구합니다.
  5. 계산된 result를 바로 위 호출에 돌려줍니다. 마지막에는 main이 받아 출력합니다.

2 10 1000의 호출은 지수 10 → 5 → 2 → 1 → 0으로 내려간 뒤 거꾸로 돌아옵니다.

반환 중인 지수 bhalf제곱의 나머지홀수 보정반환값
0해당 없음해당 없음기저 조건1
1111 × 2 % 10002
224없음4
541616 × 2 % 100032
103224없음24

3 13 1000은 323, 0 0 7은 1을 출력합니다. a가 0이고 b가 양수이면 답은 0입니다.

자료형, 복잡도, 실수하기 쉬운 점

두 나머지는 각각 m보다 작으므로 곱은 10^18 미만입니다. long long은 이를 담을 수 있지만 보통의 32비트 int는 담지 못합니다. % m은 곱셈 이후 실행되므로 int로 곱한 뒤 long long에 저장하는 방식은 안전하지 않습니다.

지수가 매번 절반이 되어 재귀 단계는 로그 개수입니다. 시간과 호출 스택 공간은 모두 **O(1 + log(b + 1))**입니다. b가 0인 경우에도 기저 조건을 확인하는 상수 비용이 있습니다. 입력 범위에서는 최대 31개의 호출이 쌓입니다.

powerMod(a, b / 2, m)를 두 번 호출하면 같은 하위 문제를 반복 계산합니다. 지수를 1씩 줄이거나 a를 b번 곱하는 방법은 큰 입력에서 너무 느립니다. ^는 C++에서 거듭제곱 연산자가 아니라 비트 XOR입니다.


Quiz 2 - 최소 변환 횟수

난이도: 최상

Problem (Original)

CPP Programming Exercise 04.pdf, slide 21

Quiz 2: Minimum Transformations

Problem. Start with a positive integer a. In one operation, either double the current value or subtract 1. The value must remain positive. Find the minimum number of operations needed to reach b.

Input. Two integers a b on one line.

Output. The minimum number of operations, followed by a newline.

Constraints. 1 ≤ a, b ≤ 10⁹

SampleInputOutput
14 72
210 37
33 103
문제 한국어 번역 보기

문제. 양의 정수 a에서 시작합니다. 한 번의 연산으로 현재 값을 두 배로 만들거나 1을 뺄 수 있습니다. 값은 항상 양수여야 합니다. b에 도달하는 데 필요한 최소 연산 횟수를 구합니다.

입력. 한 줄에 두 정수 a b가 주어집니다.

출력. 최소 연산 횟수를 출력하고 줄을 바꿉니다.

제약 조건. 1 ≤ a, b ≤ 10⁹

예제 입력과 출력은 위 표와 같습니다.

개념과 예제

접근 방법

  • a가 b보다 크거나 같으면 값을 늘리는 두 배 연산은 도움이 되지 않습니다. 1을 a-b번 빼는 것이 최적입니다.
  • b가 더 크면 정방향에서 두 선택을 비교하기보다 b에서 a로 거꾸로 이동합니다.
  • 정방향의 두 배 연산은 역방향에서 짝수를 2로 나누는 연산이 됩니다.
  • 정방향의 1 빼기 연산은 역방향에서 1 더하기 연산이 됩니다.
  • 현재 b가 홀수라면 어떤 정수의 두 배일 수 없으므로 먼저 1을 더해야 합니다.
  • 현재 b가 짝수라면 2로 나누어 값을 가장 빠르게 줄입니다.
  • 역방향 값이 a 이하가 되면, 남은 차이만큼 정방향에서 1을 빼면 됩니다.
  • 입력과 역연산 중의 값은 10억 이하입니다. 연산 횟수도 a - 1 + 60 이하라는 충분한 상한을 잡을 수 있어, 일반적인 32비트 int 범위 안에서 계산할 수 있습니다.

정답 코드 · Solution 04

출처: CPP Programming Exercise Solution 04.pdf, PDF 6쪽(슬라이드 26). 제공된 minOperations 함수를 사용합니다.

정답 코드 보기정답 코드 숨기기
exercise04_quiz2.cpp
#include <iostream>
long long minOperations(long long a, long long b) {
    long long steps = 0;
    while (b > a) {
        if (b % 2 != 0) {
            ++b;
        } else {
            b /= 2;
        }
        ++steps;
    }
    return steps + a - b;
}
int main() {
    long long a = 0, b = 0;
    std::cin >> a >> b;
    std::cout << minOperations(a, b) << '\n';
    return 0;
}

코드 해설: 목표에서 거꾸로 돌아오기

정방향에서 무작정 두 배와 1 빼기를 모두 시도하면 후보 경로가 많아집니다. 목표 b에서 시작해 연산을 거꾸로 생각하면 선택이 단순해집니다.

정방향에서 허용되는 연산역방향에서 대응하는 연산
x → 2x짝수 y → y / 2
x → x - 1y → y + 1

코드에서 a는 처음 출발점으로 유지하고, b는 역방향으로 움직이는 현재 값으로 사용합니다. steps에는 지금까지 수행한 역연산 수를 셉니다. 경로를 뒤집어도 연산 개수는 같습니다.

  1. b > a일 때만 반복합니다. 아직 목표에서 출발점까지 내려와야 하는 상태입니다.
  2. b가 홀수이면 2로 나눌 수 없으므로 1을 더합니다.
  3. b가 짝수이면 2로 나눕니다.
  4. 어느 연산이든 한 번 수행했으므로 steps를 1 늘립니다.
  5. b <= a가 되면 역방향에서 a - b번 1을 더하면 도착합니다. return steps + a - b로 반복문 없이 합산해 반환합니다.

왜 짝수일 때 바로 나누는 것이 최적인가요?

그냥 “빨리 작아진다”는 이유만으로 최소 횟수를 보장할 수는 없습니다. 다른 선택이 더 나을 수 없는지 비교해야 합니다.

홀수인 경우: 역방향에서 허용되는 연산 중 2로 나누기는 불가능합니다. 따라서 +1이 반드시 필요합니다. 정수 나눗셈으로 홀수를 억지로 버림 처리하면 원래 허용된 연산을 거꾸로 한 것이 아닙니다.

짝수 y > a인 경우: 지금 나누지 않고 +1만 한다면 더 큰 값으로 가므로 a에 도착할 수 없습니다. 결국 언젠가는 나누어야 합니다. 첫 나눗셈 직전까지 더한 횟수는 짝수여야 하므로 이를 2k번이라고 해 봅시다.

나눗셈을 미룬 경로:
y → (+1을 2k번) → y + 2k → (/2) → y/2 + k
필요한 연산: 2k + 1번
 
먼저 나눈 경로:
y → (/2) → y/2 → (+1을 k번) → y/2 + k
필요한 연산: k + 1번

같은 값에 도달하는데 먼저 나누면 연산을 더 많이 쓰지 않습니다. k > 0이면 오히려 k번 적습니다. 따라서 나눗셈을 먼저 하는 최적 경로가 항상 존재하며, 짝수에서 바로 나누는 선택이 정당합니다. 중간에 a에 먼저 도착한다면 거기서 종료하면 되므로 더 유리합니다.

왜 b <= a이면 차이만 더하나요?

역방향에서 작은 b를 큰 a로 올려야 할 때, 값을 늘리는 방법은 +1뿐입니다. /2는 값을 더 작게 만들어 필요한 증가량만 늘립니다. 따라서 남은 최소 횟수는 정확히 a - b입니다.

정방향으로 보면 a에서 그 작은 값까지 1씩 빼는 구간입니다. 경로상의 값은 모두 1 이상이므로 양수 조건도 지킵니다.

실제 입력 4 7 추적

단계역방향의 현재 b선택연산 후 b누적 횟수
17홀수이므로 +181
28짝수이므로 /242
종료4b > a가 거짓남은 차이 4 - 4 = 02

역방향은 7 → 8 → 4입니다. 이를 뒤집으면 실제로 실행할 정방향 경로 4 → 8 → 7이 됩니다. 두 배 한 번, 1 빼기 한 번으로 총 2번입니다.

추가 입력 3 10: 홀수와 짝수가 번갈아 나오는 경우

역방향: 10 → 5 → 6 → 3
연산:       /2   +1   /2
누적 횟수:   1    2    3
 
정방향:  3 → 6 → 5 → 10
연산:       ×2   -1   ×2

출력은 3입니다. 여기서 초기 a - b = -7은 연산 횟수의 상한이 될 수 없습니다. 차이는 반복이 끝난 뒤 b <= a인 상태에서만 남은 횟수로 사용합니다.

추가 입력 5 8: 시작값 아래로 내려간 경우

역방향 첫 연산: 8 → 4       steps = 1
이제 4 <= 5이므로 반복 종료
남은 증가: 4 → 5           a - b = 1
전체 횟수: 1 + 1 = 2
 
정방향으로 복원: 5 → 4 → 8

처음부터 a >= b라면 반복문 없이 a - b를 출력합니다. 예를 들어 10 3은 7번, 5 5는 0번입니다.

자료형과 반복 횟수 확인

공식 코드는 입력과 횟수를 모두 long long으로 선언합니다. 이 범위는 32비트 int로도 계산 가능하지만 정답 코드는 제공된 자료형을 그대로 유지합니다.

입력 상한 10^9은 짝수입니다. 역방향에서 1을 더하는 값은 홀수일 때뿐이므로, 가장 큰 경우에도 999999999 + 1 = 1000000000입니다. 나눗셈은 값을 줄이므로 현재 값은 입력 상한을 넘지 않습니다.

홀수라면 +1 후 다음 단계에서 /2가 가능합니다. 즉 최대 두 연산마다 값을 절반 수준으로 줄이고, 이런 단계를 최대 30번 거치면 1에 도달합니다. 실제로는 b <= a가 되는 순간 더 일찍 멈춥니다. 반복 횟수에 대한 충분한 상한은 60이고, 마지막 차이는 양수인 b 때문에 a - b <= a - 1입니다.

따라서 전체 횟수는 60 + a - 1 <= 1000000059로 제한할 수 있습니다. 이는 정확한 최대값을 주장하는 식이 아니라 32비트 int에 안전하게 들어간다는 것을 보이는 여유 있는 상한입니다.

복잡도

  • 시간 복잡도: a < b이면 O(log b), a >= b이면 O(1)입니다. 남은 차이는 반복하지 않고 한 번에 더합니다.
  • 공간 복잡도: O(1)
오늘도 한 걸음 앞으로.

내용을 이해했다면 학습 완료를 체크해 주세요.

학습 기록은 현재 브라우저에 저장됩니다.
HJS / STUDY한지석