[ALGORITHM]PROGRAMMERS/Greedy-큰수만들기

문제

어떤 숫자에서 k개의 수를 제거했을 때 얻을 수 있는 가장 큰 숫자를 구하려 합니다.

예를 들어, 숫자 1924에서 수 두 개를 제거하면 [19, 12, 14, 92, 94, 24] 를 만들 수 있습니다. 이 중 가장 큰 숫자는 94 입니다.

문자열 형식으로 숫자 number와 제거할 수의 개수 k가 solution 함수의 매개변수로 주어집니다. number에서 k 개의 수를 제거했을 때 만들 수 있는 수 중 가장 큰 숫자를 문자열 형태로 return 하도록 solution 함수를 완성하세요.

제한 조건
  • number는 1자리 이상, 1,000,000자리 이하인 숫자입니다.
  • k는 1 이상 number의 자릿수 미만인 자연수입니다.
입출력 예
number k return
1924 2 94
1231234 3 3234
4177252841 4 775841

코드 ver1-순열

import java.util.ArrayList;
import java.util.Collections;

class Solution {
    static ArrayList<String> arr = new ArrayList<>();

    public static String solution(String number, int k) {
        String answer = "";
        DFS(0, 0, number.length() - k, number, "");

        Collections.sort(arr);
        return arr.get(arr.size()-1);
    }

    public static void DFS(int start, int depth, int size, String number, String result) {
        if (depth == size) {
            arr.add(result);
            return;
        }

        for (int i = start; i < number.length(); i++) {
            DFS(i + 1, depth + 1, size, number, result + number.charAt(i));
        }
    }
}
  • 처음은 딱 생각 난 것은 모든 순열 구하고 가장 큰 수 구하면 되겠다 ! 였다.
  • 이것은 정말로 최악의 코드 였다. 모든 순열을 다 해보는 이 작업는 시간초과 공간초과 파티였다.
  • 그래서 머리를 좀 더 쓰기로 했다.

코드 ver 2-뒤에 문자 비교

import java.util.ArrayList;

class Solution {
    public String solution(String number, int k) {
       StringBuilder answer = new StringBuilder();
        ArrayList<Character> arrayList = new ArrayList<>();

        for (int i = 0; i < number.length(); i++) {
            arrayList.add(number.charAt(i));
        }

        for (int i = 0; i < k; i++) {
            for (int j = 0; j < arrayList.size() - 1; j++) {
                if (arrayList.get(j) < arrayList.get(j + 1)) {
                    arrayList.remove(j);
                    break;
                }
            }
        }

        for (int i = 0; i < number.length() - k; i++) answer.append(arrayList.get(i));
        return answer.toString();
    }
}

결과를 보자면 이러하다. 저 10번 문제가 도대체 뭔지 모르겠다 !!!!!!!

테스트 1 〉통과 (1.10ms, 52.3MB)
테스트 2 〉통과 (1.11ms, 50.8MB)
테스트 3 〉통과 (1.06ms, 50.2MB)
테스트 4 〉통과 (2.38ms, 50.7MB)
테스트 5 〉통과 (4.12ms, 52.3MB)
테스트 6 〉통과 (47.87ms, 52.6MB)
테스트 7 〉통과 (101.85ms, 56.8MB)
테스트 8 〉통과 (559.95ms, 58.4MB)
테스트 9 〉통과 (129.13ms, 81.8MB)
테스트 10 〉실패 (시간 초과)
테스트 11 〉통과 (0.96ms, 52.7MB)
테스트 12 〉통과 (1.00ms, 50.4MB)
  • 일단 모든 순열을 만들어서 찾는 것은 무식한 방법이라는 것을 알고 다시 생각했다.

  • 가장 큰 수를 구하려면 가장 앞에 있는 숫자가 가장 커야한다.

  • 즉, 앞에서부터 작은 숫자들을 지워나가면 된다.

    for (int i = 0; i < k; i++) {
      for (int j = 0; j < arrayList.size() - 1; j++) {
        if (arrayList.get(j) < arrayList.get(j + 1)) {
          arrayList.remove(j);
          break;
        }
      }
    }
    

    현재 숫자와 다음숫자를 비교해서 다음숫자가 크면, 현재 숫자를 지워주는 셈이다.

    하지만 821에서 2개를 지워야 한다면, 8이 정답이 될 것이다. 하지만 위의 알고리즘상 for문을 끝내도 821상태일 것이다.

  • 즉 저 for 문을 끝내면 내림차순으로 정렬이 되어 있을 것이다.

    for (int i = 0; i < number.length() - k; i++) answer.append(arrayList.get(i));
    return answer.toString();
    

    따라서 이런식으로, 원하는 갯수만큼 앞에서 숫자를 가져와서 결과를 만들어 내면 된다.

