# ---------- Import ----------
import sys
input = sys.stdin.readline

# ---------- Main ----------
N, K = map(int, input().split())
coins = [int(input()) for _ in range(N)]

dp = [0] * (K+1)
dp[0] = 1

for coin in coins:
    for k in range(coin, K+1):
        dp[k] += dp[k-coin]

print(dp[-1])

 

 

2293번: 동전 1

첫째 줄에 n, k가 주어진다. (1 ≤ n ≤ 100, 1 ≤ k ≤ 10,000) 다음 n개의 줄에는 각각의 동전의 가치가 주어진다. 동전의 가치는 100,000보다 작거나 같은 자연수이다.

www.acmicpc.net

 

N개의 서로 다른 동전이 주어질 때, 동전을 이용해서 K를 만들 수 있는 경우의 수는 몇 개인가? 묻는 문제이다.

예를 들어서 동전이 1, 2, 3 있고 K는 5라고 가정해보자.

1+1+1+1+1, 1+1+1+2, 1+1+3, 1+2+2, 2+3해서 4가지가 있다.

 

와... 처음에 문제를 받았을 때, 살짝 이게 뭐지 싶었다.

받을 수 있는 동전의 개수도 다른데, 동전의 가치도 입력마다 달라진다..?

확실한 건 memoization을 통해서, 빠른 연산을 하는 종류라는 감이 왔다.

 

그럼 DP이고, 1차원 배열로는 문제를 해결할 수 없을 것 같았다.

2차원 배열을 생성하고, 첫 번째 index는 N(coin), 두 번째 index는 K로 잡았다.

DP를 풀면서 느낀 건데, dp[N][K]나 dp[N] 같은 게 무엇을 의미하는지, 정확하게 정의해야 나중에 헷갈리지 않는다.

고민하다가 "N번째 코인까지 사용했을 때, K를 만들 수 있는 경우의 수"로 정의했다.

 

예를 들어서 코인이 1, 2, 3 있고, K가 7이라고 해보자. 그럼 위와 같은 표를 그릴 수 있다.

색칠한 3개의 회색 칸을 자세하게 살펴보자. 왼쪽에서부터 순서대로 살펴본다.

가장 왼쪽 회색은 dp[1][2]로 표현할 수 있다. 해석하면, 코인 1을 가지고 2를 만들 수 있는 경우의 수다.

중앙의 회색은 dp[3][4]로 표현할 수 있다. 해석하면, 코인 1, 2, 3을 가지고 4를 만들 수 있는 경우의 수다.

가장 오른쪽 회색은 dp[2][6]로 표현할 수 있다. 해석하면, 코인 1, 2를 가지고 6을 만들 수 있는 경우의 수다.

이제 대충 표는 그릴 수 있으니 예제 입력으로 가보자.

 

값을 채워넣은 것은 규칙을 발견해서 채운 게 아니라, 규칙을 찾기 위해 직접 계산하였다.

검은색 숫자는 dp[N][K]를 의미하고, 빨간색 숫자 A + B에서 A는 dp[N-1][K]를, B는 새로 생긴 경우의 수이다.

30개 칸에서 마지막 칸을 채웠을 때, 비로소 규칙을 찾을 수 있었다.

 

dp[3][10]이라고 할 때, 두 가지를 더해야 한다.

1. 1, 2로 10을 만들 수 있는 경우의 수. A+B에서 A([N-1][K])에 해당한다.

2. 1, 2, 5로 새롭게 10을 만들 수 있는 경우의 수. A+B에서 B에 해당한다.

2번의 케이스를 반대로 생각해보면, 코인을 똑같이 사용하면서 10을 만들기 이전의 상황을 생각하면 된다.

즉, 1, 2, 5로 5를 만들 수 있는 경우에 5가 하나 더 생긴 것이다.

5를 coin이라고 하면, [N][K-coin]의 위치를 더하면 되는 것이다.

 

for i, coin in enumerate(coins, 1):
    for k in range(1, K+1):
        dp[i][k] += dp[i-1][k] + dp[i][k-coin]

