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

Tổng Lớn Nhất Đạt Được Từ Một Hoán Vị Bất Kỳ

Tổng Lớn Nhất Đạt Được Từ Một Hoán Vị Bất Kỳ
Nguồn: Codeforces Group Độ khó: Trung bình Mảng hiệu Tham lam

Đề bài

Cho một mảng số nguyên nums và một mảng các yêu cầu requests với requests[i] = [start_i, end_i]. Yêu cầu thứ $i$ yêu cầu tính tổng:

nums[start_i] + nums[start_i + 1] + … + nums[end_i − 1] + nums[end_i]

Cả start_i và end_i đều được đánh chỉ số từ 0.

Hãy tìm tổng lớn nhất của tất cả các yêu cầu khi xét trên mọi hoán vị có thể có của mảng nums. Vì đáp án có thể rất lớn, hãy trả về kết quả modulo $10^9 + 7$.

Input

  • Dòng đầu tiên chứa một số nguyên $n$ ($1 \le n \le 10^5$) là kích thước của mảng nums.
  • Dòng thứ hai chứa $n$ số nguyên nums[0], nums[1], …, nums[n-1] ($0 \le$ nums[i] $\le 10^5$) là các phần tử của mảng.
  • Dòng thứ ba chứa một số nguyên $q$ ($1 \le q \le 10^5$) là số lượng yêu cầu.
  • $q$ dòng tiếp theo, mỗi dòng chứa hai số nguyên start_i và end_i ($0 \le$ start_i $\le$ end_i $\lt n$) mô tả một yêu cầu.

Output: một số nguyên duy nhất là tổng lớn nhất tìm được sau khi lấy modulo $10^9 + 7$.

Subtask

  • 20% số test đầu tiên có $n, q \le 9$.
  • 50% số test tiếp theo có $n, q \le 10^4$.
  • 30% số test còn lại không có giới hạn gì thêm.
InputOutputGiải thích
5
1 2 3 4 5
2
1 3
0 1
19Một hoán vị của nums là [2,1,3,4,5] cho tổng: yêu cầu [1,3] → 1 + 3 + 4 = 8; yêu cầu [0,1] → 2 + 1 = 3; tổng = 11.
Một hoán vị tối ưu là [3,5,4,2,1] cho tổng: yêu cầu [1,3] → 5 + 4 + 2 = 11; yêu cầu [0,1] → 3 + 5 = 8; tổng = 19.
6
1 2 3 4 5 6
1
0 1
11Hoán vị tối ưu là [6,5,4,3,2,1]. Tổng yêu cầu [0,1] = 6 + 5 = 11.
6
1 2 3 4 5 10
3
0 2
1 3
1 1
47

Lời mở đầu

Bài này mình được một em học viên hỏi và mình đã hướng dẫn chi tiết cách thức giải quyết cho em ấy. Tin rằng sẽ có giá trị với nhiều bạn nên mình chia sẻ đề bài để các bạn thử sức làm và kèm lời giải để bạn nào không giải quyết được thì có thể tham khảo nhen. Hi 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

Tóm tắt đề bài: cho một dãy số $a$ gồm $N$ số nguyên và một mảng request, mỗi request gồm 2 số start và end, ta cần tính tổng a[start] + a[start + 1] + … + a[end], rồi cộng trên toàn bộ request. Xét toàn bộ các hoán vị của mảng $a$, chọn ra hoán vị sao cho tổng này lớn nhất, in phần dư khi chia cho $10^9 + 7$.

100%Ý tưởng: tần suất xuất hiện + sắp xếp

Vì ta tính tổng các giá trị sau toàn bộ truy vấn, nên nếu gọi freq[i] là số lần vị trí $i$ được tính sau toàn bộ các request, thì kết quả chính là tổng các a[i] * freq[i]. Việc đầu tiên là phải build ra được mảng freq.

Kỹ thuật mảng hiệu (difference array)
Với mỗi request (start, end): tăng freq[start] lên 1 đơn vị, giảm freq[end + 1] xuống 1 đơn vị. Sau đó duyệt $i$ từ 1 đến $n - 1$: freq[i] += freq[i - 1]. Như vậy mỗi request chỉ tốn $O(1)$ thay vì phải cộng 1 cho từng vị trí trong đoạn.

