1563. 개근상




문제

백준중학교에서는 학기가 끝날 무렵에 출결사항을 보고 개근상을 줄 것인지 말 것인지 결정한다. 이 학교는 이상해서 학생들이 학교를 너무 자주 빠지기 때문에, 개근상을 주는 조건이 조금 독특하다.

출결사항이 기록되는 출결은 출석, 지각, 결석이다.

개근상을 받을 수 없는 사람은 지각을 두 번 이상 했거나, 결석을 세 번 연속으로 한 사람이다.

한 학기가 4일이고, O를 출석, L을 지각, A를 결석이라고 했을 때, 개근상을 받을 수 있는 출결정보는

OOOO OOOA OOOL OOAO OOAA OOAL OOLO OOLA OAOO OAOA 
OAOL OAAO OAAL OALO OALA OLOO OLOA OLAO OLAA AOOO 
AOOA AOOL AOAO AOAA AOAL AOLO AOLA AAOO AAOA AAOL
AALO AALA ALOO ALOA ALAO ALAA LOOO LOOA LOAO LOAA 
LAOO LAOA LAAO

으로 총 43가지이다.

한 학기는 N일이다. N이 주어졌을 때, 개근상을 받을 수 있는 출결정보의 개수를 세는 프로그램을 작성하시오.

입력

첫째 줄에 N이 주어진다. N은 1,000보다 작거나 같다.

출력

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

예제 입력 

4

예제 출력 

43



1. 접근


하루에 할 수 있는 출결은 세 가지이다. 몇일 동안 출결할지 정해지고, 상을 탈 수 있는 조건이 주어졌을 때 조건을 만족하는 출결순열의 경우가 총 몇가지 인지 구해야 한다.


조건이 없다면 3 ^ N 가지의 경우가 있겠지만, 조건은 지각은 한번만 가능하고, 결석은 이틀까지만 연속으로 가능하다.


이틀 결석하고 출석하고, 이틀 결석하고 출석하는 경우가 가능하다는 뜻이다.


어떤 방법을 써야 모든 경우를 잘 셀 수 있을까?




2. 풀이


직관적으로 가능한 모든 조합을 확인하는 방법이 있겠다. 하지만 N은 최대 1000일 이므로, 3 ^ 1000 개의 조합을 확인하기엔 시간이 모자르다.



그러면 어떤 규칙을 통해 모든 조합을 확인하지 않아도 될까?


당장에 어떤 수학적인 규칙은 보이지 않으므로,

가능한 경우를 계속 늘려가면서 세버리는 다이나믹 프로그래밍을 쓰기로 하자.


그러면 조건을 잘 활용해야 한다. 지각의 횟수는 계속 누적되고, 결석은 누적되지 않는다는게 특징이다.


배열은 다음과 같이 정의한다.


DP[지금까지 출결한 날][지금까지 지각한 횟수][지금까지 연속으로 결석한 횟수] = DP[day][cnt1][cnt2]

= 지금까지 상황이 이러저러 할 때, 최종적으로 조건을 만족하는 경우의 수


당연히 문제의 답은 DP[0][0][0]에 있을 것이다.


N = 4 라면 DP[4][1][2] = 1이다. 반대로 DP[4][2][2] = 1이다.



DP배열을 갱신해줄 함수는 다음과 같다.


func(int day, int cnt1, int cnt2) = DP[day][cnt1][cnt2] 를 잘 계산해준다.



함수가 종료될 기저사례는 무엇이 있을까


우선 cnt1이 2까지 차버렸거나, cnt2가 3까지 차버린 경우다. 이러면 앞으로 무슨 출결을 해도 상을 못받는다


= return 0


우여곡절로 N일 까지 도착했다면, 앞의 기저사례는 통과했다는 뜻이니까 상을 받을 수 있다.


 = return 1



내일의 출결은 세가지 가능하다.


1. 잘 출결한다 -> func(day + 1, cnt1, 0) 지각은 누적제이고, 결석은 사라진다.


2. 지각한다 -> func(day + 1, cnt1 + 1, 0) 지각은 누적제이고, 결석은 사라진다.


3. 결석한다 -> func(day+1, cnt1, cnt2 + 1) 지각은 누적제이고, 결석을 연속으로 해버렸다.


함수는 잘 확장되고 기저사례에 알아서 도착해 값을 리턴해줄 것이다.

메모이제이션을 통해 중복계산을 줄일 수도 있다.


3. 코드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include <stdio.h>
#include <string.h>
using namespace std;
 
int n;
int dp[1001][3][4];
 
int func(int day, int c1, int c2) {
    if (c1 >= 2 || c2 >= 3)
        return 0;
 
    if (day == n)
        return 1;
 
    int& ref = dp[day][c1][c2];
    if (ref != -1)
        return ref;
 
    return ref = 
        (func(day + 1, c1, 0+ 
        func(day + 1, c1 + 10+ 
        func(day + 1, c1, c2 + 1)) % 1000000;
}
 
int main() {
    scanf("%d"&n);
 
    memset(dp, -1sizeof(dp));
 
    printf("%d", func(000));
 
    return 0;
}
cs



'알고리즘 > Dynamic Programming 동적계획법' 카테고리의 다른 글

백준) 2098 외판원 순회  (1) 2017.09.17
백준) 1937 욕심쟁이 판다  (0) 2017.09.07
백준) 2240 자두나무  (0) 2017.09.05
백준) 1102 발전소  (0) 2017.09.05
백준) 1562 계단  (0) 2017.09.04

2240. 자두나무



문제

자두는 자두를 좋아한다. 그래서 집에 자두나무를 심어두고, 여기서 열리는 자두를 먹고는 한다. 하지만 자두는 키가 작아서 자두를 따먹지는 못하고, 자두가 떨어질 때까지 기다린 다음에 떨어지는 자두를 받아서 먹고는 한다. 자두를 잡을 때에는 자두가 허공에 있을 때 잡아야 하는데, 이는 자두가 말랑말랑하여 바닥에 떨어지면 못 먹을 정도로 뭉개지기 때문이다.


매 초마다, 두 개의 나무 중 하나의 나무에서 열매가 떨어지게 된다. 만약 열매가 떨어지는 순간, 자두가 그 나무의 아래에 서 있으면 자두는 그 열매를 받아먹을 수 있다. 두 개의 나무는 그다지 멀리 떨어져 있지 않기 때문에, 자두는 하나의 나무 아래에 서 있다가 다른 나무 아래로 빠르게(1초보다 훨씬 짧은 시간에) 움직일 수 있다. 하지만 자두는 체력이 그다지 좋지 못해서 많이 움직일 수는 없다.


자두는 T(1≤T≤1,000)초 동안 떨어지게 된다. 자두는 최대 W(1≤W≤30)번만 움직이고 싶어 한다. 매 초마다 어느 나무에서 자두가 떨어질지에 대한 정보가 주어졌을 때, 자두가 받을 수 있는 자두의 개수를 구해내는 프로그램을 작성하시오. 자두는 1번 자두나무 아래에 위치해 있다고 한다.

입력

첫째 줄에 두 정수 T, W가 주어진다. 다음 T개의 줄에는 각 순간에 자두가 떨어지는 나무의 번호가 1 또는 2로 주어진다.

출력

첫째 줄에 자두가 받을 수 있는 자두의 최대 개수를 출력한다.

