본문 바로가기
카테고리 없음

백준15719🐨중복된 숫자

by redcubes 2025. 1. 10.

인풋을 받았을 때 표준입력 버퍼의 상태 변화

기본적인 인풋 함수의 동작


open(0)의 경우


os.read()의 경우


sys.stdin의 경우


알 수 있는 점

  1. input()과 표준 입력 버퍼의 상관관계
    • input() 함수는 표준 입력 버퍼에서 데이터를 한 줄 단위로 읽어오며, 입력이 버퍼에서 처리된 이후에는 다음 읽기에서 이전 데이터를 사용할 수 없다.
    • 입력이 input()에 의해 처리된 이후 남아있는 입력이 없다면, 후속 표준 입력 함수는 빈 데이터를 반환하거나 에러를 발생시킬 수 있다.
  2. open(0)과 표준 입력 처리
    • open(0)을 통해 표준 입력을 파일 객체로 열면, 사용된 메소드(read()/readline())에 따라 데이터의 처리 범위와 버퍼 상태가 달라진다.
    • 여러 번 표준 입력을 읽으려고 하면 에러(예: Bad file descriptor)가 발생한다.
  3. os.read()와 바이트 데이터 처리
    • os.read(0, n)은 표준 입력을 바이트 단위로 읽는다.
    • 바이트 데이터를 처리하기 때문에, 문자열이 아닌 바이트로 결과가 출력된다.
  4. sys.stdin.read()와의 차이
    • sys.stdin.read(n)은 문자 데이터를 반환한다.
    • 표준 입력을 파일처럼 다룰 수 있는 유사성을 보여주지만, input() 함수와의 상호작용에 따라 읽히는 데이터가 달라진다.
  반복가능? input() 뒤 가능? input() 앞 가능? 임포트 필요 바이트/문자열 입력
open(0).read(n) ◯  둘 다 가능
sys.stdin.read(n) 문자열
os.read(0,n) 바이트


os.read()와 open(0)이 빈 값을 반환한 이유는 표준 입력 버퍼의 EOF(End of File) 상태가 발생했기 때문이다. 이를 좀 더 구체적으로 분석해보자.


 

1. 표준 입력과 같은 FD(파일 디스크립터) 0을 공유한다는 점

  • 파이썬에서 input()은 내부적으로 sys.stdin(FD 0)을 사용해 한 줄을 읽는다.
  • open(0) 역시 표준 입력과 같은 FD(파일 디스크립터) 0을 사용한다.

즉,

  • a = input() 호출 시: FD 0에서 한 줄을 읽고, 커서를 그 다음 위치로 옮긴다.
  • b = open(0).read() 호출 시: 이미 이동된 커서 위치부터 읽어야 한다.

만약 그 시점에 커서가 이미 EOF에 있으면 b는 ''(빈 문자열)이 된다.


2. 그런데 왜 EOF(End Of File)에 도달해 버리는가?

이 부분이 여러 모로 혼동을 준다. "분명 2줄을 준비해 두었는데, input()이 한 줄만 읽었을 텐데 나머지는 다 어디로 갔지?" 하는 의문이 생긴다. 이유는 크게 두 가지가 있다:

  1. 실행 환경(특히 온라인 컴파일러/실행기)에서 입력을 한 덩어리로만 주고 더 이상 데이터를 주지 않는 경우
    • 예: TIO, Repl.it, 백준 채점 서버 등등.
    • 사용자가 2~3줄을 입력해 놓았어도, 첫 줄을 input()이 읽으면서 내부 버퍼가 소진될 때, 해당 환경에서 스트림을 바로 닫아버리거나, 파이썬 쪽에서 “이만큼만 데이터가 왔다”며 EOF로 처리해 버릴 수 있다.
  2. input()이 실제로는 버퍼를 더 많이 가져와서(혹은 운영체제 수준에서) 이미 모든 입력을 소비했을 수도 있음
    • input()은 “한 줄씩 읽는다”라고 알려져 있지만, 내부적으로 운영체제나 파이썬이 **버퍼링(buffering)**을 할 수 있다.
    • 즉, OS 차원에서 “표준 입력 FD 0”으로 들어온 데이터를 한꺼번에 읽어 놓고, 파이썬 input() 레벨에서는 그중 필요한 한 줄만 반환하고 나머지를 내부 버퍼 어딘가에 쥐고 있다가, 이후 추가로 input()을 호출하면 그 내부 버퍼에서 꺼내 주는 식이다.
    • 그런데 open(0).read()를 호출하면, 이미 OS 레벨에서 “모든 데이터를 읽었지만, 파이썬의 내부 버퍼에 있는 부분”을 open(0)이 다시는 볼 수 없게 되는 상황이 생길 수 있다.
      • 왜냐하면 open(0)은 OS 수준 FD 0의 "남은" 데이터를 가져오는데, 파이썬 인터프리터(내부 버퍼) 쪽에서 이미 input()을 위해 가져가 버렸을 수 있기 때문이다.