Ví dụ 1 với 2 request [1, 3] và [0, 1]:

Vị trí01234
freq sau khi +1 / −1+1+1-10-1
freq sau khi cộng dồn12110

Khi đã có bảng freq rồi, ta cần chọn ra một hoán vị của dãy $a$ sao cho tổng a[i] * freq[i] lớn nhất. Để tổng này lớn nhất thì số lớn nhất cần đặt ở vị trí có tần suất nhiều nhất, số lớn nhì đặt ở vị trí có tần suất lớn nhì, … Do đó ta sắp xếp mảng freq tăng dần, sắp xếp mảng $a$ tăng dần, rồi lấy tổng các a[i] * freq[i] với mọi $i$:

freq (tăng dần)01112
nums (tăng dần)12345
Tích023410
Tổng0 + 2 + 3 + 4 + 10 = 19
Vì sao ghép lớn với lớn là tốt nhất? (mình giải thích thêm)

Giả sử có 2 vị trí với tần suất $f_1 \le f_2$ và 2 số $x \le y$. Đặt $y$ vào vị trí $f_2$ cho tổng $x f_1 + y f_2$, còn đặt ngược lại cho $y f_1 + x f_2$. Hiệu giữa hai cách là $(y - x)(f_2 - f_1) \ge 0$, nên đặt số lớn vào vị trí tần suất lớn không bao giờ tệ hơn. Cứ đổi chỗ từng cặp "ngược" như vậy thì sẽ ra cách ghép đã sắp xếp cùng chiều (đây chính là bất đẳng thức hoán vị).

Em nhập mảng nums và các request (mỗi request 2 số start end) rồi bấm Tiếp để xem từng bước: đánh dấu mảng hiệu, cộng dồn ra tần suất, rồi sắp xếp ghép lớn với lớn:

Ví dụ nhanh:

Đánh giá độ phức tạp: $O(N \log N)$ cho 2 lần sắp xếp, cộng $O(q)$ cho việc xử lý request.

Source code đã nộp thành công:

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

const long long MOD = 1000000007;
long long freq[100005];

int maxSumRangeQuery(vector<int> &nums, vector<vector<int>> &requests){
    int n = (int)nums.size();

    // Reset mảng freq
    memset(freq, 0, sizeof(freq));

    // Áp dụng kỹ thuật mảng hiệu (difference array)
    for(int i = 0; i < (int)requests.size(); i++){
        freq[requests[i][0]]++;
        freq[requests[i][1] + 1]--;
    }

    // Tính tần suất thực tế bằng tổng tích lũy
    for(int i = 1; i < n; i++){
        freq[i] += freq[i - 1];
    }

    // Sắp xếp tần suất và mảng nums
    sort(freq, freq + n);
    sort(nums.begin(), nums.end());

    // Tính tổng: ghép số lớn nhất với tần suất lớn nhất
    long long ans = 0LL;
    for(int i = 0; i < n; i++){
        ans = (ans + (1LL * nums[i] * freq[i]) % MOD) % MOD;
    }

    return (int)ans;
}

int main(){
    ios::sync_with_stdio(0);
    cin.tie(0);

    int n, q;

    // Đọc mảng nums
    cin >> n;
    vector<int> nums(n);
    for(int i = 0; i < n; i++){
        cin >> nums[i];
    }

    // Đọc số lượng requests
    cin >> q;
    vector<vector<int>> requests(q, vector<int>(2));
    for(int i = 0; i < q; i++){
        cin >> requests[i][0] >> requests[i][1];
    }

    // Tính kết quả
    int result = maxSumRangeQuery(nums, requests);

    // In kết quả
    cout << result << endl;

    return 0;
}
import sys

MOD = 1000000007