예제 입력 

7 2
2
1
1
2
2
1
1

예제 출력 

6



1. 접근


두 나무 중에 내 위치를 w번 갈아탈 수 있다. t초 동안 자두는 t개 떨어지며,

매 초마다 두 나무 중에 자두를 떨궈줄 나무가 입력으로 주어진다.


t번 갈아탈 수 있다면 모두 받아먹을 수 있겠지만, w번으로 갈아탈 수 있는 기회가 한정된다.


그러면 우리는 어떤 전략으로 임해야 최대한 많은 자두를 받아먹을 수 있을까?



2. 풀이


전략을 생각해내기 보다 걍 컴퓨터에게 계산을 시키기로 하자.


심플함이 잘 팔리므로, 다이나믹 프로그래밍을통한 메모이제이션으로 모든 경우를 빠르게 확인해버리는 것이다.


얼마나 빠른지가 중요할텐데, 시간은 DP 배열의 크기에 비례한다.



문제의 변수는 세가지다 : 시간, 위치를 바꾼 횟수, 나의 위치


무언가 전략이 있다면 (최대 갯수가 세 가지 변수보다 적은 변수로 결정난다면) 더 작은 배열로 가능하겠지만, 나는 생각이 안나므로,


삼차원 배열로 DP 배열을 정의하도록하자.



DP[현재시간][현재 나의 위치][자리바꾼 횟수] = 현재 상황이 이러저러 할 때, 지금 상황부터 받아 먹을 수 있는 자두의 최대 갯수


정의 덕분에 함수 정의도 술술 풀린다.


func(int now, int pos, int cnt) = DP[now][pos][cnt] 를 계산해주는 함수.



나머지 할 일은 점화식을 세워주기만 하면된다. 계산은 컴터가 다 해준다.


자두를 받아먹는 경우는 다음 여섯가지로 나눠진다.


1. 현재 시간에 나의 위치와 자두가 떨어질 위치가 같다.


1-1 위치변경의 기회가 남았다.


1-1-1. 그래서 자리를 바꾸지 않고 1개 챙길 수 있다.

1-1-2. 자리를 바꾸고 후일을 도모한다.


-> max(func(now + 1, pos, cnt) + 1, func(now + 1, pos ^ 1, cnt + 1));


1-2 자리바꿈의 기회를 다 써버려서 가만히 있는다 -> 1개 챙긴다.


-> func(now + 1, pos, cnt) + 1;


2. 현재 시간에 나의 위치와 자두가 떨어질 위치가 다르다.


2-1 위치변경의 기회가 남았다.


2-1-1. 그래서 자리를 바꾸지 않고 후일을 도모한다.

2-1-2. 자리를 바꾸고 1개 챙긴다.


-> max(func(now + 1, pos, cnt), func(now + 1, pos ^ 1, cnt + 1) + 1);


2-2. 자리바꿈의 기회를 다 써버려서 가만히 있는다 


-> func(now + 1, pos, cnt);



케이스별로 함수를 호출해주고, 정답을 꾸려나가면 된다.


기저사례는 now가 T를 넘어가버리는 경우로, 정의에 따라 0을 리턴해야한다.


케이스를 더 효율적으로 나눌 수도 있겠다.



3. 코드


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#include <stdio.h>
#include <string.h>
#include <algorithm>
using namespace std;
 
int t, n, i;
int arr[1001];
 
int dp[1001][2][30];
 
int func(int now, int pos, int cnt) {
    if (now == t + 1)
        return 0;
    
    int& ref = dp[now][pos][cnt];
    if (ref != -1)
        return ref;
 
    if (pos == arr[now]) {
        int a1 = 0, a2 = 0;
        if (cnt < n)
            a1 = max(func(now + 1, pos, cnt) + 1, func(now + 1, pos ^ 1, cnt + 1));
        else
            a2 = func(now + 1, pos, cnt) + 1;
        return ref = max(a1, a2);
    }
    else {
        int a1 = 0, a2 = 0;
        if (cnt < n)
            a1 = max(func(now + 1, pos, cnt), func(now + 1, pos ^ 1, cnt + 1+ 1);
        else
            a2 = func(now + 1, pos, cnt);
        return ref = max(a1, a2);
    }
}
 
int main() {
    scanf("%d %d"&t, &n);
    for (i = 1; i <= t; ++i) {
        scanf("%d"&arr[i]);
        arr[i]--;
    }
 
    memset(dp, -1sizeof(dp));
 
    printf("%d", func(100));
 
    return 0;
}
cs


'알고리즘 > Dynamic Programming 동적계획법' 카테고리의 다른 글

백준) 1937 욕심쟁이 판다  (0) 2017.09.07
백준) 1563 개근상  (0) 2017.09.06
백준) 1102 발전소  (0) 2017.09.05
백준) 1562 계단  (0) 2017.09.04
백준) 10844 쉬운 계단 수  (3) 2017.08.31

1102. 발전소


문제

은진이는 발전소에서 근무한다. 은진이가 회사에서 잠깐 잘 때마다, 몇몇 발전소가 고장이난다. 게다가, 지금 은진이의 보스 형택이가 은진이의 사무실로 걸어오고 있다. 만약 은진이가 형택이가 들어오기 전까지 발전소를 고쳐놓지 못한다면, 은진이는 해고당할 것이다.

발전소를 고치는 방법은 간단하다. 고장나지 않은 발전소를 이용해서 고장난 발전소를 재시작하면 된다. 하지만, 이 때 비용이 발생한다. 이 비용은 어떤 발전소에서 어떤 발전소를 재시작하느냐에 따라 다르다.

적어도 P개의 발전소가 고장나 있지 않도록, 발전소를 고치는 비용의 최솟값을 구하는 프로그램을 작성하시오.

입력

첫째 줄에 발전소의 개수 N이 주어진다. N은 16보다 작거나 같은 자연수이다. 둘째 줄부터 N개의 줄에는 발전소 i를 이용해서 발전소 j를 재시작할 때 드는 비용이 주어진다. i줄의 j번째 값이 그 값이다. 그 다음 줄에는 각 발전소가 켜져있으면 Y, 꺼져있으면 N이 순서대로 주어진다. 마지막 줄에는 P가 주어진다.. 비용은 50보다 작거나 같은 음이 아닌 정수이고, P는 0보다 크거나 같고, N보다 작거나 같은 정수이다.

출력

첫째 줄에 문제의 정답을 출력한다. 불가능한 경우에는 -1을 출력한다.

예제 입력 

3
0 10 11
10 0 12
12 13 0
YNN
3

예제 출력 

21




1. 접근


p개 이상의 발전소를 규칙에 따라 모두 가동시켜야 한다.


발전소를 가동시킬려면 기존에 가동되고 있던 발전소가 필요하고, 발전소끼리 가동시키는 비용이 각기 다르다.


p개 이상의 발전소를 가동시키는데 드는 총 비용의 합이 최소가 되는 경우는 무엇일까?




2. 풀이


p개 이상이란 뜻을 생각해보자.


p + 1, p + 2, ... n 개를 가동시키는데 드는 최소비용이 p개 보다 작을 경우가 있다는 말일까?


있다고 쳐보자, 그러면 p + x 개를 가동시키는 방법은 p개를 가동시키는 방법과 다른 부분이 존재한다는 뜻이다.


완전한 부분집합이라면 당연히 p개가 더 작다.


