1. 문제

programmers.co.kr/learn/courses/30/lessons/17684

 

코딩테스트 연습 - [3차] 압축

TOBEORNOTTOBEORTOBEORNOT [20, 15, 2, 5, 15, 18, 14, 15, 20, 27, 29, 31, 36, 30, 32, 34]

programmers.co.kr

문제 설명

신입사원 어피치는 카카오톡으로 전송되는 메시지를 압축하여 전송 효율을 높이는 업무를 맡게 되었다. 메시지를 압축하더라도 전달되는 정보가 바뀌어서는 안 되므로, 압축 전의 정보를 완벽하게 복원 가능한 무손실 압축 알고리즘을 구현하기로 했다.

어피치는 여러 압축 알고리즘 중에서 성능이 좋고 구현이 간단한LZW(Lempel–Ziv–Welch) 압축을 구현하기로 했다. LZW 압축은 1983년 발표된 알고리즘으로, 이미지 파일 포맷인 GIF 등 다양한 응용에서 사용되었다.

LZW 압축은 다음 과정을 거친다.

  1. 길이가 1인 모든 단어를 포함하도록 사전을 초기화한다.
  2. 사전에서 현재 입력과 일치하는 가장 긴 문자열w를 찾는다.
  3. w에 해당하는 사전의 색인 번호를 출력하고, 입력에서w를 제거한다.
  4. 입력에서 처리되지 않은 다음 글자가 남아있다면(c),w+c에 해당하는 단어를 사전에 등록한다.
  5. 단계 2로 돌아간다.

압축 알고리즘이 영문 대문자만 처리한다고 할 때, 사전은 다음과 같이 초기화된다. 사전의 색인 번호는 정수값으로 주어지며, 1부터 시작한다고 하자.

색인 번호 1 2 3 ... 24 25 26
단어 A B C ... X Y Z

예를 들어 입력으로KAKAO가 들어온다고 하자.

  1. 현재 사전에는KAKAO의 첫 글자K는 등록되어 있으나, 두 번째 글자까지인KA는 없으므로, 첫 글자K에 해당하는 색인 번호 11을 출력하고, 다음 글자인A를 포함한KA를 사전에 27 번째로 등록한다.
  2. 두 번째 글자A는 사전에 있으나, 세 번째 글자까지인AK는 사전에 없으므로,A의 색인 번호 1을 출력하고,AK를 사전에 28 번째로 등록한다.
  3. 세 번째 글자에서 시작하는KA가 사전에 있으므로,KA에 해당하는 색인 번호 27을 출력하고, 다음 글자O를 포함한KAO를 29 번째로 등록한다.
  4. 마지막으로 처리되지 않은 글자O에 해당하는 색인 번호 15를 출력한다.
현재 입력(w) 다음 글자(c) 출력 사전 추가(w+c)
K A 11 27: KA
A K 1 28: AK
KA O 27 29: KAO
O   15  

이 과정을 거쳐 다섯 글자의 문장KAKAO가 4개의 색인 번호 [11, 1, 27, 15]로 압축된다.

입력으로TOBEORNOTTOBEORTOBEORNOT가 들어오면 다음과 같이 압축이 진행된다.

현재 입력(w) 다음 글자(c) 출력 사전 추가(w+c)
T O 20 27: TO
O B 15 28: OB
B E 2 29: BE
E O 5 30: EO
O R 15 31: OR
R N 18 32: RN
N O 14 33: NO
O T 15 34: OT
T T 20 35: TT
TO B 27 36: TOB
BE O 29 37: BEO
OR T 31 38: ORT
TOB E 36 39: TOBE
EO R 30 40: EOR
RN O 32 41: RNO
OT   34  

제한 사항

입력 형식

입력으로 영문 대문자로만 이뤄진 문자열msg가 주어진다.msg의 길이는 1 글자 이상, 1000 글자 이하이다.

출력 형식

주어진 문자열을 압축한 후의 사전 색인 번호를 배열로 출력하라.

입출력 예

msg answer
KAKAO [11, 1, 27, 15]
TOBEORNOTTOBEORTOBEORNOT [20, 15, 2, 5, 15, 18, 14, 15, 20, 27, 29, 31, 36, 30, 32, 34]
ABABABABABABABAB [1, 2, 27, 29, 28, 31, 30]

