일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- dfs
- DP
- 가끔은_말로
- dropout
- 알고리즘
- 이분 탐색
- 크루스칼
- 자바스크립트
- back propagation
- 2023
- object detection
- 세그먼트 트리
- 플로이드 와샬
- pytorch
- 미래는_현재와_과거로
- 우선 순위 큐
- 문자열
- 조합론
- lazy propagation
- 너비 우선 탐색
- BFS
- 분할 정복
- 회고록
- 가끔은 말로
- tensorflow
- 백트래킹
- c++
- 다익스트라
- Overfitting
- NEXT
- Today
- Total
Doby's Lab
[알고리즘] 백준 11659번: 구간 합 구하기 4 (C++), DP 개념 본문
이 문제를 직관적으로 풀었을 때는 시간 초과가 발생한다. 구간이 예를 들어 1~100,000이고, 합을 구하는 횟수가 1~100,000이라면 어마한 양의 시간이 발생하기 때문에 다른 방법을 생각해내야 한다.
DP (Dynamic Programming) 개념
DP란 큰 문제를 해결하기 위해 작은 문제들로 나누어 작은 문제들을 해결하여 큰 문제들 해결할 수 있도록 도와주는 알고리즘이다.
예를 들어 피보나치 수가 DP의 대표적인 문제라고 할 수 있다.
피보나치 4를 구한다고 생각해보자.
일반적으로 코드는 이렇게 짤 것이다.
#include <iostream>
using namespace std;
int fibo(int n) {
if (n == 0) {
return 0;
}
else if (n == 1) {
return 1;
}
else {
return fibo(n - 1) + fibo(n - 2);
}
}
int main() {
int n;
cin >> n;
cout << fibo(n);
return 0;
}
피보나치 4를 구하는 과정에서 fibo(2)가 두 번 호출된다.
'겨우 2번인데 DP를 배워야 하나'라고 생각할 수 있지만 이건 아주 극단적으로 작은 예이다. 피보나치 100,000을 구한다고 하면 피보나치 2가 몇 번 수행되고, 피보나치 3이 몇 번 수행되고를 생각한다면 엄청 낭비적인 코드구나 라고 생각할 수 있다.
한 번 호출된 값은 어디에 저장해 두고 다시 부를 때 함수를 호출하지 않고, 저장된 값을 가져온다면 어떨까?
그렇게 되면 더 효율적인 코드를 만들 수 있다.
이 기법을 메모이제이션(Memoization)이라고 한다.
이 메모이제이션을 코드로 나타내면 이렇다.
#include <iostream>
using namespace std;
int cache[46];
int fibo(int n) {
if (n == 0) {
return 0;
}
else if (n == 1) {
return 1;
}
else {
if (cache[n] != 0) {
return cache[n];
}
else {
return cache[n] = fibo(n - 1) + fibo(n - 2);
}
}
}
int main() {
int n;
cin >> n;
cout << fibo(n);
return 0;
}
cache라는 배열에서 피보나치 2가 한 번 호출되었다면 cache의 역할이 '아직 구하지 않았으니 한 번이라도 구해두면 내가 알아두고 나중에 귀찮게 함수 호출 안 하게 해 주겠다'라고 생각할 수 있다.
[그림으로 이해]
점화식
DP가 어떻게 돌아가는지 알았다면 DP에서 제일 중요한 점화식을 알아야 한다.
점화식이란 '수열의 항(項) 사이에 성립하는 관계식' (출처: 위키백과)라는 것이다.
말 그대로 '피보나치 N'을 구한다고 했을 때, 점화식은
fibo(N) = fibo(N - 1) + fibo(N - 2)
라는 관계식이라 볼 수 있다.
fibo(N)이라는 큰 문제를 fibo(N - 1)과 fibo(N - 2)로 나누어 작은 문제들을 풀어가며 최종적으로 큰 문제를 해결해나가는 것을 볼 수 있다.
즉, DP에서 가장 중요하다고 생각하는 것이 '큰 문제가 무엇인지 > 어떻게 작은 문제로 분할할지 (점화식을 세울지)'가 중요한 포인트가 될 거 같다.
다시 문제로 돌아와서
이번 문제는 위에서 말한 대로 직관적으로 풀었다가는 시간 초과가 발생한다.
그렇다면 이런 풀이는 어떨까?
cache 배열에 1번째 항부터 N번째 항까지 담고, 어떠한 구간 a와 b가 주어지면 'cache[b] - cache[a - 1]'을 통해 구할 수 있지 않을까?
큰 문제:
a~b구간까지의 합
작은 문제:
1~a-1구간까지의 합, 1~b구간까지의 합
[코드]
#include <iostream>
#include <vector>
using namespace std;
int cache[100001] = { 0, };
int main() {
ios_base::sync_with_stdio(false);
cin.tie(0);
int n, m;
cin >> n >> m;
vector<int> arr(n + 1, 0);
for (int i = 1; i <= n; i++) {
int a;
cin >> a;
arr[i] = a;
cache[i] = a + cache[i - 1];
}
for (int i = 0; i < m; i++) {
int a, b;
cin >> a >> b;
cout << cache[b] - cache[a - 1] << '\n';
}
return 0;
}
이번 문제를 통해 DP가 무엇인지 어떻게 돌아가는 알고리즘인지 알 수 있었다.
DP의 방식은 크게 '탑다운', '바텀업' 두 가지 방식으로 나뉜다.
다음 포스팅에서 이 두 가지의 차이점에 대해 알아봐야겠다.
'PS > BOJ' 카테고리의 다른 글
[자료구조] 백준 11279번: 최대 힙 (C++), 우선 순위 큐 사용 (0) | 2021.10.07 |
---|---|
[알고리즘] 백준 14403번: 경로 찾기 (C++), 경우의 수 조건 (0) | 2021.10.06 |
[알고리즘] 백준 2178번: 미로탐색(C++), DFS와 BFS의 차이 (0) | 2021.10.05 |
[알고리즘] 백준 5913번: 준규와 사과 (C++), 배운 게 많은 문제 (0) | 2021.10.02 |
[알고리즘] 백준 2417번: 정수 제곱근 (C++), 더 큰 정수 자료형 unsigned long long (0) | 2021.10.01 |