dp가 정말 풀 것 같으면서도 잘 안 풀린다.
계속 고민하다가.. 해답 보고 실소 짓고 풀고..
또 고민하다가.. 해답 보고 실소 짓고 풀고.. 반복..
감각을 키우는 것 외에는 답이 없는 것 같다.
그래도 어느정도는 감을 잡은듯 싶다.
하지만 또 뒤돌면 못 풀 것 같은 녀석이다.
푼 것들 정리 해보자
-
* 1, 2, 3 더하기 3 (low): 하던 것처럼 맨 끝을 나눠서 생각해보자
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 <iostream>
using namespace std;
long long d[1000001][4];
const long long mod = 1000000009;
int main()
{
for (int i = 1; i <= 1000000; i++) {
if (i >= 1) {
d[i][1] = d[i-1][1] + d[i - 1][2] + d[i - 1][3];
if (i == 1) {
d[i][1] = 1;
}
}
if (i >= 2) {
d[i][2] = d[i-2][1] + d[i - 2][2] + d[i - 2][3];
if (i == 2) {
d[i][2] = 1;
}
}
if (i >= 3) {
d[i][3] = d[i - 3][1] + d[i - 3][2] + d[i-3][3];
if (i == 3) {
d[i][3] = 1;
}
}
d[i][1] %= mod;
d[i][2] %= mod;
d[i][3] %= mod;
}
int t;
cin >> t;
while (t--) {
int n;
cin >> n;
cout << (d[n][1] + d[n][2] + d[n][3]) % mod << '\n';
}
return 0;
}
|
cs |
=> 1, 2, 3으로 더하는 것 외에 별다른 조건이 없으므로, 마지막에 1, 2, 3이 모두 올 수 있고 마지막 전에도 1, 2, 3이 다 올 수 있다. 크기에 따른 구별만 생각해주면 된다.
* RGB 거리 (low) : 앞의 문제처럼 맨 끝을 기준으로 생각해주면 된다.
- price[i][j]에 i번 집에 j색을 칠하는 비용을 입력한다. 색은 빨강, 초록, 파랑 밖에 없으므로 j는 0~2로 생각한다.
- d[i][j]는 i개 집을 마지막으로 j색으로 칠했을 때의 최소값으로 생각한다. 이에 따라 점화식을 세우면,
- d[i][0] = min(d[i-1][1],d[i-1[2]) + price[i][0] : 마지막에 빨강이 오면 그 전에는 초록, 파랑 밖에 못 온다. 그 중의 최솟값에 i번째에 빨강으로 칠하는 비용을 더한다. 이런 식으로 3개의 점화식을 구하고 그 중에 최솟값을 구하면 i개 집을 색칠하는 비용의 최솟값을 구할 수 있다.
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 <iostream>
#include <algorithm>
int price[1001][4];
int d[1001][4];
using namespace std;
int main()
{
int n;
cin >> n;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= 3; j++) {
cin >> price[i][j];
}
}
d[1][1] = price[1][1];
d[1][2] = price[1][2];
d[1][3] = price[1][3];
for (int i = 2; i <= n; i++) {
d[i][1] = min(d[i - 1][2], d[i - 1][3]) + price[i][1];
d[i][2] = min(d[i - 1][1], d[i - 1][3]) + price[i][2];
d[i][3] = min(d[i - 1][1], d[i - 1][2]) + price[i][3];
}
cout << min(min(d[n][1], d[n][2]), d[n][3]) << '\n';
return 0;
}
|
cs |
* 동물원 (mid) : 풀릴듯 말듯 했는데 해답을 보고 유레카를 외칠 수 밖에 없었던 문제다.
- 가로로 두칸, 세로로 n칸인 우리에 사자를 넣고자 한다. 가로로도 세로로도 붙어 있게 배치할 수는 없다고 할 때 그 경우의 수를 구하는 문제다.
- 처음에 문제를 봤을 때 사자가 몇 마리인지도 모르고 대체 어떻게 구하라는 건지 감이 안 잡혔다.
- 1마리일 때부터 경우의 수를 생각하기도 해봤지만, 별다른 규칙을 찾기도 힘들었다.
- 한참을 고민하다가 힌트를 봤다. 결국 이것도 쪼개는 거였다. dp는 결국 쪼개는 거니까...후...
- 맨 끝에 올 수 있는 경우를 생각해보자. 추가될 칸은 3가지로 생각할 수 있다. 두 칸이 모두 비어있는 경우, 왼쪽 칸이 차있는 경우, 오른쪽 칸이 차있는 경우. 그럼 조건에 따라 식을 세울 수 있다.
- d[i][j]를 i 크기인, 마지막에 j조건이 오는 경우의 수라고 생각하면 j는 위에서 말한 것처럼 3가지 경우의 수가 올 수 있다. 따라서 j는 0~2로 생각한다. 이를 바탕으로 점화식을 세우면,
- d[i][0] = d[i-1][0] + d[i-1][1] + d[i-1][2] / d[i-1][1] = d[i-1][0] + d[i-1][2] / d[i-1][2] = d[i-1][0] + d[i-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
|
#include <iostream>
using namespace std;
long long d[100001][3];
const int mod = 9901;
int main()
{
int n;
cin >> n;
d[1][0] = 1;
d[1][1] = 1;
d[1][2] = 1;
for (int i = 2; i <= n; i++) {
d[i][0] = d[i - 1][0] + d[i - 1][1] + d[i - 1][2];
d[i][1] = d[i - 1][0] + d[i - 1][2];
d[i][2] = d[i - 1][0] + d[i - 1][1];
d[i][0] %= mod;
d[i][1] %= mod;
d[i][2] %= mod;
}
cout << (d[n][0] + d[n][1] + d[n][2]) % mod << '\n';
return 0;
}
|
cs |
* 오르막 수 (mid) : lis와는 조금 다른 방법으로 생각해야 한다.
- 오르막 수는 수의 자리가 오름차순을 이루는 수를 말하며, 인접한 수가 같아도 오름차순으로 친다. 계속 해왔던 것처럼 마지막에 올 수 있는 경우의 수로 나눠서 생각하면 된다.
- d[i][j]를 i자리 수인, 마지막에 j가 온 경우의 수라고 생각해보자. j에 올 수 있는 건 한 자리 자연수이므로 0~9이다. 오르막 수가 되려면 i-1번째의 마지막에 온 수가 j보다 작거나 같아야 한다. 이를 통해 점화식을 구해보면,
- d[i][j] = ∑ d[i-1][k] (k <= j) : i-1자리 수에 마지막으로 k가 왔을 때, 그 k값이 j보다 작거나 같아야 오름차 수가 된다. 조건을 만족하는 경우를 모두 더해주면 d[i][j] 값을 구할 수 있다.
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 <iostream>
using namespace std;
long long d[1001][10];
const int mod = 10007;
int main()
{
int n;
cin >> n;
for (int i = 0; i < 10; i++)
d[1][i] = 1;
for (int i = 2; i <= n; i++) {
for (int j = 0; j < 10; j++) {
for (int k = 0; k <= j; k++) {
d[i][j] += d[i - 1][k];
}
d[i][j] %= mod;
}
}
long long sum = 0;
for (int i = 0; i < 10; i++)
sum += d[n][i];
cout << sum % mod << '\n';
return 0;
}
|
cs |
- 14 : 1자리 수에 마지막에 0~9가 오는 건 1개씩이다.
* 스티커 (mid) : 아까 풀었던 동물원과 비슷하게 접근하면 된다.
- 스티커를 떼면, 뗀 스티커의 왼쪽, 오른쪽, 위, 아래에 있는 스티커는 사용할 수 없을 때, 뜯을 수 있는 스티커 점수의 최댓값을 구하는 문제다. 동물원 문제에 점수를 추가했다고 생각하면 된다.
- a[i][j]를 i행 j열의 점수 (j는 위(1), 아래(2) 두가지 경우 밖에 없다) d[i][j]를 2행 i열이 j 경우일 때의 최댓값이라고 생각해보자. j 경우는 아무 것도 안 뜯을 때, 위쪽을 뜯을 때, 아래쪽을 뜯을 때로 총 3가지로 나뉜다. 따라서 j는 0~2이 온다. 이에 따라 점화식을 세우면,
- d[i][0] = max(d[i][1], d[i][2]) , d[i][1] = max(d[i][0], d[i][2]) + a[i][2] , d[i][2] = max(d[i][0], d[i][1]) + a[i][1]
- i번째에 아무 것도 뜯지 않을 때는, i-1번째 1, 2 경우 중 최댓값을 취하면 된다. 위쪽을 뜯을 때는, i-1번째에 아무 것도 안 뜯은 것과 아래쪽을 뜯은 것 중 최댓값을 구하고 거기에 a[i][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
|
#include <iostream>
#include <algorithm>
using namespace std;
int a[2][100001];
int d[100001][3];
int main()
{
int t;
cin >> t;
while (t--) {
int n;
cin >> n;
for (int i = 0; i < 2; i++) {
for (int j = 1; j <= n; j++) {
cin >> a[i][j];
}
}
d[1][1] = a[0][1];
d[1][2] = a[1][1];
for (int i = 2; i <= n; i++) {
d[i][0] = max(d[i - 1][1], d[i - 1][2]);
d[i][1] = max(d[i - 1][0], d[i - 1][2]) + a[0][i];
d[i][2] = max(d[i - 1][0], d[i - 1][1]) + a[1][i];
}
cout << max(max(d[n][0], d[n][1]), d[n][2]) << '\n';
}
return 0;
}
|
cs |
* 포도주 시식 (mid) : 최대한 많은 포도주를 시식하는 경우를 구하는 문제다.
- 연속으로 3잔 이상은 못 마신다는 조건이 있다. 이 또한 마지막으로 마신 잔에 관심을 둔다.
- a[i]를 i번째 포도주의 양으로, d[i][j]를 i번째에 왔을 때, 연속해서 j번째 마신 경우의 최댓값이라고 생각해보자. 연속해서 3번 이상 마실 수 없기 때문에 j는 0~2라는 것을 알 수 있다. 이를 통해 점화식을 구해보면,
- d[i][0] = max(d[i-1][0], d[i-1][1], d[i-1][2]) , d[i][1] = d[i-1][0] + a[i] , d[i][2] = d[i-1][1] + a[i]
- i번째에 마시지 않는 경우는 i-1번 째에 올 수 있는 값의 최대를 구하면 된다. i번째에 연속 1번째 마시려면 i-1번째에 안 마셔야 하고 i번째에 마셔야 한다. 연속 2번째 마시려면 i-1번째에 마셔야 하고 i번째에도 마셔야 한다. 이 세 값 중의 최댓값이 포도수 시식의 최댓값이다.
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
|
#include <iostream>
#include <algorithm>
using namespace std;
int a[10001];
int d[10001][3];
int main()
{
int n;
cin >> n;
for (int i = 1; i <= n; i++)
cin >> a[i];
d[1][1] = a[1];
for (int i = 2; i <= n; i++) {
d[i][0] = max(max(d[i-1][0], d[i - 1][1]), d[i - 1][2]);
d[i][1] = d[i - 1][0] + a[i];
d[i][2] = d[i - 1][1] + a[i];
}
cout << max(max(d[n][0], d[n][1]), d[n][2]) << '\n';
return 0;
}
|
cs |
* 정수 삼각형 (low) : 양쪽 끝의 예외를 잘 처리해주고 계산하면 무난하게 풀린다.
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 <iostream>
#include <algorithm>
using namespace std;
int a[501][501];
int d[501][501];
int main()
{
int n;
cin >> n;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= i; j++) {
cin >> a[i][j];
}
}
d[1][1] = a[1][1];
for (int i = 2; i <= n; i++) {
for (int j = 1; j <= i; j++) {
d[i][j] = max(d[i - 1][j - 1], d[i - 1][j]) + a[i][j];
if (j == 1) d[i][j] = d[i - 1][j] + a[i][j];
if (j == i) d[i][j] = d[i - 1][j - 1] + a[i][j];
}
}
int dmax = 0;
for (int i = 1; i <= n; i++) {
if (d[n][i] > dmax)
dmax = d[n][i];
}
cout << dmax << '\n';
return 0;
}
|
cs |
* 가장 큰 증가 부분 수열 (mid) : 가장 긴 증가 수열은 구해봤으니 이제 합이 가장 큰 증가 수열을 구해보자
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
|
#include <iostream>
#include <algorithm>
using namespace std;
long long a[1001], d[1001];
int main()
{
int n;
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> a[i];
d[i] = 1;
}
d[1] = a[1];
int dmax = a[1];
for (int i = 2; i <= n; i++) {
d[i] = a[i];
for (int j = 1; j <= i; j++) {
if (a[j] < a[i] && d[i] < d[j] + a[i]) {
d[i] = d[j] + a[i];
}
}
if (dmax < d[i]) dmax = d[i];
}
cout << dmax << ' ';
return 0;
}
|
cs |
=> 가장 긴 증가 수열을 구할 때는 1씩 더해줬다면, 가장 큰 증가 수열을 구할 때는 현재 순서의 값을 더해준다.
* 가장 긴 감소하는 부분 수열 (mid) : 같은 맥락이다. 조금만 다르게 생각해주면 된다.
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 <iostream>
#include <algorithm>
using namespace std;
long long a[1001], d[1001];
int main()
{
int n;
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> a[i];
d[i] = 1;
}
int dmin = 1;
for (int i = 1; i <= n; i++) {
d[i] = 1;
for (int j = 1; j <= i; j++) {
if (a[j] > a[i] && d[i] < d[j] + 1) {
d[i] = d[j] + 1;
}
}
if (dmin < d[i]) dmin = d[i];
}
cout << dmin << '\n';
return 0;
}
|
cs |
=> a 값을 비교할 때 부등호 순서만 반대로 바꿨다. 감소 순이니까 뒤에서부터 비교해서 구하는 방법도 있다.
* 가장 긴 바이토닉 부분 수열 (mid) : 해답을 알고 너무 어이가 없었던 문제다.
- 바이토닉 부분 수열은 증가했다가 감소하는 모양의 함수다. 왼쪽에서부터 증가하다가 정점을 찍고 다시 내려가는 가장 긴 수열의 길이를 구하면 된다.
- 앞에서 구현했던 증가하는 함수의 정보와 감소하는 함수의 정보를 각각 따로 구한다. 그 다음 같은 인덱스에 위치한 두 값을 더한 값에 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
38
39
40
|
#include <iostream>
#include <algorithm>
using namespace std;
int a[1001], up[1001], down[1001];
int main()
{
int n;
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> a[i];
up[i] = 1; down[i] = 1;
}
for (int i = 2; i <= n; i++) {
for (int j = 1; j < i; j++) {
if (a[j] < a[i] && up[i] < up[j] + 1)
up[i] = up[j] + 1;
}
}
for (int i = n-1; i >= 1; i--) {
for (int j = n; j > i; j--) {
if (a[j] < a[i] && down[i] < down[j] + 1)
down[i] = down[j] + 1;
}
}
int ans = 0;
for (int i = 1; i <= n; i++) {
if (up[i] + down[i] - 1 > ans)
ans = up[i] + down[i] - 1;
}
cout << ans << '\n';
return 0;
}
|
cs |
* 연속합 2 (high) : 여기서 꽤나 고생했다. 한 개의 수를 제거할 수 있다는 조건을 생각하는 게 까다로웠다.
- 연속합은 이미 앞에서 구현한 바가 있다. 하지만 이 문제에서는 한 가지 수를 제거할 수 있다는 조건이 붙는다. 매우 작은 음수를 제거하면 연속합의 최댓값이 바뀔 수도 있는 것이다.
- a[i]에 수열의 값을 받고, d[i]에 첫번째부터 i번째까지 연속합의 최댓값을 넣어준다. 한 가지 수를 제거할 수 있다고 했기에, 만약 i번째가 제거 되었다면 첫번째부터 i-1번째까지 연속합의 최댓값과 i+1번째부터 n번째까지 연속합의 최댓값을 더한 값이 최댓값이 된다. 따라서 i번째부터 n번째까지 연속합의 최댓값을 넣어줄 d2[i]도 구해준다.
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
|
#include <iostream>
using namespace std;
int a[100001], d[100001], d2[100001];
int main()
{
int n;
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> a[i];
}
for (int i = 1; i <= n; i++) {
d[i] = a[i];
if (d[i - 1] + a[i] > a[i])
d[i] = d[i - 1] + a[i];
}
for (int i = n; i >= 1; i--) {
d2[i] = a[i];
if (i == n) continue;
if (d2[i + 1] + a[i] > a[i]) {
d2[i] = d2[i + 1] + a[i];
}
}
int ans = d[1];
for (int i = 2; i <= n; i++) {
if (d[i] > ans)
ans = d[i];
}
for (int i = 2; i <= n - 1; i++) {
if (d[i - 1] + d2[i + 1] > ans)
ans = d[i - 1] + d2[i + 1];
}
cout << ans << '\n';
return 0;
}
|
cs |
* 타일 채우기 (high) : 타일 채우기 문제 중에서 제일 어려웠다. 맨 끝의 경우 외에 특수한 경우도 생각해줘야 풀 수 있다.
- 3*N 크기의 벽을 2*1, 1*2 크기의 타일로 채우는 경우의 수를 구하는 문제다. 2*n이면 맨 끝에 올 경우만 생각해줘도 풀 수 있는데 3이 되니까 몇 가지를 더 생각해줘야 했다.
- 우선 맨 끝에 올 수 있는 경우는 그냥 짝대기 3개가 가로로 쌓인 모양, 짝대기 하나 위에 눕혀서 두고 아래에 두개를 세운 모양, 아래에 하나 깔고 위에 두개를 세운 모양으로 총 3가지로 나뉜다. (이 정도면 거의 글로 그림을 그렸다)
- d[i]를 3*i 크기의 벽을 채우는 경우의 수라고 할 때, d[i] = 3 * d[i-2] 인 것을 알 수 있지만 여기서 끝이 아니다. 3*n 모양에서는 특수한 경우가 있다. 이건 그림이 필요할 것 같다.
- 3 * 12 를 타일로 채운 예시다. 왼쪽에서 3~6번째를 보면 위에 두 타일이 누워있고 아래에 양쪽 타일이 누운 타일을 감싸는 모양이 있다. 위 아래가 바뀐 모양을 9~12번째에서 볼 수 있다. 4칸부터 시작해서 짝수 칸에 들어갈 수 있는 특수한 모양이 2개씩 생기는 것이다. 중간에 감싸지는 누운 타일이 옆으로 추가되는 흐름으로 4, 6, 8 ... 이런 주기로 특수한 모양을 만들 수 있다. 이를 반영해서 최종 점화식을 세우면,
- d[i] = 3 * d[i-2] + ∑ 2 * d[j] ( 0<= j <= i - 4, j는 짝수) 라고 생각할 수 있다.
- 좀 뜬금 없는데, 티스토리 코드 블록 항목을 이제 발견했다... (넌 진짜 대단하다) 지금 한번 써봐야겠다.
#include <iostream>
using namespace std;
long long d[31];
int main() {
int n;
cin >> n;
d[0] = 1;
for (int i = 2; i <= n; i += 2) {
d[i] = d[i - 2] * 3;
for (int j = i - 4; j >= 0; j -= 2) {
d[i] += d[j] * 2;
}
}
cout << d[n] << '\n';
return 0;
}
- 아니 이렇게 편한 방법이 있었는데 대체 뭐했냐 지금까지
- 음... 코드는 위에서 설명한 그대로다. d[0]가 1로 초기화 되어 있는데 아무 것도 없는 경우도 1개의 경우로 생각하기 위함이기도 하고 반복문을 돌리기 편하게 하기 위해서기도 하다. 직관적으로 생각하긴 좀 어려운 문제였다.
-
쓰고 보니 진짜 많은 문제를 푼 것 같다..
dp에 대해서 어느 정도 감은 잡았지만, 뭔가 dp 공부는 끝이 없을 것 같다.
주기적으로 해주지 않으면 까먹을 것 같기도 하고.. 감각을 잃지 않게 꾸준히 복습해줘야겠다.
다음은 dp의 마지막 강의인데 강의 이름이 '도전'이다. 엄청 어려운 문제가 나오려나보다.
하나라도 풀 수 있을지 도전해봐야긋다.
정리 끝!
'알고리즘 > 백준 알고리즘 강의' 카테고리의 다른 글
백준 알고리즘 기초 - 브루트 포스 (0) | 2020.07.15 |
---|---|
백준 알고리즘 기초 - 다이나믹 프로그래밍 (도전) (0) | 2020.07.12 |
백준 알고리즘 기초 - 다이나믹 프로그래밍 (예제2) (0) | 2020.07.09 |
백준 알고리즘 기초 - 다이나믹 프로그래밍 (예제1) (0) | 2020.07.09 |
백준 알고리즘 기초 - 다이나믹 프로그래밍 1 + 기초 예제 (0) | 2020.07.08 |