
Bài toán tìm Mảng con có tổng lớn nhất (Maximum Subarray Problem) là một bài toán kinh điển trong khoa học máy tính. Nó không chỉ thường xuyên xuất hiện trong các cuộc phỏng vấn kỹ thuật mà còn là một ví dụ hoàn hảo để minh họa sức mạnh của tư duy Quy hoạch động (Dynamic Programming).
Bạn xem thêm:
- 7 Sai Lầm Chí Mạng Khi Tự Học Python – Đừng Làm Những Điều Này
- Hướng Dẫn Cài Đặt Python và PyCharm Chi Tiết: Tránh 5 Lỗi Sai Người Mới Hay Gặp
- 50 Bài Tập Python Từ Dễ Đến Khó – Rèn Tư Duy Thuật Toán
Trong bài viết này, chúng ta sẽ phân tích chi tiết bài toán, khám phá các hướng tiếp cận khác nhau, và làm chủ Thuật toán Kadane – giải pháp thanh lịch và hiệu quả nhất để đạt được độ phức tạp thời gian tuyến tính O(N).
1. Phân tích Chuyên sâu Bài toán
Bài toán yêu cầu chúng ta tìm một mảng con (subarray) liên tục (contiguous) trong một mảng số nguyên cho trước sao cho tổng các phần tử trong mảng con đó là lớn nhất.
Phân loại Bài toán
Đây là một bài toán tối ưu hóa thuộc nhóm Quy hoạch động (Dynamic Programming – DP). Bài toán này thể hiện rõ đặc tính Cấu trúc con tối ưu (Optimal Substructure): giải pháp tối ưu cho toàn bộ mảng có thể được xây dựng dựa trên các giải pháp tối ưu của các mảng con nhỏ hơn. Cụ thể, tổng lớn nhất kết thúc tại một chỉ số i phụ thuộc trực tiếp vào tổng lớn nhất kết thúc tại chỉ số i-1.
Phân tích Input/Output và Bối cảnh ban đầu
- Input: Một danh sách (List) các số nguyên
nums. Các phần tử có thể là số dương, số âm hoặc số 0. - Output: Một số nguyên duy nhất, đại diện cho tổng lớn nhất của một mảng con liên tục.
Ví dụ:
Input: nums = [-2, 1, -3, 4, -1, 2, 1, -5, 4]
Output: 6
Giải thích: Mảng con [4, -1, 2, 1] có tổng lớn nhất là 6.
Phân tích Ràng buộc (Constraints)
Việc phân tích ràng buộc là yếu tố then chốt để lựa chọn thuật toán phù hợp. Giả sử các ràng buộc điển hình:
1 <= len(nums) <= 10^5-10^4 <= nums[i] <= 10^4
Với độ dài mảng N lên tới 10^5, chúng ta phải loại bỏ các giải pháp có độ phức tạp O(N^2) hoặc O(N^3), vì chúng sẽ dẫn đến Time Limit Exceeded (TLE). Ràng buộc này yêu cầu một giải pháp có độ phức tạp O(N) hoặc tối đa là O(N log N).
Các Trường hợp biên (Edge Cases) Quan trọng
- Mảng toàn số dương: Tổng lớn nhất là tổng của toàn bộ mảng. Ví dụ:
[1, 2, 3]-> 6. - Mảng toàn số âm: Tổng lớn nhất là phần tử đơn lẻ có giá trị lớn nhất (gần 0 nhất). Ví dụ:
[-5, -1, -3]-> -1. Thuật toán phải xử lý đúng trường hợp này, không được trả về 0 (vì mảng con phải không rỗng). - Mảng có một phần tử: Tổng lớn nhất chính là giá trị của phần tử đó. Ví dụ:
[5]hoặc[-5]. - Mảng chứa số 0: Số 0 có thể là một phần của mảng con tối ưu.
2. Nền tảng Lý thuyết và Công cụ Python

