문제
과제를 받은 루는 다음과 같은 순서대로 과제를 하려고 계획을 세웠습니다.
- 과제는 시작하기로 한 시각이 되면 시작합니다.
- 새로운 과제를 시작할 시각이 되었을 때, 기존에 진행 중이던 과제가 있다면 진행 중이던 과제를 멈추고 새로운 과제를 시작합니다.
- 진행중이던 과제를 끝냈을 때, 잠시 멈춘 과제가 있다면, 멈춰둔 과제를 이어서 진행합니다.
- 만약, 과제를 끝낸 시각에 새로 시작해야 되는 과제와 잠시 멈춰둔 과제가 모두 있다면, 새로 시작해야 하는 과제부터 진행합니다.
- 멈춰둔 과제가 여러 개일 경우, 가장 최근에 멈춘 과제부터 시작합니다.
과제 계획을 담은 이차원 문자열 배열
plans
가 매개변수로 주어질 때, 과제를 끝낸 순서대로 이름을 배열에 담아 return 하는 solution 함수를 완성해주세요.제한사항
- 3 ≤
plans
의 길이 ≤ 1,000 plans
의 원소는 [name, start, playtime]의 구조로 이루어져 있습니다.- name : 과제의 이름을 의미합니다.
- 2 ≤ name의 길이 ≤ 10
- name은 알파벳 소문자로만 이루어져 있습니다.
- name이 중복되는 원소는 없습니다.
- start : 과제의 시작 시각을 나타냅니다.
- "hh:mm"의 형태로 "00:00" ~ "23:59" 사이의 시간값만 들어가 있습니다.
- 모든 과제의 시작 시각은 달라서 겹칠 일이 없습니다.
- 과제는 "00:00" ... "23:59" 순으로 시작하면 됩니다. 즉, 시와 분의 값이 작을수록 더 빨리 시작한 과제입니다.
- playtime : 과제를 마치는데 걸리는 시간을 의미하며, 단위는 분입니다.
- 1 ≤ playtime ≤ 100
- playtime은 0으로 시작하지 않습니다.
- 배열은 시간순으로 정렬되어 있지 않을 수 있습니다.
- 진행중이던 과제가 끝나는 시각과 새로운 과제를 시작해야하는 시각이 같은 경우 진행중이던 과제는 끝난 것으로 판단합니다.
입출력 예
plans | result |
[["korean", "11:40", "30"], ["english", "12:10", "20"], ["math", "12:30", "40"]] | ["korean", "english", "math"] |
[["science", "12:40", "50"], ["music", "12:20", "40"], ["history", "14:00", "30"], ["computer", "12:30", "100"]] | ["science", "history", "computer", "music"] |
[["aaa", "12:00", "20"], ["bbb", "12:10", "30"], ["ccc", "12:40", "10"]] | ["bbb", "ccc", "aaa"] |
입출력 예 설명
입출력 예 #1
"korean", "english", "math"순으로 과제를 시작합니다. "korean" 과제를 "11:40"에 시작하여 30분 후인 "12:10"에 마치고, 즉시 "english" 과제를 시작합니다. 20분 후인 "12:30"에 "english" 과제를 마치고, 즉시 "math" 과제를 시작합니다. 40분 후인 "01:10"에 "math" 과제를 마칩니다. 따라서 "korean", "english", "math" 순으로 과제를 끝내므로 차례대로 배열에 담아 반환합니다.
입출력 예 #2
"music", "computer", "science", "history" 순으로 과제를 시작합니다.
시각 | 진행 중 과제 | 잠시 멈춘 과제 | 설명 |
"12:20" | "music" | [ ] | "music"을 시작합니다. |
"12:30" | "computer" | ["music"] | "music"을 잠시 멈추고(남은 시간 30분) "computer"를 시작합니다 |
"12:40" | "science" | ["music", "computer"] | "computer"를 잠시 멈추고(남은 시간 90분) "science"를 시작합니다 |
"13:30" | "computer" | ["music"] | "science"를 끝내고 가장 최근에 멈춘 "computer"를 다시 시작합니다 |
"14:00" | "history" | ["music", "computer"] | "computer"를 잠시 멈추고(남은 시간 60분) "history"를 시작합니다 |
"14:30" | "computer" | ["music"] | "history"를 끝내고 가장 최근에 멈춘 "computer"를 다시 시작합니다" |
"15:30" | "music" | [ ] | "computer"를 끝내고 가장 최근에 멈춘 "music"을 다시 시작합니다" |
"16:00" | - | [ ] | "music"을 끝냅니다 |
따라서 ["science", "history", "computer", "music"] 순서로 과제를 마칩니다.
풀이
재영
다음과 같은 two-track으로 문제를 접근 및 해결했어요.
- 아직
FP
가 익숙하지는 않으니, 기존에 짜던 대로 문제를 해결한다.
FP
로 문제를 접근하여 해결하자.
함수형 이전
해답
const getTime = (time) => { return time .split(':') .reduce((acc, cur, index) => acc + (!index ? 60 * +cur : +cur), 0); }; function solution(plans) { const result = []; const sortedPlans = plans .map(([name, time, period]) => [name, getTime(time), +period]) .sort((a, b) => a[1] - b[1]); const stack = []; sortedPlans.forEach((plan, index) => { let breakTime = index > 0 ? plan[1] - sortedPlans[index - 1][1] : 0; while (stack.length) { const [nowName, nowTime, nowPeriod] = stack.pop(); const remainTime = nowPeriod - breakTime; if (remainTime <= 0) { breakTime -= nowPeriod; result.push(nowName); } else { stack.push([nowName, nowTime, remainTime]); break; } } stack.push(plan); }); while (stack.length) { const now = stack.pop(); result.push(now[0]); } return result; }
고민
while
문과for
문이 많이 혼합되어 있다 → 어떻게 함수형으로 제어할 수 있을까?
- 비즈니스 로직(
과제를 진행하는 조건
)과 일반적인 유틸 함수로 기능할 수 있는 로직(예컨대 이러한 비슷한 로직으로 반복하는 것을 스택으로 처리하는 로직)을 분리할 수 있을까?
- 함수형으로 풀이를 할 때, 조건문으로 인한 블록 스코프의 참조로 인해 함수형 풀이가 쉽지 않다. 어떻게 해결할 수 있을까?
FP로 풀이
- 재귀를 사용하여
for
과while
를 사용하지 않고 해결
stack
과result
를 반환하는 콜백 역시closure
를 이용함으로써 유연하게 받을 수 있도록 처리
flag
를 함수로 넘겨주는 방식을 통해 스코프를 flat하게 가져가면서도 원하는 데이터를 변수에 담을 수 있었다.
const getTime = (time) => { return time .split(':') .reduce((acc, cur, index) => acc + (!index ? 60 * +cur : +cur), 0); }; const getSortedPlans = (plans) => { return plans .map(([name, time, period]) => [name, getTime(time), +period]) .sort((a, b) => a[1] - b[1]); }; const isNoLength = (arr) => !arr.length; const getArrangedProgress = ( result, // 결과 stack, // 진행 미룬 과제들 breakTime, // 자투리시간 getNextResult, getNextStack ) => { if (isNoLength(stack)) { return { result, stack }; } const lastItem = stack.at(-1); const [nowName, nowTime, nowPeriod] = lastItem; const remainTime = nowPeriod - breakTime; // 미룬 과제에서의 시간 - 현재 뜨는 시간 const nextResult = getNextResult(result, remainTime <= 0, nowName); const nextStack = getNextStack(stack, remainTime > 0, [ nowName, nowTime, remainTime, ]); if (remainTime > 0) { return { breakTime: breakTime - nowPeriod, result, stack: nextStack }; } return getArrangedProgress( nextResult, nextStack, breakTime - nowPeriod, getNextResult, getNextStack ); }; const getNextResult = (result, flag, name) => { return flag ? [...result, name] : result; }; const getNextStack = (stack, flag, item) => { return stack.slice(0, -1).concat(flag ? [item] : []); }; const progress = ( sortedPlans, index, result, stack, getNextResult, getNextStack ) => { if (index === sortedPlans.length) { return result.concat([...stack].reverse().map((v) => v[0])); } const breakTime = index > 0 ? sortedPlans[index][1] - sortedPlans[index - 1][1] : 0; const { result: nextResult, stack: nextStack } = getArrangedProgress( result, stack, breakTime, getNextResult, getNextStack ); return progress( sortedPlans, index + 1, nextResult, [...nextStack, sortedPlans[index]], getNextResult, getNextStack ); }; function solution(plans) { return progress( getSortedPlans(plans), 0, [], [], getNextResult, getNextStack ); }
차후 고민
- 매개변수로 받는 파라미터가 굉장히 많아 헷갈린다. 어떻게 이를 효과적으로 처리할 수 있을까
Monad
,Currying
,Pipe
,Composition
과 같은 함수형 프로그래밍 로직을 어떻게 추가적으로 곁들일 수 있을까
느낀 점
- 반복문이 얼마나 간편했으며, 또 달콤했는지를 깨달은 한편, 결국 반복문을 사용한다는 것은 스코프의
depth
역시 깊어지게 만든다. 이는 계속하여 상위 스코프에 등록된 변수를 참조할 가능성을 높이므로 side effect를 만들 가능성을 높인다는 것을 느꼈다.
- 하지만 함수형을 통해 재귀함수로 구현한다는 것은 하여금 쉬운 문제를 어렵게 다가간다는 느낌을 받았다. 만약 함수 내에서 오직 반복문만 실행시키고 return시키는 정도는 용납할 수 있지 않을까?
- 간단한 로직이면 함수형으로도 충분히할 수 있을 것 같다. 하지만 로직이 복잡해질 수록, 이를 구현하기까지의 과정이 굉장히 고통스럽다. 실제로 스택을 이중으로 반복문 처리했던 로직을 재귀함수 내에서 재귀를 돌리는 것 자체가 내게 굉장한 부하를 주었다.
진짜진짜최종
const L = {}; const curry = (f) => (a, ..._) => _.length ? f(a, ..._) : (...__) => f(a, ...__); const reduce = curry(function reduce(f, acc, iter) { if (!iter) { iter = acc[Symbol.iterator](); acc = iter.next().value; } for (const a of iter) { acc = f(acc, a); } return acc; }); const go = (...args) => reduce((acc, f) => f(acc), args); const pipe = (f, ...fs) => (...as) => go(f(...as), ...fs); const sortedArray = curry(function sortedArr(compareCallback, iter) { return [...iter].sort(compareCallback); }); L.map = curry(function* map(f, iter) { for (const a of iter) { yield f(a); } }); const take = curry((len, iter) => { const res = []; for (const a of iter) { res.push(a); if (res.length === len) return res; } return res; }); const takeAll = take(Infinity); const map = curry(pipe(L.map, takeAll)); function getTime(time) { return time .split(':') .reduce((acc, cur, idx) => (idx ? acc + +cur : acc + +cur * 60), 0); } const format = (plan) => [plan[0], getTime(plan[1]), Number(plan[2])]; function progress(prevResult, plan) { const { stack, result, lastAt } = prevResult; const nextResult = { stack: [...stack], result: [...result], lastAt, }; while (nextResult.stack.length) { const prev = nextResult.stack.pop(); const [prevSubject, prevStartAt, prevRemainTime] = prev; const diff = plan[1] - nextResult.lastAt; if (prevRemainTime <= diff) { nextResult.result.push(prevSubject); nextResult.lastAt += prevRemainTime; } else { nextResult.stack.push([prevSubject, prevStartAt, prevRemainTime - diff]); break; } } return { ...nextResult, stack: [...nextResult.stack, plan], lastAt: plan[1], }; } function finishAssignment(plans) { return reduce( progress, { stack: [], result: [], lastAt: 0, }, plans ); } const reverseMap = curry(function reverse(f, iter) { const arr = [...iter]; const len = arr.length; const res = []; for (let i = len - 1; i >= 0; i -= 1) { res.push(arr[i]); } return go(res, map(f)); }); const filterStack = pipe( reverseMap((v) => v[0]), takeAll ); const merge = curry((iter1, iter2) => { const res = []; for (const a of iter1) { res.push(a); } for (const b of iter2) { res.push(b); } return res; }); function solution(plans) { const { stack, result } = go( plans, map(format), sortedArray((a, b) => a[1] - b[1]), finishAssignment ); console.log({ stack, result }) return merge(result, filterStack(stack)); }
효성
첫 번째 풀이 - 절차형 끝판왕..
function solution(plans) { const answer = []; const stack = []; const len = plans.length; const sortedPlans = plans.sort((a, b) => +a[1].replace(':', '') - +b[1].replace(':', '')); const convertedPlans = sortedPlans.map(plan => { const [name, start, playtime] = plan; const splitedStart = start.split(':'); return [name, +splitedStart[0], +splitedStart[1], +plan[2]]; }); for(let i = 0; i < len - 1; i++) { const [curName, curHour, curMin, curPlaytime] = convertedPlans[i]; const [nextName, nextHour, nextMin, nextPlaytime] = convertedPlans[i + 1]; let diff = nextMin >= curMin ? nextMin - curMin + (nextHour - curHour) * 60 : nextMin + 60 - curMin + (nextHour - 1 - curHour) * 60; if(curPlaytime <= diff) { answer.push(curName); diff -= curPlaytime; while(diff > 0 && stack.length) { const [name, playtime] = stack.pop(); if(playtime <= diff) { answer.push(name); } else { const restPlaytime = playtime - diff; stack.push([name, restPlaytime]); } diff -= playtime; } } else { const restPlaytime = curPlaytime - diff; stack.push([curName, restPlaytime]); } if(i === len - 2) { answer.push(nextName); } } stack.reverse().forEach(s => answer.push(s[0])); return answer; }
테스트 1 〉 통과 (0.25ms, 33.5MB) 테스트 2 〉 통과 (0.21ms, 33.5MB) 테스트 3 〉 통과 (0.31ms, 33.4MB) 테스트 4 〉 통과 (0.47ms, 33.5MB) 테스트 5 〉 통과 (0.49ms, 33.4MB) 테스트 6 〉 통과 (0.75ms, 33.6MB) 테스트 7 〉 통과 (0.61ms, 33.6MB) 테스트 8 〉 통과 (1.00ms, 33.6MB) 테스트 9 〉 통과 (0.93ms, 33.6MB) 테스트 10 〉 통과 (0.89ms, 33.6MB) 테스트 11 〉 통과 (1.43ms, 34MB) 테스트 12 〉 통과 (3.16ms, 34.7MB) 테스트 13 〉 통과 (3.16ms, 34.7MB) 테스트 14 〉 통과 (29.94ms, 38.2MB) 테스트 15 〉 통과 (6.69ms, 37MB) 테스트 16 〉 통과 (0.19ms, 33.4MB) 테스트 17 〉 통과 (0.18ms, 33.6MB) 테스트 18 〉 통과 (0.17ms, 33.4MB) 테스트 19 〉 통과 (0.30ms, 33.4MB) 테스트 20 〉 통과 (0.88ms, 33.6MB) 테스트 21 〉 통과 (0.87ms, 33.7MB) 테스트 22 〉 통과 (6.79ms, 37.1MB) 테스트 23 〉 통과 (6.46ms, 37.1MB) 테스트 24 〉 통과 (6.86ms, 37.2MB)
두 번째 풀이 - 최대한 함수형이라 했지만 그냥 리팩터링 느낌..ㅠ
function sortByTime(arr, pos) { return arr.sort((a, b) => +a[pos].replace(':', '') - +b[pos].replace(':', '')) } function getRestMin(curTime, nextTime) { const [curHour, curMin] = curTime.split(':'); const [nextHour, nextMin] = nextTime.split(':'); return nextMin >= curMin ? nextMin - curMin + (nextHour - curHour) * 60 : nextMin - curMin + (nextHour - curHour) * 60; } function solution(plans) { const answer = []; const stack = []; const len = plans.length; sortByTime(plans, 1); for(let i = 0; i < len - 1; i++) { const [curName, curStart, curPlaytime] = plans[i]; const [nextName, nextStart, nextPlaytime] = plans[i + 1]; let diff = getRestMin(curStart, nextStart); if(+curPlaytime <= diff) { answer.push(curName); diff -= curPlaytime; while(diff > 0 && stack.length) { const [name, playtime] = stack.pop(); if(playtime <= diff) { answer.push(name); } else { stack.push([name, playtime - diff]); } diff -= playtime; } } else { stack.push([curName, +curPlaytime - diff]); } if(i === len - 2) { answer.push(nextName); stack .reverse() .forEach(s => answer.push(s[0])); } } return answer; }
테스트 1 〉 통과 (0.19ms, 33.6MB) 테스트 2 〉 통과 (0.34ms, 33.4MB) 테스트 3 〉 통과 (0.30ms, 33.6MB) 테스트 4 〉 통과 (0.90ms, 33.6MB) 테스트 5 〉 통과 (0.80ms, 33.7MB) 테스트 6 〉 통과 (0.61ms, 33.6MB) 테스트 7 〉 통과 (0.85ms, 33.6MB) 테스트 8 〉 통과 (1.08ms, 33.6MB) 테스트 9 〉 통과 (1.19ms, 33.7MB) 테스트 10 〉 통과 (1.62ms, 33.9MB) 테스트 11 〉 통과 (1.99ms, 34.1MB) 테스트 12 〉 통과 (4.42ms, 34.9MB) 테스트 13 〉 통과 (6.06ms, 34.8MB) 테스트 14 〉 통과 (11.27ms, 37.2MB) 테스트 15 〉 통과 (8.09ms, 37.2MB) 테스트 16 〉 통과 (0.17ms, 33.5MB) 테스트 17 〉 통과 (0.31ms, 33.5MB) 테스트 18 〉 통과 (0.30ms, 33.5MB) 테스트 19 〉 통과 (0.25ms, 33.5MB) 테스트 20 〉 통과 (0.89ms, 33.7MB) 테스트 21 〉 통과 (1.60ms, 33.7MB) 테스트 22 〉 통과 (8.16ms, 37.2MB) 테스트 23 〉 통과 (12.05ms, 37.2MB) 테스트 24 〉 통과 (7.22ms, 37.2MB)
함수로 빼서 더 안 좋아짐 ㅋㅋ
- 함수형으로 짜려면 sort, reverse를 함수형으로?
- getRestMin 묵시적 형변환 방어하기
- diff 변수명은 쓸 수 있는 시간이 나을듯 remainTime?
세 번째 풀이 - 다시 한번 함수형으로..!
const sortByTime = (arr, pos) => arr.slice().sort((a, b) => +a[pos].replace(':', '') - +b[pos].replace(':', '')) const getRestMin = (curTime, nextTime) => { const [curHour, curMin] = curTime.split(':') const [nextHour, nextMin] = nextTime.split(':') return nextMin - curMin + (nextHour - curHour) * 60 } const processPlans = (plans) => plans.reduce( ([stack, answer], plan, i) => { if(i === plans.length - 1) { return [ [], [...answer, plan[0], ...stack.reverse().map(s => s[0]) ] ]; } const [curName, curTime, curPlaytime] = plan; const [, nextStart] = plans[i + 1]; let diff = getRestMin(curTime, nextStart); if(+curPlaytime <= diff) { let newStack = [...stack]; while(diff > 0 && newStack.length > 0) { let [[name, playtime], ...restStack] = newStack; if(playtime <= diff){ answer.push(name); newStack = restStack; } else { newStack = [...restStack, [name, +playtime - diff]] } diff -= playtime; } return [ newStack, [...answer, curName ] ]; } else { return [ [[curName, +curPlaytime - diff], ...stack], answer ]; } }, [[], []] ) const solution= (plans) => processPlans(sortByTime(plans, 1))[1];