행렬의 모양·덧셈·곱셈
행·열 크기 조건을 검사하고 행과 열의 내적으로 곱을 만듭니다.
그래프의 행렬을 읽었다면 숫자를 담은 행렬 연산도 할 수 있습니다. 목표는 행·열 크기를 먼저 정하고 행렬 곱을 원소별 곱과 구분하는 것입니다. 중첩 리스트·반복·곱셈을 복습하세요.
개념과 약속
행렬은 열 수가 같은 행들로 이루어진 숫자 표입니다. m행 k열은 m×k입니다. 덧셈은 모양이 같은 두 행렬의 같은 칸을 더합니다. 곱셈 A(m×k)·B(k×n)은 A의 열 수와 B의 행 수가 같아야 하며 결과는 m×n입니다. 결과 칸 (i,j)는 A의 행 i와 B의 열 j를 대응해 곱한 뒤 모두 더한 값입니다.
아래 입력은 직사각형 중첩 리스트입니다. []는 0×0으로 허용하지만 [[]]처럼 원소가 없는 행과 행 길이가 다른 입력은 거부합니다. 결과 각 행을 독립적으로 만드는 리스트 컴프리헨션을 씁니다. [[0]*n]*m은 같은 행을 여러 번 참조할 수 있어 이후 한 칸 변경이 다른 행에도 나타나는 문제에 주의하세요.
행·열의 대응 곱을 더해 한 칸을 만든다
결과의 첫 셀에 필요한 두 벡터를 읽는다. 부분 곱을 합해 결과 셀을 채운다. 각 단계의 값과 전환 이유를 비교합니다.
자료구조·알고리즘 · 상태와 관계
행·열의 대응 곱을 더해 한 칸을 만든다
A 첫 행 1,2와 B 첫 열 5,7을 읽어 1×5+2×7=19를 계산합니다.
위 두 줄은 한 행과 한 열의 값들을 대응 위치에 펼친 것입니다. 전체 행렬의 모양을 그린 것이 아닙니다. 화살표는 곱과 합의 계산 의존 관계입니다.
이 예의 조건
- 본문과 전체 도표의 예를 따라 행과 열 선택부터 합해서 기록까지 진행합니다.
관계도의 보충 설명
| 구분 | 전체 도표의 내용 |
|---|---|
| 관계 설명 | A 행의 값 1,2와 B 열을 가로로 펼쳐 놓은 값 5,7이 대응된다. 두 곱 1 곱하기 5와 2 곱하기 7을 더해 결과 한 칸의 값 19를 만든다. |
| 노드·선 표기 | A 행 |
| 노드·선 표기 | B 열 |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 행과 열 선택 결과의 첫 셀에 필요한 두 벡터를 읽는다. |
|
|
| 대응 항 곱하기 같은 위치의 두 항을 각각 곱한다. |
|
|
| 합해서 기록 부분 곱을 합해 결과 셀을 채운다. |
|
|
행·열의 대응 곱을 더해 한 칸을 만든다
결과의 첫 셀에 필요한 두 벡터를 읽는다. 부분 곱을 합해 결과 셀을 채운다. 각 단계의 값과 전환 이유를 비교합니다.
자료구조·알고리즘 · 상태와 관계
행·열의 대응 곱을 더해 한 칸을 만든다
A 첫 행 1,2와 B 첫 열 5,7을 읽어 1×5+2×7=19를 계산합니다.
위 두 줄은 한 행과 한 열의 값들을 대응 위치에 펼친 것입니다. 전체 행렬의 모양을 그린 것이 아닙니다. 화살표는 곱과 합의 계산 의존 관계입니다.
이 예의 조건
- 본문과 전체 도표의 예를 따라 행과 열 선택부터 합해서 기록까지 진행합니다.
관계도의 보충 설명
| 구분 | 전체 도표의 내용 |
|---|---|
| 관계 설명 | A 행의 값 1,2와 B 열을 가로로 펼쳐 놓은 값 5,7이 대응된다. 두 곱 1 곱하기 5와 2 곱하기 7을 더해 결과 한 칸의 값 19를 만든다. |
| 노드·선 표기 | A 행 |
| 노드·선 표기 | B 열 |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 행과 열 선택 결과의 첫 셀에 필요한 두 벡터를 읽는다. |
|
|
| 대응 항 곱하기 같은 위치의 두 항을 각각 곱한다. |
|
|
| 합해서 기록 부분 곱을 합해 결과 셀을 채운다. |
|
|
손으로 상태 추적하기
A=[[1,2],[3,4]], B=[[5,6],[7,8]]입니다.
| 결과 칸 | 행·열 계산 | 값 |
|---|---|---|
| (0,0) | 1×5 + 2×7 | 19 |
| (0,1) | 1×6 + 2×8 | 22 |
| (1,0) | 3×5 + 4×7 | 43 |
| (1,1) | 3×6 + 4×8 | 50 |
직접 실행하기
아래 코드는 이 본문만으로 실행할 수 있습니다. lesson.py로 저장하고 첫 장에서 확인한 Python 명령으로 실행하세요.
중첩 리스트 컴프리헨션에서 안쪽은 결과 한 행의 각 열을 만들고 바깥쪽은 모든 행을 만듭니다. sum(...)은 k별 곱들을 모두 더합니다. zip은 대응하는 두 행 원소를 한 쌍씩 읽으며, 이 코드에서는 모양 검사를 먼저 해 길이 누락을 막습니다.
def matrix_shape(matrix):
if not matrix:
return 0, 0
columns = len(matrix[0])
if columns == 0 or any(len(row) != columns for row in matrix):
raise ValueError("nonempty rectangular rows required")
return len(matrix), columns
def matrix_add(first, second):
if matrix_shape(first) != matrix_shape(second):
raise ValueError("equal shapes required")
return [[a + b for a, b in zip(row_a, row_b)] for row_a, row_b in zip(first, second)]
def matrix_multiply(first, second):
rows, shared = matrix_shape(first)
other_rows, columns = matrix_shape(second)
if shared != other_rows:
raise ValueError("first columns must equal second rows")
return [[sum(first[i][k] * second[k][j] for k in range(shared))
for j in range(columns)] for i in range(rows)]
a = [[1, 2], [3, 4]]
b = [[5, 6], [7, 8]]
print(matrix_add(a, b))
print(matrix_multiply(a, b))
print(matrix_multiply(b, a))
try:
matrix_add([[1]], [[1, 2]])
except ValueError:
print("모양 불일치 거부")실행 결과:
[[6, 8], [10, 12]]
[[19, 22], [43, 50]]
[[23, 34], [31, 46]]
모양 불일치 거부왜 맞고 언제 끝나는가
공통 인덱스 k를 같은 범위로 순회하면 각 행·열에 필요한 모든 대응 곱을 한 번씩 더합니다. 모양 검사가 범위 접근을 보장합니다. 결과 행 i·열 j를 모두 생성하므로 빠진 칸이 없습니다. 세 범위가 유한해서 종료합니다.
비용과 적용 범위
덧셈은 O(mn), 일반적인 세 반복 곱셈은 O(mkn) 시간이며 결과 공간 O(mn)입니다. 이 교육용 구현은 수치 안정성·벡터화 최적화·희소 행렬을 다루는 전문 라이브러리의 대체가 아닙니다. 작은 정수로 정확한 상태를 확인하는 목적입니다.
스스로 풀어 보기
- 2×3 행렬과 3×4 행렬의 곱은 가능한가요? 결과 모양은 무엇인가요?
- AB와 BA가 항상 같다는 주장을 위 출력으로 판단하세요.
힌트
- 안쪽 두 수가 같아야 합니다.
- (0,0)부터 값이 다릅니다.
해설
- 가능하며 2×4입니다.
- 일반적으로 같지 않습니다. 이 예에서 19와 23 등 값이 다릅니다. 또한 AB가 가능해도 BA의 크기 조건이 맞지 않을 수 있습니다.
흔한 오해
행렬 곱은 같은 위치끼리만 곱하는 연산이 아닙니다. 그래프 인접 행렬의 숫자 곱과 Warshall의 불리언 식도 목적이 다릅니다.