프로그래밍/코딩테스트

[프로그래머스] 큰 수 만들기 / Swift

turu 2021. 5. 25. 00:46

[문제 보기]

더보기

어떤 숫자에서 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"

 

 

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

 

코딩테스트 연습 - 큰 수 만들기

 

programmers.co.kr


풀이 과정

프로그래머스에서의 문제조건에는 number와 k는 모두 1이상이라고 적혀있는데, 같은 경우는 어떤건지 싶어 실제 문제 출처에서 확인해보니 바로 이해가 갔다.

https://hsin.hr/coci/archive/2011_2012/contest4_tasks.pdf

 

 

문제를 여러번 다른 방법으로 시도했다.

처음에 풀었을 때는 조건에 맞는 가능한 모든 조합전부 구해서 그중 가장 큰 값을 반환하는 방법으로 구현했다.

 

 

1. DFS 재귀로 조합을 구함 -> 시간 초과 + 실패(signal: illegal instruction (core dumped))

(signal: illegal instruction (core dumped))라는 오류를 처음 겪었는데, 찾아보니 인덱스 범위 오류 또는 옵셔널 오류 등이라고 해서 다시 코드를 살펴 봤는데 긴 String이 주어졌을 때 Int로 바꾸는 과정에서 오류인가 싶어 그 부분을 고쳐보니 테스트 2번은 해결되었다. 그 전에 재귀 깊이 제한 때문인건가 싶어서 DFS 스택으로 바꿔서 풀어보기도 했다. (2번째 시도)

첫번째 시도 결과

[1번 시도 코드 보기]

더보기
// 1. DFS 재귀 조합 버전
import Foundation

var combinationResult = [Int]()
var numbers = [Int]()
var needToRemove = [[Int]]()

func combinationDFS(phase: Int, startIndex: Int, k: Int) {
    if phase == k {
        needToRemove.append(combinationResult)
        return
    } else {
        for i in (startIndex..<numbers.count) {
            combinationResult[phase] = i
            combinationDFS(phase: phase + 1, startIndex: i + 1, k: k)
        }
    }
}

func combination(_ number: String, _ k: Int) {
    numbers = number.map { Int(String($0))! }
    combinationResult = [Int](repeating: 0, count: k)
    
    combinationDFS(phase: 0, startIndex: 0, k: k)
}

func solution(_ number:String, _ k:Int) -> String {
    combination(number, k)

    // needToRemove에 있는 원소들 값에 해당하는 위치를 지워준다.
    var result = [Int]()
    needToRemove.forEach { (indices) in
        let remainder = numbers.enumerated()
            .filter { !indices.contains($0.offset) }
            .reduce(0) { $0 * 10 + $1.element }
        result.append(remainder)
    }
    
    return "\(result.max()!)"
}

2. DFS 스택으로 조합을 구함 -> 시간초과

(signal: illegal instruction (core dumped))오류 때 재귀 깊이 때문인건가 싶어 스택을 이용한 방법으로 바꾸었다.

실제 오류 원인은 String->Int 변환 부분이었다.

// 2. DFS 스택 조합 버전
import Foundation

var needToRemove = [[Int]]()

func combinationWithStack(length: Int, k: Int) {
    var stack = [Int]()
    var temp = 0
    
    stack.append(0)
    while !stack.isEmpty {
        if stack.count == k {
            needToRemove.append(stack)
            temp = stack.removeLast()
            continue
        }
        
        if temp + 1 == length {
            temp = stack.removeLast()
            if temp + 1 == length {
                continue
            }
        }
        temp += 1
        stack.append(temp)
    }
}

func solution(_ number:String, _ k:Int) -> String {
    let numbers = number.map { Int(String($0))! }
    
    combinationWithStack(length: numbers.count, k: k)

    // needToRemove에 있는 원소들 값에 해당하는 위치를 지워준다.
    var result = [Int]()
    needToRemove.forEach { (indices) in
        let remainder = numbers.enumerated()
            .filter { !indices.contains($0.offset) }
            .reduce(0) { $0 * 10 + $1.element }
        result.append(remainder)
    }
    
    return "\(result.sorted(by: >)[0])"
}

 

3. 2번 코드를 C++로 바꿈 -> 시간초과

C++은 좀 다른가 싶어 바꿔봤는데 역시나 시간초과가 나옴. 이때부터 새로 다시풀어야겠다고 생각함.

// 3. 2번에서 구현(DFS 스택 조합)한 것을 C++로 컨버팅한 버전
#include <iostream>
#include <vector>
#include <string>

using namespace std;

vector<int> numbers;
vector<int> needToRemove;
string maxValue = "";