정리하면, "input()이 단지 한 줄만 가져가고, 나머지는 놔두지 않느냐"라는 생각과는 달리,

  • (a) 실제로는 운영체제나 파이썬 내부의 버퍼링 메커니즘으로 인해 이미 전부 소비되었을 수 있고,
  • (b) 온라인 실행 환경 같은 곳에서는 첫 줄 읽은 뒤 바로 EOF가 되어버리는 상황이 종종 벌어진다.

이렇게 해서 os.read(0,n)이나 open(0).read(n) 입장에서는 “커서가 이미 끝까지 가 있네?”가 되어, 결과적으로 ''만 돌아오게 된다.
“커서가 둘째 줄 처음에 있을 것”이라고 순진하게 생각하지만, 이미 “파이썬 내부 버퍼”나 “환경”에서 그 뒤줄도 다 가져가 버린 상태가 되어 있을 수 있다는 것이다.


3. 같은 코드라도 환경마다 결과가 달라질 수 있음

위에서 언급한 대로,

  • 로컬 환경(직접 터미널에서 파이썬을 실행)에서 여러 줄을 차례대로 입력하면 input()이 차례대로 정상 동작하고, open(0).read()로도 남은 줄을 읽을 수 있는 경우도 있다.
  • 온라인 채점 환경이나 TIO 같은 샌드박스에서는 입력을 한 덩어리로 처리하거나, 첫 줄만 읽힌 뒤 EOF가 되어버리는 경우가 흔하다.

즉, "반드시 이론적으로는 3\n4\n5\n가 남았으니 open(0).read()가 4\n5\n를 읽을 것이다"라는 기대가 실행 환경에 따라 달라지는 것이다.

# test.py
import sys

print("=== First input() ===")
a = input()  # 첫 줄 읽기
print("=== open(0).read() ===")
b = open(0).read()
print("a =", a)
print("b =", b)
print("=== Done ===")

# test.py
import os
print("=== First input() ===")
a = input()  # 첫 줄 읽기
print("=== open(0).read() ===")
b = os.read(0,2024)
print("a =", a)
print("b =", b)
print("=== Done ===")

# test.py
print("=== First input() ===")
a = input()  # 첫 줄 읽기
print("=== open(0).read() ===")
b = open(0).read()
print("a =", a)
print("b =", b)
print("=== Done ===")

 


4. readline()을 여러 번 호출할 때도 비슷한 문제가 생길 수 있음

아래 코드처럼 readline()을 여러 번 호출하면 에러가 날 수 있다:

a = open(0).readline()
b = open(0).readline().split() print(a, b)
  • 어떤 환경에서는 open(0)을 여러 번 열었다 닫았다고 간주해 Bad file descriptor 오류가 날 수 있다.
  • 혹은 두 번째로 열었을 때는 이미 EOF 상태여서 빈 결과가 나올 수도 있다.

