알고리즘 · 기본
논리로 푸는 알고리즘 - 코딩 테스트까지
문제를 논리로 읽기 - 입력·출력·조건·반례
문제 문장을 입력·출력·제약으로 바꾸기, 조건문을 명제로 쓰기(그리고·또는·아니면), 반례 먼저 찾기, 완전 탐색으로 정답 기준 만들기, 완성 코드는 조건 판정 문제를 명제식으로 풀고 모든 입력 조합을 표로 확인한다
개발자KR · 원고 갱신
이 장에서 배우는 것
코드를 쓰기 전에 결정해야 할 것은 문법이 아니라 판단 기준이다. “예약했거나 현장 접수를 마친 사람은 들어갈 수 있다”라는 문장은 짧지만, 운영 중단 여부가 덧붙으면 조건을 묶는 방법에 따라 결과가 달라진다. 코드는 작성한 식을 그대로 실행한다. 문장의 뜻을 다르게 옮겼다면 문법 오류 없이도 잘못된 답을 낸다.
이 장에서는 작은 출입 판정 문제와 재고 요청 문제를 새로 만들어 푼다. 먼저 문장을 입력·출력·제약으로 나누고, 손으로 몇 가지 경우를 판단한다. 그다음 조건을 식으로 옮기고, 식과 다른 방식으로 만든 정답 기준을 이용해 작은 입력을 모두 대조한다. 빠른 코드를 떠올리는 것보다 무엇을 정답으로 삼을지 정하는 일이 먼저다.
- 문제 문장에서 주어지는 값과 구해야 하는 값을 구분한다.
- 그리고·또는·아니면을 명제로 옮기고 괄호로 적용 범위를 드러낸다.
- 잘못된 풀이를 구별하는 반례와 경계 입력을 찾는다.
- 작은 입력의 모든 조합을 확인하는 정답 기준을 만든다.
- 조건식과 독립적인 판정 절차를 비교해 구현을 검증한다.
문제 상황
작은 공방에서 체험실 출입 화면을 만든다고 하자. 안내문에는 다음 규칙이 적혀 있다.
예약을 마쳤거나 현장 접수를 마친 사람은 입장할 수 있다. 다만 체험실 운영이 중단된 동안에는 누구도 입장할 수 없다.
화면에는 예약 여부, 현장 접수 여부, 운영 중단 여부가 각각 참 또는 거짓으로 전달된다. 이름이나 나이는 판정에 쓰지 않는다. 예약과 현장 접수를 모두 마친 경우도 입력으로 들어올 수 있다. 규칙에 두 경우가 함께 발생할 수 없다는 설명이 없으므로 임의로 제외하지 않는다.
첫 번째 문제는 이 세 값을 받아 입장 가능 여부를 구하는 것이다. 출력이 입장 가능한 사람의 수인지, 한 사람의 입장 가능 여부인지도 구분해야 한다. 여기서는 한 사람에 대한 참 또는 거짓을 반환한다. 실행 예시에서는 사람이 읽기 쉽도록 참을 “허용”, 거짓을 “거절”로 표시한다.
같은 공방에서는 재료 꾸러미 요청도 처리한다. 두 번째 문제의 규칙은 다음과 같다.
요청 수량이 1개 이상이고 현재 재고 이하이면 요청 전체를 처리한다. 수량이 0이거나 재고보다 많으면 처리하지 않는다. 일부 수량만 처리하는 경우는 없다.
재고와 요청 수량은 0 이상의 정수다. 실제 요청에 큰 값이 들어오더라도 판정 방법은 같다. 다만 검증에서는 두 값을 각각 0부터 4까지로 제한해 모든 조합을 확인한다. 문제에서 허용한 입력 범위와 우리가 시험하는 입력 범위는 서로 다른 개념이다.
| 문제 | 입력 | 출력 | 제약과 해석 |
|---|---|---|---|
| 체험실 출입 | 예약, 현장 접수, 중단 여부 | 입장 가능 여부 | 각 값은 참 또는 거짓이며 중복 접수도 허용한다. |
| 재료 요청 | 재고, 요청 수량 | 전체 처리 가능 여부 | 두 값은 0 이상의 정수이며 부분 처리는 없다. |
이렇게 나누면 구현에 필요하지 않은 배경과 판정에 필요한 조건이 구별된다. 공방이라는 장소는 문제를 이해하는 데 도움을 주지만, 함수가 받아야 할 입력은 아니다. 반대로 “일부만 처리하지 않는다”는 문장은 출력의 뜻을 바꾸므로 생략하면 안 된다.
문장을 입력·출력·제약으로 바꾸기
입력은 함수가 판단하기 전에 주어지는 값이다. 출력은 입력으로부터 계산해야 하는 값이다. 제약은 어떤 입력이 들어올 수 있는지, 어떤 결과를 정답으로 인정하는지를 정한다. 이 세 가지를 적으면 풀이가 문제 밖의 가정에 기대고 있는지 확인하기 쉽다.
출입 문제에서 “예약자는 보통 운영 시간에 온다”라는 추측은 사용할 수 없다. 입력에는 예약 여부와 운영 중단 여부가 동시에 참인 경우도 들어올 수 있다. 문제에 없는 현실의 관행을 넣으면 특정 조합을 빠뜨리게 된다. 먼저 문장에 명시된 규칙만으로 답이 정해지는지 살펴야 한다.
재고 문제에서도 “요청은 당연히 1개 이상일 것”이라고 넘기면 안 된다. 이번 문제에서는 0도 입력으로 허용한다. 따라서 0을 거절하는 조건이 필요하다. 입력으로 들어올 수 없는 값과 입력으로 들어오지만 거절해야 하는 값은 다르다. 전자는 입력 제약의 문제이고, 후자는 판정 규칙의 문제다.
숫자의 의미와 단위를 먼저 적는다
재고가 4이고 요청 수량이 4라면 요청을 처리한다. “재고 이하”에는 같은 값이 포함되기 때문이다. 재고가 4이고 요청 수량이 5라면 거절한다. 이 두 경우를 나란히 적으면 비교 기호가 <인지 <=인지 결정하기 쉽다.
수량은 꾸러미 개수이며 정수다. 무게나 금액으로 바꾸어 해석하지 않는다. 같은 숫자라도 의미가 달라지면 허용되는 연산과 경계가 달라진다. 문제를 읽을 때 변수 이름 옆에 “현재 남은 개수”, “이번에 요청한 개수”처럼 뜻을 짧게 붙여 두면 인자를 거꾸로 전달하는 실수도 줄어든다.
입력 형식은 규칙과 따로 확인한다
판정 규칙을 이해했다고 해서 입력 형식까지 정해지는 것은 아니다. 출입 문제에서는 참과 거짓을 각각 1과 0으로 나타내고, 한 줄에 예약·현장 접수·중단 순서로 제공한다고 정한다. 값의 순서가 바뀌면 같은 세 숫자도 다른 상황을 뜻한다.
완성 코드에서는 첫 줄에 사례 수를 적고, 이어지는 줄에 세 값을 적는다. 실제 표준 입력을 기다리는 대신 코드 안의 문자열을 io.StringIO로 감싼다. 이렇게 하면 코딩 테스트에서 쓰는 줄 단위 읽기를 연습하면서도 프로그램을 실행한 직후 결과를 확인할 수 있다.
입력이 정해진 형식을 지킨다는 가정과 입력이 규칙에 따라 허용된다는 판단도 구별한다. 예약 값에 7이 들어오는 경우는 이번 입력 형식 밖이다. 반면 예약이 1이고 중단이 1인 경우는 정상적인 입력이며, 결과가 거절일 뿐이다.
조건을 명제로 쓰고 반례로 읽기
참과 거짓 중 하나로 판단할 수 있는 문장을 명제(proposition)라고 한다. 출입 문제에서는 “예약을 마쳤다”, “현장 접수를 마쳤다”, “운영이 중단되었다”를 각각 하나의 명제로 둔다. 코드의 변수 이름은 reserved, onsite, closed로 정한다.
“그리고”는 두 조건이 모두 참이어야 한다는 뜻이며 Python에서는 and로 쓴다. “또는”은 하나 이상 참이면 된다는 뜻이며 or로 쓴다. “아니다”는 참과 거짓을 뒤집으며 not으로 쓴다. 여기서 “예약했거나 현장 접수를 마쳤다”는 두 조건이 함께 참인 경우도 포함한다.
일상어의 “아니면”은 문맥을 확인해야 한다. “예약하거나 아니면 현장에서 접수한다”는 안내만으로 두 접수가 동시에 성립하면 거절한다는 규칙을 만들 수는 없다. “둘 중 정확히 하나만 참이어야 한다”는 조건이 따로 있어야 그런 해석이 가능하다. 또한 “조건이 아니면”처럼 부정을 뜻하는 표현과도 구별해야 한다.
| A, B | A and B | A or B | 정확히 하나만 참 |
|---|---|---|---|
| 거짓, 거짓 | 거짓 | 거짓 | 거짓 |
| 거짓, 참 | 거짓 | 참 | 참 |
| 참, 거짓 | 거짓 | 참 | 참 |
| 참, 참 | 참 | 참 | 거짓 |
출입 규칙은 다음 식으로 옮긴다. 먼저 접수 조건을 괄호로 묶고, 그 전체에 운영 중단이 아니라는 조건을 붙인다.
(reserved or onsite) and not closed
괄호를 지운 reserved or onsite and not closed는 같은 뜻이 아니다. Python은 and를 or보다 먼저 묶으므로 reserved or (onsite and not closed)로 해석한다. 예약이 참이면 운영 중단 여부와 관계없이 전체 식이 참이 될 수 있다.
반례는 두 해석이 갈라지는 입력이다
반례(counterexample)는 주장이나 풀이가 틀렸음을 보여 주는 구체적인 입력이다. 괄호를 빠뜨린 식을 검사하려면 아무 입력이나 많이 고르는 것보다 예약이 참이고 운영 중단도 참인 입력 하나가 유용하다. 규칙은 거절을 요구하지만 잘못된 식은 허용을 반환한다.
반례를 찾을 때는 “어떤 조건이 다른 조건을 가려 버리는가”를 묻는다. 예약이 참이면 앞부분만으로 or가 참이 되는 식에서는 중단 조건이 가려진다. 반대로 or를 and로 잘못 옮긴 식에서는 예약만 있는 사람이 거절된다. 오류의 종류마다 결과가 갈라지는 입력을 찾을 수 있다.
반례 하나로 잘못된 풀이를 버릴 수는 있지만, 반례를 찾지 못했다는 이유만으로 풀이가 맞는 것은 아니다. 출입 문제는 입력 조합이 작으므로 전부 확인한다. 수량 문제처럼 입력이 넓게 열려 있다면 작은 범위의 전수 확인에 더해 조건 자체의 정확성 근거가 필요하다.
| 입력 | 먼저 확인할 사실 | 결과 | 확인하는 오류 |
|---|---|---|---|
| 예약 1, 현장 0, 중단 1 | 중단 중에는 입장할 수 없다. | 거절 | 중단 조건의 적용 범위 |
| 예약 1, 현장 0, 중단 0 | 예약만으로 접수 조건을 만족한다. | 허용 | 또는을 그리고로 바꾸는 오류 |
| 재고 4, 요청 4 | 요청이 재고와 같아도 된다. | 허용 | 이하에서 등호 누락 |
| 재고 4, 요청 0 | 요청은 1개 이상이어야 한다. | 거절 | 수량의 하한 누락 |
재고 요청은 두 비교를 그리고로 연결한다. 요청이 1개 이상이라는 조건과 재고 이하라는 조건을 모두 만족해야 한다. Python에서는 이어진 비교 1 <= quantity <= stock으로 쓸 수 있다. 이 식은 1 <= quantity and quantity <= stock과 같은 판정이다.
모든 조합으로 정답 기준 만들기
가능한 입력을 빠짐없이 확인하는 방법을 완전 탐색(exhaustive search)이라고 한다. 여기서는 탐색 기법을 넓게 다루지 않고, 작은 입력에서 풀이를 검증하는 용도로만 사용한다. 출입 문제는 참과 거짓 두 값이 세 자리에 들어가므로 2 × 2 × 2 = 8개 조합뿐이다.
세 반복문에서 예약, 현장 접수, 운영 중단 값을 각각 False와 True로 바꾸면 모든 조합이 한 번씩 나온다. 안쪽 반복문이 끝나면 바깥쪽의 현재 두 값에 대한 중단 여부 두 가지를 모두 확인한 상태다. 이 과정을 바깥쪽 값마다 반복하므로 빠지는 조합이 없다.
정답 기준은 최종 조건식을 복사해서 만들지 않는다. 똑같은 식을 두 함수에 넣으면 같은 실수도 함께 복제된다. 출입 문제의 기준 함수는 중단 상태를 먼저 제외하고, 예약과 현장 접수를 차례로 확인한다. 그 절차로 허용 조합 목록을 만든 다음, 빠른 풀이의 결과와 목록 포함 여부를 비교한다.
왜 이 판정이 규칙과 일치하는가
출입 문제에서 중단이 참이면 not closed가 거짓이므로 전체 식도 거짓이다. 중단이 거짓이면 not closed가 참이므로 전체 결과는 reserved or onsite에 달려 있다. 이때 접수 둘 중 하나 이상이 참일 때만 허용된다. 중단 여부 두 경우가 모든 상황을 나누며, 각 경우의 결과가 안내문과 일치한다.
재고 문제의 느린 기준은 처리할 수 있는 수량을 1부터 재고까지 하나씩 나열한다. 요청 수량이 그중 하나와 같으면 허용한다. 재고가 0이면 나열할 수량이 없으므로 어떤 요청도 허용하지 않는다. 이 목록에 들어가는 정수는 정확히 1 이상 재고 이하이므로 이어진 비교식과 같은 결과를 낸다.
검증 반복문에서는 지금까지 검사한 모든 입력에서 두 방법의 결과가 같다는 성질을 유지한다. 다음 입력도 같음을 확인하면 이 성질이 한 입력만큼 확장된다. 반복이 끝나면 검사 범위 전체에서 결과가 같다는 결론을 얻는다. 다만 두 방법이 같은 잘못된 해석에 바탕을 두었다면 대조만으로 찾지 못할 수 있으므로 문장에 대한 근거도 함께 살핀다.
시간과 공간을 미리 말로 세어 본다
시간 복잡도(time complexity)는 입력 크기에 따라 수행량이 어떻게 달라지는지 나타낸다. 공간 복잡도(space complexity)는 필요한 저장 공간의 증가를 나타낸다. 여기서는 정수 한 번의 비교를 일정한 비용으로 보는 범위에서 계산한다. 표기 자체보다 무엇을 반복하고 무엇을 저장하는지에 주목한다.
출입 조건식은 정해진 수의 논리 연산만 하므로 시간 O(1), 추가 공간 O(1)이다. 허용 목록을 차례로 찾는 기준은 목록 길이를 K라 할 때 시간 O(K)다. 이번 문제에서는 K가 3으로 고정되어 있어 이 방법도 전체적으로는 O(1)이다. 따라서 출입 문제의 “빠른 풀이”라는 이름은 실제 속도 차이를 강조하기보다 직접 판정하는 쪽을 구별한다.
재고를 S라고 하면 느린 기준은 최대 S번 수량을 확인하므로 시간 O(S), 추가 공간 O(1)이다. 비교식은 시간과 추가 공간이 모두 O(1)이다. 검증 상한을 M으로 늘려 재고와 요청을 각각 0부터 M까지 검사하면 조합은 (M + 1)²개다. 느린 기준까지 실행하는 전체 검증 시간은 O((M + 1)³)이며, 사례를 쌓아 두지 않으므로 추가 공간은 O(1)이다.
전수 대조의 목적은 작은 범위에서 오류를 드러내는 것이다. 모든 재고 값을 끝없이 검사하는 일은 할 수 없다. 그래서 작은 범위에서는 직접 비교하고, 범위 밖에 대해서는 앞서 설명한 “1부터 재고까지의 정수와 비교식이 같은 집합을 가리킨다”는 근거를 사용한다.
완성 코드
다음 내용을 main.py로 저장한다. 외부 패키지와 입력 대기를 사용하지 않는다. 출입 사례의 문자열에는 가능한 여덟 조합을 모두 넣었다. 프로그램은 표를 출력한 다음 출입과 재고 문제의 대조 결과를 출력하고 끝난다.
import io
EXAMPLE_INPUT = """8
0 0 0
0 0 1
0 1 0
0 1 1
1 0 0
1 0 1
1 1 0
1 1 1
"""
def can_enter(reserved, onsite, closed):
return (reserved or onsite) and not closed
def build_allowed_cases():
allowed = []
for reserved in (False, True):
for onsite in (False, True):
for closed in (False, True):
if closed:
continue
if reserved:
allowed.append((reserved, onsite, closed))
continue
if onsite:
allowed.append((reserved, onsite, closed))
return allowed
def can_enter_slow(case, allowed):
for candidate in allowed:
if case == candidate:
return True
return False
def can_supply(stock, quantity):
return 1 <= quantity <= stock
def can_supply_slow(stock, quantity):
for possible in range(1, stock + 1):
if quantity == possible:
return True
return False
def main():
source = io.StringIO(EXAMPLE_INPUT)
case_count = int(source.readline())
allowed = build_allowed_cases()
seen = set()
print("예약 현장 중단 | 판정")
for _ in range(case_count):
values = tuple(map(int, source.readline().split()))
reserved, onsite, closed = tuple(value == 1 for value in values)
case = (reserved, onsite, closed)
fast = can_enter(*case)
slow = can_enter_slow(case, allowed)
if fast != slow:
raise AssertionError(f"출입 불일치: {values}")
seen.add(case)
label = "허용" if fast else "거절"
print(f"{values[0]} {values[1]} {values[2]} | {label}")
if case_count != 8 or len(seen) != 8:
raise AssertionError("출입 입력 조합이 빠졌거나 중복되었다.")
print(f"출입 전수 대조: {len(seen)}개 일치")
checked = 0
for stock in range(5):
for quantity in range(5):
fast = can_supply(stock, quantity)
slow = can_supply_slow(stock, quantity)
if fast != slow:
raise AssertionError(
f"재고 불일치: stock={stock}, quantity={quantity}"
)
checked += 1
print(f"재고 전수 대조: {checked}개 일치")
if __name__ == "__main__":
main()
줄별 해설
import io는 문자열을 입력 스트림처럼 읽는 데 필요한 표준 라이브러리를 가져온다. EXAMPLE_INPUT의 첫 줄은 사례 수 8이며, 이후 각 줄의 숫자는 예약·현장 접수·중단 순서다. 세 자리의 0과 1을 사전식 순서로 나열해 출력 순서를 고정했다.
can_enter의 return은 안내문을 옮긴 명제식이다. 괄호 안에서 접수 조건을 먼저 묶고, 그 결과와 운영 중단의 부정을 and로 연결한다. 이 함수는 파일이나 화면에 접근하지 않고 전달된 세 값만 사용하므로 개별 사례를 확인하기 쉽다.
build_allowed_cases의 allowed는 허용할 입력 조합을 담는다. 세 반복문은 여덟 조합을 만든다. if closed는 운영 중단 중인 조합을 먼저 제외한다. continue를 만나면 현재 조합의 남은 판정을 건너뛰고 다음 조합으로 넘어간다.
그다음 if reserved에서 예약자를 목록에 넣는다. 바로 뒤의 continue는 예약과 현장 접수가 모두 참인 조합을 두 번 추가하지 않게 한다. 예약이 거짓인 경우에만 마지막 if onsite까지 내려오며, 현장 접수가 참이면 목록에 넣는다. 결과적으로 허용 조합은 세 개다.
can_enter_slow는 만들어 둔 목록을 앞에서부터 확인한다. 같은 튜플을 찾으면 참을 반환하고, 끝까지 없으면 거짓을 반환한다. 기준 목록을 만드는 절차와 목록을 조회하는 절차를 나누었으므로 어떤 조합이 허용되었는지도 직접 살펴볼 수 있다.
can_supply는 요청의 하한과 상한을 한 식에 담는다. can_supply_slow의 range(1, stock + 1)은 1부터 stock까지를 만든다. range의 끝값은 포함되지 않으므로 1을 더한다. 반복 도중 요청과 같은 수를 찾으면 참이며, 찾지 못하면 거짓이다.
main에서 source.readline()은 문자열의 다음 한 줄을 읽는다. split은 공백으로 값을 나누고, map(int, ...)는 각 조각을 정수로 바꾼다. tuple은 그 결과를 순서 있는 묶음으로 만든다. 입력이 0과 1이라는 약속 아래 value == 1로 각 값을 논리값으로 바꾼다.
can_enter(*case)의 별표는 튜플의 세 값을 함수의 세 인자로 펼친다. fast와 slow가 다르면 AssertionError를 직접 발생시킨다. 불일치 입력도 오류 메시지에 담으므로 어느 조합을 다시 읽어야 하는지 알 수 있다. 검증 결과를 보장하는 데 assert 문 실행 여부에 의존하지 않도록 명시적인 조건 검사로 작성했다.
seen은 이미 확인한 조합을 저장하는 집합이다. 같은 조합을 다시 넣어도 원소 수가 늘지 않는다. 각 자리가 두 값뿐이므로 가능한 조합은 여덟 개다. 읽은 사례가 여덟 개이고 서로 다른 조합도 여덟 개라면 모든 조합을 한 번씩 확인한 것이다.
재고 검증의 두 반복문은 각각 0, 1, 2, 3, 4를 고른다. 따라서 5 × 5 = 25개 입력을 확인한다. checked는 비교가 일치한 뒤에만 증가한다. 마지막 출력에 25가 나타나면 정해 둔 작은 범위의 모든 대조가 끝났다는 뜻이다.
마지막 if 문은 이 파일을 직접 실행했을 때 main을 호출한다. 판정 함수와 실행 부분을 나누면 예시 데이터를 바꾸더라도 판정 규칙을 그대로 유지할 수 있다. 입력을 읽는 코드와 답을 정하는 코드를 분리하는 습관은 문제의 뜻을 다시 확인할 때도 도움이 된다.
실행 결과
macOS 또는 Linux에서 Python 3.12 이상으로 다음 명령을 실행한다.
python3 main.py
예상 출력은 다음과 같다. 첫 표의 여덟 줄은 출입 문제의 모든 입력 조합이다. 예약과 현장 접수가 모두 참이어도 중단이 참이면 거절한다.
예약 현장 중단 | 판정
0 0 0 | 거절
0 0 1 | 거절
0 1 0 | 허용
0 1 1 | 거절
1 0 0 | 허용
1 0 1 | 거절
1 1 0 | 허용
1 1 1 | 거절
출입 전수 대조: 8개 일치
재고 전수 대조: 25개 일치
출입 대조는 가능한 입력 전체에 대한 확인이다. 재고 대조는 0부터 4까지로 한정한 확인이다. 두 출력에 모두 “전수”라는 말이 들어가더라도 어떤 범위를 전부 확인했는지 함께 읽어야 한다. 검증 범위를 기록하지 않으면 결과를 실제보다 넓게 해석하기 쉽다.
실무에서 자주 틀리는 것
중단 조건을 현장 접수에만 붙인다
다음 식은 예약자가 운영 중단을 우회하게 만든다. 예약 1, 현장 0, 중단 1을 대입하면 잘못된 식은 참이다.
# 틀린 코드
return reserved or onsite and not closed
# 고친 코드
return (reserved or onsite) and not closed
연산 순서를 외웠더라도 괄호로 규칙의 묶음을 드러내는 편이 읽기 쉽다. “중단 여부가 어느 사람에게 적용되는가”를 문장으로 다시 설명하면 괄호의 위치를 결정할 수 있다.
또는을 정확히 하나로 해석한다
두 논리값이 다른지를 검사하면 둘 중 하나만 참일 때 허용한다. 예약과 현장 접수를 모두 마친 사람도 허용해야 하는 이번 규칙과 다르다.
# 틀린 코드
return (reserved != onsite) and not closed
# 고친 코드
return (reserved or onsite) and not closed
예약 1, 현장 1, 중단 0이 두 식을 구별한다. 반대로 문제에서 “정확히 하나”를 요구했다면 논리값끼리의 !=가 그 뜻에 맞는다. 어떤 연산이 익숙한지보다 문장이 무엇을 허용하는지가 먼저다.
상한만 확인하고 하한을 빠뜨린다
재고보다 많지 않다는 사실만으로 요청을 처리할 수는 없다. 요청 0은 재고가 0이어도 상한 조건을 통과한다.
# 틀린 코드
return quantity <= stock
# 고친 코드
return 1 <= quantity <= stock
반대로 상한에 <를 쓰면 남은 재고를 정확히 모두 요청하는 경우를 거절한다. 수량 조건을 읽을 때는 하한 바로 아래, 하한, 상한, 상한 바로 위를 후보로 적는다. 두 경계가 겹치는 재고 0도 따로 확인한다.
검증 코드에 같은 식을 복사한다
아래의 틀린 검증은 변수 이름만 다르고 계산이 같다. 두 결과가 같다는 사실이 원래 규칙과의 일치를 뒷받침하지 못한다.
# 틀린 검증
fast = can_enter(*case)
slow = can_enter(*case)
# 고친 검증
fast = can_enter(*case)
slow = can_enter_slow(case, allowed)
고친 검증에서는 중단을 먼저 제외하는 절차로 기준 목록을 만들었다. 그래도 기준 목록을 만드는 해석이 틀릴 가능성은 남아 있다. 그래서 명제식에 대한 정확성 근거와 손으로 판단한 반례를 함께 사용한다. 서로 다른 확인 방법이 같은 규칙을 가리키는지 살피는 것이 핵심이다.
한눈에 보기
| 작업 | 확인할 질문 | 이번 장의 예 | 남길 결과 |
|---|---|---|---|
| 입력 구분 | 판정 전에 어떤 값이 주어지는가 | 예약·현장·중단 여부 | 변수와 값의 범위 |
| 출력 구분 | 한 입력에 무엇을 답하는가 | 입장 허용 여부 | 반환값의 뜻 |
| 조건 결합 | 모두 필요한가, 하나면 되는가 | 접수 조건과 중단 조건 | 괄호가 있는 명제식 |
| 반례 찾기 | 잘못 읽으면 어느 입력이 달라지는가 | 예약 1, 중단 1 | 기대 결과가 있는 사례 |
| 정확성 설명 | 왜 모든 허용 입력에서 규칙과 같은가 | 중단 여부로 경우 나누기 | 짧은 논리적 근거 |
| 전수 대조 | 작은 범위의 모든 입력이 일치하는가 | 출입 8개, 재고 25개 | 범위를 명시한 검증 결과 |
문제를 읽은 결과는 짧은 문장으로도 남길 수 있다. “예약 또는 현장 접수 중 하나 이상이 필요하고, 중단 중이면 거절한다”라고 적은 뒤 코드와 비교한다. 함수의 식을 한국어로 다시 읽었을 때 다른 뜻이 나오면 구현보다 해석부터 고친다.
다음에는 같은 정답을 구하는 코드라도 수행하는 연산 수가 어떻게 달라지는지 살펴본다. 이 장의 수량 나열과 직접 비교는 그 차이를 관찰할 출발점이 된다.
연습 문제
출입 규칙을 “예약과 현장 접수 중 정확히 하나를 마쳤고, 운영 중단이 아닐 때 허용한다”로 바꾼다. 세 입력이 논리값일 때 조건식을 작성하고, 기존 식과 결과가 달라지는 입력을 하나 제시한다.
재고 요청을 처리한 뒤 최소 2개의 꾸러미를 남겨야 한다는 조건을 추가한다. 요청은 여전히 1개 이상이어야 한다. 조건식을 작성하고 재고 2, 3, 4에서 허용할 요청 수량을 각각 나열한다.
출입 문제의 예시 입력에서 마지막 줄을 지우고 첫 줄도 7로 바꾸면 어떤 검사에서 실패하는지 설명한다. 마지막 줄을 앞의 줄과 같은 조합으로 바꾸되 첫 줄을 8로 유지하면 무엇을 통해 누락을 발견하는지도 설명한다.
재고 전수 대조 범위를 두 변수 모두 0부터 6까지로 넓힌다. 바꿔야 할 반복문과 예상 대조 개수를 적는다. 이 검사만으로 모든 0 이상의 정수에 대한 정확성을 주장할 수 있는지도 설명한다.
정답과 해설
논리값 두 개가 서로 다를 때 정확히 하나가 참이다. 조건식은 다음과 같다.
(reserved != onsite) and not closed예약 1, 현장 1, 중단 0에서 기존 식은 허용하고 바뀐 식은 거절한다. 두 접수가 모두 있는 경우를 허용할지 여부가 달라졌기 때문이다. 예약과 현장이 모두 거짓인 경우에는 두 식 모두 거절한다.
처리 후 재고는 stock - quantity다. 이 값이 2 이상이어야 하므로 다음처럼 쓴다.
1 <= quantity and stock - quantity >= 2같은 조건을 1 <= quantity <= stock - 2로 쓸 수도 있다. 재고 2에서는 허용할 요청이 없다. 재고 3에서는 1개, 재고 4에서는 1개와 2개가 허용된다. 재고가 작으면 조건을 만족하는 수량이 없을 수 있으며, 이를 별도의 허용 사례로 만들 필요는 없다.
사례를 일곱 개로 줄이면 각 사례의 판정 대조는 일치할 수 있지만, 반복 뒤 case_count != 8이 참이 되어 오류가 발생한다. 사례 수를 여덟 개로 유지하면서 한 줄을 중복시키면 seen의 원소 수가 일곱 개가 된다. 따라서 len(seen) != 8로 실패한다. 입력 형식이 0과 1로 제한되어 있으므로 서로 다른 여덟 조합을 확인했다는 사실이 전체 조합 확인으로 이어진다.
두 range(5)를 각각 range(7)로 바꾼다. 두 변수에 가능한 값이 각각 일곱 개이므로 대조 개수는 49개다.
for stock in range(7): for quantity in range(7): fast = can_supply(stock, quantity) slow = can_supply_slow(stock, quantity) if fast != slow: raise AssertionError((stock, quantity))49개가 일치해도 검사하지 않은 큰 값의 결과까지 직접 확인한 것은 아니다. 모든 허용 입력에 대한 근거는 별도로 필요하다. 요청 수량이 1부터 재고까지 나열한 정수 중 하나라는 조건과 1 이상 재고 이하라는 조건이 같다는 설명이 그 근거다.
READER FEEDBACK
질문·의견
내용에 관한 질문이나 더 나은 설명을 위한 의견을 남겨 주세요. 오탈자는 위의 제보 양식이 더 빨리 반영됩니다. 이 댓글은 원래 게시글과 같은 자리에 쌓입니다.
댓글 0
아직 댓글이 없습니다. 첫 댓글을 남겨 보세요.