백준

백준 1463번 문제풀이 [python]

ys.k 2023. 6. 1. 04:35

포스팅에 앞서 내용이 틀릴 수 있으며, 해당 부분 지적은 감사히 받습니다.

문제를 보자.

이땐 몰랐다.

힌트가 나에게 어떤 걸 이야기해 주려는지..

 

처음 나의 생각은 연산 횟수가 제일 적으려면, 수를 제일 작게 하는 연산을 해야 한다 생각했다.

그래서 초기 코드는

 

a = int(input())
count=0
while a>=1:
 if (a%3 == 0):
  count+=1
  a/=3
 elif (a%2 == 0):
  count +=1
  a/=2
 else:
  count+=1
  a-=1
print(count)

 

이렇게 작성하였고, 제출했다.

틀렸다.

 

나는 나의 논리에 오점이 있을 거라 생각하지 못했기에, 현실 부정하며 한번 더 제출했다.

 

결과는 냉정했다.

 

통학시간이 다가왔기에, 준비를 하며 복잡한 머릿속을 정리했다.

 

통학을 하면서도 계속 생각했다.

 

학교에 도착해, 첫 번째 수업이 끝나도 떠오르지 않았다.

 

그래서 검색했다.

 

DP 문제란다.

 

필자는 컴퓨터 알고리즘에 대해 단 1도 공부한 적이 없어, 이 접근 방식에 대해 더욱 긴 시간을 투자했어도 생각할 수 없었을 거다.

 

어쨌든 DP에 대해 알아보자.

 

Dynamic Programming ( 동적 프로그래밍 )

 복잡한 문제를 간단한 여러 개의 문제로 나누어 푸는 방법

 

DP에는 2가지 기법이 있다.

1. Bottom Up

2. Top Down

 

간단히 Bottom Up은 작은 단위에서 큰 단위로, Top Down은 큰 단위에서 작은 단위로 계산해 나가는 방식이다.

이 문제는 Botom Up이 적합하다.

 

또 DP를 사용하기 위해 아래 조건을 만족해야 한다.

1. Overlappint subproblems (겹치는 부분 문제)

2. Optimal substructure ( 최적 부분 구조 )

 

해석해 보면

1. 동일한 작은 문제들이 반복해서 계산될 때

2. 부분 문제의 최적 결괏값을 통해 전체 문제의 최적 결괏값을 낼 수 있을 때 

    다시, 부분 문제의 최적 결괏값이 전체 문제에서도 동일 적용되어 결과가 변하지 않을 때

 

두 가지 경우를 만족할 때 DP를 사용할 수 있다.

 

이 문제에서는 3과 2로 나누거나 1을 빼는 연산을 계속 반복하기에 1번 조건을 만족시킨다.

n의 입력값이 2와 3일 때 각 1, 1이라는 부분 문제에 대해 결괏값이 변하지 않으므로 2번 조건을 만족시킨다.

 

따라서 이 문제는 DP를 사용하여 해결할 수 있다.

 

이 문제는 결괏값이 변하지 않는 부분 문제가 입력값 2와 3의 케이스가 있기에 Bottom Up 형식으로 구현할 것이다.

 

코드

a = int(input())

array=[0] * (a+1)

 

