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


수열에서 연속 부분 수열 두개를 골라 그 곱이 가장 클 때 얼마나 클지 알아내야 한다.


연속 부분 수열이 얼마나 클지를 알아내는 방법은 디피 문제를 풀 때 쉽게 접할 수 있는 문제다.


dp[x]를 x번째 인자를 포함한 부분 수열 중 합이 가장 큰 수를 저장하게 정의해 놓으면,


dp[x]는 dp[x-1]이 음수인 경우와 양수인 경우로 나눠 원타임에 값이 정해진다.



또한 새로운 디피 배열 lmax를 정의할 수 있게 되는데,


이번엔 lmax[x]는 x번째 인자를 반드시 포함하지 않아도, x까지 수열의 부분 수열 중 합이 가장 큰 수를 저장한다고 정의해 놓으면,


lmax[x] = max(lmax[x-1], dp[x])로 원타임에 값이 정해진다.


즉 x가 포함되던지 안되던지 두 케이스 중 큰 값으로 정해지는 것이다.



이를 응용해서 왼쪽에서 부터, 오른쪽에서 부터, 최대합 부분수열, 최소합 부분수열을 모두 n타임에 구할 수 있다.


그러고 난 다음 부분 수열 두 개로 나눌 기준점을 n개 중에 골라서 비교하는 작업도 n타임에 수행할 수 있다.


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
#include <bits/stdc++.h>
using namespace std;
 
typedef long long ll;
 
int n;
ll arr[100001];
 
ll dmin[100001], dmax[100001];
ll lmin[100001], lmax[100001];
ll rmin[100001], rmax[100001];
 
int main() {
    cin.tie(0);
    ios_base::sync_with_stdio(0);
 
    cin >> n;
    for (int i = 1; i <= n; ++i) {
        cin >> arr[i];
    }
 
    lmin[1= lmax[1= dmin[1= dmax[1= arr[1];
 
    for (int i = 2; i <= n; ++i) {
        if (dmax[i - 1< 0)
            dmax[i] = arr[i];
        else
            dmax[i] = dmax[i - 1+ arr[i];
 
        if (dmin[i - 1> 0)
            dmin[i] = arr[i];
        else
            dmin[i] = dmin[i - 1+ arr[i];
 
        lmin[i] = min(lmin[i - 1], dmin[i]);
        lmax[i] = max(lmax[i - 1], dmax[i]);
    }
 
    memset(dmin, 0sizeof(dmin));
    memset(dmax, 0sizeof(dmax));
 
    rmin[n] = rmax[n] = dmin[n] = dmax[n] = arr[n];
 
    for (int i = n - 1; i >= 1--i) {
        if (dmax[i + 1< 0)
            dmax[i] = arr[i];
        else
            dmax[i] = dmax[i + 1+ arr[i];
 
        if (dmin[i + 1> 0)
            dmin[i] = arr[i];
        else
            dmin[i] = dmin[i + 1+ arr[i];
 
        rmin[i] = min(rmin[i + 1], dmin[i]);
        rmax[i] = max(rmax[i + 1], dmax[i]);
    }
 
    ll ans = LLONG_MIN;
    for (int i = 1; i < n; ++i) {
        ans = max(ans, lmin[i] * rmin[i + 1]);
        ans = max(ans, lmin[i] * rmax[i + 1]);
        ans = max(ans, lmax[i] * rmin[i + 1]);
        ans = max(ans, lmax[i] * rmax[i + 1]);
    }
 
    cout << ans;
}
cs


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

백준) 2965 캥거루 세마리  (0) 2018.02.06
백준) 2156 포도주 시식  (0) 2018.02.04
백준) 1463 1로 만들기  (0) 2018.02.02
백준) 2579 계단 오르기  (0) 2018.02.02
백준) 1149 RGB거리  (0) 2018.02.01

캥거루 세마리




캥거루 세 마리가 직선 위에 존재하고, 양 끝의 캥거루가 나머지 두 마리의 가운데로 점프해서 끼어들 수 있다.

단, 같은 지점에 두마리 이상 존재할 수 없을 때, 점프를 최대한 어람나 할 수 있을까?



세 마리가 동일한 지점에 있다면 항상 같은 결과가 나온다는 생각으로, dp를 시도해 볼 수 있다.


점프 이후 전에 계산했던 상황이 나오면 재활용 할 수 있는 것이다.


각 상황은 세 마리의 위치로 구분되므로, dp[100][100][100]으로 조질 수 있다. 최악의 경우 백만번의 연산을 수행한다.



재귀적으로 왼쪽, 오른쪽 캥거루가 움직일 경우에 최대 점프수를 알아내서, 그 중 최대값에 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
38
39
40
41
42
43
44
#include <stdio.h>
#include <string.h>
#include <algorithm>
using namespace std;
 
 
int a, b, c;
int dp[100][100][100];
 
 
int func(int a, int b, int c) {
    if (a + 1 == b && b + 1 == c)
        return 0;
 
    int& ref = dp[a][b][c];
    if (ref != -1)
        return ref;
 
    int pp = 0;
    for (int p = a + 1; p < b; ++p) {
        int t = func(a, p, b);
        if (pp < t)
            pp = t;
    }
 
    int qq = 0;
    for (int q = b + 1; q < c; ++q) {
        int t = func(b, q, c);
        if (qq < t)
            qq = t;
    }
 
    return ref = max(pp, qq) + 1;
}
 
 
int main() {
    memset(dp, -1sizeof(dp));
 
    scanf("%d %d %d"&a, &b, &c);
    printf("%d", func(a, b, c));
 
    return 0;
}
cs


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

백준)15673 헤븐스 키친 2  (0) 2018.10.02
백준) 2156 포도주 시식  (0) 2018.02.04
백준) 1463 1로 만들기  (0) 2018.02.02
백준) 2579 계단 오르기  (0) 2018.02.02
백준) 1149 RGB거리  (0) 2018.02.01

포도주 시식




포도주를 최대한 마시려고 한다. 앞에서 부터 잔들을 마실 수 있지만, 연속해서 세잔은 마실 수 없다.

계단 문제(http://js1jj2sk3.tistory.com/85?category=725178)와 같은 상황이다.

다만 한 번에 한 층만 오를 수 있게 조건만 변화했을 뿐이다.

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
#include <stdio.h>
#include <string.h>
#include <algorithm>
using namespace std;
 
int n;
int arr[10000];
int dp[10000][3];
 
int func(int p, int cnt) {
    if (p >= n)
        return 0;
 
    int& ref = dp[p][cnt];
    if (ref != -1)
        return ref;
 
    if (cnt == 0) {
        int a = func(p + 11+ arr[p];
        int b = func(p + 10);
 
        return ref = max(a, b);
    }
    else if (cnt == 1) {
        int a = func(p + 12+ arr[p];
        int b = func(p + 10);
 
        return ref = max(a, b);
    }
    else
        return ref = func(p + 10);
}
 
int main() {
    scanf("%d"&n);
    for (int i = 0; i < n; ++i)
        scanf("%d"&arr[i]);
 
    memset(dp, -1sizeof(dp));
 
    printf("%d", func(00));
 
    return 0;
}
cs


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

백준)15673 헤븐스 키친 2  (0) 2018.10.02
백준) 2965 캥거루 세마리  (0) 2018.02.06
백준) 1463 1로 만들기  (0) 2018.02.02
백준) 2579 계단 오르기  (0) 2018.02.02
백준) 1149 RGB거리  (0) 2018.02.01