하지만 다른 부분으로 이어지는 경로의 p개 까지의 경로가 당연히 p + x 보다 짧다!


따라서 우리는 문제를 괜히 꼬아서 낸거라고 생각하고 p개로 고정시키자.



일단 -1을 출력해야 하는 상황을 생각해보자면,


하나라도 켜져 있으면 전부 가동시킬 수 있으니까, 전부 고장난 상태라면 -1이다.


마찬가지로 이미 p개보다 많이 켜져있다면 보나마나 0이다.



한편 이 문제를 그리디적으로 풀 수 있을까?


당장에 가동중인 발전기로 가동시킬 최소비용을 선택해 나가는 것이다.


하지만 최소비용이 같을 경우, 나머지 부분 문제는 당연히 가동중인 발전기 종류가 달라지므로 그리디적으로 풀 수 없다.



그러면 다 계산해보자!


n개의 발전기가 있으므로, 각 발전기가 가동중인 상태를 비트로 나타내기 위해선 n + 1 개의 비트가 보장되어야 한다.


그러면 DP[2 ^ N]의 배열을 잡자. 이 배열은 현 가동 상태에서 추가로 발생하는 비용을 담는다.


우리의 답은 DP[처음의 가동 상태]에 있을 것이다.



이제 계산을 해줄 함수를 정의해보자.


func(int num, int stat) = num개를 가동 중이고, 가동상태는 비트로 stat일때, 추가로 발생하는 비용



계산식을 어떻게 만들어야 할까?


n=3이고 현 상태가 100이라 해보자(1발전기가 가동중이다)


먼저 가동중인 발전기를 찾아야 한다. 그 다음으로 고장난 발전기를 찾아야한다.


이 후 가동시키려면 당장의 비용이 발생하니까, 식은 다음과 같다.


dp[stat] = min(dp[stat], func(num + 1, stat | (1 << 고장난 발전기)) + cost[가동 발전기][고장 발전기]



기저사례는 num이 p까지 도달해버린 경우로, func(p, stat) 은 당연히 정의에 따라 0을 반환해야 한다.


메모이제이션을 통해 시간이 단축된다.




3. 코드


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
#include <stdio.h>
#include <string.h>
#include <limits.h>
#include <algorithm>
using namespace std;
 
int n, p, i, j, cur, full;
int cost[16][16];
int dp[1 << 16];
char str[17];
 
int func(int num, int stat) {
    if (num == p)
        return 0;
 
    int& ref = dp[stat];
    if (ref != -1)
        return ref;
 
    ref = INT_MAX;
 
    for (int from = 0; from < n; ++from) {
        if (stat & (1 << from)) {
            for (int to = 0; to < n; ++to) {
                if (from == to)
                    continue;
 
                if ((stat & (1 << to)) == 0)
                    ref = min(ref, func(num + 1, stat | (1 << to)) + cost[from][to]);
            }
        }
    }
 
    return ref;
}
 
int main() {
    memset(dp, -1sizeof(dp));
 
    scanf("%d"&n);
 
    full = (1 << n) - 1;
    for (i = 0; i < n; ++i)
        for (j = 0; j < n; ++j)
            scanf("%d"&cost[i][j]);
 
    scanf("%s %d", str, &p);
 
    int cnt = 0;
    for (i = 0; i < n; ++i) {
        if (str[i] == 'Y') {
            cur = cur | (1 << i);
            cnt++;
        }
    }
 
    if (cnt == 0) {
        if (p == 0)
            puts("0");
        else
            puts("-1");
    }
    else if (cnt >= p)
        puts("0");
 
    else
        printf("%d", func(cnt, cur));
 
    return 0;
}
cs




4. 후기


메모이제이션을 이상하게 해서 한참을 해맸다. 기초의 중요성 ㅎ

'알고리즘 > Dynamic Programming 동적계획법' 카테고리의 다른 글

백준) 1563 개근상  (0) 2017.09.06
백준) 2240 자두나무  (0) 2017.09.05
백준) 1562 계단  (0) 2017.09.04
백준) 10844 쉬운 계단 수  (3) 2017.08.31
백준) 11049 행렬 곱셈 순서  (0) 2017.08.25

1562. 계단 수



문제

45656이란 수를 보자.

이 수는 인접한 모든 자리수의 차이가 1이 난다. 이런 수를 계단 수라고 한다.

그럼, 오늘도 역시 세준이는 0부터 9까지 모든 한 자리수가 자리수로 등장하면서, 수의 길이가 N인 계단 수가 몇 개 있는지 궁금해졌다.

N이 주어질 때, 길이가 N이면서 0에서 9가 모두 등장하는 계단 수가 총 몇 개 있는 지 구하는 프로그램을 작성하시오. (0으로 시작하는 수는 없다.)

입력

첫째 줄에 N이 주어진다. N은 1보다 크거나 같고, 100보다 작거나 같은 자연수이다.

출력

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

예제 입력 

10

예제 출력 

1




1. 접근


계단수는 인접한 자리수의 차이가 모두 1인 수를 일컫는다. 987654321 같은 수가 대표적이다.


쉬운 계단수 문제 ( http://js1jj2sk3.tistory.com/38 ) 에서 자리수가 n으로 주어지면

그런 계단수가 몇개 있는지는 O(10*N)에 구할 수 있었다.


그러면 그중에서 문제처럼 0 부터 9 까지 전부 있는 계단수는 몇개 인지 어떻게 알 수 있을까?




2. 풀이


쉬운 계단수 문제 처럼 생각해보자.


길이가 n인 계단수로 길이가 n+1인 계단수를 만들 수 있었다. n인 수의 마지막 수와 차이가 1나는 수를 뒤에 붙여주면 됐었다.



하지만 이제는 지금까지 무슨 수를 써왔는지까지 반영해야 한다.


그러면 모든 경우의 수를 무지막지하게 따지기로 하자. 계산은 컴퓨터가 해줄테니 걱정없다.



쉬운 계단수 문제처럼 DP[n][x] 배열을 만들자. n은 자리수, x는 맨 마지막 자리의 숫자이다.


하지만 이번 문제에선 지금까지 쓴 자리수를 기억해야하니,


DP[n][x][a0][a1]...[a9] 를 만들 수도 있다. a0 ~ a9 는 각기 0 ~ 9 를 썼는지에 대한 정보를 담을 것이다.


하지만 이제 계산을 어떻게 시켜야할지 코드가 매애우 어려워진다. (본인은 그럼)



그래서 차라리 DP[n][x][2 ^ 10] 짜리 배열로 만든다.


그러면


DP[10][0][1111111111(이진수)] = 10자리면서 1의 자리수는 0이며, 0 ~ 9 까지 모두 사용한 계단 수의 갯수


같은 정보를 담을 수 있다.


1111111111(2진수)가 나타내는 것은 0 ~ 9 까지 사용한 수의 상태를 0 / 1비트로 나타내고 있는 것이다.


(뒤에서 부터 0,1,2, ...)



계산을 컴퓨터에게 어떻게 맡길까?


1자리인 계단수는 9개가 있다. DP[1][X][2 ^ X] = 1 로 기저사례가 된다.


2자리인 계단수부터 점화식으로 구할 수 있다.


DP[N][X][상태비트 OR 2 ^ X] = DP[N - 1][X - 1][상태비트] + DP[N - 1][X + 1][상태비트]


상태비트를 OR 연산으로 사용한 숫자에 해당하는 비트를 세우고 넘겨주는 것이다.



따라서 가장 큰 포문 : 자리수에 해당 / 그 안에 포문 : 마지막으로 쓴 수 0 ~ 9 에 해당 / 그 안에 마지막 포문 : 가능한 모든 상태비트


로 삼중포문으로 계산할 수 있다. -> O(N * 10 * ( 2 ^ 10 )).


코드로 확인해보자.




3. 코드


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include <stdio.h>
using namespace std;
 
#define mod 1000000000
 
int n, i, j, k, ans;
int dp[101][10][1 << 10];
 
int main() {
    scanf("%d"&n);
 
    int full = (1 << 10- 1;
 
    for (j = 1; j <= 9++j)
        dp[1][j][1 << j] = 1;
 
    for (i = 2; i <= n; ++i) {
        for (j = 0; j <= 9++j) {
            for (k = 0; k <= full; ++k) {
                if (j == 0)
                    dp[i][0][k | (1 << 0)] = (dp[i][0][k | (1 << 0)] + dp[i - 1][1][k]) % mod;
                else if (j == 9)
                    dp[i][9][k | (1 << 9)] = (dp[i][9][k | (1 << 9)] + dp[i - 1][8][k]) % mod;
                else {
                    dp[i][j][k | (1 << j)] = (dp[i][j][k | (1 << j)] + dp[i - 1][j - 1][k]) % mod;
                    dp[i][j][k | (1 << j)] = (dp[i][j][k | (1 << j)] + dp[i - 1][j + 1][k]) % mod;
                }
            }
        }
    }
 
    for (j = 0; j <= 9++j)
        ans = (ans + dp[n][j][full]) % mod;
 
    printf("%d", ans);
 
    return 0;
}
cs


'알고리즘 > Dynamic Programming 동적계획법' 카테고리의 다른 글

백준) 2240 자두나무  (0) 2017.09.05
백준) 1102 발전소  (0) 2017.09.05
백준) 10844 쉬운 계단 수  (3) 2017.08.31
백준) 11049 행렬 곱셈 순서  (0) 2017.08.25
백준) 9520 NP-hard (PUTNIK)  (0) 2017.08.24

