티스토리 뷰
문제 링크
https://programmers.co.kr/learn/courses/30/lessons/81305
코딩테스트 연습 - 시험장 나누기
3 [12, 30, 1, 8, 8, 6, 20, 7, 5, 10, 4, 1] [[-1, -1], [-1, -1], [-1, -1], [-1, -1], [8, 5], [2, 10], [3, 0], [6, 1], [11, -1], [7, 4], [-1, -1], [-1, -1]] 40
programmers.co.kr
풀이
자바스크립트로 풀었는데 큰 문제가 있었습니다. 제출 시 몇몇 효율성 테케에서 의문의 런타임 에러를 뱉더라구요.
최대 범위 테스트 케이스를 따로 만들어 로컬에서 돌려보니 다음 에러가 원인이었습니다.
RangeError: Maximum call stack size exceeded
그래프 최대 깊이가 1만 정도 되는데, 이를 재귀 함수로 구현하다 보니 콜 스택이 터져나가 버리는 상황이었습니다. 파이썬은 스택 크기 조절이 되는데 왜 JS는... ㅜㅜ
꼬리 재귀 최적화를 시도해봤지만 포스트 오더로 구현한지라 리턴 값을 가지고 연산을 해야 하기 때문에 먹히질 않아서 그냥 깡 스택 반복문으로 유사 재귀 함수를 구현해서 해결했습니다. 진짜 슬프네요 이건..
문제를 더 간단하게 만들기
이진트리가 주어지고, k개의 그룹으로 나눴을 때 각 그룹 인원수 중 최댓값이 최소가 되는 값을 구해야 합니다.
효율성을 생각하면 풀이가 감도 안오죠? 좀 다르게 생각해봅시다.
만약 인원 최댓값이 주어지고(= 고정시킨다), 이 최댓값을 넘기지 않고 적절히 그룹을 만들었을 때 최적의 그룹 수를 구할 수 있다고 합시다.
그럼 가능한 모든 최댓값을 대입해서 그룹의 수가 k이하가 되는 값 중 가장 작은 값을 정답으로 취하면 됩니다.
이런 방식을 조금 어려운 말로 "최적화 문제를 결정 문제로 바꾼다"라고 합니다.
결정 문제로 바꿔도 아직 문제가 남아있습니다. 확인해봐야 할 유효한 인원 최댓값의 범위가 너무 큽니다.
가능한 범위의 최소는 트리의 정점 중 가장 큰 가중치 값이고, 최대로는 모든 정점의 가중치 합이 될 수 있습니다.
최소 1부터 최대 100,000,000 까지인데 1억개의 후보를 다 확인하기엔 시간이 너무 오래 걸립니다. 최적 그룹 수를 구하는데도 시간이 드니까요.
이를 해결하기 위해 이분 탐색을 이용할 수 있습니다. 이분 탐색을 사용하면 O(log2108) 정도의 복잡도로 후보를 쳐낼 수 있습니다.
최적 그룹 수 구하기
최댓값(limit)을 고정한 상태에서 최적 그룹의 수는 다음처럼 구할 수 있습니다.
1) 포스트 오더로 트리를 순회합니다.
2) 각 순회의 리턴 값은 { [아래에서부터 그룹을 지어왔을 때, 현재까지의 코스트 합], [현재까지의 최적 그룹 수] }입니다.
3) 왼쪽, 오른쪽 자식의 정보를 바탕으로 현재 정점을 어떻게 그룹 지을지 결정합시다.
3-1) 만약 두 자식과 현재 정점의 코스트 합이 limit 이하라면, 현재 정점도 그룹에 포함시키면 됩니다.
3-2) 만약 한쪽 자식이랑만 그룹을 지을 수 있다면, 추가되는 그룹의 수는 같으니 코스트가 더 적은 쪽과 그룹 지으면 됩니다. 추가되는 그룹 수는 1입니다.
3-3) 어떤 자식과도 그룹 지을 수 없다면, 자식들과 그룹지을 수 없으니 그룹의 수를 2 올려줍니다.
4) 루트 정점의 그룹수를 반환하여 판단해줍시다.
DP라고 의식해서 풀진 않았는데 풀이를 쓰고 보니 그냥 DP였네요. 리턴 값을 dp 배열의 값이라고 생각하시면 되겠습니다.
정답 코드
function solution(k, num, links) { const tree = links.reduce((tree, v, i) => { tree[i] = { cost: num[i], l: v[0], r: v[1], }; return tree; }, {}); const n = num.length; const root = links.reduce((root, v) => { const [a, b] = v; if (a > 0) root -= a; if (b > 0) root -= b; return root; }, n * (n - 1) / 2); const go = limit => { const callStack = [root]; const returnValues = { "-1": { cost: 0, cnt: 0, } }; callStack.back = function () { return this[this.length - 1];}; while (callStack.length) { const now = callStack.back() , {cost, l, r} = tree[now] , lRes = returnValues[l] , rRes = returnValues[r]; if (!lRes) { callStack.push(l); continue; } if (!rRes) { callStack.push(r); continue; } callStack.pop(); returnValues[now] = { cost, cnt: returnValues[l].cnt + returnValues[r].cnt, } const ret = returnValues[now]; const underLimit = (...nodes) => { const sum = nodes.reduce((sum, v) => sum += v.cost, 0); return sum <= limit; } if (underLimit(ret, lRes, rRes)) { ret.cost += lRes.cost + rRes.cost; } else if (underLimit(ret, lRes) || underLimit(ret, rRes)) { ret.cost += Math.min(lRes.cost, rRes.cost); ret.cnt += 1; } else { ret.cnt += 2; } } return returnValues[root]; }; const getAnswer = () => { let l = Math.max(...num), r = 11111 * n; let ans = r; while (l <= r) { const m = Math.floor((l + r) / 2); if (go(m).cnt <= k - 1) { r = m - 1; ans = Math.min(ans, m); } else { l = m + 1; } } return ans; }; return getAnswer(); }
'알고리즘 > 문제 풀이' 카테고리의 다른 글
[BOJ] 백준 22991 - 수요응답형 버스 (SUAPC 2021 Summer) (0) | 2021.09.12 |
---|---|
[BOJ] 백준 4828 - XML (0) | 2021.08.15 |
[프로그래머스] 미로 탈출 (2021 카카오 채용연계형 인턴십) (3) | 2021.07.12 |
[프로그래머스] 표 편집 (Javascript, 2021 카카오 채용연계형 인턴십) (1) | 2021.07.12 |
[프로그래머스] 거리두기 확인하기 (Javascript, 2021 카카오 채용연계형 인턴십) (0) | 2021.07.12 |