Tuyển sinh 🎓 Mình nhận đào tạo lập trình giải thuật thi đấu: thi vào lớp 10 chuyên tin · thi học sinh giỏi tin học cấp tỉnh/thành · thi vào đội tuyển quốc gia · Olympic tin học sinh viên / ICPC · phỏng vấn thuật toán. Bấm vào đây để xem thông tin khóa học → Xem khóa học

Khuyến mãi trà sữa

Khuyến mãi trà sữa
Nguồn: Codeforces Group Độ khó: Trung bình Sắp xếp Tìm kiếm nhị phân lower_bound

Đề bài

Nhân dịp sinh nhật, Lazada tổ chức chương trình khuyến mãi trà sữa cho khách hàng. Mỗi lượt khuyến mãi sẽ có tất cả $\mathbf{n}$ ly trà sữa được chế biến, với giá tiền mỗi ly đã được khuyến mãi là $\mathbf{b_i}$. Do số lượng trà sữa có hạn, nên $\mathbf{n}$ vị khách ngẫu nhiên sẽ được lựa chọn để tham gia chương trình, và số tiền mỗi người có thể chi trả cho việc mua trà sữa là $\mathbf{a_i}$.

Ban đầu mỗi người chơi sẽ được chỉ định để mua ly trà sữa đúng với vị trí của mình. Tuy nhiên, để tăng tính tương tác cho khách hàng và đảm bảo công bằng, Lazada quyết định tổ chức khuyến mãi theo hình thức ghép cặp người chơi. Một cặp đôi 'tốt' được định nghĩa là tổng số tiền họ bỏ ra để mua trà sữa phải lớn hơn tổng giá tiền hai ly trà sữa tại vị trí tương ứng, hay nói cách khác là $\mathbf{a_i + a_j > b_i + b_j}$với hai người tham gia tại vị trí$i$ và $j$.

Lazada muốn biết trước rằng có thể có bao nhiêu cặp đôi tốt tham gia được chương trình khuyến mãi. Lưu ý rằng mỗi người có thể ghép cặp được với nhiều người chơi khác và không nhất thiết phải có điều kiện: $a_i > b_i, \forall i: 1 \le i \le n$.

Các bạn cài đặt chương trình để giúp Lazada giải quyết vấn đề trên, sẽ có hai mức điểm cho câu hỏi này:

Subtask:

  • 50% số test đầu tiên có $n \le 10^4$.
  • 50% số test còn lại không có giới hạn gì thêm.

Đầu vào:

  • Dòng đầu tiên quy định số ly trà sữa được chế biến hay số người được tham gia chương trình, là số nguyên $n$ thỏa $2 \le n \le 2 \cdot 10^5$.
  • Dòng tiếp theo bao gồm $n$ số nguyên $a_1, a_2, \dots, a_n$ $(1 \le a_i \le 10^6)$, trong đó $a_i$ là số tiền của mỗi người chơi bỏ ra để tham gia chương trình.
  • Dòng tiếp theo bao gồm $n$ số nguyên $b_1, b_2, \dots, b_n$ $(1 \le b_i \le 10^6)$, trong đó $b_i$ là giá tiền được khuyến mãi của mỗi ly trà sữa.

Đầu ra: Yêu cầu đầu ra của chương trình là tổng số cặp đôi 'tốt' có thể tham gia khuyến mãi, là một số nguyên.

Ví dụ 1 – input
4
3 2 4 5
2 3 6 4
Ví dụ 1 – output
1
Ví dụ 2 – input
4
3 2 4 5
2 2 6 4
Ví dụ 2 – output
3

Giải thích ví dụ 1: chỉ có một cặp đôi tốt là người chơi thứ 1 và người chơi cuối cùng, thỏa mãn $a_1 + a_4 = 8 > 6 = b_1 + b_4$.

