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

Ma trận xoắn ốc 2

Ma trận xoắn ốc 2
Nguồn: Codeforces Group Độ khó: Trung bình Ma trận Tìm quy luật Tính trước

Đề bài

Khi học về ma trận (mảng 2 chiều có $M$ hàng, $N$ cột), thầy sẽ ra cho các bạn 1 bài về ma trận xoắn ốc. Ma trận xoắn ốc là một ma trận vuông có $M = N$, được sinh ra khi biết kích thước $N$. Với số 1 ở trung tâm ma trận, di chuyển theo hình xoắn ốc ngược chiều kim đồng hồ, rồi lần lượt điền các số tăng dần 1, 2, 3, 4, 5 …

Một số ví dụ về ma trận xoắn ốc:

Với N = 2
43
12
Với N = 3
543
612
789
Với N = 4
16151413
54312
61211
78910
Với N = 5
1716151413
1854312
1961211
2078910
2122232425

Việc sinh ra ma trận xoắn ốc trở nên nhàm chán, thầy quyết định các bạn không cần phải sinh ra ma trận này làm gì. Thầy chỉ muốn biết tổng của các số trên 2 đường chéo chính của ma trận xoắn ốc là bao nhiêu. Ví dụ:

NTổng các số trên 2 đường chéo
21 + 3 + 2 + 4 = 10
35 + 1 + 9 + 7 + 3 = 25
416 + 4 + 2 + 10 + 13 + 3 + 1 + 7 = 56
517 + 5 + 1 + 9 + 25 + 21 + 7 + 3 + 13 = 101

Vì tổng này quá lớn, các kiểu dữ liệu cơ bản không thể chứa được, thầy chỉ cần các bạn tính được tổng này mod $(10^9 + 9)$ là được.

Input

  • Dòng 1: số $T$ là số test case ($1 \le T \le 10^5$).
  • Dòng 2 đến $T + 1$: chứa số $N$ ($1 \le N \le 10^7$).

Output: gồm $T$ dòng, dòng thứ $i$ chứa kết quả tương ứng với test case thứ $i$.

Subtask

  • 20% số test đầu tiên có $T \le 10$, $N \le 1000$.
  • 20% số test tiếp theo có $T \le 10^3$, $N \le 8000$.
  • 60% số test còn lại không có giới hạn gì thêm.
Ví dụ – input
5
2
3
4
5
10000000
Ví dụ – output
10
25
56
101
679604006

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

Bài này cách vét cạn có thể lấy được 40% điểm khi mà tạo ra cái ma trận xoắn ốc theo quy luật đề mô tả, không khó để làm được. Bây giờ anh sẽ tập trung vào cách làm tối ưu để full 100% điểm nhé.

Nháp ra giấy và tìm quy luật

Lời khuyên là em hãy nháp ra giấy vẽ hình ma trận xoắn ốc với các trường hợp $N$ từ 2 đến 6 thôi là được rồi; với mỗi trường hợp em ghi ra dãy các số nằm trên 2 đường chéo chính và chéo phụ (các ô màu cam bên dưới) và hãy ghi nó theo thứ tự giá trị tăng dần:

Với N = 2
43
12
Với N = 3
543
612
789
Với N = 4
16151413
54312
61211
78910
Với N = 5
1716151413
1854312
1961211
2078910
2122232425

Em sẽ nhìn ra quy luật như sau (số màu xanh là 4 số mới thêm vào so với trường hợp $N - 2$):

NDãy số trên 2 đường chéo (tăng dần)Tổng
21 2 3 410
31 3 5 7 925
41 2 3 4 7 10 13 1656
51 3 5 7 9 13 17 21 25101
61 2 3 4 7 10 13 16 21 26 31 36170

Nhìn kỹ em sẽ thấy rõ quy luật của nó, nếu vẫn chưa thấy được thì xem tiếp anh sẽ chỉ ra cho em thấy rõ. Hãy chú ý đến 2 trường hợp khởi đầu: trường hợp $N$ chẵn ($N = 2$) và trường hợp $N$ lẻ ($N = 3$). Từ 2 dãy này ta sẽ có các quy luật cho các trường hợp tiếp theo như sau:

  • Trường hợp $N = 4$ sẽ là dãy các số của trường hợp $N = 2$ và thêm 4 giá trị mới.
  • Trường hợp $N = 5$ sẽ là dãy các số của trường hợp $N = 3$ và thêm 4 giá trị mới.
  • Trường hợp $N = 7$ sẽ là dãy các số của trường hợp $N = 5$ và thêm 4 giá trị mới.