Trọng tâm của giải pháp tối ưu nằm ở việc áp dụng Quy hoạch động một cách thông minh.
Quy hoạch động (Dynamic Programming)
Quy hoạch động giải quyết các bài toán bằng cách chia chúng thành các bài toán con đơn giản hơn và lưu trữ kết quả của các bài toán con để tránh tính toán lại.
Để áp dụng DP vào Maximum Subarray, chúng ta cần xác định trạng thái DP và công thức truy hồi.
Xác định Trạng thái DP
Thay vì cố gắng tìm tổng lớn nhất của bất kỳ mảng con nào ngay lập tức, chúng ta hãy đơn giản hóa câu hỏi:
“Tổng lớn nhất của mảng con KẾT THÚC tại chỉ số
ilà bao nhiêu?”
Gọi trạng thái này là DP[i].
Thiết lập Công thức Truy hồi (Recurrence Relation)
Làm thế nào để tính DP[i] dựa trên các trạng thái trước đó? Khi xem xét phần tử nums[i], có hai khả năng cho mảng con tối ưu kết thúc tại đây:
- Mở rộng mảng con trước đó: Bao gồm
nums[i]cộng với mảng con tối ưu kết thúc tạii-1(tức làDP[i-1]). - Bắt đầu một mảng con mới: Chỉ bao gồm chính
nums[i]. Điều này xảy ra nếuDP[i-1]là số âm, vì việc tiếp tục nó sẽ làm giảm tổng thể.
Do đó, công thức truy hồi là:
DP[i] = max(nums[i], nums[i] + DP[i-1])
Kết quả cuối cùng của bài toán (tổng lớn nhất của bất kỳ mảng con nào) sẽ là giá trị lớn nhất trong toàn bộ mảng DP.
Global Maximum = max(DP[0], DP[1], ..., DP[N-1])
Thuật toán Kadane: Tối ưu hóa Không gian DP
Thuật toán Kadane, được phát triển bởi Joseph B. Kadane, là một phiên bản tối ưu hóa không gian của phương pháp DP trên.
Quan sát công thức truy hồi, chúng ta thấy rằng DP[i] chỉ phụ thuộc vào DP[i-1]. Chúng ta không cần toàn bộ lịch sử của mảng DP để tính toán giá trị tiếp theo.
Điều này cho phép chúng ta tối ưu hóa không gian lưu trữ từ O(N) xuống O(1). Chúng ta chỉ cần sử dụng hai biến:
current_max(hoặclocal_max): Lưu trữ trạng thái DP hiện tại (DP[i]).global_max: Lưu trữ tổng lớn nhất đã tìm thấy cho đến nay (kết quả cuối cùng).
Công cụ Python
Chúng ta sẽ sử dụng các tính năng cơ bản của Python:
max()function: Hàm built-in hiệu quả để triển khai công thức truy hồi một cách ngắn gọn.- List Iteration: Duyệt qua mảng hiệu quả.
- Type Hinting (
typing.List): Đảm bảo mã nguồn rõ ràng và dễ bảo trì.
3. Phát triển Tư duy Thuật toán

Chúng ta sẽ phân tích các hướng tiếp cận từ cơ bản đến tối ưu để hiểu rõ lý do tại sao Thuật toán Kadane lại vượt trội.
3.1. Hướng tiếp cận Vét cạn (Brute Force)
Ý tưởng 1: O(N^3)
Kiểm tra tất cả các cặp chỉ số bắt đầu (i) và kết thúc (j), và sử dụng một vòng lặp thứ ba để tính tổng mảng con từ i đến j.
- Lý do không hiệu quả: Độ phức tạp O(N^3) là quá lớn. Với N=10^5, điều này không khả thi. Nút thắt cổ chai là việc lặp đi lặp lại việc tính tổng trong vòng lặp thứ ba.
Ý tưởng 2: Tối ưu hóa Vét cạn O(N^2)
Loại bỏ vòng lặp thứ ba bằng cách cập nhật tổng khi mở rộng mảng con.
# Pseudocode O(N^2)
max_sum = -infinity
for i in range(N):
current_sum = 0
for j in range(i, N):
current_sum += nums[j]
max_sum = max(max_sum, current_sum)
- Lý do không hiệu quả: Mặc dù tốt hơn, O(N^2) vẫn quá chậm cho N=10^5. Chúng ta vẫn đang thực hiện nhiều phép tính dư thừa và chưa tận dụng được Cấu trúc con tối ưu.
3.2. Tối ưu hóa và Hướng tiếp cận Hiệu quả: Thuật toán Kadane (O(N))
Để đạt được O(N), chúng ta phải áp dụng tư duy Quy hoạch động đã phân tích ở Phần 2. Thuật toán Kadane triển khai công thức truy hồi DP một cách hiệu quả chỉ trong một lần duyệt.
Logic cốt lõi và Các bước
Chúng ta duy trì current_max (tổng tối ưu kết thúc tại đây) và global_max (tổng tối ưu tốt nhất từng thấy).
- Khởi tạo: Khởi tạo
current_maxvàglobal_maxbằng phần tử đầu tiên của mảng (nums[0]). Điều này rất quan trọng để xử lý chính xác các trường hợp mảng toàn số âm (nếu khởi tạo bằng 0, kết quả sẽ sai). - Lặp: Duyệt qua mảng bắt đầu từ phần tử thứ hai (chỉ số 1).
- Áp dụng DP Transition: Tại mỗi phần tử num, cập nhật current_max.current_max = max(num, num + current_max)
Ý nghĩa: Quyết định xem nên bắt đầu một mảng con mới tại num hay mở rộng mảng con trước đó. Nếu tổng trước đó là âm, chúng ta loại bỏ nó và bắt đầu lại.
- Cập nhật Global Maximum:global_max = max(global_max, current_max)
Thuật toán này đảm bảo độ phức tạp thời gian O(N) và không gian O(1).
4. Minh họa Thuật toán Tối ưu (Algorithm Walkthrough)