Giải thích ví dụ 2: có ba cặp đôi tốt là người chơi thứ 1 và người chơi cuối cùng, thỏa mãn $a_1 + a_4 = 8 > 6 = b_1 + b_4$; người chơi thứ 1 và người chơi thứ hai,$ $thỏa mãn$a_1 + a_2 = 5 > 4 = b_1 + b_2$; người chơi thứ 2 và người chơi cuối cùng,$ $thỏa mãn$a_2 + a_4 = 7 > 6 = b_2 + b_4$.

Lờ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 😇.

Dành cho học viên · Ưu tiênNộp bài trên hệ thống CLUE OJ

Link này chỉ học viên đã tham gia tổ chức mới vào được. Học viên nên ưu tiên nộp ở đây vì sẽ được chấm nhanh hơn.

Nộp bài trên CLUE OJ
Dành cho tất cả mọi ngườiNộp bài trên Codeforces

Bạn nào muốn thử nộp bài để hệ thống chấm xem có đúng hay không thì dùng link này nhé. Chưa có tài khoản thì tạo tài khoản rồi đăng nhập nộp bài thử hen.

Nộp bài trên Codeforces

Lộ trình lời giải, bấm vào từng phần để nhảy tới:

50%Subtask 1: vét cạn mọi cặp O(N²)

Với Subtask 1 (50% test đầu tiên) cài đặt với thuật $O(N^2)$ thì quá đơn giản rồi, nó là vét cạn xét hết tất cả các cặp có thể thôi: 2 vòng lặp for lồng nhau giống các thuật toán sắp xếp mảng cơ bản mà bạn nào mới học mảng cũng biết đó (Bubble Sort, Interchange Sort). Với mỗi cặp $(i, j)$, $i < j$ ta kiểm tra $a_i + a_j > b_i + b_j$ thì tăng biến đếm.

Đây mình code nhanh ra để các bạn tham khảo, vì nó quá cơ bản rồi nên mình sẽ không giải thích gì thêm nhé 🤪

Code Subtask 1 – vét cạn O(N²)
#include <bits/stdc++.h>
using namespace std;

const int MAX = 2e5;
int a[MAX + 5], b[MAX + 5];

int main(){
    ios::sync_with_stdio(0);
    cin.tie(0);
    int n;
    cin >> n;
    for(int i = 0; i < n; ++i){
        cin >> a[i];
    }
    for(int i = 0; i < n; ++i){
        cin >> b[i];
    }
    long long cnt = 0;
    for(int i = 0; i < n; ++i){
        for(int j = i + 1; j < n; ++j){
            if(a[i] + a[j] > b[i] + b[j]){
                cnt++;
            }
        }
    }
    cout << cnt;
    return 0;
}
import sys
input = sys.stdin.readline

n = int(input())
a = list(map(int, input().split()))
b = list(map(int, input().split()))
cnt = 0
for i in range(n):
    ai = a[i]
    bi = b[i]
    for j in range(i + 1, n):
        if ai + a[j] > bi + b[j]:
            cnt += 1
print(cnt)
C++▶ Chạy tay từng dòng codeSubtask 1: vét cạn mọi cặp (i, j) với 2 vòng for lồng nhau, O(N²).
Dữ liệu mẫu:
1#include <bits/stdc++.h>
2using namespace std;
3
4const int MAX = 2e5;
5int a[MAX + 5], b[MAX + 5];
6
7int main(){
8 ios::sync_with_stdio(0);
9 cin.tie(0);
10 int n;
11 cin >> n;
12 for(int i = 0; i < n; ++i){
13 cin >> a[i];
14 }
15 for(int i = 0; i < n; ++i){
16 cin >> b[i];
17 }
18 long long cnt = 0;
19 for(int i = 0; i < n; ++i){
20 for(int j = i + 1; j < n; ++j){
21 if(a[i] + a[j] > b[i] + b[j]){
22 cnt++;
23 }
24 }
25 }
26 cout << cnt;
27 return 0;
28}
Không gian$O(N)$
Thời gian$O(N^2)$

Có $\frac{N(N-1)}{2}$ cặp. Với $N = 10^4$ là khoảng $5 \cdot 10^7$ phép so sánh, vẫn qua được 1 giây; nhưng với $N = 2 \cdot 10^5$ thì lên tới $2 \cdot 10^{10}$, chắc chắn TLE. Lưu ý biến đếm là long long vì số cặp có thể tới gần $2 \cdot 10^{10}$, vượt giới hạn int.