Vậy nghĩa là ta cần có được dãy số của trường hợp trước đó của nó (phân rõ theo chẵn/lẻ: nếu trường hợp hiện tại $N$ đang là lẻ thì nghĩa là xét trường hợp $N$ lẻ gần nhất trước đó, và dãy hiện tại sẽ bằng dãy trước đó với thêm 4 số mới; tương tự như vậy với trường hợp $N$ chẵn).

Tìm 4 số mới

Vấn đề 4 số mới của trường hợp $N$ hiện tại cần tìm ra nó bằng quy luật thế nào? Để ý em sẽ thấy quy luật như sau: ví dụ khi $N = 2$ thì dãy là 1 2 3 4. Vậy khi $N = 4$ là trường hợp chẵn tiếp theo thì số cuối cùng trong 4 số sẽ là $N^2$ $= 4^2 = 16$. Khi đã xác định được số cuối cùng trong 4 số thì việc tìm ra 3 số đứng trước nó khá đơn giản, lúc này chỉ là lần lượt lấy giá trị số cuối cùng tìm được trừ đi cho $(N - 1)$ liên tiếp 3 lần là ra.

N = 4
4² = 16
16 − 3 = 13
13 − 3 = 10
10 − 3 = 7
Dãy: 1 2 3 4 7 10 13 16
N = 5
5² = 25
25 − 4 = 21
21 − 4 = 17
17 − 4 = 13
Dãy: 1 3 5 7 9 13 17 21 25

Hoặc đơn giản nhất ta lấy số cuối cùng của trường hợp $N$ trước đó liên tục cộng dồn thêm 4 lần của $(N - 1)$. Ví dụ số cuối cùng của trường hợp $N = 2$ là số 4, vậy 4 số tiếp theo sẽ là: 4 + 3 = 7, 7 + 3 = 10, 10 + 3 = 13, 13 + 3 = 16.

Giờ đây khi đã nhìn ra quy luật tạo dãy số này rồi thì em thử suy nghĩ xem cần phải code thế nào cho dễ dàng đơn giản nhất mà hiệu suất chạy tối ưu, không bị quá thời gian với độ lớn dữ liệu đề bài đưa ra. Lúc này đây cũng sẽ phải tư duy suy nghĩ thêm nhiều đấy nhé 😉, thử sức xem, nếu không được thì xem tiếp phần dưới anh sẽ nói nốt luôn.

Em nhập $N$ rồi bấm Tiếp để xem ma trận lớn dần từ trong ra ngoài, mỗi lần N tăng 2 thì chỉ có thêm đúng 4 số ở 4 góc:

Ví dụ nhanh:
Ô mờ: phần chưa xét · Ô xanh: đường chéo của ma trận cũ (N − 2) · Ô cam: 4 góc mới thêm vào.

Tính trước toàn bộ kết quả

Với những phân tích ở trên thì thấy rằng để tính được giá trị của trường hợp $N$ cần tính được trước đó trường hợp $N - 2$: tính tổng dãy số của trường hợp $N - 2$ rồi lấy kết quả tổng đó cộng thêm tổng của 4 số mới của trường hợp $N$ hiện tại là ra kết quả hiện tại cần tìm của trường hợp $N$:

res[N] = res[N − 2] + N² + (N² − (N−1)) + (N² − 2(N−1)) + (N² − 3(N−1))

Vậy nhìn thấy nếu theo công thức này để tính được trường hợp $N$ hiện tại thì cần tính được các trường hợp $N - 2$, $N - 4$, $N - 6$ … trước đó. Như thế nếu với $T$ bộ test, cứ với mỗi trường hợp $N$ ta lại đi tính từ đầu đến $N$ và lặp đi lặp lại quy trình này $T$ lần, thì như vậy đó sẽ không phải cách làm tốt vì cứ bị tính đi tính lại nhiều lần không cần thiết, và có thể dẫn đến thuật toán bị chạy quá giới hạn thời gian cho phép. Ví dụ đầu tiên $N = 10$ thì ta tính qua các trường hợp $N = 2, 4, 6, 8$ rồi mới có kết quả tính ra $N = 10$. Nhưng tiếp đó lại có yêu cầu tính $N = 8$ thì lúc này đây ta lại đi tính các trường hợp $N = 2, 4, 6$, và em có thấy rõ ràng những trường hợp này mình đã tính trước đó ở trường hợp $N = 10$ rồi không? Nên làm như thế này sẽ không tốt.

