0220. Tồn tại cặp phần tử trùng nhau III

  • Thẻ: Array, Bucket Sort, Ordered Set, Sorting, Sliding Window
  • Độ khó: Khó

Tóm tắt đề bài

Mô tả: Cho một mảng số nguyên và hai số nguyên .

Yêu cầu: Kiểm tra xem trong mảng có tồn tại hai chỉ số khác nhau sao cho và đồng thời . Nếu thỏa mãn điều kiện thì trả về True, ngược lại trả về False.

Giải thích:

  • .
  • .
  • .
  • .

Ví dụ:

  • Ví dụ 1:
Input: nums = [1,2,3,1], k = 3, t = 0
Output: True
  • Ví dụ 2:
Input: nums = [1,0,1,1], k = 1, t = 2
Output: True

Ý tưởng giải quyết

Đề bài yêu cầu thỏa mãn hai yêu cầu, một là yêu cầu về giá trị phần tử (), và hai là yêu cầu về phạm vi chỉ số ().

Đối với mỗi vị trí , vị trí phù hợp phải nằm trong khoảng , đồng thời giá trị phải nằm trong khoảng .

Cách đơn giản nhất là sử dụng hai vòng lặp để duyệt qua mảng, vòng lặp bên ngoài duyệt qua vị trí , vòng lặp bên trong duyệt qua các phần tử trong khoảng và kiểm tra xem có thỏa mãn hay không. Tuy nhiên, cách làm này có độ phức tạp thời gian là , trong đó là độ dài của mảng .

Chúng ta cần tối ưu phần kiểm tra xem các phần tử liền kề có thỏa mãn hay không. Có hai cách để làm điều này: “sắp xếp theo thùng” và “cửa sổ trượt (độ dài cố định)“.

Ý tưởng 1: Sắp xếp theo thùng (bucket sort)

  1. Sử dụng ý tưởng sắp xếp theo thùng, đặt kích thước của mỗi thùng là . Chỉ cần sử dụng một vòng lặp để duyệt qua vị trí , sau đó dựa vào để quyết định đưa vào thùng nào.
  2. Như vậy, các phần tử trong cùng một thùng sẽ có độ chênh lệch tuyệt đối nhỏ hơn hoặc bằng . Còn các phần tử liền kề giữa các thùng, chỉ cần kiểm tra xem chênh lệch giữa hai thùng có vượt quá hay không. Như vậy, ta có thể kiểm tra xem các phần tử liền kề có thỏa mãn trong thời gian .
  3. Đồng thời, ta cũng cần kiểm tra xem bằng cách xóa các thùng chứa các phần tử vượt quá giới hạn. Điều này đảm bảo các phần tử trong thùng luôn thỏa mãn .

Các bước cụ thể như sau:

  1. Đặt kích thước của mỗi thùng là . Ta sử dụng một từ điển để lưu trữ các thùng.
  2. Duyệt qua các phần tử trong mảng , với mỗi phần tử :
    1. Nếu trước khi đưa vào thùng, thì thùng đã có phần tử, thì hai phần tử này chắc chắn thỏa mãn ,
    2. Nếu thùng trước đó không có phần tử, thì đưa vào thùng tương ứng.
    3. Kiểm tra xem các thùng bên trái và bên phải có phần tử thỏa mãn hay không.
    4. Sau đó xóa các thùng cũ trước , vì các thùng cũ này không còn thỏa mãn nữa.
  3. Nếu có trường hợp thỏa mãn yêu cầu thì trả về True, nếu không thỏa mãn yêu cầu sau khi duyệt qua tất cả các phần tử thì trả về False.

Ý tưởng 1: Code

