Post

[BOJ][KOI] 조약돌 - 25378 (G1)

[BOJ][KOI] 조약돌 - 25378 (G1)
시간 제한메모리 제한
0.5 초1024 MB

문제

좌우 한 줄로 있는 N개의 장소 각각에 조약돌이 몇 개씩 놓여 있다.

철수가 할 수 있는 작업의 종류는 아래 두 가지이다.

  1. 인접한 두 장소에서 임의의 동일한 개수의 조약돌을 가져가기
  2. 한 장소에서 임의의 개수의 조약돌을 가져가기

어떤 장소에 조약돌이 더 이상 없는 경우에도 그 장소는 그대로 남아 있어서, 초기에 인접하지 않았던 두 장소가 인접한 것으로 바뀌지 않는다.

철수는 위의 두 작업 중 하나를 골라서 실행하는 것을 반복하여 모든 조약돌을 가져가려고 한다.

초기에 각 장소에 있는 조약돌들의 개수를 입력받아, 철수가 할 수 있는 최소의 작업 횟수를 계산하는 프로그램을 작성하라.

입력

첫 번째 줄에 장소의 개수 N이 주어진다.

두 번째 줄에 N개의 장소 각각에 있는 조약돌 개수가 왼쪽 장소에 해당하는 것부터 순서대로 공백 하나씩을 사이로 두고 주어진다.

출력

첫 번째 줄에 답을 출력한다.

제한

  • 2 ≤ N ≤ 2 500
  • 각 장소의 초기 조약돌 개수는 $1$ 이상 $10^8$ 이하이다.

서브태스크

번호배점제한
16N = 3.
211N ≤ 15.
319N ≤ 300.
427각 장소의 초기 조약돌 개수가 2 500 이하이다.
537추가 제약 조건 없음.

풀이

dynamic programing으로 접근해 보면 조약돌의 오른쪽에서 작업을 시작하여 각 자리마다 그 때의 최소 작업 횟수를 저장하여 memoization할 수 있다.

하지만 단순히 이전까지의 최소 작업 횟수에 현재 자리의 최소 작업을 더한다고 최종적인 최소 작업 횟수가 될 수는 없다. 아래와 같은 예를 보자.

1 3 2

조약돌의 개수가 위와 같이 주어졌을 경우 두 번째 자리까지 최소 횟수는 2이다. 이를 저장하고 다음으로 넘어가서 마지막 자리인 두 개의 조약돌을 빼내는 횟수 1을 단순히 더하면 총 횟수는 3회이다.

하지만 처음부터 1번 작업으로 모두 진행하게 되면 두 번 만에 끝낼 수 있다.

그러므로 단순히 이전까지 최소 횟수에 현재의 최소 횟수를 더하면 안된다.

1번 작업과 2번 작업 간의 작업 횟수 차이를 조금 더 자세히 알아보자.

1번 작업

  • r 번째에서 l 번째까지 1번 작업 만으로 모든 자리의 돌의 개수를 0으로 만들 수 있다고 했을 때 작업 횟수는 (l - r) 번이다. (r, r + 1), (r + 1, r + 2), … , (l - 2, l - 1), (l - 1, l) 이와 같은 쌍으로 작업이 진행되므로 (l -r)번 만에 작업을 끝낼 수 있다.

2번 작업

  • 이 경우는 해당 범위의 모든 자리의 조약돌을 한 번씩 가져가는 것이므로 범위 내의 자리 개수인 (l - r + 1)과 같은 횟수만큼 작업을 해야 한다.

자리의 범위에 따라 1번 작업으로만 조약돌을 모두 비울 수 있는지 여부가 완전히 달라지기 때문에 이 정보를 사용할 수 있도록 미리 모든 범위에 대해 1번 작업으로만 가능한지 체크해둔다.

위에서 볼 수 있듯이 1번 작업을 최적으로 이용하는 방법을 찾는 것이 이 문제를 푸는데 중요하다는 것을 알 수 있다.

체크하기 가장 쉬운 방법은 매번 인접한 두 조약돌에 대해 1번 작업을 하면서 매번 해당 범위의 조약돌 수의 합을 구해 합이 0이 되었는지 확인해보는 것이지만, 이 경우 $O(n^3)$의 시간복잡도를 가지기 때문에, 이 문제의 모든 서브태스크를 만족하기에 부적합하다.

