Tác giả: Phan Đình Khôi - Đại học Bách Khoa Đà Nẵng
Reviewer: Nguyễn Khánh
Bài viết này sẽ giúp bạn tìm hiểu thêm về kỹ thuật hai con trỏ. Kỹ thuật này được sử dụng khá phổ biến, giúp chương trình tiết kiệm thời gian và không gian xử lý.
Cho hai dãy số nguyên đã được sắp xếp không giảm và lần lượt có và phần tử. Hãy ghép chúng thành dãy được bố trí theo thứ tự không giảm.
Giới hạn: và .
Hãy cùng xem ví dụ sau đây.
Cho trước hai dãy số và được sắp xếp không giảm:
Làm cách nào để có thể ghép chúng thành một dãy số cũng được sắp xếp không giảm ?
Trước tiên, hãy cùng xác định phần tử đầu tiên của dãy .
Vì dãy được bố trí theo thứ tự không giảm, cho nên phần tử đầu tiên của dãy phải là phần tử có giá trị nhỏ nhất trong cả hai dãy và .
Ta có thể so sánh hai phần tử nhỏ nhất của hai dãy , và đưa phần tử có giá trị nhỏ hơn vào vị trí đầu tiên của dãy .
Dãy và đã được sắp xếp không giảm, vì thế hai phần tử nhỏ nhất ở đây chính là hai phần tử ở vị trí đầu tiên ở mỗi dãy ( và ).
Bây giờ, phần tử tiếp theo của dãy sẽ là phần tử nhỏ nhất trong các phần tử chưa được đưa vào dãy .
Dãy và đã được sắp xếp không giảm, vì thế sau khi đưa vào dãy , là phần tử nhỏ nhất chưa được chọn ở dãy và là phần tử nhỏ nhất chưa được chọn ở dãy .
So sánh và , chọn phần tử có giá trị nhỏ hơn và đưa vào dãy .
Sau khi đưa vào dãy , trở thành phần tử nhỏ nhất chưa được chọn ở dãy .
Vẫn như thế, phần tử tiếp theo của dãy sẽ là phần tử nhỏ nhất trong các phần tử chưa được đưa vào dãy .
So sánh và , chọn phần tử có giá trị nhỏ hơn dãy và đưa vào dãy .
Sau khi đưa vào dãy , trở thành phần tử nhỏ nhất chưa được chọn ở dãy .
Ta nhận thấy rằng
Dựa vào những phân tích ta có giải pháp sử dụng hai con trỏ như sau:
Để hiểu rõ hơn, ta hãy cùng xem qua ví dụ sau đây:
Đặt và .
Vì nên ta đưa vào mảng và tăng vị trí lên một.
Vì nên ta đưa vào mảng và tăng vị trí lên một.
Vì nên ta đưa vào mảng và tăng vị trí lên một.
Vì nên ta có thể đưa bất kỳ một trong hai phần tử. Ở đây ta đưa phần tử vào và tăng vị trí lên một.
Vì nên ta đưa vào mảng và tăng vị trí lên một.
Vì nên ta đưa vào mảng và tăng vị trí lên một.
Vì nên ta đưa vào mảng và tăng vị trí lên một.
Vì nên ta đưa vào mảng và tăng vị trí lên một.
Vì tất cả các phần tử trong dãy đều đã được đưa vào dãy nên từ đưa lần lượt các phần tử chưa được chọn trong dãy vào trong dãy
Cài đặt
int i = 1, j = 1;
vector<int> c;
while (i <= n || j <= m){
if (j == m + 1 || (i <= n && a[i] <= b[j]))
c.push_back(a[i++]);
else
c.push_back(b[j++]);
}
for (auto it: c)
cout << it << " ";
Độ phức tạp
Vị trí con trỏ luôn tăng và tăng quá không quá lần, vị trí con trỏ cũng luôn tăng và tăng không quá lần.
Vì thế độ phức tạp của giải pháp là .
Cho một mảng số nguyên có phần tử, mảng này đã được sắp xếp tăng dần. Hãy tìm hai vị trí khác nhau bất kỳ sao cho tổng của hai phần tử ở hai vị trí đó có giá trị là .
Giới hạn: và
Hãy cùng xem ví dụ sau đây.
Cho trước mảng số được sắp xếp tăng dần và :
Làm cách nào để có thể tìm hai vị trí khác nhau mà tổng hai phần tử ở hai vị trí đó có tổng là ?
Trước tiên, ta có một chút nhận xét sau:
Có thể thấy, tổng của với các phần tử khác trong dãy đều lớn hơn . Vì thế ta không quan tâm đến nữa.
Có thể thấy, tổng của với các phần tử khác trong các phần tử ta quan tâm đều nhỏ hơn . Vì thế ta không quan tâm đến nữa.
Có thể thấy, tổng của với các phần tử khác trong các phần tử ta quan tâm đều lớn hơn . Vì thế ta không quan tâm đến nữa.
Như vậy, tại một thời điểm bất kỳ, những phần tử chúng ta cần quan tâm đến sẽ là các phần tử trong đoạn nào đó.
Ta có một số nhận xét sau:
Từ những phân tích vừa rồi ta có giải pháp sử dụng hai con trỏ như sau:
Để hiểu rõ hơn, ta hãy cùng xem qua một số ví dụ sau đây:
Ví dụ 1: và .
Đặt và .
Vì nên giảm vị trí đi một đơn vị.
Vì nên tăng vị trí lên một đơn vị.
Vì nên giảm vị trí đi một đơn vị.
Vì nên tăng vị trí lên một đơn vị.
Vì nên hai vị trí cần tìm là hai vị trí và .
Ví dụ 2: và .
Đặt và .
Vì nên giảm vị trí đi một đơn vị.
Vì nên tăng vị trí lên một đơn vị.
Vì tăng vị trí lên một đơn vị.
Vì giảm vị trí đi một đơn vị.
Vì giảm vị trí đi một đơn vị.
Vì tăng vị trí lên một đơn vị.
Vì nên không tìm được hai vị trí cần tìm.
Cài đặt
int i = 1, j = N;
while (i < j) {
if (a[i] + a[j] == x) {
cout << i << " " << j;
return 0;
}
if (a[i] + a[j] < x)
i += 1;
else
j -= 1;
}
cout << "No solution";
Độ phức tạp
Vị trí con trỏ luôn tăng, vị trí con trỏ thì luôn giảm.
Hơn nữa, sự thay đổi vị trí hai con trỏ này sẽ dừng lại khi tổng hai phần tử ở hai vị trí con trỏ có tổng là hay khi vị trí bằng vị trí .
Vì thế, việc thay đổi vị trí hai con trỏ sẽ không quá lần, độ phức tạp của giải pháp là .
Cho dãy số nguyên dương có phần tử. Hãy tìm độ dài đoạn con dài nhất trong dãy sao cho tổng các phần tử trong đoạn này không quá .
Dữ liệu đảm bảo các phần tử trong dãy đều có giá trị không quá .
Giới hạn: , và .
Để dễ dàng phân tích, ta tạm gọi
Qua đây, bài toán của chúng ta sẽ là tìm độ dài đoạn con "tốt" dài nhất.
Vì dãy là một dãy số nguyên dương cho nên
Với là một vị trí bất kỳ, nếu như là vị trí nhỏ nhất sao cho đoạn là một đoạn "tốt" thì
Từ đó, với mỗi từ đến , nếu ta xác định được vị trí , ta có thể biết được độ dài của đoạn con "tốt" dài nhất của dãy .
Hãy cùng nhận xét vị trí của với mỗi từ đến qua ví dụ sau đây:
Cho trước dãy và
Độ dài đoạn con | ||
---|---|---|
Độ dài của đoạn con "tốt" dài nhất của dãy là giá trị lớn nhất của độ dài các đoạn con "tốt" dài nhất với vị trí kết thúc từ đến .
Ở đây, độ dài đoạn con "tốt" dài nhất của dãy là .
Qua ví dụ vừa rồi, ta thấy rằng, vị trí đối với các giá trị từ đến có giá trị không giảm.
Thật vậy, với mọi thì , vì thế giá trị đối với phải không quá giá trị đối với .
Hơn nữa vì các phần tử trong dãy đều có giá trị không quá cho nên luôn tồn tại vị trí sao cho đoạn là một đoạn "tốt".
Với những phân tích như trên, ta có giải quyết bài toán với phương pháp hai con trỏ như sau:
Để hiểu rõ hơn, ta hãy cùng xem qua một số ví dụ sau đây:
và
Sử dụng biến để lưu lại giá trị lớn nhất của độ dài đoạn "tốt" có vị trí kết thúc tại , với từ đến .
Đặt và
Tăng vị trí lên đơn vị
Tăng vị trí lên đơn vị
Tăng vị trí lên đơn vị
Tăng vị trí lên đơn vị
Tăng vị trí lên đơn vị
Tăng vị trí lên đơn vị
Tăng vị trí lên đơn vị
Tăng vị trí lên đơn vị
Tăng vị trí lên đơn vị
Tăng vị trí lên đơn vị
Tăng vị trí lên đơn vị
Cài đặt
Để có thể tính được tổng các phần tử từ đến trong khi và đang di động, ta sẽ sử dụng biến để lưu lại tổng của đoạn hiện tại.
Sau khi di chuyển sang phải, biến sẽ cộng thêm giá trị .
Trước khi di chuyển sang phải, biến sẽ trừ đi giá trị .
int ans = 0, sum = 0;
for (int l = 1, r = 1; r <= n; r++) {
sum += a[r];
while (sum > s) {
sum -= a[l];
l++;
}
ans = max(ans, r - l + 1);
}
cout << ans;
Độ phức tạp
Vị trí con trỏ luôn tăng, vị trí con trỏ luôn tăng và luôn tăng không giá trị .
Hơn nữa, mỗi vị trí và tăng không quá lần.
Vì thế độ phức tạp của giải pháp là .
Bạn được cho một dãy số nguyên như sau:
Tìm nhỏ nhất sao cho tồn tại và . Dữ liệu đảm bảo không quá .
Giới hạn: và .
Để dễ dàng phân tích ta định nghĩa hàm như sau:
Dãy số của chúng ta sẽ có dạng
Với phép chia lấy dư cho thì mọi , giá trị của sẽ có giá trị nằm trong khoảng .
Vì thế, dãy số với vô hạn phần tử này sẽ tồn tại với . (theo nguyên lý Dirichlet)
Có thể thấy, khi dãy tồn tại , dãy sẽ xuất hiện chu kỳ. Cụ thể như sau:
Gọi là giá trị nhỏ nhất thỏa mãn tồn tại sao cho .
Khi đó, dãy sẽ có chu kỳ lặp lại các phần tử từ đến
Dãy số có thể biễu diễn như hình sau đây:
Bài toán có thể giải quyết nếu chúng ta phần tử bắt đầu chu kỳ (
Cụ thể, xem ví dụ sau đây:
Ta có dãy số
Giá trị
Ta có thể tính được giá trị này bằng cách xác định
Ở đây, phần tử bắt đầu chu kỳ là
Giá trị
Để xác định giá trị
Khởi tạo hai con trỏ,
Tại mỗi thời điểm, ta tịnh tiến hai con trỏ này như sau:
Ngoài lúc ban đầu, hai con trỏ
Cách cài đặt để
int tortoise = 1, hare = 1;
while (true) {
tortoise = f(tortoise);
hare = f(f(hare));
if (tortoise == hare)
break;
}
Khởi tạo một con trỏ mới
Tịnh tiến cùng lúc hai con trỏ
Số lần tịnh tiến ở đây chính là
Chứng minh:
Cách cài đặt tìm
int mu = 0, p = 1;
while (p != tortoise) {
p = f(p);
tortoise = f(tortoise);
mu++;
}
Bây giờ cả hai con trỏ
Chúng ta giữ nguyên giá trị
Vì
int lambda = 0;
while (true) {
lambda++;
p = f(p);
if (tortoise == p)
break;
}
Để hiểu rõ hơn, ta hãy cùng xem qua một số ví dụ sau đây:
Ta có dãy số
Giá trị
Ta có thể tính được giá trị này bằng cách xác định
Ở đây, phần tử bắt đầu chu kỳ là
Giá trị
Độ phức tạp
Khi khởi tạo hai con trỏ
Cụ thể
Vì thế việc xác định được vị trí
Kết luận: độ phức tạp của bài toán là