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

Exercise 03 · 최대공약수와 최대 부분합

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

Exercise 02 · Exercise 03 · Exercise 04 · Exercise 05

출처: CPP Programming Exercise 03.pdf. 정답은 별도 Solution PDF가 제공되지 않아 기존 Exercise 풀이를 유지했습니다. 정답 코드는 접어서 확인할 수 있습니다.

핵심 개념

1. 자료형 변환: 나누기 전에 바꾸기

int total = 7;
int count = 2;
double average = static_cast<double>(total) / count;  // 3.5

나눗셈 전에 실수형으로 변환하면 실수 나눗셈을 할 수 있습니다.

개념동작주의점
실수 → 정수표현 가능한 범위에서 소수 부분을 0 방향으로 버림반올림이 아님
중괄호 초기화축소 변환을 제한값의 손실을 막는 데 도움
부동소수점유한한 비트로 실수를 근사0.1도 정확히 저장하지 못할 수 있음

2. 조건에 따라 실행 경로 고르기

문법실행 방식
if / else if / else순서대로 검사해 처음 참인 분기 하나 실행
switch정수·열거형 값에 맞는 case로 이동

조건을 조합하는 논리 연산자

  • AND — &&: 조건이 모두 참
  • OR — ||: 조건 중 하나 이상 참
  • NOT — !: 참·거짓 반전

3. 반복문 세 가지 비교

반복문조건 확인 시점최소 실행 횟수
while본문 실행 전0번
do while본문 실행 후1번
for초기화 후, 매 본문 실행 전0번
for: 초기화 → 조건 확인 → 본문 → 갱신 → 조건 확인 → …
반복 제어하는 일
break반복을 끝냄
continue현재 회차의 나머지를 건너뜀

4. 전위 증가와 후위 증가

표현식x의 변화표현식의 결과
++x1 증가증가한 값
x++1 증가증가하기 전 값

5. 이번 문제에서 유지할 상태

반복문을 쓰기 전에 다음 세 가지를 먼저 정하세요.

  1. 저장할 값: 현재 변수에는 무엇이 들어 있는가?
  2. 갱신 규칙: 한 회차에서 무엇이 바뀌는가?
  3. 종료 조건: 언제 반복을 끝내는가?
문제반복 중 기억할 값
최대공약수두 수와 나머지
최대 부분합현재 위치에서 끝나는 최대 합, 전체 최대 합

Exercise 03

Quiz 1 - 최대공약수

난이도: 중

Problem (Original)

CPP Programming Exercise 03.pdf, slide 19

Quiz 1: Greatest Common Divisor

Problem. Find the greatest common divisor of two positive integers: the largest positive integer that divides both without a remainder.

Input. One line containing two integers a b.

Output. Print their greatest common divisor followed by a newline.

Constraints. 1 ≤ a, b ≤ 10⁹

SampleInputOutput
148 186
217 131
3987654312 76543210836
문제 한국어 번역 보기

문제. 두 양의 정수의 최대공약수, 즉 두 수를 모두 나누어떨어지게 하는 가장 큰 양의 정수를 구합니다.

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

출력. 최대공약수를 출력하고 줄을 바꿉니다.

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

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

개념과 예제

접근 방법

  • 모든 수를 1부터 차례로 검사하면 최대 10억 번의 검사가 필요하므로 너무 느립니다.
  • 유클리드 호제법의 성질 gcd(a, b) = gcd(b, a % b)를 사용합니다.
  • b가 0이 될 때 남아 있는 a가 최대공약수입니다.
  • 반복할 때마다 두 번째 값이 나머지로 작아지므로 매우 빠르게 종료됩니다.
정답 코드 보기정답 코드 숨기기
exercise03_quiz1.cpp
#include <iostream>
using namespace std;
 
int main() {
    // a와 b는 최대 1,000,000,000이고 나머지는 항상 b보다 작다.
    // 따라서 모든 계산이 int 범위 안에서 이루어진다.
    int a;
    int b;
    cin >> a >> b;
 
    // b가 0이 될 때까지 유클리드 호제법을 반복한다.
    while (b != 0) {
        // 다음 단계에서 사용할 나머지를 먼저 보관한다.
        const int remainder = a % b;
 
        // gcd(a, b)는 gcd(b, a % b)와 같다.
        a = b;
        b = remainder;
    }
 
    // 반복이 끝나면 a에 최대공약수가 남는다.
    cout << a << '\n';
 
    return 0;
}

코드 해설: 나머지를 보관한 뒤 두 값을 이동

a, b는 매 반복에서 최대공약수를 구할 두 수입니다. while (b != 0)은 두 번째 수가 0이 아닐 때만 나머지를 계산합니다.