코드 ver 3 - 범위정해줘서 가장 큰 수 뽑음

class Solution {
    public String solution(String number, int k) {
        StringBuilder answer = new StringBuilder();
        int startIndex = 0;
        int length = number.length() - k;
        int endIndex = number.length()-length+1;

        for (int i = 0 ; i < number.length()-k; i++){
            int max = Integer.MIN_VALUE;
            int index = 0;
            for (int j = startIndex; j < endIndex+i; j++){
                if (max < number.charAt(j)-'0'){
                    max = number.charAt(j)-'0';
                    index = j;
                }
            }
            answer.append(max);
            startIndex = index+1;
        }
        return answer.toString();
    }
}
  • 두번째 방법에서는 숫자들을 제거하면서 가장 큰 수를 뽑았다면, 이 문제에서는 범위 내에서 가장 큰 숫자들을 뽑는 방법이다 (새로운 발상이다 👉 아이디어 from 미진)

  • 예시를 들면서 설명을 하겠다.

    입력: 4177252841 / k : 4

    라면, 만들어야 하는 문자의 길이는 number.legth - 4 = 6이다.

    따라서,

    for (int i = 0 ; i < number.length()-k; i++){//이만큼 반복한다.
    }
    
  • 문자를 뽑는 기준은,(설명 어려워서 막막)

    4177252841 에서 우선 1개를 뽑아야한다. 총 6개를 뽑아야 하기 때문에 가장 큰것을 8을 뽑는다면 그 다음꺼에서 뽑을 게 없어진다. 따라서

    startIndex와 endIndex로 범위를 정해준다.

    처음에는 0에서부터 number.length - 뽑아야할길이 +1이다. 즉 (0 <= x < 5) 다.

  • 0 <= x < 5 Max: 7 , index:2
    3 <= x < 6 Max: 7 , index:3
    4 <= x < 7 Max: 5 , index:5
    6 <= x < 8 Max: 8 , index:7
    8 <= x < 9 Max: 4 , index:8
    9 <= x < 10 Max: 1 , index:9

    코드를 보면

    class Solution {
        public String solution(String number, int k) {
            StringBuilder answer = new StringBuilder();
            int startIndex = 0;
            int length = number.length() - k;
            int endIndex = number.length()-length+1;
      
            for (int i = 0 ; i < number.length()-k; i++){
                int max = Integer.MIN_VALUE;
                int index = 0;
                
              //endIndex+i인 이유는 한글자씩 선택될때마다 범위가 줄기 때문이다.
                for (int j = startIndex; j < endIndex+i; j++){ 
                  //범위내에서 가장 큰 수와 그 수의 인덱스 저장한다.
                    if (max < number.charAt(j)-'0'){
                        max = number.charAt(j)-'0';
                        index = j;
                    }
                }
                answer.append(max);
                startIndex = index+1;//다음 범위의 시작은 가장큰수 뽑은거 다음부터다!
            }
            return answer.toString();
        }
    }
    
느낀점

스터디를 하다보니 다양한 방법들로 풀 수 있어서 좋았다. 도대체 난 왜 시간초과가 나는지 몰랐는데 새로운 아이디어로 푸니 시간초과가 사라졌다. 뿌듯했다.

‼️ 그리고 String + 을 하면 매번 내부에서 StringBuilder 가 불린다고 한다. 그러니까 한번 StringBuilder를 생성해서 append로 붙여주면, 나중에 toString해서 한번만 해도 된다 .!

문제 : https://programmers.co.kr/learn/courses/30/lessons/42883

저장소

  • https://github.com/yoo-chaewon/HELLO_JAVA/blob/master/Algorithm/2.PROGRAMMERS/with%20Study/Greedy_큰수만들기_순열_시간초과.java
  • https://github.com/yoo-chaewon/HELLO_JAVA/blob/master/Algorithm/2.PROGRAMMERS/with%20Study/Greedy_큰수만들기_91.7_시간초과.java
  • https://github.com/yoo-chaewon/HELLO_JAVA/blob/master/Algorithm/2.PROGRAMMERS/with%20Study/Greedy_큰수만들기.java