def main():
    data = sys.stdin.buffer.read().split()
    n = int(data[0])
    nums = list(map(int, data[1:1 + n]))
    q = int(data[1 + n])
    freq = [0] * (n + 1)

    # Kỹ thuật mảng hiệu (difference array)
    pos = 2 + n
    for _ in range(q):
        start = int(data[pos]); end = int(data[pos + 1]); pos += 2
        freq[start] += 1
        freq[end + 1] -= 1

    # Tính tần suất thực tế bằng tổng tích lũy
    for i in range(1, n):
        freq[i] += freq[i - 1]

    # Sắp xếp tần suất và mảng nums, ghép số lớn nhất với tần suất lớn nhất
    freq = sorted(freq[:n])
    nums.sort()
    ans = 0
    for i in range(n):
        ans = (ans + nums[i] * freq[i]) % MOD
    print(ans)

main()
C++▶ Chạy tay từng dòng codeMảng hiệu tính tần suất từng vị trí, sắp xếp 2 mảng rồi ghép lớn với lớn, O(N log N).
Dữ liệu mẫu:
1#include <bits/stdc++.h>
2using namespace std;
3
4const long long MOD = 1000000007;
5long long freq[100005];
6
7int maxSumRangeQuery(vector<int> &nums, vector<vector<int>> &requests){
8 int n = (int)nums.size();
9
10 // Reset mảng freq
11 memset(freq, 0, sizeof(freq));
12
13 // Áp dụng kỹ thuật mảng hiệu (difference array)
14 for(int i = 0; i < (int)requests.size(); i++){
15 freq[requests[i][0]]++;
16 freq[requests[i][1] + 1]--;
17 }
18
19 // Tính tần suất thực tế bằng tổng tích lũy
20 for(int i = 1; i < n; i++){
21 freq[i] += freq[i - 1];
22 }
23
24 // Sắp xếp tần suất và mảng nums
25 sort(freq, freq + n);
26 sort(nums.begin(), nums.end());
27
28 // Tính tổng: ghép số lớn nhất với tần suất lớn nhất
29 long long ans = 0LL;
30 for(int i = 0; i < n; i++){
31 ans = (ans + (1LL * nums[i] * freq[i]) % MOD) % MOD;
32 }
33
34 return (int)ans;
35}
36
37int main(){
38 ios::sync_with_stdio(0);
39 cin.tie(0);
40
41 int n, q;
42
43 // Đọc mảng nums
44 cin >> n;
45 vector<int> nums(n);
46 for(int i = 0; i < n; i++){
47 cin >> nums[i];
48 }
49
50 // Đọc số lượng requests
51 cin >> q;
52 vector<vector<int>> requests(q, vector<int>(2));
53 for(int i = 0; i < q; i++){
54 cin >> requests[i][0] >> requests[i][1];
55 }
56
57 // Tính kết quả
58 int result = maxSumRangeQuery(nums, requests);
59
60 // In kết quả
61 cout << result << endl;
62
63 return 0;
64}
Lưu ý nhỏ: tổng thật có thể lên tới khoảng $10^5 \cdot 10^5 \cdot 10^5 = 10^{15}$ nên phải dùng long long và lấy modulo trong lúc cộng như code ở trên. Lấy modulo sau khi đã chọn được hoán vị tốt nhất là đúng, vì việc so sánh "tổng nào lớn hơn" được làm trên giá trị thật (thông qua sắp xếp), không phải trên giá trị đã modulo.

Mình đã kiểm tra code với 150 bộ test ngẫu nhiên so với cách thử mọi hoán vị, đều cho cùng kết quả. Với $n = q = 10^5$, bản C++ chạy khoảng 0.04 giây, bản Python mình viết thêm khoảng 0.12 giây.

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 😇

Số Lượng Cách Xóa

Số Lượng Cách Xóa
Nguồn: Codeforces Group Độ khó: Trung bình Prefix sum Modulo

Đề bài

Cho số tự nhiên $N$. Bằng cách giữ nguyên hoặc xoá đi một số chữ số liên tiếp của $N$ (nhưng không xoá hết) ta nhận được một số mới; nếu số $N$ bị chia thành 2 phần thì số mới được ghép lại từ hai phần này và giữ nguyên trật tự.

Yêu cầu: hãy xác định tất cả số cách xoá như trên để số $N$ mới sau khi xoá chia hết cho 3. Lưu ý là hai vị trí khác nhau sẽ tạo ra hai cách xoá khác nhau. Số $N$ giữ nguyên cũng được coi là một cách xoá.

