JavaScript/[백준] 문제 풀기

[11066] 파일 합치기

Tristy 2021. 9. 8. 19:01
11066번: 파일 합치기
 
www.acmicpc.net

 


🤔 문제 설명

소설가인 김대전은 소설을 여러 장(chapter)으로 나누어 쓰는데, 각 장은 각각 다른 파일에 저장하곤 한다. 소설의 모든 장을 쓰고 나서는 각 장이 쓰여진 파일을 합쳐서 최종적으로 소설의 완성본이 들어있는 한 개의 파일을 만든다. 이 과정에서 두 개의 파일을 합쳐서 하나의 임시파일을 만들고, 이 임시파일이나 원래의 파일을 계속 두 개씩 합쳐서 소설의 여러 장들이 연속이 되도록 파일을 합쳐나가고, 최종적으로는 하나의 파일로 합친다. 두 개의 파일을 합칠 때 필요한 비용(시간 등)이 두 파일 크기의 합이라고 가정할 때, 최종적인 한 개의 파일을 완성하는데 필요한 비용의 총 합을 계산하시오.

 

예를 들어, C1, C2, C3, C4가 연속적인 네 개의 장을 수록하고 있는 파일이고, 파일 크기가 각각 40, 30, 30, 50 이라고 하자. 이 파일들을 합치는 과정에서, 먼저 C2와 C3를 합쳐서 임시파일 X1을 만든다. 이때 비용 60이 필요하다. 그 다음으로 C1과 X1을 합쳐 임시파일 X2를 만들면 비용 100이 필요하다. 최종적으로 X2와 C4를 합쳐 최종파일을 만들면 비용 150이 필요하다. 따라서, 최종의 한 파일을 만드는데 필요한 비용의 합은 60+100+150=310 이다. 다른 방법으로 파일을 합치면 비용을 줄일 수 있다. 먼저 C1과 C2를 합쳐 임시파일 Y1을 만들고, C3와 C4를 합쳐 임시파일 Y2를 만들고, 최종적으로 Y1과 Y2를 합쳐 최종파일을 만들 수 있다. 이때 필요한 총 비용은 70+80+150=300 이다.

 

소설의 각 장들이 수록되어 있는 파일의 크기가 주어졌을 때, 이 파일들을 하나의 파일로 합칠 때 필요한 최소비용을 계산하는 프로그램을 작성하시오.

 

🤨 제한 사항

  • 첫 입력 때 T개의 테스트 데이터를 받습니다.
  • 각 테스트 데이터의 첫 행에는 소설을 구성하는 장의 수를 나타내는 양의 정수 K(3 <= K <= 500)가 주어집니다.
  • 각 테스트 데이터의 두 번째 행에는 1장부터 K장까지 수록한 파일의 크기를 나타내는 양의 정수 K개가 주어집니다. 
  • 파일의 크기는 10000을 초과하지 않습니다.

 

😀 입출력 예

입력 출력
2
4
40 30 30 50
15
1 21 3 4 5 35 5 4 3 5 98 21 14 17 32
300
864

 

👩‍🏫 어떻게 풀어요?

  • 이 과정에서 두 개의 파일을 합쳐서 하나의 임시파일을 만들고, 이 임시파일이나 원래의 파일을 계속 두 개씩 합쳐서 소설의 여러 장들이 연속이 되도록 파일을 합쳐나가고, 최종적으로는 하나의 파일로 합친다.  
  • 소설의 각 장들이 수록되어 있는 파일의 크기가 주어졌을 때, 이 파일들을 하나의 파일로 합칠 때 필요한 최소비용을 계산하는 프로그램을 작성하시오. 

 

문제를 풀기 전에 문제를 자세히 읽어봐야지 제대로 풀 수 있는 문제입니다. 문제를 제대로 안 읽고 풀었다가는 여러 장들이 연속이 되도록 파일을 합쳐나간다는 규칙을 지키지 않은 알고리즘을 만들게 됩니다. 이 문제는 인접하지 않은 장은 합칠 수 없고, 서로 인접한 상태여야지만 합칠 수 있습니다. 이러한 방식으로 합쳐나가며 모든 장을 합치는데 필요한 최소 비용을 구하는 것이지요.

 

 

 