Mấu chốtChuyển vế: chỉ còn 1 mảng số tiền còn lại

Với Subtask 2 (Full test) thì phải code với thuật $O(N \log N)$ thì mới AC, cái này mới khó hơn đòi hỏi tư duy nhiều hơn. Để đếm xem tổng cộng có bao nhiêu cặp có tổng tiền hiện có lớn hơn tổng giá tiền của 2 ly trà sữa tương ứng của 2 người đó, thay vì phải xét qua lần lượt tất cả các cặp như cách làm ở trên thì muốn giải quyết tối ưu hơn ta phải nhìn ra mấu chốt sau:

Biến đổi
$$a_i + a_j > b_i + b_j \iff (a_i - b_i) + (a_j - b_j) > 0$$ tức là $(a_i - b_i) + (a_j - b_j)$ phải từ 1 trở lên (vì đều là số nguyên).

Thì ta có thể tính xem số tiền còn lại của từng người sau khi trả cho ly trà sữa tương ứng của người đó, tức là a[i] -= b[i]. Tận dụng luôn mảng a đã có khỏi mất công đi tạo mảng mới. Mảng a giờ đây không còn là mảng chứa số tiền ban đầu của mỗi người mà là số tiền còn lại của mỗi người sau khi đã trả cho ly trà sữa của mình.

Người1234
a (tiền có)3245
b (giá ly)2264
a − b (còn lại)10-21
Sau khi sắp tăng dần0123
a-2011

Ví dụ 2: số tiền còn lại là $1, 0, -2, 1$. Các cặp có tổng $\ge 1$: $(1, 0)$ của người 1 và 2, $(1, 1)$ của người 1 và 4, $(0, 1)$ của người 2 và 4 → đúng 3 cặp như đề.

Ý tưởngSắp xếp tăng dần + tìm kiếm nhị phân

Rồi thì đếm xem với số tiền còn lại này sẽ có thể bắt cặp được với số tiền còn lại của bao nhiêu người đằng sau vị trí người đang xét hiện tại để tổng số tiền còn lại của 2 người sẽ từ 1 trở lên. Vậy sau khi tính ra số tiền còn lại của mỗi người, ta đi sắp xếp tăng dần. Rồi xét lần lượt a[i] là số tiền còn lại của người thứ $i$ thì ta sẽ đếm xem đằng sau đó (từ a[i + 1] đến cuối, tức là a[n - 1]) có bao nhiêu người có số tiền còn lại $\ge 1 - a[i]$.

Điều này hết sức nhanh chóng khi mảng đã có thứ tự tăng dần: áp dụng tìm kiếm nhị phân để tìm ra vị trí index bé nhất của người đằng sau vị trí $i$ mà thoả $\ge 1 - a[i]$. Vì là vị trí bé nhất nên từ index đó đến cuối mảng toàn bộ đều là những người thoả mãn (mảng càng về sau càng lớn). Vậy số lượng tính nhanh bằng công thức n - idx.

Việc tìm kiếm nhị phân từ vị trí $i + 1$ trở về sau để tìm vị trí bé nhất thoả $\ge 1 - a[i]$ thì ta dùng hàm lower_bound có sẵn của thư viện STL trong C++ để giải quyết nhanh gọn. Bạn nào chưa biết cách dùng hàm này thì Google search tìm hiểu nhé, nó cũng đơn giản thôi.

Mình bổ sung thêm: sắp xếp lại thì thứ tự người bị xáo trộn, có sao không? Không sao cả! Cặp đôi là cặp không thứ tự (cặp $\{i, j\}$ cũng chính là cặp $\{j, i\}$) và điều kiện chỉ phụ thuộc vào 2 giá trị còn lại $a_i - b_i$, $a_j - b_j$. Sau khi sắp xếp, mỗi cặp vẫn được đếm đúng 1 lần: tại phần tử đứng trước trong mảng đã sắp, khi tìm ở phía sau nó.

