Coding Memo
최대 연속 부분 구간 합 문제 (여러가지 풀이) 본문
최대 연속 부분 구간 합 문제로 어떤 하나의 문제를 다양한 방법으로도 풀 수 있는 경우가 있다는 것을 확인하고 시간복잡도가 어떻게 차이가 나는지 확인해보자.
참고: 이 내용은 `프로그래밍 대회에서 배우는 알고리즘 문제해결전략 - 구종만 지음`에서 나온 내용을 일부 가져왔다.
문제
정수로 이루어진 1차원 배열에서 연속된 부분 구간 중 그 합이 최대인 구간의 합을 구하는 문제이다.
예를 들어 다음과 같인 배열이 있을 때, 연속된 부분 구간 중 최대 합은 12 이다.
[ -5, 10, -5, 7, 0, -3, 1, -5, -2, 5 ]
=> [ 10 , -5, 7 ]
=> 10 + (-5) + 7 = 12
풀이 1 : 모든 길이에 대해 모든 합 계산하기
첫번째 인덱스 부터 시작해서 길이가 1, 2, 3, ...인 연속된 구간의 합을 모두 구하는 (무식한?) 방법을 사용해보자.
const int MIN = numeric_limits<int>::min();
int getMaxSum(const vector<int>& v) {
const int s = v.size();
int ret = MIN;
// i번째 수부터 마지막까지 확인
for (int i=0; i<s; ++i) {
// 길이(j)를 하나씩 늘리기
for (int j=i; j<s; ++j) {
int sum = 0;
// [i..j]의 합 구하기
for (int k=i; k<j+1; ++k) {
sum += v[k];
}
ret = max(sum, ret);
}
}
return ret;
}
3중 반복문으로 시간 복잡도는 O(N^3) 이다.
풀이 2: 모든 합 계산하기
사실 이번 풀이는 풀이 1을 좀 더 간소화 한 것에 불과하다. 길이에 대한 반복문을 하나로 줄였다.
int getMaxSum2(const vector<int>& v) {
const int s = v.size();
int ret = MIN;
for (int i=0; i<s; ++i) {
int sum = 0;
// [i..j]에 대한 합 구하기
for (int j=i; j<s; ++j) {
sum += v[j];
ret = max(sum, ret);
}
}
return ret;
}
풀이 1번 보다 훨씬 빨라질 것이라고 확신할 수 있는 풀이 2이다. 시간 복잡도는 O(N^2)이다.
풀이 3: 분할 정복 (Divide and Conquer)
뭔가 이 문제에서 분할 정복을 떠오르기는 쉬워보이지 않지만 어차피 구간의 최대 합을 구하는 거면 그 구간을 잘게 쪼갠 다음 잘개 쪼갠 구간들 중 최대합을 각각 구하고 구간들을 다시 합쳐서 값을 비교하면 될 것이다.
즉, 좌-우로 쪼겐다고 했을 때,
0. 기저(base case)는 확인하려는 구간의 길이가 1일 때이다. (길이가 1일때까지 쪼갠다.)
1. left ~ mid와 mid+1 ~ right로 나눌 수 있을 것이다. (right는 배열의 길이가 아닌 마지막 index 번호이다.)
2. left ~ mid에서의 연속된 숫자의 최대합을 구하고 mid+1 ~ right에서도 마찬가지로 최대합을 구한다.
(여기서 주의 할 점은 나중에 좌-우를 합쳐서 최대합을 구하려고 할 때, 연속된 부분이어야 하므로, 왼쪽은 mid부터 시작하여 left까지 값을 더해야 하고, 오른쪽은 mid+1부터 right까지 값을 더하면서 최대합을 구해야 한다는 점이다.)
3. 1~2내용을 반복
4. 왼쪽부분에서의 연속된 구간 최대합(l)과 오른쪽 부분에서의 연속된 구간 최대합(r) 그리고 이 둘을 연속으로 이었을 때 나올 수 있는 최대값(l + r)을 서로 비교한다.
5. 위에서 비교한 값 중 가장 큰 것을 반환한다.
int getMaxSum3(const vector<int>& v, const int left, const int right) {
// 기저 사례 (길이가 1일때 그 자체가 최대합이다.)
if (left == right) return v[left];
int mid = (left + right) >> 1; // (left +right) / 2 와 같다.
int l = MIN; // 왼쪽 부분에서의 연속된 구간의 최대 합
int r = MIN; // 오른쪽 부분에서의 연속된 구간의 최대 합
int sum = 0; // 합을 계산할 임시 변수
// l + r 시에 연속된 배열이어야 하기 때문에 mid 부터 하나씩 내려감!
for (int i=mid; i>=left; --i) {
sum += v[i];
l = max(sum, l);
}
sum = 0;
for (int i=mid+1; i<=right; ++i) {
sum += v[i];
r = max(sum, r);
}
// 좌우 비교
int single = max(getMaxSum3(v, left, mid), getMaxSum3(v, mid+1, right));
// 합쳤을 때와 비교
return max(single, l + r);
}
시간복잡도는 O(NlogN)이 된다!!!
풀이 4: 점화식을 이용한 Down-Top 메모이제이션 (동적 계획법)
배열을 순차적으로 확인하여 구간 합의 최대 값을 찾으려고 할 때 조금만 더 생각해보자.
[a, b, c, d, e, ...] 라는 배열이 있다.
A를 a를 마지막으로 하는 구간의 최대합, B를 b를 마지막으로 하는 구간의 최대합... 이라고 했을 때,
끝을 포함하는 연속된 부분의 합과 끝 값 중 큰 값이 해당 값이 된다.
A = a
B = A+b > b ? A+b : b
C = B+c > c ? B+c : c
...
즉, I(i번째 원소를 끝으로 갖는 구간의 최대합)은 `(I-1) + v[i]`와 `v[i]`중 큰 값이 된다.
이를 점화식으로 나타낸다면,
Ai = max(Ai-1 + v[i], v[i]) =>
위 식을 코드로 표현하면 다음과 같다.
int getMaxSum4(const vector<int>& v) {
const int s = v.size();
int ret = MIN;
int psum = 0;
for(int i=0; i<s; ++i) {
psum = max(psum, 0) + v[i]; // psum = max(psum + v[i], v[i]);
ret = max(ret, psum); // 가장 큰 값 찾기
}
return ret;
}
무려 반복문 하나로 이 문제를 풀 수 있다!! (시간복잡도: O(N))
이게 얼마나 차이나는지는 시간복잡도에 대한 그래프가 말해줄 것이다.
아니면, 간단하게 생각해보자.
v의 길이가 1000이라고 했을 때,
풀이1은 1000^3 = 1000000000,
풀이2는 1000^2 = 10000000,
풀이3은 1000 * lg1000 ~~ 30000,
풀이4는 1000...
물론 v의 길이가 훨신 더 커지면 거의 N^3와 N 값은 정말 매우 큰 차이가 날 것이고, 이에 따라 시간도 오래 걸릴 것이다.
이처럼 어떤 문제를 푸는 데에 여러가지 방법이 존재할 수는 있지만, 그 여러가지 방법 중 가장 효율적인 방법을 찾아 고민하고 그렇게 문제를 해결하는 것이 우리들이 계속해서 해야하는 일이라고 생각한다.
'etc' 카테고리의 다른 글
C++ vs. C# ?? (0) | 2023.10.13 |
---|---|
빌드 시 이벤트 추가 (Visual Studio) (0) | 2023.10.10 |
[C#] Visual Studio 조건부 컴파일 심볼 설정 (0) | 2023.08.15 |
구글 프로토버퍼 (Google Protobuf) 패킷 설계 (0) | 2023.07.27 |
[C#] 에코 서버 만들기 (0) | 2023.07.03 |