Hãy cùng đi qua từng bước của Thuật toán Kadane với ví dụ cụ thể.
Input: nums = [-2, 1, -3, 4, -1, 2, 1, -5, 4]
Khởi tạo:
current_max = nums[0] = -2global_max = nums[0] = -2
Quá trình lặp:
| Index (i) | nums[i] | Phép tính current_max max(nums[i], nums[i] + current_max) | current_max mới | global_max mới | Ghi chú |
| 0 | -2 | (Khởi tạo) | -2 | -2 | |
| 1 | 1 | max(1, 1 + (-2)) = max(1, -1) |
1 | 1 | Bắt đầu mảng con mới tại 1 tốt hơn là mở rộng từ -2. |
| 2 | -3 | max(-3, -3 + 1) = max(-3, -2) |
-2 | 1 | Mở rộng mảng con [1] với -3 tạo ra tổng -2. |
| 3 | 4 | max(4, 4 + (-2)) = max(4, 2) |
4 | 4 | Bắt đầu mảng con mới tại 4 là tối ưu nhất tại đây. Cập nhật global_max. |
| 4 | -1 | max(-1, -1 + 4) = max(-1, 3) |
3 | 4 | Mở rộng mảng con [4] với -1. |
| 5 | 2 | max(2, 2 + 3) = max(2, 5) |
5 | 5 | Mở rộng mảng con [4, -1] với 2. Cập nhật global_max. |
| 6 | 1 | max(1, 1 + 5) = max(1, 6) |
6 | 6 | Mở rộng mảng con. Đây là đỉnh điểm. |
| 7 | -5 | max(-5, -5 + 6) = max(-5, 1) |
1 | 6 | Tổng giảm xuống nhưng vẫn dương. |
| 8 | 4 | max(4, 4 + 1) = max(4, 5) |
5 | 6 | Tổng tăng lên 5, nhưng không vượt qua global_max. |
Kết quả: Sau khi vòng lặp kết thúc, thuật toán trả về global_max = 6.
5. Phân tích So sánh và Các Hướng Tiếp cận Khác
Mặc dù Thuật toán Kadane là tối ưu, việc hiểu các phương pháp thay thế giúp củng cố kiến thức nền tảng.
Hướng tiếp cận Chia để trị (Divide and Conquer – D&C)
Một phương pháp kinh điển khác là sử dụng kỹ thuật Chia để trị.
Ý tưởng:
Chia mảng thành hai nửa. Mảng con có tổng lớn nhất có thể nằm ở một trong ba vị trí:
- Hoàn toàn nằm trong nửa bên trái (Giải quyết bằng đệ quy).
- Hoàn toàn nằm trong nửa bên phải (Giải quyết bằng đệ quy).
- Đi qua điểm giữa (Crosses the midpoint) (Giải quyết trong O(N)).
Phân tích Độ phức tạp:
Công thức truy hồi cho độ phức tạp thời gian là: T(N) = 2 * T(N/2) + O(N). Theo Định lý thợ (Master Theorem), độ phức tạp này là O(N log N).
So sánh và Đánh đổi (Trade-offs)
| Thuật toán | Độ phức tạp Thời gian (Time) | Độ phức tạp Không gian (Space) | Độ phức tạp Triển khai |
| Brute Force (O(N^2)) | O(N^2) | O(1) | Dễ |
| Divide and Conquer | O(N log N) | O(log N) (Do stack đệ quy) | Phức tạp |
| DP tiêu chuẩn (Dùng mảng) | O(N) | O(N) | Dễ |
| Thuật toán Kadane | O(N) | O(1) | Dễ |
Trong trường hợp này, không có sự đánh đổi thực sự. Thuật toán Kadane vượt trội hơn hẳn về mọi mặt: nhanh hơn, sử dụng ít bộ nhớ hơn, và dễ triển khai hơn. Nó đạt được hiệu suất tối ưu cả về thời gian và không gian đồng thời.
Tìm vị trí Bắt đầu và Kết thúc
Thuật toán Kadane tiêu chuẩn chỉ trả về tổng. Chúng ta có thể sửa đổi nó để theo dõi chỉ số bắt đầu và kết thúc của mảng con tối ưu bằng cách thêm các biến để lưu trữ chỉ số hiện tại và cập nhật chúng khi global_max thay đổi. Việc sửa đổi này vẫn giữ nguyên độ phức tạp O(N) và O(1).
6. Triển khai Code Python Hoàn chỉnh và Kiểm thử

