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
Thi thử miễn phí
🏆 Thi thử HSG Tin học tuần này trên ClueOJ
Mở công khai cho mọi người · đề do thầy Nguyễn Việt Nam Sơn biên soạn
Lịch thi, đề cũ & lời giải →

Trò Chơi (HSG12 Hà Nội 2025 – Bài 4)

Trò Chơi (HSG12 Hà Nội 2025 – Bài 4)
Nguồn: Đề thi HSG lớp 12 Hà Nội 2025 (CLUE OJ, SQRTOJ) Độ khó: Khó Quy hoạch động Chia để trị Mảng cộng dồn Tham lam

Đề bài

Bài IV. Trò chơi (3,0 điểm)

Công ty của Chiến tổ chức trò chơi chọn số trong buổi tiệc cuối năm. Ban tổ chức chuẩn bị sẵn một bộ số gồm $N$ số nguyên $A_1, A_2, \ldots, A_N$ và $N$ tấm thẻ. Trên mỗi tấm thẻ ghi một số tự nhiên có giá trị từ $1$ đến $N$, các giá trị trên thẻ đôi một khác nhau. Ban tổ chức sẽ phát cho mỗi người chơi một tấm thẻ bất kỳ trong $N$ tấm thẻ trên. Luật chơi đưa ra như sau:

  • Giả sử người chơi nhận được tấm thẻ ghi số nguyên $K$;
  • Sau đó, người chơi chọn ra tối đa $K$ đoạn con trên bộ số ban đầu mà Ban tổ chức đã chuẩn bị, mỗi đoạn gồm một hoặc nhiều phần tử liên tiếp, các đoạn con không có phần tử chung. Người chơi có quyền không chọn đoạn con nào;
  • Điểm của người chơi là tổng các số trong các đoạn đã chọn. Ban tổ chức sẽ trao phần thưởng tương ứng với số điểm mà người chơi đạt được.

Yêu cầu: Có $Q$ người chơi khác nhau. Em hãy lập trình đưa ra số điểm lớn nhất mà mỗi người chơi có thể nhận được.

Dữ liệu vào từ file văn bản TROCHOI.INP:

  • Dòng đầu tiên chứa hai số nguyên $N, Q$ $(1 \le Q \le N \le 10^5)$ tương ứng số lượng phần tử trong bộ số và số lượng người tham gia trò chơi;
  • Dòng thứ hai chứa $N$ số nguyên, số nguyên thứ $i$ biểu diễn giá trị của $A_i$ $(-10^4 \le A_i \le 10^4)$;
  • Dòng thứ ba chứa $Q$ số nguyên, số nguyên thứ $i$ là $K_i$ $(1 \le K_i \le N)$ mô tả số ghi trên tấm thẻ của người chơi thứ $i$. Các $K_i$ đảm bảo phân biệt và được sắp xếp theo thứ tự tăng dần.

Kết quả ghi ra file văn bản TROCHOI.OUT: Gồm $Q$ dòng, dòng thứ $i$ tương ứng là kết quả của người chơi thứ $i$ khi được phát tấm thẻ ghi số $K_i$.

Ràng buộc:

  • Có 20% số test ứng với 20% số điểm có $N \le 10^5$; $Q = 1$; $K = 1$;
  • 20% số test khác ứng với 20% số điểm có $N \le 10^5$; $Q = 1$; $K \le 2$;
  • 20% số test khác ứng với 20% số điểm có $N \le 10^5$; $Q = 1$; $K \le 50$;
  • 20% số test khác ứng với 20% số điểm có $N \le 10^5$; $Q = 1$; $K \le N$;
  • 20% số test còn lại ứng với 20% số điểm không có ràng buộc gì thêm.

Lưu ý: đề gốc đọc/ghi file TROCHOI.INP / TROCHOI.OUT; trên CLUE OJ thì đọc từ bàn phím, in ra màn hình (giới hạn trên CLUE: 1 giây, 1 GB).

TROCHOI.INP
5 3
1 -1 2 -2 3
1 2 5
TROCHOI.OUT
3
5
6
Giải thích
– Với $K_1 = 1$, chọn đoạn $[5, 5]$;
– Với $K_2 = 2$, chọn đoạn $[3, 3]$ và đoạn $[5, 5]$;
– Với $K_3 = 5$, chọn đoạn $[1, 1]$, $[3, 3]$ và $[5, 5]$.

Lời mở đầu

Đây là bài 4 – bài cuối và cũng là bài khó nhất trong đề thi học sinh giỏi Tin học lớp 12 thành phố Hà Nội năm 2025. Đề bài phát biểu rất gọn: chọn tối đa $K$ đoạn con không giao nhau sao cho tổng lớn nhất, nhưng để ăn trọn 100% điểm thì cần tới kỹ thuật chia để trị kết hợp tổng Minkowski của hàm lồi – một kỹ thuật khá "nặng đô" với học sinh. Vì vậy mình viết lời giải đi từng bậc theo subtask: subtask 1 là bài toán đoạn con có tổng lớn nhất quen thuộc, subtask 2 mở rộng ra 2 đoạn, subtask 3 dùng quy hoạch động, và cuối cùng là lời giải đầy đủ. Bạn nào đi thi chỉ cần làm chắc 3 subtask đầu là đã có 60% điểm bài này rồi. 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é:

Link nộp bài

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 OJ

Mình giải thích ví dụ để bạn nào chưa hiểu đề sẽ hiểu rõ. Dãy số là $1, -1, 2, -2, 3$ (vị trí từ $1$ đến $5$):

Thẻ KCách chọn tốt nhấtĐiểm
$K = 1$Chỉ 1 đoạn: $[5, 5]$ (chỉ lấy số $3$). Các đoạn dài hơn như $[1, 5]$ chỉ được $1 - 1 + 2 - 2 + 3 = 3$, không hơn.$3$
$K = 2$$[3, 3]$ và $[5, 5]$: $2 + 3$.$5$
$K = 5$$[1, 1]$, $[3, 3]$, $[5, 5]$: $1 + 2 + 3$. Dù được chọn tới $5$ đoạn, nhưng chọn thêm đoạn nào chứa số âm cũng chỉ làm giảm điểm, nên chỉ chọn $3$ đoạn (đề cho phép chọn tối đa $K$ đoạn).$6$
Hai chữ "tối đa" quan trọng
Được chọn không quá $K$ đoạn, và được phép không chọn đoạn nào (điểm $0$). Vậy đáp án luôn $\ge 0$: nếu cả dãy toàn số âm thì đáp án là $0$, chứ không phải số âm lớn nhất đâu nha.

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

