HomeAboutMeBlogGuest
© 2025 Sejin Cha. All rights reserved.
Built with Next.js, deployed on Vercel
👻
개발 기록
/
코딩테스트 스터디
코딩테스트 스터디
/
과제 진행하기

과제 진행하기

Link
https://school.programmers.co.kr/learn/courses/30/lessons/176962
Deadline
Apr 4, 2023
Status
Archived
Type
stack
recursive function

문제

과제를 받은 루는 다음과 같은 순서대로 과제를 하려고 계획을 세웠습니다.
  • 과제는 시작하기로 한 시각이 되면 시작합니다.
  • 새로운 과제를 시작할 시각이 되었을 때, 기존에 진행 중이던 과제가 있다면 진행 중이던 과제를 멈추고 새로운 과제를 시작합니다.
  • 진행중이던 과제를 끝냈을 때, 잠시 멈춘 과제가 있다면, 멈춰둔 과제를 이어서 진행합니다.
    • 만약, 과제를 끝낸 시각에 새로 시작해야 되는 과제와 잠시 멈춰둔 과제가 모두 있다면, 새로 시작해야 하는 과제부터 진행합니다.
  • 멈춰둔 과제가 여러 개일 경우, 가장 최근에 멈춘 과제부터 시작합니다.
과제 계획을 담은 이차원 문자열 배열 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으로 문제를 접근 및 해결했어요.
  1. 아직 FP가 익숙하지는 않으니, 기존에 짜던 대로 문제를 해결한다.
  1. 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; }

고민

  1. while문과 for문이 많이 혼합되어 있다 → 어떻게 함수형으로 제어할 수 있을까?
  1. 비즈니스 로직(과제를 진행하는 조건)과 일반적인 유틸 함수로 기능할 수 있는 로직(예컨대 이러한 비슷한 로직으로 반복하는 것을 스택으로 처리하는 로직)을 분리할 수 있을까?
  1. 함수형으로 풀이를 할 때, 조건문으로 인한 블록 스코프의 참조로 인해 함수형 풀이가 쉽지 않다. 어떻게 해결할 수 있을까?

FP로 풀이

  1. 재귀를 사용하여 for과 while를 사용하지 않고 해결
  1. stack과 result를 반환하는 콜백 역시 closure를 이용함으로써 유연하게 받을 수 있도록 처리
  1. 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과 같은 함수형 프로그래밍 로직을 어떻게 추가적으로 곁들일 수 있을까

느낀 점

  1. 반복문이 얼마나 간편했으며, 또 달콤했는지를 깨달은 한편, 결국 반복문을 사용한다는 것은 스코프의 depth 역시 깊어지게 만든다. 이는 계속하여 상위 스코프에 등록된 변수를 참조할 가능성을 높이므로 side effect를 만들 가능성을 높인다는 것을 느꼈다.
  1. 하지만 함수형을 통해 재귀함수로 구현한다는 것은 하여금 쉬운 문제를 어렵게 다가간다는 느낌을 받았다. 만약 함수 내에서 오직 반복문만 실행시키고 return시키는 정도는 용납할 수 있지 않을까?
  1. 간단한 로직이면 함수형으로도 충분히할 수 있을 것 같다. 하지만 로직이 복잡해질 수록, 이를 구현하기까지의 과정이 굉장히 고통스럽다. 실제로 스택을 이중으로 반복문 처리했던 로직을 재귀함수 내에서 재귀를 돌리는 것 자체가 내게 굉장한 부하를 주었다.
 

진짜진짜최종

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];