2. 어떻게 풀까?

  1. Map을 생성해서 A~Z까지 index 1~26을 붙여 저장한다.
  2. 문자열을 for문으로 돌며 Map에 존재하는지 체크하고 있다면 문자열 길이를 늘이고, 없다면 이전 문자열에 해당하는 index값을 출력하고, 늘린 문자열은 Map에 추가한다.

3. 코드

테스트 코드

package programmers.level2.압축_20210111;

import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.*;

import org.junit.Test;

public class SolutionTest {

  @Test
  public void test1() {
    assertThat(new Solution().solution("KAKAO"), is(new int[]{11, 1, 27, 15}));
  }

  @Test
  public void test2() {
    assertThat(new Solution().solution("TOBEORNOTTOBEORTOBEORNOT"),
        is(new int[]{20, 15, 2, 5, 15, 18, 14, 15, 20, 27, 29, 31, 36, 30, 32, 34}));
  }

  @Test
  public void test3() {
    assertThat(new Solution().solution("ABABABABABABABAB"),
        is(new int[]{1, 2, 27, 29, 28, 31, 30}));
  }
}

실제 코드

package programmers.level2.압축_20210111;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class Solution {

  public int[] solution(String msg) {
    Map<String, Integer> dictionary = buildBasicDictionary();
    List<Integer> indexList = new ArrayList<>();
    int letterIndex = 27;
    int startIndex = 0;

    while (startIndex < msg.length()) {
      int endIndex = getEndIndex(msg, dictionary, startIndex);
      String target = msg.substring(startIndex, endIndex);

      indexList.add(dictionary.get(target));
      if (endIndex < msg.length()) {
        dictionary.put(msg.substring(startIndex, endIndex + 1), letterIndex++);
      }

      startIndex = endIndex;
    }

    return indexList.stream().mapToInt(x -> x).toArray();
  }

  private int getEndIndex(String msg, Map<String, Integer> dictionary, int startIndex) {
    int index;
    for (index = startIndex + 1; index <= msg.length(); index++) {
      if (!dictionary.containsKey(msg.substring(startIndex, index))) {
        break;
      }
    }
    return index - 1;
  }

  private Map<String, Integer> buildBasicDictionary() {
    Map<String, Integer> map = new HashMap<>();
    int index = 1;
    for (char letter = 'A'; letter <= 'Z'; letter++, index++) {
      map.put(letter + "", index);
    }
    return map;
  }
}
  • buildBasicDictionary()에서 A~Z까지 Map에 추가한다.
  • while문에서 Map에 존재하는 문자열의 valueList에 저장하고, 존재하지 않는 문자열은 Map에 저장한다.
  • getEndIndex()에서 endIndex를 구한다.

4. 느낀 점

  • 처음에 getEndIndex()에서 index를 반환했다. 근데 그러면 입력 문자열의 startIndex ~ endIndex - 1까지의 subStringMap에 존재하고, startIndex ~ endIndex까지의 subString은 존재하지 않는다. 이름이 너무 애매하고 오히려 더 헷갈려서 getEndIndex()에서 index - 1을 반환하는것으로 해결했다.
  • 문제 자체는 카카오 문제치고는 어렵지 않은 편이다. level 2까지는 확실히 다 맞혀야 할듯

1. 문제

programmers.co.kr/learn/courses/30/lessons/17679

 

코딩테스트 연습 - [1차] 프렌즈4블록

프렌즈4블록 블라인드 공채를 통과한 신입 사원 라이언은 신규 게임 개발 업무를 맡게 되었다. 이번에 출시할 게임 제목은 프렌즈4블록. 같은 모양의 카카오프렌즈 블록이 2×2 형태로 4개가 붙

programmers.co.kr

문제 설명

블라인드 공채를 통과한 신입 사원 라이언은 신규 게임 개발 업무를 맡게 되었다. 이번에 출시할 게임 제목은프렌즈4블록.
같은 모양의 카카오프렌즈 블록이 2×2 형태로 4개가 붙어있을 경우 사라지면서 점수를 얻는 게임이다.

만약 판이 위와 같이 주어질 경우, 라이언이 2×2로 배치된 7개 블록과 콘이 2×2로 배치된 4개 블록이 지워진다. 같은 블록은 여러 2×2에 포함될 수 있으며, 지워지는 조건에 만족하는 2×2 모양이 여러 개 있다면 한꺼번에 지워진다.