Em nhập 2 dãy $a$, $b$ rồi bấm Tiếp để xem từng bước đếm:

Ví dụ nhanh:
Ô vàng: a[i] đang xét; xanh: phía sau, thoả ≥ 1 − a[i]; đỏ: phía sau nhưng chưa đủ; mờ: đã xét xong.

100%Code O(N log N) với sort và lower_bound

Source code cho các bạn tham khảo để thấy rõ hơn những gì mình đã phân tích ở trên:

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

const int MAX = 2e5;
int a[MAX + 5], b[MAX + 5];

int main(){
    ios::sync_with_stdio(0);
    cin.tie(0);
    int n;
    cin >> n;
    for(int i = 0; i < n; ++i){
        cin >> a[i];
    }
    for(int i = 0; i < n; ++i){
        cin >> b[i];
    }
    for(int i = 0; i < n; ++i){
        a[i] -= b[i];
    }
    sort(a, a + n);
    long long cnt = 0;
    for(int i = 0; i < n; ++i){
        int idx = lower_bound(a + i + 1, a + n, 1 - a[i]) - a;
        if(idx != n){
            cnt += n - idx;
        }
    }
    cout << cnt;
    return 0;
}
import sys
from bisect import bisect_left
input = sys.stdin.readline

n = int(input())
a = list(map(int, input().split()))
b = list(map(int, input().split()))
for i in range(n):
    a[i] -= b[i]
a.sort()
cnt = 0
for i in range(n):
    idx = bisect_left(a, 1 - a[i], i + 1, n)
    cnt += n - idx
print(cnt)
C++▶ Chạy tay từng dòng codeFull: a[i] −= b[i], sort tăng dần, lower_bound tìm số đầu tiên ≥ 1 − a[i] ở phía sau, O(N log N).
Dữ liệu mẫu:
1#include <bits/stdc++.h>
2using namespace std;
3
4const int MAX = 2e5;
5int a[MAX + 5], b[MAX + 5];
6
7int main(){
8 ios::sync_with_stdio(0);
9 cin.tie(0);
10 int n;
11 cin >> n;
12 for(int i = 0; i < n; ++i){
13 cin >> a[i];
14 }
15 for(int i = 0; i < n; ++i){
16 cin >> b[i];
17 }
18 for(int i = 0; i < n; ++i){
19 a[i] -= b[i];
20 }
21 sort(a, a + n);
22 long long cnt = 0;
23 for(int i = 0; i < n; ++i){
24 int idx = lower_bound(a + i + 1, a + n, 1 - a[i]) - a;
25 if(idx != n){
26 cnt += n - idx;
27 }
28 }
29 cout << cnt;
30 return 0;
31}

Giải thích một số chỗ trong code

  1. ios::sync_with_stdio(0); cin.tie(0); 2 dòng đầu tiên của hàm main sẽ giúp đẩy nhanh tốc độ nhập xuất dữ liệu nếu các bạn đang code theo ngôn ngữ C++ (để hiểu vì sao thì các bạn cứ gõ mấy từ này lên Google là sẽ ra rất nhiều bài viết giải thích nhé), nên kinh nghiệm trong các bài cần nhập xuất nhiều, như bài này có tới $4 \cdot 10^5$ số cần đọc vào, ta hãy nên để 2 dòng này vào để chạy nhanh hơn. Tất nhiên nếu các bạn code theo cú pháp của C thì chẳng cần mấy dòng đó làm gì, tuy nhiên thường khi code các bài lập trình thi đấu thế này ta dùng C++ vì cú pháp ngắn gọn cũng như nhiều hỗ trợ của nó, vậy nên bạn nào code C++ thì nhớ thêm 2 dòng này vào đầu hàm main nhé.
  2. lower_bound(a + i + 1, a + n, 1 - a[i]) tìm trong đoạn a[i + 1..n - 1] (đã tăng dần) phần tử đầu tiên $\ge 1 - a[i]$ và trả về con trỏ tới nó; trừ đi a ta được index. Bản Python dùng bisect_left(a, 1 - a[i], i + 1, n) có ý nghĩa y hệt.
  3. if(idx != n): các bạn phải xem hiểu về cách sử dụng hàm lower_bound mới hiểu chỗ này nhé. Trong trường hợp không tìm thấy phần tử nào thoả mãn thì hàm sẽ trả về vị trí là $n$ (chính là số lượng phần tử của mảng), và lúc này ta hiểu là không tồn tại phần tử thoả mãn, bởi index hợp lệ là từ $0$ đến $n - 1$ nên index $n$ nghĩa là không hợp lệ. Nên ta chỉ xét những index trả về khác $n$ tức là có tồn tại. (Thực ra khi idx == n thì n - idx = 0, cộng vào cũng không sao, nên bản Python mình bỏ luôn câu if.)

