안녕하세요. 이번 글은 Swift로 백준 사이트에서 알고리즘 문제의 입력을 받는 방법에 대해서 소개하려고 합니다.
백준 사이트를 처음 접한 후 어느 문제를 봤을 때, 알고리즘으로 어떻게 풀 지 생각이 떠오르는 것 같은데 입력을 어떻게 받을지 정말 몰랐습니다.
C나 C++은 입력받는 다양한 방법을 문법 공부 하자마자 배웠는데.. Swift는 앱 만들때 입력도 텍스트 필드로 받은 기억만 있었습니다. UIKit 컴포넌트 백준 제출 사이트에 추가할 수도 없고,,,,
다른 블로그의 글을 봤을 때, 엄청 낯선 함수들이 많았습니다.
readLine()!.split(separator:"").map{...} // 이게 뭐지,,
"UIKit으로 앱 만들 때 이런 함수들 한 번도 안 써봤는데" 하면서 뒷걸음질 쳤던 적도 있었고, 이 함수를 복사 붙여넣기 하면서, 도데체 왜, 어떤 결과가 반환되는지도 모른 채로 쓴 기억이 새록 새록 떠오릅니다. (현재, 입력 만큼은 잘 받을 자신 있습니다)
아직도 알고리즘 문제 접근하기가 어려운 문제들이 많은데, 계속 하다보니(꾸준히는 아닙니다.. 재학 기간 중 시험기간도 과제도 있고 UIKit 프레임워크도 공부하다보니 중간 중간 멀리하게 됬지만 계속하려고 다짐합니다.) 언제부터인지 모르겠지만 브론즈, 실버로 가득 찼던 상위 100문제가 골드로 뒤덮였습니다. (내년에는 플레 문제로 뒤덮여 있으면 좋겠네요,,)
백준을 풀기 위해서는 문제의 입력을 받아야 합니다. 프로그래머스에서는 지정된 함수의 매개변수에 입력을 자동으로 넣어 주지만, 백준 사이트는 입력의 형태가 이런 식이다~로 알려줍니다. 이 입력을 어떻게 받을 지는 개인 취향 차이입니다.
개인 취향 존중?(백준), 표준으로 입력 받을 텐가?(프로그래머스)로 비유할 수 있겠습니다.
그래서 백준은 거의 대부분의 문제에서 입력을 직접 받아야 합니다. 표준 입력 함수를 사용해야 사이트에서 어느 입력을 주든 해당 입력 함수를 통해 받은 값을 문제에 맞는 알고리즘 설계와 출력을 통해 채점을 할 수 있습니다.
문제마다 입력 방식이 다르지만,
이렇게 예제 입력과 이 입력에 대한 문제의 로직 구한 후에는 2라는 결과를 print()함수로 출력해야 합니다.
위의 사진에서, 예제를 입력받기 위해서는 readLine() 함수를 통해 한 라인씩 입력을 받아야 합니다. 그리고 readLine() 함수를 사용하기 위해선 플레이그라운드가 아닌 mac OS의 commend Line tool로 프로젝트 파일을 생성을 해야 합니다.
이제 readLine()함수를 사용할 수 있습니다.
임시로 이렇게 입력을 하고 cmd + r을 해서 프로젝트 실행을 한 후에
이 곳에서 입력을 합니다.
그리고 엔터를 입력하면
성공적으로 사용자의 키보드로 입력받은 문자열 readLine() execute!!를 print()함수로 출력합니다.
그럼 Optional 타입으로 wrapped된 문자열이 출력됩니다. 그럼 바인딩이나 강제 옵셔널 해제를 통해서 문자열을 얻을 수 있습니다.
참고로(스킵 가능한 구간입니다. 아래 회색 선이 보일 때까지 건너 뛰어주세요. 그냥 궁금해서..) readLine()에 대해 간단히 살펴보자면,
현재 line의 끝이나 EOF(텍스트 파일의 끝)에 도달할 때까지, 표준 입력(프로그램으로 들어가는 utf-8타입의 문자열)으로부터 읽어들인 String!! 문자열을 반환합니다.
문자열을 반환하는데 이때 그냥 문자열이 아니라 옵셔널 타입의 문자열을 반환합니다.
왜 반환 타입에 옵셔널을 붙였을까요? 키보드로 입력받는 모든건 문자열로 표현 가능할텐데 참 의문입니다,,
친절하게도 EOF에 도달 할 경우에 nil을 반환하기 때문이라고 설명되어 있습니다. 기본적으로 readLine()를 통해 사용자의 입력을 받는 것을 대기 중일 때 엔터를 입력하면, 문자를 입력하지 않았음에도 ""(빈 문자열)을 반환합니다. 하지만 언제 nil 을 반환하냐면 EOF가 readLine()에 입력받았을 경우입니다.
소켓 통신을 연습하기위해 Swift 언어로 제작된 서버용 프로젝트를 만들어서 터미널에서 실행시킨 적이 있습니다. 요럴 때,, EOF 입력합니다. (입력 끝냄을 나타내기 위해,,) commend + D키를(터미널은 ctrl+d) 눌르면 됩니다. 백준에서 어느 문제는 EOF를 요구하는 문제도 있습니다.
어느 문제의 입력은 이렇게 되어있습니다. readLine()으로 받는다면, 이 예제의 모든 입력을 받기 위해서는 5번 사용해야 합니다. 왜냐구요? readLine()은 한 줄을 읽어오기 때문에, 위에서 예제 입력은 다섯 줄로 되어 있습니다.
"""
6 4 \n // 첫 번째 줄
0 0 0 0 0 0 \n // 두 번째 줄
...
0 0 0 0 0 1 \n // 다섯 번 째 줄
"""
let line1 = readLine()
let line2 = readLine()
let line3 = readLine()
let line4 = readLine()
let line5 = readLine()
가장 쉬운 방법 은 readLine()을 다섯 번 사용하는 것입니다.
그리고 루프를 통해 각각의 변수가 갖고 있는 값을 출력을 한다면?
[line1,line2,line3,line4,line5].map{print($0)}
(초 : 내가 입력한 것, 빨: 내가 출력한 것)
아래와 같이 Optional 타입의 문자열이 출력됩니다!! 이 5개의 문자열들 각각의 문자열에 대해 숫자 타입의 배열로 변환시키고 싶을 때 사용하는 함수가 split(separator:) 함수와, map()함수입니다.
아!! 그전에 map() 함수에서 일어나는 변화를 자세히 보겠습니다. 동작 방식과 함수 설명은 다른 포스트의 2번 주제 "map(_:) func and ..."에 썼네요,, 여기에도 쓰면 더 길어질 것 같으니 생략하겠습니다.
[line1,line2,line3,line4,line5] //1. 각각 변수들을 포함하는 배열을 만든다.
.enumerated() //2. 변수들에 index를 부여한다.
.map{ (idx, line) in //3. 포문과 같이 배열의 첫 원소부터 마지막 원소까지 반복해서 idx+변수들 값을 받는다.
print("line\(idx+1) 값 출력:\(line)") //4. 특정 탐색중인 원소 출력!
}
enumerated() 함수도 알고 있으면 언젠간 쓰일 것입니다.
split(separator:)함수 입니다. 함수의 매개변수가 많지만 백준에서 사용될 매개변수는 separator 요게 핵심입니다. 이 기준에 초점을 맞춰 설명하자면, separator에 의해서 split(분할) 이후 Self.Subsequence타입의 배열로 반환합니다. 공백을 사용하던, 콤마를 사용하던 언더바를 사용하던 다양한 separator로 문자열을 부분 문자열들로 분리할 수 있습니다.
Self란?무엇일까요?
예를들어
let splited = "HI I'm String Type".split(separator: " ")
문자열에 split(separator: " ")함수를 사용할 경우 이 문자열은 " " 공백을 기준으로 분리되어서 배열로 반환됩니다. 이때 타입은 String.Subsequence 타입이 됩니다. 이 경우 Self == String이고, String의 subsequence 타입이 반환됩니다.
print()함수로 출력할 경우
배열로 반환됩니다. 하지만 String.SubSequence타입을 직접 사용하기엔 제약이 많습니다. 그래서 Character나 String타입으로 변환해 주어야 합니다. 근데 String타입에서 사용가능한 함수가 많음으로 String으로!!
splited 변수는 String.SubSequence타입의 배열입니다.( 4개의 값을 갖고 있는,,) 이를 String 타입으로 반환하기 위해서 생각해 볼것은 크게 for in구문, while문, forEach, map이 있습니다. 이들 중 map() 함수는 정말 대단합니다.
map()함수는 배열 객체에 사용할 수 있는데, 포문의 경우
var converted: [String] = []
for subsequence in splited {
converted.append(String(subsequence))
}
이렇게 converted 변수에 String.SubSequence 타입의 배열 값들을 String타입으로 변환할 수 있습니다.
반면, map()을 사용할 경우
var converted = splited.map{ subsequence -> String in
return (String(subsequence))
}
이렇게 표현할 수 있지만, Swift의 뛰어난 타입 추론 덕에 클로저에서 생략할 수 있는 것들이 있죠? 클로저 입력, 반환타입, in, return
이들을 다 생략할 경우,
var converted = splited.map{ String($0) }
이렇게 표현 가능합니다. 호호..
이제 converted 배열은 [String]타입이고, 4개의 변수를 갖게 될 것입니다. 이를 chains로 한번에 사용할 수도 있습니다.
let res = "HI I'm String Type".split(separator: " ").map{String($0)}
왜냐면 각각의 함수들은!! 저마다 값이나 배열을 반환하기 때문입니다.
readLine()!은 -> Stirng을 반환하고, (여기선 그냥 split(separator:)함수를 사용할 문자열"HI I'm ... Type"이 있네요!!)
반환된 한 개의 String을 split(separator: )함수를 사용해서 [Self.SubSequence]를(한개의 문자열을 여러 부분 문자열로 쪼갬, 차례대로 뭉치면 다시 한 개의 원본 문자열) 반환하고,
이렇게 반환된 배열은 map()을 사용할 수 있습니다.
그리고 map()을 사용한 결과로 또 다시 배열을 반환하기 때문입니다.
그 최종 결과를 res 변수에 전달합니다.
다시 위의 예제 입력에서,
line1,2,3,4,5 변수를 다섯개 사용했었습니다. 하지만 여기서는 반복되는 것들임으로 배열로 저장 받을 수도 있을 것입니다. 여기서는 첫번 째 입력인 6과 4를 받으면, 6*4 크기의 배열을 생성하고, 그 배열의 각 값을 두번째, 세번째, 네번째, 다섯번째 입력 값의 결과로 채우면 될 것입니다.
let widthAndHeight = readLine()! // "6 4"
.split(separator: " ") // ["6", "4"] 각각의 타입은 String.subsequence
.map{Int($0)!} // [6,4] 각각의 타입은 Int
let height = heightAndWidth[1]
let width = heightAndWidth.first!
여기서 받을 입력 값은 무조건 존재하기 때문에 강제 옵셔널 해제 연산자를 통해 옵셔널 타입의 문자열을 언래핑합니다.
그 결과는 오른쪽의 주석 처리로 표시했습니다. split을 통해 문자열을 스페이스 바를 통해 String.Subsequence타입의 배열로 반환합니다. 이 반환된 값을 map 함수를 통해 Int타입의 배열로 바꿉니다.
이제 2,3,4,5의 입력을 각각 readLine()으로 4개의 문자열로 받은 후에!!!! 모두 " " 스페이스바를 통해 4개의 배열로 나눈 후에 Int타입의 배열로 반환할 것입니다.
let board = (0..<height).map { _ in
readLine()!.split(separator: " ").map{Int(String($0))!}
}
값을 잘 입력 받았는지 출력을 해 보기 위해서는 print(board)를 하면
4*6 크기의 2차원 배열로 잘 받아진 모양입니다. 근데 확인하기 어렵네요... 다시 이쁘게 2차원 배열을 출력하는 함수를 만들면, 각각의 행을 출력하기 위한 함수도 만들어 봤습니다.
func printPretty(_ board: [[Int]]) {
print(board.map{$0.map{String($0)}.joined(separator: " ")}.joined(separator: "\n"))
}
printPretty(board)
확실하게 잘 입력 받은 것을 알 수 있었습니다.
아? 입력을 받아야 하는데, 스페이스바로 분리가 되어 있지 않은 문자열 각각의 문자들을 원소로 하는 배열을 입력 받고 싶은 경우도 있습니다.
이 경우는 어떻게 해야 할까요?
1. 일단 2차원 배열을 만들기 전에 배열의 크기에 사용될 4,6을 숫자로 입력받습니다.
2. let height = 4, width =6으로 배열에 값을 입력 받았다면
let board = (0..<height).map{ _ in readLine()!.map{Int(String($0))!} }
//또는
var board = Array(repeating: Array(repeating: 0, count: width), count: height)
for i in 0..<height {
board[i] = readLine()!.map{Int(String($0))!}
}
요렇게 입력을 받을 수 있습니다.
이때는 readLine()으로 입력받은 문자열은 스페이스 바를 통해 분리를 할 수 없으니, 분리할 것이 없으니 split(separator:) 사용을 할 수 없습니다. 그대신 문자열 에 대해 map 함수를 사용할 수 있습니다. 아래의 예를 보면 알 수 있지만 포인 구문은 "text"자리에 readLine()!이 오겠네요,,
for char in "text" {
...
}
or
"text".map { char in
...
}
or
"text".map{ $0으로 사용 }
문자열을 루프처리 할 때 문자열 각각의 문자에 대해 Character 타입으로 각각받을 수 있는 문자열과 루프의 특징을 이용해 포 인 구문을 사용하거나 이렇게 사용해서 문자열 각각의 원소를 Character 타입으로 얻어올 수 있습니다. 문자열을 탐색할 수 있는 특징이 있기 때문입니다.
이는 문자열 문법 공식문서의 for-in 루프를 보면 좋습니다. 또는 제 swift 문자열 관련 포스트를 ㅎㅎ.
이번엔 도데체 언제 끝내야 할 지 감이 안오는 입력이 있을 때가 있습니다. 분명히, 이럴땐 상당히 당황스러운데요.
입력 조건에서 몇 번의 입력이 들어올 지는 알려주지 않았습니다. 아마 채점하면서 위의 입력 4개 뿐 아니라 1000개든 11개든 진짜 여러개의 테스트 입력으로 들어올 수 있습니다.
이럴 때는
while let input = readLine() {
// 입력 받은 한 줄이 EOF가 아니라 문자열로 반환받을 수 있는 표준 입력이라면~
}
// EOF가 입력될 경우라면 while문 종료
readLine()에 바인딩을 활용하면 됩니다.
아! 그럼에도 readLine()함수로는 도저히 해결이 안되는 경우가 있을 수 있습니다..... 그럴 땐 라이노 님의 FileIO 클래스를 통해 해결 할 수 있습니다. ( 관련 소스 코드 링크 .. 전 언제 이런 로직을 구현할 생각을 할까요..)
Swift에서 제공되는 고차함수의 반환타입을 알게 됬을 때 한줄에 모든 걸 해결 할 수 있습니다.
그 예제로 제가 풀었던 나이순 정렬 문제를 예로 들 수 있습니다. 자세하게는 아니더라도, 코드 리뷰를 해봤습니다. ( 관련 포스트 링크 )
입력이 숫자 하나라면?
Int(readLine()!)!
입력받은 문자열은 정확히 한 개일 테니, 강제 옵셔널 해제 연산자를 사용하고 이는 String타입임으로 Int타입으로 변환할 수 있습니다.
그렇다면 이 문제는?
print(readLine()!.split{$0==" "}.map{Int(String($0))!}.reduce(0,+))
or
let inputList = readLine()!.split(separator: " ").map{Int(String($0))!}
let first = inputList[0]
let second = inputList[1]
printt(first+second)
이렇게 풀 수 있습니다.
그럼 이 경우는,,??
print((0..<2).map{_ in Int(readLine()!)! }.reduce(0,+))
or
var inputs: [Int] = []
for _ in 0..<2 {
inputs.append(Int(readLine()!)!)
}
let res = inputs.reduce(0,+)
print(res)
or
let first = Int(readLine()!)!
let second = Int(readLine()!)!
print(first+second)
다 같은 결과가 나옵니다.
모든 입력을 배열로 받는 경우도 있는 반면, 딕셔너리나 Set으로 받았을 때도 유리한 경우가 많습니다.
cf. 24.06.20
프로그래머스 Swift 5.2.5버전에서는
이렇게 (12345가 담긴 storey) 숫자 -> 문자열 -> 숫자배열로 만들 때 let floor = String(storey).map { Int($0)! }
이렇게 사용한다면 지원되지 않는 init이라는 경고가 발생됩니다. 그리서 위와 같이 String으로 변환 후에 Int로 변환해주어야 합니다.
혹시 궁금한 내용이 있다면 질문은 언제든 환영입니다. 제가 아는 선에서,,(알고리즘 질문은 해도 모를 가능성이 크지만, 백준 입력에 관해서는 기가맥힙니다.) 도움을 드릴 수 있다면 최대한 알려드릴게요.