6. 어떻게 하면 제대로 여러 줄을 읽을 수 있나?

  1. input()만 써서 줄바꿈마다 읽기
    • 이렇게 하면 파이썬 내부 버퍼에서 그 줄들을 차례대로 잘 꺼내온다.
      a = input() # 첫 줄
      b = input() # 두 번째 줄
      c = input() # 세 번째 줄
  2. sys.stdin.read()나 open(0).read()를 맨 처음에 한 번만 써서 전체 입력을 받아 놓고 파싱
    import sys
    data = sys.stdin.read().strip().splitlines() # 이제 data 리스트에 모든 줄이 들어 있음
    print(data) # data[0] -> 첫 줄, data[1] -> 두 번째 줄, ...


  3. sys.stdin을 한 번만 사용
    • input()과 open(0).read() 같은 식으로 혼합하지 말고, 한 가지 방식을 일관되게 쓰면 이러한 문제를 피할 수 있다.

7. 최종 결론

  • 표준 입력 FD 0단 하나이며, 한 번 읽으면 해당 부분(혹은 그 이상)이 **소비(consume)**된다.
  • 파이썬의 input()과 open(0), sys.stdin은 모두 동일한 FD 0에서 데이터를 가져온다.
  • 이때 “한 줄만 읽었는데 나머지 줄은 남아 있지 않느냐?”라는 순진한 기대와 달리,
    • (a) 파이썬 내부 버퍼가 이미 모든 줄을 끌어와서 input()에 대비하고 있을 수 있고,
    • (b) 온라인 환경 같은 곳에서는 한 줄 읽은 뒤 바로 EOF가 되어 다음 데이터를 주지 않을 수 있다.
  • 따라서 input() 다음에 open(0).read()를 부르면 빈 문자열이 나오거나, 환경에 따라서는 잘 나오기도 하는 등, 일관성이 없어 보이나 사실 같은 원리에서 비롯된다.
  • 여러 줄 입력을 제대로 처리하고 싶다면, 처음부터 input()만 사용하거나, 처음에 sys.stdin.read()로 한 번에 몽땅 읽은 뒤 필요한 만큼 파싱하는 방식으로 코드를 짜는 것이 안전하다.

1. 스트림의 주요 상태

스트림의 상태는 주로 다음과 같은 요소들로 구성된다.

1.1 현재 위치 (Stream Position)

  • 스트림은 읽기/쓰기 포인터(cursor 또는 pointer)를 유지한다.
  • 예를 들어:
    • 파일을 읽을 때, 읽은 바이트나 줄 이후의 다음 읽기 위치를 포인터가 가리킨다.
    • input()을 호출하면 현재 줄을 읽고 포인터는 다음 줄의 시작으로 이동한다.
  • 표준 입력에서는 읽힌 데이터 이후의 위치를 유지하며, 모든 데이터가 소모되면 EOF(End of File) 상태가 된다.

1.2 상태 플래그 (Flags)

  • 스트림이 특정 상태인지 확인하거나 제어하기 위해 상태 플래그를 제공한다.
    • EOF (End of File):
      • 스트림의 데이터를 모두 읽었을 때 설정된다.
      • 이후 읽기 동작은 빈 값을 반환하거나 오류를 발생시킨다.
    • 에러 상태:
      • 읽기/쓰기 중 에러가 발생하면 스트림이 에러 상태로 전환된다.
      • 예: 네트워크 연결이 끊겼거나 파일 접근 권한이 없을 때.

1.3 버퍼 상태

  • 스트림은 데이터를 메모리 내의 버퍼에 임시 저장한 뒤 처리한다.
    • 표준 입력 스트림의 경우, 사용자가 입력한 데이터는 운영 체제의 버퍼에 저장된 후 프로그램에 제공된다.
    • 읽기 동작은 버퍼에서 데이터를 소모하며, 버퍼가 비면 EOF 상태로 전환된다.

2. 표준 입력 스트림의 상태

2.1 표준 입력 스트림의 동작

  • 입력 과정:
    1. 사용자가 데이터를 입력하면, 운영 체제는 이를 표준 입력 버퍼에 저장한다.
    2. 프로그램이 input(), sys.stdin.read(), open(0) 등을 호출하면, 데이터는 버퍼에서 소비된다.
    3. 버퍼에 데이터가 더 이상 없으면 EOF 상태가 된다.
  • 상태 변화 예시:
    • 초기 상태: 데이터가 입력되면 버퍼에 저장되고, 읽기 포인터는 버퍼의 시작을 가리킨다.
    • 읽기 후: 읽힌 데이터는 버퍼에서 제거되거나 포인터가 이동하며, 남은 데이터만 읽을 수 있다.
    • 모든 데이터 소모 후: 읽기 포인터는 버퍼의 끝에 도달하며, EOF 상태가 된다.

