Trong bài này
Lan ghi số cây ở hai luống, mỗi luống có ba vị trí. Luống đầu có 2, 4, 1 cây; luống sau có 3, 0, 5 cây. Lan muốn tính số cây từng luống và xem các vị trí cạnh nhau. Một bảng giúp Lan giữ được cả hàng lẫn cột của từng số.
Mỗi hàng trong bảng là một danh sách
Lan đặt mỗi luống trong một danh sách, rồi đặt hai danh sách ấy vào một danh sách lớn:
bang = [[2, 4, 1], [3, 0, 5]]
print(bang[0])
print(bang[1][2])[2, 4, 1] 5
bang có hai phần tử. Mỗi phần tử là một danh sách chứa một hàng. Đây là danh sách hai chiều (two-dimensional list). Trong bang[1][2], chỉ số đầu chọn hàng 1; chỉ số sau chọn cột 2 trong hàng đó. Cả hai chỉ số đều bắt đầu từ 0, nên giá trị nhận được là 5.
| Hàng | Cột 0 | Cột 1 | Cột 2 |
|---|---|---|---|
| 0 | 2 | 4 | 1 |
| 1 | 3 | 0 | 5 |
Trong bảng, cột đầu ghi chỉ số hàng và các tiêu đề còn lại ghi chỉ số cột. Giao điểm của hàng 1 và cột 2 chứa 5. Đừng nhầm chỉ số hàng với chỉ số cột.
Bạn có thể thay một ô bằng phép gán:
bang = [[2, 4, 1], [3, 0, 5]]
bang[1][1] = 2
print(bang[1])[3, 2, 5]
Thao tác này thay ô giữa của hàng 1. Các ô khác giữ nguyên. Nếu chọn một chỉ số không tồn tại, Python báo lỗi:
bang = [[2, 4, 1], [3, 0, 5]]
print(bang[2][0])Lỗi khi chạy, ở dòng 2
IndexError: list index out of range
Hai hàng chỉ có chỉ số 0 và 1. Lỗi IndexError nhắc bạn kiểm tra chỉ số trước khi truy cập. len(bang) cho số hàng; nếu bảng có ít nhất một hàng, len(bang[0]) cho số phần tử của hàng đầu. Các ví dụ còn lại dùng bảng chữ nhật (rectangular grid): mọi hàng có cùng số cột.
Danh sách hai chiều không tự đảm bảo các hàng dài bằng nhau. Với [[1, 2], [3]], hàng sau chỉ có một ô. Vì vậy bang[1][1] sẽ gây lỗi dù hàng đầu có cột 1. Trước khi dùng số cột của hàng đầu cho toàn bảng, dữ liệu cần đảm bảo mọi hàng có cùng độ dài.
Tạo các hàng độc lập để sửa đúng một ô
Bạn cần một bảng hai hàng, ba cột ban đầu đều bằng 0. Hãy tạo từng hàng riêng bằng biểu thức danh sách đã học:
n = 2
m = 3
bang = [[0] * m for _ in range(n)]
bang[0][1] = 7
print(bang)[[0, 7, 0], [0, 0, 0]]
[0] * m tạo một hàng gồm ba số 0. Phần for _ in range(n) tạo một hàng mới ở mỗi lượt. Vì vậy khi sửa hàng 0, hàng 1 không đổi. Biến _ cho biết bạn không cần dùng số thứ tự của lượt tạo hàng.
Một cách viết có vẻ ngắn hơn lại cho kết quả khác:
bang = [[0] * 3] * 2
bang[0][1] = 7
print(bang)[[0, 7, 0], [0, 7, 0]]
Phép nhân ngoài lặp lại tham chiếu tới cùng một hàng, không tạo hai hàng độc lập. Sửa một ô qua hàng 0 cũng làm thay đổi hàng nhìn qua hàng 1. Hãy dùng [[0] * m for _ in range(n)] khi cần cập nhật từng hàng riêng.
Điều tương tự xảy ra nếu bạn tạo một hàng, rồi thêm chính hàng đó nhiều lần. Các vị trí trong danh sách ngoài vẫn cùng trỏ tới hàng ban đầu. Tạo hàng mới trong từng lượt giúp mỗi luống có dữ liệu riêng.
Khi tạo bảng, n là số hàng và m là số ô trong mỗi hàng. Nếu n = 1, biểu thức vẫn tạo một hàng độc lập. Nếu m = 1, mỗi hàng chỉ có một ô. Những kích thước nhỏ này hữu ích khi kiểm tra chương trình xử lý biên.
Đọc và in từng hàng của lưới
Giả sử dòng đầu cho số hàng n và số cột m. Sau đó có đúng n dòng, mỗi dòng chứa m số nguyên cách nhau bằng dấu cách:
2 3 2 4 1 3 0 5
n, m = map(int, input().split())
bang = []
for _ in range(n):
hang = list(map(int, input().split()))
bang.append(hang)
print(bang[1][0])3
Mỗi lần input() đọc một dòng. split() tách các số viết dưới dạng chữ; map(int, ...) đổi chúng thành số nguyên. list(...) tạo một hàng, rồi append thêm hàng đó vào bảng. Theo dữ liệu mẫu, hàng 1 bắt đầu bằng 3.
Lưới ký tự có thể được cho bằng các dòng không có dấu cách giữa các ô. Chuyển từng dòng thành danh sách ký tự nếu bạn cần sửa một ô:
2 3 ... .#.
n, m = map(int, input().split())
luoi = []
for _ in range(n):
hang = list(input())
luoi.append(hang)
luoi[0][1] = "X"
print(luoi[0])['.', 'X', '.']
Mỗi dòng có đúng m ký tự. list(input()) tách dòng thành các ký tự để ô luoi[0][1] có thể được thay. Đừng dùng split() khi các ký tự liền nhau: không có dấu cách để tách chúng thành từng ô.
Để in bảng số, duyệt từng hàng và mở các phần tử của hàng trong print:
bang = [[2, 4, 1], [3, 0, 5]]
for hang in bang:
print(*hang)2 4 1 3 0 5
print(*hang) in các số trong hàng, ngăn bằng một dấu cách. Với hàng ký tự, print("".join(hang)) nối các ô không có dấu cách. Thao tác in không thay đổi bảng.
Chẳng hạn, sau khi sửa lưới ký tự, bạn có thể in lại từng dòng theo đúng dạng ban đầu:
luoi = [[".", "X", "."], [".", "#", "."]]
for hang in luoi:
print("".join(hang)).X. .#.
Chuỗi rỗng trước join là phần ngăn giữa các ký tự. Nếu dùng print(*hang), các ô sẽ được ngăn bởi dấu cách mặc định. Khi đề bài quy định định dạng đầu ra, khác biệt này có thể làm đáp án không khớp.
Duyệt theo hàng và theo cột cho hai cách tổng hợp
Tổng từng hàng cho biết số cây ở mỗi luống. Duyệt lần lượt các hàng rồi cộng phần tử của hàng:
bang = [[2, 4, 1], [3, 0, 5]]
for hang in bang:
tong = 0
for x in hang:
tong += x
print(tong)7 8
Biến tong được đặt lại về 0 trước mỗi hàng. Nếu đặt nó trước vòng lặp ngoài, tổng của hàng sau sẽ cộng cả hàng trước. Có thể dùng sum(hang) cho cùng kết quả sau khi đã hiểu cách cộng từng ô.
Muốn tính theo cột, giữ chỉ số cột cố định và thay đổi chỉ số hàng:
bang = [[2, 4, 1], [3, 0, 5]]
n = len(bang)
m = len(bang[0])
for cot in range(m):
tong = 0
for hang in range(n):
tong += bang[hang][cot]
print(tong)5 4 6
Ở cột 0, chương trình cộng bang[0][0] và bang[1][0]. Sau đó nó chuyển sang cột 1. Vòng ngoài chạy m lần, còn vòng trong chạy n lần. Cách này áp dụng cho bảng chữ nhật không rỗng; số hàng không nhất thiết bằng số cột.
Nếu cần giữ các tổng để dùng tiếp, hãy tạo danh sách kết quả trước vòng ngoài. Mỗi khi tính xong một cột, thêm tổng của cột đó vào danh sách. Không thêm kết quả trong vòng hàng: khi đó bạn mới có tổng một phần của cột.
Đường chéo chọn ô bằng quan hệ giữa hai chỉ số
Với bảng vuông có n hàng và n cột, đường chéo chính (main diagonal) gồm các ô có chỉ số hàng bằng chỉ số cột:
bang = [[2, 1, 4], [3, 5, 0], [6, 2, 7]]
n = len(bang)
tong = 0
for i in range(n):
tong += bang[i][i]
print(tong)14
Ba ô được chọn là [0][0], [1][1] và [2][2]. Đường chéo còn lại đi từ góc trên bên phải xuống góc dưới bên trái. Ở hàng i, cột tương ứng là n - 1 - i:
bang = [[2, 1, 4], [3, 5, 0], [6, 2, 7]]
n = len(bang)
tong = 0
for i in range(n):
tong += bang[i][n - 1 - i]
print(tong)15
Với bảng có kích thước lẻ, ô giữa thuộc cả hai đường chéo. Nếu tính tổng hợp nhất hai đường, bạn cần tránh cộng ô giữa hai lần.
Ví dụ, khi cạnh bằng 3, ô [1][1] nằm trên cả hai đường. Khi cạnh bằng 4, hai đường không có ô chung. Cách xét ô chung dựa trên chỉ số, không dựa trên giá trị: hai ô khác nhau vẫn có thể cùng chứa số 5.
Kiểm tra biên trước khi xét ô kề
Một ô có tối đa bốn ô kề theo hướng lên, xuống, trái và phải. Ô ở mép có ít hơn vì một số vị trí nằm ngoài bảng. Với ô hàng r, cột c, vị trí phía trên là (r - 1, c).
bang = [[2, 4, 1], [3, 0, 5]]
n = len(bang)
m = len(bang[0])
r = 0
c = 1
if 0 <= r - 1 < n:
print(bang[r - 1][c])
else:
print("Khong co o phia tren")Khong co o phia tren
Ở hàng 0, r - 1 bằng -1 và không phải hàng phía trên trong bảng. Python cho phép chỉ số âm để đếm từ cuối danh sách. Vì thế bang[-1][c] vẫn chạy nhưng chọn nhầm hàng cuối. Kiểm tra biên giúp tránh lỗi kết quả âm thầm này.
Để xét đủ bốn hướng, bạn có thể dùng hai danh sách thay đổi hàng và cột tương ứng:
bang = [[2, 4, 1], [3, 0, 5]]
n = len(bang)
m = len(bang[0])
r = 0
c = 1
doi_hang = [-1, 1, 0, 0]
doi_cot = [0, 0, -1, 1]
tong = 0
for i in range(4):
rr = r + doi_hang[i]
cc = c + doi_cot[i]
if 0 <= rr < n and 0 <= cc < m:
tong += bang[rr][cc]
print(tong)3
Hai danh sách mô tả lần lượt lên, xuống, trái và phải. Mỗi lượt tạo chỉ số ứng viên rr, cc. Chỉ khi cả hai nằm trong biên, chương trình mới đọc ô. Cách này cũng xử lý bảng một hàng, một cột hoặc chỉ một ô.
Điều kiện dùng and vì cả hai chỉ số phải hợp lệ cùng lúc. Điều kiện 0 <= rr < n or 0 <= cc < m chưa đủ: một chỉ số hợp lệ không làm chỉ số còn lại hợp lệ. Ô kề chéo thay cả hàng lẫn cột nên không nằm trong bốn hướng trên.
Chỉ cần một chỉ số vượt biên, vị trí đó không phải một ô của bảng và phải được bỏ qua.
Với ô góc trên bên trái trong bảng có ít nhất hai hàng và hai cột, chỉ hai hướng xuống và phải hợp lệ. Với bảng chỉ có một ô, cả bốn hướng đều nằm ngoài biên. Vì thế vòng lặp không cộng thêm ô nào và tổng bắt đầu bằng 0 vẫn là 0.
Chuyển vị đổi hàng thành cột
Chuyển vị (transpose) tạo bảng mới bằng cách đổi vai trò hàng và cột. Ô ở hàng r, cột c của bảng cũ trở thành ô hàng c, cột r của bảng mới:
bang = [[2, 4, 1], [3, 0, 5]]
n = len(bang)
m = len(bang[0])
ket_qua = [[0] * n for _ in range(m)]
for r in range(n):
for c in range(m):
ket_qua[c][r] = bang[r][c]
for hang in ket_qua:
print(*hang)2 3 4 0 1 5
Bảng cũ có hai hàng và ba cột. Bảng mới có ba hàng và hai cột, nên khi tạo bảng bạn đổi n và m. Mỗi cột cũ xuất hiện thành một hàng mới. Việc tạo các hàng độc lập giúp phép gán không vô tình thay nhiều hàng.
Phép chuyển vị trong ví dụ tạo bảng mới, nên bang vẫn giữ dữ liệu cũ. Khi chuyển vị bảng một hàng, kết quả có một cột. Chuyển vị kết quả thêm lần nữa đưa các giá trị về cùng vị trí ban đầu.
Lỗi thường gặp
| Lỗi | Vì sao sai | Cách sửa |
|---|---|---|
Nhầm bang[c][r] với bang[r][c] | Chỉ số đầu chọn hàng. | Viết thứ tự hàng rồi cột. |
Dùng [[0] * m] * n để sửa từng ô | Các hàng dùng chung một danh sách. | Tạo hàng riêng bằng biểu thức danh sách. |
| Không đặt lại tổng ở mỗi hàng | Tổng mới giữ cả giá trị của hàng trước. | Gán tong = 0 trong vòng lặp ngoài. |
| Truy cập ô kề trước khi kiểm tra biên | Chỉ số âm có thể chọn nhầm hàng cuối. | Kiểm tra cả hàng và cột trước khi đọc. |
Dùng n làm số cột khi chuyển vị | Bảng chữ nhật có thể có hai kích thước khác nhau. | Tạo bảng mới gồm m hàng, mỗi hàng n ô. |
Dùng split() để tách dòng .#. thành ba ô | Dòng không chứa dấu cách phân tách. | Dùng list(input()) với định dạng này. |
Tóm tắt
- Bảng là danh sách các hàng;
bang[r][c]truy cập ô hàngr, cộtc. - Tạo hàng độc lập bằng
[[0] * m for _ in range(n)]. - Duyệt hàng hoặc cột tùy cách tổng hợp; bảng vuông có đường chéo với cột
ihoặcn - 1 - i. - Kiểm tra hai chỉ số trước khi truy cập ô kề.
- Chuyển vị đưa ô
bang[r][c]sang vị trí[c][r].