Featured image of post [Algorithm] C++/Python 백준 1126번 : 같은 탑

[Algorithm] C++/Python 백준 1126번 : 같은 탑

본 글은 백준 문제 1126번 “같은 탑"에 대하여 상세하게 설명하고, C++와 Python을 이용한 최적화된 풀이 코드를 제공하고자 작성한 글이다. 문제의 조건과 제약을 분석하여 동적 계획법(Dynamic Programming)을 활용한 해결 방법을 단계별로 소개하며, 문제 풀이 과정에서의 핵심 아이디어와 최적화 기법을 함께 다루고 있다.

문제 : https://www.acmicpc.net/problem/1126

문제 설명

1126번 “같은 탑” 문제는 N개의 직사각형 블록을 이용하여 두 개의 탑을 쌓되, 두 탑의 높이가 동일하도록 만드는 문제이다. 각 탑은 반드시 한 개 이상의 블록을 포함하여야 하며, 모든 블록을 반드시 사용할 필요는 없다. 각 블록은 주어진 높이를 가지며, 문제에서는 블록의 높이가 500,000보다 작거나 같은 자연수임이 보장된다. 또한, 모든 블록의 높이 합은 500,000을 넘지 않는다. 문제의 목표는 두 탑의 높이를 같게 만드는 경우들 중에서 탑의 높이가 최대가 되는 경우를 찾아내어 그 높이를 출력하는 것이다. 만약 두 탑의 높이를 같게 만들 수 없는 경우에는 -1을 출력하도록 요구된다. 이 문제는 블록을 어느 탑에 배치할지에 따라 경우의 수가 급격히 증가하게 되어 단순한 완전 탐색(Brute Force) 방식으로는 해결이 어려우며, 동적 계획법(Dynamic Programming)을 통해 상태를 효율적으로 관리할 필요가 있다. 구체적으로, 두 탑의 높이 차이를 상태로 정의하고, 각 단계에서 블록을 사용하지 않는 경우, 높은 탑에 추가하는 경우, 낮은 탑에 추가하는 경우로 나누어 상태 전이를 진행함으로써 문제를 해결할 수 있다. 이러한 접근 방식은 배낭 문제(Knapsack Problem)와 유사한 기법을 활용하므로, 블록의 배치에 따른 높이 차이와 누적된 탑의 높이를 효과적으로 관리할 수 있게 된다.

접근 방식

문제 해결을 위해 우선 각 블록을 사용할지 말지, 그리고 어느 탑에 쌓을지를 결정하는 3가지 경우로 문제를 분할할 수 있다. 구체적으로 다음과 같이 접근하였다.

  1. 상태 정의: 두 탑의 높이 차이를 key로 하여, 해당 차이를 이루었을 때 낮은 탑에 쌓인 블록들의 총 높이의 최대값을 저장하는 dp 배열을 정의한다. 즉, dp[d]는 지금까지 선택된 블록들로 두 탑의 높이 차이가 d일 때 낮은 탑의 총 높이의 최댓값을 의미한다.
  2. 초기 상태 설정: 두 탑 모두 블록을 사용하지 않은 상태는 높이 차이가 0이며, 이때 낮은 탑의 높이는 0이므로 dp[0] = 0으로 초기화한다. 나머지 상태는 도달할 수 없음을 나타내기 위해 -1로 초기화한다.
  3. 상태 전이: 각 블록 h에 대하여 다음의 세 가지 경우에 대해 상태를 갱신한다.
    • 블록을 사용하지 않는 경우: dp[d]의 값은 그대로 유지된다.
    • 블록을 높은 탑에 쌓는 경우: 높이 차이가 d에서 d+h로 증가하며, 낮은 탑의 높이는 변하지 않는다.
    • 블록을 낮은 탑에 쌓는 경우: 높이 차이가 |d-h|로 변화하며, 낮은 탑에는 min(d, h)만큼 블록 높이가 추가된다.
  4. 최종 결과 도출: 모든 블록에 대해 dp 배열을 갱신한 후, dp[0]가 양수이면 두 탑의 높이가 같으면서 최대 높이를 구한 것이므로 그 값을 출력하고, 그렇지 않으면 -1을 출력한다.

이와 같은 방식으로 문제의 상태를 정의하고 전이함으로써 시간 복잡도는 O(N×total)로 관리할 수 있으며, N과 total(블록 높이 합)의 제약 내에서 충분히 빠른 풀이가 가능하다.

C++ 코드와 설명

다음은 최적화된 C++ 코드이며, 각 줄마다 주석을 통해 코드의 동작을 상세하게 설명하였다.

 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
51
#include <iostream>    // 입출력 관련 라이브러리이다.
#include <vector>      // 동적 배열 사용을 위한 라이브러리이다.
#include <algorithm>   // max 함수 등을 사용하기 위한 라이브러리이다.
#include <cmath>       // abs 함수를 사용하기 위한 라이브러리이다.
using namespace std;

