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 $a$ và $b$ lần lượt có $n$ và $m$ phần tử. Hãy ghép chúng thành dãy $c$ được bố trí theo thứ tự không giảm.
Giới hạn: $n, m \leq 10^5$ và $0 \leq a_i, b_i \leq 10^{9}$.
Hãy cùng xem ví dụ sau đây.
Cho trước hai dãy số $a$ và $b$ đượ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$ 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 $c$.
Vì dãy $c$ được bố trí theo thứ tự không giảm, cho nên phần tử đầu tiên của dãy $c$ phải là phần tử có giá trị nhỏ nhất trong cả hai dãy $a$ và $b$.
Ta có thể so sánh hai phần tử nhỏ nhất của hai dãy $a$, $b$ và đưa phần tử có giá trị nhỏ hơn vào vị trí đầu tiên của dãy $c$.
Dãy $a$ và $b$ đã đượ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 ($a[1]$ và $b[1]$).
Bây giờ, phần tử tiếp theo của dãy $c$ sẽ là phần tử nhỏ nhất trong các phần tử chưa được đưa vào dãy $c$.
Dãy $a$ và $b$ đã được sắp xếp không giảm, vì thế sau khi đưa $a[1]$ vào dãy $c$, $a[2]$ là phần tử nhỏ nhất chưa được chọn ở dãy $a$ và $b[1]$ là phần tử nhỏ nhất chưa được chọn ở dãy $b$.
So sánh $a[2]$ và $b[1]$, chọn phần tử có giá trị nhỏ hơn và đưa vào dãy $c$.
Sau khi đưa $b[1]$ vào dãy $c$, $b[2]$ trở thành phần tử nhỏ nhất chưa được chọn ở dãy $b$.
Vẫn như thế, phần tử tiếp theo của dãy $c$ sẽ là phần tử nhỏ nhất trong các phần tử chưa được đưa vào dãy $c$.
So sánh $b[2]$ và $a[2]$, chọn phần tử có giá trị nhỏ hơn dãy và đưa vào dãy $c$.
Sau khi đưa $a[2]$ vào dãy $c$, $a[3]$ trở thành phần tử nhỏ nhất chưa được chọn ở dãy $a$.
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:
$a = [1, 3, 6, 8, 10], b = [2, 6, 7, 12, 14,15]$
Đặt $i = 1$ và $j = 1$.
$a = [\overset{\underset{\downarrow}{\color{red}i}}{\color{red}1}, 3, 6, 8, 10]$
$b = [\underset{\overset{\uparrow}{\color{blue}j}}{\color{blue}2}, 6, 7, 12, 14,15]$
$c = []$
Vì $a[i]<b[j]$ nên ta đưa $a[i]$ vào mảng $c$ và tăng vị trí $i$ lên một.
$a = [1,\overset{\underset{\downarrow}{\color{red}i}}{\color{red}3}, 6, 8, 10]$
$b = [\underset{\overset{\uparrow}{\color{blue}j}}{\color{blue}2}, 6, 7, 12, 14,15]$
$c = [1]$
Vì $b[j]<a[i]$ nên ta đưa $b[j]$ vào mảng $c$ và tăng vị trí $j$ lên một.
$a = [1, \overset{\underset{\downarrow}{\color{red}i}}{\color{red}3}, 6, 8, 10]$
$b = [2, \underset{\overset{\uparrow}{\color{blue}j}}{\color{blue}6}, 7, 12, 14,15]$
$c = [1, 2]$
Vì $a[i]<b[j]$ nên ta đưa $a[i]$ vào mảng $c$ và tăng vị trí $i$ lên một.
$a = [1, 3, \overset{\underset{\downarrow}{\color{red}i}}{\color{red}6}, 8, 10]$
$b = [2, \underset{\overset{\uparrow}{\color{blue}j}}{\color{blue}6}, 7, 12, 14,15]$
$c = [1, 2, 3]$
Vì $a[i]=b[j]$ nên ta có thể đưa bất kỳ một trong hai phần tử. Ở đây ta đưa phần tử $a[i]$ vào $c$ và tăng vị trí $i$ lên một.
$a = [1, 3, 6, \overset{\underset{\downarrow}{\color{red}i}}{\color{red}8}, 10]$
$b = [2, \underset{\overset{\uparrow}{\color{blue}j}}{\color{blue}6}, 7, 12, 14,15]$
$c = [1, 2, 3, 6]$
Vì $b[j]<a[i]$ nên ta đưa $b[j]$ vào mảng $c$ và tăng vị trí $j$ lên một.
$a = [1, 3, 6, \overset{\underset{\downarrow}{\color{red}i}}{\color{red}8}, 10]$
$b = [2, 6, \underset{\overset{\uparrow}{\color{blue}j}}{\color{blue}7}, 12, 14,15]$
$c = [1, 2, 3, 6, 6]$
Vì $b[j]<a[i]$ nên ta đưa $b[j]$ vào mảng $c$ và tăng vị trí $j$ lên một.
$a = [1, 3, 6, \overset{\underset{\downarrow}{\color{red}i}}{\color{red}8}, 10]$
$b = [2, 6, 7, \underset{\overset{\uparrow}{\color{blue}j}}{\color{blue}12}, 14,15]$
$c = [1, 2, 3, 6, 6, 7]$
Vì $a[i]<b[j]$ nên ta đưa $a[i]$ vào mảng $c$ và tăng vị trí $i$ lên một.
$a = [1, 3, 6, 8, \overset{\underset{\downarrow}{\color{red}i}}{\color{red}10}]$
$b = [2, 6, 7, \underset{\overset{\uparrow}{\color{blue}j}}{\color{blue}12}, 14,15]$
$c = [1, 2, 3, 6, 6, 7, 8]$
Vì $a[i]<b[j]$ nên ta đưa $a[i]$ vào mảng $c$ và tăng vị trí $i$ lên một.
$a = [1, 3, 6, 8, 10]\overset{\underset{\downarrow}{\color{red}i}}{ }$
$b = [2, 6, 7, \underset{\overset{\uparrow}{\color{blue}j}}{\color{blue}12}, 14,15]$
$c = [1, 2, 3, 6, 6, 7, 8, 10]$
Vì tất cả các phần tử trong dãy $a$ đều đã được đưa vào dãy $c$ nên từ đưa lần lượt các phần tử chưa được chọn trong dãy $b$ vào trong dãy $c$ $c = [1, 2, 3, 6, 6, 7, 8, 10, 12, 14,15]$
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ỏ $i$ luôn tăng và tăng quá không quá $n$ lần, vị trí con trỏ $j$ cũng luôn tăng và tăng không quá $m$ lần.
Vì thế độ phức tạp của giải pháp là $O(n+m)$.
VNOJ - NKSGAME
CODEFORCES - 1251C
CODEFORCES - 1036D
Cho một mảng số nguyên $a$ có $n$ 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à $x$.
Giới hạn: $2 \leq n \leq 10^6$ và $0 \leq a_i, x \leq 10^9$
Hãy cùng xem ví dụ sau đây.
Cho trước mảng số $a$ được sắp xếp tăng dần và $x=16$:
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à $x$ ?
Trước tiên, ta có một chút nhận xét sau:
Có thể thấy, tổng của $a[7]$ với các phần tử khác trong dãy đều lớn hơn $X$. Vì thế ta không quan tâm đến $a[7]$ nữa.
Có thể thấy, tổng của $a[1]$ với các phần tử khác trong các phần tử ta quan tâm đều nhỏ hơn $x$. Vì thế ta không quan tâm đến $a[1]$ nữa.
Có thể thấy, tổng của $a[6]$ với các phần tử khác trong các phần tử ta quan tâm đều lớn hơn $x$. Vì thế ta không quan tâm đến $a[6]$ 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 $[i,j]$ 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: $a = [2, 5, 6, 8, 10, 12, 15]$ và $x = 16$.
Đặt $i=1$ và $j=N$.
$a = [\overset{\underset{\downarrow}{\color{red}i}}{\color{red}2}, 5, 6, 8, 10, 12, \underset{\overset{\uparrow}{\color{blue}j}}{\color{blue}15}]$
Vì $a[i]+a[j]=2+15=17>x$ nên giảm vị trí $j$ đi một đơn vị.
$a = [\overset{\underset{\downarrow}{\color{red}i}}{\color{red}2}, 5, 6, 8, 10, \underset{\overset{\uparrow}{\color{blue}j}}{\color{blue}12}, 15]$
Vì $a[i]+a[j]=2+12=14<x$ nên tăng vị trí $i$ lên một đơn vị.
$a = [2, \overset{\underset{\downarrow}{\color{red}i}}{\color{red}5}, 6, 8, 10, \underset{\overset{\uparrow}{\color{blue}j}}{\color{blue}12}, 15]$
Vì $a[i]+a[j]=5+12=17>x$ nên giảm vị trí $j$ đi một đơn vị.
$a = [2, \overset{\underset{\downarrow}{\color{red}i}}{\color{red}5}, 6, 8, \underset{\overset{\uparrow}{\color{blue}j}}{\color{blue}10}, 12, 15]$
Vì $a[i]+a[j]=5+10<x$ nên tăng vị trí $i$ lên một đơn vị.
$a = [2, 5, \overset{\underset{\downarrow}{\color{red}i}}{\color{red}6}, 8, \underset{\overset{\uparrow}{\color{blue}j}}{\color{blue}10}, 12, 15]$
Vì $a[i]+a[j]=6+10=x$ nên hai vị trí cần tìm là hai vị trí $i$ và $j$.
Ví dụ 2: $a = [2, 3, 7, 8, 10, 12, 15]$ và $x = 16$.
Đặt $i=1$ và $j=N$.
$a = [\overset{\underset{\downarrow}{\color{red}i}}{\color{red}2}, 3, 7, 8, 10, 12, \underset{\overset{\uparrow}{\color{blue}j}}{\color{blue}15}]$
Vì $a[i]+a[j]=5+12=17>x$ nên giảm vị trí $j$ đi một đơn vị.
$a = [\overset{\underset{\downarrow}{\color{red}i}}{\color{red}2}, 3, 7, 8, 10, \underset{\overset{\uparrow}{\color{blue}j}}{\color{blue}12}, 15]$
Vì $a[i]+a[j]=2+12=14<x$ nên tăng vị trí $i$ lên một đơn vị.
$a = [2, \overset{\underset{\downarrow}{\color{red}i}}{\color{red}3}, 7, 8, 10, \underset{\overset{\uparrow}{\color{blue}j}}{\color{blue}12}, 15]$
Vì $a[i]+a[j]=3+12=15<x$ tăng vị trí $i$ lên một đơn vị.
$a = [2, 3, \overset{\underset{\downarrow}{\color{red}i}}{\color{red}7}, 8, 10, \underset{\overset{\uparrow}{\color{blue}j}}{\color{blue}12}, 15]$
Vì $a[i]+a[j]=7+12=19>x$ giảm vị trí $j$ đi một đơn vị.
$a = [2, 3, \overset{\underset{\downarrow}{\color{red}i}}{\color{red}7}, 8, \underset{\overset{\uparrow}{\color{blue}j}}{\color{blue}10}, 12, 15]$
Vì $a[i]+a[j]=7+10=17>x$ giảm vị trí $j$ đi một đơn vị.
$a = [2, 3, \overset{\underset{\downarrow}{\color{red}i}}{\color{red}7}, \underset{\overset{\uparrow}{\color{blue}j}}{\color{blue}8}, 10, 12, 15]$
Vì $a[i]+a[j]=7+8=15<x$ tăng vị trí $i$ lên một đơn vị.
$a = [2, 3, 7, \overset{\underset{\downarrow}{\color{red}i}}{ \underset{\overset{\uparrow}{\color{blue}j}}{\color{purple}8}}, 10, 12, 15]$
Vì $i=j$ 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ỏ $i$ luôn tăng, vị trí con trỏ $j$ 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à $X$ hay khi vị trí $i$ bằng vị trí $j$.
Vì thế, việc thay đổi vị trí hai con trỏ sẽ không quá $n$ lần, độ phức tạp của giải pháp là $O(n)$.
LQDOJ - FINDPAIR
LQDOJ - CNTPAIR02
VNOJ - NDCCARD
VNOJ - TWOSUM
Cho dãy số nguyên dương $a$ có $n$ 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á $s$.
Dữ liệu đảm bảo các phần tử trong dãy $a$ đều có giá trị không quá $s$.
Giới hạn: $1\leq n \leq 10^6$, $1 \leq a_i \leq 10^9$ và $s \leq 10^{18}$.
Để 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 $a$ là một dãy số nguyên dương cho nên
Với $r$ là một vị trí bất kỳ, nếu như $l$ là vị trí nhỏ nhất sao cho đoạn $[l,r]$ là một đoạn "tốt" thì
Từ đó, với mỗi $r$ từ $1$ đến $n$, nếu ta xác định được vị trí $l$, ta có thể biết được độ dài của đoạn con "tốt" dài nhất của dãy $a$.
Hãy cùng nhận xét vị trí của $l$ với mỗi $r$ từ $1$ đến $n$ qua ví dụ sau đây:
Cho trước dãy $a = [2, 6, 5, 3, 6, 8, 9]$ và $s=20$
$r$ | $l$ | Độ dài đoạn con |
---|---|---|
$1$ | $1$ | $1$ |
$2$ | $1$ | $2$ |
$3$ | $1$ | $3$ |
$4$ | $1$ | $4$ |
$5$ | $2$ | $4$ |
$6$ | $4$ | $3$ |
$7$ | $6$ | $2$ |
Độ 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ừ $1$ đến $n$.
Ở đây, độ dài đoạn con "tốt" dài nhất của dãy là $4$.
Qua ví dụ vừa rồi, ta thấy rằng, vị trí $l$ đối với các giá trị $r$ từ $1$ đến $n$ có giá trị không giảm.
Thật vậy, với mọi $x<l$ thì $sum(x,r)>s \Rightarrow sum(x,r+1)>s$, vì thế giá trị $l$ đối với $r+1$ phải không quá giá trị $l$ đối với $r$.
Hơn nữa vì các phần tử trong dãy $a$ đều có giá trị không quá $s$ cho nên luôn tồn tại vị trí $l \leq r$ sao cho đoạn $[l,r]$ 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:
$a = [2, 6, 5, 3, 6, 8, 9]$ và $s=20$
Sử dụng biến $ans$ để 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 $r$, với $r$ từ $1$ đến $n$.
Cài đặt
Để có thể tính được tổng các phần tử từ $l$ đến $r$ trong khi $l$ và $r$ đang di động, ta sẽ sử dụng biến $sum$ để lưu lại tổng của đoạn $[l,r]$ hiện tại.
Sau khi di chuyển $r$ sang phải, biến $sum$ sẽ cộng thêm giá trị $a[r]$.
Trước khi di chuyển $l$ sang phải, biến $sum$ sẽ trừ đi giá trị $a[l]$.
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ỏ $r$ luôn tăng, vị trí con trỏ $l$ luôn tăng và luôn tăng không giá trị $r$.
Hơn nữa, mỗi vị trí $l$ và $r$ tăng không quá $n$ lần.
Vì thế độ phức tạp của giải pháp là $O(n)$.
VNOJ - SOPENP
VNOJ - PRODUCT
VNOJ - KRECT
VNOJ - VMQUABEO
Bạn được cho một dãy số nguyên như sau:
Tìm $n$ nhỏ nhất sao cho tồn tại $m < n$ và $x_m = x_n$. Dữ liệu đảm bảo $n$ không quá $2 \cdot 10^7$.
Giới hạn: $1 \leq a \leq 10^4$ và $1 \leq b,c \leq 10^{14}$.
Để dễ dàng phân tích ta định nghĩa hàm $f$ như sau:
Dãy số của chúng ta sẽ có dạng
Với phép chia lấy dư cho $c$ thì mọi $i > 0$, giá trị của $x_i$ sẽ có giá trị nằm trong khoảng $[0, c-1] $.
Vì thế, dãy số với vô hạn phần tử này sẽ tồn tại $x_m = x_n$ với $m < n$. (theo nguyên lý Dirichlet)
Có thể thấy, khi dãy tồn tại $x_m = x_n$, dãy sẽ xuất hiện chu kỳ. Cụ thể như sau:
Gọi $n$ là giá trị nhỏ nhất thỏa mãn tồn tại $m < n$ sao cho $x_m=x_n$.
Khi đó, dãy sẽ có chu kỳ lặp lại các phần tử từ $x_m$ đến $x_{n-1}$
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ỳ ($x_{\mu}$) và độ dài của chu kỳ $\lambda$.
Cụ thể, xem ví dụ sau đây:
Ta có dãy số
Giá trị $n$ cần tìm của bài toán là $n = 9$.
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à $x_5$ và độ dài chu kỳ là $4$.
Giá trị $n = \mu + \lambda = 5 + 4 = 9$.
Để xác định giá trị $\mu$ và $\lambda$, ta sử dụng thuật toán Floyd's tortoise and hare
Khởi tạo hai con trỏ, $toroise$ (rùa) và $hare$ (thỏ).
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ỏ $tortoise$ và $hare$ sẽ luôn gặp nhau tại thời điểm nào đó. Thật vậy:
Cách cài đặt để $tortoise$ và $hare$ gặp nhau:
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 $p=x_0$, con trỏ này được tịnh tiến giống như con trỏ $tortoise$.
Tịnh tiến cùng lúc hai con trỏ $p$ và $tortoise$ và dừng lại cho đến khi chúng gặp nhau.
Số lần tịnh tiến ở đây chính là $\mu$.
Chứng minh:
Cách cài đặt tìm $\mu$:
int mu = 0, p = 1;
while (p != tortoise) {
p = f(p);
tortoise = f(tortoise);
mu++;
}
Bây giờ cả hai con trỏ $tortoise$ và $p$ đang có giá trị là $x_{\mu}$.
Chúng ta giữ nguyên giá trị $tortoise$, và tịnh tiến $p$ cho đến khi $p$ có giá trị $x_{\mu}$ lại.
Vì $p$ đã ở trong chu kỳ cho nên, sau khi tinh tiến $\lambda$ lần, $p$ sẽ lại có giá trị là $x_{\mu}$.
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ị $n$ cần tìm của bài toán là $n = 12$.
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à $x_4$ và độ dài chu kỳ là $8$.
Giá trị $n = \mu + \lambda = 4 + 8 = 12$.
Độ phức tạp
Khi khởi tạo hai con trỏ $tortoise$ và $hare$ ở $x_0$, hai con trỏ sẽ gặp lại nhau lần đầu tiên sau $t$ bước.
Cụ thể $t$ là số nguyên nhỏ nhất sao cho $t$ có giá trị lớn hơn hoặc bằng $\mu$ và chia chết cho $\lambda$.
Vì thế việc xác định được vị trí $tortoise$ và $hare$ gặp nhau sẽ mất không quá $\mu + \lambda$ bước. Hơn nữa, việc xác định $\mu$ mất $\mu$ bước, xác định $\lambda$ mất $\lambda$ bước.
Kết luận: độ phức tạp của bài toán là $O(\mu + \lambda)$. (trong đó $\mu + \lambda \leq 2 \cdot 10^7$)
LODOJ - TORHAR
CODEFORCES - Sequence analysis
CODEFORCES - Pseudo-Random Number Generator
CODEFORCES - Cooperative Game