[프로그래머스] 레벨3 (level3) 보석 쇼핑

2020년 07월 03일, 10:00

보석 쇼핑

문제 설명

개발자 출신으로 세계 최고의 갑부가 된 어피치는 스트레스를 받을 때면 이를 풀기 위해 오프라인 매장에 쇼핑을 하러 가곤 합니다.
어피치는 쇼핑을 할 때면 매장 진열대의 특정 범위의 물건들을 모두 싹쓸이 구매하는 습관이 있습니다.
어느 날 스트레스를 풀기 위해 보석 매장에 쇼핑을 하러 간 어피치는 이전처럼 진열대의 특정 범위의 보석을 모두 구매하되 특별히 아래 목적을 달성하고 싶었습니다.
진열된 모든 종류의 보석을 적어도 1개 이상 포함하는 가장 짧은 구간을 찾아서 구매

예를 들어 아래 진열대는 4종류의 보석(RUBY, DIA, EMERALD, SAPPHIRE) 8개가 진열된 예시입니다.

진열대 번호 1 2 3 4 5 6 7 8
보석 이름 DIA RUBY RUBY DIA DIA EMERALD SAPPHIRE DIA

진열대의 3번부터 7번까지 5개의 보석을 구매하면 모든 종류의 보석을 적어도 하나 이상씩 포함하게 됩니다.

진열대의 3, 4, 6, 7번의 보석만 구매하는 것은 중간에 특정 구간(5번)이 빠지게 되므로 어피치의 쇼핑 습관에 맞지 않습니다.

진열대 번호 순서대로 보석들의 이름이 저장된 배열 gems가 매개변수로 주어집니다.
이때 모든 보석을 하나 이상 포함하는 가장 짧은 구간을 찾아서 return 하도록 solution 함수를 완성해주세요.
가장 짧은 구간의 시작 진열대 번호와 끝 진열대 번호를 차례대로 배열에 담아서 return 하도록 하며,
만약 가장 짧은 구간이 여러 개라면 시작 진열대 번호가 가장 작은 구간을 return 합니다.

[제한사항]

  • gems 배열의 크기는 1 이상 100,000 이하입니다.
  • gems 배열의 각 원소는 진열대에 나열된 보석을 나타냅니다.
  • gems 배열에는 1번 진열대부터 진열대 번호 순서대로 보석이름이 차례대로 저장되어 있습니다.
  • gems 배열의 각 원소는 길이가 1 이상 10 이하인 알파벳 대문자로만 구성된 문자열입니다.

입출력 예

gems result
["DIA", "RUBY", "RUBY", "DIA", "DIA", "EMERALD", "SAPPHIRE", "DIA"] [3, 7]
["AA", "AB", "AC", "AA", "AC"] [1, 3]
["XYZ", "XYZ", "XYZ"] [1, 1]
["ZZZ", "YYY", "NNNN", "YYY", "BBB"] [1, 5]

입출력 예에 대한 설명

입출력 예 #1
문제 예시와 같습니다.

입출력 예 #2
3종류의 보석(AA, AB, AC)을 모두 포함하는 가장 짧은 구간은 [1, 3], [2, 4]가 있습니다.
시작 진열대 번호가 더 작은 [1, 3]을 return 해주어야 합니다.

입출력 예 #3
1종류의 보석(XYZ)을 포함하는 가장 짧은 구간은 [1, 1], [2, 2], [3, 3]이 있습니다.
시작 진열대 번호가 가장 작은 [1, 1]을 return 해주어야 합니다.

입출력 예 #4
4종류의 보석(ZZZ, YYY, NNNN, BBB)을 모두 포함하는 구간은 [1, 5]가 유일합니다.
그러므로 [1, 5]를 return 해주어야 합니다.

처음에 생각했던 풀이의 문제점

처음에 생각했던 풀이도 정답을 맞추는 풀이긴 했다. 하지만, 처음에는 13번 효율성만 통과하지 못했는데,
이는 자바스크립트에서 shift나 splice는 배열 자체의 원소를 지우고 모든 배열의 원소를 땡겨(?)주기 때문에 시간이 오래걸린다.
그래서 계속 효율성에서 통과하지 못했던 것이다. linkedlist를 이용해서 queue를 만들어서 쓰게 되면 문제가 풀린다.
자바스크립트에서 shift나 splice를 쓰면 모든 배열을 건드리지만, linkedlist로 사용하게 되면 head만 지우고 끝이기 때문에,
차이가 엄청나게 크다. 하지만, 이렇게 풀게 되면 자바스크립트는 linkedlist를 구현해서 사용해야하기 때문에 귀찮다.
(큐를 사용하지 않는 다른 문제 풀이 바로 가기.)

