ABOUT ME

Today
Yesterday
Total
  • 그리디_프로그래머스_큰 수 만들기
    알고리즘/알고리즘 문제풀이 2021. 8. 2. 16:52

    https://programmers.co.kr/learn/courses/30/lessons/42883?language=swift 

     

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

     

    programmers.co.kr

     

    문제 접근

    하나씩 빼면서 제일 큰 경우를 sol에 저장해서 k번 반복하려고 했으나,,,

    test case실패 및 time over

    실패 코드

    더보기
    import Foundation
    
    func solution(_ number:String, _ k:Int) -> String {
        let arr = number.map { String($0)}
        var sol = 0
        
        for j in 0..<k {
            var temp : [String] = []
            if j == 0 {
                temp = arr
            } else {
                temp = String(sol).map { String($0)}
            }
            
            for i in 0..<temp.count {
                var tmp = temp
                tmp.remove(at:i)
                guard let num = Int(tmp.joined()) else {
                    continue
                }
                
                if i == 0 {
                    sol = num
                } else if num > sol {
                    sol = num
                }
            } 
            
        }
        
        
        return String(sol)
    }

    2일차

    접근 방법

    이전에는 하나씩 뺀 경우를 비교했다면, 다른 이의 접근방법을 참고하여, i, i+1인덱스의 숫자를 비교해서 진행했다.

    실패 코드

    더보기
    import Foundation
    
    func solution(_ number:String, _ k:Int) -> String {
        var arr = number.map { String($0)}
        var sol = 0
        var skipCounter = 0
        
        for j in 0..<k {
            if arr.first! == "0" {
                arr.removeFirst()
                continue
            }
            
            if skipCounter > arr.count-1 {
                skipCounter = arr.count-2
            }
            if skipCounter < 0 {
                skipCounter = 0
            }
            
            for i in skipCounter..<arr.count - 1 {
                if Int(arr[i])! < Int(arr[i+1])! {
                    arr.remove(at:i)
                    skipCounter = i - 1
                    break
                }
                
                if i == arr.count - 2 {
                    arr.remove(at: i+1)
                }
            }
        }
        
        
        return arr.joined()
    }

    결과

    테스트 케이스 결과

    더보기
    테스트 1 통과 (0.07ms, 16.2MB)
    테스트 2 통과 (0.07ms, 16.2MB)
    테스트 3 통과 (0.14ms, 16.6MB)
    테스트 4 통과 (0.30ms, 16.5MB)
    테스트 5 통과 (0.76ms, 16.4MB)
    테스트 6 통과 (36.09ms, 16.5MB)
    테스트 7 통과 (194.12ms, 16.8MB)
    테스트 8 통과 (1874.52ms, 16.9MB)
    테스트 9 통과 (138.72ms, 21MB)
    테스트 10 실패 (시간 초과)
    테스트 11 통과 (0.06ms, 16.3MB)
    테스트 12 통과 (0.06ms, 16.2MB)

    몇몇 케이스에 많은 시간 소요가 발생한다.

    또 다른 이의 접근 방법을 확인했는데, stack으로 풀 경우 매우 효율적으로 해결 가능하다고 한다.

    내 코드에서는 remove(at) 혹은 first에서 배열의 변형이 발생하므로, 많은 자원적 소모가 있을것으로 생각한다. ( 반복문은 당연히 사용하겠지..?, 하지만 이중 반복문도 해결 가능한 것일까?)

     

    해당 문제는 3일차도 계속된다....

     

    21.12.01

    한참 지나 다시 풀어보았다.

    더보기
    import Foundation
    
    func solution(_ number:String, _ k:Int) -> String {
        var arr = number.map { Int(String($0))!}
        
        for j in 0..<k {
            if arr.first == nil {
                break
            }
            
            if arr.first! == 0 {
                arr.removeFirst()
                continue
            }
            
            for (index, element) in arr.enumerated() {
                if index + 1 >= arr.count {
                    // print("index error")
                    arr.removeLast()
                    break
                }
                
                if element < arr[index + 1] {
                    // print("index: \(index) is removed. \(element) \(arr[index])")
                    arr.remove(at: index)
                    break
                } 
            }
        }
        let sol = arr.map({String($0)})
        
        return sol.joined()
    }

    조금 더 깔끔히 짜기는 했지만, 해당 포스트를 다시 보기 전 까지, 

    stack을 사용한다는 생각을 하지 못했다. 심지어 쓰고도 못 풀었다.

     

    stack이라는 힌트를 알고도 못 푼 코드

    더보기
    import Foundation
    import Dispatch
    
    func solution(_ number:String, _ k:Int) -> String {
        var arr = number.reversed().map { Int(String($0))!}
        var stack = [Int]()
        
        while stack.count != number.count - k {
            if arr.isEmpty == true {
                break
            }
            
            if arr.count + stack.count <= number.count - k {
                stack.append(contentsOf: arr.reversed().map({$0}))
                break
            }
            
            let first = arr.removeLast()
            
            if first == 0 {
                continue
            }
            
            if stack.isEmpty == true {
                stack.append(first)
                continue
            }
            
            if stack.last! < first {
                stack[stack.endIndex - 1] = first
                continue
            } else {
                stack.append(first)
                continue
            }
            
        }
        
        while stack.count > number.count - k {
            stack.remove(at: stack.firstIndex(where: {
                $0 == stack.min()
            })!)
        }
        
        var sol = stack.map({String($0)})
        
        return sol.joined()
    }

    이제는 최선을 다 한것 같으므로, 이 후는 고수의 코드를 참조한다.

     

    고수의 풀이 참조

    고수의 의식의 흐름을 읽고 와서, 재창조한 로직, 입력 testCase는  number: "4177252841", k: 4

    1. 위 number.count - k = 6이다. 최종적으로 return될 문자열의 길이임.
    2. 첫번째 숫자를 뽑을 때, 뽑힌 숫자 외의 5개의 숫자가 더 필요하므로, 뒤에서부터 5개의 숫자는 제외하고(52841) 나머지(41772) 중 가장 큰 숫자를 뽑는다(7), 제외한 숫자의 갯수를 카운트한다(+2)
    3. 뽑힌 숫자(7)의 앞부분(41)을 제외하고, 남은 숫자(7252841)중 4개의 숫자를 제외하고 (2841) 나머지 중(725) .max()하여 뽑는다. (7)
    4. 252841 중 뒤의 3개숫자를 제외하고(841) 남은숫자(252)중 max를 뽑는다, 이 중 앞부분은 버린다.(5) 버린숫자.count  += 1
    5. 2841중 뒤의 2개를 제외하고, max를 뽑는다(8) 앞 부분은 버린다(2) 버린숫자 += 1 하여 k와 같아진다. 반복을 종료한다. 이 떄 반복을 종료하는 조건은 2가지로 한다. 버린숫자.count == k ||  남은숫자열.count + 뽑은숫자열.count == number.count - k
      • 반복 종료 조건이 두 가지인 이유는 k 가 작거나,
      • number.count - k 가 작은 case를 빠르게 완료시키기 위함.
    6. 남은 41을 뽑은 숫자 뒤에 붙여서 return한다.

     

    개선 코드

    더보기
    import Foundation
    import Dispatch
    
    func solution(_ number:String, _ k:Int) -> String {
        var arr = number.map { Int(String($0))!}
        var stack = [Int]()
        var removeCount = 0
        
        let targetNumberCount = number.count - k
        
        while !(removeCount == k || stack.count >= targetNumberCount) {
            
            if arr.isEmpty == true {
                break
            }
            
            let excludingCorrection = targetNumberCount - 1 - stack.count
            
            let corrctedEndIndex = arr.index(arr.endIndex, offsetBy: -excludingCorrection)
            
            let targetClosedRange = arr[arr.startIndex..<corrctedEndIndex]
            guard let max = targetClosedRange.max() else {
                arr.removeFirst()
                continue
            }
            
            guard let maxNumIndex = arr.firstIndex(of: max) else {
                arr.removeFirst()
                continue
            }
            
            let removeRange = arr.startIndex...maxNumIndex
            
            arr.removeSubrange(removeRange)
            removeCount += removeRange.count - 1
            stack.append(max)
            
            if removeCount == k || arr.count + stack.count <= targetNumberCount {
                stack.append(contentsOf: arr)
                break
            }
        }
        
        var sol = stack.map({String($0)})
        
        return sol.joined()
    }

    결과

    이전 2일차 보다 성능이 나빠졌다. 성능의 개선을 위해 arr의 수정 및 removeCount를 세기를 포기한다

    • 참고한 고수의 코드가 그렇다..
    • 이는 arr의 수정에 의해 발생한 성능 저하로 생각된다.
    테스트 1 통과 (0.17ms, 16.4MB)
    테스트 2 통과 (0.20ms, 16.6MB)
    테스트 3 통과 (0.27ms, 16.5MB)
    테스트 4 통과 (0.71ms, 16.8MB)
    테스트 5 통과 (1.86ms, 16.6MB)
    테스트 6 통과 (464.84ms, 16.8MB)
    테스트 7 통과 (1273.03ms, 17.7MB)
    테스트 8 실패 (시간 초과)
    테스트 9 통과 (214.87ms, 25.5MB)
    테스트 10 실패 (시간 초과)
    테스트 11 통과 (0.17ms, 16.6MB)
    테스트 12 통과 (0.16ms, 16.6MB)

     

    고수 참조 풀이의 개선

    arr의 변형을 하지 않는다.

    -> arr[startIndex...로직에 따른 endIndex]와 같은 방법을 사용한다.

    반복 종료 조건을 closedRange의 startIndex와 endIndex가 같을 때로 정한다.

     

    더보기
    func solution(_ number:String, _ k:Int) -> String {
        var arr = number.map { Int(String($0))!}
        var stack = [Int]()
        
        var startIndexCorrection = 0
        let targetNumberCount = number.count - k
        
        while stack.count < targetNumberCount {
            
            if arr.isEmpty == true {
                break
            }
            
            let excludingCorrection = targetNumberCount - stack.count - 1
            
            let corrctedEndIndex = arr.index(arr.endIndex, offsetBy: -excludingCorrection - 1)
            
            
            let targetClosedRange = arr[startIndexCorrection...corrctedEndIndex]
            
            guard let max = targetClosedRange.max() else {
                break
            }
            
            guard let maxNumIndex = targetClosedRange.firstIndex(of: max) else {
                break
            }
            
            
            startIndexCorrection = maxNumIndex + 1
            stack.append(max)
            
            if startIndexCorrection == corrctedEndIndex {
                stack.append(contentsOf: arr[(corrctedEndIndex + 1)..<arr.endIndex])
                break
            }
        }
        
        var sol = stack.map({String($0)})
        
        return sol.joined()
    }
    테스트 1 통과 (0.12ms, 16.4MB)
    테스트 2 통과 (0.15ms, 16.6MB)
    테스트 3 통과 (0.21ms, 16.5MB)
    테스트 4 통과 (0.62ms, 16.7MB)
    테스트 5 통과 (2.13ms, 16.3MB)
    테스트 6 통과 (463.45ms, 16.6MB)
    테스트 7 통과 (1090.71ms, 16.8MB)
    테스트 8 실패 (시간 초과)
    테스트 9 실패 (155.17ms, 25.1MB)
    테스트 10 실패 (시간 초과)
    테스트 11 통과 (0.10ms, 16.6MB)
    테스트 12 실패 (0.11ms, 16.3MB)

    .... 좌절감이 느껴진다.

    이렇게 된 이상, 고수의 알고리즘 코드를 클론코딩한다!

     

    클론 코딩

    나만의 맛을 추가하고 싶었는데,,,, 추가하기 정말 애매하다,,,,

    아래의 코드는 몇 번 반복해서 써보며, 익힌 후 작성한 코드이다.

    원문과는 정말 별 것 아닌 부분만 바뀐 거의 똑같은 코드.

     

    간단한 해설과 코드

    더보기
    func solution(_ number:String, _ k:Int) -> String {
        var arr = number.map { String($0) } // Array(number) 만을 사용하면 Array<Character>가 된다. 이는 stack.last! < arr[i]를 사용 할 수 없게 함.
        var removeCount = 0 // 제거한 요소를 count 하는 변수
        var stack = Array<String>() //정답이 될 요소들이 들어가는 stack
        
        for i in 0..<number.count { // 각 글자에 대하여 반복함.
            while removeCount < k && !stack.isEmpty && stack.last! < arr[i] {
            
            // while 조건을 쉽게 이해하고 싶은 사람들을 위한 3줄 요약
            // 1. 지운 숫자의 갯수가 k 보다 작을 것
            // 2. stack이 empty가 아닐 것
            // 3. stack.last!가 arr[i] 보다 작을 것.
            
                stack.removeLast()  // 스택의 마지막 요소가 arr[i]보다 작으니 지운다. 아래에서 arr[i]를 추가하며 더 큰 수를 만들기 위함.
                removeCount += 1 //지웠으니까 count를 하나 늘린다.
            }
            
            if removeCount >= k { // 사실 부등호는 의미 없다고 생각한다. 등호로 작성해도 문제없이 작동 할 것이며, 실제 문제 제출과 테스트케이스에서도 문제없이 작동했다.
                stack.append(contentsOf: arr[i...]) //이미 지울 수 있는 횟수를 모두 사용했으므로, 나머지는 지울 수 없으므로, 모두 stack에 들어가는 것.
                break // 반복을 통해 뒤에 사용되야할 i...endIndex까지를 바로 위의 코드에서 모두 stack.append 했으므로, for문을 break한다.
            } else { // 지울 수 있는 횟수가 남아있으므로, while문이 arr[i+1]을 대상으로 작동 할 수 있다.
                stack.append(arr[i])
            }
        }
        
        return String(stack.joined().prefix(number.count - k)) //prefix하면 type: SubString이므로, String으로 변환한다.
        // number.count - k 는 주어진 string의 갯수에서 뺄 수 있는 횟수인 k를 빼낸 수. 최종으로 나와야 할 글자의 수다.
    }

     

    성능 

    더보기
    테스트 1 통과 (0.12ms, 16.7MB)
    테스트 2 통과 (0.13ms, 16.6MB)
    테스트 3 통과 (0.16ms, 16.3MB)
    테스트 4 통과 (0.31ms, 16.4MB)
    테스트 5 통과 (0.47ms, 16.7MB)
    테스트 6 통과 (6.57ms, 16.8MB)
    테스트 7 통과 (15.45ms, 16.9MB)
    테스트 8 통과 (38.28ms, 18MB)
    테스트 9 통과 (54.33ms, 25.7MB)
    테스트 10 통과 (212.54ms, 27.9MB)
    테스트 11 통과 (0.09ms, 16.2MB)
    테스트 12 통과 (0.06ms, 16.5MB)

     

    회고

    WorkDay로만 4일 정도 들어간 알고리즘 문제.

    로직이 좀 구린내가 날 지라도, 정상 작동하지만, 성능면에서 너무 나빠서 몇번이고 시도했던 문제였다.

    자력으로 풀지 못한점에서 너무 아쉽다.

     

    나의 마지막 코드와 고수의 코드의 차이는

    max(), firstIndex(where:) 로 인한 배열 내 순회가 영향을 끼쳤다고 생각한다.

    배열의 순회를 최소화하는 것이 퍼포먼스에 많은 영향을 끼친다는 것을 다시 한 번 느끼게 해 준 문제에 감사!

Designed by Tistory.