Trong cả bài, mình dùng mảng cộng dồn $pre[i] = A_1 + A_2 + \cdots + A_i$ (với $pre[0] = 0$). Khi đó tổng đoạn $[l, r]$ là $pre[r] - pre[l - 1]$.

Subtask 1K = 1: đoạn con có tổng lớn nhất – O(N)

Với $K = 1$, bài toán chuyển về tìm đoạn con có tổng lớn nhất trong mảng $A$. Đối với mỗi vị trí $i$ $(1 \le i \le n)$ là điểm kết thúc của đoạn, ta sẽ tìm vị trí bắt đầu $j$ sao cho $pre[i] - pre[j - 1]$ là lớn nhất có thể.

Vì $pre[i]$ đã cố định, muốn hiệu lớn nhất thì $pre[j - 1]$ phải nhỏ nhất. Nghĩa là $j$ tối ưu sẽ có $pre[j - 1]$ nhỏ nhất trong khoảng $j - 1$ từ $0$ đến $i - 1$. Giá trị nhỏ nhất này rất dễ cập nhật sau mỗi lần tăng $i$: chỉ cần một biến mn, xét xong vị trí $i$ thì mn = min(mn, pre[i]).

Công thức
$res = \max\limits_{1 \le i \le n} \Big( pre[i] - \min\limits_{0 \le t \le i - 1} pre[t] \Big)$, rồi lấy $\max(res, 0)$ vì được phép không chọn đoạn nào.

Minh hoạ: quét mảng cộng dồn

Các bạn nhập dãy $A$ rồi bấm Tiếp. Ô viền cam là $pre[i]$ đang xét, ô tím là $pre[t]$ nhỏ nhất phía trước, các ô xanh trên hàng $A$ là đoạn tốt nhất tìm được đến lúc này:

Ví dụ nhanh:

Source code để các bạn tham khảo nếu làm theo hướng này:

#include<bits/stdc++.h>
#define int long long
using namespace std;
int n, q;
int a[100005];
int k[100005];
int pre[100005];
namespace sub1{
    void solve(){
        for (int i = 1; i <= q; i ++){
            int res = 0; // được phép không chọn đoạn nào
            int mn = 0;
            for (int j = 1; j <= n; j ++){
                res = max(res, pre[j] - mn);
                mn = min(mn, pre[j]);
            }
            cout << res << endl;
        }
    }
};
signed main(){
    if(fopen("TROCHOI.INP", "r")){
        freopen("TROCHOI.INP", "r", stdin);
        freopen("TROCHOI.OUT", "w", stdout);
    }
    cin >> n >> q;
    for (int i = 1; i <= n; i ++){
        cin >> a[i];
    }
    for (int i = 1; i <= q; i ++){
        cin >> k[i];
    }
    for (int i = 1; i <= n; i ++) pre[i] = pre[i - 1] + a[i];
    sub1::solve();
}
import sys

data = sys.stdin.buffer.read().split()
n, q = int(data[0]), int(data[1])
a = [0] + [int(x) for x in data[2:2 + n]]
k = [0] + [int(x) for x in data[2 + n:2 + n + q]]
pre = [0] * (n + 1)
for i in range(1, n + 1):
    pre[i] = pre[i - 1] + a[i]

out = []
for i in range(1, q + 1):
    res = 0  # được phép không chọn đoạn nào
    mn = 0
    for j in range(1, n + 1):
        res = max(res, pre[j] - mn)
        mn = min(mn, pre[j])
    out.append(res)
print("\n".join(map(str, out)))
Lưu ý nhỏ về code
Trong bản code đầu tiên mình đăng, biến res được khởi tạo bằng -1e18, nên nếu dãy toàn số âm thì code in ra số âm lớn nhất (ví dụ -2 với dãy -5 -2 -7). Nhưng đề cho phép không chọn đoạn nào, nên đáp án đúng là 0. Khi soạn lại bài này mình đã sửa thành res = 0. Code này chỉ dành cho $K = 1$ (nó không đọc tới giá trị $K$).
C++▶ Chạy tay từng dòng codeSubtask 1 (K = 1): với mỗi i, res = max(res, pre[i] - mn), mn là pre nhỏ nhất phía trước.
Dữ liệu mẫu:
1#include<bits/stdc++.h>
2#define int long long
3using namespace std;
4int n, q;
5int a[100005];
6int k[100005];
7int pre[100005];
8namespace sub1{
9 void solve(){
10 for (int i = 1; i <= q; i ++){
11 int res = 0; // được phép không chọn đoạn nào
12 int mn = 0;
13 for (int j = 1; j <= n; j ++){
14 res = max(res, pre[j] - mn);
15 mn = min(mn, pre[j]);
16 }
17 cout << res << endl;
18 }
19 }
20};
21signed main(){
22 if(fopen("TROCHOI.INP", "r")){
23 freopen("TROCHOI.INP", "r", stdin);
24 freopen("TROCHOI.OUT", "w", stdout);
25 }
26 cin >> n >> q;
27 for (int i = 1; i <= n; i ++){
28 cin >> a[i];
29 }
30 for (int i = 1; i <= q; i ++){
31 cin >> k[i];
32 }
33 for (int i = 1; i <= n; i ++) pre[i] = pre[i - 1] + a[i];
34 sub1::solve();
35}

Đánh giá độ phức tạp của cách làm này: mỗi truy vấn duyệt mảng một lần, mà subtask 1 có $Q = 1$.

Không gian$O(N)$
Thời gian$O(N)$

Subtask 2K ≤ 2: hai đoạn không giao nhau – O(N)

Với $K = 2$, chúng ta cần tìm (tối đa) hai đoạn con không giao nhau sao cho có tổng lớn nhất. Vậy đối với mỗi $i$, ta sẽ coi $i$ là vị trí bắt đầu của đoạn con thứ 2. Khi này, tổng tối ưu có thể đạt được bằng:

  • Vế 1: tổng của đoạn con lớn nhất mà đầu cuối của nó nhỏ hơn $i$ (nằm gọn trong $[1, i - 1]$). Vế này giống hệt subtask 1: ta duy trì biến mx_sum = tổng đoạn con lớn nhất trong $[1, i - 1]$, cập nhật sau mỗi lần tăng $i$.
  • Vế 2: tổng của đoạn con lớn nhất mà bắt đầu tại $i$, tức là ta cần tìm $j$ trong khoảng từ $i$ đến $n$ sao cho $pre[j] - pre[i - 1]$ lớn nhất. Đến đây ta tính trước $mx\_pre[i] = \max(pre[i], pre[i + 1], \ldots, pre[n])$ bằng cách duyệt ngược từ $n$ về và cập nhật mỗi lần giảm chỉ số.