3. 상태 확인과 관련 메서드

스트림의 상태를 확인하거나 조작하는 주요 메서드는 다음과 같다.

3.1 EOF 확인

  • EOF 상태를 확인하는 방법은 스트림 구현에 따라 다르다.
    • 파일 스트림:
      with open('example.txt', 'r') as f:
          while True:
              line = f.readline()
              if not line:  # EOF에 도달
                  break
              print(line)
      
    • 표준 입력: 표준 입력에서는 데이터가 남아 있지 않을 경우 EOF로 간주된다.
      import sys
      data = sys.stdin.read()
      print("EOF 상태:", data == "")
      

3.2 스트림 포인터 이동

  • 파일 스트림에서는 읽기/쓰기 포인터를 수동으로 조정할 수 있다.
    with open('example.txt', 'r') as f:
        f.seek(0)  # 스트림의 시작으로 이동
        print(f.read(5))  # 처음 5바이트 읽기
    

 

 

next를 쓰지 않고 이터레이터 쓰기

1바이트씩 읽어오기

바이트로 디짓 처리하기

중복된 숫자 찾기

1부터 N - 1까지의 정수가 하나씩 정렬되지 않은 채로 저장되어 있는 어떤 수열 A가 있다. 수열 A에 임의의 정수 M(1 ≤ M ≤ N – 1)을 넣어 크기가 N인 수열로 만들었을 때, 임의의 정수 M을 찾는 프로그램을 작성하라.

입력

  • 첫째 줄에 수열의 크기 N(2 ≤ N ≤ 10,000,000)이 주어진다.
  • 둘째 줄에 수열 A의 원소인 N개의 정수가 주어진다. 입력으로 주어지는 정수는 모두 1보다 크거나 같고, N-1보다 작거나 같은 정수이며 문제의 답인 M을 제외하고는 모두 서로 다른 정수이다.

출력

M을 출력하라.

제약 조건

시간 제한 메모리 제한
2 초 256 MB

예제

입력

10
1 2 2 5 6 4 3 7 8 9

출력

2

다음 코드는 os.read 함수를 이용해 표준 입력(파일 디스크립터 0) 으로부터 모든 바이트를 읽어 들인 뒤, 이를 직접 순회(iterate)하면서 필요한 정보를 추출한 뒤 결과를 출력한다.

import os
r = iter(os.read(0, os.fstat(0).st_size))
N = 0
for c in r:
    if c == 10:
        break
    N = N * 10 + (c - 48)
ref = (N * (N - 1)) >> 1
while True:
    num = 0
    for c in r:
        if c <= 47:
            break
        num = num * 10 + (c - 48)
    ref -= num
    if c == 10:
        break
print(-ref)

1. 표준 입력을 한 번에 읽고, 이터레이터로 변환하기

import os
r = iter(os.read(0, os.fstat(0).st_size))
  • os.read(0, os.fstat(0).st_size):
    • os.read를 사용하여 **파일 디스크립터 0(표준 입력)**에서,
    • os.fstat(0).st_size(표준 입력에 들어 있는 전체 바이트 수)만큼 데이터를 한 번에 읽는다.
  • 그 결과로 얻은 바이트열(byte string)을 iter(...)로 감싸 이터레이터로 만든다.
    • 이터레이터를 사용하면, 한 바이트(문자)씩 순회할 수 있다.

2. 첫 번째 줄에서 N 읽어들이기