먼저 40 30 30 50 이라는 파일 크기가 있다고 가정을 하고 문제를 풀어보도록 하겠습니다.

 

 

맨 앞의 40과 30을 더했을 때는 총 70이라는 비용을 사용하여 하나의 파일로 합칠 수 있습니다. 이렇게만 보면 간단해 보이지만, 여기서 하나의 파일이 늘어난다면 이야기가 달라지게 됩니다.

 

 

 

 

40, 30, 30, 50 이라는 파일이 있을 때, 첫 번째 파일부터 세 번째 파일까지의 최소 비용의 합을 구하기 위해서는 2가지 경우를 비교해 볼 수 있습니다. 먼저 40과 30을 합치고 마지막 30을 합치는 방법과, 30과 30을 합치고 마지막에 40을 합치는 방법의 경우가 있습니다. 

두가지 경우의 방법 모두 잘 보시면, 앞에서 했던 연산을 반복하고 있습니다. 첫 번째 방법의 경우, 40과 30을 합치는 작업이 다시 반복되고 있고, 두 번째 방법의 경우에는 30과 30을 합치는 방법이 반복되고 있습니다. 즉, 일반적인 방법으로 풀면 같은 연산을 반복하기 때문에 시간 초과가 발생하게 될 것이며, 해당 문제는 동적 할당법을 이용해 풀어야 한다는 것을 알 수 있습니다. 또한 모든 파일을 합쳤을 때의 최소 비용을 구하기 위해서는 각 방법마다 나오는 비용이 최소가 되어야 한다는 것도 알 수 있습니다. 40, 30, 30, 50을 모두 합쳤을 때 나오는 최소비용을 구하기 위해서는 반드시 위 그림에서는 두 번째 방법을 택해야 하는 것이죠. 그렇지 않으면 최소 비용으로 만들 수 없습니다.

 

 

40, 30, 30, 50 이라는 파일이 있을 때, 

 

40 + 30 

30 + 30

30 + 50 

40 + 30 | 30 = (40 + 30) + (40 + 30 + 30)

40 | 30 + 30 = (40 + 30+ 30) + 30

30 + 30 | 50 = (30 + 30) + (30 + 30 + 50)

30 | 30 + 50 = (30 + 30 + 50) + (30 + 50) 

 

...

 

첫 번째 부터 4번째 파일인 50까지의 최소 비용을 구하기 위해서는 각각 인접한 장끼리 합한 값을 계속해서 연산해 주고 있는 것을 알 수 있습니다. 따라서 동적 할당법을 위한 배열 말고도, 인접한 장끼리 미리 더한 값을 저장해 둔 sum 배열을 사용할 것 입니다.

 

위의 경우라면 sum 배열에는 다음과 같은 값이 저장됩니다.

 

sum[0] = 0

sum[1] = 40

sum[2] = 40 + 30 (첫번째 장과 두 번째 장의 합) = 70

sum[3] = 70 + 30 (첫번째 장 ~세 번째 장의 합) = 100

sum[4] = 100 + 50 (첫번째 장 ~네 번째 장의 합) = 150

 

그리고 각 구간의 최소 비용을 저장하기 위해 i 번째 장 ~ j 번째 장까지 합치는 최소 비용을 저장하는 2차원 배열인 dp를 사용하겠습니다.

 

dp[i][j] =  i 번째 장 ~ j 번째 장까지 합치는 최소 비용

dp[i][i + 1] = sum[i + 1] - sum[i - 1]

 

ex)

dp[1][2] = 첫 번째 장 ~ 두 번째 장까지 합치는 최소 비용 = sum[2] - sum [0] = 70

dp[2][3] = 두 번째 장 ~ 세 번째 장까지 합치는 최소 비용 = sum[3] - sum [1] = 60

dp[3][4] = 세 번째 장 ~ 네 번째 장까지 합치는 최소 비용 = sum[4] - sum [2] = 80

 

 

 

만약 40, 30, 30의 파일을 합친다고 가정을 해볼 때 구할 수 있는 경우는 위의 그림에서 보았던 것 처럼 2가지 입니다. 2가지 중에서 최소값을 저장해야 하기 때문에 Math.min을 사용해서 저장합니다. 또한 40 + 30 을 먼저 연산하고 30과 합칠 것인지, 30 + 30을 먼저 연산하고 40과 합칠 것인지 기준을 정하는 변수 값이 필요합니다. 여기서는 k라고 정의하겠습니다. 

 