Công thức
$res = \max\limits_{2 \le i \le n} \Big( mx\_sum(i - 1) + mx\_pre[i] - pre[i - 1] \Big)$, với $mx\_pre[i] = \max(mx\_pre[i + 1], pre[i])$ và $mx\_pre[n + 1] = -\infty$.
Cuối cùng còn phải so với trường hợp chỉ chọn 1 đoạn và không chọn đoạn nào.
Lưu ý: code gốc có 3 chỗ mình đã sửa
  1. Bản đầu mình viết mx_pre[j + 1] - pre[j - 1] và để mx_pre[n + 1] = 0 (mảng toàn cục). Như vậy đoạn thứ 2 bắt đầu tại $j$ nhưng lại kết thúc từ $j + 1$ trở đi, và còn "kết thúc tại $n + 1$" với giá trị $0$ – sai. Ví dụ dãy 5 -100 3, $K = 2$: đúng là $8$ nhưng code cũ in $100$. Đúng theo phân tích phải là mx_pre[j] và mx_pre[n + 1] = -1e18.
  2. Subtask 2 có $K \le 2$, tức là có cả test $K = 1$; code cũ luôn ghép 2 đoạn. Giờ chỉ ghép khi k[i] >= 2.
  3. Được chọn ít hơn $K$ đoạn (1 đoạn hoặc không đoạn nào), nên res bắt đầu từ $0$ và cuối cùng lấy thêm max(res, mx_sum).

Source code để các bạn tham khảo nếu làm theo hướng này:

#include<bits/stdc++.h>
#define int long long
using namespace std;
int n, q;
int a[100005];
int k[100005];
int pre[100005];
namespace sub2{
    int mx_pre[100005];
    void solve(){
        for (int i = 1; i <= q; i ++){
            int res = 0; // được phép không chọn đoạn nào
            int mn = 0;
            int mx_sum = -1e18;
            mx_pre[n + 1] = -1e18;
            for (int j = n; j >= 1; j --){
                mx_pre[j] = max(mx_pre[j + 1], pre[j]);
            }
            for (int j = 1; j <= n; j ++){
                if (k[i] >= 2) res = max(res, mx_sum + mx_pre[j] - pre[j - 1]);
                mx_sum = max(mx_sum, pre[j] - mn);
                mn = min(mn, pre[j]);
            }
            res = max(res, mx_sum); // chỉ chọn 1 đoạn
            cout << res << endl;
        }
    }
};
signed main(){
    if(fopen("TROCHOI.INP", "r")){
        freopen("TROCHOI.INP", "r", stdin);
        freopen("TROCHOI.OUT", "w", stdout);
    }
    cin >> n >> q;
    for (int i = 1; i <= n; i ++){
        cin >> a[i];
    }
    for (int i = 1; i <= q; i ++){
        cin >> k[i];
    }
    for (int i = 1; i <= n; i ++) pre[i] = pre[i - 1] + a[i];
    sub2::solve();
}
import sys

NEG = -10**18
data = sys.stdin.buffer.read().split()
n, q = int(data[0]), int(data[1])
a = [0] + [int(x) for x in data[2:2 + n]]
k = [0] + [int(x) for x in data[2 + n:2 + n + q]]
pre = [0] * (n + 1)
for i in range(1, n + 1):
    pre[i] = pre[i - 1] + a[i]

out = []
mx_pre = [NEG] * (n + 2)
for i in range(1, q + 1):
    res = 0  # được phép không chọn đoạn nào
    mn = 0
    mx_sum = NEG
    for j in range(n, 0, -1):
        mx_pre[j] = max(mx_pre[j + 1], pre[j])
    for j in range(1, n + 1):
        if k[i] >= 2:
            res = max(res, mx_sum + mx_pre[j] - pre[j - 1])
        mx_sum = max(mx_sum, pre[j] - mn)
        mn = min(mn, pre[j])
    res = max(res, mx_sum)  # chỉ chọn 1 đoạn
    out.append(res)
print("\n".join(map(str, out)))
C++▶ Chạy tay từng dòng codeSubtask 2 (K ≤ 2): coi j là đầu đoạn thứ 2, ghép đoạn tốt nhất trong [1, j-1] với đoạn tốt nhất bắt đầu tại j.
Dữ liệu mẫu:
1#include<bits/stdc++.h>
2#define int long long
3using namespace std;
4int n, q;
5int a[100005];
6int k[100005];
7int pre[100005];
8namespace sub2{
9 int mx_pre[100005];
10 void solve(){
11 for (int i = 1; i <= q; i ++){
12 int res = 0; // được phép không chọn đoạn nào
13 int mn = 0;
14 int mx_sum = -1e18;
15 mx_pre[n + 1] = -1e18;
16 for (int j = n; j >= 1; j --){
17 mx_pre[j] = max(mx_pre[j + 1], pre[j]);
18 }
19 for (int j = 1; j <= n; j ++){
20 if (k[i] >= 2) res = max(res, mx_sum + mx_pre[j] - pre[j - 1]);
21 mx_sum = max(mx_sum, pre[j] - mn);
22 mn = min(mn, pre[j]);
23 }
24 res = max(res, mx_sum); // chỉ chọn 1 đoạn
25 cout << res << endl;
26 }
27 }
28};
29signed main(){
30 if(fopen("TROCHOI.INP", "r")){
31 freopen("TROCHOI.INP", "r", stdin);
32 freopen("TROCHOI.OUT", "w", stdout);
33 }
34 cin >> n >> q;
35 for (int i = 1; i <= n; i ++){
36 cin >> a[i];
37 }
38 for (int i = 1; i <= q; i ++){
39 cin >> k[i];
40 }
41 for (int i = 1; i <= n; i ++) pre[i] = pre[i - 1] + a[i];
42 sub2::solve();
43}

Đánh giá độ phức tạp của cách làm này: một vòng duyệt ngược để tính $mx\_pre$ và một vòng duyệt xuôi, mỗi vòng $O(N)$.

Không gian$O(N)$
Thời gian$O(N)$

Subtask 3K ≤ 50: quy hoạch động – O(N·K)

Khi $K$ lớn hơn thì cách "ghép" ở trên không dùng được nữa, ta chuyển sang quy hoạch động.

