LeetCode 53. Maximum Subarray
https://leetcode.com/problems/maximum-subarray/?envType=study-plan-v2&envId=top-interview-150
문제 설명
정수 배열 nums가 주어졌을 때 합이 가장 큰 부분 배열을 찾아서 그 합을 반환
예시 1
입력 : nums = [-2,1,-3,4,-1,2,1,-5,4]
출력 : 6
설명 :하위 배열 [4, -1, 2, 1]의 합이 가장 큽니다.
예시 2
입력 : nums = [1]
출력 : 1
설명 : 하위 배열 [1]의 합이 가장 큽니다.
예시 3
입력 : nums = [5,4,-1,7,8]
출력 : 23
설명 : 하위 배열 [5,4,-1,7,8]의 합이 가장 큽니다.
제약 조건
1 <= nums.length <= 105
-104 <= nums [i] <= 104
풀이 전략
1. 완전 탐색으로 모든 구간 합 구하기
- 인덱스 i <= j에 대해 nums[i...j]의 합을 계산
- 세 중첨 루프가 필요하므로 시간복잡도가 O(N³)으로 비효율적
2. 누적합을 구한 뒤 모든 구간 합 구하기
- 구간 합을 루프로 매번 구할 필요가 있을까?
- prefix(누적합)을 구한 뒤 구간 합 = prefix[j + 1] - prefix[i]로 계산
- 두 중첩 루프가 필요하므로 시간복잡도가 O(N²), 더 효율적인 방법은 없을까?
3. Kadane 알고리즘(1차원 DP)
- 이전까지의 최대 구간 합이 음수라면 현재 원소에 이어 붙일 필요가 있을까? -> 없다!!!
- 이전 합 + 현재가 손해라면 구간을 새로 시작
- 예시 : nums = [-2,1,-3,4,-1,2,1,-5,4]
- -2는 1에 붙이면 손해이므로 1부터 구간 새로 시작
- 1은 -3에 붙이면 이득이므로 지속, 현재 구간합 -2
- 현재 구간합 -2는 4에 붙이면 손해이므로 4부터 구간 새로 시작
- 시간복잡도가 O(N)으로 효율적, 공간복잡도도 O(1)로 효율적
4. 분할 정복
- 구간 합을 구하는 정보를 유지하면서 한 번의 순회로 해결할 수는 없을까?
- sum : 현재 구간 전체 합계
- prefix : 앞에서부터 더했을 때 제일 큰 합
- suffix : 뒤에서부터 더했을 때 제일 큰 합
- best : 모든 경우 최대 연속 구간 합(왼쪽 구간 최대, 오른쪽 구간 최대, 왼쪽 구간 뒤 + 오른쪽 구간 앞 이어 붙였을 때)
- 분할 후 병합 시 L.best, R.best, L.suffix + R.prefix 중 가장 큰 값 구하기
- 예시 : nums = [2, -1, 3, -2]
- 분할 1 : [2, -1] / [3, -2]
- 왼쪽 분할 및 병합 : [2] / [-1] => sum = 1, prefix = max(2, 2 - 1), suffix = max(-1, -1 + 2), best = 2
- 오른쪽 분할 및 병합 : [3] / [-2] => sum = 1, prefix = max(3, 3 - 2), suffix = max(-2, -2 + 3), best = 3
- 분할 1 병합
- sum = L.sum(1) + R.sum(1) = 2
- prefix = L.prefix(2), L.sum(1) + R.prefix(3) 중 최대 4
- suffix = R.suffix(1), R.sum(1) + L.suffix(1) 중 최대 2
- best = L.best(2), R.best(3), L.suffix(1) + R.prefix(3) 중 최대 4
- 분할 정복은 절반씩 나누므로 재귀 호출의 트리 깊이는 logN, 전체 배열의 크기 N만큼 병합해야 하므로 시간복잡도는 O(NlogN)
풀이
Kadane 알고리즘(1차원 DP)
class Solution {
public int maxSubArray(int[] nums) {
int cur = 0;
int best = Integer.MIN_VALUE;
for (int x : nums) {
// 이전 합 + 현재가 손해라면 구간을 현재값부터 새로 시작
cur = Math.max(x, cur + x);
best = Math.max(best, cur);
}
return best;
}
}
분할 정복
class Solution {
private static class Info {
// 구간 합, 앞부터 더했을 때 최대, 뒤부터 더했을 때 최대, 헌재 최대 구간 합
int sum, pref, suff, best;
Info(int v) {
sum = pref = suff = best = v;
}
Info(int sum, int pref, int suff, int best) {
this.sum = sum;
this.pref = pref;
this.suff = suff;
this.best = best;
}
}
public int maxSubArray(int[] nums) {
return divide(nums, 0, nums.length - 1).best;
}
private Info divide(int[] a, int l, int r) {
if (l == r) return new Info(a[l]);
int m = l + (r - l) / 2;
Info left = divide(a, l, m);
Info right = divide(a, m + 1, r);
return merge(left, right);
}
private Info merge(Info L, Info R) {
int sum = L.sum + R.sum;
int pref = Math.max(L.pref, L.sum + R.pref);
int suff = Math.max(R.suff, R.sum + L.suff);
int best = Math.max(Math.max(L.best, R.best), L.suff + R.pref);
return new Info(sum, pref, suff, best);
}
}
LeetCode 918. Maximum Sum Circular Subarray
문제 설명
길이 n의 원형 정수 배열 nums가 주어졌을 때 비어 있지 않은 수의 부분 배열의 최대 합을 반환
- 원형 배열은 배열의 끝이 배열의 시작 부분에 연결되는 것을 의미
- 부분 배열은 각 요소를 최대 한 번만 포함할 수 있음
예시 1
입력 : nums = [1,-2,3,-2]
출력 : 3
설명 :하위 배열 [3]의 합이 가장 큽니다.
예시 2
입력 : nums = [5,-3,5]
출력 : 10
설명 : 하위 배열 [5, 5]의 합이 가장 큽니다.
예시 3
입력 : nums = [-3,-2,-3]
출력 : -2
설명 : 하위 배열 [-2]의 합이 가장 큽니다.
제약 조건
n == nums.length
1 <= n <= 30,000
- 30,000 <= nums [i] <= 30,000
풀이 전략
1. 완전 탐색으로 모든 구간 합 구하기
- 모든 시작 인덱스 i, 끝 인덱스 j에 대해 nums[i...j]의 합을 계산
- 세 중첨 루프가 필요하므로 시간복잡도가 O(N³)으로 1초 내 실행 불가
2. 누적합을 구한 뒤 모든 구간 합 구하기
- 원형 배열을 간단하게 표현할 수는 없을까?
- 자기 자신을 이어 붙인 2n 길이의 배열 만들기(같은 요소는 한번만 사용할 수 있으므로 충분)
- 구간 합을 루프로 매번 구할 필요가 있을까?
- prefix(누적합)을 구한 뒤 구간 합 = prefix[j + 1] - prefix[i]로 계산
- 두 중첩 루프가 필요하므로 시간복잡도가 O(N²)으로 1초 내 실행 불가
3. Kadane 알고리즘(1차원 DP)
- 원형 배열의 최대는 어떤 경우가 있을까?
- Case 1 : 최대 구간이 한바퀴를 돌지 않음 -> 최대 부분합
- Case 2 : 최대 구간이 한바퀴를 돔(배열의 시작과 끝을 포함) -> 전체합 - 최소 부분 합
- 최대 부분합과 최소 부분합은 Kadane 알고리즘을 사용
- 단, 모든 원소가 음수일 때 Case 2의 경우 항상 0이 되므로 Case 1 결과를 사용해야 함에 주의
- 이전 합 + 현재가 손해라면 구간을 새로 시작
- 예시 : nums = [5, -3, 5]
- Case 1 : 최대 부분합 5
- Case 2 : 전체 합(7) - 최소 부분 합(-3) = 10
- Case1이 더 크므로 Case2의 10이 정답
- 시간복잡도가 O(N)으로 효율적, 공간복잡도도 O(1)로 효율적
풀이
Kadane 알고리즘(1차원 DP)
class Solution {
public int maxSubarraySumCircular(int[] nums) {
int maxKadane = Integer.MIN_VALUE, curMax = 0;
int minKadane = Integer.MAX_VALUE, curMin = 0;
int total = 0;
for (int x : nums) {
// 최대 부분합
curMax = Math.max(x, curMax + x);
maxKadane = Math.max(maxKadane, curMax);
// 최소 부분합
curMin = Math.min(x, curMin + x);
minKadane = Math.min(minKadane, curMin);
total += x;
}
// 모두 음수인 경우 처리
if (total == minKadane) return maxKadane;
// 한바퀴를 돌지 않는 경우와 도는 경우 중 최대를 반환
return Math.max(maxKadane, total - minKadane);
}
}
팁
- 문제 풀이 : 최대 부분합, 최소 부분합을 요구 -> Kadane 알고리즘
- 실무 활용
- Kadane 알고리즘 : 최대 손익 구간, 최대 트래픽 구간
- 원형 패턴 : 순환되는 데이터(24시간, 순환 라운드)
'코딩테스트 > 알고리즘' 카테고리의 다른 글
예제로 배우는 알고리즘 - 다차원 DP (3) | 2025.06.29 |
---|---|
예제로 배우는 알고리즘 - 1D DP (3) | 2025.06.13 |
예제로 배우는 알고리즘 - 그래프 BFS (0) | 2025.06.06 |
예제로 배우는 알고리즘 - 힙 (0) | 2025.05.25 |
예제로 배우는 알고리즘 - 스택 (0) | 2025.05.18 |