class Solution:
    def containsNearbyAlmostDuplicate(self, nums: List[int], k: int, t: int) -> bool:
        bucket_dict = dict()
        for i in range(len(nums)):
            # Đặt nums[i] vào các thùng có kích thước là t + 1
            num = nums[i] // (t + 1)
 
            # Nếu thùng đã có phần tử trước đó
            if num in bucket_dict:
                return True
 
            # Đưa nums[i] vào thùng
            bucket_dict[num] = nums[i]
 
            # Kiểm tra thùng bên trái có thỏa mãn yêu cầu hay không
            if (num - 1) in bucket_dict and abs(bucket_dict[num - 1] - nums[i]) <= t:
                return True
            # Kiểm tra thùng bên phải có thỏa mãn yêu cầu hay không
            if (num + 1) in bucket_dict and abs(bucket_dict[num + 1] - nums[i]) <= t:
                return True
            # Xóa các thùng cũ trước i - k, vì các thùng này không còn thỏa mãn yêu cầu nữa
            if i >= k:
                bucket_dict.pop(nums[i - k] // (t + 1))
 
        return False

Ý tưởng 1: Phân tích độ phức tạp

  • Độ phức tạp thời gian: . là độ dài của mảng đầu vào.
  • Độ phức tạp không gian: . Số lượng phần tử tối đa trong các thùng là .

Ý tưởng 2: Cửa sổ trượt (độ dài cố định)

  1. Sử dụng một cửa sổ trượt có độ dài cố định là . Mỗi khi duyệt đến , cửa sổ trượt sẽ chứa tối đa phần tử trước . Chỉ cần kiểm tra xem phần tử đầu tiên trong cửa sổ trượt có nằm trong khoảng hay không.
  2. Kiểm tra xem phần tử trong cửa sổ trượt có nằm trong khoảng có thể được thực hiện bằng cách sắp xếp cửa sổ trượt và kiểm tra các phần tử liền kề. Điều này giúp giảm độ phức tạp thời gian.

Các bước cụ thể như sau:

  1. Sử dụng lớp SortedList tự để duy trì một cửa sổ trượt có độ dài và đảm bảo các phần tử trong cửa sổ trượt được sắp xếp.
  2. đều trỏ đến phần tử đầu tiên của dãy con. Tức là: left = 0, right = 0.
  3. Đưa phần tử hiện tại vào cửa sổ trượt, tức là window.add(nums[right]).
  4. Khi số phần tử trong cửa sổ trượt lớn hơn , tức là khi , loại bỏ phần tử bên trái nhất của cửa sổ trượt và di chuyển sang phải.
  5. Khi số phần tử trong cửa sổ trượt nhỏ hơn hoặc bằng :
    1. Sử dụng thuật toán tìm kiếm nhị phân để tìm vị trí của trong .
    2. Kiểm tra giá trị tuyệt đối của chênh lệch giữa và phần tử liền kề bên trái, nếu nhỏ hơn hoặc bằng thì trả về True.
    3. Kiểm tra giá trị tuyệt đối của chênh lệch giữa , nếu nhỏ hơn hoặc bằng thì trả về True.
  6. Di chuyển sang phải.
  7. Lặp lại các bước 3-6 cho đến khi đến cuối mảng. Nếu không tìm thấy trường hợp thỏa mãn yêu cầu sau khi duyệt qua tất cả các phần tử, trả về False.

Ý tưởng 2: Code

from sortedcontainers import SortedList
 
class Solution:
    def containsNearbyAlmostDuplicate(self, nums: List[int], k: int, t: int) -> bool:
        size = len(nums)
        window = SortedList()
        left, right = 0, 0
        while right < size:
            window.add(nums[right])
            
            if right - left > k:
                window.remove(nums[left])
                left += 1
            
            idx = bisect.bisect_left(window, nums[right])
            
            if idx > 0 and abs(window[idx] - window[idx - 1]) <= t:
                return True
            if idx < len(window) - 1 and abs(window[idx + 1] - window[idx]) <= t:
                return True
 
            right += 1
 
        return False

Ý tưởng 2: Phân tích độ phức tạp

  • Độ phức tạp thời gian: .
  • Độ phức tạp không gian: .