Bài toán con
Gọi $f[i][j]$ là tổng lớn nhất có thể đạt được nếu chọn đúng $j$ đoạn trong $i$ phần tử đầu của dãy, sao cho phần tử cuối của đoạn thứ $j$ là $i$.

Giả sử đoạn thứ $j$ là $[s, i]$, còn $j - 1$ đoạn trước đó kết thúc tại $u$ nào đó với $u \lt s$. Tổng là $f[u][j - 1] + pre[i] - pre[s - 1]$. Với $u$ cố định, $s - 1$ có thể là bất kỳ vị trí nào trong $[u, i - 1]$, nên ta chọn $pre[s - 1]$ nhỏ nhất:

Công thức truy hồi
$f[i][j] = \max\limits_{0 \le u \le i - 1} \Big( f[u][j - 1] + pre[i] - \min(pre[u], pre[u + 1], \ldots, pre[i - 1]) \Big)$
(quy ước "chọn $0$ đoạn" có giá trị $0$, ứng với $u = 0$, $j - 1 = 0$).

Tính thẳng công thức này là $O(N^2 K)$ – quá chậm. Để tối ưu hoá, ta tạo thêm hai mảng:

  • $mx[j]$: max của các giá trị $f[x][j]$ với $x \le i$ (vị trí đang xét hiện tại) – tức là tổng lớn nhất khi chọn đúng $j$ đoạn trong $i$ phần tử đầu. Có thể dễ dàng cập nhật sau mỗi lần duyệt $i$. Riêng $mx[0] = 0$.
  • $cur[j]$: max của các giá trị $mx[j]$ tại thời điểm $t$ trừ đi $pre[t]$, với mọi $t \le i - 1$. Hiểu nôm na: "$j$ đoạn đã chốt xong trong $[1, t]$, đoạn tiếp theo sẽ bắt đầu từ $t + 1$".

Ở đây, có thể thấy rằng trạng thái $f[i][j] = cur[j - 1] + pre[i]$. Việc của chúng ta bây giờ là tìm cách cập nhật $cur[j]$: sau khi xét xong vị trí $i$, có thêm một lựa chọn mới là "đoạn tiếp theo bắt đầu từ $i + 1$", giá trị của nó là $mx[j] - pre[i]$. Ta so với $cur[j]$ hiện có để xem ai lớn hơn mà lấy:

Ba bước cho mỗi i
1. $f[i][j] = cur[j - 1] + pre[i]$ với $j = 1..50$.
2. $mx[j] = \max(mx[j], f[i][j])$.
3. $cur[j] = \max(cur[j], mx[j] - pre[i])$ với $j = 0..50$.
Đáp án với thẻ $K$: $\max(mx[0], mx[1], \ldots, mx[K])$ – lấy max vì được chọn tối đa $K$ đoạn.

Source code để các bạn tham khảo nếu làm theo hướng này:

#include<bits/stdc++.h>
#define int long long
using namespace std;
int n, q;
int a[100005];
int k[100005];
int pre[100005];
namespace sub3{
    int f[100005][55];
    int mx[55];
    int cur[55];
    void solve(){
        for (int j = 0; j <= 50; j ++){
            mx[j] = cur[j] = -1e18;
        }
        mx[0] = 0;
        cur[0] = 0;
        for (int i = 0; i <= n; i ++){
            for (int j = 1; j <= 50; j ++) f[i][j] = -1e18;
        }
        f[0][0] = 0;
        for (int i = 1; i <= n; i ++){
            for (int j = 1; j <= 50; j ++){
                f[i][j] = cur[j - 1] + pre[i];
            }
            for (int j = 1; j <= 50; j ++){
                mx[j] = max(mx[j], f[i][j]);
            }
            for (int j = 0; j <= 50; j ++){
                cur[j] = max(cur[j], mx[j] - pre[i]);
            }
        }
        for (int i = 1; i <= q; i ++){
            int res = -1e18;
            for (int j = 0; j <= k[i]; j ++){
                res = max(res, mx[j]);
            }
            cout << res << endl;
        }
    }
};
signed main(){
    if(fopen("TROCHOI.INP", "r")){
        freopen("TROCHOI.INP", "r", stdin);
        freopen("TROCHOI.OUT", "w", stdout);
    }
    cin >> n >> q;
    for (int i = 1; i <= n; i ++){
        cin >> a[i];
    }
    for (int i = 1; i <= q; i ++){
        cin >> k[i];
    }
    for (int i = 1; i <= n; i ++) pre[i] = pre[i - 1] + a[i];
    sub3::solve();
}
import sys

NEG = -10**18
K = 50
data = sys.stdin.buffer.read().split()
n, q = int(data[0]), int(data[1])
a = [0] + [int(x) for x in data[2:2 + n]]
k = [0] + [int(x) for x in data[2 + n:2 + n + q]]
pre = [0] * (n + 1)
for i in range(1, n + 1):
    pre[i] = pre[i - 1] + a[i]

mx = [NEG] * (K + 1)
cur = [NEG] * (K + 1)
mx[0] = 0
cur[0] = 0
for i in range(1, n + 1):
    p = pre[i]
    # f[i][j] = cur[j - 1] + pre[i]  (chỉ cần hàng hiện tại)
    f = [NEG] + [cur[j - 1] + p for j in range(1, K + 1)]
    for j in range(1, K + 1):
        if f[j] > mx[j]:
            mx[j] = f[j]
    for j in range(K + 1):
        if mx[j] - p > cur[j]:
            cur[j] = mx[j] - p

out = []
for i in range(1, q + 1):
    out.append(max(mx[0:k[i] + 1]))
print("\n".join(map(str, out)))
Lưu ý: code gốc bị tràn mảng
Bản đầu mình khai báo f[100005][50], mx[50], cur[50] nhưng lại duyệt j tới 50, tức là truy cập phần tử thứ 51 – ra ngoài mảng (chạy với -fsanitize sẽ báo lỗi ngay). Mình đã sửa kích thước thành 55. Ngoài ra mảng $f$ thực ra chỉ cần hàng hiện tại (bản Python mình chỉ giữ một hàng), nhưng bản C++ mình vẫn giữ nguyên mảng 2 chiều cho dễ hiểu. Code này chỉ đúng khi $K \le 50$.

Trong khung chạy tay, để đỡ dài mình chỉ chạy tay từng $j$ từ $0$ tới $n$ (với $j \gt n$ không thể chọn đủ $j$ đoạn nên giá trị vẫn là $-10^{18}$, ghi là −∞) và gom các $j$ còn lại tới $50$ thành 1 bước.