처음에 생각했던 문제 풀이

  1. Set을 이용해서 보석의 종류를 구함(kinds)
  2. arr는 queue(linkedlist), check는 Map을 이용해서 선언.
  3. 시작 지점인 start와 start와 비교할 startPoint를 선언
  4. result는 배열의 길이(처음 시작은 Infinity로 시작.)
  5. map에 보석을 넣어줌(있으면 +1 증가, 없으면 1로 새로 생성)
  6. 보석을 arr 큐에 담아줌.
  7. arr에 맨 앞에 배열원소를 뺄 수 있으면 지워줌.(remove 함수) 지워주고 ,startPoint를 remove의 리턴 값으로 설정.
  8. 만약에 map의 사이즈와 kind의 사이즈가 같으면 모든 보석이 들어가있는 경우. 그리고 result와 arr의 길이를 비교해야함.
  9. arr의 길이가 더 작으면 start,result를 바꿔줌.
  10. for문이 모두 끝난 후 [start+1, start+result]를 반환.
function solution(gems) {
  var answer = [];
  let kinds = new Set();
  gems.forEach(e => kinds.add(e));
  let check = new Map();
  let arr = new Queue();
  let start = 0;
  let startPoint = 0;
  let result = Infinity;
  for (let i = 0; i < gems.length; i++) {
    if (!check.has(gems[i])) check.set(gems[i], 1);
    else check.set(gems[i], check.get(gems[i]) + 1);
    arr.enqueue(gems[i]);
    startPoint = remove(arr, check, startPoint);
    if (check.size == kinds.size && result > arr.size) {
      result = arr.size;
      start = startPoint;
    }
  }
  return [start + 1, start + result];
}

function remove(arr, check, start) {
  while (true) {
    let el = arr.peek();
    if (check.get(el) > 1) {
      check.set(el, check.get(el) - 1);
      arr.dequeue();
      ++start;
    } else {
      break;
    }
  }
  return start;
}

function Node(data) {
  this.data = data;
  this.next = null;
}
function Queue() {
  this.head = null;
  this.tail = null;
  this.size = 0;
}

Queue.prototype.peek = function () {
  return this.head.data;
};

Queue.prototype.enqueue = function (data) {
  var newNode = new Node(data);
  this.size += 1;
  if (this.head === null) {
    this.head = newNode;
    this.tail = newNode;
  } else {
    this.tail.next = newNode;
    this.tail = newNode;
  }
};

Queue.prototype.dequeue = function () {
  var newNode;
  if (this.head !== null) {
    newNode = this.head.data;
    this.head = this.head.next;
    this.size -= 1;
  }
  return newNode;
};

문제 풀이

  1. 보석의 종류를 알기 위해 Set을 이용해 구함.
  2. gems를 돌면서, map에 gem를 넣어주는데, 중복되는 gem이 들어오는 경우 이전의 gem은 지워줌.
  3. map과 set의 길이가 같은 경우, result에 보석의 시작점 즉 map의 value와 index의 값을 넣어줌.(각각 +1)
  4. 그 후에 result를 정렬 해주는데, 보석의 수가 가장 작은 값을 구해야하기 때문에 a[1]-a[0] - b[1]-b[0]을 빼준다.
  5. 만약 보석의 수가 같은 값이 있다면, 시작번호가 빠른 순 즉 a[0] - b[0]로 비교해준다.
function solution(gems) {
  const kinds = new Set(gems).size;

  const check = new Map();
  const result = [];
  gems.forEach((gem, i) => {
    check.delete(gem);
    check.set(gem, i);
    if (check.size === kinds) {
      result.push([check.values().next().value + 1, i + 1]);
    }
  });

  result.sort((a, b) => {
    if (a[1] - a[0] === b[1] - b[0]) {
      return a[0] - b[0];
    }
    return a[1] - a[0] - (b[1] - b[0]);
  });

  return result[0];
}

아쉬운 점 || 느낀 점

어떤 풀이 방식이 옳은지는 잘 모르겠다. 처음에는 내가 푼 방식이 더 낭비가 심한 풀이라고 느껴졌는데,
실제로는 효율성 테스트에서 시간은 더 빨랐다.(아마 큐를 사용하지 않는 문제에서는 sort를 하기 때문?)
하지만, 자바 언어의 같은 경우 linkedlist를 사용한 queue를 지원해주기 때문에 상관 없지만,
자바스크립트는 그렇지 않기 때문에 만약 코딩테스트라고 가정하면 별로 좋지 않은 방법 같다.
그래도 풀었으니 다행이다...