Đánh giá độ phức tạp

Đầu tiên phải xét đến độ phức tạp của việc sắp xếp mảng tăng dần. Nếu bạn nào dùng những thuật như Bubble Sort, Interchange Sort hay Selection Sort với độ phức tạp $O(N^2)$ thì như vậy là không hợp lệ với yêu cầu của câu này rồi, các bạn phải dùng những thuật có độ phức tạp $O(N \log N)$ như Quick Sort, Merge Sort, Heap Sort nhé. Bình thường thì mình dùng hàm sort có trong thư viện STL C++ để độ phức tạp luôn đảm bảo $O(N \log N)$ và nhanh gọn gọi 1 dòng ra xài thôi, chứ tự code thuật thì vừa dài mà còn có thể sai, hoặc ví dụ Quick Sort nếu bị rơi vào trường hợp xấu nhất thì độ phức tạp cũng vẫn là $O(N^2)$. Còn hàm sort trong STL C++ luôn đảm bảo $O(N \log N)$ trong mọi trường hợp: nó dùng thuật Intro Sort, là sự kết hợp, mặc định dùng Quick Sort nhưng nếu phát hiện rơi vào trường hợp xấu nó sẽ tự chuyển qua Heap Sort.

Tiếp đó là xét duyệt qua lần lượt số tiền còn lại của mỗi người, bước này tốn $O(N)$; với mỗi giá trị a[i] đi tìm kiếm nhị phân bước này tốn $O(\log N)$ → bước này độ phức tạp cũng là $O(N \log N)$.

⇒ Tổng quát toàn hàm có độ phức tạp là $O(2N \log N)$, theo quy tắc BigO vẫn tính là $O(N \log N)$ và vẫn đúng như yêu cầu của đề.

Không gian$O(N)$
Thời gian$O(N \log N)$
Ghi chú kiểm tra của mình: code C++ và Python của cả 2 cách là của mình trong bài viết gốc (chỉ sửa lại định dạng). Tất cả đã được kiểm tra với 250 bộ test ngẫu nhiên ($n \le 12$, giá trị nhỏ để có nhiều trường hợp tổng bằng nhau, xen lẫn giá trị lớn tới $10^6$) so với cách đếm trực tiếp theo định nghĩa: khớp 100%. Với $n = 2 \cdot 10^5$: bản C++ O(N log N) chạy khoảng 0.05–0.15 giây, bản Python khoảng 0.3–0.6 giây; còn bản vét cạn C++ mất hơn 20 giây (đúng là chỉ qua Subtask 1: với $n = 10^4$ chạy khoảng 0.04 giây). Riêng bản vét cạn Python với $n = 10^4$ đã mất khoảng 9 giây nên bằng Python thì cách vét cạn khó qua được Subtask 1. Kết quả lớn nhất $19\,999\,900\,000$ (khi mọi cặp đều tốt) xác nhận phải dùng long long.
Rất vui khi anh em nào đã đọc đến những dòng cuối cùng này. Anh em đọc chắc cũng mỏi mắt luôn chứ 😅 vậy anh em nghĩ người viết ra hết đống ở trên còn thế nào nữa 🤪. Vậy nên đừng tiếc gì cho mình một lời bình luận hoặc chia sẻ bài viết để động viên mình nè. Chúc anh em học tốt và gặt hái nhiều thành công 😇

