[코딩테스트] 백준 - 14888번 연산자 끼워넣기 DFS, Back Tracking (JS)

POLO·2024년 2월 6일

문제설명

N개의 수로 이루어진 수열 A1, A2, ..., AN이 주어진다. 또, 수와 수 사이에 끼워넣을 수 있는 N-1개의 연산자가 주어진다. 연산자는 덧셈(+), 뺄셈(-), 곱셈(×), 나눗셈(÷)으로만 이루어져 있다.

우리는 수와 수 사이에 연산자를 하나씩 넣어서, 수식을 하나 만들 수 있다. 이때, 주어진 수의 순서를 바꾸면 안 된다.

예를 들어, 6개의 수로 이루어진 수열이 1, 2, 3, 4, 5, 6이고, 주어진 연산자가 덧셈(+) 2개, 뺄셈(-) 1개, 곱셈(×) 1개, 나눗셈(÷) 1개인 경우에는 총 60가지의 식을 만들 수 있다. 예를 들어, 아래와 같은 식을 만들 수 있다.

1+2+3-4×5÷6
1÷2+3+4-5×6
1+2÷3×4-5+6
1÷2×3-4+5+6
식의 계산은 연산자 우선 순위를 무시하고 앞에서부터 진행해야 한다. 또, 나눗셈은 정수 나눗셈으로 몫만 취한다. 음수를 양수로 나눌 때는 C++14의 기준을 따른다. 즉, 양수로 바꾼 뒤 몫을 취하고, 그 몫을 음수로 바꾼 것과 같다. 이에 따라서, 위의 식 4개의 결과를 계산해보면 아래와 같다.

1+2+3-4×5÷6 = 1
1÷2+3+4-5×6 = 12
1+2÷3×4-5+6 = 5
1÷2×3-4+5+6 = 7
N개의 수와 N-1개의 연산자가 주어졌을 때, 만들 수 있는 식의 결과가 최대인 것과 최소인 것을 구하는 프로그램을 작성하시오.

제한사항

첫째 줄에 수의 개수 N(2 ≤ N ≤ 11)가 주어진다. 둘째 줄에는 A1, A2, ..., AN이 주어진다. (1 ≤ Ai ≤ 100) 셋째 줄에는 합이 N-1인 4개의 정수가 주어지는데, 차례대로 덧셈(+)의 개수, 뺄셈(-)의 개수, 곱셈(×)의 개수, 나눗셈(÷)의 개수이다.

첫째 줄에 만들 수 있는 식의 결과의 최댓값을, 둘째 줄에는 최솟값을 출력한다. 연산자를 어떻게 끼워넣어도 항상 -10억보다 크거나 같고, 10억보다 작거나 같은 결과가 나오는 입력만 주어진다. 또한, 앞에서부터 계산했을 때, 중간에 계산되는 식의 결과도 항상 -10억보다 크거나 같고, 10억보다 작거나 같다.

문제풀이

입력받은 연산자의 개수만큼 배열에 해당 연산자를 담는 것부터 시작한다.
이후 연산자 배열을 순회하면서 매번 깊이 우선 탐색을 진행하면 된다.
DFS를 수행할 때마다 이전 수식에 현재 수를 넣고, 수식을 계산한다.
DFS는 현재 depth가 수식의 개수와 동일하지 않을 때까지 재귀하고,
동일하면 현재의 최댓값, 최솟값과 비교하여 값을 갱신하고 return한다.
DFS를 수행할 때는 visited 배열을 사용해 이전에 사용한 연산자의 인덱스는 사용하지 않도록 해야한다.

코드

const fs = require('fs');
const input = fs.readFileSync('/dev/stdin').toString().split('\n');

const N = Number(input[0]);
const NUM_LIST = input[1].split(' ').map(Number);
const OP_LIST = input[2].split(' ').map(Number);
const OP = ['+',"-","*", "/"];
let opArray = [];
for(let i = 0 ; i < 4; i++) {
  for(let j = 0; j < OP_LIST[i]; j++) {
    opArray.push(OP[i]);
  }
}