Giải pháp xử lý tốt hơn đó là với phạm vi dữ liệu đề bài cho, trường hợp $N$ lớn nhất là $10^7$. Như thế ta sẽ đi tính hết tất cả kết quả các trường hợp $N$ từ 2 đến $10^7$ và lưu trữ các kết quả đó vào trong mảng đáp án tại vị trí index tương ứng, ví dụ res[N] là đáp án của trường hợp $N$. Tạo ra được mảng res lưu trữ hết tất cả các tình huống như thế thì với mỗi bộ test với giá trị $N$ đọc vào chỉ cần gọi res[N] là lấy ra ngay lập tức đáp án chỉ trong $O(1)$ phép tính. Thực tế ta không cần tính đủ $10^7$ trường hợp vì đây chỉ là tình huống xấu nhất, chỉ cần tìm ra Max (số lớn nhất) trong các trường hợp dữ liệu đầu vào rồi tính đến Max là được.

Lưu ý vì dữ liệu kết quả tổng có thể rất lớn nên đề đã ghi rõ kết quả chỉ cần chia lấy dư cho $(10^9 + 9)$. Vậy nên em cũng phải biết phép toán modulo để giảm bớt độ lớn dữ liệu tránh bị tràn số. Chứ bạn nào không biết mà đi cộng tổng các phần tử trong dãy số lại hết sau đó mới chia lấy dư thì cũng chẳng giải quyết được gì cả, chúng ta phải chia lấy dư trong quá trình cộng tổng của các phần tử trong dãy nhé. Bạn nào nếu chưa biết thì có thể xem qua một số phép toán modulo cơ bản như sau:
(a + b) % c = ((a % c) + (b % c)) % c
(a − b) % c = ((a % c) − (b % c)) % c
(a * b) % c = ((a % c) * (b % c)) % c

Vậy nên nếu áp dụng theo tư tưởng giải thuật này thì độ phức tạp bước tạo ra mảng res là $O(\max N)$ với trường hợp lớn nhất $N = 10^7$. Sau đó $O(T)$ để xử lý $T$ bộ test, với trường hợp lớn nhất $T = 10^5$; còn với mỗi bộ test thì chỉ mất $O(1)$ để in ra res[N]. Vậy tổng quát độ phức tạp là $O(\max N + T)$. Trường hợp xấu nhất là khi $T = 10^5$, $N = 10^7$, lúc này sẽ là $O(10^7 + 10^5)$ và vẫn không lo bị TLE, vì nguyên tắc để không bị TLE thì số phép toán tối đa thực hiện trong ngưỡng $(3 \div 5) \times 10^7$, và với giới hạn này vẫn chưa bị vượt qua nên thuật toán này đảm bảo ổn.

Đây là code xử lý theo tư tưởng anh đã nói ở trên cho em tham khảo:

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

const long long M = 1000000009;
const int MAX = 1e7;
long long res[MAX + 10];

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

    res[1] = 1;  // 1
    res[2] = 10; // 1 2 3 4
    res[3] = 25; // 1 3 5 7 9

    // vì tính i*i nên chắc ăn biến i nên là kiểu long long để không bị tràn dữ liệu
    for(long long i = 4; i <= MAX; i++){
        long long num1 = i * i;
        long long num2 = num1 - (i - 1);
        long long num3 = num2 - (i - 1);
        long long num4 = num3 - (i - 1);
        res[i] = (res[i - 2] + (num1 % M) + (num2 % M) + (num3 % M) + num4 % M) % M;
    }

    int t, x;
    cin >> t;
    while(t--){
        cin >> x;
        cout << res[x] << '\n';
    }
    return 0;
}
M = 1000000009
MAX = 10**7

res = [0] * (MAX + 10)

res[1] = 1   # 1
res[2] = 10 # 1 2 3 4
res[3] = 25 # 1 3 5 7 9

for i in range(4, MAX + 1):
    num1 = i * i
    num2 = num1 - (i - 1)
    num3 = num2 - (i - 1)
    num4 = num3 - (i - 1)
    res[i] = (res[i - 2] + (num1 % M) + (num2 % M) + (num3 % M) + (num4 % M)) % M

import sys
input = sys.stdin.readline

t = int(input())
for _ in range(t):
    x = int(input())
    print(res[x])