Xâu đặc biệt 2

Xâu đặc biệt 2
Nguồn: CLUE OJ – Map/Set Độ khó: Khá Tiền tố map<vector<int>, int>

Đề bài

Cho một chuỗi có độ dài $n$ gồm các chữ cái tiếng Anh. Gọi số lượng kí tự phân biệt trong chuỗi này là $k$. Một xâu con (các kí tự liền kề) được gọi là đặc biệt nếu $k$ ký tự khác nhau trong xâu con xuất hiện với số lần bằng nhau.

Viết chương trình đếm số xâu con đặc biệt trong chuỗi đã cho. Kết quả được chia lấy dư cho $10^9 + 7$.

Dữ liệu:

  • Dòng 1: chứa một số nguyên $n$ $(2 \le n \le 10^5)$ – số ký tự trong chuỗi.
  • Dòng 2: chứa một chuỗi $n$ ký tự, gồm các chữ cái tiếng Anh in hoa và in thường. Lưu ý rằng các chữ in hoa và in thường là khác nhau (ví dụ A và a là khác nhau).

Kết quả: dòng 1 ghi số lượng xâu con đặc biệt, chia lấy dư cho $10^9 + 7$.

Giới hạn: thời gian 1 giây, bộ nhớ 128 MB.

Subtask: 50% điểm đầu tiên với $n \le 10^3$; 50% điểm còn lại không có giới hạn gì thêm.

Thầy giải thích lại đề: đầu tiên xác định toàn bộ chuỗi có $k$ ký tự phân biệt (tức là tìm ra $k$ là bao nhiêu). Như ví dụ 1 thì $k = 3$ (chỉ gồm 3 ký tự a, b, c). Rồi ta tìm tất cả các chuỗi con sao cho chuỗi con đó có đúng $k$ ký tự phân biệt bên trong (đủ cả $k$ ký tự đã tìm được), và số lần xuất hiện của mỗi ký tự trong chuỗi con đó đều phải bằng nhau. Ví dụ 1 có 4 chuỗi con thoả mãn:

  1. Vị trí 1 → 3: abc (đủ 3 ký tự a, b, c, mỗi ký tự xuất hiện 1 lần).
  2. Vị trí 1 → 6: abccba (mỗi ký tự xuất hiện 2 lần).
  3. Vị trí 4 → 6: cba (mỗi ký tự xuất hiện 1 lần).
  4. Vị trí 6 → 8: abc (mỗi ký tự xuất hiện 1 lần).
Ví dụ 1 – input
8
abccbabc
Ví dụ 1 – output
4
Ví dụ 2 – input
7
abcABCC
Ví dụ 2 – output
1
Ví dụ 3 – input
20
SwSSSwwwwSwSwwSwwwwS
Ví dụ 3 – output
22

Giải thích ví dụ 2: abcABCC có $k = 6$ ký tự phân biệt; chỉ có chuỗi abcABC (vị trí 1 → 6) chứa đủ 6 ký tự, mỗi ký tự 1 lần.

Lời mở đầu

Đề hơi khó hiểu lúc đầu, nhưng sau khi "dịch" lại thì đây là một bài đếm xâu con rất đẹp. Cách vét cạn $O(n^2)$ khá dễ, còn để chạy $n = 10^5$ ta cần một mẹo quen thuộc: biến điều kiện của đoạn $[l, r]$ thành điều kiện "hai tiền tố giống nhau", rồi dùng map đếm số cặp tiền tố giống nhau.

Dành cho học viên · Ưu tiênNộp bài trên hệ thống CLUE OJ

Link này chỉ học viên đã tham gia tổ chức mới vào được. Học viên nên ưu tiên nộp ở đây vì sẽ được chấm nhanh hơn.

Nộp bài trên CLUE OJ

Lộ trình lời giải, bấm vào từng phần để nhảy tới:

Bộ testBộ test chính thức có gì?

Bộ test có 100 test:

Testnk (số ký tự
phân biệt)
Kiểu chuỗiĐáp án
1–202 – 201 – 6ngẫu nhiên, nhỏ0 – 24
21–271.00010ngẫu nhiên 10 chữ a–e, A–E0 – 6
28–501.0004ngẫu nhiên 4 chữ a, b, A, B176 – 328
51–69100.0004ngẫu nhiên 4 chữ a, b, A, B83.329 – 84.450
70–100100.00022 – 52lặp lại chu kỳ "abc…" gồm k chữ96.105.771 – 227.227.275
  • Có 5 test chỉ gồm 1 loại ký tự ($k = 1$, test 4, 8, 9, 10, 14): khi đó mọi xâu con đều đặc biệt, đáp án là $n(n+1)/2$.
  • Có test đáp án bằng 0 (test 1, 21).
  • Test 70–100 là chuỗi lặp lại abcd… theo chu kỳ $k$ chữ ($k$ giảm dần từ 52 xuống 22): mọi xâu con có độ dài là bội của $k$ đều đặc biệt, nên đáp án rất lớn. Đáp án lớn nhất trong bộ test là 227.227.275, vẫn nhỏ hơn $10^9 + 7$, nên thực tế phép chia lấy dư chưa bao giờ "có tác dụng". Nhưng về lý thuyết đáp án có thể tới $n(n+1)/2 \approx 5 \cdot 10^9$, nên vẫn phải dùng long long và lấy dư.
SubtaskĐiều kiệnCác testCách ăn được
1 (50%)$n \le 10^3$1 – 50Cách 1, 2
2 (50%)$n \le 10^5$51 – 100Cách 2

Cách 1Vét cạn mọi xâu con – O(n² · k)

Với mỗi điểm đầu $l$, ta mở rộng dần điểm cuối $r$ và cập nhật mảng đếm cnt. Sau mỗi lần thêm $s[r]$ ta kiểm tra xem $k$ ký tự có cùng số lần xuất hiện không, bằng cách so từng ký tự với ký tự đầu tiên ch[0].

C++▶ Chạy tay từng dòng codeCách 1: thử mọi xâu con, cập nhật mảng đếm và so k ký tự với ký tự đầu.
Dữ liệu mẫu:
  • Vì so sánh cả $k$ ký tự của chuỗi gốc, nếu xâu con thiếu ký tự nào (đếm = 0) mà ký tự khác có mặt thì sẽ khác nhau → không đếm. Còn trường hợp "tất cả đều bằng 0" không xảy ra vì xâu con luôn có ít nhất 1 ký tự.
  • Mảng cnt[128] đánh số theo mã ASCII nên dùng trực tiếp cnt[s[r]] cho cả chữ hoa lẫn chữ thường.
Không gian$O(n)$
Thời gian$O(n^2 \cdot k)$

Kết quả chạy thật: bản C++ đúng 50/100 test (Subtask 1: 50/50, Subtask 2: 0/50). Subtask 1 chạy tối đa 0.01 giây; với $n = 10^5$ thì $n^2/2 = 5 \cdot 10^9$ cặp $(l, r)$, quá thời gian toàn bộ subtask 2.

Ý tưởngĐoạn đặc biệt = hai tiền tố có cùng "dấu vân tay"

Gọi $P_c(i)$ là số lần ký tự $c$ xuất hiện trong $i$ ký tự đầu tiên của chuỗi (tiền tố độ dài $i$, $P_c(0) = 0$). Xâu con $s[l..r-1]$ (từ vị trí $l$ đến $r - 1$, đánh số từ 0) chứa ký tự $c$ đúng $P_c(r) - P_c(l)$ lần. Xâu con đặc biệt khi các số này bằng nhau với mọi $c$, tức là với $c_0$ là ký tự đầu tiên:

$P_c(r) - P_c(l)$ $= P_{c_0}(r) - P_{c_0}(l) \iff P_c(r) - P_{c_0}(r)$ $= P_c(l) - P_{c_0}(l)$ với mọi $c$.