int main(){
    ios::sync_with_stdio(false);  // 입출력 속도 향상을 위한 설정이다.
    cin.tie(nullptr);             // cin과 cout의 동기화를 해제한다.
    
    int n;
    cin >> n;                     // 블록의 개수를 입력받는다.
    vector<int> blocks(n);
    int total = 0;                // 모든 블록의 높이 합을 저장할 변수이다.
    for (int i = 0; i < n; i++){
        cin >> blocks[i];         // 각 블록의 높이를 입력받는다.
        total += blocks[i];       // 블록의 높이를 total에 누적한다.
    }
    
    // dp[d]는 지금까지의 블록 배치로 두 탑의 높이 차이가 d일 때, 
    // 낮은 탑에 쌓인 블록들의 총 높이의 최댓값을 의미한다.
    // 도달할 수 없는 상태는 -1로 초기화한다.
    vector<int> dp(total + 1, -1);
    dp[0] = 0;                    // 초기 상태: 두 탑 모두 블록이 없는 상태이다.
    
    // 각 블록을 순회하며 dp 배열을 갱신한다.
    for (int h : blocks) {
        vector<int> ndp = dp;     // 현재 상태를 복사하여 새로운 상태 배열을 생성한다.
        for (int d = 0; d <= total; d++){
            if(dp[d] < 0) continue;  // 도달할 수 없는 상태는 건너뛴다.
            
            // 1. 블록을 사용하지 않는 경우: 상태 변화가 없으므로 그대로 유지된다.
            
            // 2. 블록을 높은 탑에 추가하는 경우:
            //    현재 높이 차 d에 h를 더하여 d+h가 되며, 낮은 탑의 높이는 그대로 유지된다.
            if(d + h <= total)
                ndp[d + h] = max(ndp[d + h], dp[d]);
            
            // 3. 블록을 낮은 탑에 추가하는 경우:
            //    두 탑의 높이 차는 abs(d-h)가 되며, 낮은 탑에는 min(d, h)만큼 높이가 추가된다.
            int nd = abs(d - h);
            ndp[nd] = max(ndp[nd], dp[d] + min(d, h));
        }
        dp = ndp;                 // 새로운 상태를 dp 배열에 반영한다.
    }
    
    // dp[0]이 양수이면 두 탑의 높이를 같게 만들 수 있는 경우이므로 결과를 출력하고,
    // 그렇지 않으면 -1을 출력한다.
    cout << (dp[0] > 0 ? dp[0] : -1) << "\n";
    return 0;                   // 프로그램을 정상 종료한다.
}

위 코드는 각 블록을 순차적으로 고려하며, 상태 전이를 통해 두 탑의 높이 차이와 낮은 탑의 높이의 최댓값을 갱신하는 방식이다. 이를 통해 모든 가능한 경우를 효율적으로 탐색할 수 있다.

Python 코드와 설명

다음은 위 C++ 풀이와 동일한 아이디어를 적용한 Python 코드이다. 코드의 각 부분마다 주석을 추가하여 동작 과정을 상세하게 설명하였다.

 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
import sys
input = sys.stdin.readline  # 빠른 입력을 위한 설정이다.

n = int(input())            # 블록의 개수를 입력받는다.
blocks = list(map(int, input().split()))  # 각 블록의 높이를 입력받아 리스트에 저장한다.
total = sum(blocks)         # 모든 블록의 높이 합을 계산한다.

# dp는 현재까지의 상태를 딕셔너리로 관리한다.
# 키(key)는 두 탑의 높이 차이를 나타내며, 값(value)은 낮은 탑에 쌓인 블록들의 총 높이의 최댓값이다.
dp = {0: 0}  # 초기 상태: 높이 차이가 0이며, 낮은 탑의 높이는 0이다.

# 각 블록을 순회하며 dp 상태를 갱신한다.
for h in blocks:
    new_dp = dp.copy()  # 기존 상태를 복사하여 새로운 상태 딕셔너리를 생성한다.
    for d, lower in dp.items():
        # 1. 블록을 사용하지 않는 경우: 상태 변화가 없으므로 그대로 유지된다.
        
        # 2. 블록을 높은 탑에 쌓는 경우:
        #    높이 차이가 d에서 d+h로 증가하며, 낮은 탑의 높이는 그대로 유지된다.
        new_d = d + h
        if new_d <= total:
            new_dp[new_d] = max(new_dp.get(new_d, -1), lower)
        
        # 3. 블록을 낮은 탑에 쌓는 경우:
        #    새로운 높이 차는 abs(d-h)가 되며, 낮은 탑에는 min(d, h)만큼 높이가 추가된다.
        new_d = abs(d - h)
        new_dp[new_d] = max(new_dp.get(new_d, -1), lower + min(d, h))
    dp = new_dp  # 갱신된 상태를 dp에 반영한다.

# 최종 상태에서 높이 차이가 0인 경우의 낮은 탑의 높이가 양수이면 결과를 출력하고, 그렇지 않으면 -1을 출력한다.
print(dp.get(0, -1) if dp.get(0, 0) > 0 else -1)

위 Python 코드는 딕셔너리를 사용하여 상태를 관리함으로써, 불필요한 배열의 크기 제약 없이 동적 계획법을 구현하였다. 각 단계에서 가능한 모든 높이 차이에 대해 낮은 탑의 높이 최댓값을 갱신하는 과정을 통해 문제의 조건을 만족하는 해답을 도출한다.

결론

본 문제는 두 탑의 높이 차이를 상태로 정의하여 동적 계획법으로 효율적으로 해결할 수 있는 문제이다. 단순한 완전 탐색 방식으로는 경우의 수가 기하급수적으로 증가하여 해결이 어려우므로, 상태 압축(State Compression)과 메모이제이션(Memoization) 기법을 활용하여 문제를 해결하였다. C++와 Python 두 가지 언어로 구현한 풀이 코드는 각각의 언어에서 제공하는 기본 자료 구조와 최적화 기법을 효과적으로 적용한 사례이다. 본 풀이를 통해 동적 계획법의 응용과 문제 추상화의 중요성을 다시 한 번 느낄 수 있었으며, 추가적인 최적화를 위해 메모리 사용량 감소나 불필요한 상태 제거 기법 등을 고려해볼 수 있음을 확인할 수 있었다.