C++▶ Chạy tay từng dòng codeTính trước res[N] = res[N − 2] + 4 số ở 4 góc mới, mỗi test chỉ tra mảng.
Dữ liệu mẫu:
1#include <bits/stdc++.h>
2using namespace std;
3
4const long long M = 1000000009;
5const int MAX = 9;
6long long res[MAX + 10];
7
8int main(){
9 ios::sync_with_stdio(0);
10 cin.tie(0);
11
12 res[1] = 1; // 1
13 res[2] = 10; // 1 2 3 4
14 res[3] = 25; // 1 3 5 7 9
15
16 // vì tính i*i nên chắc ăn biến i nên là kiểu long long để không bị tràn dữ liệu
17 for(long long i = 4; i <= MAX; i++){
18 long long num1 = i * i;
19 long long num2 = num1 - (i - 1);
20 long long num3 = num2 - (i - 1);
21 long long num4 = num3 - (i - 1);
22 res[i] = (res[i - 2] + (num1 % M) + (num2 % M) + (num3 % M) + num4 % M) % M;
23 }
24
25 int t, x;
26 cin >> t;
27 while(t--){
28 cin >> x;
29 cout << res[x] << '\n';
30 }
31 return 0;
32}
Để chạy tay được, khung này đổi MAX = 1e7 thành MAX = 9 (dòng 5), nên test đề bỏ dòng N = 10^7; phần còn lại giữ nguyên code gốc.

Mình chạy thử với $T = 10^5$: bản C++ mất khoảng 0.1 giây. Bản Python thì phải tạo list $10^7$ phần tử và lặp $10^7$ lần nên mất khoảng 7.5 giây và tốn rất nhiều bộ nhớ; bạn nào code Python thì xem phần đọc thêm ngay dưới đây nhé.

O(1)Đọc thêm: công thức tính thẳng cho N bất kỳ

Lúc viết bài anh có nói "có thể sẽ có cách làm tốt hơn đi tính trực tiếp ra kết quả của trường hợp $N$ bất kỳ chỉ trong $O(1)$ bằng cách xác định đúng công thức toán học". Thì thực ra là có thật nha các em 😜. Cộng 4 số mới lại ta được $4N^2 - 6(N - 1)$, tức là mỗi bước cộng thêm $4N^2 - 6N + 6$. Cộng dồn các bước đó (dùng công thức tổng các bình phương) sẽ ra:

N lẻ
S = (4N³ + 3N² + 8N − 9) / 6
N chẵn
S = (4N³ + 3N² + 8N) / 6

Thử lại: $N = 3$: $(108 + 27 + 24 - 9) / 6 = 25$ ✓; $N = 4$: $(256 + 48 + 32) / 6 = 56$ ✓; $N = 5$: $(500 + 75 + 40 - 9) / 6 = 101$ ✓. Mình cũng đã so kết quả của công thức với code ở trên trên $10^5$ test ngẫu nhiên (kể cả $N = 10^7$), kết quả khớp hoàn toàn.

Chỉ có 1 chỗ cần cẩn thận: với $N = 10^7$ thì $4N^3$ lên tới $4 \times 10^{21}$, vượt kiểu long long, nên trong C++ ta phải tính tử số theo modulo rồi nhân với nghịch đảo modulo của 6 (vì $10^9 + 9$ là số nguyên tố nên nghịch đảo của 6 là $6^{10^9+7} \bmod (10^9+9)$, tính bằng lũy thừa nhanh). Còn Python thì số nguyên không giới hạn nên tính thẳng luôn:

Xem code công thức O(1)
#include <bits/stdc++.h>
using namespace std;

const long long M = 1000000009;

long long power(long long a, long long b){ // a^b % M
    long long res = 1;
    a %= M;
    while(b > 0){
        if(b & 1){
            res = res * a % M;
        }
        a = a * a % M;
        b >>= 1;
    }
    return res;
}

int main(){
    ios::sync_with_stdio(0);
    cin.tie(0);
    long long inv6 = power(6, M - 2); // nghịch đảo modulo của 6
    int t;
    cin >> t;
    while(t--){
        long long n;
        cin >> n;
        long long x = n % M;
        long long tu = (4 * (x * x % M) % M * x % M + 3 * (x * x % M) % M + 8 * x) % M;
        if(n % 2 == 1){
            tu = (tu - 9 + M) % M; // N lẻ thì trừ thêm 9
        }
        cout << tu * inv6 % M << '\n';
    }
    return 0;
}
import sys

def main():
    M = 1000000009
    data = sys.stdin.buffer.read().split()
    t = int(data[0])
    out = []
    for i in range(1, t + 1):
        n = int(data[i])
        if n % 2 == 1:
            s = (4 * n**3 + 3 * n**2 + 8 * n - 9) // 6
        else:
            s = (4 * n**3 + 3 * n**2 + 8 * n) // 6
        out.append(s % M)
    print("\n".join(map(str, out)))