블록이 지워진 후에 위에 있는 블록이 아래로 떨어져 빈 공간을 채우게 된다.

만약 빈 공간을 채운 후에 다시 2×2 형태로 같은 모양의 블록이 모이면 다시 지워지고 떨어지고를 반복하게 된다.

위 초기 배치를 문자로 표시하면 아래와 같다.

TTTANT
RRFACC
RRRFCC
TRRRAA
TTMMMF
TMMTTJ

각 문자는 라이언(R), 무지(M), 어피치(A), 프로도(F), 네오(N), 튜브(T), 제이지(J), 콘(C)을 의미한다

입력으로 블록의 첫 배치가 주어졌을 때, 지워지는 블록은 모두 몇 개인지 판단하는 프로그램을 제작하라.

제한 사항

입력 형식

  • 입력으로 판의 높이m, 폭n과 판의 배치 정보board가 들어온다.
  • 2 ≦n,m≦ 30
  • board는 길이n인 문자열m개의 배열로 주어진다. 블록을 나타내는 문자는 대문자 A에서 Z가 사용된다.

출력 형식

입력으로 주어진 판 정보를 가지고 몇 개의 블록이 지워질지 출력하라.

입출력 예

m n board answer
4 5 ["CCBDE","AAADE","AAABF","CCBBF"] 14
6 6 ["TTTANT", "RRFACC", "RRRFCC", "TRRRAA", "TTMMMF", "TMMTTJ"] 15

2. 어떻게 풀까?

  1. 입력으로 주어진 String 배열 board를 이차원 배열로 만든다.
  2. 이차원 배열의 현재 index의 block을 기준으로 block을 지울 수 있는지 확인한다.
  3. 지운 이후 위쪽의 block들을 아래로 내린다.
  4. 지울 수 있는 block이 없을 때까지 반복한다.
  5. 해결 과정은 명확하게 이해할 수 있는데, 이것을 코드로 작성하는데 계속 막히고, 틀렸다. 특히, 지울 수 있는지 확인하는 과정에서 boolean 타입을 선언해서 확인하려했는데, 잘 안됐다. 그래서 답을 참고했다.

velog.io/@hyeon930/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%ED%94%84%EB%A0%8C%EC%A6%884%EB%B8%94%EB%A1%9D-Java

 

[프로그래머스] 프렌즈4블록 (Java)

프로그래머스 프렌즈4블록이 문제의 핵심은 다음과 같다.4x4 블록이 같을 경우 지워지는데, 4x4가 겹쳐있는 경우에도 모두 지워진다.따라서, 이 문제는사라질 블록을 모두 체크한다.체크된 블록

velog.io


3. 코드

테스트 코드

import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.*;

import org.junit.Test;

public class SolutionTest {

  @Test
  public void test1() {
    String[] board = {"CCBDE", "AAADE", "AAABF", "CCBBF"};
    assertThat(new Solution().solution(4, 5, board), is(14));
  }

  @Test
  public void test2() {
    String[] board = {"TTTANT", "RRFACC", "RRRFCC", "TRRRAA", "TTMMMF", "TMMTTJ"};
    assertThat(new Solution().solution(6, 6, board), is(15));
  }

}

실제 코드

public class Solution {

  public int solution(int m, int n, String[] board) {
    String[][] table = buildTable(m, n, board);
    int count = 0;

    while (true) {
      int dropBlockCount = countFourBlock(m, n, table);
      if (dropBlockCount == 0) {
        break;
      }
      count += dropBlockCount;
      dropTable(m, n, table);
    }

    return count;
  }

  private String[][] buildTable(int m, int n, String[] board) {
    String[][] table = new String[m][n];
    for (int i = 0; i < m; i++) {
      for (int j = 0; j < n; j++) {
        table[i][j] = board[i].charAt(j) + "";
      }
    }
    return table;
  }

  private int countFourBlock(int m, int n, String[][] table) {

    boolean[][] check = new boolean[m][n];

    for (int row = 0; row < m - 1; row++) {
      for (int col = 0; col < n - 1; col++) {
        if (table[row][col].equals(".")) {
          continue;
        }
        checkFourBlock(table, check, row, col);
      }
    }

    int count = 0;
    for (int row = 0; row < m; row++) {
      for (int col = 0; col < n; col++) {
        if (check[row][col]) {
          count++;
          table[row][col] = ".";
        }
      }
    }

    return count;
  }

