Lời Giải HSG Tin Tỉnh Quảng Ninh (B) - 2023
Bài 1. Tài khoản
An có một phần mềm máy tính, người dùng muốn sử dụng được phần mềm này thì phải đăng ký tài khoản.
Tuy nhiên với số lượng người dùng ngày càng nhiều, dẫn tới việc có thể người dùng đăng ký tài khoản trùng nhau (tài khoản người đăng ký sau trùng với tài khoản người đã đăng ký trước đó).
Để xử lí việc này An nghĩ ra một ý tưởng như sau:
- Nếu tên tài người dùng đăng ký chưa được lưu trong hệ thống thì người dùng sẽ được đăng ký với tên tài khoản đó.
- Nếu tên tài khoản người dùng muốn đăng ký trùng với tên tài khoản đã được đăng ký trước đó thì hệ sống sẽ tự động thêm một số nguyên dương nhỏ nhất vào sau tên tài khoản đó sao cho tên tài khoản đó chưa được đăng ký.
Ví dụ giả sử trong hệ thống đã tồn tại tên mà người dùng tiếp theo đăng ký trùng tên thì sẽ được lưu tên tài khoản là , tương tự như vậy trong trường hợp có thêm người dùng đăng ký trùng tên thì hệ thống sẽ lưu tiếp theo là , …
Yêu cầu
Bạn hãy giúp An lập trình để thực hiện ý tưởng của mình.
Dữ liệu
Vào từ tệp văn bản acco.inp:
- Dòng đầu tiên chứa số nguyên dương là số lượng tên tài khoản mà người dùng muốn đăng ký.
- Dòng thứ trong dòng tiếp theo chứa một xâu kí tự chỉ gồm các chữ cái tiếng Anh thường và có độ dài không quá 10 kí tự là tên các tài khoản muốn đăng ký.
Kết quả
Ghi ra tệp văn bản acco.out gồm dòng là kết quả các tài khoản của người dùng được lưu trong hệ thống tương ứng với dữ liệu vào.
Ví dụ
| acco.inp | acco.out |
|---|---|
| 10 ngoc mai hung mai dung mai huy ngoc mai mai | ngoc mai hung mai1 dung mai2 huy ngoc1 mai3 mai4 |
Ràng buộc
- Có số test ứng với số điểm của bài thoả mãn: .
- số test khác ứng với số điểm của bài thoả mãn: .
- số test còn lại ứng với số điểm của bài không có thêm ràng buộc nào.
Lời giải
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
#define nl '\n'
int main()
{
int n;
cin >> n;
map<string, int> mp;
for (int i = 0; i < n; i++)
{
string ten;
cin >> ten;
if (mp.find(ten) != mp.end())
cout << ten << mp[ten] << nl;
else
cout << ten << nl;
mp[ten]++;
}
return 0;
}Bài 2. Tổng ước
Cho một số nguyên dương .
Yêu cầu
Đếm các số nguyên dương trong đoạn mà có tổng các ước nguyên dương là số nguyên tố.
Dữ liệu
Vào từ tệp văn bản prim.inp một số nguyên dương duy nhất .
Kết quả
Ghi ra tệp văn bản prim.out một số nguyên duy nhất là số các số nguyên trong đoạn mà có tổng các ước là số nguyên tố.
Ví dụ
| prim.inp | prim.out |
|---|---|
| 10 | 3 |
Lời giải
Thuật toán ngây thơ 1
Với từng từ , ta tính tổng các ước của . Sau đó kiểm tra xem tổng đó có phải là số nguyên tố hay không.
Chi phí tính tổng các ước là: .
Thực tế, tổng các ước nguyên dương của một số nguyên sẽ nằm trong khoảng . Và khi sử dụng thuật toán ngây thơ để in ra các tổng ước mà là số nguyên tố thì ta nhận thấy tổng đó . Vậy chi phí kiểm tra tính nguyên tố sẽ là: .
Và ta thực hiện 2 việc đó lần từ , nên độ phức tạp cuối cùng của thuật toán là: .
Với ràng buộc thì thuật toán này không thể nào được full điểm.
Thuật toán ngây thơ 2
Xét ví dụ với , ta sử dụng thuật ngây thơ 1 để in ra những số thoả mãn, , thừa số nguyên tố của , tổng các ước của .
4 2 2^1 7
9 3 3^1 13
16 4 2^2 31
25 5 5^1 31
64 8 2^3 127
289 17 17^1 307
729 27 3^3 1093
1681 41 41^1 1723
2401 49 7^2 2801
3481 59 59^1 3541
4096 64 2^6 8191
5041 71 71^1 5113
7921 89 89^1 8011Ta nhìn thấy rằng các số thoả mãn ở đây đều là các số chính phương. Và đặc biệt hơn nữa chỉ có duy nhất ước nguyên tố.
Vậy việc tính tổng các ước của sẽ trở lên vô cùng nhanh chóng bằng cách sử dụng Sàng Eratosthenes để tìm ước nguyên tố nhỏ nhất của nhiều số trong .
Giả sử . Ta có tổng các ước là:
Đây là tổng cấp số nhân nên ta có thể tính tổng đó trong :
Thay vì duyệt từ , ta chỉ cần duyệt từ , sau đó cộng vào kết quả (tính thêm số ).
Ở đây có lẽ chúng ta nghĩ sao không sử dụng sàng nguyên tố để kiểm tra tính nguyên tố của tổng. Tuy nhiên, tổng các ước có thể rất lớn nên việc khai báo sàng sẽ rất tốn bộ nhớ và không phù hợp. Thế nên ta ở thuật toán ngây thơ này, ta vẫn sử dụng giải thuật để kiểm tra tính nguyên tố.
Vậy độ phức tạp là: . Và thuật toán này đã đủ tốt để chạy được full điểm.
Thuật toán tối ưu
Tới đây đã tối ưu hết các bước trừ việc kiểm tra tính nguyên tố của tổng ước. Vì tổng ước có thể rất lớn, nên sàng nguyên tố là không phù hợp. Do vậy ta sử dụng phép Kiểm tra Miller-Rabin với cơ số là: .
Độ phức tạp chỉ còn: .
Ta có thể dễ dàng chạy đến , thậm chí còn hơn.
Code
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
#define nl '\n'
const int maxn = 1e6;
vector<int> minPrime(maxn + 1, 0);
void sieve()
{
for (int i = 2; i * i <= maxn; i++)
{
if (minPrime[i] == 0)
{
for (int j = i * i; j <= maxn; j += i)
{
if (minPrime[j] == 0)
minPrime[j] = i;
}
}
}
for (int i = 2; i <= maxn; i++)
{
if (minPrime[i] == 0)
minPrime[i] = i;
}
}
int prime_power(ll num)
{
int k = 0, p = minPrime[num];
while (num % p == 0)
{
num /= p;
k++;
}
return (num == 1 ? k : -1);
}
ll sum_of_divisor(ll num)
{
ll q = minPrime[num];
ll qn = num * num * q;
return (qn - 1) / (q - 1);
}
ll power(ll a, ll b, ll m)
{
a %= m;
ll res = 1;
while (b > 0)
{
if (b & 1)
res = (res * a) % m;
b >>= 1;
a = (a * a) % m;
}
return res;
}
bool test(ll a, ll n, ll k, ll m)
{
ll mod = power(a, m, n);
if (mod == 1 || mod == n - 1)
return true;
for (int l = 1; l < k; l++)
{
mod = (mod * mod) % n;
if (mod == n - 1)
return true;
}
return false;
}
bool RabinMiller(ll n)
{
if (n == 2 || n == 3 || n == 5 || n == 7)
return true;
if (n < 11)
return false;
ll k = 0, m = n - 1;
while (!(m & 1))
{
m >>= 1;
k++;
}
vector<int> checkSet = {2, 3, 5, 7, 61};
for (auto a : checkSet)
{
if (!test(a, n, k, m))
return false;
}
return true;
}
int solve(ll n)
{
if (n < 2)
return 0;
if (n < 4)
return 1;
int ans = 1;
for (ll i = 2; i * i <= n; i++)
{
if (prime_power(i) == -1)
continue;
if (RabinMiller(sum_of_divisor(i)))
ans++;
}
return ans;
}
int main()
{
sieve();
ll n;
cin >> n;
cout << solve(n);
return 0;
}Bài 3. Khai thác radium
Để thăm dò địa chất trước khi khai thác radium trên cao nguyên , một vệ tinh đặc biệt đã được phóng lên quỹ đạo giúp đo mức độ phóng xạ trên bề mặt.
Chúng ta biểu diễn cao nguyên dưới dạng một hình chữ nhật bao gồm ô vuông đơn vị. Ký hiệu ô vuông trong hàng thứ và cột thứ là .
Kết quả của việc khảo sát cao nguyên đã xác định được mức độ phóng xạ cho từng ô vuông. Mức độ phóng xạ của ô được cho bởi một số nguyên dương . Độ chính xác của phép đo cao đến mức tất cả các số đều khác nhau. Ô được coi là phù hợp để khai thác radium nếu giá trị của lớn nhất ở hàng thứ và cũng lớn nhất ở cột thứ .
Trong quá trình đo, lần tinh chỉnh liên tiếp mức độ phóng xạ đã được thực hiện. Cụ thể, lần tinh chỉnh thứ đã thay đổi giá trị của thành một giá trị hoàn toàn lớn hơn. Hơn nữa, sau mỗi lần tinh chỉnh, tất cả các giá trị của vẫn khác nhau.
Yêu cầu
Cho các giá trị ban đầu và danh sách các lần tinh chỉnh, bạn hãy viết một chương trình xác định số ô phù hợp để khai thác radium sau mỗi lần tinh chỉnh.
Dữ liệu
Vào từ tệp văn bản radi.inp:
- Dòng đầu tiên chứa ba số nguyên dương và tương ứng là số hàng, số cột của hình chữ nhật và số lần tinh chỉnh.
- Mỗi dòng trong dòng tiếp theo chứa số nguyên dương, số thứ trong dòng thứ là giá trị ban đầu của tất cả các đều khác nhau .
- Tiếp theo, có dòng mô tả các tinh chỉnh mức độ phóng xạ, dòng thứ chứa ba số nguyên mô tả sự thay đổi mức độ phóng xạ của ô thành giá trị mới là .
- Dữ liệu đảm bảo rằng hoàn toàn lớn hơn mức độ phóng xạ trước đó trong ô này và tất cả các mức độ phóng xạ đều khác nhau sau mỗi lần thay đổi.
Kết quả
Ghi ra tệp văn bản radi.out gồm dòng, ở dòng thứ chứa một số nguyên là số ô phù hợp để khai thác radium sau lần tinh chỉnh thứ .
Ví dụ
| radi.inp | radi.out |
|---|---|
| 2 3 3 1 4 3 6 5 2 2 2 9 1 3 5 2 2 10 | 1 2 2 |
Giải thích:
- Trong lần tinh chỉnh thứ nhất, ta thay đổi mức độ phóng xạ của ô từ thành . Sau lần tinh chỉnh này, mức độ phóng xạ trong các ô như sau và chỉ có 1 ô phù hợp là .
1 4 3
6 9 2 - Trong lần tinh chỉnh thứ hai, ta thay đổi mức độ phóng xạ của ô từ thành . Sau lần tinh chỉnh này, mức độ phóng xạ trong các ô như sau và có 2 ô phù hợp là và .
1 4 5
6 9 2 - Trong lần tinh chỉnh thứ ba, ta thay đổi mức độ phóng xạ của ô từ thành . Sau lần tinh chỉnh này, mức độ phóng xạ trong các ô như sau và có 2 ô phù hợp là và .
1 4 5
6 10 2
Ràng buộc
- Có số test ứng với số điểm của bài toán thoả mãn: và .
- số test khác ứng với số điểm của bài thoả mãn: và .
- số test khác ứng với số điểm của bài thoả mãn: và .
- số test còn lại ứng với số điểm của bài không có thêm ràng buộc nào.
Lời giải
Theo đề bài, ô được coi là phù hợp để khai thác radium nếu giá trị của là lớn nhất ở hàng thứ và cũng lớn nhất ở cột thứ .
Mặt khác, tất cả các giá trị của đều khác nhau, kể cả khi sau tinh chỉnh.
Do vậy, 2 ô khai thác được sẽ không bao giờ nằm cùng hàng hay cùng cột.
Ta cần khai báo các mảng sau:
- : vị trí trong hàng của ô khai thác được ở hàng từ trái sang. Nếu trong hàng không có ô nào khai thác được thì .
- : vị trí trong cột của ô khai thác được ở cột từ trên xuống. Nếu trong cột không có ô nào khai thác được thì .
- : mức độ phóng xạ lớn nhất ở hàng .
- : mức độ phóng xạ lớn nhất ở cột .
Từ đây ta duyệt từng ô và đếm xem ban đầu có bao nhiêu ô phù hợp để khai thác.
Ta có là ô phù hợp nếu:
⇒ Kéo theo là và .
Khi tinh chỉnh mức độ phóng xạ là tại ô thì có 2 trường hợp sau:
- Trường hợp 1: Ô đã là ô có thể khai thác được ( và ). Thì sau khi tinh chỉnh với mức độ phóng xạ lớn hơn thì vẫn là ô có thể khai thác được, nên tổng số ô phù hợp để khai thác là không thay đổi.
- Trường hợp 2: Ô chưa phải ô khai thác được. Nên sau khi tỉnh chỉnh với mức độ phóng xạ lớn hơn thì ta lại có các trường hợp sau:
Trường hợp 2.1: Ô trở thành ô khai thác được nếu:
Lúc này ta cập nhật .
Như đã giải thích, 2 ô khai thác được sẽ không bao giờ nằm cùng hàng hay cùng cột, nên nhiều nhất 1 ô khai thác được ở hàng và nhiều nhất 1 ô khai thác được ở cột sau lần tinh chỉnh này thì không còn khai thác được nữa.
Nếu có 1 ô khai thác được ở hàng nghĩa là và . Sau khi tinh chỉnh thì ô đó không còn khai thác được, vậy tổng số ô khai thác được giảm đi .
Tương tự, nếu có 1 ô khai thác được ở cột nghĩa là và . Sau khi tinh chỉnh thì ô đó không còn khai thác được, vậy tổng số ô khai thác cũng giảm đi .
Bây giờ là ô khai thác được nên và .
Trường hợp 2.2: Ô vẫn chưa trở thành ô khai thác được vì không thoả mãn điều kiện: và .
- Nếu đều nhỏ hơn và thì không có điều gì thay đổi.
- Nếu chỉ lớn hơn , thì ô khai thác được ở hàng sẽ không còn khai thác được nữa. Số ô khai thác được giảm đi .
- Nếu chỉ lớn hơn , thì ô khai thác được ở cột sẽ không còn khai thác được nữa. Số ô khai thác được giảm đi .
Độ phức tạp là: .
Code
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
#define nl '\n'
int main()
{
int m, n, q;
cin >> m >> n >> q;
int a[m][n];
const int INF = 1e9;
vector<int> r(m, -1), c(n, -1);
vector<int> r_max(m, -INF), c_max(n, -INF);
for (int i = 0; i < m; i++)
{
for (int j = 0; j < n; j++)
cin >> a[i][j];
}
for (int i = 0; i < m; i++)
{
for (int j = 0; j < n; j++)
r_max[i] = max(r_max[i], a[i][j]);
}
for (int j = 0; j < n; j++)
{
for (int i = 0; i < m; i++)
c_max[j] = max(c_max[j], a[i][j]);
}
int res = 0;
for (int i = 0; i < m; i++)
{
for (int j = 0; j < n; j++)
{
if (a[i][j] == r_max[i] && a[i][j] == c_max[j])
{
r[i] = j;
c[j] = i;
res++;
}
}
}
while (q--)
{
int i, j, x;
cin >> i >> j >> x;
i--, j--;
a[i][j] = x;
if (r[i] == j && c[j] == i)
{
r_max[i] = c_max[j] = x;
}
else
{
if (x > max(r_max[i], c_max[j]))
{
res++;
r_max[i] = c_max[j] = x;
if (r[i] != -1 && c[r[i]] != -1)
{
res--;
c[r[i]] = -1;
}
if (c[j] != -1 && r[c[j]] != -1)
{
res--;
r[c[j]] = -1;
}
r[i] = j;
c[j] = i;
}
else if (x > r_max[i])
{
r_max[i] = x;
if (r[i] != -1 && c[r[i]] != -1)
{
res--;
r[i] = c[r[i]] = -1;
}
}
else if (x > c_max[j])
{
c_max[j] = x;
if (c[j] != -1 && r[c[j]] != -1)
{
res--;
c[j] = r[c[j]] = -1;
}
}
}
cout << res << nl;
}
return 0;
}Bài 4. Khôi phục bức tường
Bạn phải khôi phục lại bức tường. Bức tường gồm cột gạch, chiều cao cột thứ ban đầu bằng , chiều cao được đo bằng số viên gạch. Sau khi khôi phục, tất cả cột phải có chiều cao bằng nhau.
Bạn được phép thực hiện các thao tác sau:
- Đặt một viên gạch lên trên đỉnh một cột, chi phí của thao tác này là .
- Dỡ một viên gạch khỏi đỉnh một cột không trống, chi phí cho thao tác này là .
- Di chuyển một viên gạch từ đỉnh một cột không trống này sang đỉnh một cột khác, chi phí cho thao tác này là .
Bạn không thể tạo thêm cột hoặc bỏ qua cột tồn tại trước đó ngay cả khi chiều cao của nó trở thành .
Yêu cầu
Bạn hãy tính tổng chi phí khôi phục tối thiểu, hay nói cách khác là tính tổng chi phí tối thiểu để làm tất cả các cột có chiều cao bằng nhau.
Dữ liệu
Vào từ tệp văn bản rest.inp:
- Dòng đầu tiên chứa số nguyên tương ứng là số cột và chi phí các thao tác đặt, dỡ, di chuyển một viên gạch.
- Dòng thứ hai chứa số nguyên là độ cao các cột.
Kết quả
Ghi ra tệp rest.out một số nguyên là chi phí khôi phục tối thiểu.
Ví dụ
| rest.inp | rest.out |
|---|---|
| 5 1 2 4 5 5 3 6 5 | 4 |
| 5 1 2 2 5 5 3 6 5 | 3 |
Giải thích:
Trong ví dụ thứ nhất, một cách khôi phục bức tường với chi phí tối thiểu là:
- Đặt một viên gạch lên đỉnh cột thứ , chi phí cho thao tác này là . Bây giờ độ cao của các cột là: .
- Tiếp tục đặt một viên gạch lên đỉnh cột thứ , chi phí cho thao tác này là . Bây giờ độ cao của các cột là: .
- Dỡ một viên gạch khỏi đỉnh cột thứ , chi phí cho thao tác này là . Bây giờ độ cao của các cột là: .
⇒ Sau các thao tác trên, tất cả các cột đều có chiều cao bằng nhau và tổng chi phí là .
Trong ví dụ thứ hai, một cách khôi phục bức tường với chi phí tối thiểu là:
- Di chuyển một viên gạch từ đỉnh cột thứ sang đỉnh cột thứ , chi phí cho thao tác này là . Bây giờ độ cao của các cột là: .
- Đặt một viên gạch lên đỉnh cột thứ , chi phí cho thao tác này là . Bây giờ độ cao của các cột là: .
⇒ Sau các thao tác trên, tất cả các cột đều có chiều cao bằng nhau và tổng chi phí là .
Ràng buộc
- Có số test ứng với số điểm của bài thoả mãn: và .
- số test khác ứng với số điểm của bài thoả mãn: và .
- số test khác ứng với số điểm của bài thoả mãn: .
- số test còn lại ứng với số điểm của bài không có thêm ràng buộc nào.
Lời giải
Ý tưởng
Ta sẽ duyệt từng độ cao, và đi tìm chi phí nhỏ nhất để khôi phục lại bức tường đến độ cao đó. Kết quả của bài toán chính là giá trị nhỏ nhất của những chi phí khôi phục đó.
Trước hết, ta cần tìm ra cách để khôi phục bức tường đến độ cao . Ta sẽ sắp xếp mảng độ cao của các cột theo thứ tự không giảm. Sau đó thực hiện các thao tác để giảm đi số viên gạch ở những cột cao hơn , và tăng thêm số viên gạch ở những cột thấp hơn .
Lưu ý rằng, ta không được tạo thêm cột hay bỏ qua những cột có độ cao là .
Ta thấy rằng có 2 trường hợp là:
- : tổng chi phí đặt và gỡ ít hơn hoặc bằng chi phí di chuyển, ta nên gỡ đi những viên gạch ở cột cao hơn và đặt thêm vào những cột thấp hơn.
- : tổng chi phí đặt và gỡ nhiều hơn phí di chuyển, ta nên di chuyển những viên gạch ở cột cao hơn sang cột thấp hơn.
Ta cần tìm vị trí sao cho: độ cao các cột từ đều , độ cao các cột từ đều . Tiếp đến ta sử dụng mảng cộng dồn để tính xem từ có tổng cộng bao nhiêu viên gạch.
Gọi là tổng số viên gạch cần thêm vào những cột thấp hơn hoặc bằng , ta có công thức:
Và là tổng số viên gạch cần bớt đi ở những cột cao hơn , ta có công thức:
Từ đây ta có trường hợp khi khôi phục bức tường đến độ cao :
- Nếu thì chi phí khôi phục tối ưu sẽ là: .
- Nếu thì sau khi thực hiện các thao tác đặt, gỡ và chuyển thì tổng số viên gạch cần bớt đi ở những cột cao hơn chỉ còn , nên chi phí khôi phục là: .
- Nếu thì cũng tương tự, còn viên gạch cần thêm ở những cột thấp hơn , nên chi phí khôi phục là: .
Ta đã thực hiện những công việc sau:
- Sắp xếp: ,
- Duyệt từng độ cao với là hằng số chiều cao lớn nhất của các cột ban đầu,
- Tìm kiếm vị trí với ,
- Tính chi phí khôi phục trong .
⇒ Độ phức tạp của thuật toán là:
Tối ưu
Với thì thuật toán trên không thể nào được full điểm. Ta mất chi phí nhiều nhất ở việc duyệt qua từng độ cao , vậy nên ta phải tối ưu bằng cách chỉ duyệt những độ cao cần thiết.
Khi duyệt từng độ cao từ và in ra chi phí khôi phục bức tường đến độ cao tương ứng thì ta thấy rằng, chi phí khôi phục có xu hướng giảm đến một giá trị nào đó rồi lại tăng lên. Nghĩa là đường biểu diễn chi phí của từng độ cao là một đường cong đi xuống rồi đi lên.
Vậy kết quả của bài toán chính là điểm cực tiểu của đường cong đó.
Ta tìm điểm cực tiểu đó bằng tìm kiếm tam phân. Ở đây, ta chọn cận dưới và cận trên để đảm bảo là luôn có kết quả.
Tới đây ta đã tối ưu bằng cách chỉ xét những độ cao cần thiết.
Độ phức tạp của thuật toán chỉ còn: .
Code
#include <bits/stdc++.h>
using namespace std;
typedef unsigned long long ll;
#define nl '\n'
const int maxn = 1e5 + 5;
int n;
ll a, r, m;
ll h[maxn], pref[maxn];
ll cost(ll height)
{
ll p = upper_bound(h, h + n + 1, height) - h - 1;
ll A = p * height - pref[p];
ll B = pref[n] - pref[p] - (n - p) * height;
if (A == B)
return A * min(a + r, m);
else if (A < B)
return A * min(a + r, m) + (B - A) * r;
return B * min(a + r, m) + (A - B) * a;
}
ll ternarySearch()
{
ll low = 0, high = 1e10;
while (high - low >= 3)
{
ll mid1 = low + (high - low) / 3;
ll mid2 = high - (high - low) / 3;
ll cost1 = cost(mid1);
ll cost2 = cost(mid2);
if (cost1 == cost2)
{
low = mid1;
high = mid2;
}
else if (cost1 < cost2)
{
high = mid2 - 1;
}
else
{
low = mid1 + 1;
}
}
ll ans = cost(low);
for (ll i = low + 1; i <= high; i++)
{
ans = min(ans, cost(i));
}
return ans;
}
int main()
{
cin >> n >> a >> r >> m;
h[0] = pref[0] = 0;
for (int i = 1; i <= n; i++)
cin >> h[i];
sort(h, h + n + 1);
for (int i = 1; i <= n; i++)
pref[i] = pref[i - 1] + h[i];
cout << ternarySearch();
return 0;
}