C++▶ Chạy tay từng dòng codeSubtask 3 (K ≤ 50): f[i][j] = cur[j-1] + pre[i]; mx[j] = max f[x][j]; cur[j] = max(cur[j], mx[j] - pre[i]).
Dữ liệu mẫu:
1#include<bits/stdc++.h>
2#define int long long
3using namespace std;
4int n, q;
5int a[100005];
6int k[100005];
7int pre[100005];
8namespace sub3{
9 int f[100005][55];
10 int mx[55];
11 int cur[55];
12 void solve(){
13 for (int j = 0; j <= 50; j ++){
14 mx[j] = cur[j] = -1e18;
15 }
16 mx[0] = 0;
17 cur[0] = 0;
18 for (int i = 0; i <= n; i ++){
19 for (int j = 1; j <= 50; j ++) f[i][j] = -1e18;
20 }
21 f[0][0] = 0;
22 for (int i = 1; i <= n; i ++){
23 for (int j = 1; j <= 50; j ++){
24 f[i][j] = cur[j - 1] + pre[i];
25 }
26 for (int j = 1; j <= 50; j ++){
27 mx[j] = max(mx[j], f[i][j]);
28 }
29 for (int j = 0; j <= 50; j ++){
30 cur[j] = max(cur[j], mx[j] - pre[i]);
31 }
32 }
33 for (int i = 1; i <= q; i ++){
34 int res = -1e18;
35 for (int j = 0; j <= k[i]; j ++){
36 res = max(res, mx[j]);
37 }
38 cout << res << endl;
39 }
40 }
41};
42signed main(){
43 if(fopen("TROCHOI.INP", "r")){
44 freopen("TROCHOI.INP", "r", stdin);
45 freopen("TROCHOI.OUT", "w", stdout);
46 }
47 cin >> n >> q;
48 for (int i = 1; i <= n; i ++){
49 cin >> a[i];
50 }
51 for (int i = 1; i <= q; i ++){
52 cin >> k[i];
53 }
54 for (int i = 1; i <= n; i ++) pre[i] = pre[i - 1] + a[i];
55 sub3::solve();
56}

Đánh giá độ phức tạp của cách làm này:

  • Không gian: $O(N \cdot 50)$ cho mảng $f$ (khoảng 44 MB với long long; nếu chỉ giữ một hàng thì còn $O(N + 50)$).
  • Thời gian: mỗi $i$ duyệt $j$ tới $50$ ba lần, nên $O(N \cdot K)$ với $K = 50$. Cách này giải được cả subtask 1, 2, 3 (60% điểm).
Không gian$O(N \cdot K)$
Thời gian$O(N \cdot K)$
Subtask 4 thì sao?
Subtask 4 có $K \le N$: muốn dùng quy hoạch động này thì phải duyệt $j$ tới $N$, thành $O(N^2) = 10^{10}$ – TLE. Subtask 4 và 5 được giải chung bằng cách dưới đây.

Subtask 4 + 5Chia để trị + tổng Minkowski của hàm lồi – O(N log N)

Phần này khá nặng, mình đi từng bước nha.

Bước 1: Định nghĩa hàm f

Gọi $f^{a,b}(l, r, x)$ là tổng tối đa khi ta chọn không quá $x$ đoạn liên tiếp không giao nhau trong $A[l..r]$, khi:

  • Nếu $a = 1$ thì phần tử $l$ có nằm trong 1 trong các đoạn, $a = 0$ thì ngược lại (phần tử $l$ không được chọn);
  • Nếu $b = 1$ thì phần tử $r$ có nằm trong 1 trong các đoạn, $b = 0$ thì ngược lại.

Để cho thuận tiện, ở phần sau nếu mình viết $f(l, r)$ thì có thể hiểu là biểu thức đúng với mọi cặp giá trị $(a, b)$, và $f(l, r)$ được coi như một dãy số $f(l, r, 0), f(l, r, 1), \ldots, f(l, r, r - l + 1)$ (chọn nhiều hơn $r - l + 1$ đoạn thì không thể có thêm gì nữa). Trong code, đây chính là vector dp[a][b] của struct Answer. Trạng thái không thể xảy ra (ví dụ $a = 1$ mà $x = 0$) có giá trị $-\infty$ (code dùng $-10^9$).

Đáp án của truy vấn $K$ là $\max\limits_{a, b} f^{a,b}(1, n, K)$.

Bước 2: Chia để trị – ghép hai nửa

Gọi $m = \left\lfloor \frac{l + r}{2} \right\rfloor$. Giả sử ta đã tính được $f(l, m)$ và $f(m + 1, r)$ (bằng đệ quy). Một cách chọn đoạn trên $[l, r]$ "cắt" ở giữa $m \mid m + 1$ sẽ thành một cách chọn trên nửa trái và một cách chọn trên nửa phải. Chỉ có một điểm cần chú ý: nếu có một đoạn vắt qua chỗ cắt, nó bị tách thành 2 mảnh (một mảnh kết thúc tại $m$, một mảnh bắt đầu tại $m + 1$), nên khi ghép lại số đoạn phải trừ đi 1.

1-1245-23
Ví dụ dãy $1, -1, 2, 4, 5, -2, 3$ ($l = 1$, $r = 7$, $m = 4$), chọn 3 đoạn $[1, 1]$, $[3, 5]$, $[7, 7]$. Đường cắt đỏ nằm giữa vị trí $4$ và $5$ chia đoạn vàng $[3, 5]$ làm 2 mảnh: nửa trái thấy 2 mảnh ($[1, 1]$, $[3, 4]$), nửa phải thấy 2 mảnh ($[5, 5]$, $[7, 7]$) – tổng 4 mảnh nhưng thực ra chỉ có 3 đoạn, vì ô $m = 4$ và ô $m + 1 = 5$ đều được chọn nên 2 mảnh ở giữa nối lại thành 1.

Vì vậy ta cần biết ô $m$ và ô $m + 1$ có được chọn hay không – đó là lý do phải lưu thêm 2 chỉ số $a, b$. Gọi $c$ là trạng thái của ô $m$ (bên trái), $d$ là trạng thái của ô $m + 1$ (bên phải). Có 4 trường hợp:

