Doby's Lab

[알고리즘] 백준 11659번: 구간 합 구하기 4 (C++), DP 개념 본문

PS/BOJ

[알고리즘] 백준 11659번: 구간 합 구하기 4 (C++), DP 개념

도비(Doby) 2021. 10. 5. 22:06

이 문제를 직관적으로 풀었을 때는 시간 초과가 발생한다. 구간이 예를 들어 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의 방식은 크게 '탑다운', '바텀업' 두 가지 방식으로 나뉜다.

다음 포스팅에서 이 두 가지의 차이점에 대해 알아봐야겠다.

728x90