10844. 쉬운 계단 수



문제

45656이란 수를 보자.

이 수는 인접한 모든 자리수의 차이가 1이 난다. 이런 수를 계단 수라고 한다.

세준이는 수의 길이가 N인 계단 수가 몇 개 있는지 궁금해졌다.

N이 주어질 때, 길이가 N인 계단 수가 총 몇 개 있는지 구하는 프로그램을 작성하시오. (0으로 시작하는 수는 없다.)

입력

첫째 줄에 N이 주어진다. N은 1보다 크거나 같고, 100보다 작거나 같은 자연수이다.

출력

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

예제 입력 1 

1

예제 출력 1 

9

예제 입력 2 

2

예제 출력 2 

17



1. 접근


인접한 모든 자리수의 차이가 1인 수를 계단 수라고 한다.


자리수가 주어질 때, 가능한 계단 수는 몇개나 되는가??



직관적인 다이나믹 프로그래밍이 가능하겠다.


작은 계단 수에서 큰 계단 수를 만들거나, 큰 계단 수에서 작은 계단 수를 만들 수도 있다.




2-1) 탑-다운 풀이


길이가 n인 계단 수는 어떤 수에서 비롯되었을까?


예를 들어 끝이 4라면, 이 계단 수는 끝이 3이거나 5인 계단 수로 만들 수 있다. (3 ->34 or 5 -> 54)



따라서


func(n,x) = 자리수가 n이고 끝이 x인 계단 수의 총 갯수 라고 정의 한다면 다음과 같다.


func(n, x) = func(n-1,x-1) + func(n-1,x+1)


끝의 예외 케이스를 정리해주면 쉽게 재귀적으로 구현 할 수 있다.



2-2) 바텀-업 풀이


길이가 n인 계단 수로 길이가 n+1인 계단 수를 몇개나 만들 수 있을까?


끝이 0이나 9가 아닌 이상 2개나 만들 수 있음은 자명하다.



그러면 이중 포문을 통해, 길이가 가장 작은 경우에서 끝 자리의 10가지 경우를 기록하고, 


이를 토대로 길이를 1늘려서 다시 확인하는 코드가 떠오를 것이다.


dp[d][x] = dp[d-1][x-1] + dp[d-1][x+1]


그게 맞당!




3-1) 코드 1


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include <stdio.h>
#include <string.h>
using namespace std;
 
#define MOD 1000000000
 
int n, ans;
int dp[101][10];
 
int func(int d, int x) {
    if (d == 1)
        return x == 0 ? 0 : 1;
 
    int& ref = dp[d][x];
    if (ref != -1)
        return ref;
 
    if (x == 0)
        return ref = func(d - 11) % MOD;
    if (x == 9)
        return ref = func(d - 18) % MOD;
    else
        return ref = (func(d - 1, x - 1+ func(d - 1, x + 1)) % MOD;
}
 
int main() {
    scanf("%d"&n);
 
    memset(dp, -1sizeof(dp));
 
    for (int i = 0; i <= 9++i)
        ans = (ans + func(n, i)) % MOD;
 
    printf("%d", ans);
 
    return 0;
}
cs



3-2) 코드 2


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <stdio.h>
 
#define MOD 1000000000
 
int dp[101][10];
int n, ans = 0;
 
int main(void) {
    scanf("%d"&n);
 
    for (int i = 1; i < 10; i++)
        dp[1][i] = 1;
 
    for (int i = 2; i <= n; i++) {
        for (int j = 0; j < 10; j++) {
            if (j == 0)
                dp[i][0= dp[i - 1][1] % MOD;
            else if (j == 9)
                dp[i][9= dp[i - 1][8] % MOD;
            else
                dp[i][j] = (dp[i - 1][j - 1+ dp[i - 1][j + 1]) % MOD;
        }
    }
 
    for (int i = 0; i < 10; i++)
        ans = (ans + dp[n][i]) % MOD;
 
    printf("%d\n", ans);
    return 0;
}
cs


'알고리즘 > Dynamic Programming 동적계획법' 카테고리의 다른 글

백준) 1102 발전소  (0) 2017.09.05
백준) 1562 계단  (0) 2017.09.04
백준) 11049 행렬 곱셈 순서  (0) 2017.08.25
백준) 9520 NP-hard (PUTNIK)  (0) 2017.08.24
백준) 10835 카드게임  (2) 2017.08.24

11049. 행렬 곱셈 순서



문제

크기가 N×M인 행렬 A와 M×K인 B를 곱할 때 필요한 곱셈 연산의 수는 총 N×M×K번이다. 행렬 N개를 곱하는데 필요한 곱셈 연산의 수는 행렬을 곱하는 순서에 따라 달라지게 된다.

예를 들어, A의 크기가 5×3이고, B의 크기가 3×2, C의 크기가 2×6인 경우에 행렬의 곱 ABC를 구하는 경우를 생각해보자.

  • AB를 먼저 곱하고 C를 곱하는 경우 (AB)C에 필요한 곱셈 연산의 수는 5×3×2 + 5×2×6 = 30 + 60 = 90번이다.
  • BC를 먼저 곱하고 A를 곱하는 경우 A(BC)에 필요한 곱셈 연산의 수는 3×2×6 + 5×3×6 = 36 + 90 = 126번이다.