Dưới đây là triển khai hoàn chỉnh của Thuật toán Kadane trong Python, tuân thủ các tiêu chuẩn về Type Hinting, PEP 8 và Docstrings.
from typing import List
def max_subarray_sum_kadane(nums: List[int]) -> int:
"""
Tìm tổng lớn nhất của một mảng con liền kề sử dụng Thuật toán Kadane.
Thuật toán này là một phương pháp Quy hoạch động (Dynamic Programming) hiệu quả,
hoạt động với độ phức tạp thời gian O(N) và không gian O(1). Nó dựa trên việc
duy trì tổng tối đa kết thúc tại vị trí hiện tại.
Args:
nums: Một danh sách các số nguyên (dương, âm hoặc 0).
Returns:
Tổng lớn nhất của một mảng con liền kề trong nums.
Raises:
ValueError: Nếu danh sách đầu vào rỗng (dựa trên ràng buộc thông thường là
mảng phải có ít nhất 1 phần tử).
"""
if not nums:
# Xử lý trường hợp mảng rỗng tùy thuộc vào yêu cầu bài toán.
raise ValueError("Input list cannot be empty.")
# Khởi tạo current_max và global_max bằng phần tử đầu tiên.
# Điều này rất quan trọng để xử lý chính xác các trường hợp mảng toàn số âm
# và trường hợp mảng chỉ có một phần tử.
current_max = global_max = nums[0]
# Duyệt qua mảng bắt đầu từ phần tử thứ hai.
for num in nums[1:]:
# Công thức DP của Kadane:
# Quyết định xem nên mở rộng mảng con hiện tại (current_max + num)
# hay bắt đầu một mảng con mới tại phần tử hiện tại (num).
current_max = max(num, current_max + num)
# Cập nhật tổng tối đa toàn cục nếu tổng tối đa cục bộ hiện tại lớn hơn.
if current_max > global_max:
global_max = current_max
return global_max
# Khối kiểm thử
if __name__ == "__main__":
def run_test(test_name: str, nums: List[int], expected: int):
try:
result = max_subarray_sum_kadane(nums)
status = "PASSED" if result == expected else f"FAILED (Expected {expected}, Got {result})"
print(f"Test '{test_name}': {status}")
except Exception as e:
print(f"Test '{test_name}': ERROR ({e})")
print("Bắt đầu kiểm thử Thuật toán Kadane...\n")
# Test Case 1: Ví dụ tiêu chuẩn (Pha trộn số âm và dương)
run_test(
"Standard Mixed Array",
[-2, 1, -3, 4, -1, 2, 1, -5, 4],
6 # [4, -1, 2, 1]
)
# Test Case 2: Edge Case - Tất cả đều dương
run_test(
"All Positive Numbers",
[1, 2, 3, 4, 5],
15 # Toàn bộ mảng
)
# Test Case 3: Edge Case - Tất cả đều âm
run_test(
"All Negative Numbers",
[-5, -1, -3, -2],
-1 # Phần tử đơn lẻ lớn nhất
)
# Test Case 4: Edge Case - Mảng chỉ có một phần tử
run_test(
"Single Element (Positive)",
[10],
10
)
run_test(
"Single Element (Negative)",
[-10],
-10
)
# Test Case 5: Mảng có số 0
run_test(
"Array with Zeroes",
[2, -3, 4, 0, -1, 5],
8 # [4, 0, -1, 5]
)
# Test Case 6: Tổng giảm sâu rồi tăng mạnh
run_test(
"Deep Dip then Surge",
[8, -19, 5, -4, 20],
21 # [5, -4, 20]
)
# Test Case 7: Xử lý mảng rỗng
try:
max_subarray_sum_kadane([])
except ValueError as e:
print(f"Test 'Empty Array': PASSED (Caught expected exception: {e})")
print("\nKiểm thử hoàn tất.")
Các khóa học liên quan: