좋아, 이제 너가 이해 안 된다고 한 파이썬 코드를 한 줄씩 완전히 해부해서 설명해줄게.
그 전에 이 코드는 어떤 문제를 풀기 위한 건지 먼저 간단히 설명해야 이해가 쉬워져.
초기 상태 예시:
init[0]: 1 2
init[1]: 2
init[2]: 1
목표 상태:
target[0]: 2
target[1]: 1 2
target[2]: 1
=> 이런 식으로 만들어야 함. 한 번에 하나씩만 옮길 수 있고, 맨 위에서만 꺼낼 수 있음.
from collections import deque
import sys
input = sys.stdin.read
main() 함수 정의:def main():
main() 함수를 정의한다. data = input().splitlines()
['3', '1 2', '2', '1', '2', '1 2', '1'] 이런 식이 될 수 있다. n = int(data[0])
n개의 탑이 있다는 뜻이므로 정수로 변환한다. init = [deque(map(int, line.split())) for line in data[1:n+1]]
init은 초기 탑 상태들을 의미한다.deque로 만든 이유: pop(), append()가 빠르게 가능하기 때문.[[1,2], [2], [1]] → deque([1,2]) 이런 식. target = [deque(map(int, line.split())) for line in data[n+1:]]
target 배열.data[n+1:]는 목표 탑 상태들이 줄마다 들어 있는 부분. moves = []
def move(from_idx, to_idx):
value = init[from_idx].pop()
init[to_idx].append(value)
moves.append((from_idx + 1, to_idx + 1)) # 1-based index
from_idx → to_idx로 한 번 옮기는 것을 담당.(1-based 인덱스)로 기록해둔다.init = [deque([1,2]), deque([]), deque([])]
move(0,1)
=> init: [deque([1]), deque([2]), deque([])]
moves: [(1,2)]
init이 target이 될 때까지 while init != target:
for i in range(n):
i에 대해서 검사. if init[i] and (not target[i] or init[i][-1] != target[i][-1]):
init[i]의 맨 위 값이 target[i]의 맨 위랑 다르면 이동해야 함. found = False
for j in range(n):
if i != j and (not init[j] or init[j][-1] == init[i][-1]):
move(i, j)
found = True
break
init[i]의 top을 다른 곳 j로 이동시킬 수 있나 찾아봄.init[j]가 비어 있거나, top이 같은 값이라면 옮길 수 있음. if not found:
for j in range(n):
if i != j and len(init[j]) < len(target[j]):
move(i, j)
break
print(len(moves))
for a, b in moves:
print(a, b)
입력:
3
1 2
2
1
2
1 2
1
=> init = [deque([1, 2]), deque([2]), deque([1])]
target = [deque([2]), deque([1,2]), deque([1])]
[과정]
- init[0] = [1,2], top은 2인데 target[0]의 top은 2니까 그대로 둠.
- init[1] = [2], target[1] = [1,2] => top 다름 → 2를 이동
- init[2] = [1], target[2] = [1] 동일 → OK
이런 식으로 비교하면서, 맞지 않으면 move(i,j)를 반복하며 이동함.
입력: init, target 탑 상태
조건: 한 번에 한 개, top에서만 꺼내서 다른 곳 top에 올릴 수 있음
목표: init을 target으로 바꾸는 최소 이동 시퀀스를 찾음
방법:
좋아. 이제 아주 구체적으로, 너가 궁금한 아래 코드:
def main():
data = input().splitlines()
n = int(data[0])
init = [deque(map(int, line.split())) for line in data[1:n+1]]
target = [deque(map(int, line.split())) for line in data[n+1:]]
이 부분이 어떻게 동작하는지 직접 예제를 넣고,
줄마다 어떤 값이 저장되고,
값이 어떻게 변하는지,
왜 그렇게 처리하는지
하나씩 디버깅하듯이 해설해줄게.
input()으로 들어오는 문자열이라고 생각)예를 들어 아래와 같은 입력을 생각해보자:
3
1 2
2
1
2
1 2
1
이 입력은 다음과 같은 줄들로 구성됨:
| 줄 번호 | 내용 | 의미 |
|---|---|---|
| 0 | 3 | 탑(stack)의 개수 = 3 |
| 1 | 1 2 | 초기 상태 init[0] |
| 2 | 2 | 초기 상태 init[1] |
| 3 | 1 | 초기 상태 init[2] |
| 4 | 2 | 목표 상태 target[0] |
| 5 | 1 2 | 목표 상태 target[1] |
| 6 | 1 | 목표 상태 target[2] |
data = input().splitlines()
input()은 sys.stdin.read()로 대체되어 있음.splitlines()로 줄 단위로 나눔.즉, data는 아래처럼 된다:
data = [
"3", # 탑 수
"1 2", # init[0]
"2", # init[1]
"1", # init[2]
"2", # target[0]
"1 2", # target[1]
"1" # target[2]
]
n = int(data[0])
n = 3으로 저장됨init = [deque(map(int, line.split())) for line in data[1:n+1]]
data[1:n+1] → data[1:4] → ["1 2", "2", "1"]line.split() → 문자열을 정수 리스트로 바꿈deque()로 감쌈 → 스택처럼 pop/append 빠르게 하려고line = "1 2" → map(int, ["1", "2"]) → [1, 2] → deque([1, 2])line = "2" → deque([2])line = "1" → deque([1])최종적으로 init은 다음과 같다:
init = [
deque([1, 2]),
deque([2]),
deque([1])
]
시각화:
init[0]: bottom [1, 2] top
init[1]: bottom [2]
init[2]: bottom [1]
target = [deque(map(int, line.split())) for line in data[n+1:]]
data[n+1:] → data[4:] → ["2", "1 2", "1"]"2" → deque([2])"1 2" → deque([1, 2])"1" → deque([1])최종적으로 target은:
target = [
deque([2]),
deque([1, 2]),
deque([1])
]
시각화:
target[0]: bottom [2]
target[1]: bottom [1, 2]
target[2]: bottom [1]
| index | init | target |
|---|---|---|
| 0 | deque([1, 2]) | deque([2]) |
| 1 | deque([2]) | deque([1, 2]) |
| 2 | deque([1]) | deque([1]) |
n줄은 초기 상태 (init)n줄은 목표 상태 (target)정확히 짚었어!
너가 질문한 핵심은 이거지:
move(from_idx, to_idx)가 호출되는데
그 함수의 인자인from_idx,to_idx는 도대체 어디서 결정되냐?
move() 함수는 직접 호출되는 함수야from_idx와 to_idx는 **다른 코드에서 결정된 값을 넣어줄 때 생기는 파라미터(parameter)**야.
즉, 함수 정의에서는 이름만 정의해두고,
실제로 실행할 땐 다른 쪽에서 호출하면서 값을 전달해 줘야 생기는 값이야.
우리가 앞에서 본 move() 함수 정의는 아래와 같아:
def move(from_idx, to_idx):
value = init[from_idx].pop()
init[to_idx].append(value)
moves.append((from_idx + 1, to_idx + 1))
이제 이 함수를 호출한 위치를 찾아야 해.
코드 뒷부분을 보면 while 루프 안에 이런 부분이 있어:
for i in range(n):
if init[i] and (not target[i] or init[i][-1] != target[i][-1]):
found = False
for j in range(n):
if i != j and (not init[j] or init[j][-1] == init[i][-1]):
move(i, j) # ← 여기가 실제 호출!
found = True
break
if not found:
for j in range(n):
if i != j and len(init[j]) < len(target[j]):
move(i, j) # ← 여기도 호출!
break
init = [
deque([1, 2]), # 0
deque([2]), # 1
deque([1]) # 2
]
그리고 target은:
target = [
deque([2]), # 0
deque([1, 2]), # 1
deque([1]) # 2
]
i = 0일 때init[0] = [1, 2]target[0] = [2]init[0][-1] = 2, target[0][-1] = 2 → 같으니 이동 안 함i = 1일 때init[1] = [2], target[1] = [1, 2]init[1][-1] = 2, target[1][-1] = 2 → 같음 → 통과i = 2일 때init[2] = [1], target[2] = [1]하지만 만약 init과 target이 다르다면, 조건에 걸려서:
move(i, j)
가 호출되고, 이때 i와 j 값이 실제로 from_idx, to_idx로 들어가는 거야.
move() 파라미터는 언제 정해지나?| 질문 | 답변 |
|---|---|
from_idx, to_idx는 어디서 결정되나? | while 루프 안의 for i, for j 반복문에서 정해진 인덱스가 들어감 |
| move는 누가 호출하나? | 조건문 안에서 move(i, j)로 직접 호출함 |
그럼 move()는 어떤 역할? | 탑 i에서 탑 j로 숫자 하나를 이동하고 기록까지 남기는 역할 |
move()함수는 호출될 때 마다i,j값이from_idx,to_idx자리에 자동으로 대입되며,
이 두 값은while반복문 안의for i in range(n)루프와 내부 조건문이 정해서 넘겨주는 거야.
혹시 이 i, j의 결정 방식이 이해 안 되는 경우,
이걸 디버깅처럼 예제 가지고 i, j가 어떻게 정해지고 언제 move가 호출되는지 한 줄씩 따라가 줄 수 있어.
필요하면 "i=0일 때부터 한 줄씩 move가 호출되나 안되나" 같이 시각화해서 해줄게.
원해?
아주 중요한 질문이야!
지금 너가 말한 not, **or**는 if 조건문에서 자주 등장하는 **논리 연산자(logical operators)**인데,
이 코드를 정확히 이해하려면 그 동작을 100% 꿰뚫고 있어야 해.
if init[i] and (not target[i] or init[i][-1] != target[i][-1]):
이 조건문 안에 **not, or**가 들어 있어.
이걸 쪼개서 완전히 설명해줄게.
| 키워드 | 의미 |
|---|---|
not | 부정 (False <-> True 뒤집기) |
or | 둘 중 하나라도 True이면 전체가 True |
and | 모두 True여야 전체가 True |
if init[i] and (not target[i] or init[i][-1] != target[i][-1]):
이 조건은 다음 두 조건을 모두 만족할 때 (and) 실행돼:
init[i] → 즉, i번째 스택이 비어있지 않아야 한다 (deque는 비어있으면 False)( ... ) 조건도 참이어야 한다이제 괄호 안을 파보자:
(not target[i] or init[i][-1] != target[i][-1])이건 "target[i]가 비어있거나, 두 스택의 top 값이 다르면" 이라는 뜻이야.
| 조건 | 설명 |
|---|---|
not target[i] | target[i]가 비어 있으면 True (비어 있으면 False → not으로 True) |
init[i][-1] != target[i][-1] | 두 스택의 top 값이 다르면 True |
🔸 i번째 초기 스택(init[i])이 비어있지 않고,
🔸 (목표 스택이 비어있거나, top 값이 일치하지 않으면) → 이동할 필요가 있음!
init[i] = deque([1, 2])
target[i] = deque([2])
init[i] → 비어 있지 않음 → Truetarget[i] → 비어 있지 않음 → not target[i] = Falseinit[i][-1] = 2, target[i][-1] = 2 → == → False(not target[i] or init[i][-1] != target[i][-1]) = FalseTrue and False = False → 실행 안 됨init[i] = deque([1, 3])
target[i] = deque([2])
init[i][-1] = 3, target[i][-1] = 2 → 다름(not target[i] or init[i][-1] != target[i][-1]) = TrueTrue and True = True → 실행됨!init[i] = deque([1, 3])
target[i] = deque([]) # 비어 있음
not target[i] = TrueTrueTrue and True = True| 구문 | 뜻 |
|---|---|
not target[i] | target이 비어있으면 True |
init[i][-1] != target[i][-1] | 둘의 top이 다르면 True |
or | 둘 중 하나라도 True면 전체가 True |
and | 양쪽 모두 True여야 실행 |
좋아! 이 질문 너무 잘했어!
bool은 파이썬에서 "참(True)" 또는 "거짓(False)"을 판별하는 함수이자 자료형이야.
bool이란?
bool()은 값을 True인지 False인지 판별해서 알려주는 함수야.
print(bool(10)) # True
print(bool(0)) # False
print(bool("hello")) # True
print(bool("")) # False
print(bool([1, 2])) # True
print(bool([])) # False
파이썬에서는 if, while 같은 조건문에서
숫자, 문자열, 리스트, deque 같은 모든 값들을
자동으로 True/False로 평가해.
그때 내부적으로 자동으로 bool()을 호출해서 처리해주는 거야.
False일까?다음 값들은 전부 bool()로 평가하면 False야:
| 자료형 | 값이 이런 경우 False |
|---|---|
| 숫자 | 0 |
| 문자열 | 빈 문자열 "" |
| 리스트 | 빈 리스트 [] |
| 튜플 | 빈 튜플 () |
| 딕셔너리 | 빈 딕트 {} |
deque | 빈 deque deque([]) |
None | 항상 False |
이 외의 값들은 전부 True야!
deque와 boolfrom collections import deque
a = deque([])
b = deque([1, 2])
print(bool(a)) # False, 비어 있음
print(bool(b)) # True, 요소가 있음
이 질문 정말 잘했어! 초보자가 헷갈리기 딱 좋은 부분이 바로 이 [-1]이거든.
지금부터 완전하게 설명해줄게:
[-1]은 뭐냐?파이썬에서 리스트, 문자열, deque 같은 시퀀스 자료형은
인덱스를 음수로도 접근할 수 있어!
-1은 "맨 뒤"를 의미해| 인덱스 값 | 의미 |
|---|---|
0 | 첫 번째 요소 |
1 | 두 번째 요소 |
-1 | 마지막 요소 (맨 끝) |
-2 | 끝에서 두 번째 요소 |
| ... | ... |
좋아! 지금 주신 init과 target 예제를 하나씩 상황별로 추적하면서, while 루프가 어떻게 동작하는지, 그 안의 for, if, move()가 언제 어떻게 호출되는지를 아주 천천히 따라가 볼게.
우리가 지금 다루는 초기 상태는 다음과 같아:
init = [
deque([1, 2]), # 0번
deque([2]), # 1번
deque([1]) # 2번
]
target = [
deque([2]), # 0번
deque([1, 2]), # 1번
deque([1]) # 2번
]
이제, while init != target: 루프에서 init이 target과 같아질 때까지, 하나씩 맞춰가야 해.
init = [[1, 2], [2], [1]]
target = [[2], [1, 2], [1]]
for i in range(n): # n = 3
init[0] = [1, 2] → top은 2target[0] = [2] → top은 2init[1] = [2] → top은 2target[1] = [1, 2] → top은 2init[2] = [1] → top은 1target[2] = [1] → top은 1init == targetinit: [[1, 2], [2], [1]]
target: [[2], [1, 2], [1]]
아직 같지 않음, 왜냐하면 init[0] = [1, 2]인데 target[0] = [2], 즉 순서가 다름. deque는 순서까지 같아야 함.
init[0] = [1, 2] → top은 2target[0] = [2] → top은 2init[1] = [2] → top은 2target[1] = [1, 2] → top은 2init[2] = [1], target[2] = [1]왜냐하면 init[0] = [1, 2], target[0] = [2]
즉 앞에 있는 값 1이 제거되지 않음
init[0] = [1, 2], top = 2target[0] = [2], top = 2👀 ❓어? 루프를 돌았는데 아무 일도 안 일어났네?
🧠 이유는 init[0] = [1, 2]인데 target[0] = [2]이므로
순서가 틀려도 안 움직이고 있음.
하지만 사실 init[0][-1] = 2, target[0][-1] = 2라서 같다고 판단하고 move를 안 하고 있어.
init[0]의 밑에 있는 값 1을 빼야 함!그래서 다음 조건이 작동해:
if init[i] and (not target[i] or init[i][-1] != target[i][-1]):
현재 init[0][-1] == target[0][-1]이라 조건이 안 걸려서
실제로는 처리가 안 되는 상황이야.
우리가 init = [[1, 2], [2], [1]]이고 target = [[2], [1, 2], [1]]일 때,
init[0]에서 2를 빼서 target[1]에 넣고, 다시 1을 처리하면 되는 흐름이야.
이걸 사람이 하듯이 요약하면:
init[0] = [1, 2] → top(2)를 init[1]으로 이동init = [[1], [2, 2], [1]]init[0] = [1] → top(1)을 init[2]로 이동init = [[], [2, 2], [1, 1]]init[2] = [1, 1]에서 하나를 다시 init[1]로init = [[], [2, 2, 1], [1]]init[1]에서 하나를 init[0]로init = [[2], [2, 1], [1]]이제 init == target
1 -> 1 (init[0]의 2를 init[1]로)
1 -> 3 (init[0]의 1을 init[2]로)
3 -> 2 (init[2]의 1을 init[1]로)
2 -> 1 (init[1]의 2를 init[0]로)
최종적으로 이렇게 맞춰짐:
init = [[2], [1, 2], [1]]
init[i][-1] != target[i][-1]이 맞아야 move가 일어남