같은 곱셈이지만, 곱셈을 하는 순서에 따라서 곱셈 연산의 수가 달라진다.

행렬 N개의 크기가 주어졌을 때, 모든 행렬을 곱하는데 필요한 곱셈 연산 횟수의 최소값을 구하는 프로그램을 작성하시오.

입력으로 주어진 행렬의 순서를 바꾸면 안된다.

입력

첫째 줄에 행렬의 개수 N(1 ≤ N ≤ 500)이 주어진다.

둘째 줄부터 N개 줄에는 행렬의 크기 r과 c가 주어진다. (1 ≤ r, c ≤ 500)

항상 순서대로 곱셈을 할 수 있는 크기만 입력으로 주어진다.

출력

첫째 줄에 입력으로 주어진 행렬을 곱하는데 필요한 곱셈 연산의 최소값을 출력한다. 정답은 231-1 보다 작거나 같은 자연수이다.

예제 입력 

3
5 3
3 2
2 6

예제 출력 

90



1. 접근


행렬 곱셈 연산의 수를 계산하는 방법이 따로 존재할 때, 임의의 행렬들이 주어질 때(순서는 고정), 어떤 행렬들 부터 곱해나가야 연산을 가장 적게하는지 묻는 문제다.


행렬들이 ABCD 로 주어진다면 다음의 곱셈 순서들이 가능하다


1) { { { A * B } * C } * D }


2) { { A * B } * { C * D } }


3) { { A * { B * C } } * D }

 

4) { A * { B * { C * D } } }


5) { A * { { B * C } * D } }



역시나 위의 순서들을 빠르게 만들어 낼 수 있는 패턴을 찾아내는게 포인트다. 


그러면 패턴을 구현해내고, 다이나믹 프로그래밍으로 빠르게 연산 수를 계산할 수 있겠다.




2. 풀이


패턴 : 묶음은 그보다 작은 두 묶음으로 재귀적으로 나눠진다.


2)의 경우 가장 큰 묶음은 { A * B } 와 { C * D } 로 나눠진다. 나머지 경우들은 2번씩 나눠진다.


그러면 DP 배열을 다음과 같이 정의해보자.


DP[X][Y] = 인덱스 x인 행렬부터 y까지 주어졌을 때, 우리가 원하는 정답을 보관한다.



그러면 다음과 같은 재귀함수로 배열을 갱신해 나갈 수 있다.


func(int x, int y) = DP[X][Y]를 계산하는 함수


재귀함수가 탈출하기위한 기저사례는 무엇일까?


간단히 두행렬을 곱하는 경우는 쉽게 연산 수를 계산할 수 있다.


하지만 구현을 더 간단히 하기 위해 x==y 인 경우로 탈출시키자. -> 연산하지 않는다 = return 0



이제 두 묶음으로 나눌 수 있는 경우들을 구현해야 한다.


나누는 기준은 x~y까지 가능하다. 


for (int k = x; k < y; ++k)

mm = min(mm, func(x, k) + func(k + 1, y) + m[x].r * m[k].c * m[y].c);


// m[k].c = m[k+1].r


나눠주고 계산해주는 함수들을 호출한 다음, 최소값을 기록하면 된다.




3. 코드


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include <stdio.h>
#include <string.h>
#include <limits.h>
#include <algorithm>
using namespace std;
 
struct mat {
    int r, c;
};
mat m[501];
 
int dp[501][501];
int n, i;
 
int func(int x, int y) {
    if (x == y)
        return 0;
 
    int& ref = dp[x][y];
    if (ref != -1)
        return ref;
 
    int mm = INT_MAX;
 
    for (int k = x; k < y; ++k)
        mm = min(mm, func(x, k) + func(k + 1, y) + m[x].r * m[k].c * m[y].c);
 
    return ref = mm;
}
 
int main() {
    scanf("%d"&n);
    for (i = 0; i < n; ++i)
        scanf("%d %d"&m[i].r, &m[i].c);
 
    memset(dp, -1sizeof(dp));
 
    printf("%d", func(0, n - 1));
    return 0;
}
cs



4. 후기


하나씩 곱해야 하는 줄 알고 삽질좀 했다.

'알고리즘 > Dynamic Programming 동적계획법' 카테고리의 다른 글

백준) 1562 계단  (0) 2017.09.04
백준) 10844 쉬운 계단 수  (3) 2017.08.31
백준) 9520 NP-hard (PUTNIK)  (0) 2017.08.24
백준) 10835 카드게임  (2) 2017.08.24
백준) 3032 승리(IVANA)  (0) 2017.08.23

9520. NP-hard



문제

TSP는 매우 유명한 문제이다. 이 문제는 NP-hard 문제이기 때문에 효율적인 해법이 존재하지 않는다. 이번에는 약간 변형된 TSP 문제를 풀어보자.

TSP문제는 도시 N개를 모두 한 번씩 방문하는 문제이다. 도시는 1번부터 N번까지 번호가 붙여져 있다. 각 도시 사이의 거리는 모두 알려져 있으며, 모든 도시를 방문하는데 드는 시간을 최소로 해야 한다.

번호가 K인 도시를 방문하려면, K보다 작은 번호를 가진 모든 도시를 K번을 방문하기 전에 모두 방문하거나, 방문한 후에 모두 방문해야 한다. 즉, K보다 번호가 작은 도시 중 하나를 K번 이전에 방문하고, 다른 하나를 K번 이후에 방문하면 안된다.

위의 조건을 만족하면서 모든 도시를 방문하는데 드는 시간의 최소값을 구하는 프로그램을 작성하시오. 첫 도시와 마지막 도시는 같지 않아도 되며, 어느 도시에서 시작하고 마쳐도 상관없다.

입력

첫째 줄에 도시의 수 N (2 ≤ N ≤ 1500)이 주어진다.

다음 N개 줄에는 [0, 1000] 구간에 포함되는 양의 정수가 주어진다. A번째 행의 B번째 숫자는 A에서 B로 가는데 필요한 시간이고, 이 수는 B번 행의 A번째 숫자와 같다. A와 B가 같은 경우에는 0이 주어지며, 이외의 경우에는 항상 양의 정수이다.

출력

첫째 줄에 문제의 조건을 만족하면서 모든 도시를 방문하는데 드는 시간의 최소값을 출력한다.

예제 입력 

3
0 5 2
5 0 4
2 4 0

예제 출력 

7



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



1. 접근


모든 도시들은 서로 연결되어 있고, 일련의 규칙으로 모든 도시를 방문해야 한다. 모든 경우 중에 최소 비용을 구해야 한다.


이 일련의 규칙이란


1) k도시를 방문하려면 k보다 작은 도시들을 전부 미리 방문했거나,

2) k를 방문한 이후 딴짓 하지 말고 k보다 작은 모든 도시들을 이후에 방문해야 한다.


따라서 도시가 1, 2 두 개라면,


[1->2] 혹은 [2->1] 이 가능하다.


도시가 3까지 있다면,


[1->2->3] 혹은 [3->1->2] 혹은 [3->2->1] 혹은 [2->1->3] 이 가능하다


[1->3->2] 순서는 3을 방문하려고 했는데, 2번 규칙에 의하면 1도시를 먼저 방문해버렸기 때문에 옳지 않은 방법이다.