  private void checkFourBlock(String[][] table, boolean[][] check, int currentRow,
      int currentCol) {
    String currentBlock = table[currentRow][currentCol];

    for (int row = currentRow; row < currentRow + 2; row++) {
      for (int col = currentCol; col < currentCol + 2; col++) {
        if (!table[row][col].equals(currentBlock)) {
          return;
        }
      }
    }

    for (int row = currentRow; row < currentRow + 2; row++) {
      for (int col = currentCol; col < currentCol + 2; col++) {
        check[row][col] = true;
      }
    }
  }

  private void dropTable(int m, int n, String[][] table) {
    for (int col = 0; col < n; col++) {
      for (int row = m - 1; row >= 0; row--) {
        if (table[row][col].equals(".")) {
          for (int notDotRow = row - 1; notDotRow >= 0; notDotRow--) {
            if (!table[notDotRow][col].equals(".")) {
              table[row][col] = table[notDotRow][col];
              table[notDotRow][col] = ".";
              break;
            }
          }
        }
      }
    }
  }

}
  • buildTable()에서 일차원 배열의 board를 이차원 배열로 만들었다.
  • 반복문을 돌며 countFourBlock()을 통해 지울 수 있는 block의 수를 계산 후, 전체 count에 더해주고 dropTable()을 통해 지웠다.
  • countFourBlock()에서 지울 수 있는 block인지 확인하는 boolean 이차원 배열을 선언 후, checkFourBlock()을 통해그림의 왼쪽 위부터 차례대로 확인했다. 마지막 행과 열은 확인 할 수 없기 때문에 반복문을 m-1, n-1까지만 돌며 확인했다. 지울 수 있는 block은 .으로 바꿔주었다.
  • dropTable()에서 element가 .인 칸을 찾은 후에 그 위로 .가 아닌 칸을 찾아서 두 element를 바꿔주었다.

4. 느낀 점

이차원 배열을 왼쪽 위부터 오른쪽 아래까지 차례대로 확인 👉 지울 수 있는 블록이라면 따로 마킹 👉 이차원 배열에서 마킹해놓은 개수를 덧셈 👉 블록들 삭제 👉 지울 수 있는 블록이 없을 때까지 반복

이라는 흐름은 머리로 간단히 그려졌는데, 막상 코드로 구현하려고 할 때 어려움을 느꼈다.

마킹을 어떻게 해야할까? Key, Value로 구현해야하나? 그럼 Entry를 써야하나? 이런 고민들도 있었고,

지울 수 있는 블록들은 어떻게 삭제를 해야할까? 배열에서는 delete하기 어려우니 List를 써야하나? 하는 고민도 있었다.

다른 분이 푼 코드를 보니, 그냥 따로 boolean 타입의 이차원 배열을 선언하여 해결했고

배열은 delete 대신 .으로 바꿔주었다.

어렵다 ㅜㅜ

1. 문제

programmers.co.kr/learn/courses/30/lessons/17677

문제 설명

여러 언론사에서 쏟아지는 뉴스, 특히 속보성 뉴스를 보면 비슷비슷한 제목의 기사가 많아 정작 필요한 기사를 찾기가 어렵다. Daum 뉴스의 개발 업무를 맡게 된 신입사원 튜브는 사용자들이 편리하게 다양한 뉴스를 찾아볼 수 있도록 문제점을 개선하는 업무를 맡게 되었다.

개발의 방향을 잡기 위해 튜브는 우선 최근 화제가 되고 있는 카카오 신입 개발자 공채 관련 기사를 검색해보았다.

  • 카카오 첫 공채..'블라인드' 방식 채용
  • 카카오, 합병 후 첫 공채.. 블라인드 전형으로 개발자 채용
  • 카카오, 블라인드 전형으로 신입 개발자 공채
  • 카카오 공채, 신입 개발자 코딩 능력만 본다
  • 카카오, 신입 공채.. 코딩 실력만 본다
  • 카카오 코딩 능력만으로 2018 신입 개발자 뽑는다

