2 minute read

백준 문제 중 1699번

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

문제

어떤 자연수 N은 그보다 작거나 같은 제곱수들의 합으로 나타낼 수 있다. 예를 들어 11=32+12+12(3개 항)이다. 이런 표현방법은 여러 가지가 될 수 있는데, 11의 경우 11=22+22+12+12+12(5개 항)도 가능하다. 이 경우, 수학자 숌크라테스는 “11은 3개 항의 제곱수 합으로 표현할 수 있다.”라고 말한다. 또한 11은 그보다 적은 항의 제곱수 합으로 표현할 수 없으므로, 11을 그 합으로써 표현할 수 있는 제곱수 항의 최소 개수는 3이다.

주어진 자연수 N을 이렇게 제곱수들의 합으로 표현할 때에 그 항의 최소개수를 구하는 프로그램을 작성하시오.

입력

첫째 줄에 자연수 N이 주어진다. (1 ≤ N ≤ 100,000)

출력

주어진 자연수를 제곱수의 합으로 나타낼 때에 그 제곱수 항의 최소 개수를 출력한다.


풀이

문제의 핵심은 제곱수들의 합중 어떤게 제일 최소의 항을 가지는지 판별하는 방법이다.

주어진 n의 제곱수의 합중 최소의 항개수를 dp[n]이라 할때 예시를 몇개 살펴보자.

  • 1 : 1^2
  • dp[1] = 1
  • 2 : 1^2 + 1^2
  • dp[2] = dp[1] + dp[1]
  • 3 : 1^2 + 1^2 + 1^2
  • dp[3] = dp[2] + dp[1]
  • 4 : 2^2
  • dp[4] = 1
  • 5 : 2^2 +1^2
  • dp[5] = dp[2^2] + dp[1]
  • 6 : 2^2 + 1^2 + 1^2
  • dp[6] = dp[5] + dp[1]
  • 8 : 2^2 + 2^2
  • dp[8] = dp[2^2] + dp[2^2]
  • 9 : 3^2
  • dp[9] = 1

… …

  • 13 : 3^2 + 2^2
  • dp[13] = dp[9] + dp[4]

… …

  • 41: 5^2 + 4^2
  • dp[41] = dp[25] + dp[16]

n이 주어졌을 때 1 ~ n사이의 수 i에 대하여 모든 dp[i]를 살펴보는데 i가 자연수의 제곱일 때는 dp[i] = 1을 대입하고, 제곱이 아닐때는 i보다 작거나 같은 최대 제곱수를 j라 하자. 이때 2 ~ j 사이의 수 k에 대하여 dp[i-k^2] + dp[k^2] 의 값중 최소일 때를 찾으면 그 값이 dp[i]라 할 수 있다.

이를 식으로 표현하면 dp[i] = min(dp[i-j^2] + dp[j^2], dp[i]) 라 할 수 있고 이를 코드로 구현라면 아래와 같다.

Try1


%%time
from collections import defaultdict

import math

n = int(input())

 # n보다 작거나 같은 최대의 제곱수
m = int(math.sqrt(n))

dp = defaultdict(int)

 # 제곱수에 해당하는 모든 i*i에 1 할당
for i in range(1,m+1): dp[i*i] = 1

 # dp[1] ~ dp[n]을 스캔하되 dp[i]가 0이아닌 값이 있다면 스킵
for i in range(1,n+1):
    
    if dp[i]:

        continue

     # i이하의 최대 제곱수 j    
    j = int(math.sqrt(i))

     # dp[i]에 기본값 설정
    dp[i] = dp[i-1]+1

     # 2 ~ j사이의 제곱수들의 조합 중 가장 작은 dp[i] 를 저장
    for k in range(2,j+1):
        dp[i] = min(dp[k*k]+dp[i-k*k],dp[i])

print(dp[n])
41
2
CPU times: user 8.86 ms, sys: 1.72 ms, total: 10.6 ms
Wall time: 1.89 s

이렇게 코드를 구성하고 정답처리가 됐으나 조금 더 깔끔한 방법이 없을까 검색을 해봤다.

Try2

이 문제를 처음 보고 코드를 구성할때 가장 곤란했던게 41의 처리 였다. 41은 5^2 + 4^2 의 합으로 최적화 되니 41이하의 최대 제곱수인 6^2 을 참조해서 dp를 최신화 하면 안된다.

이를 아주 깔끔하게 모든 dp를 1^2으로 나타낸 수로 초기화를 한후, 제곱수를 1부터스캔 하되
dp[i] > dp[i-j^2] + 1을 만족할때만 최신화 하게끔 설정하여 깔끔하게 처리했다. 즉 dp[41]이 이미 j=2 일때 dp[41 - 4^2] + 1로 dp[25] +1 즉 2로 설정이 되어 있어 그이후 2보다 큰 dp[i-j^2] +1 들만이 오며 이 문제가 해결됐고 가장 코드도 간결하며 실행속도 또한 가장 빨랐다.


%%time
n = int(input())

dp = [x for x in range (n+1)]
 
for i in range(1,n+1):
 
    for j in range(1,i):
 
        if j*j > i :
 
            break
 
        if dp[i] > dp[i-j*j] + 1 :

            dp[i] = dp[i-j*j] + 1 
print(dp[n])
41
2
CPU times: user 10.4 ms, sys: 50 µs, total: 10.4 ms
Wall time: 1.67 s

배운점

다이나믹 프로그래밍에서 dp배열을 다룰때 dp배열에 최적의 값을 미리 넣으면 다른 값들을 쉽게 거를 수 있음을 배웠다.

Leave a comment