[2->3->1] 순서는 2번 규칙에 의하면 2도시를 방문했으면 딴짓 하지 말고 작은 도시를 방문해야 하지만

3도시로 가버렸으므로 옳지 않은 방법이다.



이와 같은 방문순서들을 n이 주어졌을 때 만드는 패턴이 있지 않을까?


만약 그렇다면 우리는 빠르게 만들어진 순서들을 시도해보는 다이나믹 프로그래밍을 할 수 있을 텐데!




2. 풀이


패턴은 다음과 같다


먼저 1을 시퀀스에 추가한다.

그 다음 2를 시퀀스의 왼쪽이나 오른쪽에 추가한다.

그 다음 3을 시퀀스의 왼쪽이나 오른쪽에 추가한다.

...


위의 패턴으로 우리는 규칙을 만족하는 모든 방법을 만들어 낼 수 있다.


귀납적으로 증명해보자.


1) 1밖에 없는 시퀀스는 규칙을 만족한다.

2) n까지 위의 패턴으로 만든 시퀀스가 규칙을 만족한다고 가정해보자.

3) n+1 도시를 왼쪽이나 오른쪽에 추가해도 규칙을 만족한다면 우리의 패턴은 참이다.


2단계 까지 이뤄진 시퀀스의 가장 번호가 큰 도시는 당연히 n도시다.


이제 3단계에서 n+1 도시를 왼쪽에 추가한다고 하면, 우리는 규칙 2를 만족하는 방문순서를 만들어 낸것이다.

반대로 오른쪽에 추가한다고 하면, 이 시퀀스는 규칙 1을 지키는 방문순서가 될 것이다.



이제 패턴에 따라 모든 경우를 계산해 줄 함수를 정의해보자.


func(현재 시퀀스의 왼쪽 도시 번호, 현재 시퀀스의 오른쪽 도시 번호, 추가할 도시 번호)

= 현재 상황에서의 만들 수 있는 종래의 최소비용 = func(int x, int y, int num)


기저사례는 num이 n+1까지 확장될 경우다. 더 이상 추가할 도시가 없다.


이 도시 num을 왼쪽에 추가한다면, 시퀀스의 왼쪽은 num이 될 것이므로,


func(num, y, num+1) + dist[x][num] 


이 도시 num을 오른쪽에 추가한다면, 시퀀스의 오른쪽은 num이 될 것이므로,


func(x, num, num+1) + dist[y][num]



두 경우를 계산해보고, 둘 중 작은 값이 정답이다. 메모이제이션으로 O(n*n).



3. 코드


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include <stdio.h>
#include <string.h>
#include <algorithm>
using namespace std;
 
int dp[1501][1501];
int dist[1501][1501];
int n, i, j;
 
int func(int x, int y, int num) {
    if (num == n + 1)
        return 0;
 
    int& ref = dp[x][y];
    if (ref != -1)
        return ref;
 
    int left = func(num, y, num + 1+ dist[x][num];
    int right = func(x, num, num + 1+ dist[y][num];
 
    return ref = min(left, right);
}
 
int main() {
    scanf("%d"&n);
    for (i = 1; i <= n; ++i)
        for (j = 1; j <= n; ++j)
            scanf("%d"&dist[i][j]);
 
    memset(dp, -1sizeof(dp));
 
    printf("%d", func(1,1,1));
 
    return 0;
}
cs



4. 후기


패턴을 못찾았다면 난리 필 문제

10835. 카드게임


문제

지훈이는 최근에 혼자 하는 카드게임을 즐겨하고 있다. 게임에 사용하는 각 카드에는 양의 정수 하나가 적혀있고 같은 숫자가 적힌 카드는 여러 장 있을 수 있다. 게임방법은 우선 짝수개의 카드를 무작위로 섞은 뒤 같은 개수의 두 더미로 나누어 하나는 왼쪽에 다른 하나는 오른쪽에 둔다. 그리고 빈 통을 하나 준비한다. 

이제 각 더미의 제일 위에 있는 카드끼리 서로 비교하며 게임을 한다. 게임 규칙은 다음과 같다. 지금부터 왼쪽 더미의 제일 위 카드를 왼쪽 카드로, 오른쪽 더미의 제일 위 카드를 오른쪽 카드로 부르겠다.

  1. 언제든지 왼쪽 카드만 통에 버릴 수도 있고 왼쪽 카드와 오른쪽 카드를 둘 다 통에 버릴 수도 있다. 이때 얻는 점수는 없다.
  2. 오른쪽 카드에 적힌 수가 왼쪽 카드에 적힌 수보다 작은 경우에는 오른쪽 카드만 통에 버릴 수도 있다. 오른쪽 카드만 버리는 경우에는 오른쪽 카드에 적힌 수만큼 점수를 얻는다.
  3. (1)과 (2)의 규칙에 따라 게임을 진행하다가 어느 쪽 더미든 남은 카드가 없다면 게임이 끝나며 그때까지 얻은 점수의 합이 최종 점수가 된다. 

다음 예는 세 장 씩 두 더미의 카드를 가지고 게임을 시작하는 경우이다

카드 순서왼쪽 더미오른쪽 더미
132
224
351

이 경우, 우선 오른쪽 카드 2가 왼쪽 카드 3보다 작으므로 규칙 (1)에 따라 왼쪽 카드만 버리거나 왼쪽 카드와 오른쪽 카드를 모두 버리거나, 규칙 (2)에 따라 오른쪽 카드만 버릴 수 있다.

만약 오른쪽 카드만 버리는 것으로 선택하면, 2만큼 점수를 얻고 오른쪽 카드 2는 버린다.

이제 오른쪽 더미의 제일 위 카드는 4이고 이는 왼쪽 카드 3보다 크므로 규칙 (1)에 따라 왼쪽 카드만 버리거나 왼쪽 카드와 오른쪽 카드를 둘 다 버릴 수 있다.

만약 둘 다 버리는 것으로 선택하면, 이제 왼쪽 카드는 2가 되고 오른쪽 카드는 1이 된다.

이 경우 다시 규칙 (1)과 (2)에 따라 세 가지 중 한가지를 선택할 수 있고, 그 중 왼쪽 카드만 버리는 것으로 선택하면 이제 왼쪽 카드는 5가 되고 오른쪽 카드는 1이 된다.

이 경우에도 역시 규칙 (1)과 (2)에 따라 세 가지 중 한가지를 선택할 수 있고, 그 중 오른쪽 카드만 버리는 것으로 선택하면 1만큼 점수를 얻고 오른쪽 카드 1은 버린다.

이제 오른쪽 더미에는 남은 카드가 없으므로 규칙 (3)에 따라 게임이 끝나며 최종 점수는 2+1=3이 된다.

두 더미의 카드가 주어졌을 때, 게임을 통해 얻을 수 있는 최종 점수의 최대값을 출력하는 프로그램을 작성하시오.

위 예에서 최종 점수의 최대값은 7이다.

입력

첫 줄에는 한 더미의 카드의 개수를 나타내는 자연수 N(1 ≤ N ≤ 2,000)이 주어진다. 다음 줄에는 왼쪽 더미의 카드에 적힌 정수 A(1 ≤ A ≤ 2,000)가 카드 순서대로 N개 주어진다. 그 다음 줄에는 오른쪽 더미의 카드에 적힌 정수 B(1 ≤ B ≤ 2,000)가 카드 순서대로 N개 주어진다. 각 더미에는 같은 숫자를 가진 카드가 두 개 이상 있을 수 있다.

출력

얻을 수 있는 최종 점수의 최대값을 출력한다.

예제 입력 

3
3 2 5
2 4 1

예제 출력 

7



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



1. 접근


규칙에 따라 게임을 해야 할 때, 얻을 수 있는 최대의 점수를 구해야 한다.


어쩌면 처음의 카드 배열을 보고 최대 점수를 얻는 전략이 있을 수도 있다.

하지만 빡대가리로서는 짐작도 안가므로, 모든 경우를 계산해보기로 하자.


또한 다이나믹 프로그래밍으로 시간을 절약해 보기로 하자.



2. 풀이


자연스럽게 재귀적인 풀이가 떠오른다.


func(왼쪽 더미의 남은 카드 수, 오른쪽 더미의 남은 카드 수) = 이런 상황에서 얻을 수 있는 최대의 점수

= func(int x, int y)


라는 함수로 모든 계산을 맡겨 보기로 한다.


현재 상황에서 일단 내릴 수 있는 선택은 총 세가지다.


1) 왼쪽을 버린다 (+0점) -> func(x-1, y)

2) 왼쪽, 오른쪽 둘 다 버린다 (+0) func(x-1, y-1)