기사의 제목을 기준으로 블라인드 전형에 주목하는 기사와 코딩 테스트에 주목하는 기사로 나뉘는 걸 발견했다. 튜브는 이들을 각각 묶어서 보여주면 카카오 공채 관련 기사를 찾아보는 사용자에게 유용할 듯싶었다.

유사한 기사를 묶는 기준을 정하기 위해서 논문과 자료를 조사하던 튜브는 자카드 유사도라는 방법을 찾아냈다.

자카드 유사도는 집합 간의 유사도를 검사하는 여러 방법 중의 하나로 알려져 있다. 두 집합 A, B 사이의 자카드 유사도 J(A, B)는 두 집합의 교집합 크기를 두 집합의 합집합 크기로 나눈 값으로 정의된다.

예를 들어 집합 A = {1, 2, 3}, 집합 B = {2, 3, 4}라고 할 때, 교집합 A ∩ B = {2, 3}, 합집합 A ∪ B = {1, 2, 3, 4}이 되므로, 집합 A, B 사이의 자카드 유사도 J(A, B) = 2/4 = 0.5가 된다. 집합 A와 집합 B가 모두 공집합일 경우에는 나눗셈이 정의되지 않으니 따로 J(A, B) = 1로 정의한다.

자카드 유사도는 원소의 중복을 허용하는 다중집합에 대해서 확장할 수 있다. 다중집합 A는 원소 1을 3개 가지고 있고, 다중집합 B는 원소 1을 5개 가지고 있다고 하자. 이 다중집합의 교집합 A ∩ B는 원소 1을 min(3, 5)인 3개, 합집합 A ∪ B는 원소 1을 max(3, 5)인 5개 가지게 된다. 다중집합 A = {1, 1, 2, 2, 3}, 다중집합 B = {1, 2, 2, 4, 5}라고 하면, 교집합 A ∩ B = {1, 2, 2}, 합집합 A ∪ B = {1, 1, 2, 2, 3, 4, 5}가 되므로, 자카드 유사도 J(A, B) = 3/7, 약 0.42가 된다.

이를 이용하여 문자열 사이의 유사도를 계산하는데 이용할 수 있다. 문자열 FRANCE와 FRENCH가 주어졌을 때, 이를 두 글자씩 끊어서 다중집합을 만들 수 있다. 각각 {FR, RA, AN, NC, CE}, {FR, RE, EN, NC, CH}가 되며, 교집합은 {FR, NC}, 합집합은 {FR, RA, AN, NC, CE, RE, EN, CH}가 되므로, 두 문자열 사이의 자카드 유사도 J("FRANCE", "FRENCH") = 2/8 = 0.25가 된다.

제한 사항

입력 형식

  • 입력으로는 str1과 str2의 두 문자열이 들어온다. 각 문자열의 길이는 2 이상, 1,000 이하이다.
  • 입력으로 들어온 문자열은 두 글자씩 끊어서 다중집합의 원소로 만든다. 이때 영문자로 된 글자 쌍만 유효하고, 기타 공백이나 숫자, 특수 문자가 들어있는 경우는 그 글자 쌍을 버린다. 예를 들어 ab+가 입력으로 들어오면, ab만 다중집합의 원소로 삼고, b+는 버린다.
  • 다중집합 원소 사이를 비교할 때, 대문자와 소문자의 차이는 무시한다. AB와 Ab, ab는 같은 원소로 취급한다.

출력 형식

입력으로 들어온 두 문자열의 자카드 유사도를 출력한다. 유사도 값은 0에서 1 사이의 실수이므로, 이를 다루기 쉽도록 65536을 곱한 후에 소수점 아래를 버리고 정수부만 출력한다.

입출력 예

str1 str2 answer
FRANCE french 16384
handshake shake hands 65536
aa1+aa2 AAAA12 43690
E=M*C^2 e=m*c^2 65536

2. 어떻게 풀까?

  1. 두 문자열을 소문자 or 대문자로 바꿔주고, 2개의 문자열로 분할해서 배열 같은 곳에 따로 저장한다.
  2. 두 배열을 비교해서 같은 문자열이 있으면 교집합과 합집합 count를 추가해주고, 같은 문자열이 없으면 합집합 count만 추가해준다.
  3. 근데 한 배열에 중복된 문자열이 있을 수도 있으니까 배열보다 Map을 이용하는게 더 나아 보인다.
  4. 두 Map을 비교해서 같은 문자열이 있다.
    1. 교집합 - 문자열 중복 개수 중 작은 값
    2. 합집합 - 문자열 중복 개수 중 큰 값

