백준

백준 1562 - 계단 수

황태건 2023. 9. 4. 22:42

 

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

 

1562번: 계단 수

첫째 줄에 정답을 1,000,000,000으로 나눈 나머지를 출력한다.

www.acmicpc.net

 

이 문제보다 조건이 단순한 '쉬운 계단수' 문제에서 적용했던 dp 풀이법을 발전시켜서 풀 수 있다.

0부터 9까지 정수의 등장 여부를 확인하기 위해 비트마스킹 기법을 활용한다.

 

dp식 정의

dp[i][j][k] := 비트필드 j의 숫자들로 이뤄진, 맨 앞자리가 k로 시작하는 i자리 계단수의 갯수

비트필드는 10자리로, 0~9까지 정수의 등장 정보를 저장한다.

 

예를 들어 dp[2][0000000110(2)][1]은 12를 사용하며 맨 앞자리가 12자리 계단수이므로 가능한 계단수는 12 하나 뿐이다.

 

문제에서 요구하는 값은 N자리, 모든 정수를 사용하는 비트필드 (1111111111), 맨 앞자리가 0이 아닌 계단수의 개수의 총합이므로, dp[N][1111111111(2)][1] 부터 dp[N][1111111111(2)][9]까지의 합을 계산해 얻을 수 있다.

 

초깃값

한 자리 자연수는 모두 계단수에 포함된다. 각 자연수 i의 비트필드는 1 << i로 나타낼 수 있으므로, 코드 상에서 dp배열을 다음과 같이 초기화할 수 있다.

//한 자리, 비트필드는 (i + 1)번째 자리만 1, 맨 앞자리 수는 i
for (int i = 0; i <= 9; i++)
    dp[1][1 << i][i] = 1;

점화식

스탠다드한 문제 '외판원 순회'와 비슷하게 진행된다. 자릿수를 1씩 증가시켜가며 매 자릿수마다 0000000001부터 1111111111까지 1000개 가량의 비트필드를 모두 확인한다.

for (int i = 1; i < N; i++) {

    for (int j = 1; j < (1 << 10); j++) {

이제 0부터 9까지의 정수 k에 대해, k가 필드 j에 포함되고 필드 j와 맨 앞자리 k를 가지고 계단수를 만들 수 있는 지 검사한다. 포함 여부는 비트 연산자를 통해, 계단수 확인은 현재 dp[i][j][k]에 저장된 값을 통해 검사할 수 있다.

 

예를 들어 j = 0011100101, k = 5일 경우 k는 필드 j에 포함된다. 그러나 필드의 숫자 0, 2, 5, 6, 7을 모두 사용해 계단수를 만드는 건 불가능하므로 dp[i][j][k]에는 유효한 갯수가 저장되어있지 않다. ex) -1

if ((j & (1 << k)) == 0) continue;
if (dp[i][j][k] < 0) continue;

 

만약 현재 필드 j앞 자리 수 k에 대해 계단수가 존재한다면, 이 계단 수의 앞에 k + 1, k - 1 중 0 ~ 9 범위 내의 정수를 추가해 새로운 계단수를 만들 수 있다.

 

간단한 예를 들어보자.

4로 시작하는 3자리 계단수는 432, 434, 454, 456 4개가 있으며, 각각을 dp식 정의를 이용해 나타내면 다음과 같다.

계단수 dp식 정의
432 dp[3][0000011100][4]     (2,3,4)
434 dp[3][0000011000][4]     (3,4)
454 dp[3][0000110000][4]     (4,5)
456 dp[3][0001110000][4]      (4,5,6)

이제 이 4개의 계단수 앞에 3, 5를 붙여 총 8개의 계단수를 만들 수 있다. 8개는 좀 많으니 일부만 확인해보자.

계단수 dp식 정의 확장 dp식 정의
432 dp[3][0000011100][4]     (2,3,4) 3432 dp[4][0000011100][3]     (2,3,4)
    5432 dp[4][0000111100][5]     (2,3,4,5)
456 dp[3][0001110000][4]      (4,5,6) 3456 dp[4][0001111000][3]     (3,4,5,6)
    5456 dp[4][0001110000][5]     (4,5,6)

 

다시 말해, 현재 dp[i][j][k]에 속하는 모든 계단수에 대해 k + 1과 k - 1 중 유효한 정수를 앞에 추가하면 이 계단수들을 dp[i+1][k + 1을 추가한 필드][k+1], dp[i+1][k - 1을 추가한 필드][k - 1]에 속하는 계단수로 만들 수 있다. 코드로 나타내면 다음과 같다.

if (k + 1 <= 9) {
    ll& up = dp[i + 1][j | (1 << (k + 1))][k + 1];
    
    //dp[i][j][k]를 통해 생성한 새로운 계단수를 추가
    up = up < 0 ? dp[i][j][k] : up + dp[i][j][k];
    
    //오버플로우 방지
    up %= div;
}

if (k - 1 >= 0) {
    ll& down = dp[i + 1][j | (1 << (k - 1))][k - 1];
    down = down < 0 ? dp[i][j][k] : down + dp[i][j][k];
    down %= div;
}

 

풀다보면 0을 관리하기가 까다로웠을 것이다. 참고로 말하면 0과 9는 1 차이 난다고 취급하지 않아 90은 계단수가 아니다. 어쨌든 0을 관리하기 위해서는, 그냥 0으로 시작하는 계단수도 계산 과정에 포함하면 된다. 마지막 카운트에서만 무시하라.

 

전체 코드

더보기
//dp식 정의
ll dp[101][1 << 10][10];

int main() {
    FASTIO;

    int N;
    cin >> N;
    ll div = 1000000000;
	
    //전체 초기화
    memset(dp, -1, sizeof(dp));
	
    //초깃값 설정
    for (int i = 0; i <= 9; i++)
        dp[1][1 << i][i] = 1;

	//자릿수, 필드, 맨 앞자리 3중 반복문
    for (int i = 1; i < N; i++) {

        for (int j = 1; j < (1 << 10); j++) {
			
            //0도 같은 방식으로 처리
            for (int k = 0; k <= 9; k++) {
				
                //포함 여부, 계단수 존재 여부 확인
                if ((j & (1 << k)) == 0) continue;
                if (dp[i][j][k] < 0) continue;
				
                //유효한 계단수가 있으면 맨 앞에 정수를 추가해
                //동일 개수의 새로운 계단수를 생성
                if (k + 1 <= 9) {
                    ll& up = dp[i + 1][j | (1 << (k + 1))][k + 1];
                    up = up < 0 ? dp[i][j][k] : up + dp[i][j][k];
                    up %= div;
                }

                if (k - 1 >= 0) {
                    ll& down = dp[i + 1][j | (1 << (k - 1))][k - 1];
                    down = down < 0 ? dp[i][j][k] : down + dp[i][j][k];
                    down %= div;
                }
            }
        }
    }

    ll res = 0;
	
    //마지막 계산에서는 0을 제외
    for (int i = 1; i <= 9; i++) {
        if (dp[N][(1 << 10) - 1][i] > 0)
            res = (res + dp[N][(1 << 10) - 1][i]) % div;
    }

    cout << res << endl;
}