3) 왼쪽 카드 점수(a점) < 오른쪽 카드 점수(b점) 이라면 오른쪽을 버린다 (+b점) -> func(x,y-1) + b


따라서 함수내에 재귀적으로 세가지 함수를 호출해주면 된다.


기저사례는 양쪽 카드 더미 중 한쪽이라도 바닥나는 경우다. 



3. 코드


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include <stdio.h>
#include <string.h>
#include <algorithm>
using namespace std;
 
int dp[2001][2001];
int a[2001], b[2001];
int n, i;
 
int func(int x, int y) {
    if (x == 0 || y == 0)
        return 0;
 
    int& ref = dp[x][y];
    if (ref != -1)
        return ref;
 
    int c1 = func(x - 1, y);
    int c2 = func(x - 1, y - 1);
    int c3 = 0;
 
    if (a[x] > b[y])
        c3 = func(x, y - 1+ b[y];
 
    return ref = max(max(c1, c2), c3);
}
 
int main() {
    scanf("%d"&n);
    for (i = 0; i < n; ++i)
        scanf("%d"&a[n - i]);
    for (i = 0; i < n; ++i)
        scanf("%d"&b[n - i]);
 
    memset(dp, -1sizeof(dp));
 
    printf("%d", func(n, n));
 
    return 0;
}
cs



4. 후기


응^^ 중등부

3032. 승리


문제

상근이의 여동생 선영이는 정인이가 상근이의 마이크로프로세서를 훔치는 것을 보았다. 선영이는 상근이에게 이 사실을 말해야 하는지를 고민했다. 하지만, 선영이는 상근이보다 정인이를 더 좋아하기 때문에 말하지 않았다.

선영이는 정인이에게 데이트 신청을 하였고, 영화를 같이 보러간다면 이 사실을 그 누구에게도 말하지 않기로 약속했다.

알고보니 정인이는 여자에게 별 관심이 없었다. 그 이유는 정인이가 수학문제를 푸는데 방해 된다고 생각하기 때문이다.

따라서, 그는 선영이에게 게임을 신청했고, 이기면 영화를 보러 가기로 했다.

선영이는 게임에 자신이 있었기 때문에, 정인이의 제안에 동의했다.

정인이는 N개의 양수를 원 위에 차례대로 쓰고 선영이에게 게임의 규칙을 설명했따.

1. 첫 번째 플레이어가 아무 숫자나 하나 고른다.

2. 두 번째 플레이어가 첫 번째 플레이어가 고른 수에 인접한 두 수 중 하나를 고른다.

3. 그 다음 플레이어는 고를 수 있는 수가 없을 때까지 지금까지 고른 수에 인접한 수 중에서 아무 숫자나 고른다.

4. 홀수를 가장 많이 고른 사람이 승리한다.

정인이는 최선을 다해서 게임을 한다. 그는 항상 이기거나 비길 수 있는 전략을 선택한다.

정인이는 선영이가 게임을 얼마나 잘 하는지 모른다. 또, 정인이는 신사이기 때문에, 선영이에게 첫 번째 수를 양보했다.

선영이가 게임을 승리하기 위해서 고를 수 있는 첫 번째 수의 총 개수를 구하는 프로그램을 작성하시오.

입력

첫째 줄에 원 위에 있는 수의 개수 N이 주어진다. (1 ≤ N ≤ 100)

둘째 줄에는 N개의 정수가 공백으로 구분되어 주어진다. 모든 숫자는 1 이상 1000 이하이다. 두 수가 같은 경우는 없다.

출력

선영이가 게임을 승리하기 위해서 고를 수 있는 첫 번째 수의 개수를 첫째 줄에 출력한다.

예제 입력 

3
3 1 5

예제 출력 

3



1. 접근


두 게이머가 게임을 한다.


다만 두 명 모두 최선의 전략을 구사할 줄 알고(근데 우리는 모른다) 게임에 임할 때,

첫 순서인 플레이어가 이길 수 있는 경우의 수를 묻는 문제다.


역시나 짱구를 굴려봐도 최선의 전략이 뭔지 모르겠으니까, 모든 경우를 시도해보는(하지만 빠른) 다이나믹 프로그램을 써보자.


고려할 점은 n개의 수가 일렬로 있는게 아니라 원형 배열에 있다는 점이다.



2. 풀이


처음의 선택으로 원형배열은 일렬의 배열로 바뀐다. 선택의 양쪽 수가 배열의 양쪽 끝 수가 될 것이다.

 

그러면 카드게임 : http://js1jj2sk3.tistory.com/33 의 경우와 같아진다.

이기기 위한 전략이 뭔지는 모르겠지만, 나의 점수를 최대한으로 하려는 것이다. (상대의 점수를 최소한으로 하려는 것이다)


다만 문제가 요구하는 것은 첫 번째 선택으로 올바른 경우를 세야 하므로 지금까지의 turn을 고려하는 함수는 만들기 어렵다.

(내 머리로 못함)


func(int x, int y) = 남은 배열(입력받은 배열)이 x부터 y일때, 낼 수 있는 최대한의 점수차이.


위와 같이 정의한다면 현재 순서가 누구인지 고려할 필요가 없다.


기저사례는 x==y인 경우로, 입력의 x번째 수가 홀수라면 1, 아니면 0을 리턴해주자.


이제 왼쪽을 고르거나 오른쪽을 고르는 경우를 모두 계산해주면 된다!


한 쪽을 고를 때 만들 수 있는 최대한의 점수차이는 어떻게 기술해야 할까?



한 쪽을 고르고 얻은 점수 - 나머지로 만드는 최대한의 점수 차이 = 쪽을 고르면 얻는 최대한의 점수차이


int left = func(x, x) - func((x + 1) % n, y);

int right = func(y, y) - func(x, (y + n - 1) % n);


라고 쓸 수 있다.


이걸 이해하는게 개인적으로 엄청 힘들었다 (멍청). 이번엔 턴을 고려하지 않는다.


내 턴이라 생각해보면, 선택 이후 턴이 넘어가기 때문에, 당연히 상대방은 최선의 전략을 구사하므로 함수를 호출하신다.

상대가 만드는 점수차는 당연히 내 점수에서 까야 된다.


왼쪽을 선택해서 만드는 점수차는 1점 - 3점차 = -2점차가 된다.



답은 어떻게 고를까? 내가 한 지점을 고르고 먹는 점수와, 상대방이 나머지로 만들 수 있는 최대 점수차를 비교해보면 된다.


func(i, i) - func((i + 1) % n, (i + n - 1) % n) > 0

이라면 내가 이긴다.



3. 코드


#include <stdio.h>
#include <string.h>
#include <algorithm>
using namespace std;
int dp[101][101];
int n, t, i;
int func(int x, int y) {
if (x == y)
return dp[x][y];
if (dp[x][y] != -1)
return dp[x][y];
int left = func(x, x) - func((x + 1) % n, y);
int right = func(y, y) - func(x, (y + n - 1) % n);
return dp[x][y] = max(left, right);
}
int main() {
scanf("%d", &n);
memset(dp, -1, sizeof(dp));
for (i = 0; i < n; ++i) {
scanf("%d", &dp[i][i]);
dp[i][i] %= 2;
}
int ans = 0;
for (i = 0; i < n; ++i)
if (func(i, i) > func((i + 1) % n, (i + n - 1) % n))
ans++;
printf("%d", ans);
return 0;
}


'알고리즘 > Dynamic Programming 동적계획법' 카테고리의 다른 글

백준) 9520 NP-hard (PUTNIK)  (0) 2017.08.24
백준) 10835 카드게임  (2) 2017.08.24
백준) 11062 카드게임(Card Game)  (1) 2017.08.23
백준) 9657 돌 게임 3  (0) 2017.08.18
백준) 9656 돌 게임 2  (0) 2017.08.17