main()
C++▶ Chạy tay từng dòng codeCông thức O(1): tính tử số theo modulo rồi nhân nghịch đảo của 6 (lũy thừa nhanh).
Dữ liệu mẫu:
1#include <bits/stdc++.h>
2using namespace std;
3
4const long long M = 1000000009;
5
6long long power(long long a, long long b){ // a^b % M
7 long long res = 1;
8 a %= M;
9 while(b > 0){
10 if(b & 1){
11 res = res * a % M;
12 }
13 a = a * a % M;
14 b >>= 1;
15 }
16 return res;
17}
18
19int main(){
20 ios::sync_with_stdio(0);
21 cin.tie(0);
22 long long inv6 = power(6, M - 2); // nghịch đảo modulo của 6
23 int t;
24 cin >> t;
25 while(t--){
26 long long n;
27 cin >> n;
28 long long x = n % M;
29 long long tu = (4 * (x * x % M) % M * x % M + 3 * (x * x % M) % M + 8 * x) % M;
30 if(n % 2 == 1){
31 tu = (tu - 9 + M) % M; // N lẻ thì trừ thêm 9
32 }
33 cout << tu * inv6 % M << '\n';
34 }
35 return 0;
36}
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ìm xn

Tìm xn
Nguồn: Codeforces Group Độ khó: Trung bình Dãy số Tính trước Modular

Đề bài

Cho dãy số $x_n$ được định nghĩa như sau:

  • $x_0 = 1$
  • $x_1 = 2$
  • $x_n = n \cdot x_0 + (n - 1) \cdot x_1$ $+ (n - 2) \cdot x_2$ $+ \ldots + 1 \cdot x_{n-1}$

Hãy in ra kết quả $x_n \bmod (10^9 + 7)$.

Input

  • Dòng đầu tiên là số $T$ là số lượng bộ test ($T \le 10^6$).
  • $T$ dòng tiếp theo, mỗi dòng có 1 số $n$ ($0 \le n \le 10^7$).

Output: in ra $T$ dòng, mỗi dòng là giá trị $x_n \bmod (10^9 + 7)$.

Subtask

  • 30% số test đầu tiên có $T \le 10$, $n \le 10^3$.
  • 20% số test tiếp theo có $T \le 10^6$, $n \le 10^3$.
  • 30% số test tiếp theo có $T \le 10$, $n \le 10^7$.
  • 20% số test còn lại không có giới hạn gì thêm.
Ví dụ – input
4
0
1
4
6
Ví dụ – output
1
2
29
199

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:

30%Subtask 1: tính thẳng theo định nghĩa – O(T × N²)

Bài này nhìn thoáng qua ta thấy nếu dùng mảng 1 chiều xử lý thì sẽ rất gọn, theo đúng quy tắc đề đã đưa ra thôi. Ta gán sẵn a[0] = 1, a[1] = 2, rồi giả sử cần tính $n = 6$ thì bắt đầu tiếp từ $i = 2$ đến $i = 6$, mỗi lần sẽ đi tính toàn bộ các thằng trước đó:

a[2] = 2*a[0] + 1*a[1]
a[3] = 3*a[0] + 2*a[1] + 1*a[2]
a[4] = 4*a[0] + 3*a[1] + 2*a[2] + 1*a[3]
…

Cứ thế tính cho đến $n$ thôi. Nhớ trong quá trình tính liên tục chia dư để tránh bị tràn số. Thì cách làm đơn giản nhất là dùng 2 vòng lặp lồng nhau thôi:

Xem code subtask 1
#include <bits/stdc++.h>
using namespace std;

int main(){
    ios::sync_with_stdio(0);
    cin.tie(0);
    long long m = 1e9 + 7;
    int t;
    cin >> t;
    while(t--){
        int n;
        cin >> n;
        vector<long long> a(n + 1, 0);
        a[0] = 1;
        if(n >= 1){
            a[1] = 2;
        }
        for(int i = 2; i <= n; ++i){
            long long cnt = i;
            for(int j = 0; j < i; ++j){
                a[i] += cnt * a[j];
                cnt--;
                a[i] %= m;
            }
        }
        cout << a[n] << '\n';
    }
    return 0;
}
import sys
input = sys.stdin.readline

m = int(1e9 + 7)

t = int(input())
for _ in range(t):
    n = int(input())
    a = [0] * (n + 1)
    a[0] = 1
    if n >= 1:
        a[1] = 2
    for i in range(2, n + 1):
        cnt = i
        for j in range(i):
            a[i] = (a[i] + cnt * a[j]) % m
            cnt -= 1
    print(a[n])