void combinationWithStack(int length, int k) {
	vector<int> stack;
	int temp = 0;
	string remainder = "";

	stack.push_back(temp);
	remainder.push_back('0' + numbers[temp]);
	while (stack.size() != 0) {
		if (stack.size() == k) {
			if (maxValue < remainder) {
				maxValue = remainder;
			}
			remainder.pop_back();
			temp = stack.back();
			stack.pop_back();
			continue;
		}

		if (temp + 1 == length) {
			temp = stack.back();
			stack.pop_back();
			remainder.pop_back();
			if (temp + 1 == length) {
				continue;
			}
		}
		temp += 1;
		stack.push_back(temp);
		remainder.push_back('0' + numbers[temp]);
	}
}

string solution(string number, int k) {
	for (int i = 0; i < number.size(); i++) {
		int tmp = number.at(i) - '0';
		numbers.push_back(tmp);
	}
	
	combinationWithStack(number.size(), number.size() - k);
	string answer = maxValue;

	return answer;
}

 

4. 최종코드 (그리디)

그리디의 기본 개념인 각 단계에서 최선의 선택을 하는 방법으로 해결했다.

여기서 각단계는 무엇을 이야기하는 걸까?

나는 아래와 같이 풀었다.

예를들어 입력값이 각각 [1,2,3,1,2,3,4], 제거할 횟수k = 3으로 주어졌을 때,

k는 전체 빼야 할 횟수(단계)라고 보고 1개씩 뺄때마다 가장 큰 값이 되도록 구했다.

어차피 빼야할 횟수(모든 뺀 다음의 수의 자릿수)는 정해져있으므로 가장 앞자리가 제일 크게 유지하도록 (각 단계에서 가장 최선의 선택을 유지) 했다. 

스택을 이용해서 앞자리가 가장 큰 상태를 유지하도록 했다.

(코드에서 phase 1 부분)

[1,2,3,1,2,3,4] 에서 스택에 맨 위의 원소와 현재 비교하는 원소를 비교한다. 빈 스택일 경우에는 단순히 append해주기만 하고, 만약에 스택의 맨 위 원소가 비교원소보다 작다면, 그리고 아직 제거횟수(count ==  k)가 남아있다면 (앞자리가 클수록 큰 수가 되므로 ) pop해준다. 이 비교하는 과정을 스택의 맨 밑바닥까지 반복하거나 아니면 비교원소보다 큰 수가 나올때까지 반복한다. 이 과정의 결과로 스택에는 가장 큰 앞자리수가 차곡차곡 쌓이게 된다.

 

가장 바깥 반복문은 입력받은 [1,2,3,1,2,3,4]를 계속순회를 하는데, 반복문이 끝날 때까지 count가 0이 아니게 될 경우가 발생할 때가 있다. ( [1,0,1,0,1,0], k = 4 ) 이 경우는 실제로 문제에서 요구하는 k개의 숫자를 제거하지 못한 경우다. ( 스택에는 1110이 남아있어 4자리수지만 실제로 문제조건에 맞추려면 2자리수를 반환해야함 )

스택에는 아래서부터 위쪽 방향으로 내림차순으로 정리가 되어있는 상태이므로 아래보다는 위쪽에 상대적으로 작으므로 스택의 위쪽에서 부터 남은 갯수만큼 지워나간다. (코드에서 phase 2, 3 부분)

미처 지우지 못한 남은 갯수만큼 지운 결과로 문제 조건에 맞는 자릿수의 수가 확실하게 나오게 된다.

 

// 4. 최종 버전
import Foundation

func solution(_ number:String, _ k:Int) -> String {
    let numbers = number.map { Int(String($0))! }
    var count = k
    var stack = [Int]()
    var lastIndex = numbers.count - 1
    
    // phase 1
    for (i, element) in numbers.enumerated() {
        while true {
            if let last = stack.last, last < element, count > 0 {
                stack.removeLast()
                count -= 1
            } else {
                stack.append(element)
                break
            }
        }
        
        if count == 0 {
            lastIndex = i
            break
        }
    }
	// phase 2
    let start = number.index(number.startIndex, offsetBy: lastIndex + 1)
    let range = start..<number.endIndex
    
    // phase 3
    for _ in (0..<count) {
        stack.removeLast()
    }
    
    return stack.reduce("") { $0 + String($1) } + number[range]
}

 


생각 정리

계속 시간초과가 떠서 최댓값을 구하는 과정같은 부분에서 자잘한 부분에서 최적화를 시도해보았는데 그다지 소용이 없었다.

결국에는 방법 자체를 바꾸어서 해결했다.

알고리즘 자체를 다른 방법으로 접근하려고 했으면 좀 더 빨리 풀었을 것 같다.

반응형