3. 코드

 

테스트 코드

package programmers.level2.뉴스클러스터링_20201210;

import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.*;

import org.junit.Test;

public class SolutionTest {

  @Test
  public void test1() {
    assertThat(new Solution().solution("FRANCE", "french"), is(16384));
  }

  @Test
  public void test2() {
    assertThat(new Solution().solution("handshake", "shake hands"), is(65536));
  }

  @Test
  public void test3() {
    assertThat(new Solution().solution("aa1+aa2", "AAAA12"), is(43690));
  }

  @Test
  public void test4() {
    assertThat(new Solution().solution("E=M*C^2", "e=m*c^2"), is(65536));
  }
}

실제 코드

package programmers.level2.뉴스클러스터링_20201210;

import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;

public class Solution {

  int union = 0;
  int intersection = 0;

  public int solution(String str1, String str2) {
    Map<String, Integer> map1 = getStringSet(str1);
    Map<String, Integer> map2 = getStringSet(str2);

    return getSimilarity(map1, map2);
  }

  private int getSimilarity(Map<String, Integer> map1, Map<String, Integer> map2) {
    for (Entry<String, Integer> entry : map1.entrySet()) {
      String key = entry.getKey();
      int value = entry.getValue();
      if (map2.containsKey(key)) {
        intersection += Math.min(value, map2.get(key));
        union += Math.max(value, map2.get(key));
        map2.remove(key);
      } else {
        union += value;
      }
    }
    for (Entry<String, Integer> entry : map2.entrySet()) {
      union += entry.getValue();
    }

    return union == 0 ? 65536 : (int) Math.floor(65536 * ((double) intersection / union));
  }

  private Map<String, Integer> getStringSet(String string) {
    string = string.toLowerCase();
    Map<String, Integer> map = new HashMap<>();

    for (int i = 0; i < string.length() - 1; i++) {
      String sub = string.substring(i, i + 2);
      if (sub.matches("[a-z][a-z]")) {
        map.put(sub, map.getOrDefault(sub, 0) + 1);
      }
    }
    return map;
  }
}
  • getStringSet() 메서드를 이용해서 문자열을 분할하여 저장한다. 이때, 문자열에 알파벳을 제외한 문자가 있으면 저장하지 않는다.
  • getSimiliarty() 메서드에서 문자열 count를 저장한 두 Map을 비교하여 교집합 count와 합집합 count를 추가한다.
    • union이 0이면 0으로 나눌 수 없기 때문에 1로 가정한다.

4. 느낀 점

어렵지 않은 문제, 쉽게 풀었다.

1. 문제

https://programmers.co.kr/learn/courses/30/lessons/67257

 

코딩테스트 연습 - 수식 최대화

IT 벤처 회사를 운영하고 있는 라이언은 매년 사내 해커톤 대회를 개최하여 우승자에게 상금을 지급하고 있습니다. 이번 대회에서는 우승자에게 지급되는 상금을 이전 대회와는 다르게 다음과

programmers.co.kr

문제 설명

IT 벤처 회사를 운영하고 있는 라이언은 매년 사내 해커톤 대회를 개최하여 우승자에게 상금을 지급하고 있습니다. 이번 대회에서는 우승자에게 지급되는 상금을 이전 대회와는 다르게 다음과 같은 방식으로 결정하려고 합니다. 해커톤 대회에 참가하는 모든 참가자들에게는 숫자들과 3가지의 연산 문자 (+, -, *) 만으로 이루어진 연산 수식이 전달되며, 참가자의 미션은 전달받은 수식에 포함된 연산자의 우선순위를 자유롭게 재정의하여 만들 수 있는 가장 큰 숫자를 제출하는 것입니다. 단, 연산자의 우선순위를 새로 정의할 때, 같은 순위의 연산자는 없어야 합니다. 즉, + > - > * 또는 - > * > + 등과 같이 연산자 우선순위를 정의할 수 있으나 +,* > - 또는 * > +,-처럼 2개 이상의 연산자가 동일한 순위를 가지도록 연산자 우선순위를 정의할 수는 없습니다. 수식에 포함된 연산자가 2개라면 정의할 수 있는 연산자 우선순위 조합은 2! = 2가지이며, 연산자가 3개라면 3! = 6가지 조합이 가능합니다. 만약 계산된 결과가 음수라면 해당 숫자의 절댓값으로 변환하여 제출하며 제출한 숫자가 가장 큰 참가자를 우승자로 선정하며, 우승자가 제출한 숫자를 우승상금으로 지급하게 됩니다.

 