C++▶ Chạy tay từng dòng codeSubtask 1: mỗi test tính lại cả dãy bằng 2 vòng lặp lồng nhau, O(T × N²).
Dữ liệu mẫu:
1#include <bits/stdc++.h>
2using namespace std;
3
4int main(){
5 ios::sync_with_stdio(0);
6 cin.tie(0);
7 long long m = 1e9 + 7;
8 int t;
9 cin >> t;
10 while(t--){
11 int n;
12 cin >> n;
13 vector<long long> a(n + 1, 0);
14 a[0] = 1;
15 if(n >= 1){
16 a[1] = 2;
17 }
18 for(int i = 2; i <= n; ++i){
19 long long cnt = i;
20 for(int j = 0; j < i; ++j){
21 a[i] += cnt * a[j];
22 cnt--;
23 a[i] %= m;
24 }
25 }
26 cout << a[n] << '\n';
27 }
28 return 0;
29}
Lưu ý nhỏ khi $n = 0$: code C++ tạo vector a(n + 1), nên khi $n = 0$ mảng chỉ có 1 phần tử a[0]; nếu vẫn gán a[1] = 2 thì sẽ ghi ra ngoài mảng (lỗi truy cập bộ nhớ, có thể chạy vẫn ra đúng nhưng cũng có thể crash tuỳ máy). Mà test ví dụ có luôn $n = 0$. Vì vậy trong code trên mình thêm điều kiện if(n >= 1) trước khi gán a[1] = 2, giống y như bản Python.

Độ phức tạp: $O(T \times N^2)$. Thì code trên sẽ chỉ lấy được Subtask 1 với 30% điểm đầu tiên khi $T \le 10$, $N \le 10^3$, bởi $T \times N^2 = 10^7$ thì thỏa $\le 10^8$ chạy trong 1 giây (với C++). Tuy nhiên với Subtask 2: $T \le 10^6$ và $N \le 10^3$ thì $T \times N^2 = 10^6 \times 10^6 = 10^{12}$, thì đã vượt quá giới hạn $10^8$ (1 giây của C++) nên chắc chắn TLE.

+20%Subtask 2: tính trước 1 lần cho mọi n ≤ 1000

Với Subtask 2: $T \le 10^6$, $N \le 10^3$. Ta nhận thấy rằng giá trị $N$ khá nhỏ (tối đa chỉ là 1000), trong khi đó có quá nhiều test (lên tới 1 triệu). Vì vậy ta có thể tận dụng lại code của Subtask 1 ở trên nhưng tính trước kết quả cho tối đa $N = 1000$ lưu trữ lại trong mảng kết quả, từ đó với mỗi truy vấn sẽ gọi vào trong mảng kết quả in ra thôi.

Xem code subtask 2
#include <bits/stdc++.h>
using namespace std;

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

    long long m = 1e9 + 7;
    int maxN = 1000;
    vector<long long> a(maxN + 1, 0);
    a[0] = 1;
    a[1] = 2;
    for(int i = 2; i <= maxN; ++i){
        long long cnt = i;
        for(int j = 0; j < i; ++j){
            a[i] += cnt * a[j];
            cnt--;
            a[i] %= m;
        }
    }

    int t;
    cin >> t;
    while(t--){
        int n;
        cin >> n;
        cout << a[n] << '\n';
    }
    return 0;
}
import sys
input = sys.stdin.readline

m = int(1e9 + 7)
maxN = 1000

a = [0] * (maxN + 1)
a[0] = 1
a[1] = 2
for i in range(2, maxN + 1):
    cnt = i
    for j in range(i):
        a[i] = (a[i] + cnt * a[j]) % m
        cnt -= 1

t = int(input())
for _ in range(t):
    n = int(input())
    print(a[n])
C++▶ Chạy tay từng dòng codeSubtask 2: tính trước cả mảng 1 lần, mỗi test chỉ tra mảng.
Dữ liệu mẫu:
1#include <bits/stdc++.h>
2using namespace std;
3
4int main(){
5 ios::sync_with_stdio(0);
6 cin.tie(0);
7
8 long long m = 1e9 + 7;
9 int maxN = 8;
10 vector<long long> a(maxN + 1, 0);
11 a[0] = 1;
12 a[1] = 2;
13 for(int i = 2; i <= maxN; ++i){
14 long long cnt = i;
15 for(int j = 0; j < i; ++j){
16 a[i] += cnt * a[j];
17 cnt--;
18 a[i] %= m;
19 }
20 }
21
22 int t;
23 cin >> t;
24 while(t--){
25 int n;
26 cin >> n;
27 cout << a[n] << '\n';
28 }
29 return 0;
30}
Để chạy tay được, khung này đổi maxN = 1000 thành maxN = 8 (dòng 9); phần còn lại giữ nguyên code gốc.

