Lời Giải HSG Tin Tỉnh Quảng Ninh (B) - 2016

November 4, 2025 August 16, 2024
Author Author Hung Nguyen Tuong

Bài 1. Số chính phương đặc biệt

Giáo sư Minh mới tìm ra loại số rất đặc biệt và đặt tên là số CPDB. Một số nguyên dương nn được gọi là số CPDB nếu nn thoả mãn hai tính chất sau:

  • nn chia hết cho 33.
  • nn có đúng 99 ước số.

Yêu cầu

Giáo sư muốn khảo sát mật độ các số CPDB nên nhờ các bạn tham dự kỳ thi chọn học sinh giỏi lập trình giải quyết bài toán sau: “Cho hai số nguyên không âm a,ba, b, hãy đếm số lượng số CPDB trong đoạn [a,b][a,b].

Dữ liệu

Vào từ file văn bản CPDB.INP gồm đúng 2 số tự nhiên a,ba,b viết cách nhau.

Kết quả

Ghi ra file văn bản CPDB.OUT gồm số tự nhiên duy nhất là số lượng các số trong đoạn [a,b][a,b] có tính chất nêu trên.

Ví dụ

CPDB.INPCPDB.OUTGiải thích
15 1001Số 36 chia hết cho 3 và có 9 ước là: 1, 2, 3, 4, 6, 9, 12, 18, 36.

Ràng buộc

  • 70%70\% số test ứng với 70%70\% số điểm của bài có ab2×104a \le b \le 2 \times 10^4.
  • 20%20\% số test ứng với 20%20\% số điểm của bài có ab3×104a \le b \le 3 \times 10^4.
  • 10%10\% số test khác ứng với 10%10\% số điểm của bài có ab3×107a \le b \le 3 \times 10^7.

Lời giải

Trước hết hãy viết và chạy thuật toán trâu để tìm ra quy luật của bài toán.

Giả sử với a=1, b=10000a=1, \ b=10000. Ta lần lượt in ra các số thoả mãn là:

36 225 441 1089 1521 2601 3249 4761 6561 7569 8649 36\ 225\ 441\ 1089\ 1521\ 2601\ 3249\ 4761\ 6561\ 7569\ 8649

Để ý rằng đây đều là các số chính phương với căn bậc 2 của chúng là:

6 15 21 33 39 51 57 69 81 87 93 6\ 15\ 21\ 33\ 39\ 51\ 57\ 69\ 81\ 87\ 93

Từ đây ta có thể thu nhỏ khoảng tính toán từ 3×1073 \times 10^7 xuống còn 3×1075500\sqrt{3 \times 10^7} \approx 5500. Thay vì xét từ aba \to b, ta chỉ cần xét từ ab\sqrt a \to \sqrt b.

Xét tính chất 1: Chia hết cho 33. Nếu nn chia hết cho 33 thì hiển nhiên n2n^2 cũng phải chia hết cho 33.

Xét tính chất 2: Có 99 ước. Xét nn chia hết cho 3, ta phân tích thừa số nguyên tố:

n=2a1×3a2×5a3× (a21) n=2^{a_1} \times 3^{a_2} \times 5^{a_3} \times \cdots \ (a_2 \ge 1)

Để n2n^2 có đúng 99 ước thì chắc chắn nn phải có dạng 31×p13^1 \times p^1 (pp là số nguyên tố 3\neq 3) hoặc n=34=81n = 3^4 = 81 (Số ước số).

Tóm lại số CPDB nn sẽ thoả mãn 1 trong 2 điều sau:

  • n=81n=81.
  • n9n \neq 9, nn chia hết cho 33n/3n / 3 là số nguyên tố.

Độ phức tạp: O(k+k×log(logk))O(k + k \times \log(\log k)) với kbk \approx \sqrt b.

Code

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
#define nl '\n'
const int maxn = 1e4;
vector<bool> sieve(maxn, true);
void sang(){
    sieve[0] = sieve[1] = false;
    for (int i=2;i*i<=maxn;i++){
        if (sieve[i]){
            for (int j=i*i;j<=maxn;j+=i) sieve[j] = false;
        }
    }
}
bool check(int i){
    if (i==81) return true;
    if (i==9) return false;
    return (i%3==0 && sieve[i/3]);
}
int main()
{
    sang();
    int a, b; cin >> a >> b;
    int ans = 0;
    a = ceil(sqrt(a)), b = floor(sqrt(b));
    for (int i=max(1, a);i<=b;i++){
        if (check(i)) ans++;
    }
    cout << ans;
    return 0;
}