Dữ liệu vào: số nguyên dương $N$ (không quá $10^5$ chữ số).

Dữ liệu ra: ghi ra một số nguyên là số cách xoá tìm được.

Giới hạn

  • Có 50% số test có số các chữ số của $N \le 300$.
  • Có 25% số test tiếp theo có số các chữ số của $N \le 10^4$.
  • Có 25% số test còn lại có số các chữ số của $N \le 10^5$.
Dữ liệu vàoDữ liệu raGiải thích
10054Các số mới là: 1005, 105, 105, 15
20093Các số mới là: 009, 09, 9

Lời mở đầu

Bài này mình được một em học viên hỏi và mình đã hướng dẫn chi tiết cách thức giải quyết cho em ấy. Tin rằng sẽ có giá trị với nhiều bạn nên mình chia sẻ đề bài để các bạn thử sức làm, kèm thêm đó là toàn bộ tin nhắn của mình với em ấy để bạn nào không giải quyết được thì có thể tham khảo nhen. Hi 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:

Gợi ý2 kiến thức cần có

HV
“anh cho e xin hướng tối ưu 100% vs ạ, e làm đc 75% rồi”

Ok, cần 2 kiến thức để làm:

  1. Kiến thức về prefix sum (mảng cộng dồn): cứ tính tổng cộng dồn, rồi khi cần biết tổng trong đoạn từ $L$ đến $R$ thì gọi prefixSum[R] - prefixSum[L-1], cái này thì em biết rồi hen.
  2. Kiến thức về các phép toán chia dư:
    (a + b) % c = ((a % c) + (b % c)) % c
    (a − b) % c = ((a % c) − (b % c)) % c
    (a · b) % c = ((a % c) · (b % c)) % c
    Không có phép chia nhé, chỉ có cộng, trừ, nhân.

Thì trong bài này là cần chia dư cho 3 (% 3) để xem kết quả có bằng 0 không. Vấn đề là chuỗi có chiều dài rất lớn (tới $10^5$ chữ số), không thể nào chuyển chuỗi đó về số rồi dùng phép % được, mà phải dựa theo tính chất: tổng các chữ số mà chia hết cho 3 thì số đó chia hết cho 3. Điều này dễ nhận ra thôi, em cứ lấy số 3 nhân với một số bất kỳ rồi cộng tổng các chữ số của kết quả lại, sẽ thấy tổng đó chia hết cho 3. Ví dụ $3 \times 123987 = 371961$, tổng $3 + 7 + 1 + 9 + 6 + 1 = 27$ chia hết cho 3.

75%Cách vét cạn: thử mọi đoạn xoá – O(n²)

Có phải em sẽ nhận ra cách làm vét cạn là gì: ví dụ chuỗi có chiều dài 4, tức là index từ 0 đến 3, thì sẽ xét các đoạn liên tiếp từ $i$ đến $j$ để xoá như sau: (0,0), (0,1), (0,2), (0,3), (1,1), (1,2), (1,3), (2,2), (2,3), (3,3). Lưu ý không lấy trường hợp (0,3) vì đề có nói không cho xoá hết toàn bộ chuỗi.

Với mỗi đoạn, em tính sum là tổng toàn bộ chữ số của chuỗi ban đầu, sau đó trừ đi tổng đoạn bị xoá, rồi xem phần còn lại có chia hết cho 3 không:

sum − (prefixSum[j] − prefixSum[i − 1])  có chia hết cho 3?

Bảng dưới đây là toàn bộ các cách xoá của ví dụ $N = 1005$, có đúng 4 cách cho ra số chia hết cho 3:

Đoạn xoáN (gạch = bị xoá)Số mớiTổng chữ sốChia hết 3
giữ nguyên100510056✔
i = 0, j = 010050055
i = 0, j = 11005055
i = 0, j = 2100555
i = 1, j = 110051056✔
i = 1, j = 21005156✔
i = 1, j = 3100511
i = 2, j = 210051056✔
i = 2, j = 31005101
i = 3, j = 310051001
Số cách4