Độ phức tạp: $O(maxN^2 + T)$, thì $maxN$ tối đa 1000 nên $maxN^2$ = 1 triệu, và $T$ tối đa cũng là 1 triệu nên tổng là 2 triệu và hoàn toàn yên tâm chạy trong 1 giây. Code này sẽ lấy được Subtask 1 & 2 với 50% điểm. Còn Subtask 3 khi $T$ tối đa 10 và $N$ tối đa $10^7$ thì sẽ thua, bởi vì $maxN$ tối đa $10^7$ thì $maxN^2 = 10^{14}$, đã vượt quá ngưỡng $10^8$ rồi nên chắc chắn sẽ bị TLE.

100%Subtask 3 & 4: tìm ra quy luật, mỗi số chỉ tốn O(1)

Subtask 3: $T$ tối đa 10, $N$ tối đa $10^7$. Subtask 4: $T$ tối đa $10^6$, $N$ tối đa $10^7$. Lúc này chắc chắn ta cần tìm ra cách tính a[n] với độ phức tạp tốt hơn, không thể dùng 2 vòng lặp lồng nhau như hiện tại bởi độ phức tạp quá cao. Để làm được điều đó ta sẽ phân tích 1 chút:

nKhai triểnGiá trị
x22·1 + 1·24
x33·1 + 2·2 + 1·411
x44·1 + 3·2 + 2·4 + 1·1129
x55·1 + 4·2 + 3·4 + 2·11 + 1·2976
x66·1 + 5·2 + 4·4 + 3·11 + 2·29 + 1·76199

Nếu các bạn ghi ra dãy kết quả theo 1 hàng ngang đi, sẽ dễ nhìn thấy quy luật của nó:

1   2   4   11   29   76   199

Các bạn nhìn kỹ hen: số 4 (a[2]) là mình chủ động tính ra, nhưng khi đã có nó rồi thì các bạn nhìn nhé, về sau nó có 1 quy tắc. Ví dụ tính a[3] đi hen: nó sẽ là tổng của các thằng trước đó (a[0] + a[1] + a[2]) và cộng thêm 1 lần a[2] nữa, tức là sẽ là $1 + 2 + 4 + 4 = 11$. Tương tự vậy, tính a[4] hen: nó sẽ là tổng của các thằng trước đó (a[0] + a[1] + a[2] + a[3]) và cộng thêm 1 lần a[3] nữa, tức là $1 + 2 + 4 + 11 + 11 = 29$. Cứ thế …

Quy luật
Với $n \ge 3$: a[n] = (a[0] + a[1] + … + a[n − 2]) + 2 × a[n − 1]. Nếu ta luôn lưu trữ lại tổng các số ở đằng trước nó vào 1 biến sum thì chỉ cần duyệt 1 lần, tức là độ phức tạp $O(n)$ để tính ra kết quả.

Ví dụ trong trường hợp tính a[3], ta có tổng các số từ a[0] đến a[1] là 3 rồi, cộng thêm 2 lần a[2] là 8 nữa thì thành 11. Rồi giờ muốn tính a[4] thì lấy tổng các số từ a[0] đến a[2] mà ta đã lưu trữ là 7, giờ chỉ là cộng thêm 2 lần của a[3], tức là $2 \times 11 = 22$, và $7 + 22 = 29$. Và lúc này nhớ cập nhật tổng các số từ a[0] đến a[3] = 7 + 11 = 18, để khi tính a[5] sẽ gọi tổng đã lưu là 18, rồi cộng với 2 lần a[4], tức là $18 + 2 \times 29 = 76$. Đó, các bạn nhìn ra quy luật này của dãy số hen:

Tínhsum (đã lưu)+ 2 lần số liền trướcKết quảCập nhật sum
a[3]a[0] + … + a[1] = 32 × a[2] = 2 × 4 = 811sum = 3 + 4 = 7
a[4]a[0] + … + a[2] = 72 × a[3] = 2 × 11 = 2229sum = 7 + 11 = 18
a[5]a[0] + … + a[3] = 182 × a[4] = 2 × 29 = 5876sum = 18 + 29 = 47
a[6]a[0] + … + a[4] = 472 × a[5] = 2 × 76 = 152199sum = 47 + 76 = 123

Em nhập $n$ rồi bấm Tiếp để so sánh cách tính theo định nghĩa (cộng cả dãy) với quy luật sum + 2 × a[i − 1]:

Ví dụ nhanh:
Ô tím: các số nằm trong sum · Ô vàng: số liền trước được cộng 2 lần · Ô xanh: số vừa tính.
Vì sao lại có quy luật này? (chứng minh nhanh)

Viết $x_n$ và $x_{n-1}$ theo định nghĩa rồi trừ cho nhau:

x[n]     = n·x[0] + (n−1)·x[1] + … + 2·x[n−2] + 1·x[n−1]
x[n−1] = (n−1)·x[0] + (n−2)·x[1] + … + 1·x[n−2]