Bài 2. Dãy con

Cho dãy số AA gồm nn phần tử A1,A2,...,AnA_1,A_2,...,A_n và một số nguyên dương d (1dn)d \ (1 \le d \le n). Hãy tìm một đoạn con liên tiếp của dãy AA có độ dài dd và có tổng các phần tử đạt giá trị lớn nhất (Độ dài của đoạn con là số lượng phần tử trên đoạn con đó).

Yêu cầu

Tính tổng các phần tử trên đoạn con theo yêu cầu như trên.

Dữ liệu

Từ tệp DAYCON.INP có cấu trúc như sau:

  • Dòng thứ nhất chứa hai số nguyên dương n,d (1dn106)n,d \ (1 \le d \le n \le 10^6).
  • Dòng thứ hai chứa nn số nguyên A1,A2,...,AnA_1,A_2,...,A_n trong đó (Ai104, 1in)(|A_i| \le 10^4, \ 1 \le i \le n).
  • Các số nguyên trên cùng một dòng viết cách nhau bởi một dấu cách (space).

Kết quả

Ghi vào tệp DAYCON.OUT gồm một số nguyên duy nhất là tổng các phần tử trên đoạn con tìm được có giá trị lớn nhất.

Ví dụ

DAYCON.INPDAYCON.OUTGiải thích
5 3
-4 3 -2 6 5
9Các đoạn con có độ dài 3 là: (-4,3,-2), (3,-2,6), (-2,6,5). Nên có tổng lớn nhất là 9.

Ràng buộc

  • 45%45\% điểm tương ứng với trường hợp d3000d \le 3000 n104n \le 10^4.
  • 45%45\% điểm tương ứng với trường hợp d1000d \le 1000 n=105n = 10^5.
  • 10%10\% điểm tương ứng với trường hợp d1000d \le 1000 n=106n = 10^6.

Lời giải

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
#define nl '\n'

int main()
{
    int n, d; cin >> n >> d;
    ll a[n], current = 0;
    for (int i=0;i<n;i++){
        cin >> a[i];
        if (i < d) current += a[i];
    }
    ll ans = current;
    for (int i=d;i<n;i++){
        current = current + a[i] - a[i-d];
        ans = max(ans, current);
    }
    cout << ans;
    return 0;
}

Bài 3. Xâu đối xứng

Một trong những vấn đề quan trọng của Tin học là xâu (string). Những kiến thức, thuật toán mới luôn được tìm tòi, phát triển nhanh chóng.

Chắc hẳn bạn cũng đã từng nghe qua về xâu đối xứng. Xâu đối xứng có tính chất: đọc nó từ phải sang trái cũng thu được kết quả giống như đọc từ trái sang phải (còn gọi là xâu palindrome).

Ví dụ về xâu đối xứng như: thht, omo, ioi,...thht, \ omo, \ ioi,....

Tham dự Hội thi tại trường THPT X, bạn Minh đăng ký tham dự trò chơi RCV, phần thưởng cho người chiến thắng là một chiếc thẻ nhớ 16GB từ ban tổ chức (BTC).

Trò chơi như sau: BTC đưa ra một chuỗi các kí tự chỉ bao gồm các chữ cái in thường từ aa đến zz, và một số bộ L,RL, R. Người chơi sẽ trả lời một bộ các câu hỏi của BTC có dạng: “xâu con gồm các kí tự liên tiếp của xâu đã cho từ vị trí LL đến RR có đối xứng hay không?”.

Yêu cầu

Bạn hãy giúp Minh dành chiến thắng cuộc thi này.

Dữ liệu

Từ tệp D_XUNG.INP có cấu trúc như sau:

  • Dòng đầu ghi xâu kí tự không cho biết độ dài.
  • Dòng tiếp theo ghi số nguyên dương kk - số câu hỏi của BTC.
  • kk dòng tiếp theo, mỗi dòng ghi hai số nguyên L,RL,R.

Kết quả

Ghi vào tệp D_XUNG.OUT gồm kk dòng, mỗi dòng ghi tương ứng câu trả lời, nếu có ghi yesyes, ngược lại ghi nono.