그래서 아래와 같은 방법을 사용한다.

1
2
3
4
5
6
7
8
9
10
11
check = [[0] * n for _ in range(n)]

for l in range(n - 1):
    tmp = nums[l]

    for r in range(l + 1, n):
        if tmp == nums[r]:
            check[l][r] = 1
        elif tmp > nums[r]:
            break
        tmp = abs(tmp - nums[r])

임시 변수(tmp)에 1번 작업을 하고 남은 이전 조약돌의 개수를 저장한다. 임시 변수와 현재 확인하고 있는 자리의 조약돌 개수(nums[r])가 같으면 1번 작업만으로 해당 범위의 자리를 모두 비울 수 있다는 것이므로 체크해준다.

같지 않을 경우 그대로 놔두면 되지만 만약 임시 변수가 현재 조약돌 개수보다 크다면 어떻게 해도 1번 작업만 해서 모두 다 비울 수 없기 때문에 바로 반복문을 종료한다.

위 경우에도 해당되지 않았으면 아직 1번 작업만으로 조약돌을 다 비울 수 있는 가능성이 남아있기 때문에 임시 변수에 임시 변수와 현재 조약돌 개수의 차이로 임시 변수의 값을 바꿔준다. 계속해서 두 값의 차로 값을 바꿔줘야 1번 작업을 하고 남은 이전 조약돌의 개수가 된다.

이제 위에서 체크한 것을 이용하여 최소 시행을 찾아야 한다. 최소 횟수의 후보가 될 수 있는 값은 크게 두 가지가 있다.

$memo[i]$는 $i$까지 작업을 수행했을 때의 최소 횟수, $2\leq i\leq N$ 범위에서 반복하며 확인할 것이므로 $memo[1]=1$로 초기화한다고 하자. 이때 최소 횟수의 후보는 다음과 같다.

  1. $memo[i-1] + 1$

    : 이전까지 최소 횟수에 이번 자리의 조약돌을 2번 작업으로 빼내는 횟수(1)를 더한 것

  2. $\min_{1\leq j < i, check[j][i]=1}(memo[j - 1]+i-j)$ $(2\leq i\leq N)$

    : 범위가 바뀌었으니 새롭게 1번 작업만으로 비우기가 가능한 범위(체크된 범위)가 있는지 찾아보는 과정이다. $1\leq j < i$를 만족하는 $j$부터 시작해서 새로운 범위의 끝($i$)까지 확인하면서 체크한 범위가 있는지 확인하고 체크된 것이 확인되면 ($check[j][i]=1$) 횟수를 계산한다.

    새롭게 찾은 범위가 시작하기 전까지의 최소 횟수($memo[j - 1]$)와 새로운 범위에서 1번 작업을 한 횟수($i-j$)를 더해주면 전체 횟수를 계산할 수 있다.

위 과정으로 구한 두 값 중 작은 것을 해당 위치에서의 최소 횟수로 취하면 된다. 전체 점화식은 다음과 같이 표현할 수 있다.

\[memo[i]=\min\{memo[i-1]+1, \min_{1\leq j < i, check[j][i]=1}(memo[j - 1]+i-j)\}\]
1
2
3
4
5
6
7
8
9
10
11
12
13
memo = [0] * (n + 1)
memo[1] = 1

for i in range(2, n + 1):
    new_min = float('inf')
    for j in range(1, i):
        if check[j - 1][i - 1] == 1:
            new = memo[j - 1] + i - j
            if new_min > new:
                new_min = new
            
    memo[i] = min(memo[i - 1] + 1, new_min)
print(memo[-1])

전체 코드

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
n = int(input())
nums = list(map(int, input().split()))

check = [[0] * n for _ in range(n)]
memo = [0] * (n + 1)
memo[1] = 1

for l in range(n - 1):
    tmp = nums[l]

    for r in range(l + 1, n):
        if tmp == nums[r]:
            check[l][r] = 1
        elif tmp > nums[r]:
            break
        tmp = abs(tmp - nums[r])

for i in range(2, n + 1):
    new_min = float('inf')
    for j in range(1, i):
        if check[j - 1][i - 1] == 1:
            new = memo[j - 1] + i - j
            if new_min > new:
                new_min = new
            
    memo[i] = min(memo[i - 1] + 1, new_min)
print(memo[-1])
This post is licensed under CC BY 4.0 by the author.