0303. Range Sum Query - Immutable

  • Nhãn: Thiết kế, Mảng, Tổng tiền tố
  • Độ khó: Dễ

Tóm tắt đề bài

Mô tả: Cho một mảng số nguyên nums.

Yêu cầu: Triển khai lớp NumArray có thể xử lý nhiều truy vấn tính tổng trong khoảng [left, right].

Lớp NumArray:

  • NumArray(int[] nums): Khởi tạo đối tượng với mảng nums.
  • int sumRange(int i, int j): Trả về tổng các phần tử trong mảng nums từ chỉ số left đến right bao gồm cả leftright (nghĩa là nums[left] + nums[left + 1] + … + nums[right]).

Giải thích:

  • .
  • .
  • .
  • Số lần gọi phương thức sumRange không vượt quá .

Ví dụ:

  • Ví dụ 1:
Cho    nums = [-2, 0, 3, -5, 2, -1]
 
Tổng   sumRange(0, 2) -> 1
Tổng   sumRange(2, 5) -> -1
Tổng   sumRange(0, 5) -> -3

Ý tưởng giải quyết

Ý tưởng 1: cây phân đoạn (Segment Tree)

  • Dựa trên mảng nums, xây dựng một cây phân đoạn. Mỗi nút của cây phân đoạn lưu trữ ranh giới trái và phải của khoảng hiện tại và tổng của khoảng đó.

Thời gian xây dựng cây phân đoạn là , thời gian truy vấn một khoảng là . Tổng thời gian là .

Ý tưởng này chỉ nhằm mục đích làm quen với cây phân đoạn (segment tree). Cách đơn giản hơn ở ý tưởng sau.

Ý tưởng 1: Code

# Lớp nút cây phân đoạn
class SegTreeNode:
    def __init__(self, val=0):
        self.left = -1                              # Ranh giới trái của khoảng
        self.right = -1                             # Ranh giới phải của khoảng
        self.val = val                              # Giá trị của nút (tổng của khoảng)
        
        
# Lớp cây phân đoạn
class SegmentTree:
    # Phương thức khởi tạo cây phân đoạn
    def __init__(self, nums, function):
        self.size = len(nums)
        self.tree = [SegTreeNode() for _ in range(4 * self.size)]  # Mảng lưu trữ SegTreeNode
        self.nums = nums                            # Dữ liệu gốc
        self.function = function                    # function là một hàm, phương thức tổng hợp của khoảng trái và phải
        if self.size > 0:
            self.__build(0, 0, self.size - 1)
        
    # Phương thức cập nhật điểm: thay đổi nums[i] thành val
    def update_point(self, i, val):
        self.nums[i] = val
        self.__update_point(i, val, 0)
    
    # Phương thức truy vấn khoảng: truy vấn giá trị của khoảng [q_left, q_right]
    def query_interval(self, q_left, q_right):
        return self.__query_interval(q_left, q_right, 0)
    
    # Phương thức lấy mảng nums: trả về mảng nums
    def get_nums(self):
        for i in range(self.size):
            self.nums[i] = self.query_interval(i, i)
        return self.nums
    
    
    # Các phương thức triển khai bên trong
    
    # Triển khai phương thức xây dựng cây phân đoạn: chỉ số lưu trữ của nút là index, khoảng của nút là [left, right]
    def __build(self, index, left, right):
        self.tree[index].left = left
        self.tree[index].right = right
        if left == right:                           # Nút lá, giá trị của nút là giá trị phần tử tại vị trí tương ứng
            self.tree[index].val = self.nums[left]
            return
    
        mid = left + (right - left) // 2            # Điểm chia nút trái và phải
        left_index = index * 2 + 1                  # Chỉ số lưu trữ của nút con trái
        right_index = index * 2 + 2                 # Chỉ số lưu trữ của nút con phải
        self.__build(left_index, left, mid)         # Đệ quy tạo cây con trái
        self.__build(right_index, mid + 1, right)   # Đệ quy tạo cây con phải
        self.__pushup(index)                        # Cập nhật giá trị khoảng của nút lên trên
    
    
    # Triển khai phương thức cập nhật điểm: thay đổi nums[i] thành val. Chỉ số lưu trữ của nút là index, khoảng của nút là [left, right]
    def __update_point(self, i, val, index):
        left = self.tree[index].left
        right = self.tree[index].right
        
        if left == right:
            self.tree[index].val = val              # Nút lá, cập nhật giá trị của nút thành val
            return
        
        mid = left + (right - left) // 2            # Điểm chia nút trái và phải
        left_index = index * 2 + 1                  # Chỉ số lưu trữ của nút con trái
        right_index = index * 2 + 2                 # Chỉ số lưu trữ của nút con phải
        if i <= mid:                                # Cập nhật giá trị của nút trong cây con trái
            self.__update_point(i, val, left_index)
        else:                                       # Cập nhật giá trị của nút trong cây con phải
            self.__update_point(i, val, right_index)
        
        self.__pushup(index)                        # Cập nhật giá trị khoảng của nút lên trên
        
    
    # Triển khai phương thức truy vấn khoảng: tìm kiếm giá trị của khoảng [q_left, q_right] trong khoảng [left, right] của cây
    def __query_interval(self, q_left, q_right, index):
        left = self.tree[index].left
        right = self.tree[index].right
        
        if left >= q_left and right <= q_right:     # Khoảng của nút nằm trong khoảng [q_left, q_right]
            return self.tree[index].val             # Trả về giá trị của nút
        if right < q_left or left > q_right:        # Khoảng của nút không liên quan đến [q_left, q_right]
            return 0
    
        mid = left + (right - left) // 2            # Điểm chia nút trái và phải
        left_index = index * 2 + 1                  # Chỉ số lưu trữ của nút con trái
        right_index = index * 2 + 2                 # Chỉ số lưu trữ của nút con phải
        res_left = 0                                # Kết quả truy vấn cây con trái
        res_right = 0                               # Kết quả truy vấn cây con phải
        if q_left <= mid:                           # Truy vấn trong cây con trái
            res_left = self.__query_interval(q_left, q_right, left_index)
        if q_right > mid:                           # Truy vấn trong cây con phải
            res_right = self.__query_interval(q_left, q_right, right_index)
        
        return self.function(res_left, res_right)   # Trả về kết quả tính toán tổng của giá trị các nút con trái và phải
    
    # Triển khai phương thức cập nhật lên trên: giá trị khoảng của nút có chỉ số index bằng tổng giá trị của nút con trái và phải
    def __pushup(self, index):
        left_index = index * 2 + 1                  # Chỉ số lưu trữ của nút con trái
        right_index = index * 2 + 2                 # Chỉ số lưu trữ của nút con phải
        self.tree[index].val = self.function(self.tree[left_index].val, self.tree[right_index].val)
 
 
