점차 어려운 난이도에 익숙해지고자, 프로그래머스에서 lv4 문제를 풀어봤어요.
lv4
치고는 생각보다 난이도는 어렵지 않았는데, 문제의 조건이 쉽게 주어져서 그랬던 것 같아요 🙆🏻
어떤 문제였는지, 어떻게 포인트를 잡고 풀어나갔는지를 한 번 살펴보죠!
일단, 저는 다음과 같은 흐름으로 문제의 요지를 파악해나갔어요.
l번 바구니부터 m번 바구니까지, 둘째 아들에게는 m+1번 바구니부터 r번 바구니까지를 주려합니다.
여기서 든 생각은, '음... 기준점을 따로 잡아야겠네.' 였습니다.
알고리즘을 풀 때 제가 접근하는 방법은, 내가 어떤 문제를 해결하기 위해 우선적으로 무엇을 구해야 하는지이기 때문에 저는 다음과 같은 핵심 문제를 정의했습니다.
문제의 해에 도달하기 위한
l
m
r
의 최적의 값은 무엇인가.
그렇다면 이제 해당 값을 찾아내기 위해 조건 값이 있겠죠?
이를 한 번 탐색해나가봅시다.
단, 두 아들이 받을 과자 수는 같아야 합니다(1 <= l <= m, m+1 <= r <= N). 즉, A[i] 를 i번 바구니에 들어있는 과자 수라고 했을 때, A[l]+..+A[m] = A[m+1]+..+A[r] 를 만족해야 합니다.
오...! 그냥 두 형제가 받을 값이 같으면 되겠네요.
즉 다음과 같이 세울 수 있겠죠?
[[l ~ m번째의 합]]
=[[ m + 1 ~ r번째의 합]]
따라서 저는 이 두 조건을 갖고 해결 방법을 탐색했어요.
일단 저는 가장 눈여겨 봤던 알고리즘은 바로 구간합을 사용할 수 있지 않을까였어요.
결국 각 구간의 합을 지속해서 계산하는 것은 비효율적인 것 같아서, 구간합 알고리즘을 우선 사용했습니다. 구현도 간단하니까요!
// 구간합을 구하는 함수 생성
const getPrefixSum = (arr) => {
const arrLength = arr.length;
const arrPrefixSum = new Array(arrLength + 1).fill(0);
for (let i = 0; i < arrLength; i += 1) {
const now = arr[i];
arrPrefixSum[i + 1] = now + arrPrefixSum[i];
}
return arrPrefixSum;
};
참고로 저는, i + 1번째까지 만들어서 memoization을 쉽게 하도록 만들었답니다 👐🏻 혹여나 참고해주세요!
그러면 이제 구간합을 구했으니, l
m
r
을 찾아야겠군요?
저는 여기서 투 포인터를 이용했어요.
👀 잠깐, 투 포인턴데, 이건 3개의 변수를 업데이트해야 하는데요?
그럼 쓰리 포인터로... 크... 크흡
여기서 저는 투 포인터를 좀 특이하게 쓰려고 합니다. 바로 for
문을 한 번 wrapping해주는 건데요.
어차피 기준점은 선상에서 존재하며, 여기서 l
r
이 유동적으로 움직입니다. 그리고 기준점을 토대로 서로 그 범위를 넘길 수 없죠.
따라서 m
을 기준으로 for
문을 돌립니다. 이때, m + 1
이 넘어가지 않도록, cookie.length - 1
까지만 돌아가면 되겠죠?
for (let m = 0; m < cookieLength - 1; m += 1) {
const leftEnd = m;
const rightStart = m + 1;
let leftStart = leftEnd; // l
let rightEnd = rightStart; // r
...
}
자, 이렇게 하면 이제 우리는 저 투 포인터를 요리조리 돌려가며, 문제의 조건에 부합하는 답을 찾으면 됩니다.
일단 위의 반복문 안에, 왼쪽의 구간합과 오른쪽의 구간합을 비교하는 로직을 써줍시다.
let nowLeftSum = cookiePrefixSum[m + 1] - cookiePrefixSum[leftStart];
let nowRightSum = cookiePrefixSum[rightEnd + 1] - cookiePrefixSum[m + 1];
그러면 이제 이 값을 토대로 왼쪽을 갈지, 오른쪽을 갈지 판단해주면 되는데요.
아! 여기서 제가 설명해주지 않은 것이 있어요.
이 투 포인터가 이렇게 간단하게 가능한 이유는 cookie의 각각의 원소는 1 이상 500 이하인 자연수입니다.
라는 조건이 있기 때문입니다.
만약 자연수가 아니라면, 어느 쪽을 움직여야할지 감이 안잡히므로, 해시라도 써서 이 답을 구해야 했을 것이고, 더 복잡해졌을 거에요!
다행히도 문제의 제한 사항이 친절했기에, 감지덕지하면서 구해주면 되겠네요 😁
if (nowLeftSum === nowRightSum) {
result = Math.max(nowLeftSum, result);
rightEnd += 1;
leftStart -= 1;
} else if (nowLeftSum < nowRightSum) {
leftStart -= 1;
} else {
rightEnd += 1;
}
음...
O(N)
for
문에서 O(N)
O(N)
(최악의 경우)총 O(N) + O(N) * O(N) = O(N^2)
겠군요!
// 구간합을 구하는 함수 생성
const getPrefixSum = (arr) => {
const arrLength = arr.length;
const arrPrefixSum = new Array(arrLength + 1).fill(0);
for (let i = 0; i < arrLength; i += 1) {
const now = arr[i];
arrPrefixSum[i + 1] = now + arrPrefixSum[i];
}
return arrPrefixSum;
};
const solution = (cookie) => {
if (cookie.length === 1) return 0;
let result = 0;
const cookiePrefixSum = getPrefixSum(cookie);
const cookieLength = cookie.length;
// 기준점 이동시키기
for (let m = 0; m < cookieLength - 1; m += 1) {
const leftEnd = m;
const rightStart = m + 1;
let leftStart = leftEnd;
let rightEnd = rightStart;
while (leftStart >= 0 && rightEnd < cookieLength) {
let nowLeftSum = cookiePrefixSum[m + 1] - cookiePrefixSum[leftStart];
let nowRightSum = cookiePrefixSum[rightEnd + 1] - cookiePrefixSum[m + 1];
if (nowLeftSum === nowRightSum) {
result = Math.max(nowLeftSum, result);
rightEnd += 1;
leftStart -= 1;
} else if (nowLeftSum < nowRightSum) {
leftStart -= 1;
} else {
rightEnd += 1;
}
}
}
return result;
};
짜잔. 효율성에서 큰 걱정 없이 통과하는 군요. 어썸합니다. 😉
최근에 심했던 슬럼프를 이겨내고, 다시 문제를 푸니 더 잘 되는 것 같아요.
앞으로도 꾸준히 공부하는 과정들을 업로드하려 합니다!
방법이 궁금했던 분들께 좋은 코드였길 바라며, 그럼 즐거운 코딩하시길 바라요. 🚀