for i in range(2, a+1):

    array [i] = array[i-1] +1

    if(( i % 3 ) == 0):

        array [i] = min(array [i], array[i//3] + 1)

    if(( i % 2 ) == 0):

        array[i] = min(array[i], array [i//2] + 1)

print(array [a])

 

형식으로 구현하였다.

 

Bottom Up 전개의 단점은 코드의 가독성이 낮다.

 

따라서 자세한 설명을 추가한다.

 

배열의 각 요소에 0을 담아 a +1의 크기로 만든다.

각 배열에는 0부터 a까지의 최소 연산 횟수가 들어갈 것이다.

 

 이 문제를 풀기 위해 기본 전제조건에 대한 이해가 필요하다.

 

전제 조건 

1. 모든 자연수는 2로 나뉘지 않을 때, -1을 하면 반드시 나뉜다.

 

너무나 당연한 이야기다.

하지만 이 당연한 사실로부터 아래 코드를 파생시키는 것은, 사전 지식 없이는 쉽지 않다.

array [i] = array [i-1] +1 

 

이 부분이 가장 이해하는데 어려웠다.

배열의 i번째 자리에는 i-1번째의 자리의 최소연산 횟수 보다 1회 많다.

이게 무슨 소리인가 싶다.

이해를 돕기 위한 먼저 설명하자면, 모든 경우의 수에 해당되는 게 아니라, 그러한 경우가 있다.

해당 케이스로 6의 최소연산 수는 6 > 2 > 1로 2회이다. 

그렇다면  i가 7일 때 7은 2로 나누어지지 않으니, 1을 빼는 연산이 필연적으로 수행되어야 한다.

따라서 array [i] = array [i-1] +1  코드는 2로 나누어지지 않는다고 가정하고 미리 넣어두는 수이다.

min() 함수를 모른다면, 이 부분이 가장 이해하기 어려울 것이다.

 

필자는 그랬다.

 

이제 밑 if문에서 3으로 나누어질 때, 2로 나뉠 때 min 함수를 사용해서 최소 연산 수를 배열에 삽입할 것이다.

 

if(( i % 3 ) == 0):

        array [i] = min(array [i], array [i//3] + 1)

 

간단하게 해석해 보자

array [i] = 최솟값(  2로 안 나뉠 때 최소 연산, 3으로 나뉠 때 최소 연산)

중 더 작은 값을 array [i]번째에 넣겠다는 이야기이다.

아래 코드 또한 숫자만 다르지 같은 이야기다.

 if(( i % 2 ) == 0):

        array [i] = min(array[i], array[i//2] + 1)

 

하지만 여기서 의문이 생길 수 있다.

왜 if, elif 가 아니라 if, if일까?

간단하다.

i가 6의 배수일 경우 3으로도, 2로도 나뉠 수 있지만 if, elif형식으로 작성하면 6의 배수에서 연산누락이 생기는 경우의 수가

존재하기 때문이다.

 

아래 코드를 통해 10^6의 숫자 중 연산누락으로 인해 제대로 연산되지 않는 경우를 출력시켰다.

 

직접 해보고 싶다면, 코드는 첨부한다.

추천은 하지 않는다.

출력값이 많아 렉이 걸리기 때문이다.

 

a = int(input())
array = [0] * (a+1)
array2 = [0] * (a+1)
diff_positions = []

for i in range(2, a+1):
    array [i] = array[i-1] + 1
    if i % 3 == 0:
        array [i] = min(array[i//3] + 1, array[i], array[i//2] + 1)
    if i % 2 == 0:
        array[i] = min(array [i//2] + 1, array [i])

for i in range(2, a+1):
    array2 [i] = array2[i-1] + 1
    if i % 3 == 0:
        array2 [i] = min(array2[i//3] + 1, array2[i])
    elif i % 2 == 0:
        array2[i] = min(array2 [i//2] + 1, array2 [i])

for i in range(0, a+1):
    if array [i]!= array2 [i]:
        diff_positions.append(i)

print("Different positions:", diff_positions)

 

그냥 위 코드의 결과를 통해 elif문을 사용했을 때의 오차의 경우가 존재한다는 것을 알 수 있다.

그것도 꽤 많이.

 

이 정도면 충분히 설명되었을 거라 생각한다.

 

배운 점

1. array = [0] * a // 0으로 채워진 a크기의 배열을 선언

2. min() // 최솟값을 선택함

3. 연산자 ' // ' // 정수형으로 몫을 반환해 줌

4. DP // Dynamic Programing으로 부분 문제의 최적 결괏값이 변하지 않고, 부분 문제의 최적 결괏값으로 전체의 최적

결과를 계산할 수 있어야 하며, 문제를 작은 연산들의 반복으로 쪼갤 수 있을 때 사용할 수 있다.

5. Memoization // 한번 수행한 연산은 다시 수행하지 않아, 시간 효율을 올린다.