$c = 0, d = 0$: không đoạn nào vắt qua. Số đoạn $x = x' + y'$.
$c = 0, d = 1$: ô $m$ không chọn nên không vắt qua. $x = x' + y'$.
$c = 1, d = 0$: ô $m + 1$ không chọn. $x = x' + y'$.
$c = 1, d = 1$: hai ô kề nhau đều được chọn: có thể coi là 2 đoạn riêng ($x = x' + y'$), hoặc nối thành 1 đoạn ($x = x' + y' - 1$).
Công thức ghép
$f^{a,b}(l, r, x) = \max\limits_{c, d} \ \max\limits_{x' + y' = x \ (\text{hoặc } x + 1 \text{ khi } c = d = 1)} \Big( f^{a,c}(l, m, x') + f^{d,b}(m + 1, r, y') \Big)$

Phép toán "$\max\limits_{x' + y' = x} \big(P[x'] + Q[y']\big)$" của hai dãy $P, Q$ được gọi là tích chập max-plus. Tính trực tiếp thì mất $O(|P| \cdot |Q|)$, cả thuật toán sẽ thành $O(N^2)$ – vẫn chậm. Ta cần thêm 2 bổ đề.

Bước 3: Bổ đề 1 – f là hàm lồi

Bổ đề 1
$f(l, r)$ là hàm lồi (theo nghĩa "lõm xuống" quen gọi trong lập trình thi đấu), nói cách khác: $f(l, r, x) - f(l, r, x - 1) \ \ge\ f(l, r, x + 1) - f(l, r, x)$.
Tức là: "lợi nhuận" khi được phép chọn thêm đoạn thứ $x$ không nhỏ hơn lợi nhuận khi chọn thêm đoạn thứ $x + 1$.

Nhìn vào ví dụ của đề: với cả dãy $1, -1, 2, -2, 3$ ta có $f(0..5) = 0, 3, 5, 6, 6, 6$; dãy hiệu là $3, 2, 1, 0, 0$ – giảm dần. Đoạn thứ nhất lời $3$, cho phép thêm đoạn thứ hai lời thêm $2$, thêm đoạn thứ ba lời thêm $1$, sau đó hết lời.

Chứng minh (dành cho bạn đã học luồng)
Không mất tính tổng quát, giả sử $l = 1$ và $r = n$. Xét mạng gồm đỉnh phát $s$, đỉnh thu $t$ và các đỉnh $1, 2, \ldots, n + 1$:
  • cạnh $s \to i$ và cạnh $i \to t$ với mọi $i$, khả năng thông qua $1$, giá trị $0$;
  • cạnh $i \to i + 1$ với $1 \le i \le n$, khả năng thông qua $1$, giá trị $A_i$.
Một đơn vị luồng đi $s \to l \to l + 1 \to \cdots \to r + 1 \to t$ chính là chọn đoạn $[l, r]$ (thu được $A_l + \cdots + A_r$); mỗi cạnh $i \to i + 1$ chỉ chở được 1 đơn vị nên các đoạn không có phần tử chung. Dễ thấy $f(1, n, x)$ chính là luồng $x$ đơn vị có giá trị cực đại. Xét thuật toán tìm luồng giá trị cực đại bằng cách tăng luồng lần lượt theo đường tăng tốt nhất, giá trị các đường tăng không tăng lên, nên hàm giá trị là hàm lồi theo $x$ ⇒ đpcm. Các trạng thái ép $a, b$ (bắt buộc chọn / không chọn ô đầu, ô cuối) chỉ là thêm ràng buộc vào mạng, lập luận vẫn y như vậy. Bạn nào chưa học luồng thì có thể tạm công nhận bổ đề này.

Bước 4: Bổ đề 2 – tích chập hai dãy lồi = trộn hai dãy hiệu

Đặt $p_i = P[i] - P[i - 1]$ và $q_i = Q[i] - Q[i - 1]$ (dãy hiệu). Theo bổ đề 1, cả hai dãy hiệu đều giảm dần. Gọi $c$ là dãy gồm tất cả các phần tử của $p$ và $q$, sắp xếp giảm dần.

Bổ đề 2
$\max\limits_{x' + y' = x} \big(P[x'] + Q[y']\big) = P[0] + Q[0] + \sum\limits_{i = 1}^{x} c_i$

Hiểu nôm na theo kiểu tham lam: ban đầu $x' = y' = 0$. Mỗi lần tăng $x$ thêm $1$, ta được chọn tăng $x'$ hoặc $y'$ thêm $1$, và ta chọn bên nào cho "lời" nhiều hơn (bước hiệu lớn hơn). Vì các bước hiệu mỗi bên đều giảm dần, nên lấy cái lớn trước luôn không thiệt. Về mặt hình học: gọi đa giác $A$ chứa các đỉnh $(i, P[i])$, đa giác $B$ chứa các đỉnh $(i, Q[i])$ – đều là đa giác lồi. Kết quả chính là tổng Minkowski của $A$ và $B$, và thuật toán trên mô phỏng lại thuật toán tính tổng Minkowski hai đa giác lồi (trộn các cạnh theo độ dốc).

Và vì $p$, $q$ đã sắp xếp giảm dần sẵn, ta không cần sort mà chỉ cần trộn như bước trộn của Merge sort, mất $O(|P| + |Q|)$. Đó chính là hàm convolute trong code.

Minh hoạ: trộn hai dãy hiệu

Các bạn nhập 2 dãy lồi $P$, $Q$ (dãy hiệu giảm dần) rồi bấm Tiếp. Mỗi bước lấy bước hiệu lớn hơn ở đầu 2 dãy hiệu. Bước cuối mình so luôn với cách thử mọi cặp $(x', y')$ để các bạn thấy kết quả khớp:

Ví dụ nhanh:

Bước 5: Ghép lại và đánh giá độ phức tạp

Ta có thêm nhận xét: khi tính $f(l, m, x)$ ta chỉ cần quan tâm các giá trị $0 \le x \le m - l + 1$, tương tự với $f(m + 1, r)$ và $f(l, r)$. Nên mỗi lần ghép chỉ mất $O(r - l)$. Thuật toán chia để trị: gọi đệ quy tính $f(l, m)$ và $f(m + 1, r)$, sau đó ghép ra $f(l, r)$. Độ phức tạp $T(n) = 2T(n/2) + O(n) = O(n \log n)$.

Vì sao phải trộn chứ không sort?
Nếu ở bước tạo dãy $c$ ta gom $p, q$ lại rồi sort thì mỗi lần ghép mất $O((r - l) \log n)$ và cả thuật toán là $O(n \log^2 n)$ – vẫn hơi chậm: cài đặt mẫu theo cách này bị TLE 4 test trên bộ test của SQRTOJ. Nhờ bổ đề 1, hai dãy hiệu đã sắp xếp giảm dần sẵn nên chỉ cần trộn kiểu Merge sort, giảm bước này về $O(n)$, độ phức tạp cuối cùng là $O(n \log n)$ – đủ nhanh để giải quyết bài toán.

