https://www.acmicpc.net/problem/1336
문제

접근
N, K, D의 제한이 모두 10만이라고는 했지만 사실 수열의 합 값인N의 제한이 10만이기 때문에 수열의 길이 제한인 K는 매우 제한된다.
수열은 증가하는 자연수 수열이기 때문에 길이가 K인 수열의 가장 작은 N은 K * (K+1) / 2 이다.
즉, K * (K + 1) <= 100000 이여야 하기 때문에 방정식을 풀어보면 K의 최대값은 446이다.
그리고 수열의 초기 상태를 1, 2, 3, 4, ..... K 라고 하자. 수열의 합인 K * (K + 1) / 2 값이 N 보다 작다면 우리는 그 잔차 만큼을 수열에 더해주어야 한다. 하지만 증가수열 조건을 위반하지 않기 위해서는 특정 연속한 구간에 1씩을 더해주는 연산밖에 하지 못한다.
그리고 각 항과의 차이 제한이 D임을 고려하면 현재 1부터 K 까지의 연속한 자연수 수열에서 연속한 두 항의 차이는 1이기에 특정 구간에 1을 더해주는 연산은 최대 D-1번 밖에 할 수 없다.
즉, 구해야 하는 경우의 수는
S = N - K * (K+1) / 2
(S >= 0) 일 때
일정 길이의 연속한 구간에 1을 더해주는 연산(1, 2, 3, ... K) 를 각각 최대 D-1 번 사용하여 현재의 잔차인 S를 만들 수 있는 경우의 수를 구하는 것이다. 이를 통해 해당 문제는 무제한 용량 배낭문제로 변형된다.
풀이
1, 2, 3, 4 ..... , K-1, K 을 오름차순으로 고려한다고 가정했을 때 dp 배열의 정의는
" dp[ i ][ j ] = i번째 연산까지 고려하였을 때 누적합을 j 로 만들 수 있는 경우의 수 " 이다.
하지만 점화식이 문제이다. 정말 정말 문제이다. 처음 문제를 접했을 때 상태 전이 방식을 파악하는 난이도가 극악이다.
처음에 생각한 방식은 현재 j를 채울 때 i 연산을 사용할 수 있는지 여부를 확인한 후, 한번이라도 사용할 수 있다면
채운 후, i 를 D 번 이상 사용할 수 있는 경우는 제외하는 것이였다.
for(int i = 1; i <= K; i++) {
for(int j = 0; j <= S; j++) { // 가치의 합
if(j >= i && j / i < D) {
dp[i][j] = (dp[i-1][j] + dp[i][j-i]) % MOD;
}
else {
dp[i][j] = dp[i-1][j];
}
}
}
하지만 이는 당연하게도 틀린 방법이다. 현재 연산을 D 번 이상 사용할 수 있음에도 D - 1번 이하로 사용하는 경우를 제외해버리기 때문이다.
올바른 방식은 j가 D * i 이상인 경우에 D번 이상 사용하는 경우 자체를 제외하는 것이다.
점화식을 자세히 보자.
dp[i][j] = (dp[i-1][j] + dp[i][j-i]) % MOD
dp[ i ][ j - i ] 의 의미는?
우선 ' j ' 즉, 현재 목표 누적합량에서 i를 제외하는 것은 무슨 의미일까?
j 를 완성하기 위해서 i 를 최소 한번은 사용한다는 뜻이 될 것이다. 그리고 동시에 i번째 연산까지 함께 고려하므로
해당 점화식은 i번째 연산을 전혀 사용하지 않는 경우 dp[i-1][j] 와 함께 i번째 연산을 최소 한번 사용하는 것 부터 최대로 사용하는 경우까지 모두 포괄하는 '무한 배낭'의 형태가 되는 것이다.
우리는 여기서 i 를 D 번 이상 사용하는 경우를 제외해야 한다.
여기서 고려해야될 점은 우리는 현재 목표 누적합 값 ' j '를 작은 값 부터 큰 값까지 오름차 순으로 계산하고 있다는 점이다.
j 가 D * i 보다 작은 경우들에 대해서는 당연하게도 적용되지 않으며, j가 정확히 D * i 값이 되는 순간에는 i 를 D만큼'만' 사용한 경우를 빼주면 된다.
dp[i][j] = (dp[i][j] - dp[i-1][j - D * i] + MOD) % MOD;
/*
i를 정확히 'D'번 사용한 경우만 제외한다
*/
하지만 왜 D만큼 사용한 것만 빼주는 것일까? D +1 , D + 2만큼 사용한 것들 또한 고려해주어야 하지 않는가?
라고 생각할 수 있다. 하지만 점화식을 다시 들여다보면 의문이 해소된다.
dp[i][j] = (dp[i-1][j] + dp[i][j-i]) % MOD;
우선 위에서 우리는 j 값이 정확히 D * i 가 되는 지점에서 D번 사용하는 경우를 제외해주었다.
이제 dp[i][D*i]에는 i를 최대 D-1번 사용하는 경우 밖에 안남아있다.
dp[i][D*i] = (dp[i][D*i] - dp[i-1][D*i - D*i] + MOD) % MOD;
이번에는 j값이 i * (D+1)인 경우를 생각해보자.
dp[i][i * (D+1)] = i를 사용하지 않는 경우 + dp[i][i * (D+1) - i] (i를 최소 1번 사용하는 경우의 수) 이다.
dp[i][i * (D+1)] 값을 분석해보면 이전에 계산했던 값이지만 용량이 늘어나며 정의가 바뀐다.
dp[i][i * (D + 1) - i] = dp[i-1][D * i] (i를 한개 사용하는 경우의 수) + dp[i-1][D * (i-1)] (i 를 두개 사용하는 경우의 수) ........................dp[i-1][0] (i를 D + 1개 사용하는 경우의 수)
하지만 마지막 값 dp[i-1][0] 값은 이전 식에서 이미 제거되었다.
즉, 현재 계산식에서 제외해주어야 하는 유일한 값은 이전 상태에서는 D-1개를 사용하는 경우였지만 누적합 값이 늘어나며 D개를 사용하게 된 dp[i-1][j - D * i] 값만 제외해주면 된다는 것이다.
이걸 어떻게 알아
정답코드
#include <iostream>
#define MOD 1000000007
typedef long long ll;
using namespace std;
int dp[447][100001]; /*
조건을 만족하는 K의 최대값은 446이다.
정의 : i번째 까지 고려했을 때 합이 j가 될 수 있게 하는 경우의 수
** i번째 수는 최대 D-1번 더한다
*/
int main() {
ll N, K, D;
ll S;
cin >> N >> K >> D;
S = N - K * (K + 1) / 2;
if(S < 0) {
cout << "0";
return 0;
}
for(int i = 0; i <= K; i++) {
dp[i][0] = 1;
}
for(int i = 1; i <= K; i++) { // 고려하고 있는 숫자들
for(int j = 0; j <= S; j++) { // 가치의 합
int k = i, cnt = 1;
if(j >= k) {
dp[i][j] = (dp[i-1][j] + dp[i][j-k]) % MOD;
if(j >= D * i) {
dp[i][j] = (dp[i][j] - dp[i-1][j - D * i] + MOD) % MOD;
}
}
else {
dp[i][j] = dp[i-1][j];
}
}
}
cout << dp[K][S];
}

dp의 세계는 정말 끝도 없다...
'알고리즘' 카테고리의 다른 글
| Boj 13306, 34203 오프라인 쿼리 c++ (1) | 2026.01.29 |
|---|---|
| Boj 2169 로봇 조종하기 c++ (0) | 2025.12.28 |
| Boj 2315 c++ 가로등 끄기 (0) | 2025.12.20 |
| Boj 15486 c++ 퇴사 2 (0) | 2025.12.15 |
| Boj 2240 c++ 자두나무 (0) | 2025.12.14 |