11062. 카드 게임


문제

근우와 명우는 재미있는 카드 게임을 하고 있다. N개의 카드가 일렬로 놓여 있다. 각 카드에는 점수가 적혀있다. 근우부터 시작하여 번갈아가면서 턴이 진행되는데 한 턴에는 가장 왼쪽에 있는 카드나 가장 오른쪽에 있는 카드를 가져갈 수 있다. 카드가 더 이상 남아있지 않을 때까지 턴은 반복된다. 게임의 점수는 자신이 가져간 카드에 적힌 수의 합이다.

근우와 명우는 서로 자신의 점수를 가장 높이기 위해 최선의 전략으로 게임에 임한다. 놓여있는 카드의 개수 N과 카드가 놓여있는 상태가 주어졌을 때 근우가 얻는 점수를 구하는 프로그램을 작성하시오.

예를 들어 카드가 [4, 3, 1, 2]로 놓여있다고 하자. 근우는 처음에 4가 적힌 카드를 가져가고, 명우는 3이 적힌 카드를 가져간다. 그리고 근우는 2가 적힌 카드를 가져가고, 명우는 마지막으로 1이 적힌 카드를 가져간다. 이 때 근우와 명우는 최선의 전략으로 임했으며, 근우가 얻는 점수는 6이다.

입력

입력의 첫 줄에는 테스트케이스의 수 T가 주어진다.

각 테스트케이스 마다 첫 줄에는 카드의 개수 N(1 ≤ N ≤ 1,000)이 주어진다. 두 번째 줄에는 N개의 자연수가 공백으로 구분되어 주어지는데, i번째로 주어지는 수는 왼쪽에서 i번째에 놓인 카드에 적힌 수를 의미한다. 카드에 적혀있는 수는 1이상 10,000이하다.

출력

각 테스트케이스마다 근우와 명우가 최선의 전략으로 임할 때 근우가 얻게되는 점수를 줄로 구분하여 출력한다.

예제 입력 

2
4
1 2 5 2
9
1 1 1 1 2 2 2 2 2

예제 출력 

6
8


1. 접근


두 명이 일련의 규칙에 따라 게임을 진행한다.


단 둘다 최선의 전략을 알고 있다 가정할 때(최선의 전략이 뭔지는 아직 모른다),

선 턴인 주인공이 최대로 얻을 수 있는 점수는 몇 점일까?


계속되는 알고리즘 게임의 일종으로, 아무리 짱구를 굴려봐도 최선의 전략이 무엇일지는 감이 안오므로,

모든 경우를 계산해보는(하지만 빠른) 다이나믹 프로그래밍으로 풀어보자.



2. 풀이


dp[turn][남은 카드들 중 왼쪽 끝][남은 카드들 중 오른쪽 끝]으로 배열을 잡아보자.


턴은 번갈아 진행되며, 남은 카드들의 양 쪽 끝은 0 ~ n-1 까지다.

다이나믹 프로그래밍으로 한 번 기록된 dp배열에 접근하면 상수시간에 리턴되므로 배열 크기에 비례한 O(2*n*n)에 풀 수 있겠다.


이제 문제를 풀어줄 함수를 정의해보자.


func(int turn, int x, int y) = 턴이 turn이고, 왼쪽 끝은 x, 오른쪽 끝은 y 일 때 내가 얻을 수 있는 최대의 점수.


우리는 이제 기저사례로 x==y인 경우를 생각해 볼 수 있다.

남은 카드가 한 장이란 말로, 내 턴이였다면 점수를 취하고, 상대 턴이라면 "그냥 그랬구나" 하고 넘어가면 된다.


이제 왼쪽을 가져가든지, 오른쪽을 가져가든지 점수가 얼마나 될지 계산해봐야 한다.


내 턴이였다면 실제로 점수 계산을 위한 코드가 있어야 할테고,

상대 턴이였다면, 상대도 최선의 전략으로 플레이하기 때문에 양 쪽 플레이 중 나의 점수를 최소로 하는 플레이를 할 것이다.



3. 코드


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#include <stdio.h>
#include <string.h>
#include <algorithm>
using namespace std;
 
#define my 0 
 
int dp[2][1001][1001];
int arr[1001];
int t, n, i;
 
int func(int turn, int x, int y) {
    if (x == y) {
        if (turn == my)
            return arr[x];
        else
            return 0;
    }
 
    int& ref = dp[turn][x][y];
    if (ref != -1)
        return ref;
 
    if (turn == my)
        ref = max(func(turn ^ 1, x + 1, y) + arr[x], func(turn ^ 1, x, y - 1+ arr[y]);
    else
        ref = min(func(turn ^ 1, x + 1, y), func(turn ^ 1, x, y - 1));
 
    return ref;
}
 
int main() {
    scanf("%d"&t);
    while (t--) {
        memset(dp, -1sizeof(dp));
 
        scanf("%d"&n);
 
        for (i = 0; i < n; ++i)
            scanf("%d"&arr[i]);
 
        printf("%d\n", func(my, 0, n - 1));
    }
 
    return 0;
}
cs


'알고리즘 > Dynamic Programming 동적계획법' 카테고리의 다른 글

백준) 10835 카드게임  (2) 2017.08.24
백준) 3032 승리(IVANA)  (0) 2017.08.23
백준) 9657 돌 게임 3  (0) 2017.08.18
백준) 9656 돌 게임 2  (0) 2017.08.17
백준) 9655 돌 게임  (0) 2017.08.17

+ Recent posts