remainder = a % b    이전 a와 b로 나머지 계산
a = b                두 번째 수를 첫 번째 자리로 이동
b = remainder        나머지를 두 번째 자리로 이동

이 순서가 중요합니다. 먼저 a = b를 실행하고 b = a % b라고 쓰면 원래 a가 사라져 b % b = 0을 계산하게 됩니다. 그래서 임시 변수 remainder가 필요합니다.

수학 공식: 왜 gcd(a, b) = gcd(b, a % b)인가요?

gcd는 두 수의 최대공약수입니다. 나눗셈의 몫을 q, 나머지를 r이라고 하면 a = q × b + r이고, 이를 바꾸면 r = a - q × b입니다.

  1. 어떤 수 d가 a와 b를 모두 나누면, q × b도 나눕니다. 따라서 둘의 차이인 r = a - q × b도 나눕니다.
  2. 반대로 d가 b와 r을 모두 나누면, 두 항을 합친 a = q × b + r도 나눕니다.
  3. 따라서 (a, b)의 공약수와 (b, r)의 공약수는 정확히 같습니다. 가장 큰 공약수도 같습니다.

문제의 정답은 보존하면서 두 번째 수를 더 작은 나머지로 바꾸는 것입니다. 마지막에는 gcd(a, 0)이 되는데, 양의 정수 a와 0의 최대공약수는 a입니다. 0은 a × 0으로 표현되므로 a로 나누어집니다.

실제 입력 48 18 추적

반복계산 전 a계산 전 bremainder = a % b갱신 후 (a, b)
1481812(18, 12)
218126(12, 6)
31260(6, 0)

다음 조건 검사에서 b != 0이 거짓이므로 종료하고 a, 즉 6을 출력합니다. 48 = 6 × 8, 18 = 6 × 3이므로 실제로 둘 다 나누어집니다.

입력이 반대로 18 48이어도 처음에 18 % 48 = 18이 되어 (48, 18)로 바뀝니다. 미리 두 수를 정렬할 필요가 없습니다. 7 7처럼 같으면 한 번 만에 (7, 0)이 됩니다.

나머지는 항상 나누는 수보다 작습니다. 또한 양수인 두 번째 값은 늦어도 두 번의 나머지 연산마다 절반 미만으로 줄어들므로, 약수를 하나씩 검사하는 것보다 훨씬 적은 반복으로 끝납니다.

복잡도

  • 시간 복잡도: O(log(min(a, b)))
  • 공간 복잡도: O(1)

Quiz 2 - 연속 부분 배열의 최대 합

난이도: 중상

Problem (Original)

CPP Programming Exercise 03.pdf, slide 20

Quiz 2: Maximum Subarray Sum

Problem. Read a sequence of integers. Find the largest sum of a contiguous segment containing at least one element.

Input. The first line contains an integer n. The second line contains n integers in order.

Output. Print the maximum sum followed by a newline.

Constraints. 1 <= n <= 100. Each input value is from -100 to 100.

SampleInput line 1 (n)Input line 2Output
15-2 3 -1 4 -56
24-8 -3 -6 -2-2
3100
문제 한국어 번역 보기

문제. 정수 수열을 입력받아, 원소를 하나 이상 포함하는 연속 구간의 합 중 가장 큰 값을 구합니다.

입력. 첫 줄에 정수 n, 둘째 줄에 정수 n개가 순서대로 주어집니다.

출력. 최대 합을 출력하고 줄을 바꿉니다.

제약 조건. 1 <= n <= 100, 각 값은 -100 이상 100 이하입니다.

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

개념과 예제

접근 방법

  • 연속 구간의 시작과 끝을 모두 시도하면 O(n²) 방식으로도 n=100에서는 통과할 수 있습니다.
  • 하지만 각 위치에서 끝나는 최적의 합만 유지하는 카데인 알고리즘을 사용하면 O(n)에 해결할 수 있습니다.
  • currentSum은 현재 원소에서 새 구간을 시작하는 값과 이전 구간에 현재 원소를 붙인 값 중 큰 값입니다.
  • 모든 값이 음수일 수 있으므로 합을 0으로 초기화하면 안 됩니다. 첫 번째 입력값으로 currentSum과 maximumSum을 초기화해야 합니다.
currentSum = max(현재 값, currentSum + 현재 값)
maximumSum = max(maximumSum, currentSum)
정답 코드 보기정답 코드 숨기기
exercise03_quiz2.cpp
#include <algorithm>
#include <iostream>
using namespace std;
 