40, 30, 30을 합쳐 보는 경우에서 2가지 방법으로 구할 수 있다면, k 변수를 사용해서 다음과 같은 식을 만들 수 있습니다. 

 

dp[i][j] =  Math.min(dp[i][j], dp[i][k] + dp[k + 1][j] + sum[i + 1] - sum[i - 1])

 

 

30 + 30의 최소 비용을 구하고 40을 합치는 경우의 식

dp[1][3] = Math.min(dp[1][3], dp[1][1] + dp [2][3] + sum[3] - sum[0])

 

40 + 30의 최소 비용을 구하고 30을 합치는 경우의 식

dp[1][3] = Math.min(dp[1][3], dp[1][2] + dp [3][3] + sum[3] - sum[0])

 

 

코드에서는 위의 식을 수행하기 전에 해당 위치에 Infinity 값을 넣어 줌으로서 먼저 입력된 값이 항상 최소값으로 설정되게 하였고, dp [1][1] 이나 dp [2][2]의 경우에는 합치는 것이 아니기 때문에 0을 대입하였습니다. 

 

👾트리스티의 답

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 원동균 / 11066 / 파일 합치기
"use strict";


const { memory } = require("console");


const ps = (function (process) {
  const readline = require("readline");


  const rl = readline.createInterface({
    input: process.stdin,
    output: process.stdout,
  });


  let lines = [];
  let cursor = 0;


  rl.on("line"function (line) {
    lines.push(line);
  });


  return {
    main(f) {
      f()
        .catch((err=> {
          console.error(err);
          process.exit(1);
        })
        .finally(() => {
          rl.close();
        });
    },
    use(namef) {
      this[name] = f;
    },
    readLine: async function readLine() {
      return new Promise((resolve=> {
        if (cursor < lines.length) {
          resolve(lines[cursor++]);
        } else {
          setTimeout(() =>
            readLine().then((line=> {
              resolve(line);
            })
          );
        }
      });
    },
    async readArrayLine() {
      const line = await this.readLine();
      return line.split(/\s/).map((t=> parseInt(t));
    },
    write(data) {
      process.stdout.write(data + "");
    },
    writeLine(data) {
      process.stdout.write((data === undefined ? "" : data) + "\n");
    },
    range(startendstep = 1) {
      if (end === undefined) {
        end = start;
        start = 0;
      }
      return {
        [Symbol.iterator]: function* () {
          for (let i = starti < endi += step) {
            yield i;
          }
        },
      };
    },
  };
})(process);


ps.main(async () => {
  const result = [];
  const N = parseInt(await ps.readLine());


  // 이 문제에서 dp를 직관적으로 사용하기 위해 index 0이 아니라 1부터 시작합니다. 따라서 501의 크기를 갖는 배열을 선언합니다.
  const dp = Array.from(Array(501), () => Array(501).fill(0));
  const sum = Array.from(Array(501).fill(0));


  for (let i = 0i < Ni++) {
    const K = parseInt(await ps.readLine());
    const novel = (await ps.readLine())
      .split(" ")
      .map((element=> parseInt(element));


    // 입력된 값의 누적합을 구합니다.
    for (let j = 1j <= Kj++) {
      sum[j] = sum[j - 1] + novel[j - 1];
    }


    result.push(combine(novelK));
  }


  for (const i of result) {
    console.log(i);
  }


  // 합치기 함수
  // 최소 비용으로 소설을 합치기 위해서는 최소 비용의 부분합을 구해야 합니다.
  function combine(novelK) {
    for (let i = 1i < Ki++) {
      for (let j = 1i + j <= Kj++) {
        dp[j][i + j] = Infinity;


        for (let k = jk < i + jk++) {
          dp[j][i + j] = Math.min(
            dp[j][i + j],
            dp[j][k] + dp[k + 1][i + j] + sum[i + j] - sum[j - 1]
          );
        }
      }
    }


    return dp[1][K];
  }
});
cs

 

 

※ 잘못된 정보가 있거나 수정사항이 있다면 댓글로 남겨주세요!