Đếm đoạn (HNOI19_R1_DEMDOAN) (HSG Hà Nội 2019 vòng 1)
Đề bài
Đếm đoạn
Cho số nguyên dương $N$. Đếm xem có bao nhiêu cặp số nguyên $a, b$ $(0 \lt a \le b)$ để tổng các số nguyên trong đoạn $[a, b]$ bằng $N$. Hai đoạn khác nhau là hai đoạn có ít nhất một phần tử khác nhau.
Dữ liệu vào từ tệp BAI1.INP: Gồm duy nhất một số nguyên dương $N$ $(0 \lt N \lt 10^{15})$.
Kết quả ra ghi vào tệp BAI1.OUT: Gồm một số nguyên dương duy nhất là kết quả của bài toán.
(Trên hệ thống chấm online thì đọc từ bàn phím và in ra màn hình như bình thường nhé.)
Lưu ý:
- Có 40% số điểm tương ứng với $N \le 10^4$.
- Có 30% số điểm tương ứng với $10^4 \lt N \le 10^8$.
93Có ba đoạn số thoả mãn:
[2, 4], [4, 5], [9, 9].Lời mở đầu
Bài này mình được một em học viên hỏi: "Anh ơi cho em hỏi ý tưởng bài này với ạ. Số $N$ to thế này chắc là có mẹo nhìn ra hả anh? Bài này vét cạn thì em dùng 2 vòng for, rồi dùng công thức $(i + j)(j - i + 1)/2$ để tính tổng các số từ $i$ đến $j$, nếu bằng $N$ thì tăng đếm lên 1. Chỉ được 40% thôi ạ". Mình đã code xong theo 3 cách và hướng dẫn chi tiết cho em ấy. Tin rằng sẽ có giá trị với nhiều bạn nên mình viết lại thành bài giải đầy đủ để các bạn tham khảo nhen 😇.
Bấm vào nút bên dưới để nộp bài và được hệ thống CLUE OJ chấm tự động.
Nộp bài trên CLUE OJGiải thích ví dụ: với $N = 9$, các đoạn số nguyên dương liên tiếp có tổng bằng $9$ là $2 + 3 + 4 = 9$, $4 + 5 = 9$ và chỉ một số $9$ (đoạn $[9, 9]$). Còn $[1, 3]$ có tổng $6$, $[1, 4]$ có tổng $10$... đều không được. Vậy đáp án là 3.
Gợi ý nhỏ: em học viên đã nhìn ra công thức $(i + j)(j - i + 1)/2$ ngay từ đầu, vậy là gần tiến được tới cách 100% rồi đó 😉. Mình sẽ đi qua 3 cách: cách được 40% là $O(N^2)$, cách được 70% là $O(N)$, cách được 100% là $O(\sqrt{2N})$.
Lộ trình lời giải, bấm vào từng phần để nhảy tới:
Cách 1Vét cạn điểm đầu, cộng dần điểm cuối – O(N²) · 40%
Cho $a$ chạy từ $1$ đến $N$ làm điểm đầu đoạn. Với mỗi $a$, cho $b$ chạy từ $a$ trở đi và cộng dần $b$ vào sum:
- Nếu
sum == nthì tìm được một đoạn $[a, b]$, tăngres. - Nếu
sum > nthì cộng thêm nữa chỉ càng lớn hơn (toàn số dương), nênbreakluôn để sang điểm đầu $a$ tiếp theo.
Source code để các bạn tham khảo nếu làm theo hướng này:
#include <bits/stdc++.h>
using namespace std;
// O(N^2): 40%
long long solve1(long long n){
long long res = 0;
for(long long a = 1; a <= n; ++a){
long long sum = 0;
for(long long b = a; b <= n; ++b){
sum += b;
if(sum == n){
res++;
}
else if(sum > n){
break;
}
}
}
return res;
}
int main(){
long long n;
cin >> n;
cout << solve1(n);
return 0;
}
# O(N^2): 40%
def solve1(n):
res = 0
for a in range(1, n + 1):
s = 0
for b in range(a, n + 1):
s += b
if s == n:
res += 1
elif s > n:
break
return res
n = int(input())
print(solve1(n))
Đánh giá độ phức tạp của cách làm này:
- Không gian: $O(1)$.
- Thời gian: hai vòng lặp lồng nhau, đánh giá thô là $O(N^2)$. Với $N \le 10^4$ thì chạy tức thì, ăn được 40% số điểm.
Cách 2Mảng cộng dồn + hai con trỏ – O(N) · 70%
Cách này dùng tư tưởng mảng cộng dồn. Ví dụ từ dãy $1, 2, 3, 4, 5, 6, 7, 8, 9$ ta có mảng cộng dồn (thêm phần tử đầu $a_0 = 0$):
| i | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
|---|---|---|---|---|---|---|---|---|---|---|
| a[i] | 0 | 1 | 3 | 6 | 10 | 15 | 21 | 28 | 36 | 45 |
Muốn biết đoạn từ $L = 3$ đến $R = 6$ có tổng bao nhiêu, thay vì cộng $3 + 4 + 5 + 6 = 18$ thì ta tính $a[R] - a[L - 1] = a[6] - a[2] = 21 - 3 = 18$.
Vậy với mỗi điểm đầu $i$, tính được $x = a[i - 1] + n$, cần tìm xem có tồn tại $j$ nào để $a[j] = x$ không.
Chỗ tìm $j$ này dùng hai con trỏ để chạy một lượt là xong: với mỗi $i$, đẩy $j$ tiến lên cho tới khi $a[j] \ge x$, kiểm tra $a[j] == x$ thì đếm. Sang $i$ tiếp theo thì $x$ chỉ tăng lên, nên $j$ chỉ cần chạy tiếp từ vị trí đã tìm thấy trước đó, chứ không cần quay lại $j = i$. Nếu $j$ đã vượt quá $n$ thì không còn đoạn nào nữa, dừng luôn.
Ví dụ $n = 9$: $i = 1$: $x = 0 + 9 = 9$, $j$ chạy tới $4$ ($a[4] = 10 \ge 9$), không bằng. $i = 2$: $x = 1 + 9 = 10$, $j$ vẫn ở $4$, $a[4] = 10$ ⇒ đoạn $[2, 4]$ ✅. $i = 3$: $x = 12$, $j$ tiến tới $5$ ($15$) – không. $i = 4$: $x = 15 = a[5]$ ⇒ đoạn $[4, 5]$ ✅... cứ thế đến $i = 9$: $x = 36 + 9 = 45 = a[9]$ ⇒ đoạn $[9, 9]$ ✅.
vector<long long> a(n + 1) để lưu mảng cộng dồn. Với $N = 10^8$ thì mảng này tốn khoảng 800 MB, dễ vượt giới hạn bộ nhớ. Vì dãy ở đây chính là $1, 2, 3, \ldots$ nên $a[i] = 1 + 2 + \ldots + i = \dfrac{i(i + 1)}{2}$, mình tính thẳng bằng hàm A(i) thay cho mảng, thuật toán giữ nguyên.Source code để các bạn tham khảo nếu làm theo hướng này:
#include <bits/stdc++.h>
using namespace std;
// O(N): 70%
// a[i] = 1 + 2 + ... + i = i * (i + 1) / 2 (mảng cộng dồn của dãy 1, 2, 3, ...)
long long A(long long i){
return i * (i + 1) / 2;
}
long long solve2(long long n){
long long j = 1;
long long res = 0;
for(long long i = 1; i <= n; ++i){
long long x = A(i - 1) + n;
while(j <= n && A(j) < x){
j++;
}
if(j > n){
break;
}
if(A(j) == x){
res++;
}
}
return res;
}
int main(){
long long n;
cin >> n;
cout << solve2(n);
return 0;
}
# O(N): 70%
def A(i):
# a[i] = 1 + 2 + ... + i (mảng cộng dồn của dãy 1, 2, 3, ...)
return i * (i + 1) // 2
def solve2(n):
j = 1
res = 0
for i in range(1, n + 1):
x = A(i - 1) + n
while j <= n and A(j) < x:
j += 1
if j > n:
break
if A(j) == x:
res += 1
return res
n = int(input())
print(solve2(n))
Đánh giá độ phức tạp của cách làm này:
- Không gian: $O(1)$ (vì tính $a[i]$ bằng công thức).
- Thời gian: $i$ chạy tối đa $n$ lần, $j$ cũng chỉ tiến tối đa $n$ lần trong suốt quá trình ⇒ $O(N)$. Với $N \le 10^8$ thì C++ qua được (mình đo khoảng 0.26 giây), ăn 70%.
Cách 3Phân tích 2N thành tích hai ước – O(√(2N)) · 100%
Công thức cấp số cộng: tổng từ $u_1$ đến $u_n$ bằng $\dfrac{n(u_1 + u_n)}{2}$, tức là $\dfrac{(\text{số lượng số}) \cdot (\text{số đầu} + \text{số cuối})}{2}$. Từ đó tổng các số liên tục từ $a$ đến $b$ là:
$$\frac{(b - a + 1)(a + b)}{2} = N \iff (b - a + 1)(a + b) = 2N$$
Vậy ta phân tích các cặp ước số $x_1, x_2$ của $2N$, tức là $x_1 \cdot x_2 = 2N$ với $x_1 \le x_2$, rồi đặt:
Từ phương trình đầu: $a = b + 1 - x_1$. Thế vào phương trình sau: $b + 1 - x_1 + b = x_2 \iff 2b = x_2 + x_1 - 1 \iff b = \dfrac{x_2 + x_1 - 1}{2}$.
Tính ra được $b$ rồi thế lại là tính ra được $a = b + 1 - x_1$.
Một cặp ước $(x_1, x_2)$ cho ra một đoạn hợp lệ khi:
- $x_2 + x_1 - 1$ chia hết cho $2$ (để $b$ là số nguyên), tức là $x_1$, $x_2$ phải khác tính chẵn lẻ;
- $0 \lt a \le b$ (đề yêu cầu $a$ dương).
Vì $x_1 \le x_2$ nên chỉ cần cho $x_1 = i$ chạy từ $1$ đến $\sqrt{2N}$, gặp $i$ là ước của $2N$ thì $x_2 = 2N / i$. Mỗi đoạn $[a, b]$ hợp lệ ứng với đúng một cặp $(x_1, x_2)$ (độ dài $b - a + 1$ luôn $\le a + b$ vì $a \ge 1$), nên đếm cặp chính là đếm đoạn.
Với $N = 9$: $2N = 18$, các cặp ước là $(1, 18), (2, 9), (3, 6)$:
| x1 | x2 | x1 + x2 − 1 | b | a | Đoạn |
|---|---|---|---|---|---|
| 1 | 18 | 18 | 9 | 9 | [9, 9] ✅ |
| 2 | 9 | 10 | 5 | 4 | [4, 5] ✅ |
| 3 | 6 | 8 | 4 | 2 | [2, 4] ✅ |
Đủ 3 đoạn như đề. Các bạn tự nhập $N$ khác vào khung minh hoạ dưới đây để xem có cặp ước nào bị loại nhé (ví dụ $N = 8$ hay $N = 15$):
Source code để các bạn tham khảo nếu làm theo hướng này:
#include <bits/stdc++.h>
using namespace std;
// O(sqrt(2*N)): 100%
long long solve3(long long n){
long long x = 2 * n;
long long res = 0;
for(long long i = 1; i * i <= x; ++i){
if(x % i == 0){
long long x1 = i;
long long x2 = x / i;
if((x2 + x1 - 1) % 2 == 0){
long long b = (x2 + x1 - 1) / 2;
long long a = b + 1 - x1;
if(0 < a && a <= b){
res++;
}
}
}
}
return res;
}
int main(){
long long n;
cin >> n;
cout << solve3(n);
return 0;
}
from math import isqrt
# O(sqrt(2*N)): 100%
def solve3(n):
x = 2 * n
res = 0
# lọc trước các ước i <= sqrt(x) bằng list comprehension cho nhanh
for x1 in [i for i in range(1, isqrt(x) + 1) if x % i == 0]:
x2 = x // x1
if (x2 + x1 - 1) % 2 == 0:
b = (x2 + x1 - 1) // 2
a = b + 1 - x1
if 0 < a <= b:
res += 1
return res
n = int(input())
print(solve3(n))
Các bạn bấm chạy tay code C++ để xem từng dòng nhé:
Đánh giá độ phức tạp của cách làm này:
- Không gian: $O(1)$.
- Thời gian: vòng lặp chạy $i$ tới $\sqrt{2N}$. Với $N \lt 10^{15}$ thì $\sqrt{2 \cdot 10^{15}} \approx 4.5 \cdot 10^7$ bước, mỗi bước chỉ là một phép chia lấy dư ⇒ chạy thoải mái trong 1 giây.
x, i, x1, x2, a, b đều phải là long long; điều kiện i * i <= x cũng phải tính bằng long long.- Không biết học gì trước, học gì sau, học lan man nên kiến thức bị hổng.
- Bí bài không ai gỡ, ngồi cả buổi vẫn không ra rồi bỏ cuộc.
- Code chạy được nhưng không đủ điểm, không biết sai ở đâu, chậm ở đâu.
- Không có đề thi thử và người chấm, nên không biết mình đang ở mức nào.
- Dễ nản giữa chừng khi không có ai theo sát.
- Lộ trình 111 buổi video sắp sẵn từ dòng code đầu tiên đến đề HSG thật của các tỉnh.
- Thầy trực tiếp gỡ bí, chữa bài cho từng em qua nhóm Zalo riêng.
- Hệ thống chấm bài tự động 24/7 và thi thử HSG hằng tuần để biết chính xác mình đang ở đâu.
- Đóng học phí một lần, thầy kèm lâu dài: từ thi chuyên Tin đến HSG quốc gia và cả khi lên đại học.
Nhận xét
Đăng nhận xét