Tìm Phần Thưởng (THUONG) (HSG9 TP Đà Nẵng 2018-2019 – Bài 3)
Đề bài
Câu 3. Tìm phần thưởng (4 điểm) – Đề thi HSG Tin học lớp 9 TP Đà Nẵng năm học 2018-2019
Trong Học kỳ I vừa qua, Tý đạt được danh hiệu học sinh giỏi nên được bố thưởng. Biết Tý là học sinh rất đam mê Toán và Tin học nên bố muốn thử thách Tý bằng một trò chơi có thưởng như sau:
Bố có rất nhiều hộp hình khối lập phương giống nhau xếp thành một hàng thẳng và được đánh số ký hiệu bằng các số tự nhiên lẻ bắt đầu từ $1; 3; 5; \ldots$ Trong các hộp đó có duy nhất 1 hộp đựng phần thưởng, các hộp khác là hộp rỗng. Bố cho Tý biết hộp đựng phần thưởng là hộp chính giữa của một đoạn dài nhất (ít nhất là 3 hộp liên tiếp) có tổng các số ký hiệu ghi trên các hộp bằng số $m$.
Yêu cầu: Với số $m$ cho trước, hãy tìm số ký hiệu của hộp có chứa phần thưởng.
Dữ liệu vào: Đọc ở file văn bản THUONG.INP số nguyên dương $m$ $(m \le 10^{16})$.
Dữ liệu ra: Ghi ra file văn bản THUONG.OUT một số $k$ là số ký hiệu của hộp có chứa phần thưởng.
Trên hệ thống chấm online thì đọc dữ liệu từ bàn phím và in ra màn hình (stdin/stdout) như bình thường nhé. Giới hạn thời gian: 1 giây.
459Giải thích: Các hộp được đánh số ký hiệu là $1; 3; 5; 7; 9; 11; 13; 15; 17; 19; 21; 23; 25; \ldots$ Đoạn dài nhất có tổng các số ký hiệu ghi trên hộp bằng $45$ là các hộp có số ký hiệu $5; 7; 9; 11; 13$. Do đó hộp cần tìm có số ký hiệu là $9$.
Lời mở đầu
Các bạn cố gắng giúp Tý nào, kẻo chọn sai là Tý chỉ "còn cái nịt" nhé các bạn 😆. Bài này nằm trong đề thi học sinh giỏi Tin học lớp 9 TP Đà Nẵng, 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 😇.
Trước khi đi vào vấn đề chính, các bạn nhớ giúp mình điều này: với giới hạn 1 giây, muốn không bị TLE thì toàn bài chỉ nên lặp khoảng $(3 \div 5) \cdot 10^7$ lần là an toàn, tệ nhất là $10^8$ (lúc này thì hên xui nhé). Ngưỡng này là cho C/C++, còn Python, Java, C#... chạy chậm hơn thì ngưỡng phải thấp hơn.
Đề hơi "văn vở" một chút nên mình nói lại cho gọn: có dãy số lẻ $1, 3, 5, 7, \ldots$, tìm một đoạn liên tiếp dài nhất (ít nhất 3 số) có tổng bằng $m$, rồi in ra số ở chính giữa của đoạn đó. Với $m = 45$ thì có 2 đoạn có tổng bằng 45 với độ dài $\ge 3$:
- $13 + 15 + 17 = 45$ (3 số, chính giữa là 15);
- $5 + 7 + 9 + 11 + 13 = 45$ (5 số, chính giữa là 9) ⇒ dài nhất, đáp án $9$.
Nếu bạn nào làm theo đúng cách đề "dắt" đi, tức là tạo hẳn dãy $1, 3, 5, 7, \ldots$ rồi cố tìm đoạn con liên tiếp dài nhất có tổng bằng $m$, thì xin chia buồn là các bạn đã bị đề bài đánh lừa 😅: làm theo hướng này rất phức tạp, mà $m$ lên tới $10^{16}$ thì dãy có quá nhiều phần tử, làm được cũng sẽ bị TLE.
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 OJLộ trình lời giải, bấm vào từng phần để nhảy tới:
Nhìn ra quy luật của dãy số lẻ
Những dạng bài kiểu dãy số có quy luật thì trong đầu các bạn hãy luôn nhủ rằng chắc chắn nó có quy luật, và nhìn ra đúng quy luật đó là mấu chốt để giải được bài. Bạn nào muốn tự thử thách thì đọc gợi ý dưới đây xong dừng lại tự làm tiếp nhé, khi nào bí quá hãy đọc tiếp.
Ví dụ lấy 3 làm trung tâm: trước nó là 1, sau nó là 5, ta thấy $1 + 5 = 6 = 2 \cdot 3$. Lấy 5 làm trung tâm: trước là 1, 3, sau là 7, 9, ta thấy $1 + 3 + 7 + 9 = 20 = 4 \cdot 5$. Lý do rất đơn giản: các số cách đều $X$ đi theo từng cặp, số bên trái nhỏ hơn $X$ bao nhiêu thì số bên phải lớn hơn $X$ bấy nhiêu, nên mỗi cặp cộng lại đúng bằng $2X$:
Giờ áp dụng vào bài. Đề nói lấy hộp chính giữa nên đoạn kết quả phải có số lượng phần tử lẻ thì mới có phần tử chính giữa (đề không nói gì với trường hợp số lượng chẵn, nên có thể yên tâm đoạn kết quả luôn có số phần tử lẻ). Đề cũng không nói nếu không có đáp án thì in gì, nên bộ test chắc chắn luôn có đáp án.
Cho đơn giản, giả sử đoạn kết quả có 3 phần tử: $L$ bên trái, $X$ chính giữa, $R$ bên phải. Theo đề ta có $L + X + R = M$, theo quy luật ở trên ta có $L + R = 2X$. Chuyển vế phương trình đầu thành $L + R = M - X$, hai vế trái bằng nhau nên $M - X = 2X$ ⇔ $3X = M$ ⇔ $X = \dfrac{M}{3}$.
Số 3 ở mẫu là vì $2 + 1 = 3$: 1 là $X$ chuyển vế qua, 2 là vì bên trái và bên phải mỗi bên có 1 phần tử. Tức là chia 3 vì đoạn có 3 phần tử. Đoạn có 5 phần tử thì chia 5, có 7 phần tử thì chia 7, cứ thế...
$X$ phải là số nguyên ⇒ $S$ phải là ước lẻ của $M$. Đề muốn đoạn dài nhất ⇒ $S$ phải lớn nhất có thể.
Thử với $M = 45$, các ước (đều lẻ) là $1, 3, 5, 9, 15, 45$:
| Ước $S$ | $X = 45 / S$ | Cần $\frac{S-1}{2}$ số trước $X$ | Trước $X$ có $\lfloor X/2 \rfloor$ số | Đoạn |
|---|---|---|---|---|
| 1 | 45 | 0 | 22 | 45 ✔ |
| 3 | 15 | 1 | 7 | 13, 15, 17 ✔ |
| 5 | 9 | 2 | 4 | 5, 7, 9, 11, 13 ✔ |
| 9 | 5 | 4 | 2 (là 1, 3) | không đủ số ✘ |
| 15 | 3 | 7 | 1 (là 1) | không đủ số ✘ |
| 45 | 1 | 22 | 0 | không đủ số ✘ |
Các bạn chú ý kỹ: với ước 9 thì $X = 5$, cần 4 số trước 5 nhưng trước 5 chỉ có 1, 3 thôi, đào đâu ra 4 số? Ước 15, 45 cũng vậy. Nên chỉ nhận các ước hợp lệ $1, 3, 5$, ước hợp lệ lớn nhất là 5 ⇒ đáp án $45 / 5 = 9$.
Minh hoạ: thử từng ước lẻ của M
Em nhập $M$ rồi bấm Tiếp để xem với mỗi ước lẻ $S$, đoạn $S$ số có số giữa $X = M / S$ trông như thế nào, và khi nào thì "hụt" số (ô viền đỏ đứt là những số không tồn tại vì nhỏ hơn 1):
Cách 1Duyệt các ước lẻ từ nhỏ đến lớn – O(M)
Từ phân tích trên, cách làm đầu tiên rất tự nhiên: cho $Y$ chạy qua các số lẻ $1, 3, 5, \ldots$, gặp $Y$ là ước của $M$ thì tính $X = M / Y$. Muốn biết trước $X$ có bao nhiêu số thì lấy phép chia nguyên $X / 2$. Nếu $Y / 2 \le X / 2$ thì $Y$ hợp lệ, ta ghi nhận đáp án là $X$; còn nếu $Y / 2 \gt X / 2$ thì loại.
Ví dụ $M = 45$: $Y = 1$ ⇒ $X = 45$, $X/2 = 22$, $Y/2 = 0$ ⇒ nhận. $Y = 3$ ⇒ $X = 15$, $X/2 = 7$, $Y/2 = 1$ ⇒ nhận. $Y = 5$ ⇒ $X = 9$, $X/2 = 4$, $Y/2 = 2$ ⇒ nhận. $Y = 9$ ⇒ $X = 5$, $X/2 = 2$, $Y/2 = 4 \gt 2$ ⇒ loại.
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;
int main() {
long long m;
cin >> m;
long long ans = 0;
for(long long y = 1; y <= m; y += 2){
if(m % y == 0){
long long x = m / y;
if(y / 2 > x / 2){
break;
}
ans = x;
}
}
cout << ans;
return 0;
}
m = int(input())
ans = 0
y = 1
while y <= m:
if m % y == 0:
x = m // y
if y // 2 > x // 2:
break
ans = x
y += 2
print(ans)
Đá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 qua mọi số lẻ cho tới ước không hợp lệ đầu tiên, tức là ước nhỏ nhất lớn hơn $\sqrt{M}$. Nếu $M = 3 \cdot p$ với $p$ là số nguyên tố lớn thì ước đó chính là $p$, vòng lặp chạy cỡ $p / 2 \approx M / 6$ lần ⇒ $O(M)$.
Cách 2Đi lùi từ √M về 1, gặp ước lẻ đầu tiên là xong – O(√M)
Nếu cố gắng nhìn thêm chút nữa thì sẽ thấy hướng tối ưu hơn. Nhắc lại bài toán liệt kê tất cả ước số của một số nguyên mà mình đã từng hướng dẫn: ta chỉ cần chạy từ 1 đến $\sqrt{M}$, mỗi khi tìm được ước $i$ thì ước còn lại chính là $M / i$ (vì $M$ chia hết cho $i$ thì kết quả phép chia cũng là một ước). Bài này chỉ cần ước lẻ nên chỉ xét các giá trị lẻ.
Ví dụ $M = 45$: $\sqrt{45} \approx 6{,}708$, lấy phần nguyên là 6, chỉ xét các số lẻ $1, 3, 5$ (bỏ qua $2, 4, 6$):
- $i = 1$: 45 chia hết cho 1 ⇒ có 2 ước là $1$ và $45/1 = 45$;
- $i = 3$: 45 chia hết cho 3 ⇒ có 2 ước là $3$ và $45/3 = 15$;
- $i = 5$: 45 chia hết cho 5 ⇒ có 2 ước là $5$ và $45/5 = 9$.
Mà ở trên ta thấy chỉ $1, 3, 5$ hợp lệ, còn $9, 15, 45$ thì không. Quy luật là: các ước lẻ $\le \sqrt{M}$ đều hợp lệ, còn các ước $\gt \sqrt{M}$ đều không hợp lệ. Điều này khớp với điều kiện $S \le X = M / S$ ⇔ $S^2 \le M$ ⇔ $S \le \sqrt{M}$.
Cách tìm: cho $i$ chạy từ $\lfloor \sqrt{M} \rfloor$ (nếu chẵn thì giảm đi 1 cho thành lẻ) lùi về 1, mỗi bước giảm 2. Giá trị đầu tiên mà $M$ chia hết chính là $S$ (vì đang xét từ lớn về bé nên nó lớn nhất ⇒ đoạn dài nhất). In $M / i$ rồi kết thúc luôn.
Như $M = 45$: $\lfloor \sqrt{45} \rfloor = 6$ chẵn nên giảm còn 5, mà 45 chia hết cho 5 ⇒ in $45 / 5 = 9$ luôn, chỉ 1 lần lặp.
Source code để các bạn tham khảo với tất cả những gì mình phân tích ở trên:
#include <bits/stdc++.h>
using namespace std;
int main() {
long long m;
cin >> m;
int sqr = (int)sqrt(m);
if(sqr % 2 == 0){
sqr--;
}
for(int i = sqr; i >= 1; i -= 2){
if(m % i == 0){
cout << m / i;
return 0;
}
}
return 0;
}
import math
m = int(input())
sqr = math.isqrt(m)
if sqr % 2 == 0:
sqr -= 1
for i in range(sqr, 0, -2):
if m % i == 0:
print(m // i)
break
math.isqrt(m) để lấy phần nguyên căn bậc 2 chính xác tuyệt đối với số nguyên lớn, thay vì int(math.sqrt(m)) có thể bị sai số khi $m$ gần $10^{16}$.Bấm Tiếp để chạy tay từng dòng code C++ ở trên:
Đánh giá độ phức tạp của cách làm này:
- Không gian: $O(1)$.
- Thời gian: trường hợp xấu nhất vòng lặp chạy đủ $\sqrt{M} / 2$ lần (chia 2 vì mỗi bước nhảy 2 đơn vị). $\sqrt{M}/2 = 0{,}5 \cdot \sqrt{M}$, theo quy tắc Big O thì nhân hằng số được bỏ qua ⇒ $O(\sqrt{M})$.
Trường hợp xấu nhất là khi nào? Là khi ước lẻ hợp lệ lớn nhất tìm được rất nhỏ, ví dụ $M$ là số nguyên tố thì chỉ có ước 1, vòng lặp phải đi hết từ $\sqrt{M}$ về 1. Với $M$ lớn nhất là $10^{16}$ thì $\sqrt{10^{16}} = 10^8$, số lần lặp tối đa là $10^8 / 2 = 5 \cdot 10^7$, vẫn nằm trong ngưỡng nên yên tâm không bị TLE. Còn bình thường thì vòng lặp sẽ kết thúc sớm hơn nhiều.
- 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