예를 들어, 참가자 중 네오가 아래와 같은 수식을 전달받았다고 가정합니다.

"100-200*300-500+20"

일반적으로 수학 및 전산학에서 약속된 연산자 우선순위에 따르면 더하기와 빼기는 서로 동등하며 곱하기는 더하기, 빼기에 비해 우선순위가 높아 * > +,- 로 우선순위가 정의되어 있습니다. 대회 규칙에 따라 + > - > * 또는 - > * > + 등과 같이 연산자 우선순위를 정의할 수 있으나 +,* > - 또는 * > +,-+,-처럼 2개 이상의 연산자가 동일한 순위를 가지도록 연산자 우선순위를 정의할 수는 없습니다. 수식에 연산자가 3개 주어졌으므로 가능한 연산자 우선순위 조합은 3! = 6가지이며, 그중+ > - > * 로 연산자 우선순위를 정한다면 결괏값은 22,000원이 됩니다. 반면에 * > + > - 로 연산자 우선순위를 정한다면 수식의 결괏값은 -60,420이지만, 규칙에 따라 우승 시 상금은 절댓값인 60,420원이 됩니다.

 

참가자에게 주어진 연산 수식이 담긴 문자열 expression이 매개변수로 주어질 때, 우승 시 받을 수 있는 가장 큰 상금 금액을 return 하도록 solution 함수를 완성해주세요.

 

제한 사항

  • expression은 길이가 3이상 100 이하인 문자열이다.
  • expression은 공백문자, 괄호 문자 없이 오로지 숫자와 3가지의 연산자(+, -, *)만으로 이루어진 올바른 중위 표기법으로 표현된 연산식이다. 잘못된 연산식은 주어지지 않는다.
    • 즉, 402+-561* 같은 식은 주어지지 않는다.
    • -56+100 처럼 피연산자가 음수인 수식도 입력으로 주어지지 않는다.
  • expression은 적어도 1개 이상의 연산자를 포함한다.
  • 연산자 우선순위를 어떻게 적용하더라도, expression의 중간 계산 값과 최종 결괏값은 절댓값이 2^63 - 1 이하가 되도록 입력이 주어진다.
  • 같은 연산자끼리는 앞에 있는 것의 우선순위가 더 높다.

입출력 예

1. 100-200*300-500+20 👉 * > + > - 순으로 연산자 우선순위를 정했을 때
100-(200*300)-500+20
100-60000-(500+20)
(100-60000)-520
(-59900-520)
-60420
절댓값 60420이 가장 큰 값이다.

 

2. 50*6-3*2 👉 - > * 순으로 연산자 우선순위를 정했을 때
50*(6-3)*2
(50*3)*2
150*2
300
절댓값 300이 가장 큰 값이다.


2. 어떻게 풀까?

  • 우선 사용되는 연산자의 종류는 최소 1개 최대 3개이다. 그러면, 연산자를 1개 사용했을 때는 우선순위 경우가 한 가지뿐이고, 2개를 사용했을 때는 우선 순위 경우가 두 가지, 3개를 사용했을 때는 6가지가 나온다.
  • 일단 우선순위 list나 array를 만들어 저장한뒤 우선순위 경우에 따라 주어진 expression을 계산하고 최댓값을 찾는 방법으로 풀면 될 것 같다.
  • 우선순위 list를 만드는 거는 순열을 이용하면 금방 만들 것 같아서 바로 해결, 계산하는 과정은 고민을 좀 많이 했다. 처음에는 정규식을 이용해서 replaceAll() 메서드로 할 수 있을까? 생각해서 접근해봤는데 안돼서 포기
  • 그럼 다른 방법이 있나??? 음.. expression을 연산자와 숫자로 구분해서 list를 만든 다음에 연산자에 해당하는 숫자들을 계산하면 될 것 같다. 연산자가 현재 우선순위에 일치하면 계산을 하고 list에서 빼준다. 숫자는 계산한 값을 해당 index에 삽입하면 될 것 같다.