Ví dụ

D_XUNG.INPD_XUNG.OUTGiải thích
abxbagrednooojhggohoreomobiabba
3
1 5
10 14
28 31
yes
no
yes
Xâu abxba đối xứng
Xâu noooj không đối xứng
Xâu abba đối
xứng

Ràng buộc

  • 50%50\% số test có độ dài xâu 255.\le 255.
  • 30%30\% số test có độ dài xâu 103\le 10^3.
  • Trong tất cả các test độ dài xâu không quá 10510^5k105, 1LRk \le 10^5, \ 1 \le L \le R.

Lời giải

Ý tưởng của thuật toán là với mỗi truy vấn, ta dùng 2 con trỏ chạy từ đầu và cuối của xâu con s[L...R]s[L...R] đến khi gặp nhau.

Nếu ký tự của ss tại 2 con trỏ đó khác nhau thì ta kết luận được ngay đó không phải là xâu đối xứng.

Nếu 2 con trỏ đó gặp được nhau, nghĩa là không có cặp ký tự nào khác nhau trong quá trình kiểm tra thì đó là xâu đối xứng.

Với độ phức tạp khá lớn: O(k×n)O(k \times n) với nn là độ dài xâu, thì ta chỉ được 80%80\% số điểm của bài.

Để lấy được 20%20\% số điểm còn lại, ta sử dụng kỹ thuật String Hashing.

Đầu tiên ta chọn p=31, m=109+9p=31, \ m=10^9 + 9.

Tiếp theo, ta tính trước các mảng sau:

  • p_pow[i]\text{p\_pow} [i]: luỹ thừa modulo mm của pip^i.
  • prefix_hash[i]\text{prefix\_hash}[i]: hash của xâu con bắt đầu tại 00 và kết thúc tại i1i-1.
  • suffix_hash[i]\text{suffix\_hash}[i]: hash của xâu con bắt đầu tại i1i-1 và kết thúc tại n1n-1.

Xâu con s[L...R]s[L...R] là xâu đối xứng khi và chỉ khi hash của s[L...R]s[L...R] bằng với hash của đảo ngược xâu đó. Nghĩa là:

(prefix_hash[R]prefix_hash[L1])×pnL=(suffix_hash[L]suffix_hash[R+1])×pR1 (\text{prefix\_hash}[R] - \text{prefix\_hash}[L-1]) \times p^{n-L} = (\text{suffix\_hash}[L] - \text{suffix\_hash}[R+1]) \times p^{R-1}

Chi phí tính toán trước chỉ là O(n)O(n), và với mỗi truy vấn ta có thể kiểm tra đối xứng trong O(1)O(1). Vậy độ phức tạp của thuật toán chỉ còn: O(max(n,k))O(\max(n,k)).

Code

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
#define nl '\n'
string s; int n;

const int maxn = 1e5+5, p = 31, m = 1e9+9;
ll prefix_hash[maxn], suffix_hash[maxn], p_pow[maxn];
void precompute(){
    n = s.size();

    p_pow[0] = 1;
    for (int i=1;i<n;i++){
        p_pow[i] = (p_pow[i-1] * p) % m;
    }

    prefix_hash[0] = 0;
    for (int i=0;i<n;i++){
        prefix_hash[i+1] = (prefix_hash[i] + (s[i] - 'a' + 1) * p_pow[i]) % m;
    }
    
    suffix_hash[n+1] = 0;
    for (int i=n-1;i>=0;i--){
        suffix_hash[i+1] = (suffix_hash[i+2] + (s[i] - 'a' + 1) * p_pow[n-i-1]) % m;
    }
}
string solve(int l, int r){
    if (l==r) return "yes";

    ll left_hash = (prefix_hash[r] - prefix_hash[l-1] + m) % m;
    left_hash = (left_hash * p_pow[n-l]) % m;

    ll right_hash = (suffix_hash[l] - suffix_hash[r+1] + m) % m;
    right_hash = (right_hash * p_pow[r-1]) % m;

    return (left_hash == right_hash ? "yes" : "no");
}

int main()
{
    cin >> s;
    precompute();
    int k; cin >> k;
    while (k--){
        int l, r; cin >> l >> r;
        cout << solve(l, r) << nl;
    }
    return 0;
}