N = 0
for c in r:
    if c == 10:
        break
    N = N * 10 + (c - 48)
  • N = 0으로 초기화하고, for c in r:를 통해 이터레이터 r에서 바이트를 하나씩 꺼낸다.
  • if c == 10: 은 ASCII 코드 10(개행 문자)인지 확인한다. 만약 개행이라면 break로 반복을 종료한다.
    • 즉, 첫 번째 줄이 끝날 때까지 숫자를 읽는다.
  • N = N * 10 + (c - 48):
    • c는 읽은 문자의 바이트값이다.
    • '0' 문자의 ASCII 코드가 48이므로, (c - 48)를 통해 실제 숫자로 변환한다.
    • N = N * 10 + ... 누적하면, 문자열 형태의 숫자를 정수로 계산할 수 있다.
    • 예: 만약 입력 첫 줄이 "123"라면, c는 [49, 50, 51](ASCII)이고, 순서대로 (49 - 48), (50 - 48), (51 - 48)이 되어 1, 2, 3이 된다.

3. ref 계산

ref = (N * (N - 1)) >> 1
  • (N * (N - 1)) >> 1는 오른쪽 비트 시프트 연산으로, 사실상 $\frac{N(N - 1)}{2}$ 와 같은 값을 얻는다.
    • 예: N * (N-1)을 2로 나눈 것과 동일 (정수 나눗셈).
  • 이는 흔히 1부터 N-1까지의 합(또는 조합 계산) 등에 자주 사용되는 공식이다.
    • 구체적으로 $$ 
      \binom{N}{2} = \frac{N(N - 1)}{2} 
      $$
      값이 된다.

4. 다음 줄부터 숫자를 계속 읽으며 ref 감소시키기

while True:
    num = 0
    for c in r:
        if c <= 47:
            break
        num = num * 10 + (c - 48)
    ref -= num
    if c == 10:
        break
  • while True:로 무한 루프를 돌며 다음과 같은 과정을 반복한다.
  • num = 0으로 초기화하고, for c in r:에서 바이트를 하나씩 꺼내 숫자로 변환한다.
    • if c <= 47: break
      • 공백(32), 개행(10), 구분자 등 '0'(48)보다 작은 ASCII 코드가 나오면 숫자 읽기를 멈춘다.
      • 여기서는 대부분 개행(10) 또는 공백(32) 등이 구분자로 쓰일 것이므로, 이를 기준으로 하나의 숫자 읽기를 종료하는 로직이다.
    • num = num * 10 + (c - 48)을 통해 숫자를 누적하여 정수로 만든다.
  • ref -= num
    • 읽어들인 숫자를 ref에서 뺀다.
  • if c == 10: break
    • 만약 현재 바이트가 개행(10)이라면 while 루프도 빠져나오도록 설정되어 있다.
    • 즉, 한 줄 읽기를 종료하면 반복을 끝낸다.

정리하면, N 이후의 두 번째 줄부터는(혹은 뒤이어 오는 숫자 리스트) 각 숫자를 읽어 ref에서 감소시킨 뒤, 개행을 만나면 종료한다.


5. 최종 결과 출력

print(-ref)
  • 마지막으로 ref에 대해 음수 부호를 붙인 -ref를 출력한다.
  • 위에서 ref는 $\frac{N(N - 1)}{2}$ 에서 시작해 입력받은 숫자들을 모두 뺀 값이므로, 그 결과에 -1을 곱한 값을 출력한다.

요약

  1. 표준 입력으로부터 모든 바이트를 읽어 이터레이터로 만든다.
  2. 첫 번째 줄에서 숫자 N을 읽어 정수로 변환한다.
  3. ref = (N * (N - 1)) >> 1을 구한다 $\frac{N(N - 1)}{2}$.
  4. 이어서 읽은 숫자들을 모두 ref에서 빼준다.
  5. 마지막으로 -ref를 출력한다.

*ARRAY활용해서 조금 더 빠르게

import os
import array
r = array.array('B', os.read(0, os.fstat(0).st_size))  # Byte
idx = 0
N = 0
while idx < len(r):
    c = r[idx]
    idx += 1
    if c == 10:
        break
    N = N * 10 + (c - 48)

ref = (N * (N - 1)) >> 1

while idx < len(r):
    num = 0
    while idx < len(r):
        c = r[idx]
        idx += 1
        if c <= 47:
            break
        num = num * 10 + (c - 48)
    ref -= num
    if c == 10:
        break

print(-ref)