Array Technique

👉👈 Two Pointers – Kỹ thuật hai con trỏ

Dùng hai chỉ số di chuyển có quy luật để loại bỏ nhiều trạng thái không cần xét, thường giảm O(n²) xuống O(n).

Pair SumHai đầu mảng
Sliding WindowHai con trỏ cùng chiều
Read–WriteLoại trùng tại chỗ
Điều kiệnCần tính đơn điệu

🎯 Mục tiêu bài học

Cần hiểu

  • Vì sao hai con trỏ có thể thay vòng lặp lồng nhau.
  • Ba mẫu: đối đầu, cùng chiều, đọc–ghi.
  • Tính đơn điệu quyết định di chuyển con trỏ.
  • Vì sao một vòng while bên trong vẫn có thể là O(n).

Dấu hiệu nhận biết

  • Mảng đã sắp xếp.
  • Tìm cặp/bộ ba thỏa tổng.
  • Đoạn con liên tiếp với điều kiện đơn điệu.
  • Loại phần tử trùng tại chỗ.

1. Đặt vấn đề

Bài toán mở đầu.
Cho mảng đã sắp xếp tăng dần và số X. Hãy tìm hai phần tử ở hai vị trí khác nhau có tổng bằng X.

1.1. Cách trực tiếp

Duyệt mọi cặp (i,j), mất O(n²).

1.2. Quan sát

  • Nếu a[L]+a[R]<X, phải tăng L.
  • Nếu a[L]+a[R]>X, phải giảm R.
  • Nếu bằng X, đã tìm thấy đáp án.
Mỗi bước loại bỏ được cả một nhóm cặp chắc chắn không thể là đáp án
Cách làmĐộ phức tạpÝ tưởng
Hai vòng lặpO(n²)Xét mọi cặp.
Two PointersO(n)L chỉ tăng, R chỉ giảm.
Bản chất: phải chứng minh được hướng di chuyển của con trỏ không bỏ sót đáp án.

2. Ba dạng Two Pointers

Đối đầu

L ở đầu, R ở cuối. Dùng cho Pair Sum, palindrome, container with most water.

Cùng chiều

L và R cùng đi sang phải. Dùng cho Sliding Window và đoạn thỏa điều kiện.

Đọc–ghi

read duyệt dữ liệu, write ghi phần hợp lệ. Dùng cho loại trùng, lọc phần tử.

Không phải bài có hai chỉ số nào cũng là Two Pointers O(n). Tổng số lần di chuyển của các con trỏ phải được chặn bởi O(n).

3. Mô phỏng hai con trỏ đối đầu

4. Hai con trỏ cùng chiều – đoạn dài nhất có tổng ≤ K

Điều kiện: các phần tử phải không âm. Có số âm thì tổng không còn biến đổi đơn điệu.

5. Hai con trỏ đọc–ghi – loại phần tử trùng

Với mảng đã sắp xếp, read đọc từng phần tử, write chỉ tăng khi gặp giá trị mới.

6. Phân tích thuật toán

DạngĐiều kiệnĐộ phức tạpLý do
Pair SumMảng tăngO(n)L tăng, R giảm.
Longest sum ≤ KSố không âmO(n)Mỗi con trỏ tăng tối đa n lần.
Remove duplicatesMảng tăngO(n)Một lượt đọc–ghi.
3SumSắp xếpO(n²)Cố định một phần tử, Two Pointers cho hai phần tử còn lại.

7. Code mẫu

#include <bits/stdc++.h>
using namespace std;

int main() {
    int n;
    long long target;
    cin >> n >> target;

    vector<long long> a(n);
    for (long long &x : a) cin >> x;

    sort(a.begin(), a.end());

    int left = 0;
    int right = n - 1;

    while (left < right) {
        long long sum = a[left] + a[right];

        if (sum == target) {
            cout << a[left] << ' ' << a[right];
            return 0;
        }

        if (sum < target) left++;
        else right--;
    }

    cout << "NO";
}
int left = 0;
int best = 0;
long long sum = 0;

for (int right = 0; right < n; right++) {
    sum += a[right];

    while (left <= right && sum > k) {
        sum -= a[left];
        left++;
    }

    best = max(best, right - left + 1);
}
int removeDuplicates(vector<int>& a) {
    if (a.empty()) return 0;

    int write = 1;

    for (int read = 1; read < (int)a.size(); read++) {
        if (a[read] != a[write - 1]) {
            a[write] = a[read];
            write++;
        }
    }

    return write;
}
sort(a.begin(), a.end());

for (int i = 0; i < n; i++) {
    if (i > 0 && a[i] == a[i - 1]) continue;

    int left = i + 1;
    int right = n - 1;

    while (left < right) {
        long long sum = 1LL * a[i] + a[left] + a[right];

        if (sum == target) {
            // xử lý bộ ba
            left++;
            right--;
        } else if (sum < target) {
            left++;
        } else {
            right--;
        }
    }
}

8. Giải thích code

8.1. Vì sao Pair Sum cần mảng đã sắp xếp?

Nhờ thứ tự tăng, tăng L chắc chắn không làm giá trị bên trái giảm; giảm R chắc chắn không làm giá trị bên phải tăng.

8.2. Vì sao điều kiện là left < right?

Hai vị trí phải khác nhau. Nếu bằng nhau, ta đang dùng cùng một phần tử hai lần.

8.3. Vì sao vòng while trong for vẫn O(n)?

Con trỏ left chỉ tăng từ 0 đến n−1 trong toàn bộ chương trình. Tổng số lần chạy của while là O(n), không phải O(n²).

8.4. Vì sao Sliding Window cần số không âm?

Nếu có số âm, khi mở rộng R tổng có thể giảm, nên không thể quyết định co L chỉ dựa trên sum > K.

8.5. Vì sao remove duplicates ghi vào a[write]?

Đoạn a[0..write-1] luôn chứa các giá trị duy nhất đã tìm thấy.

9. Lỗi thường gặp

  • Dùng Pair Sum Two Pointers trên mảng chưa sắp xếp.
  • Quên long long cho tổng.
  • Dùng left <= right và dùng một phần tử hai lần.
  • Di chuyển sai con trỏ khi tổng nhỏ/lớn hơn target.
  • Dùng Sliding Window sum ≤ K với số âm.
  • Không bỏ qua phần tử trùng trong 3Sum.
  • Không chứng minh tính đơn điệu.

10. Quiz và bài tập

Câu 1. Tổng nhỏ hơn target thì làm gì?
Câu 2. Sliding Window sum ≤ K cần điều kiện nào?
Câu 3. Vì sao thường là O(n)?

📘 Cơ bản

  1. Pair Sum.
  2. Palindrome.
  3. Loại trùng.

📗 Trung bình

  1. Đếm cặp tổng ≤ K.
  2. Longest sum ≤ K.
  3. Container With Most Water.

📙 Nâng cao

  1. 3Sum, 4Sum.
  2. Merge hai mảng tăng.
  3. Two Pointers + Prefix Sum.

🐉 HSG

  1. Đếm đoạn thỏa tính đơn điệu.
  2. Offline queries.
  3. Tối ưu DP theo cửa sổ.