let minAnswer = Infinity;
let maxAnswer = -(Infinity);
let opVisited = new Array(opArray.length).fill(false);

for(let i = 0 ; i < opArray.length; i++) {
  opVisited[i] = true;
  const prevNum = NUM_LIST[0];
  const prevOp = opArray[i];
  dfs(1,prevNum,prevOp);
  opVisited[i] = false;
}

function dfs(depth,prevNum,prevOp) {
  const curNum = NUM_LIST[depth];
  let result = prevNum;
  switch (prevOp) {
    case '+':
      result += curNum;
      break;
    case '-':
      result -= curNum;
      break;
    case '*':
      result *= curNum;
      break;
    case '/':
      result /= curNum;
      result = parseInt(result);
      break;
  }

  if(depth === opArray.length) {
    minAnswer = Math.min(minAnswer, result);
    maxAnswer = Math.max(maxAnswer, result);
    return;
  }
  for(let i = 0 ; i < opArray.length; i++) {
    if(opVisited[i]) continue;
    opVisited[i] = true;
    const curOP = opArray[i];
    dfs(depth + 1, result, curOP);
    opVisited[i] = false;
  }
}

console.log(maxAnswer+'\n'+minAnswer);

처음에 Case문을 사용하지 않고 eval() 함수를 사용해 식을 계산했더니 시간 초과가 발생했다.
eval() 함수 성능이 많이 안 좋은 것으로보인다.

현재 코드도 실행 시간이 상당히 높게 나왔는데, DFS 말고 백트래킹으로 구현하면 훨씬 빠르게 작동한다.

const N = Number(input[0]);
const NUM_LIST = input[1].split(' ').map(Number);
const OP_LIST = input[2].split(' ').map(Number);
const OP = ['+',"-","*", "/"];
let opArray = [];
for(let i = 0 ; i < 4; i++) {
  for(let j = 0; j < OP_LIST[i]; j++) {
    opArray.push(OP[i]);
  }
}

let minAnswer = Infinity;
let maxAnswer = -(Infinity);
let opVisited = new Array(opArray.length).fill(false);
let curOpOuter = null;
for(let i = 0 ; i < opArray.length; i++) {
  if(curOpOuter === opArray[i]) continue;
  curOpOuter = opArray[i];
  opVisited[i] = true;
  const prevNum = NUM_LIST[0];
  const prevOp = opArray[i];
  dfs(1,prevNum,prevOp);
  opVisited[i] = false;
}

function dfs(depth,prevNum,prevOp) {
  const curNum = NUM_LIST[depth];
  let curOpInner = null;
  let result = prevNum;
  switch (prevOp) {
    case '+':
      result += curNum;
      break;
    case '-':
      result -= curNum;
      break;
    case '*':
      result *= curNum;
      break;
    case '/':
      result /= curNum;
      result = parseInt(result);
      break;
  }

  if(depth === opArray.length) {
    minAnswer = Math.min(minAnswer, result);
    maxAnswer = Math.max(maxAnswer, result);
    return;
  }
  for(let i = 0 ; i < opArray.length; i++) {
    if(opVisited[i]) continue;
    if(curOpInner === opArray[i]) continue;
    curOpInner = opArray[i];
    opVisited[i] = true;
    const curOP = opArray[i];
    dfs(depth + 1, result, curOP);
    opVisited[i] = false;
  }
}

console.log(maxAnswer+'\n'+minAnswer);

DFS와 비교햇을 때 무려 5배나 빨랐다.

단순하게 이전 연산자랑 동일하면 DFS를 수행하지 않도록만 해 주면 된다.
나는 curOpOuter, curOpInner 변수를 사용해 최근 DFS를 수행한 연산자를 저장해 두고 같으면 continue 하는 코드로 작성했다.

0개의 댓글