Source code của tất cả các phân tích ở trên để các bạn tham khảo:

#include<bits/stdc++.h>
#define int long long
using namespace std;
int n, q;
int a[100005];
int k[100005];
int pre[100005];
namespace sub5{
    vector<int> convolute(vector<int> a, vector<int> b)
    {
        vector<int> sus;
        int j = 1;
        for(int i = 1; i < a.size(); i++){
            while(j < b.size() && b[j]-b[j-1] > a[i]-a[i-1]){
                sus.push_back(b[j]-b[j-1]); j++;
            }
            sus.push_back(a[i]-a[i-1]);
        }
        while(j < b.size()){
            sus.push_back(b[j]-b[j-1]); j++;
        }
        int cur = a[0] + b[0];
        vector<int> ans = {cur};
        for(int i : sus){
            cur += i; ans.push_back(cur);
        }
        return ans;
    }
    vector<int> combine(vector<int> a, vector<int> b)
    {
        vector<int> ans(a.size());
        for(int i = 0; i < a.size(); i++) ans[i] = max(a[i], b[i]);
        return ans;
    }
    vector<int> reduce(vector<int> a)
    {
        for(int i = 0; i+1 < a.size(); i++) a[i] = max(a[i], a[i+1]);
        return a;
    }
    struct Answer
    {
        vector<int> dp[2][2];
    };
    Answer solve(int l, int r)
    {
        if(l == r){
            Answer ans;
            ans.dp[0][0] = {0, 0};
            ans.dp[1][1] = {-1000000000, a[l]};
            ans.dp[0][1] = {-1000000000, -1000000000};
            ans.dp[1][0] = {-1000000000, -1000000000};
            return ans;
        }
        Answer ans;
        Answer x = solve(l, (l+r)/2), y = solve((l+r)/2+1, r);
        for(int i = 0; i <= 1; i++){
            for(int j = 0; j <= 1; j++){
                ans.dp[i][j] = convolute(x.dp[i][0], y.dp[0][j]);
                ans.dp[i][j] = combine(ans.dp[i][j], convolute(x.dp[i][0], y.dp[1][j]));
                ans.dp[i][j] = combine(ans.dp[i][j], convolute(x.dp[i][1], y.dp[0][j]));
                ans.dp[i][j] = combine(ans.dp[i][j], reduce(convolute(x.dp[i][1], y.dp[1][j])));
            }
        }
        return ans;
    }
    void solve(){
        Answer ans = solve(1, n);
        for (int i = 1; i <= q; i ++){
           cout<<max(max(ans.dp[0][0][k[i]], ans.dp[0][1][k[i]]), max(ans.dp[1][0][k[i]], ans.dp[1][1][k[i]]))<<'\n';
        }
    }
};
signed main(){
    if(fopen("TROCHOI.INP", "r")){
        freopen("TROCHOI.INP", "r", stdin);
        freopen("TROCHOI.OUT", "w", stdout);
    }
    cin >> n >> q;
    for (int i = 1; i <= n; i ++){
        cin >> a[i];
    }
    for (int i = 1; i <= q; i ++){
        cin >> k[i];
    }
    for (int i = 1; i <= n; i ++) pre[i] = pre[i - 1] + a[i];
    sub5::solve();
}
import sys
from operator import sub

NEG = -1000000000

def convolute(a, b):
    # trộn 2 dãy hiệu (đều giảm dần) giống bước trộn của Merge sort
    da = list(map(sub, a[1:], a[:-1]))
    db = list(map(sub, b[1:], b[:-1]))
    sus = []
    j = 0
    for x in da:
        while j < len(db) and db[j] > x:
            sus.append(db[j])
            j += 1
        sus.append(x)
    sus.extend(db[j:])
    cur = a[0] + b[0]
    ans = [cur]
    for d in sus:
        cur += d
        ans.append(cur)
    return ans

def combine(a, b):
    return list(map(max, a, b))

def reduce_(a):
    return list(map(max, a, a[1:])) + a[-1:]

def solve(l, r):
    if l == r:
        return [[[0, 0], [NEG, NEG]],
                [[NEG, NEG], [NEG, A[l]]]]
    m = (l + r) // 2
    x = solve(l, m)
    y = solve(m + 1, r)
    ans = [[None, None], [None, None]]
    for i in range(2):
        for j in range(2):
            t = convolute(x[i][0], y[0][j])
            t = combine(t, convolute(x[i][0], y[1][j]))
            t = combine(t, convolute(x[i][1], y[0][j]))
            t = combine(t, reduce_(convolute(x[i][1], y[1][j])))
            ans[i][j] = t
    return ans

data = sys.stdin.buffer.read().split()
n, q = int(data[0]), int(data[1])
A = [0] + [int(x) for x in data[2:2 + n]]
k = [int(x) for x in data[2 + n:2 + n + q]]
dp = solve(1, n)
out = []
for kk in k:
    out.append(max(dp[0][0][kk], dp[0][1][kk], dp[1][0][kk], dp[1][1][kk]))
print("\n".join(map(str, out)))

Giải thích 1 số chỗ trong code ở trên:

  1. convolute(a, b): tích chập max-plus của 2 dãy lồi bằng cách trộn 2 dãy hiệu (bổ đề 2). Biến sus là dãy $c$ sau khi trộn, rồi cộng dồn từ a[0] + b[0].
  2. combine(a, b): lấy max từng vị trí của 2 dãy – dùng để lấy max giữa 4 trường hợp $(c, d)$.
  3. reduce(a): a[i] = max(a[i], a[i + 1]) – dùng cho trường hợp $c = d = 1$: dùng $i + 1$ "mảnh" nhưng 2 mảnh ở giữa nối lại thành 1 đoạn, nên chỉ tính là $i$ đoạn.
  4. Ở lá $l = r$: dp[0][0] = {0, 0} (không chọn ô $l$, được $0$ dù cho phép 0 hay 1 đoạn); dp[1][1] = {-1e9, a[l]} (chọn ô $l$: cần ít nhất 1 đoạn); dp[0][1], dp[1][0] là trạng thái mâu thuẫn (ô $l$ cũng chính là ô $r$) nên đều là $-\infty$.
  5. ans.dp[i][j] (với $i = a$, $j = b$) là max của 4 trường hợp ghép x.dp[i][c] với y.dp[d][j]. Đáp án truy vấn $K$ là max của 4 trạng thái của cả dãy tại vị trí $K$.

