Khoảng Cách Bằng X
Đề bài
KHOẢNG CÁCH BẰNG X
Cho mảng $A[\,]$ gồm $N$ phần tử và số $X$. Nhiệm vụ của bạn là tìm cặp phần tử $A[i] - A[j] = X$. Nếu tồn tại $A[i] - A[j] = X$ đưa ra $1$, ngược lại đưa ra $-1$.
Input:
- Dòng đầu tiên đưa vào số lượng bộ test $T$.
- Những dòng kế tiếp đưa vào các bộ test. Mỗi bộ test gồm hai dòng: dòng thứ nhất là cặp số $N$, $X$; dòng tiếp theo là $N$ số $A[i]$ là các phần tử của mảng $A[\,]$.
- $T, N, X, A[i]$ thỏa mãn ràng buộc: $1 \le T \le 100$; $1 \le N, X \le 10^5$; $1 \le A[i] \le 10^5$.
Output: Đưa ra kết quả mỗi test theo từng dòng.
Giới hạn thời gian: 1 giây.
2
6 78
5 20 3 2 5 80
5 45
90 70 20 80 501
-1Lời mở đầu
Bài này mình được một bạn học viên hỏi nên sẵn tiện mình viết bài giải chi tiết để nhiều bạn được tham khảo luôn. Hy vọng mang đến được giá trị cho các bạn 😇.
Bạn nào muốn thử nộp bài để hệ thống chấm xem có đúng hay không thì link bài đây nhé:
Bấm vào nút bên dưới để nộp bài và được hệ thống CLUE OJ chấm tự động.
Nộp bài trên CLUE OJGiải thích ví dụ cho bạn nào chưa hiểu đề:
- Bộ test 1: $X = 78$, mảng $\{5, 20, 3, 2, 5, 80\}$. Có cặp $80 - 2 = 78$ ⇒ in 1.
- Bộ test 2: $X = 45$, mảng $\{90, 70, 20, 80, 50\}$. Hiệu của mọi cặp là $20, 70, 10, 40, 50, 10, 20, 60, 30, 30$ – không có hiệu nào bằng $45$ ⇒ in -1.
Lộ trình lời giải, bấm vào từng phần để nhảy tới:
Cách 1Vét cạn xét tất cả các cặp – O(T·N²)
Cách đơn giản nhất là vét cạn xét tất cả các cặp số: khi nào tìm được cặp thoả mãn thì dừng lại in ra $1$, còn xét đến tận cùng vẫn không có cặp nào thì in ra $-1$. Cứ thế xét hết tất cả các bộ test, đơn giản vậy thôi.
Giải thích thêm một số chỗ trong code bên dưới:
- Dùng
absđể viết gọn. Mọi phần tử và $X$ đều dương. Hiệu $a[i] - a[j]$ có thể âm (khi $a[i] \lt a[j]$) hoặc dương, trong khi $X$ luôn dương. Hai vòng lặp lồng nhau với $j \gt i$ chỉ xét mỗi cặp một lần, lẽ ra phải kiểm tra cả $a[i] - a[j]$ lẫn $a[j] - a[i]$; dùng hàm trị tuyệt đốiabsthì hiệu âm hay dương đều thành dương, chỉ việc so với $X$, gọn hơn hẳn. - Cờ hiệu
checkvà mẹo thoát 2 vòng lặp. Ban đầucheck = -1(chưa tìm thấy), tìm thấy thì gáncheck = 1; cuối cùng in luôncheckcũng chính là đáp án. Khi tìm thấy rồi muốn thoát cả 2 vòng lặp, nhưngbreakchỉ thoát vòng gần nhất (vòngfor j), vòngfor ivẫn chạy tiếp. Mẹo: trước khibreakta gáni = n; vòngfor ităngi++thành $n + 1$, không còn thoải < nnữa nên cũng kết thúc luôn.
Source code để các bạn tham khảo nếu làm theo hướng này:
#include <bits/stdc++.h>
using namespace std;
int main(){
int t;
cin >> t;
while(t--){
int n, x;
cin >> n >> x;
int a[n];
for(int i = 0; i < n; ++i){
cin >> a[i];
}
int check = -1;
for(int i = 0; i < n; ++i){
for(int j = i + 1; j < n; ++j){
if(abs(a[i] - a[j]) == x){
check = 1;
i = n;
break;
}
}
}
cout << check << endl;
}
return 0;
}
import sys
def main():
data = sys.stdin.read().split()
p = 0
t = int(data[p]); p += 1
out = []
for _ in range(t):
n, x = int(data[p]), int(data[p + 1]); p += 2
a = [int(v) for v in data[p:p + n]]; p += n
check = -1
for i in range(n):
for j in range(i + 1, n):
if abs(a[i] - a[j]) == x:
check = 1
break
if check == 1: # đã tìm thấy thì thoát luôn vòng ngoài
break
out.append(str(check))
print('\n'.join(out))
main()
Đánh giá độ phức tạp của cách làm này:
- Không gian: mảng $a$ của mỗi bộ test ⇒ $O(N)$.
- Thời gian: $O(N)$ đọc mảng + $O(N^2)$ tìm cặp, nhân với $T$ bộ test ⇒ $O(T \cdot N^2)$. Trường hợp xấu nhất là không bộ test nào có cặp thoả mãn (luôn phải xét tới cùng) với $T = 100$, $N = 10^5$: $100 \cdot (10^5 + 10^{10}) \approx 10^{12}$, tức khoảng 1000 tỷ lần lặp ⇒ chắc chắn TLE.
Cách 2Kỹ thuật mảng đánh dấu – O(T·(N + 10^5))
Dấu hiệu dễ nhìn ra bài này giải được tối ưu bằng kỹ thuật mảng đánh dấu: giá trị $X$ cũng như các $a[i]$ chỉ nằm trong đoạn từ $1$ đến $10^5$.
Muốn có cặp $a_i - a_j = X$ thì chuyển vế đổi dấu: $a_i = a_j + X$ hoặc $a_j = a_i - X$. $X$ đề đã cho sẵn, $a_i, a_j$ đại diện cho các giá trị trong mảng. Nhìn ví dụ 1: $X = 78$, mảng $\{5, 20, 3, 2, 5, 80\}$. Khi xét $a[0] = 5$, nếu trong mảng có một số $y = 5 + 78 = 83$ hoặc $y = 5 - 78 = -73$ thì ta tìm được cặp thoả mãn (vì $83 - 5 = 78$, hay $5 - (-73) = 78$). Mà các giá trị trong mảng chỉ nằm trong $[1, 10^5]$, nên $y$ nào nằm ngoài đoạn này thì bỏ qua, không cần xét; ở đây chỉ cần tìm xem có số $83$ trong mảng không.
Nếu kiểm tra "$y$ có trong mảng không" bằng cách duyệt lại cả mảng thì vẫn là $O(N^2)$. Đây là lúc mảng đánh dấu ra tay:
- Tạo mảng
checkcó $10^5 + 1$ phần tử (chỉ số $0..10^5$, bỏ qua chỉ số 0), khởi tạo toànfalse. - Khi đọc mỗi $a[i]$, gán
check[a[i]] = true. Thao tác này chỉ tốn $O(1)$ vì mảng truy cập được vào chỉ số bất kỳ ngay lập tức. - Đọc xong, muốn biết $y$ có xuất hiện trong mảng không thì chỉ cần nhìn
check[y]:truelà có,falselà không ⇒ $O(1)$.
Vậy chỉ tốn thêm bộ nhớ cho mảng check, nhưng bù lại bước tìm cặp rút từ $O(N^2)$ xuống còn $O(N)$.
Minh hoạ
Em nhập $X$ và mảng $a$ (giá trị nhỏ để dễ nhìn) rồi bấm Tiếp: đầu tiên các ô check[v] được tô xanh dương cho mọi $v$ có trong mảng, sau đó với từng $a[i]$ ta "nhảy" tới ô $a[i] + X$ và $a[i] - X$ để kiểm tra:
Source code để các bạn tham khảo nếu làm theo hướng này:
#include <bits/stdc++.h>
using namespace std;
int main(){
ios_base::sync_with_stdio(false); // đọc nhanh vì dữ liệu vào có thể tới 10^7 số
cin.tie(NULL);
int t;
cin >> t;
while(t--){
int n, x;
cin >> n >> x;
int a[n];
bool check[100001] = {false};
for(int i = 0; i < n; ++i){
cin >> a[i];
check[a[i]] = true;
}
int res = -1;
for(int i = 0; i < n; ++i){
int y = a[i] + x;
if(y >= 1 && y <= 100000){
if(check[y] == true){
res = 1;
break;
}
}
y = a[i] - x;
if(y >= 1 && y <= 100000){
if(check[y] == true){
res = 1;
break;
}
}
}
cout << res << endl;
}
return 0;
}
import sys
def main():
data = sys.stdin.read().split()
p = 0
t = int(data[p]); p += 1
out = []
for _ in range(t):
n, x = int(data[p]), int(data[p + 1]); p += 2
a = [int(v) for v in data[p:p + n]]; p += n
check = [False] * 100001 # mảng đánh dấu: check[v] = True nếu v có trong mảng
for v in a:
check[v] = True
res = -1
for v in a:
y = v + x
if 1 <= y <= 100000 and check[y]:
res = 1
break
y = v - x
if 1 <= y <= 100000 and check[y]:
res = 1
break
out.append(str(res))
print('\n'.join(out))
main()
ios_base::sync_with_stdio(false); cin.tie(NULL); vào code gốc. Lý do: dữ liệu vào xấu nhất có tới $T \cdot N = 10^7$ số (gần 60 MB). Mình đo thử, riêng việc đọc bằng cin thường đã mất gần 3 giây, thêm 2 dòng này thì cả chương trình chỉ còn khoảng 0.7 giây. Thuật toán không đổi gì cả.Đánh giá độ phức tạp của cách làm này:
- Không gian: mảng $a$ $O(N)$ + mảng đánh dấu $O(10^5 + 1)$.
- Thời gian: mỗi bộ test tốn $O(10^5)$ khởi tạo mảng
check+ $O(N)$ đọc dữ liệu + $O(N)$ tìm cặp; nhân với $T$ bộ test ⇒ $O(T \cdot \max(N, 10^5))$. Trường hợp xấu nhất: $10^2 \cdot 10^5 \cdot 3 = 3 \cdot 10^7$ lần lặp, vừa đủ ở ngưỡng an toàn nên không sợ TLE.
Đến đây các bạn đã thấy cái hay của giải thuật rồi chứ? Cách làm trước trong trường hợp xấu nhất phải lặp khoảng 1000 tỷ lần, cách này rút xuống chỉ còn 30 triệu lần lặp 😍.
- Không biết học gì trước, học gì sau, học lan man nên kiến thức bị hổng.
- Bí bài không ai gỡ, ngồi cả buổi vẫn không ra rồi bỏ cuộc.
- Code chạy được nhưng không đủ điểm, không biết sai ở đâu, chậm ở đâu.
- Không có đề thi thử và người chấm, nên không biết mình đang ở mức nào.
- Dễ nản giữa chừng khi không có ai theo sát.
- Lộ trình 111 buổi video sắp sẵn từ dòng code đầu tiên đến đề HSG thật của các tỉnh.
- Thầy trực tiếp gỡ bí, chữa bài cho từng em qua nhóm Zalo riêng.
- Hệ thống chấm bài tự động 24/7 và thi thử HSG hằng tuần để biết chính xác mình đang ở đâu.
- Đóng học phí một lần, thầy kèm lâu dài: từ thi chuyên Tin đến HSG quốc gia và cả khi lên đại học.
Nhận xét
Đăng nhận xét