int main() {
    // n은 최대 100이므로 반복 횟수와 배열 위치는 int가 적합하다.
    int n;
    cin >> n;
 
    // 각 값은 -100~100이고 가능한 합도 -10,000~10,000이므로
    // value와 누적 합 모두 int로 안전하게 표현할 수 있다.
    int value;
    cin >> value;
 
    // 첫 값으로 초기화해야 모든 입력이 음수인 경우도 올바르게 처리된다.
    int currentSum = value;
    int maximumSum = value;
 
    for (int i = 1; i < n; i++) {
        cin >> value;
 
        // 이전 구간을 이어 갈지, 현재 값부터 새로 시작할지 선택한다.
        currentSum = max(value, currentSum + value);
 
        // 지금까지 발견한 가장 큰 연속 구간 합을 갱신한다.
        maximumSum = max(maximumSum, currentSum);
    }
 
    cout << maximumSum << '\n';
    return 0;
}

코드 해설: 두 합의 의미를 구분하기

변수정확한 의미
value방금 읽은 현재 숫자
currentSum현재 숫자에서 반드시 끝나는 연속 구간 중 최대 합
maximumSum지금까지 읽은 범위 전체에서 발견한 최대 합

currentSum은 현재 위치에 따라 작아질 수 있지만, maximumSum은 이미 찾은 좋은 답을 보관하므로 작아지지 않습니다.

  1. 첫 번째 숫자를 읽고 두 합을 모두 그 값으로 초기화합니다. 길이 1인 구간도 정답 후보입니다.
  2. 두 번째 숫자부터 읽으면서 이전 구간을 이어 갈지, 현재 숫자부터 새로 시작할지 결정합니다.
  3. 결정한 currentSum과 기존 maximumSum을 비교해 전체 정답을 갱신합니다.
  4. 숫자를 모두 읽은 뒤 maximumSum을 출력합니다.

점화식: 왜 두 경우만 비교하나요?

현재 숫자를 x라고 해 봅시다. 현재 위치에서 끝나는 비어 있지 않은 연속 구간은 두 종류뿐입니다.

  • 새로 시작: 현재 숫자 하나만 선택합니다. 합은 x입니다.
  • 이전 구간 연결: 바로 이전 위치에서 끝나는 구간 뒤에 x를 붙입니다. 그중 가장 좋은 합은 이전 currentSum + x입니다.
새 currentSum = max(x, 이전 currentSum + x)
새 maximumSum = max(이전 maximumSum, 새 currentSum)

이전 위치에서 끝나는 구간들은 모두 같은 x를 더하므로, 그중 최대 합 하나만 기억하면 충분합니다. 이전 합이 음수라면 붙일수록 손해이므로 버리고, 양수라면 연결하는 편이 유리합니다.

실제 입력 5 / -2 3 -1 4 -5 추적

첫 입력 5는 원소 개수입니다. 첫 원소 -2를 읽은 뒤 currentSum = maximumSum = -2로 시작합니다.

위치value새로 시작이전 합에 연결갱신한 currentSummaximumSum현재 위치에서 끝나는 선택 구간
1-2-2초기화-2-2[-2]
233-2 + 3 = 133[3]
3-1-13 - 1 = 223[3, -1]
4442 + 4 = 666[3, -1, 4]
5-5-56 - 5 = 116[3, -1, 4, -5]

마지막 currentSum은 1이지만 정답은 6입니다. 가장 좋은 구간이 마지막 원소에서 끝날 필요는 없기 때문에 maximumSum을 따로 저장합니다. 표의 구간은 이해를 위한 표시이며 실제 코드는 구간 자체를 저장하지 않습니다.

모든 값이 음수인 경우

입력이 3과 -8 -3 -6이면 다음처럼 됩니다.

첫 값 -8: currentSum = -8, maximumSum = -8
다음 -3: max(-3, -8 + -3) = -3 → maximumSum = -3
다음 -6: max(-6, -3 + -6) = -6 → maximumSum = -3
정답: -3

처음부터 maximumSum = 0으로 두면 실제로 선택할 수 없는 빈 구간의 합 0을 답으로 내놓게 됩니다. 문제는 하나 이상을 선택하라고 했으므로 첫 원소로 초기화해야 합니다. n = 1이면 반복문 없이 그 첫 원소가 답입니다.

vector<int>로 입력을 저장해도 시간은 O(n)이지만 저장 공간이 O(n) 필요합니다. 이 문제는 과거 원소를 다시 읽을 필요가 없어 입력을 하나씩 처리하는 현재 코드가 O(1) 추가 공간으로 충분합니다.

복잡도

  • 시간 복잡도: O(n)
  • 공간 복잡도: O(1)

오늘도 한 걸음 앞으로.

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

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