문제


https://www.acmicpc.net/problem/17592


풀이


내 제출

풀다가 정말 울뻔했다.

간선을 아무거나 고르고 나눠진 두 구역에서 부분 문제를 풀면 될 거라 생각했다.

하지만 이 생각만으로는 구현을 할 수 없었다.

예로 들어 N이 20일 때, 1-15 엣지를 택한 후 5-10 엣지를 택하면 2~4번 노드는 11~14번 노드와 연결 가능한데 이를 모두 고려하려면 한도 끝도 없기 때문이다.

재귀 함수의 파라미터로는 부분 문제의 상황을 명시하면서도 메모리를 적당히 잡아먹는 것이어야 한다.

엣지의 연결 상태는 길이 N짜리 배열로 나타낼 수 있지만 메모이제이션을 할 수가 없었고, 비트마스크는 당연히 N이 500이하라서 불가능했다.

조금 더 생각을 해보니, 0번 노드부터 차례로 엣지를 정하고 나중엔 거들떠보지도 않으면 되겠단 생각이 들었다.

그래서 다음과 같은 함수 f를 정의했다.

f(x, y) : x~y번 노드끼리만을 겹치지 않게 연결할 수 있는 최대 엣지 수

def f(x, y):
    if dp[x][y] != -1:
        return dp[x][y]
    
    res = 0
    for i in range(x, y):
        for j in range(i + 1, y + 1):
            if board[i][j]:
                res = max(res, f(i + 1, j - 1) + f(j + 1, y) + 1)
    
    dp[x][y] = res
    return res

이 함수를 이용한 맨 처음 풀이는 TLE가 떴다.

문제의 시간제한이 12초 (추가시간 없음)이었기에, 파이썬에 대한 불신이 살아나며 C++로 그대로 짜봤다.

그랬더니 틀렸습니다가 떴고, 잘못된 부분을 고치니 다시 TLE가 떴다.

Top-Down 방식에서 Bottom-Up 방식으로 고쳐도 봤지만 여전히 TLE여서, 반쯤 울면서 깊은 생각에 잠들었다.

얼마 후 다음과 같은 사실을 알 수 있었다.

f(x, y)에서 i-j 간선이 연결됐다면, i가 j + 1 이상일 땐 무시해도 된다.

i-j 간선이 연결되면 res 값은 f(i + 1, j - 1) + f(j + 1, y) + 1 이상인데, f(j + 1, y) 항이 j + 1-* 간선이 연결됐을 때의 값 이상이기 때문이다.

조금 다르게 얘기해보면, j + 1-* 간선을 연결하는 경우는 x~j번 노드를 무시하는 상황인데 i-j 간선을 고르지 않을 이유가 없다.

def f(x, y):
    if x > y:
        return 0
    
    if dp[x][y] != -1:
        return dp[x][y]
    
    res = 0
    flag = 1000
    for i in range(x, y):
        if i > flag:
            break
        for j in range(i + 1, y + 1):
            if board[i][j]:
                flag = min(flag, j)
                res = max(res, f(i + 1, j - 1) + f(j + 1, y) + 1)
    
    dp[x][y] = res
    return res

그래서 이어진 간선들 중 최소의 j값을 의미하는 flag 변수를 만들어, i가 flag를 넘어가면 break하도록 했다.

결국 AC를 받아냈다.


코드


import sys

n = int(sys.stdin.readline())
board = [list(map(int, sys.stdin.readline().split())) for _ in range(n)]
dp = [[-1] * n for _ in range(n)]

for k in range(1, n):
    for x in range(n - k):
        y = x + k
        flag = 1000
        res = 0
        for i in range(x, y):
            if i > flag:
                break
            for j in range(i + 1, y + 1):
                if board[i][j]:
                    flag = min(flag, j)
                    sub_res = 1
                    if i + 1 < j - 1:
                        sub_res += dp[i + 1][j - 1]
                    if j + 1 < y:
                        sub_res += dp[j + 1][y]
                    res = max(res, sub_res)
        
        dp[x][y] = res

print(dp[0][-1])

TLE가 나올까봐 Bottom-Up 방식의 코드를 제출했는데, Top-Down 방식도 통과한다.


#include <iostream>
#include <algorithm>

using namespace std;

int n, temp;
int board[500][500], dp[500][500];

int f(int x, int y) {
    if (x > y) {
        return 0;
    }
    
    if (dp[x][y] != -1) {
        return dp[x][y];
    }

    int res = 0;
    int flag = 1000;
    for (int i = x; i < y; i++) {
        if (i > flag) {
            break;
        }
        for (int j = i + 1; j < y + 1; j++) {
            if (board[i][j]) {
                flag = min(flag, j);
                res = max(res, f(i + 1, j - 1) + f(j + 1, y) + 1);
            }
        }
    }

    dp[x][y] = res;
    return res;
}

int main() {
    ios::sync_with_stdio(0);
    cin.tie(NULL); cout.tie(NULL);

    cin >> n;

    for (int x = 0; x < n; x++) {
        for (int y = 0; y < n; y++) {
            cin >> temp;
            board[x][y] = temp;
            dp[x][y] = -1;
        }
    }

    cout << f(0, n - 1);

    return 0;
}

C++로 제출한 코드이다.


시간 비교


언어 방식 시간 메모리
PyPy3 재귀 2160ms 136048KB
PyPy3 for문 8684ms 130380KB
C++ 재귀 468ms 3974KB
C++ for문 2060ms 3972KB
Python3 재귀 TLE -

PyPy3와 메모리를 비교해보려 Python3도 제출해봤지만 역시나 TLE가 뜬다.

PyPy3와 C++ 둘 다 Bottom-Up 방식보다 Top-Down 방식이 훨씬 빠른 것을 알 수 있는데, 이는 전자가 발생하지 않을 부분 문제에 대해서도 계산을 해서 그런 것으로 추측된다.

완전 그래프일 때는 시간이 비슷할 것 같은데 더 이상의 깊은 생각은 생략한다.

아무튼 DP는 너무 어렵다.

Leave a comment