이 부분을 함수로 나타내면 이렇다. coins는 동전의 가치를 저장한 list이다.

enumerate로 index(i)와 coin을 반복해서 받아온다.

그리고 1부터 K까지 하나하나 채워나간다. dp[i-1][k] + dp[i][k-coin] 식을 따라서.

 

# ---------- Main ----------
N, K = map(int, input().split())
coins = [int(input()) for _ in range(N)]

이제 코드를 작성해보자. N, K, coins를 받아오는 코드다.

 

# ---------- Main ----------
dp = [[0] * (K+1) for _ in range(N+1)]

for i in range(1, N+1):
    dp[i][0] = 1

2차원 dp를 [N][K] 담을 수 있는 공간만큼 선언해준다.

그리고 각 row에서 첫 번째 인덱스는 1로 초기화해준다.

1로 초기화 해주는 이유는 동전을 한 개만 쓰는 경우 때문이다.

 

예를 들어 [2][2]라고 해보자. 그럼 [2-1][2]와 [2][2-2]를 더해야 한다.

[2-1][2]는 [1][2]로 1 동전을 가지고 2를 만드는 경우의 수다(A).

[2][2-2]는 [2][0]로 2 동전을 가지고 0을 만드는 경우의 수다(B).

즉, B에서 [K]가 0인 경우는 동전 하나만 경우의 수에 새로 추가되는 케이스다.

 

[3][5]일 때, 5를 새롭게 만들 수 있는 경우의 수는?

5 동전 하나만을 사용하는 것이다. 즉 [3][5-5]의 값은 1로 초기화해두어야 한다.

 

# ---------- Main ----------
for i, coin in enumerate(coins, 1):
    for k in range(1, K+1):
        dp[i][k] += dp[i-1][k] + dp[i][k-coin]

위에서 언급한 핵심 아이디어 코드이다.

 