Trong khung chạy tay, mình gom mỗi lời gọi convolute / combine / reduce thành 1 bước (bên trong convolute chính là quá trình trộn ở khung minh hoạ phía trên). Bảng "Dữ liệu" hiện 4 dãy dp[a][b] của nửa trái x, nửa phải y và kết quả ans đang ghép; −∞ là giá trị $-10^9$ (trạng thái không thể xảy ra, hoặc $-10^9$ cộng thêm vài số nhỏ).

C++▶ Chạy tay từng dòng codeSubtask 4 + 5: solve(l, r) trả về 4 dãy dp[a][b]; ghép 2 nửa bằng convolute (trộn dãy hiệu), combine (max) và reduce (nối đoạn vắt qua giữa).
Dữ liệu mẫu:
1#include<bits/stdc++.h>
2#define int long long
3using namespace std;
4int n, q;
5int a[100005];
6int k[100005];
7int pre[100005];
8namespace sub5{
9 vector<int> convolute(vector<int> a, vector<int> b)
10 {
11 vector<int> sus;
12 int j = 1;
13 for(int i = 1; i < a.size(); i++){
14 while(j < b.size() && b[j]-b[j-1] > a[i]-a[i-1]){
15 sus.push_back(b[j]-b[j-1]); j++;
16 }
17 sus.push_back(a[i]-a[i-1]);
18 }
19 while(j < b.size()){
20 sus.push_back(b[j]-b[j-1]); j++;
21 }
22 int cur = a[0] + b[0];
23 vector<int> ans = {cur};
24 for(int i : sus){
25 cur += i; ans.push_back(cur);
26 }
27 return ans;
28 }
29 vector<int> combine(vector<int> a, vector<int> b)
30 {
31 vector<int> ans(a.size());
32 for(int i = 0; i < a.size(); i++) ans[i] = max(a[i], b[i]);
33 return ans;
34 }
35 vector<int> reduce(vector<int> a)
36 {
37 for(int i = 0; i+1 < a.size(); i++) a[i] = max(a[i], a[i+1]);
38 return a;
39 }
40 struct Answer
41 {
42 vector<int> dp[2][2];
43 };
44 Answer solve(int l, int r)
45 {
46 if(l == r){
47 Answer ans;
48 ans.dp[0][0] = {0, 0};
49 ans.dp[1][1] = {-1000000000, a[l]};
50 ans.dp[0][1] = {-1000000000, -1000000000};
51 ans.dp[1][0] = {-1000000000, -1000000000};
52 return ans;
53 }
54 Answer ans;
55 Answer x = solve(l, (l+r)/2), y = solve((l+r)/2+1, r);
56 for(int i = 0; i <= 1; i++){
57 for(int j = 0; j <= 1; j++){
58 ans.dp[i][j] = convolute(x.dp[i][0], y.dp[0][j]);
59 ans.dp[i][j] = combine(ans.dp[i][j], convolute(x.dp[i][0], y.dp[1][j]));
60 ans.dp[i][j] = combine(ans.dp[i][j], convolute(x.dp[i][1], y.dp[0][j]));
61 ans.dp[i][j] = combine(ans.dp[i][j], reduce(convolute(x.dp[i][1], y.dp[1][j])));
62 }
63 }
64 return ans;
65 }
66 void solve(){
67 Answer ans = solve(1, n);
68 for (int i = 1; i <= q; i ++){
69 cout<<max(max(ans.dp[0][0][k[i]], ans.dp[0][1][k[i]]), max(ans.dp[1][0][k[i]], ans.dp[1][1][k[i]]))<<'\n';
70 }
71 }
72};
73signed main(){
74 if(fopen("TROCHOI.INP", "r")){
75 freopen("TROCHOI.INP", "r", stdin);
76 freopen("TROCHOI.OUT", "w", stdout);
77 }
78 cin >> n >> q;
79 for (int i = 1; i <= n; i ++){
80 cin >> a[i];
81 }
82 for (int i = 1; i <= q; i ++){
83 cin >> k[i];
84 }
85 for (int i = 1; i <= n; i ++) pre[i] = pre[i - 1] + a[i];
86 sub5::solve();
87}

Đánh giá độ phức tạp của cách làm này:

  • Không gian: các lời gọi đang mở trên ngăn xếp đệ quy giữ các vector có độ dài $N/2, N/4, \ldots$ (giảm một nửa mỗi tầng), cộng lại vẫn là $O(N)$.
  • Thời gian: mỗi lần ghép $[l, r]$ làm 16 lần convolute / combine, mỗi lần $O(r - l)$; có $\log N$ tầng, mỗi tầng tổng độ dài $N$ ⇒ $O(N \log N)$. Mỗi truy vấn trả lời $O(1)$.
Không gian$O(N)$
Thời gian$O(N \log N + Q)$
Bạn nào code Python để ý
Hằng số của cách này khá lớn (16 phép ghép ở mỗi nút, lại copy vector liên tục). Mình đo bản C++ với $N = Q = 10^5$ mất khoảng 0.6 giây, còn bản Python mất khoảng 7.5 giây – chắc chắn TLE. Bài này các bạn nên nộp C++.
Ghi chú của mình: code C++ và Python của cả 4 phần đều cho đúng output ví dụ (code subtask 1, 2 chạy với $K$ tương ứng). Mỗi phần đã được so với code quy hoạch động vét cạn (tính đáp án cho mọi $K$, $N \le 14$) trên 300 bộ test ngẫu nhiên đúng ràng buộc của subtask đó – khớp hết. Riêng code đầy đủ còn được so với một cách giải độc lập (kỹ thuật "Aliens trick" / nhân tử Lagrange) trên 7 bộ test lớn $N = Q = 10^5$ (ngẫu nhiên, toàn âm, toàn dương, xen kẽ, các khối lớn…) – khớp hết; thời gian chạy bản C++ khoảng 0.6 giây. Các bản code subtask 1, 2, 3 bằng C++ với $N = 10^5$ chạy dưới 0.1 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 😇
Tự học thuật toán: khó ở đâu?
Đọc xong một bài giải là bước đầu. Phần lớn học sinh tự học sẽ gặp những trở ngại sau:
  • 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.
Học cùng thầy Nam Sơn, các trở ngại đó được gỡ bỏ:
  • 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