Mỗi hệ số chênh nhau đúng 1, nên $x_n - x_{n-1} = x_0 + x_1 + \ldots + x_{n-1}$, tức là $x_n = (x_0 + \ldots + x_{n-2}) + 2x_{n-1}$, đúng như quy luật ở trên. Riêng $x_2$ không theo quy luật này vì $x_1 = 2$ là giá trị đề cho sẵn chứ không tính theo công thức (theo công thức thì $x_1$ phải là $1 \cdot x_0 = 1$), nên ta phải tự tính $x_2 = 4$ trước rồi mới bắt đầu áp dụng quy luật từ $x_3$.

Thú vị hơn nữa: từ $x_4$ trở đi còn có thể viết gọn thành $x_n = 3x_{n-1} - x_{n-2}$ (ví dụ $3 \times 11 - 4 = 29$, $3 \times 29 - 11 = 76$), và các số 4, 11, 29, 76, 199, … chính là các số Lucas ở vị trí lẻ. Bạn nào thích toán có thể tìm hiểu thêm.

Và ta có 1 đoạn code sau đây để giải quyết full điểm cho toàn bộ các Subtask. Đó là ta thấy $N$ tối đa $10^7$ thì ta sẽ đi tính trước toàn bộ $10^7$ trường hợp lưu vào mảng kết quả, và nhớ là tính theo cách dùng 1 vòng lặp xét qua thôi với độ phức tạp $O(maxN)$. Sau đó với mỗi bộ test ta gọi vào mảng kết quả lấy giá trị in ra, nên độ phức tạp bước sau đó nữa là $O(T)$. Vậy tổng lại độ phức tạp sẽ là $O(maxN + T)$, thì $maxN$ tối đa $10^7$, $T$ tối đa $10^6$ thì tổng lại chỉ là 11 triệu, hoàn toàn chạy được trong 1 giây (với C++ vì ngưỡng 1 giây của C++ là $10^8$).

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

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

    int n = 1e7;
    long long m = 1e9 + 7;
    vector<long long> a(n + 1);
    a[0] = 1;
    a[1] = 2;
    a[2] = 4;
    long long sum = a[0] + a[1];
    for(int i = 3; i <= n; ++i){
        a[i] = sum + 2 * a[i - 1];
        sum += a[i - 1];
        a[i] %= m;
        sum %= m;
    }

    int t, x;
    cin >> t;
    while(t--){
        cin >> x;
        cout << a[x] << '\n';
    }
    return 0;
}
import sys
input = sys.stdin.readline

n = int(1e7)
m = int(1e9 + 7)

a = [0] * (n + 1)
a[0] = 1
a[1] = 2
a[2] = 4
sum_ = a[0] + a[1]

for i in range(3, n + 1):
    a[i] = (sum_ + 2 * a[i - 1]) % m
    sum_ = (sum_ + a[i - 1]) % m

t = int(input())
for _ in range(t):
    x = int(input())
    print(a[x])
C++▶ Chạy tay từng dòng codeSubtask 3 & 4: a[i] = sum + 2 × a[i − 1], tính trước O(n), mỗi test O(1).
Dữ liệu mẫu:
1#include <bits/stdc++.h>
2using namespace std;
3
4int main(){
5 ios::sync_with_stdio(0);
6 cin.tie(0);
7
8 int n = 10;
9 long long m = 1e9 + 7;
10 vector<long long> a(n + 1);
11 a[0] = 1;
12 a[1] = 2;
13 a[2] = 4;
14 long long sum = a[0] + a[1];
15 for(int i = 3; i <= n; ++i){
16 a[i] = sum + 2 * a[i - 1];
17 sum += a[i - 1];
18 a[i] %= m;
19 sum %= m;
20 }
21
22 int t, x;
23 cin >> t;
24 while(t--){
25 cin >> x;
26 cout << a[x] << '\n';
27 }
28 return 0;
29}
Để chạy tay được, khung này đổi n = 1e7 thành n = 10 (dòng 8); phần còn lại giữ nguyên code gốc.

Mình chạy thử với $T = 10^6$ truy vấn: bản C++ mất khoảng 0.34 giây. Mảng vector<long long> $10^7$ phần tử tốn khoảng 80 MB; nếu hệ thống chấm giới hạn bộ nhớ chặt thì có thể đổi sang vector<int> (vì giá trị sau khi chia dư luôn nhỏ hơn $10^9 + 7$, vẫn vừa kiểu int) để chỉ còn khoảng 40 MB. Bản Python cùng ý tưởng mất khoảng 5 giây vì phải lặp $10^7$ lần, nên với subtask lớn các bạn nên nộp bản C++ 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 😇