0 >>> [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
1 >>> [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
2 >>> [1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6]
5 >>> [1, 1, 2, 2, 3, 4, 5, 6, 7, 8, 10]

코드를 완성하고 예제 입력을 넣어 출력한 dp이다.

올바르게 2차원 배열을 생성했음을 알 수 있다. 그럼 dp[-1][-1]이 최종 답이 된다.

 

TLE가 아니라 MLE가 떴다? 문제 조건을 살펴보았다. N 최댓값 100, K 최댓값 10,000, coin 최댓값 100,000.

생각보다 값이 그렇게까지 크지 않다고 생각했다. 1,000,000(백만) 개의 칸이면 충분히 할 수 있지 않나?

객체 공간을 선언하는 거라서 값이 0이나 100,000이나 이건 상관없을텐데...

 

최악의 경우로 메모리를 테스트해보았다. 엥? 8 MB밖에 안 쓰는데?

하지만 어림도 없지! 문제 조건으로 4 MB 제한 걸어버리기!

예전에 DP 문제를 풀 때, 굳이 2차원으로 선언을 하지 않았던 방법이 있다.

내 코드 상에서 row 각 줄을 한 줄 한 줄 돌기 때문에 1차원 공간에서도 충분히 해결 가능하다고 생각했다.

 

dp = [0] * (K+1)
dp[0] = 1

for coin in coins:
    for k in range(coin, K+1):
        dp[k] += dp[k-coin]

print(dp[-1])

우선 dp를 K+1만큼만 1차원으로 선언해주었다. 0번째 index를 반복문으로 초기화할 필요도 없어졌다.

코드에서 필요한 것은 결국 dp에서 가장 마지막 값 뿐이다. 앞에 불필요한 부분도 탐색은 생략했다.

예를 들어서 coin이 5라면, K가 1부터 4까지는 탐색할 필요가 없다. 따라서 range 시작을 coin부터 한다.

당연히 1차원 배열로 줄었으니 더하기 연산도 dp[k-coin]으로 줄었다.

 

생각보다 간단한 리팩토링을 거쳐서 AC를 받을 수 있었다.

살짝 난이도 있는 DP를 풀고 싶다면 적당한 문제라고 생각한다.

 

P.S.1

살짝 사족을 붙이자면, 이번에 작성할 세 문제는 좋은 느낌이었다.

2293번_동전, 2493번_탑, 1461_도서관으로, 각각 DP, stack, greedy로 알고리즘 분류도 다 달랐다!

알고리즘이 전부 다 달라서 생각하는 맛이 있었고, 난이도도 적당했다.

엄청 쉽지도 않으면서 그렇다고 감이 안 올 정도로 어렵지도 않은, 적당히 타오를 수 있는 그런 문제들.

 

실제로 '대충 흐름이 이러면 될 거 같은데?' 느낌이 오고, 직접 손으로 적어가며 확인해보았다.

결국 1시간만에 3문제를 모두 풀 수 있었다.

문제당 20분인 셈이니, 코테에 점점 다가가는 기분이 들어 살짝 상기됐다.

머리로만 어어 이렇게 되나? 고민하기보다는 확실히 적어가면서 생각하는 게 효과적이다.

생각하면서 막히던 것이 바로 뚫려버렸다. 

 

마지막으로, 불과 몇 달 전까지만 해도 실버 1~2 문제를 풀 때 엄청나게 허덕였다.

지금은 어떤가. 골드 4~5 문제를 풀면서, '오, 이정도면 적당한데?'라는 느낌을 받았다.

20230822 기준으로 동전_46.4%(26257/56568), 탑_31.7%(21374/63821), 도서관_47.6%(4275/8923)이다.

물론 정답률을 보면 알겠지만 그렇게 어려운 문제는 아니었다. 그럼에도 이 느낌은 중요하다.

매일 노력해도 내가 잘하고 있는지 확인할 수 없어서 멈추는 경우가 정말 많으니까.

# -------------------- Case 1 --------------------
# ---------- Import ----------
import sys
input = sys.stdin.readline

# ---------- Function ----------
def DP(N):
	if N == 1:
		return 10
		
	else:
		dp = [1 for _ in range(10)]
		
		for _ in range(2, N+1):
			for i in range(1, 10):
				dp[i] += dp[i-1]
				
		return sum(dp)

# ---------- Main ----------
N = int(input())
result = DP(N)
print(result % 10007)

DP를 이용한 일반적인 풀이

 

# -------------------- Case 2 --------------------
# ---------- Import ----------
import sys
input = sys.stdin.readline

# ---------- Function ----------
def factorial(n: int) -> int:
    global dp
    
    if n == 0 or n == 1:
        return 1
    
    if dp[n]:
        return dp[n]
    else:
        dp[0], dp[1] = 1, 1
        for i in range(2, n+1):
            dp[i] = i * dp[i-1] % MOD
            
    return dp[n]


def power(a: int, n: int) -> int:
    if n == 0:
        return 1
    
    tmp = power(a, n//2)
    
    if n % 2 == 0:
        return tmp * tmp % MOD
    else:
        return tmp * tmp * a % MOD

# ---------- Main ----------
N = int(input()) + 9
K = 9
MOD = 10_007
dp = [0] * (N+1)    # 팩토리얼 계산용 dynamic programming 리스트

numerator = factorial(N)    # 분자
denominator = factorial(N-K) * factorial(K) % MOD   # 분모

# 페르마의 소정리 이용
print(numerator * power(denominator, MOD-2) % MOD)

파스칼의 삼각형을 이용한 풀이 

 

 

11057번: 오르막 수

오르막 수는 수의 자리가 오름차순을 이루는 수를 말한다. 이때, 인접한 수가 같아도 오름차순으로 친다. 예를 들어, 2234와 3678, 11119는 오르막 수이지만, 2232, 3676, 91111은 오르막 수가 아니다. 수

www.acmicpc.net

입력 길이가 주어졌을 때, 특정 조건을 만족하는 해당 자릿수의 개수를 구하는 문제이다.

조건은 오름차순이다. 이때 오름차순은 같은 숫자도 포함한다.

 

예를 들어서 입력이 1이라면, 길이가 1인 오름차순 숫자를 찾으면 된다.

오해의 소지가 있을까봐 예제 입력과 출력으로 주어졌다.

1이라면 0부터 9까지 총 10개의 오르막 수가 있는 것이다

 

00부터 99까지의 경우의 수(왼쪽), 두 자리인 오르막 수(오른쪽)

그렇다면 2자리일 때, 00을 세야 하는 것인가? 아니면 무시해야 하는 것인가?  

이에 따라 우선 00부터 99까지 총 100가지의 경우의 수를 나열해보았다.

나열하고 보니 끝자리가 0부터 9까지로 정렬할 수 있어보였다.

마치 구구단 9단이나, 1부터 6까지를 7로 나눈 숫자같은 느낌이랄까...?

 

하여튼 두 자리인 오르막 수를 찾아 빨간 경계선으로 구분하였다.

그랬더니 00까지 세줘야 총 개수가 55개로 예제 출력 2와 동일함을 알 수 있었다.

세 자리인 경우까지는 예제 입력이 있기 때문에 확인해주어야 겠다고 생각했다.

 

끝자리가 0부터 4인, 길이가 3인 오르막 수들

하나하나 세는 와중 하나의 공통점을 발견했다.

1은 두 자리 오르막 수에서 0과 1로 끝나는 수 뒤에 1을 이어붙이면 된다.

2는 두 자리 오르막 수에서 0과 1과 2로 끝나는 수 뒤에 2를 이어붙이면 된다.

이런 식으로 나아가면서 앞의 수들을 더해가면 되네? 하는 생각으로 개수를 표로 표현해보았다.

 

규칙 표(왼쪽), 규칙 도식화(오른쪽)

길이(입력)이 4일 때, 끝자리가 6, 7, 8, 9일 때 적지 않은 것은 여기까지 적었을 때 규칙을 발견했기 때문이다.

왼쪽의 표 일부를 떼와서 오른쪽의 그림으로 도식화해보았다.

표 상에서 내 위의 수와, 내 왼쪽의 수 2개를 더하면 현재의 값이 완성된다.

 

만약 dp = [1 for _ in range(10)]의 배열이 있다고 가정해보자.

그럼 어떤 특정 위치의 값은 dp[i]일 것이다. 여기에 dp[i-1]만 더하면 코드는 완성이다.

1로 크기 10의 배열을 선언, 초기화한 뒤 입력받은 크기까지 반복문으로 돌면 코드는 간단하게 끝이 난다.

 

# -------------------- Case 1 --------------------
# ---------- Import ----------
import sys
input = sys.stdin.readline

# ---------- Function ----------
def DP(N):
	if N == 1:
		return 10
		
	else:
		dp = [1 for _ in range(10)]
		
		for _ in range(2, N+1):
			for i in range(1, 10):
				dp[i] += dp[i-1]
				
		return sum(dp)

# ---------- Main ----------
N = int(input())
result = DP(N)
print(result % 10007)

N이 1일 경우에는 10으로 간단하게 return 해주고 끝을 낸다.

그 다음 2부터 N까지 더하는 연산을 진행해줘야 하기에 range(2, N+1)만큼 돈다.

그리고 1부터 9까지 dp의 값들을 업데이트 해줘야 하기에 range(1, 10)만큼 돈다.

왜 0부터가 아니냐고? 0번째 인덱스, 즉 끝자리가 0인 값은 길이가 어떻든 무조건 1개이기 때문이다.

그리고 마지막으로 합(sum)을 10007로 나눈 나머지를 구하면 정답이다.

 

눈치가 빠르다면 위의 코드에서 Case 1이라고 쓴 것을 봤을 것이다.

난 위의 표에서 한 가지를 더 발견했다.

안 보이는가??? 수학자들이 환장하는 그 수들이?! 파스칼의 삼각형이!!!

정말이지 흥분하지 않을 수 없다.

간단해보이지만 그렇지 않은 이 DP 문제에서 파스칼의 삼각형이라니!

피보나치도, 시어핀스키 삼각형도, 삼각수도, 사면체수도, 이항계수도, 자연수도 모든 게 표현 가능한

파스칼의 삼각형이라니!!! 이건 분명 더 간단하게 풀 수 있는 무언가가 있다는 직감이 왔다.

 

파스칼의 삼각형을 이용한 오르막 수(왼쪽), 파스칼의 삼각형 하키스틱 패턴(오른쪽)

길이가 1, 2, 3일 때를 각각 주황색, 초록색, 하늘색으로 칠해보았다.

물론 우하향의 대각선을 칠하기는 했지만, 좌하향의 대각선을 칠해도 상관없다.

왜? 파스칼의 삼각형이니까!

 

원하는 길이(행)에서부터 대각선으로 딱 10개만 더하면 우리가 구하고자 하는 식(값)이 나온다.

이걸 어떤 방법으로 구하면 좋을까 생각을 하다 하키스틱 패턴을 생각했다.

하키스틱 패턴은 어떤 부분이든 한 변에서 시작해 쭉 직진하다가, 한 번 꺾는 순간, 직진의 합이 꺾은 수와 동일하다.

연두색으로 칠한 예시를 보자. 1 + 8 + 36 + 120은 165이다. 궁금하다면 직접 더 해보자.

 

출처 : https://blog.naver.com/vollollov/220947452823

그럼 우리가 구하고자 하는 패턴을 단순화할 수 있다. 파스칼의 삼각형의 특징을 한 번 더 이용해서.

뭐로? 이항 계수로 말이다.

파스칼의 삼각형에서 이항 계수는 너무 유명하니 설명은 생략한다.

길이가 1일 때를 생각해보자. 마지막 위치는 9번째 행의 10번째 숫자일 것이다.

(이항 계수의 특징을 이용할 것이기에 파스칼의 삼각형에서 첫 번째 행은 포함하지 않는다.)

이를 하키스틱 패턴을 이용하면 이 숫자들의 합은 10번째 행의 9번째 숫자이다. 즉 10C9이다.

길이가 2일 때를 생각해보자. 마지막 위치는 10번째 행의 10번째 숫자일 것이다.

마찬가지로 하키스틱 패턴을 이용하면  11번째 행의 9번째 숫자이다. 즉 11C9가 된다.

 

우리는 여기서 규칙을 찾았다.

입력이 N이라면 N+9C9를 계산하면 되는 문제였다!!

 

# -------------------- Case 2 --------------------
# ---------- Import ----------
import sys
input = sys.stdin.readline

# ---------- Function ----------
def factorial(n: int) -> int:
    global dp
    
    if n == 0 or n == 1:
        return 1
    
    if dp[n]:
        return dp[n]
    else:
        dp[0], dp[1] = 1, 1
        for i in range(2, n+1):
            dp[i] = i * dp[i-1] % MOD
            
    return dp[n]


def power(a: int, n: int) -> int:
    if n == 0:
        return 1
    
    tmp = power(a, n//2)
    
    if n % 2 == 0:
        return tmp * tmp % MOD
    else:
        return tmp * tmp * a % MOD

# ---------- Main ----------
N = int(input()) + 9
K = 9
MOD = 10_007
dp = [0] * (N+1)    # 팩토리얼 계산용 dynamic programming 리스트

numerator = factorial(N)    # 분자
denominator = factorial(N-K) * factorial(K) % MOD   # 분모

# 페르마의 소정리 이용
print(numerator * power(denominator, MOD-2) % MOD)

황홀했다.

11057번의 문제가 "입력이 N일 때, N+9C9를 구하시오"라는 간단한 문장으로 표현되다니...

역시 수학은 아름답다.

 

이항 계수 계산은 예전에 풀었던 '11401 이항 계수' 풀이를 참고했다.

 └ https://miny-genie.tistory.com/158

코드는 길어지긴 했지만, 수학적 아름다움이 묻어나다 못해 뚝뚝 흘러넘치는 풀이라니 행복하다.

코드까지 더 간소화할 수 있었다면 말그대로 완벽했을 테지만 그래도 괜찮다.

완벽은 존재하지도 존재해서도 안 되니까.

 

 

# ---------- Import ----------
import sys
input = sys.stdin.readline

# ---------- Declaration ----------
R, G, B = 0, 1, 2

# ---------- Main ----------
N = int(input())
RGB_list = []

for _ in range(N):
    RGB_list.append(list(map(int, input().split())))
    
for i in range(1, N):
    RGB_list[i][R] = min(RGB_list[i-1][G], RGB_list[i-1][B]) + RGB_list[i][R]
    RGB_list[i][G] = min(RGB_list[i-1][R], RGB_list[i-1][B]) + RGB_list[i][G]
    RGB_list[i][B] = min(RGB_list[i-1][R], RGB_list[i-1][G]) + RGB_list[i][B]

print(min(RGB_list[N-1]))

# ---------- Comment ----------
# N번땅에서 N+1번땅을 고려하는 것이 아닌, N번땅에서 N-1번땅을 고려하기.
# R, G, B 각각 지역에서 시작하는 경우를 구하고, 가장 작은 값 출력하기.

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

 

1149번: RGB거리

첫째 줄에 집의 수 N(2 ≤ N ≤ 1,000)이 주어진다. 둘째 줄부터 N개의 줄에는 각 집을 빨강, 초록, 파랑으로 칠하는 비용이 1번 집부터 한 줄에 하나씩 주어진다. 집을 칠하는 비용은 1,000보다 작거나

www.acmicpc.net

 

문제도 비교적 짧고 해석하기에 어려운 편은 아니다.

마치 '4색 정리'처럼 이웃하는 집들끼리는 색이 서로 달라야 한다.

그때 빨강, 초록, 파랑에 해당하는 각각의 비용이 주어질 때 최솟값으로 칠하는 비용을 물어보는 문제이다.

 

예제 입력 형태(왼쪽), 예제 입력에서 R이 시작일 때 경우의 수(오른쪽)

처음에는 R, G, B들의 색상이 고정되었고 이를 if 조건문으로 풀면 되는 줄 알았다.

그래서 그냥 싼 비용 2개를 뽑아, 홀수와 짝수에 따라 계산해주면 된다고 생각했다.

예시) R 10 G 15 B 20일 때, 집이 홀수면 RGRGR, 집이 짝수면 RGRG

 

하지만 예제 입출력을 보니 위의 그림(왼쪽)과 같았다.

각 집마다 RGB 값이 모두 달랐던 것이다.

시간 제한은 0.5초이고, 영락없는 DP 문제임을 확신했다.

 

오른쪽의 예시를 한번 생각해보았다.

처음 한 숫자에서 시작했다고 보자. 그럼 다음 경우의 수는 2개가 될 것이다.

그리고 그 2개에서 또 각각 2개의 경우의 수로 나뉠 것이고, 그 다음부터는 모든 수를 가는 경우의 수가 존재한다.

위에 사진에서만 봐도 벌써 2 * 4 * 6 * 6 = 288가지이다. 이를 식으로 나타내면 {2 * 4 * 6 * (N-2)}와 같다.

문제 입력 제한에서 N이 최대 1000이고 RGB 각각을 모두 확인한다고 했을 때, 최악의 경우는 143,712번이다.

 └ 3 * {2 * 4 * 6 * (1000-2)}

 

내가 갈 수 있는 경우에서 제일 작은 수를 더하면 되는 것 아닌가?라고 생각했다.

예를 들어 집이 3개고 각각 RGB가 다음과 같다.

H1(1, 10, 10), H2(10, 1, 2), H3(1000, 1, 1000)

 

천천히 아이디어를 따라 풀어보자.

H1에서 가장 적은 비용인 1을 선택했다. 그럼 H2에서는 첫 번째 인덱스는 넘어가야하므로 1과 2중 고민을 할 것이다.

내가 생각한 아이디어는 "갈 수 있는 경우에서 제일 작은 수"이기에 1을 골라야 한다.

자 이제 문제가 생긴다.

H3에서 두 번째 인덱스를 고르지 못하기에, 무조건 1000을 골라야 한다.

이 경우는 H2에서 2라는 손해를 감수하더라고 H3에서 1을 골라야 최소 선택이 된다.

다다음의 경우의 수를 고려하지 못한다는 점에서 '이게 정말 최소 선택 알고리즘인가'하는 문제가 발생한다.

 

# ---------- Import ----------
import sys
input = sys.stdin.readline

# ---------- Main ----------
N = int(input())
cache = [-1001] * (N+1)

list_N = [0] + list(map(int, input().split()))

for i in range(1, N+1):
    cache[i] = max(list_N[i], list_N[i] + cache[i-1])
    
print(max(cache))

# ---------- Comment ----------
# max(list_N[i], list_N[i] + cache[i-1])
# 이제껏 더한 수와, 현재 배열의 수 중 더 큰 값을 찾는다.
 

1912번: 연속합

첫째 줄에 정수 n(1 ≤ n ≤ 100,000)이 주어지고 둘째 줄에는 n개의 정수로 이루어진 수열이 주어진다. 수는 -1,000보다 크거나 같고, 1,000보다 작거나 같은 정수이다.

www.acmicpc.net

임의의 수열이 주어졌을 때, 가장 큰 연속된 수의 합을 산출하라는 문제이다.

여기서 핵심은 연속하는 길이가 얼마인지, 가장 큰 값이 얼마인지 아예 모른다는 것이다.

그래서 처음에는 길이를 1에서부터 임의의 수열만큼 for문을 돌리고

거기서 계속 더하는 방식을 생각했다.

 

# 위에서 언급한 방법 의사 코드

for LEN in range(1, len(arr)+1):				# 연속된 수의 길이
	for INDEX in range(len(arr) - LEN):		# 연속된 수의 합 시작점
    		for문으로 LEN 길이만큼 더하기		# 연속된 수 더하기
            		if MAXNUM = 가장 큰 값		# 가장 큰 값 판별하기

 음...이건 아니다. 이렇게만 봐도 시간복잡도가 O(n4)를 넘는다.

이건 동적 계획법이 아니라 브루트 포스에 더 가깝다. 조금 더 세련된 방법이 필요하다.

동적 계획법은 작은 문제로 나누어, 과거에 구한 값을 활용하는 방식임을 생각해보자.

 

우선 가장 큰 값을 활용해야 하니, 어떤 두 값을 비교하여 최댓값을 저장한다.

그리고 이 최댓값을 불러와서 다시 비교한다.

이런 식으로 작성하면 될 것 같은 느낌이 들었다.

 

for i in arr:
	tmp[i] = tmp[i-1]+arr[i]와 arr[i] 중 더 큰 값

여기서 핵심은 과거에 구한 값을 활용하는 것이다.

이전의 값들을 더해서 tmp에 저장하고, 그것을 새로운 값과 비교하여 다시 더한다.

이렇게 하면 계속해서 연속된 값을 유지할 수 있다.

 

# ---------- Import ----------
import sys
input = sys.stdin.readline

# ---------- Main ----------
N = int(input())
cache = [-1001] * (N+1)

list_N = [0] + list(map(int, input().split()))

for i in range(1, N+1):
    cache[i] = max(list_N[i], list_N[i] + cache[i-1])
    
print(max(cache))

# ---------- Comment ----------
# max(list_N[i], list_N[i] + cache[i-1])
# 이제껏 더한 수와, 현재 배열의 수 중 더 큰 값을 찾는다.

이런 식으로 작성하면 연속된 값들 중 가장 큰 값을 얻을 수 있다.

처음에 연속하는 길이가 얼마인지에 대한 고민을 조금 많이 했었는데 별로 중요하지 않았다.

결국 중요한 것은 출력이 가장 큰 값이다.

 

주어진 입력은 list_N에 저장하였고, 임의로 저장하는 배열은 cache로 선언했다.

list_N의 0번째 인덱스를 0을 넣어준 이유는 계산을 편하게 하기 위함이다.

cache를 -1001로 초기화한 이유는 조건이 "수는 -1,000보다 크거나 같고, 1,000보다 작거나 같은 정수이다."이기 때문이다.

 

이를 알기 쉽게 그림으로 그리면 위와 같다.

# ---------- Function ----------
def padovan(N):
    # padovan_list[1], padovan_list[2], padovan_list[3] = 1, 1, 1
    if N == 1 or N == 2 or N == 3:
        return 1
    
    if padovan_list[N] != 0:
        return padovan_list[N]
    
    padovan_list[N] = padovan(N-2) + padovan(N-3)
    
    return padovan_list[N]

# ---------- Main ----------
caseT = int(input())

for _ in range(caseT):
    N = int(input())
    padovan_list = [0] * (N+1)
    print(padovan(N))

 

 

9461번: 파도반 수열

오른쪽 그림과 같이 삼각형이 나선 모양으로 놓여져 있다. 첫 삼각형은 정삼각형으로 변의 길이는 1이다. 그 다음에는 다음과 같은 과정으로 정삼각형을 계속 추가한다. 나선에서 가장 긴 변의

www.acmicpc.net

아는 수학과에게 물어보았더니 실제 있는 수열이라고 한다.

백준에서 문제를 출제하기 위해 임의로 만든 수열인 줄 알았지만 아니었다.

 

파도반 수열은 '첫 정삼각형 변의 길이는 1이다. 가장 긴 변의 길이를 k라 했을 때, 그 변에 길이가 k인 정삼각형을 추가한다. 다음과 같은 과정으로 정삼각형을 계속 추가한다. 이때의 가장 긴 변의 길이의 집합이다'로 정의할 수 있다.

위에 그림에서 왼쪽이 파도반 수열이고, 오른쪽이 피보나치 수열(흔히 말하는 황금비 수열)을 도식화한 것이다.

얼핏 보아도 대충 비슷한 느낌임을 알 수 있다. 정의가 어렵지 단순히 항을 나열해보면 아래와 같다.

 

# 피보나치 수열
# 1 1 2 3 5 8 13 21

# 파도반 수열
# 1 1 1 2 2 3 4 5 7 9

피보나치 수열이 an = an-1 + an-2 또는 an+2 = an+1 + an로 쓸 수 있고,

파도반 수열은 an = an-2 + an-3 또는 an+3 = an+1 + an로 쓸 수 있다.

피보나치 수열에 대한 설명은 https://miny-genie.tistory.com/79에서 확인할 수 있다.

여기서는 파도반 수열을 기반으로 동적 프로그래밍을 연습하겠다.

 

# ---------- Function ----------
def padovan(N):
    #padovan_list[1], padovan_list[2], padovan_list[3] = 1, 1, 1
    if N == 1 or N == 2 or N == 3:
        return 1
    
    if padovan_list[N] != 0:
        return padovan_list[N]
    
    padovan_list[N] = padovan(N-2) + padovan(N-3)
    
    return padovan_list[N]

# ---------- Main ----------
caseT = int(input())

for _ in range(caseT):
    N = int(input())
    padovan_list = [0] * (N+1)
    print(padovan(N))

padovan(N) 함수를 살펴보면 1, 2, 3항은 전부 1이므로 1로 초기화한다.

그후 padovan 리스트의 N번째 인덱스 값이 비어있다면 padovan(N-2)와 padovan(N-3)을 호출한다.

10번쨰 코드인 "padovan_list[N] = padovan(N-2) + padovan(N-3)" 부분이 핵심이다.

리스트( [ ] ) 기호와 함수 매개변수( ( ) ) 기호를 헷갈리지 말자.

+ Recent posts