Dấu vân tay của tiền tố
Với mỗi tiền tố $i$, lập vector $d(i) = \big(P_{c_1}(i) - P_{c_0}(i),\ \ldots,\ P_{c_{k-1}}(i) - P_{c_0}(i)\big)$ gồm $k - 1$ số. Xâu con $s[l..r-1]$ đặc biệt $\iff d(l) = d(r)$. Đáp án = số cặp $l < r$ có $d(l) = d(r)$.

Nếu $d(l) = d(r)$ thì mọi ký tự xuất hiện cùng $t$ lần trong xâu con, và độ dài xâu con là $k \cdot t > 0$ nên $t \ge 1$: xâu con tự động chứa đủ cả $k$ ký tự. Rất gọn!

Đếm số cặp bằng nhau thì quen rồi: duyệt $i$ từ trái sang phải, cộng vào đáp án số tiền tố trước đó có cùng vector (lưu trong map<vector<int>, int>), rồi tăng đếm của vector hiện tại. Nhớ đưa tiền tố rỗng $d(0) = (0, \ldots, 0)$ vào map từ đầu. Em nhập chuỗi rồi bấm Tiếp để xem:

Ví dụ nhanh:

Cách 2map<vector<int>, int> đếm tiền tố – O(n · k · log n)

C++▶ Chạy tay từng dòng codeCách 2: vector chênh lệch d của từng tiền tố, map đếm số tiền tố cùng d.
Dữ liệu mẫu:
  • id[c] đánh số các ký tự phân biệt theo thứ tự xuất hiện $0, 1, \ldots, k-1$, nên ký tự số 0 chính là $c_0$.
  • mp[d] = 1 trước vòng lặp chính là tiền tố rỗng: nhờ nó mà các xâu con bắt đầu từ vị trí 0 (như abc, abccba ở ví dụ 1) được đếm.
  • ans += mp[d] phải đứng trước mp[d]++, để không ghép tiền tố $i$ với chính nó.
  • Khi $k = 1$ thì vector d rỗng, mọi tiền tố đều giống nhau, đáp án là $0 + 1 + 2 + \cdots + n = n(n+1)/2$ – đúng như mong đợi.
  • So sánh 2 vector trong map tốn $O(k)$, mỗi thao tác map có $O(\log n)$ lần so sánh. Với $n = 10^5$, $k \le 52$ là khoảng $10^8$ phép tính đơn giản – vẫn kịp.
Không gian$O(n \cdot k)$
Thời gian$O(n \cdot k \cdot \log n)$

Kết quả chạy thật: bản C++ đúng 100/100 test (Subtask 1: 50/50, Subtask 2: 50/50). Test lâu nhất mất 0.06 giây (thời gian CPU).

Tổng kếtKết quả chạy thật trên bộ test

Mình chấm cả 2 cách (C++ và Python) trên toàn bộ 100 test với giới hạn 1 giây, đo thời gian CPU (máy chấm của mình lúc chạy đang khá tải):

Lời giảiSubtask 1
test 1–50
Subtask 2
test 51–100
TổngCPU lớn nhất
(test đúng)
Cách 1 – C++50/500/5050/1000.01s
Cách 1 – Python50/500/5050/1000.43s
Cách 2 – C++50/5050/50100/1000.06s
Cách 2 – Python50/5050/50100/1000.64s
Ghi chú của mình: cách 2 đạt trọn 100% test. Python: cách 1 đúng 50/100, cách 2 đúng 100/100 test; bản Python cách 2 dùng tuple làm khoá của dict (vì list không băm được). Nếu muốn nhanh hơn nữa có thể thay map bằng unordered_map với hàm băm cho vector, nhưng ở bài này map là đủ.
Rất vui khi anh em nào đã đọc đến những dòng cuối cùng này. Anh em đọc chắc cũng mỏi mắt luôn chứ 😅 vậy anh em nghĩ người viết ra hết đống ở trên còn thế nào nữa 🤪. Vậy nên đừng tiếc gì cho mình một lời bình luận hoặc chia sẻ bài viết để động viên mình nè. Chúc anh em học tốt và gặt hái nhiều thành công 😇