기본 입출력 중 입력에 대해서 다룬다. 이번 글에서는 PS에서 입력으로 고생했던 것들을 위주로 적어볼 예정이다.
프로그래밍에서의 입력은 어떤 의미일까?
PS, 그리고 프로그래밍의 핵심은 입력과 출력이 있고, 그 사이의 함수를 알고리즘으로 채우는 것이다.
고등학생 때 $ f(x) = y $ 라는 수식을 본 적이 있을 것이다. $ x $는 입력(정의역), $ y $는 출력(치역), $ f $는 함수다.
이 관계를 알고 있다면 PS랑 프로그래밍에 대해서 조금 더 쉽게 접근이 가능하지 않을까 싶다.
감히 말하지만, 거의 모든 PS와 프로그래밍은 이 $ f(x) = y $의 수식에서 벗어날 일이 없다! 단지 함수를 만들어내는 게 어려울 뿐이다.
참고로 입력과 출력은 있을 수도 없을 수도 있지만, 이런 경우엔 "빈 입력과 빈 출력을 준다"라고 말하는 것이 더 옳은 대답이다.
파이썬에서의 다양한 파일 입력 방법
파이썬에서 불러올 수 있는 파일들은 매우 많다. csv, 사진, xml 등등 거진 앵간한 파일들은 쉽게 불러올 수 있다.
방법은 간단하다. 각 파일에 맞는 패키지를 import 한 후 그 패키지에서의 load 함수를 사용하면 파일을 불러온다.
다음은 실제 내 프로젝트에서의 numpy의 array 파일인 npy 파일을 불러오는 코드 예시다.
데이터를 불러올 때 주의해야 할 점이 있다면, 특히 csv 파일에서 내가 의도한 파일 크기인지를 확인해야 한다는 것이다.
간단히 말하자면, 예를 들어 내가 의도한 csv 파일은 80×4의 크기인데, 정작 불러와보니 81×4 같은 식으로 불러오는 경우다.
이런 경우는 초보자라면 매우 쉽게 당하는 문제다. 이 문제는 아주 간단하다. 마지막 줄에 입력이 존재하는 경우에 해당한다.
우리에게 안 보이는 개행 문자가 이런 오류를 일으켰을 가능성이 매우 높다! (실제로 이 문제로 저학년을 살려준 적이 있다)
그래서 csv의 마지막 줄을 드래그한 후 del키를 눌러줌으로써 살짝 수정을 가미하면 정상적으로 80×4를 불러온다.
파일을 잘 불러왔겠지 생각하고 바로 다음 작업으로 들어가지 말고, 반드시 입력 후의 데이터 크기를 꼭 확인바란다.
이 간과하기 쉬운 절차를 무시한 대가는 굉장히 무섭다. 코드 망침 나비 효과의 시발점이 될 수 있다!
PS에서의 입력 방법
그렇다면 PS에서는 입력을 어떻게 받는지를 보자.
먼저 알아야 할 것은, PS에서는 파이썬 기본 패키지만 사용이 가능하며, 설치해야 하는 패키지는 사용이 불가능하다.
그렇기에 기본 입력 방법인 input() 또는 sys.stdin.readline() 방식을 사용할 수밖에 없다.
입력만 따로 두고 가르치는 것은 좋은 방법이 아니다. 왜냐면 입력 후 바로 알고리즘 풀이로 넘어가기 때문이다. PS에서의 입력 테크닉이 출력만큼 많지도 않다.
그래서 예시 2개를 가지고 입력과 출력을 동시에 설명해보고자 한다.
예시 1 : [BOJ] 1004번 어린 왕자
- [BOJ] 1004번 어린 왕자 - [풀이]
문제의 그림과 입력의 줄 수만 보면 정신이 아찔해질 수밖에 없다! 그래도 정신을 차리고, 천천히 읽어보자.
먼저, 그림에서 곡선처럼 날아가니 바로 거부감이 들 수 있지만, 이 문제는 이 곡선을 구하는 문제가 아니다.
우리에게 주어진 입력은 테스트 케이스 개수, 출발점, 도착점, 행성계 개수, 그리고 각 행성계의 중점과 반지름이다.
또한 우리가 출력해야 하는 것은 최소 행성계 진입/이탈 횟수다. 절대로 문제 그림의 곡선에 관하여 물어본 것이 없다!
이 문제의 핵심은 출발점과 도착점, 그리고 행성계 좌표와 반지름만으로 최소 행성계 진입/이탈 횟수를 구해야 한다는 것이다.
그렇다면 진입과 이탈은 어떻게 구별해야 하는가 생각해보자.
예를 들어 하나의 행성계만 존재하고, 그것이 출발점 안에만 있다고 하자. 그렇다면 도착점으로 가려면 이탈을 한번 해야 한다. 반대로 하나의 행성계가 도착점에만 있다면, 행성계를 하나 진입해야 한다.
즉, 이탈 횟수는 출발점이 안에 존재하는 행성계 개수, 진입 횟수는 도착점이 안에 존재하는 행성계 개수다!
문제 중에 아래와 같은 말이 마지막에 있기에 더더욱 오해할 일 없이 코드를 짤 수 있다.
성계의 경계가 맞닿거나 서로 교차하는 경우는 없다. 또한, 출발점이나 도착점이 행성계 경계에 걸쳐진 경우 역시 입력으로 주어지지 않는다.
어떻게 푸는지를 알아봤으니 차례대로 풀어보자. 먼저 문제의 입력을 이 문제를 풀 수 있게끔 계속 만들어보자.
테스트 케이스의 수는 말 그대로 문제의 알고리즘을 몇 번 돌릴 것인지에 대한 수다. 아래 같은 방법으로 구성하자.
T = int(sys.stdin.readline().rstrip())
for _ in range(T):
# Algorithm part
다음 줄부터 출발점 (x1, y1)과 도착점 (x2, y2)를 받는다. 아래처럼 입력을 받아보자.
T = int(sys.stdin.readline().rstrip())
for _ in range(T):
x1, y1, x2, y2 = map(int, sys.stdin.readline().rstrip().split())
여기서 map 함수의 역할은 iterable한 객체에 대해 첫 번째 인자로 주어진 함수를 실행해주는 함수다.
이 입력에 예시를 들자면, 우리는 x1, y1, x2, y2를 구분해서 입력을 받아줘야 한다.
하지만 4개로 나눠진 것들은 전부 str 타입이고, 우리는 이걸 int로 바꿔주고 싶다.
split() 메서드를 사용하면 return 값으로 list를 주고, 이는 당연하지만 iterable 가능하다.
for-range문을 이용해서 순서대로 list에 접근할 수 있다는 것을 안다면 크게 어렵지 않은 설명이다.
그래서 map을 이용하면 각각의 list안의 str 요소들을 int 함수로 바꿔버릴 수 있다. (당연하지만 float 등으로도 바꿀 수 있다)
map 함수의 return 값은 map 객체라서 위에처럼 하나씩 받게 해 주던가, 아니면 list나 tuple 형식으로 묶어줘야 한다.
즉, 저 코드를 list로 되받고 싶다면 아래처럼 작성하면 된다.
T = int(sys.stdin.readline().rstrip())
for _ in range(T):
xy_list = list(map(int, sys.stdin.readline().rstrip().split()))
하지만 이 문제에서는 각각의 변수로 받는 것이 편하기에 변수 네 개를 썼다.
만약 map 함수를 쓰지 않는다면 for문을 써서 하나씩 int라고 지정을 해주는 식으로 가야한다. map 함수는 PS를 더 알차고 쉽게 만들어주는 고마운 함수이니 꼭 기억하자.
다음으로 행성계 개수인 n을 입력받는다. 이건 어렵지 않다. 여기에 추가로 행성계 진입/이탈을 세는 count 변수도 정의하자.
T = int(sys.stdin.readline().rstrip())
for _ in range(T):
x1, y1, x2, y2 = map(int, sys.stdin.readline().rstrip().split())
n = int(sys.stdin.readline().rstrip())
count = 0
행성계를 n번 입력받으면서 이 행성계가 출발점/도착점 안에 있는지를 확인하는 알고리즘을 짜 보자.
행성계 중점 (cx, cy)와 반지름 r을 입력받는 것은 위의 map 함수를 그대로 이용하자.
T = int(sys.stdin.readline().rstrip())
for _ in range(T):
x1, y1, x2, y2 = map(int, sys.stdin.readline().rstrip().split())
n = int(sys.stdin.readline().rstrip())
count = 0
for _ in range(n):
cx, cy, r = map(int, sys.stdin.readline().rstrip().split())
이제 진입/이탈 횟수를 세는 알고리즘으로 들어가자.
출발점/도착점과 그 행성계의 중심 간의 거리가 반지름보다 작으면 이탈/진입이다!
이걸 알고리즘대로 적어본다면 아래와 같이 될 것이다. 마지막에 count 개수도 출력하는 코드도 넣자.
T = int(sys.stdin.readline().rstrip())
for _ in range(T):
x1, y1, x2, y2 = map(int, sys.stdin.readline().rstrip().split())
n = int(sys.stdin.readline().rstrip())
count = 0
for _ in range(n):
cx, cy, r = map(int, sys.stdin.readline().rstrip().split())
if ((x1-cx)**2 + (y1-cy)**2)**0.5 < r:
count += 1
if ((x2-cx)**2 + (y2-cy)**2)**0.5 < r:
count += 1
print(count)
참고할만한 사항으로는, 우리가 루트를 취하기 위해 math 패키지를 불러올 필요가 없다는 것이다.
파이썬에서 **는 거듭제곱을 나타내는 기본 연산자인데, 정수뿐만이 아닌 실수도 사용할 수 있다.
거듭제곱으로 자주 쓰는 기호인 ^는 파이썬에서 XOR 연산자로 사용된다.
우리가 아는 루트는 ½승이므로 a ** 0.5라고 해도 루트를 취하는 것과 똑같은 결과를 얻는다.
자, 이렇게 해서 제출을 하면 틀린다. 우리가 예외 처리를 하나 못했기 때문이다.
그 예외는, "출발점과 도착점이 같은 행성계 안에 있다면?" 이다. 이 경우엔 당연히 진입/이탈을 할 필요가 없다!
하지만 저 알고리즘 대로라면 무조건 진입/이탈 횟수가 하나씩 늘어나게 된다. 우리는 이 예외를 반드시 처리해야 한다.
T = int(sys.stdin.readline().rstrip())
for _ in range(T):
x1, y1, x2, y2 = map(int, sys.stdin.readline().rstrip().split())
n = int(sys.stdin.readline().rstrip())
count = 0
for _ in range(n):
cx, cy, r = map(int, sys.stdin.readline().rstrip().split())
if ((x1-cx)**2 + (y1-cy)**2)**0.5 < r:
count += 1
if ((x2-cx)**2 + (y2-cy)**2)**0.5 < r:
count += 1
if ((x1-cx)**2 + (y1-cy)**2)**0.5 < r and ((x2-cx)**2 + (y2-cy)**2)**0.5 < r:
count -= 2
print(count)
간단하게 말하자면, 같은 행성계 안에 있다면 도로 그 횟수를 뱉어내게끔 코드를 두 줄 더 추가했다.
이렇게 처리하면 문제를 해결하게 된다. 전체 코드는 풀이 쪽에 올려두겠다.
예시 2 : [BOJ] 7785번 회사에 있는 사람
최종적으로 회사에 남아있는 사람들을 역순으로 출력하는 문제다.
입력은 이름 enter/leave 로 나타내어지며, 이걸 어떤 자료구조에 담을지를 생각하는 게 핵심이다.
일단 이 문제는 list를 쓰는 문제가 아니다. list를 써서 풀 수는 있지만, 이 문제에는 맞지 않는 자료구조다.
여기서는 set 이라는 자료구조를 사용한다. 간단하게 말해서, 해시(hash)를 사용할 것이다.
set은 말 그대로 집합을 의미하며, 집합의 의미대로 교집합/합집합/차집합 연산도 할 수 있다. 단독으로 쓰이면 해쉬의 기능을 그대로 쓸 수 있다.
또한 set 안의 메서드들 중 추가와 삭제는 둘 다 O(1)의 압도적인 성능을 자랑한다. list의 경우엔 각각 O(1)과 O(N)이다.
그렇다고 set이 절대적으로 좋은 것이 아니다! list와 구별되는 차이점이 있을 뿐이지 이 둘은 쓰이는 곳이 전혀 다르다.
주어진 입력은 간단하다. 출입 기록 횟수 n과 n번의 기록들이다. 기록은 이름과 출퇴근 기록이다.
즉, 출근을 하면 set 안에 이름을 집어넣고, 퇴근을 하면 set에서 이름을 지우면 된다.
n = int(sys.stdin.readline().rstrip())
now_comp = set()
for _ in range(n):
now_name, status = sys.stdin.readline().rstrip().split()
if status == 'enter':
now_comp.add(now_name)
else:
now_comp.remove(now_name)
이 부분까진 어려울 것은 없다.
이제 남아있는 사람들을 역순으로 정렬해서 출력해야 한다.
하지만 집합형 자료와 집합을 안다면 여기서 난감해지는데, 왜냐하면 집합은 순서가 없다!
# Main.py
print({1, 2, 3} == {2, 3, 1})
# Result
True
집합 관점에서는 A = {1, 2, 3} 이고 B = {2, 3, 1} 이어도 A == B다. 우리는 이 set 자료구조에서 벗어나야 한다.
다행히도, 파이썬은 자료구조 간의 변경이 상대적으로 쉽다. list(set 데이터)를 하면 단박에 list 자료구조로 넘어간다.
이걸 사용하면 역순으로 정렬하는 코드를 짜는 것은 금방 짤 수 있다.
n = int(sys.stdin.readline().rstrip())
now_comp = set()
for _ in range(n):
now_name, status = sys.stdin.readline().rstrip().split()
if status == 'enter':
now_comp.add(now_name)
else:
now_comp.remove(now_name)
now_comp = sorted(list(now_comp), reverse=True)
이제 출력하는 부분을 살펴보자. 개행 문자 '\n'으로 구분되어있다. print 스킬을 쓸 때가 왔다!
n = int(sys.stdin.readline().rstrip())
now_comp = set()
for _ in range(n):
now_name, status = sys.stdin.readline().rstrip().split()
if status == 'enter':
now_comp.add(now_name)
else:
now_comp.remove(now_name)
now_comp = sorted(list(now_comp), reverse=True)
print(*now_comp, sep='\n')
list 이므로 *와 sep 인자를 사용해서 답과 같은 출력을 해낼 수 있다. for문을 안 쓰고도 이렇게 쉽게 쓸 수 있다!
전체 코드는 마찬가지로 풀이 쪽을 봐주면 좋겠다.
입력만을 다루기엔 좀 그래서 출력과 입력을 복합적으로 다뤄보고자 했다.
'Python > 파이썬 알고리즘' 카테고리의 다른 글
[파이썬 알고리즘 #5] 반복문 (0) | 2022.08.10 |
---|---|
[파이썬 알고리즘 #4] 조건문 (0) | 2022.08.04 |
[파이썬 알고리즘 #2] 기본 입출력 - 출력 (0) | 2022.07.10 |
[파이썬 알고리즘 #1] 기본 입출력 - 들어가기 앞서 (0) | 2022.07.08 |
[파이썬 알고리즘 Prologue] 아무리 생각해도 나는 코딩 초보다 (0) | 2022.07.07 |
댓글