class NumArray:
 
    def __init__(self, nums: List[int]):
        self.STree = SegmentTree(nums, lambda x, y: x + y)
 
 
    def sumRange(self, left: int, right: int) -> int:
        return self.STree.query_interval(left, right)

Ý tưởng 2: Tổng tiền tố (Prefix Sum)

Tổng tiền tố (prefix sum) là một phương pháp để tính tổng của một đoạn trong . Cách thực hiện của nó rất dễ hiểu:

Xét với dãy số A:
Xây dựng dãy S: với

Với dãy S, tổng của đoạn sẽ là

Từ đó ta áp dụng vào bài toán này. Lưu ý rằng mảng trong Python bắt đầu bằng chỉ số 0 nên ta sẽ thêm 1 số 0 ở đầu cho dễ xử lý.

Thuật toán này chỉ cần duyệt mảng một lần để xây dựng mảng tổng tiền tố prefixSum với chi phí , và mất để tính tổng của một đoạn. Chung quy lại thì độ phức tạp thời gian là .

Ý tưởng 2: Code

class NumArray:
 
    def __init__(self, nums: List[int]):
	    # Thêm 0 ở đầu để xử lý tính toán thuận tiện hơn
        self.prefixSum = [0] * (len(nums) + 1)
        # Vị trí đã dịch qua phải
        for i in range(len(nums)):
            self.prefixSum[i + 1] = self.prefixSum[i] + nums[i]
 
 
    def sumRange(self, left: int, right: int) -> int:
	    # Vị trí đã dịch qua phải
        return self.prefixSum[right + 1] - self.prefixSum[left]

Trong Python có hàm accumulate để xử lý việc tính tổng tiền tố ta có thể rút gọn code lại thêm:

class NumArray:
 
    def __init__(self, nums: List[int]):
	    self.prefixSum = [0] + list(accumulate(nums))nums[i]
 
 
    def sumRange(self, left: int, right: int) -> int:
        return self.prefixSum[right + 1] - self.prefixSum[left]