1로 만들기





디피로 완전탐색하자.


덜 논리적이라 디피는 재귀로 풀곤하는데(2)(완전 거의 대부분) 포문풀이(1)와 속도 차이가 4배정도 난다.


또한 재귀적으로 나누기 3의 상태를 보는게(3) 더 빠르다. 아마 더 빨리 1에 가까워져서가 아닐까 싶다



(1)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>
 
int dp[1000001];
 
int main() {
    int n, i;
    scanf("%d"&n);
    dp[1= 0, dp[2= 1, dp[3= 1;
    for (i = 4; i <= n; i++) {
        dp[i] = dp[i - 1+ 1;
        if (i % 2 == 0 && dp[i / 2+ 1 < dp[i])
            dp[i] = dp[i / 2+ 1;
        if (i % 3 == 0 && dp[i / 3+ 1 < dp[i])
            dp[i] = dp[i / 3+ 1;
    }
    printf("%d", dp[n]);
}
cs


(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
31
32
33
34
35
36
37
38
39
#include <stdio.h>
#include <string.h>
#include <algorithm>
using namespace std;
 
 
#define fail 987654321
 
 
int n, dp[1000001];
 
int func(int x) {
    if (x < 1)
        return fail;
 
    if (x == 1)
        return 0;
 
    int& ref = dp[x];
    if (ref != -1)
        return ref;
 
    int a = fail, b = fail, c = fail;
    if (x % 3 == 0)
        a = func(x / 3+ 1;
    if (x % 2 == 0)
        b = func(x / 2+ 1;
    c = func(x - 1+ 1;
    
    return ref = min(min(a, b), c);
}
 
int main() {
    scanf("%d"&n);
    memset(dp, -1sizeof(dp));
    printf("%d", func(n));
 
    return 0;
}
cs


(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
#include <stdio.h>
#include <string.h>
#include <algorithm>
using namespace std;
 
 
#define fail 987654321
 
 
int n, dp[1000001];
 
int func(int x) {
    if (x < 1)
        return fail;
 
    if (x == 1)
        return 0;
 
    int& ref = dp[x];
    if (ref != -1)
        return ref;
 
    int a = fail, b = fail, c = fail;
    c = func(x - 1+ 1;
    if (x % 2 == 0)
        b = func(x / 2+ 1;
    if (x % 3 == 0)
        a = func(x / 3+ 1;
    
    return ref = min(min(a, b), c);
}
 
int main() {
    scanf("%d"&n);
    memset(dp, -1sizeof(dp));
    printf("%d", func(n));
 
    return 0;
}
cs


계단 오르기



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



계단을 오르면 점수를 얻는다. 도착지점 까지 일련의 규칙으로 올라갈 때, 얻을 수 있는 최고점은 얼마일까?



도착지점에서 생각해보면, 이 지점에 오기 위해서는 두 계단 밑이나, 한 계단 밑에서 올라왔을 것이다.


또한 그 지점 자체의 최고점이 존재할 것이며, 그 최고점은 두 계단 밑이나 한 계단 밑에서 올라오면서 형성된다.


따라서 각 층마다 상태를 dp로 저장해서 풀면 되겠다.



유의할 점은 한계단씩 연속으로 두번 오를 수 없다는 점이다.


즉 한번 한 계단만 올랐으면, 다음엔 반드시 두 계단을 올라야 한다.


그러면 한 층에서 상태는 두가지로 나눠지고 O(n * 2) 안에 풀 수 있다.



함수 func(int floor, int cnt) = floor층이고, 한 계단씩 연속해서 cnt번 올라왔을 때 얻는 최고점수를 계산해줌.


다음 층으로 오를 때는 cnt = 1이라면 2층 오르는 상태로 이어지며,


cnt = 0 이라면 한 계단 오르거나 두 계단 오르는 상태로 분기된다.



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
#include <stdio.h>
#include <algorithm>
using namespace std;
 
 
#define M -987654321
 
 
int n;
int stair[301], dp[301][3];
 
 
int func(int floor, int cnt) {
    if (floor > n)
        return M;
 
    if (floor == n)
        return stair[n];
 
    int& ref = dp[floor][cnt];
    if (ref != -1)
        return ref;
 
    if (cnt == 0) {
        int a = func(floor + 11+ stair[floor];
        int b = func(floor + 20+ stair[floor];
 
        return ref = max(a, b);
    }
    else if(cnt==1)
        return ref = func(floor + 20+ stair[floor];
}
 
int main() {
    scanf("%d"&n);
    for (int i = 1; i <= n; ++i) {
        scanf("%d"&stair[i]);
        for (int j = 0; j < 3++j)
            dp[i][j] = -1;
    }
 
    printf("%d", max(func(10), func(20)));
 
    return 0;
}
cs


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

백준) 2156 포도주 시식  (0) 2018.02.04
백준) 1463 1로 만들기  (0) 2018.02.02
백준) 1149 RGB거리  (0) 2018.02.01
백준) 2342 Dance Dance Revolution  (0) 2017.09.21
백준) 2618 경찰차  (1) 2017.09.17

RGB거리




각 집들을 칠하는데 비용을 최소화해야 한다.

칠하는 색은 세 종류가 있고, 각 집마다 세가지 색을 칠하는 비용이 다르게 주어진다.

또한 이웃집의 색이 같을 수 없다. 0번 집을 빨갛게 칠했으면, 1번 집은 다르게 칠해야 한다.


모든 경우를 탐색한다면 3 ^ n 만큼 걸린다.

하지만 규칙에 따르면 같은 색을 연속해서 쓸 수 없고, 비용은 최소화해야 하기 때문에

다이나믹 프로그래밍으로 조질 수 있겠다.

n개째 집을 칠하는 비용은 n-1개의 집을 칠하는 최소비용에 더해지기 때문이다.


각 상태는 몇 번째 집을 칠하고 있는지, 색은 무엇인지로 구분된다.

배열은 dp[집의 수][색의 종류]로 잡자.


집을 앞에서 부터 연속적으로 칠한다면, 함수는 다음과 같다.

func(int num, int color) = num번째 집을 color로 칠하는 최소비용을 계산해줌.

그러면 정답은 func(n-1, 0), func(n-1, 1), func(n-1, 2) 중에 작은 값이다.


함수는 다음과 같이 퍼진다.

색을 0으로 칠했다면 최소비용은 func(n-1, 1)과 func(n-1, 2)중에 있다.

계속 퍼져나가다가 n==0이 되면 더 이상 퍼지지 않는다.


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>
 
#define init -987654321
 
int n;
int dp[1000][3];
int cost[1000][3];
 
 
int func(int num, int color) {
    if (num == 0)
        return cost[0][color];
 
    int& ref = dp[num][color];
    if (ref != init)
        return ref;
 
    int min = -init;
    for (int i = 0; i < 3++i) {
        if (i == color)
            continue;
 
        int t = func(num - 1, i);
        if (min > t)
            min = t;
    }
 
    return ref = min + cost[num][color];
}
 
 
int main() {
    scanf("%d"&n);
    for (int i = 0; i < n; ++i) {
        for (int j = 0; j < 3++j) {
            scanf("%d"&cost[i][j]);
            dp[i][j] = init;
        }
    }
 
    int ans = -init;
    for (int i = 0; i < 3++i) {
        if (ans > func(n - 1, i))
            ans = dp[n - 1][i];
    }
    printf("%d", ans);
}
cs


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

백준) 1463 1로 만들기  (0) 2018.02.02
백준) 2579 계단 오르기  (0) 2018.02.02
백준) 2342 Dance Dance Revolution  (0) 2017.09.21
백준) 2618 경찰차  (1) 2017.09.17
백준) 2098 외판원 순회  (1) 2017.09.17

2342. Dance Dance Revolution




문제

너무 뚱뚱한 백승환은 요즘 “Dance Dance Revolution”이라는 게임에 미쳐서 살고 있다. 하지만 그의 춤솜씨를 보면 알 수 있듯이, 그는 DDR을 잘 하지 못한다. 그럼에도 불구하고 그는 살을 뺄 수 있다는 일념으로 DDR을 즐긴다.

DDR은 아래의 그림과 같은 모양의 발판이 있고, 주어진 스텝에 맞춰 나가는 게임이다. 발판은 하나의 중점을 기준으로 위, 아래, 왼쪽, 오른쪽으로 연결되어 있다. 편의상 중점을 0, 위를 1, 왼쪽을 2, 아래를 3, 오른쪽을 4라고 정하자.

처음에 게이머는 두 발을 중앙에 모으고 있다.(그림에서 0의 위치) 그리고 게임이 시작하면, 지시에 따라 왼쪽 또는 오른쪽 발을 움직인다. 하지만 그의 두 발이 동시에 움직이지는 않는다.

이 게임에는 이상한 규칙이 더 있다. 두 발이 같은 지점에 있는 것이 허락되지 않는 것이다. (물론 게임 시작시에는 예외이다) 만약, 한 발이 1의 위치에 있고, 다른 한 발이 3의 위치에 있을 때, 3을 연속으로 눌러야 한다면, 3의 위치에 있는 발로 반복해야 눌러야 한다는 것이다.

오랫동안 DDR을 해 온 백승환은 발이 움직이는 위치에 따라서 드는 힘이 다르다는 것을 알게 되었다. 만약, 중앙에 있던 발이 다른 지점으로 움직일 때, 2의 힘을 사용하게 된다. 그리고 다른 지점에서 인접한 지점으로 움직일 때는 3의 힘을 사용하게 된다. (예를 들면 왼쪽에서 위나 아래로 이동할 때의 이야기이다.) 그리고 반대편으로 움직일때는 4의 힘을 사용하게 된다. (위쪽에서 아래쪽으로, 또는 오른쪽에서 왼쪽으로). 만약 같은 지점을 한번 더 누른다면, 그때는 1의 힘을 사용하게 된다.

만약 1 -> 2 -> 2 -> 4를 눌러야 한다고 가정해 보자. 당신의 두 발은 처음에 (point 0, point 0)에 위치하여 있을 것이다. 그리고 (0, 0) -> (0, 1) -> (2, 1) -> (2, 1) -> (2, 4)로 이동하면, 당신은 8의 힘을 사용하게 된다. 다른 방법으로 발을 움직이려고 해도, 당신은 8의 힘보다 더 적게 힘을 사용해서 1 -> 2 -> 2 -> 4를 누를 수는 없을 것이다.

입력

입력은 지시 사항으로 이루어진다. 각각의 지시 사항은 하나의 수열로 이루어진다. 각각의 수열은 1, 2, 3, 4의 숫자들로 이루어지고, 이 숫자들은 각각의 방향을 나타낸다. 그리고 0은 수열의 마지막을 의미한다. 즉, 입력 파일의 마지막에는 0이 입력된다. 입력되는 수열의 길이는 100,000을 넘지 않는다.

출력

한 줄에 모든 지시 사항을 만족하는 데 사용되는 최소의 힘을 출력한다.

예제 입력 

1 2 2 4 0

예제 출력 

8




1. 접근


발의 위치가 5가지 혹은 4가지로 좁힐 수 있으니 뭔가 그리디 하게 풀 수 있을까 생각이 든다.


하지만 빡대가리로는 직관적인 그리디 방법이 떠오르지 않으므로 만만한 다이나믹 프로그래밍으로 완전탐색 해보자.



2. 풀이


항상 다이나믹 프로그래밍으로 풀 땐 dp배열을 잘 정의해야한다.


어떻게 정의하느냐에 따라 시간복잡도가 달라지고 시간제한에 걸릴지 말지 판가름 할 수 있기 때문이다.


두 발로 와리가리 하는걸 보니 왠지 경찰차 문제 http://js1jj2sk3.tistory.com/52 가 떠오르긴한다.


당시 dp배열은 dp[경찰차 1이 마지막으로 처리한 사건 번호][경찰차 2가 마지막으로 처리한 사건 번호]로 정의했었다.


사건은 천개가 맥시멈이니까 백만 안으로 들어오는 dp배열이 가능했었다.



이 방법으로 이 문제도 조질 수 있을까?


ddr 노트가 십만가 들어온다니까 불행히도 불가능하다.


그래서 내 생각으로는 일단 몇번째 노트 짼지는 기억해야 하니까 십만칸은 기본적으로 필요하다.


또한 직관적으로 왼발 오른발의 상태를 나타내야하니 5 * 5 = 25 칸도 필요하다.


그래서 걍 25 * 십만칸으로 조지기로 하자. 이래버리면 20 MB 정도 써야한다 ㅜㅜ


(분명히 더 나은 방법이 있다. 맞은사람들을 보면 1MB 쓴 사람도 있다.)



그래서 dp[왼발 위치][오른발 위치][다음에 밟을 노트가 몇번째] 로 정의하고 나면


함수func(int l, int r, int i) = 현재 상태가 이러저러 할 때 앞으로 힘을 얼마나 더 써야하는지 최소량으로 정의된다.



함수의 기저사례는 i가 끝까지 가버려서 게임을 다해버린 경우다.


아니라면 두 발의 상태에 따라 함수가 다르게 확장된다.



먼저 두 발이 중앙에 있었다면, 다음 노트에 따라 왼발을 옮기든지 오른발을 옮기든지 하면 된다.


따라서 두 경우를 구해보고 최소값을 리턴하자. 


하지만 잘 생각해보면, 무조건 왼발/오른발을 옮겨도 똑같은 결과다.



다음 경우는 다음 노트가 내 두발 중 하나랑 겹치는 경우다.


이 때는 발은 그대로고 힘을 1만큼 쓰기만 하면 된다.



마지막 경우는 완전 새로운 노트로, 왼발을 옮길지, 오른발을 옮길지 따져봐야 한다.


또한 발을 옮길 때 힘을 다르게 쓰므로 주의하자.


중앙에서 옮기면 2을, 반대편으로 가면 4를, 아니면 3만큼의 힘을 쓴댄다.



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
#include <stdio.h>
#include <algorithm>
#include <math.h>
#include <string.h>
using namespace std;
 
int step[100001];
int dp[5][5][100001];
int len;
 
int func(int l, int r, int i) {
    if (i == len)
        return 0;
 
    int& ref = dp[l][r][i];
    if (ref != -1)
        return ref;
 
    if (l == 0 && r == 0)
        return ref = func(step[i], r, i + 1+ 2;
 
    if (l == step[i])
        return ref = func(l, r, i + 1+ 1;
    else if (r == step[i])
        return ref = func(l, r, i + 1+ 1;
 
    int left = 0;
    if (l == 0)
        left = func(step[i], r, i + 1+ 2;
    else if (abs(l - step[i]) == 2)
        left = func(step[i], r, i + 1+ 4;
    else
        left = func(step[i], r, i + 1+ 3;
 
    int right = 0;
    if (r == 0)
        right = func(l, step[i], i + 1+ 2;
    else if (abs(r - step[i]) == 2)
        right = func(l, step[i], i + 1+ 4;
    else
        right = func(l, step[i], i + 1+ 3;
 
    return ref = min(left, right);
}
 
int main() {
    int tmp;
    while (1) {
        scanf("%d"&tmp);
        if (tmp == 0)
            break;
        else
            step[len++= tmp;
    }
    memset(dp, -1sizeof(dp));
 
    printf("%d", func(000));
 
    return 0;
}
cs




4. 후기


재귀질이니까 바텀업보다 메모리도 시간도 더 쓰긴한다. 코드를 좀 더 간결하게 써서 시간을 최대한 줄여보자.

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

백준) 2579 계단 오르기  (0) 2018.02.02
백준) 1149 RGB거리  (0) 2018.02.01
백준) 2618 경찰차  (1) 2017.09.17
백준) 2098 외판원 순회  (1) 2017.09.17
백준) 1937 욕심쟁이 판다  (0) 2017.09.07

2618. 경찰차




문제

어떤 도시의 중심가는 N개의 동서방향 도로와 N개의 남북방향 도로로 구성되어 있다.

모든 도로에 는 도로 번호가 있으며 남북방향 도로는 왼쪽부터 1에서 시작하여 N까지 번호가 할당되어 있고 동서방 향 도로는 위부터 1에서 시작하여 N까지 번호가 할당되어 있다. 또한 동서방향 도로 사이의 거리와 남 북방향 도로 사이의 거리는 모두 1이다. 동서방향 도로와 남북방향 도로가 교차하는 교차로의 위치는 두 도로의 번호의 쌍인 (동서방향 도로 번호, 남북방향 도로 번호)로 나타낸다. N이 6인 경우의 예를 들면 다음과 같다.

이 도시에는 두 대의 경찰차가 있으며 두 차를 경찰차1과 경찰차2로 부른다. 처음에는 항상 경찰차1은 (1, 1)의 위치에 있고 경찰차2는 (N, N)의 위치에 있다. 경찰 본부에서는 처리할 사건이 있으면 그 사건이 발생된 위치를 두 대의 경찰차 중 하나에 알려 주고, 연락 받은 경찰차는 그 위치로 가장 빠른 길을 통해 이동하여 사건을 처리한다. (하나의 사건은 한 대의 경찰차가 처리한다.) 그리고 사건을 처리 한 경찰차는 경찰 본부로부터 다음 연락이 올 때까지 처리한 사건이 발생한 위치에서 기다린다. 경찰 본부에서는 사건이 발생한 순서대로 두 대의 경찰차에 맡기려고 한다. 처리해야 될 사건들은 항 상 교차로에서 발생하며 경찰 본부에서는 이러한 사건들을 나누어 두 대의 경찰차에 맡기되, 두 대의 경찰차들이 이동하는 거리의 합을 최소화 하도록 사건을 맡기려고 한다.

예를 들어 앞의 그림처럼 N=6인 경우, 처리해야 하는 사건들이 3개 있고 그 사건들이 발생된 위치 를 순서대로 (3, 5), (5, 5), (2, 3)이라고 하자. (3, 5)의 사건을 경찰차2에 맡기고 (5, 5)의 사건도 경 찰차2에 맡기며, (2, 3)의 사건을 경찰차1에 맡기면 두 차가 이동한 거리의 합은 4 + 2 + 3 = 9가 되 고, 더 이상 줄일 수는 없다.

처리해야 할 사건들이 순서대로 주어질 때, 두 대의 경찰차가 이동하는 거리의 합을 최소화 하도록 사건들을 맡기는 프로그램을 작성하시오.

입력

첫째 줄에는 동서방향 도로의 개수를 나타내는 정수 N(5≤N≤1,000)이 주어진다. 둘째 줄에는 처리해야 하는 사건의 개수를 나타내는 정수 W(1≤W≤1,000)가 주어진다. 셋째 줄부터 (W+2)번째 줄까지 사건이 발생된 위치가 한 줄에 하나씩 주어진다. 경찰차들은 이 사건들을 주어진 순서대로 처리해야 한다. 각 위치는 동서방향 도로 번호를 나 타내는 정수와 남북방향 도로 번호를 나타내는 정수로 주어지며 두 정수 사이에는 빈칸이 하나 있다. 두 사건이 발생한 위치가 같을 수 있다.

출력

첫째 줄에 두 경찰차가 이동한 총 거리를 출력한다. 둘째 줄부터 시작하여 (i+1)번째 줄에 i(1≤i≤W)번째 사건이 맡겨진 경찰차 번호 1 또는 2를 출력한다.

예제 입력 

6
3
3 5
5 5
2 3

예제 출력 

9
2
2
1



1. 접근


그리디로는 풀 수 없다. 당장 가까운 경찰차를 보낸다고 하면, 남은 문제는 방금 푼 문제의 조건과 달라지기 때문이다.


그러면 당연히 다이나믹 프로그래밍으로 다 해봐야 한다!



2. 풀이


배열을 어떻게 잡아야 시간안에 풀 수 있을까? 즉, 공통된 계산부분을 잘 만들어내야한다.


모든 경우을 빡대가리처럼 다 계산해버리면 2 ^ 1000 만큼 계산해야 한다.



그럼 무엇이 공통된 사건일까? 중요한건 당연히 현재 경찰차 두대의 위치다.


사건 순서대로 경찰차가 출동한 순서를 예를들어 1->1->2->1->1 이라 해보자. 어쨌건 다섯번 출동하고 나서의 경찰차 두대의 위치는 2->1->2->1->1 와 같다. 경찰차 1은 다섯번째 사건위치에 있을 것이며, 경찰차 2는 세번째 사건위치에 있을 것이다.


그러면 우리는 dp배열을 다음과 같이 정의할 수 있다. dp[x][y] = 현재 경찰차 위치가 x, y 일 때 남은 최소이동거리.


문제는 지금까지 몇번 출동했는지를 모른다는 점이다. 하지만 경찰차 위치는 {1,1} 아니면 {n,n} 아니면 사건위치 이므로, 간략히 0~n 까지의 번호가 부여된 위치로 대체할 수 있다. 동시에 지금까지 얼마나 사건을 처리했는지도 알 수 있다.


따라서 dp[x][y] = 현재 경찰차가 가장 최근에 처리한 사건이 x, y 일 때 남은 최소이동거리. 라고 정의한다.


max(x,y) = k 라면 지금까지 k번째 사건까지 출동했다는 이야기가 된다. 그러면 이제 k+1 사건에 대해 두 대를 출동시켜보고 최소인 경우로 리턴해주면 되겠다.



문제에서 요구하는 답은 추가적으로 사건마다 어떤 경찰차가 출동했는지도 출력해야 한다. 어떻게 알 수 있을까?


dp배열을 갱신하면서 값을 알 수 있는 방법도 분명있다. (하지만 내가 빡대가리라 따로 구하기로 한다.)



dp배열을 모두 갱신하고 나면, 각 칸마다의 정보를 이용해서 어떤 경찰차가 출동했는지 알 수 있다.


만약 dp[1][2] = 10 이고, 확장되는 두 가지 dp[1][3] = 7, dp[3][2] = 5 라면, 당연히 1번 차를 출동시켜 3번 사건을 맡게 했다는 뜻이다.


모든 경우에 대해 이미 계산이 끝났으므로, 사건 수만큼 재귀를 돌고 어떤 차가 출동했는지 알 수 있다.



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
71
72
#include <stdio.h>
#include <algorithm>
#include <string.h>
#include <vector>
using namespace std;
 
int n, w;
struct col {
    int x, y;
};
col fir[1001], sec[1001];
int dp[1001][1001];
 
int func(int c1, int c2) {
    if (c1 == w || c2 == w)
        return 0;
 
    int& ref = dp[c1][c2];
    if (ref != -1)
        return ref;
 
    int np = max(c1, c2) + 1;
    int n1 = abs(fir[c1].x - fir[np].x) + abs(fir[c1].y - fir[np].y);
    int n2 = func(np, c2) + n1;
 
    int m1 = abs(sec[c2].x - sec[np].x) + abs(sec[c2].y - sec[np].y);
    int m2 = func(c1, np) + m1;
 
    return ref = min(n2, m2);
}
 
vector<int> v;
void tracking(int c1, int c2) {
    if (c1 == w || c2 == w)
        return;
 
    int np = max(c1, c2) + 1;
    int n1 = abs(fir[c1].x - fir[np].x) + abs(fir[c1].y - fir[np].y);
    int n2 = dp[np][c2] + n1;
 
    int m1 = abs(sec[c2].x - sec[np].x) + abs(sec[c2].y - sec[np].y);
    int m2 = dp[c1][np] + m1;
 
    if (n2 > m2) {
        v.push_back(2);
        tracking(c1, np);
    }
    else {
        v.push_back(1);
        tracking(np, c2);
    }
}
 
int main() {
    scanf("%d %d"&n, &w);
 
    fir[0= { 1,1 };
    sec[0= { n,n };
    for (int i = 1; i <= w; ++i) {
        scanf("%d %d"&fir[i].x, &fir[i].y);
        sec[i] = { fir[i].x, fir[i].y };
    }
 
    memset(dp, -1sizeof(dp));
    printf("%d\n", func(00));
 
    tracking(00);
    for (auto a : v)
        printf("%d\n", a);
 
    return 0;
}
cs


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

백준) 1149 RGB거리  (0) 2018.02.01
백준) 2342 Dance Dance Revolution  (0) 2017.09.21
백준) 2098 외판원 순회  (1) 2017.09.17
백준) 1937 욕심쟁이 판다  (0) 2017.09.07
백준) 1563 개근상  (0) 2017.09.06


2098. 외판원 순회

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




문제

외판원 순회 문제는 영어로 Traveling Salesman problem (TSP) 라고 불리는 문제로 computer science 분야에서 가장 중요하게 취급되는 문제 중 하나이다. 여러 가지 변종 문제가 있으나, 여기서는 가장 일반적인 형태의 문제를 살펴보자.

1번부터 N번까지 번호가 매겨져 있는 도시들이 있고, 도시들 사이에는 길이 있다. (길이 없을 수도 있다) 이제 한 외판원이 어느 한 도시에서 출발해 N개의 도시를 모두 거쳐 다시 원래의 도시로 돌아오는 순회 여행 경로를 계획하려고 한다. 단, 한번 갔던 도시로는 다시 갈 수 없다. (맨 마지막에 여행을 출발했던 도시로 돌아오는 것은 예외) 이런 여행 경로는 여러 가지가 있을 수 있는데, 가장 적은 비용을 들이는 여행 계획을 세우고자 한다.

각 도시간에 이동하는데 드는 비용은 행렬 W[i][j]형태로 주어진다. W[i][j]는 도시 i에서 도시 j로 가기 위한 비용을 나타낸다. 비용은 대칭적이지 않다; 즉, W[i][j] 는 W[j][i]와 다를 수 있다. 모든 도시간의 비용은 양의 정수이다. W[i][i]는 항상 0이다. 경우에 따라서 도시 i에서 도시 j로 갈 수 없는 경우도 있으며 이럴 경우 W[i][j]=0이라고 하자.

N과 비용 행렬이 주어졌을 때, 가장 적은 비용을 들이는 외판원의 순회 여행 경로를 구하는 프로그램을 작성하시오.

입력

첫째 줄에 도시의 수 N이 주어진다. (2<=N<=16) 다음 N개의 줄에는 비용 행렬이 주어진다. 각 행렬의 성분은 1,000,000 이하의 양의 정수이며, 갈 수 없는 경우는 0이 주어진다. W[i][j]는 도시 i에서 j로 가기 위한 비용을 나타낸다.

항상 순회할 수 있는 경우만 입력으로 주어진다.

출력

첫째 줄에 외판원의 순회에 필요한 최소 비용을 출력한다.

예제 입력 

4
0 10 15 20
5  0  9 10
6 13  0 12
8  8  9  0

예제 출력 

35




1. 접근


DP로 완전탐색 하자



2. 풀이


모든 방문경로를 dp 배열에 담으려면 dp를 어떻게 정의해야 할까?


일단 한 번 방문한 도시로는 돌아가지 않으니까(사이클 없슴) 마지막으로 방문한 도시로 경로를 일단 나눠볼 수 있다.


하지만 이것만으로는 부족하니까 지금까지 어떤 도시를 방문했는지를 기억해야 한다.


따라서 1->3->4->2 와 1->4->3->2 는 같은 dp칸에 담기겠지만, 두 경로 중 최소값이 dp에 담길 것이므로 문제에 적합하다.



dp[cur][stat] = 현재 내위치가 cur이고, 지금까지 stat의 도시들을 방문했을 때 최소값.


stat을 비트로 표현한다면 적당하다. 0이면 미방문이고, 1이면 방문했다.


예시의 경우 dp[2][000...1111]이 되겠다.


이제 dp를 채워줄 함수를 정의해보자.



func(int cur, int stat) = dp[cur][stat]을 계산해준다.


다음 지점을 방문할 때 계산은 어떻게 해야할까?


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
#include <stdio.h>
#include <limits.h>
#include <algorithm>
using namespace std;
 
int n, all;
int cost[16][16];
int dp[16][1 << 16];
 
int func(int cur, int stat) {
    if (stat == all) {
        if (cost[cur][0== 0)
            return 987654321;
        else
            return cost[cur][0];
    }
 
    int& ref = dp[cur][stat];
    if (ref != 0)
        return ref;
 
    int m = INT_MAX - 16000001;
    for (int i = 1; i < n; ++i) {
        if (((stat&(1 << i)) == 0&& (cost[cur][i] != 0))
            m = min(m, func(i, (stat | (1 << i))) + cost[cur][i]);
    }
 
    return ref = m;
}
 
int main() {
    scanf("%d"&n);
    all = (1 << n) - 1;
    for (int i = 0; i < n; ++i)
        for (int j = 0; j < n; ++j)
            scanf("%d"&cost[i][j]);
 
    printf("%d", func(01));
    return 0;
}
cs




4. 후기


방문안한 지점중에 최소값을 찾는 부문에 무턱대고 int m = INT_MAX 을 써버렸더니 틀려버렸다..


생각해보니 포문을 돌고나도 여전히 INT_MAX라 리턴해버리면 다른데가서 cost가 더해져버리고 엉망이 되는 경우가 있었다.


빡대가리


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

백준) 2342 Dance Dance Revolution  (0) 2017.09.21
백준) 2618 경찰차  (1) 2017.09.17
백준) 1937 욕심쟁이 판다  (0) 2017.09.07
백준) 1563 개근상  (0) 2017.09.06
백준) 2240 자두나무  (0) 2017.09.05

1937. 욕심쟁이 판다



문제

n*n의 크기의 대나무 숲이 있다. 욕심쟁이 판다는 어떤 지역에서 대나무를 먹기 시작한다. 그리고 그 곳의 대나무를 다 먹어 치우면 상, 하, 좌, 우 중 한 곳으로 이동을 한다. 그리고 또 그곳에서 대나무를 먹는다. 그런데 단 조건이 있다. 이 판다는 매우 욕심이 많아서 대나무를 먹고 자리를 옮기면 그 옮긴 지역에 그 전 지역보다 대나무가 많이 있어야 한다. 만약에 그런 지점이 없으면 이 판다는 불만을 가지고 단식 투쟁을 하다가 죽게 된다(-_-)

이 판다의 사육사는 이런 판다를 대나무 숲에 풀어 놓아야 하는데, 어떤 지점에 처음에 풀어 놓아야 하고, 어떤 곳으로 이동을 시켜야 둘 다 소중한 생명이지만 판다가 최대한 오래 살 수 있는지 고민에 빠져 있다. 우리의 임무는 이 사육사를 도와주는 것이다. n*n 크기의 대나무 숲이 주어져 있을 때, 이 판다가 최대한 오래 살려면 어떤 경로를 통하여 움직여야 하는지 구하여라.

입력

첫째 줄에 대나무 숲의 크기 n(1<=n<=500)이 주어진다. 그리고 둘째 줄부터 1+n번째 줄 까지 대나무 숲의 정보가 주어진다. 대나무 숲의 정보는 공백을 사이로 두고 각 지역의 대나무의 양이 정수 값으로 주어진다. 대나무의 양은 1,000,000보다 작거나 같은 자연수이다.

출력

첫째 줄에는 판다가 최대한 살 수 있는 일수(K)를 출력한다.

예제 입력 

4
14 9 12 10
1 11 5 4
7 15 2 13
6 3 16 8

예제 출력 

4



1. 접근


이차원 배열이 주어지고, 팬더는 아무 지점에서나 시작할 수 있다. 팬더의 이동은 상하좌우의 배열값에 따라 가능한데, 이웃의 배열값이 현재값보다 클 경우에 이동할 수 있다. 모든 이웃지점으로 이동할 수 없다면 팬더는 죽는다!


이러한 조건하에, 팬더가 최대한 오래 살 수 있는 일수는 얼마일까?

= 팬더가 최대한 오래 이동할 수 있는 경우는 무엇일까?


직관적으로 모든 지점에 대해 DFS를 돌려보고 최대값을 구할 수 있겠다.

하지만 한 지점 당 DFS는 N^2 걸리고, 지점은 N^2 개니까 최악 N ^ 4 걸릴 수 있다.


어떻게 최적화시켜야 할까?



2. 풀이


분명히 중복해서 계산하는 과정이 있다. 한 지점은 분명 여러번 방문될 것이며, 그 지점의 정보는 변하지 않는다.(그 지점을 방문했을 때 몇일이나 더 살 수 있는지는 변하지 않는다.)


따라서 메모이제이션과 또이나믹 프로그래밍을 써야한다.


이제 배열을 정의해보자.


DP[x][y] = 현재 내 위치가 x, y 일 때 앞으로 몇일이나 살 수 있는지 정보를 저장.


그러면 당연히 모든 이웃으로 이동이 불가능하면, 그 날만 살고 죽을테니 그런 DP[x][y] = 1 이다.



함수 func(int x, int y) = DP[x][y]를 계산해서 리턴해주는 기능을 하게 하고, 기저사례를 구분해보자.


당연히 기저사례는 위와같은 이동이 불가능한 지점이다.


아니라면, 이동가능한 지점마다 함수를 호출해주면 된다.


그런 지점으로 이동하면 하루를 더 살 수 있을테니, 그런 지점을 xx, yy 라고 하면, func(x, y) = func(xx, yy) + 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
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
#include <stdio.h>
#include <string.h>
#include <algorithm>
#include <vector>
using namespace std;
 
struct p {
    int xx, yy;
};
 
int n, i, j;
int map[500][500];
int dp[500][500];
int dir[4][2= { {-1,0},{1,0},{0,-1},{0,1} };
 
int func(int x, int y) {
    vector<p> c; // 갈 수 있는 장소들 저장
    bool all = false// 다 갈수 없다고 생각
 
    for (int i = 0; i < 4++i) {
        int xx = x + dir[i][0];
        int yy = y + dir[i][1];
 
        if (xx < 0 || yy < 0 || xx >= n || yy >= n)
            continue;
 
        if (map[xx][yy] > map[x][y]) { // 갈 수 있으면 기저사례가 아님
            all = true;
            c.push_back({ xx,yy });
        }
    }
 
    if (all == false)
        return 1;
 
    int& ref = dp[x][y];
    if (ref != 0)
        return ref;
 
    for (auto pp : c)
        if (map[pp.xx][pp.yy] > map[x][y])
            ref = max(ref, func(pp.xx, pp.yy) + 1);
 
    return ref;
}
 
int main() {
    scanf("%d"&n);
 
    for (i = 0; i < n; ++i)
        for (j = 0; j < n; ++j)
            scanf("%d"&map[i][j]);
 
    int ans = 0;
    for (i = 0; i < n; ++i)
        for (j = 0; j < n; ++j)
            ans = max(ans, func(i, j));
        
    printf("%d", ans);
 
    return 0;
}
cs


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

백준) 2618 경찰차  (1) 2017.09.17
백준) 2098 외판원 순회  (1) 2017.09.17
백준) 1563 개근상  (0) 2017.09.06
백준) 2240 자두나무  (0) 2017.09.05
백준) 1102 발전소  (0) 2017.09.05

+ Recent posts