3. 코드

 

TDD 연습을 위해 간단한 테스트 코드를 먼저 작성하고, 테스트 통과를 위한 코드를 작성 👉 다른 테스트 작성, 테스트 통과하도록 수정하는 과정을 반복했다.

 

테스트코드

package programmers.level2.수식최대화_20201030;

import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.*;

import org.junit.Test;

public class SolutionTest {

  @Test
  public void testWhenOnlyOneOperation() {
    assertThat(new Solution().solution("50*6*3*2"), is(1800L));
  }

  @Test
  public void testWhenTwoOperations() {
    assertThat(new Solution().solution("50*6-3*2"), is(300L));
  }

  @Test
  public void testWhenThreeOperations() {
    assertThat(new Solution().solution("100-200*300-500+20"), is(60420L));
  }
}

 

 

실제 코드

package programmers.level2.수식최대화_20201030;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

public class Solution {

  Set<String> operationPrioritySet;
  boolean[] visit;

  public long solution(String expression) {
    makeOperatorPriority(expression);

    long result = 0;
    for (String operationPriority : operationPrioritySet) {
      result = Math.max(result, calculateByPriority(expression, operationPriority));
    }
    return result;
  }

  private void makeOperatorPriority(String expression) {
    String distinctOperator = Arrays.stream(
        expression.replaceAll("[\\d]", "").split(""))
        .distinct()
        .collect(Collectors.joining());
    visit = new boolean[distinctOperator.length()];
    operationPrioritySet = new HashSet<>();
    permutation("", distinctOperator);
  }

  private long calculateByPriority(String expression, String operatorPriority) {

    List<Long> numbers = makeNumbers(expression);
    List<String> operators = makeOperators(expression);
    operators.remove(0);

    for (int i = 0; i < operatorPriority.length(); i++) {
      String targetPriorityOperator = operatorPriority.charAt(i) + "";
      while (operators.contains(targetPriorityOperator)) {
        int index = operators.indexOf(targetPriorityOperator);
        calculate(numbers, index, targetPriorityOperator);
        operators.remove(index);
      }
    }

    return Math.abs(numbers.get(0));
  }

  private void calculate(List<Long> numbers, int index, String operator) {
    if (operator.equals("*")) {
      numbers.add(index, numbers.remove(index) * numbers.remove(index));
    } else if (operator.equals("+")) {
      numbers.add(index, numbers.remove(index) + numbers.remove(index));
    } else {
      numbers.add(index, numbers.remove(index) - numbers.remove(index));
    }
  }

  private List<String> makeOperators(String expression) {
    return new ArrayList<>(Arrays.asList(expression.split("\\d{1,3}")));
  }

  private List<Long> makeNumbers(String expression) {
    return new ArrayList<>(Arrays.asList(
        Arrays.stream(expression.split("\\D"))
            .mapToLong(Long::parseLong)
            .boxed()
            .toArray(Long[]::new)));
  }

  private void permutation(String current, String operations) {
    if (current.length() == operations.length()) {
      operationPrioritySet.add(current);
      return;
    }

    for (int i = 0; i < operations.length(); i++) {
      if (!visit[i]) {
        visit[i] = true;
        permutation(current + operations.charAt(i), operations);
        visit[i] = false;
      }
    }
  }
}

4. 느낀 점

 

이 문제를 풀기 전에 정규 식중 전방 일치 관련 글을 봐서 정규식을 이용해서 풀면 풀 수 있지 않을까? 고민을 좀 오래 했었다. 여러 시도를 해봤는데 잘 안돼서 접고 list에 숫자, 연산자를 담아서 계산하는 방법으로 바꿨다. 판단을 빠르게 하는 게 중요한 듯

 

카카오 인턴 지원했을 때 이 문제를 풀었는데, 통과를 못했다. 몇 개는 통과하고 몇개는 실패해서.. 그때 내가 어떻게 풀었는지 기억이 잘 안 나는데, 어쨌든 이번엔 다 통과해서 기분이 좋다.

 

그때 어떻게 풀었는지 기록해뒀으면 비교 잘했을 텐데 그건 좀 아쉽다.

 

Level 2인데 이런 문제라니.. 카카오 x발..

+ Recent posts