Cách vét cạn này là $O(\text{Length}_s^2)$, với độ dài $10^5$ thì quá chậm. Bài này có thể giải quyết với độ phức tạp $O(\text{Length}_s)$ nha em, tức là chỉ lặp $10^5$ lần là xong, đảm bảo trong 1 giây. Anh gợi ý đến đây rồi đó, ráng nghĩ tiếp em nhé, nếu vẫn không ra thì nói anh sẽ nói sâu thêm.

100%Cách tối ưu: đếm theo số dư – O(n)

HV
“A ơi a nói sâu hơn 1 chút cho e vs”

Ok, vẫn chưa ra hả em, xem tiếp nhé:

sum − (prefixSum[j] − prefixSum[i − 1])  ⇔  sum + prefixSum[i − 1] − prefixSum[j]

Nếu nhóm sum + prefixSum[i-1] lại thành 1 nhóm gọi là $A$, và prefixSum[j] là $B$, thì giờ đây nó thành $A - B$. Để $A - B$ chia hết cho 3, tức là $(A - B) \% 3 = 0$. Còn nhớ phép toán chia lấy dư anh nói không? $(a - b) \% c$ $= ((a \% c) - (b \% c)) \% c$, với $c$ ở đây là 3, $a$ chính là $A$, $b$ chính là $B$.

Vậy em hiểu là sao? Là tính $A \% 3$, tức là (sum + prefixSum[i-1]) % 3 được bao nhiêu – cái này tính được vì sum đã có, prefixSum của từng vị trí mình cũng đã có. Kết quả khi % 3 chỉ có thể là 0, 1 hoặc 2:

  • Giả sử $A \% 3 = 0$, thì để $(A - B) \% 3 = 0$, $B \% 3$ cũng phải bằng 0, để $0 - 0 = 0$.
  • Nếu $A \% 3 = 1$ thì $B \% 3$ cũng phải bằng 1 để 2 thằng trừ nhau về 0. Tương tự với 2.
Mấu chốt
Tính $x$ = (sum + prefixSum[i-1]) % 3. Thì từ vị trí $j = i$ đến cuối dãy có bao nhiêu vị trí mà prefixSum[j] % 3 cũng bằng $x$, thì có bấy nhiêu cách xoá đoạn bắt đầu tại $i$ thỏa mãn. Ta chỉ cần 1 mảng cnt[3] đếm số vị trí $j$ (từ $i$ trở về sau) có prefixSum[j] % 3 bằng 0, 1, 2.

Em nhập số $N$ rồi bấm Tiếp để xem từng bước: với mỗi vị trí bắt đầu xoá $i$, tính $x$ = (sum + prefixSum[i-1]) % 3 rồi đếm các vị trí $j \ge i$ có prefixSum[j] % 3 = x (ô xanh), kèm luôn các số mới tạo ra:

Ví dụ nhanh:

Source code theo đúng tư tưởng anh đã nói ở trên, đã nộp AC trên web, em tham khảo hen:

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

long long solve(string &s){
    s = " " + s;
    long long prefixSum[s.length()] = {0}, sum = 0;
    int cnt[3] = {0};
    for(int i = 1; i < s.length(); ++i){
        int digit = s[i] - '0';
        sum += digit;
        prefixSum[i] = prefixSum[i - 1] + digit;
        cnt[prefixSum[i] % 3]++;
    }
    long long res = 0;
    for(int i = 1; i < s.length(); ++i){
        res += cnt[(sum + prefixSum[i - 1]) % 3];
        cnt[prefixSum[i] % 3]--;
    }
    if(sum % 3 != 0){
        res--;
    }
    return res;
}

int main(){
    ios::sync_with_stdio(0);
    cin.tie(0);
    string s;
    cin >> s;
    cout << solve(s);
    return 0;
}
def solve(s):
    s = " " + s
    prefix_sum = [0] * len(s)
    total_sum = 0
    cnt = [0, 0, 0]

    for i in range(1, len(s)):
        digit = int(s[i])
        total_sum += digit
        prefix_sum[i] = prefix_sum[i - 1] + digit
        cnt[prefix_sum[i] % 3] += 1

    res = 0
    for i in range(1, len(s)):
        res += cnt[(total_sum + prefix_sum[i - 1]) % 3]
        cnt[prefix_sum[i] % 3] -= 1

    if total_sum % 3 != 0:
        res -= 1

    return res

