-
그리디_프로그래머스_큰 수 만들기알고리즘/알고리즘 문제풀이 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
- 위 number.count - k = 6이다. 최종적으로 return될 문자열의 길이임.
- 첫번째 숫자를 뽑을 때, 뽑힌 숫자 외의 5개의 숫자가 더 필요하므로, 뒤에서부터 5개의 숫자는 제외하고(52841) 나머지(41772) 중 가장 큰 숫자를 뽑는다(7), 제외한 숫자의 갯수를 카운트한다(+2)
- 뽑힌 숫자(7)의 앞부분(41)을 제외하고, 남은 숫자(7252841)중 4개의 숫자를 제외하고 (2841) 나머지 중(725) .max()하여 뽑는다. (7)
- 252841 중 뒤의 3개숫자를 제외하고(841) 남은숫자(252)중 max를 뽑는다, 이 중 앞부분은 버린다.(5) 버린숫자.count += 1
- 2841중 뒤의 2개를 제외하고, max를 뽑는다(8) 앞 부분은 버린다(2) 버린숫자 += 1 하여 k와 같아진다. 반복을 종료한다. 이 떄 반복을 종료하는 조건은 2가지로 한다. 버린숫자.count == k || 남은숫자열.count + 뽑은숫자열.count == number.count - k
- 반복 종료 조건이 두 가지인 이유는 k 가 작거나,
- number.count - k 가 작은 case를 빠르게 완료시키기 위함.
- 남은 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:) 로 인한 배열 내 순회가 영향을 끼쳤다고 생각한다.
배열의 순회를 최소화하는 것이 퍼포먼스에 많은 영향을 끼친다는 것을 다시 한 번 느끼게 해 준 문제에 감사!
'알고리즘 > 알고리즘 문제풀이' 카테고리의 다른 글
배열_LeetCode_Queens That Can Attack the King (0) 2021.08.06 DFS/BFS_LeetCode_keys-and-rooms (0) 2021.08.04 해쉬_LeetCode_Group-Anagrams (0) 2021.07.26 완전탐색_프로그래머스_카펫 (0) 2021.07.22 DFS_LeetCode_Sum Root to Leaf Numbers (0) 2021.07.21