s = input().strip()
print(solve(s))
C++▶ Chạy tay từng dòng codeĐếm theo số dư: prefix sum + mảng cnt[3], O(n).
Dữ liệu mẫu:
1#include <bits/stdc++.h>
2using namespace std;
3
4long long solve(string &s){
5 s = " " + s;
6 long long prefixSum[s.length()] = {0}, sum = 0;
7 int cnt[3] = {0};
8 for(int i = 1; i < s.length(); ++i){
9 int digit = s[i] - '0';
10 sum += digit;
11 prefixSum[i] = prefixSum[i - 1] + digit;
12 cnt[prefixSum[i] % 3]++;
13 }
14 long long res = 0;
15 for(int i = 1; i < s.length(); ++i){
16 res += cnt[(sum + prefixSum[i - 1]) % 3];
17 cnt[prefixSum[i] % 3]--;
18 }
19 if(sum % 3 != 0){
20 res--;
21 }
22 return res;
23}
24
25int main(){
26 ios::sync_with_stdio(0);
27 cin.tie(0);
28 string s;
29 cin >> s;
30 cout << solve(s);
31 return 0;
32}

Vì sao lại có dòng cnt[prefixSum[i] % 3]--?

HV
“Em ko hiểu chỗ cnt[prefixSum[i]%3]-- ạ”

Em cứ lấy vài cái ví dụ chạy tay quy trình logic code xử lý là em sẽ hiểu rõ nha. Ban đầu mảng cnt đang đếm cho toàn bộ các vị trí $j$ từ 1 đến $n$. Ở vị trí $i$, ta cộng vào đáp án cnt[(sum + prefixSum[i - 1]) % 3], tức là số vị trí $j \ge i$ thỏa mãn. Rồi khi tiếp tục i++ để xét vị trí $i$ mới, thì vị trí $j = i$ cũ không còn hợp lệ nữa (vì $j$ phải $\ge i$ mới), nên phải trừ nó ra khỏi mảng cnt, em hình dung chưa nè.

Vì sao cuối cùng lại có if(sum % 3 != 0) res--?

HV
“a giải thích giúp e chỗ cuối cùng: if(sum % 3 != 0){ res--; } với”

Em xem nhé, đề không cho xoá hết, nên trường hợp $i = 1$, $j = n$ là không được lấy đúng không? Tuy nhiên code ở trên vẫn cứ tính tất cả mọi trường hợp kể cả trường hợp đó. Khi đó prefixSum[i-1] = prefixSum[0] = 0, còn prefixSum[n] chính là sum, nên $sum - sum = 0$ luôn chia hết cho 3, tức là nó luôn bị tính thêm 1 lần vào đáp án.

Sau đó đề bài có nói: giữ nguyên chuỗi ban đầu (không xoá gì) vẫn xem là 1 cách xoá. Vậy nếu sum chia hết cho 3 thì phải tính thêm 1 lần vào đáp án; mà đáp án vốn đã bị cộng dư 1 lần như anh nói ở trên rồi, nên khi sum chia hết cho 3 thì xem như không làm gì cả. Còn nếu sum không chia hết cho 3 thì đáp án không được cộng thêm, nhưng nó đã bị cộng dư 1 trước đó, nên lúc này phải trừ đi 1 thì mới chính xác. Nói chung để code rõ nghĩa hơn thì chỗ đó có thể viết thế này:

res--;
if(sum % 3 == 0){
    res++;
}

Tức là res cứ giảm đi 1 trước đã vì đề không cho xoá hết, rồi sau đó nếu sum chia hết cho 3 thì vẫn tính vào đáp án vì đề nói rõ không xoá gì thì vẫn là 1 cách xoá.

Mình đã kiểm tra cả 2 bản code với 300 số ngẫu nhiên so với cách vét cạn, đều cho cùng kết quả; với $N$ dài $10^5$ chữ số, bản C++ chạy chưa tới 0.01 giây, bản Python khoảng 0.06 giây. Lưu ý đáp án có thể lên tới khoảng $n^2 / 2 \approx 5 \cdot 10^9$, vượt int, nên res phải để kiểu long long như trong code nhé.

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 😇