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

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

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 "tinhoc"\text{"tinhoc"} 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à "tinhoc1"\text{"tinhoc1"}, tương tự như vậy trong trường hợp có thêm người dùng đăng ký trùng tên "tinhoc"\text{"tinhoc"} thì hệ thống sẽ lưu tiếp theo là "tinhoc2"\text{"tinhoc2"}, "tinhoc3"\text{"tinhoc3"}

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 n (1n104)n \ ( 1 \le n \le 10^4) là số lượng tên tài khoản mà người dùng muốn đăng ký.
  • Dòng thứ ii trong nn dòng tiếp theo chứa một xâu kí tự si (1in)s_i \ (1 \le i \le n) chỉ gồm các chữ cái tiếng Anh thường a...z'a'...'z' 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 nn 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.inpacco.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

  • 20%20\% số test ứng với 20%20\% số điểm của bài thoả mãn: n102n \le 10^2.
  • 20%20\% số test khác ứng với 20%20\% số điểm của bài thoả mãn: n103n \le 10^3.
  • 60%60\% số test còn lại ứng với 60%60\% 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 n (1n109)n \ (1 \le n \le 10^9).

Yêu cầu

Đếm các số nguyên dương trong đoạn [1,n][1,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 nn.

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 [1,n][1,n] mà có tổng các ước là số nguyên tố.

Ví dụ

prim.inpprim.out
103

Lời giải

Thuật toán ngây thơ 1

Với từng ii từ 1n1 \to n, ta tính tổng các ước của ii. 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à: O(n)O(\sqrt n).

Thực tế, tổng các ước nguyên dương của một số nguyên nn sẽ nằm trong khoảng [1,2n][1,2n]. 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 đó n\approx n. Vậy chi phí kiểm tra tính nguyên tố sẽ là: O(n)O(\sqrt n).

Và ta thực hiện 2 việc đó nn lần từ 1n1 \to n, nên độ phức tạp cuối cùng của thuật toán là: O(nn)O(n \sqrt n).

Với ràng buộc 1n1091 \le n \le 10^9 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 n104n \le 10^4, ta sử dụng thuật ngây thơ 1 để in ra những số nn thoả mãn, n\sqrt n, thừa số nguyên tố của nn, tổng các ước của nn.

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 8011

Ta nhìn thấy rằng các số nn thoả mãn ở đây đều là các số chính phương. Và đặc biệt hơn nữa n\sqrt n chỉ có duy nhất 11 ước nguyên tố.

Vậy việc tính tổng các ước của nn 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ố n\le \sqrt n trong O(1)O(1).

Giả sử n=pkn=p2k\sqrt n = p^k \Rightarrow n = p^{2k}. Ta có tổng các ước là:

1+p1+p2++p2k2+p2k1+p2k 1 + p^1 + p^2 + \cdots +p^{2k-2} + p^{2k-1} + p^{2k}

Đây là tổng cấp số nhân nên ta có thể tính tổng đó trong O(1)O(1):

p2k+11p1=n×p1p1 \frac{p^{2k+1} - 1}{p-1} = \frac{n \times p - 1}{p - 1}

Thay vì duyệt từ 1n1 \to n, ta chỉ cần duyệt từ 2n2 \to \sqrt n, sau đó cộng 11 vào kết quả (tính thêm số 22).

Ở đâ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 O(n)O(\sqrt n) để kiểm tra tính nguyên tố.

Vậy độ phức tạp là: O(n×log(logn)+n×n)O(n)O(\sqrt n \times \log (\log \sqrt n) + \sqrt n \times \sqrt n) \approx O(n). 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=5c = 5 cơ số là: 2,3,5,7,612,3,5,7,61.

Độ phức tạp chỉ còn: O(n×log(logn)+n×clog(n))O(n×logn)O(\sqrt n \times \log (\log \sqrt n) + \sqrt n \times c\log (n)) \approx O(\sqrt n \times \log n).

Ta có thể dễ dàng chạy đến n1012n \le 10^{12}, 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 XX, 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 mnm * n ô vuông đơn vị. Ký hiệu ô vuông trong hàng thứ ii và cột thứ jj(i,j)(i,j).

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 ô (i,j)(i,j) được cho bởi một số nguyên dương aija_{ij}. Độ chính xác của phép đo cao đến mức tất cả các số aija_{ij} đều khác nhau. Ô (i,j)(i,j) được coi là phù hợp để khai thác radium nếu giá trị của aija_{ij} lớn nhất ở hàng thứ ii và cũng lớn nhất ở cột thứ jj.

Trong quá trình đo, qq 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ứ kk đã thay đổi giá trị của arkcka_{r_k c_k} 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 aija_{ij} vẫn khác nhau.

Yêu cầu

Cho các giá trị ban đầu aija_{ij} 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 m,nm,nq (1m×n2×105; 1q2×105)q \ (1 \le m \times n \le 2 \times 10^5; \ 1 \le q \le 2 \times 10^5) 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 mm dòng tiếp theo chứa nn số nguyên dương, số thứ jj trong dòng thứ ii là giá trị ban đầu của aij (1aij107,a_{ij} \ (1 \le a_{ij} \le 10^7, tất cả các aija_{ij} đều khác nhau )).
  • Tiếp theo, có qq dòng mô tả các tinh chỉnh mức độ phóng xạ, dòng thứ kk chứa ba số nguyên rk,ck,xkr_k, c_k, x_k mô tả sự thay đổi mức độ phóng xạ của ô (rk,ck)(r_k,c_k) thành giá trị mới là xk (1rkm; 1ckn; 1xk107)x_k \ (1 \le r_k \le m; \ 1 \le c_k \le n; \ 1 \le x_k \le 10^7).
  • Dữ liệu đảm bảo rằng xkx_k 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 qq dòng, ở dòng thứ kk chứa một số nguyên là số ô phù hợp để khai thác radium sau lần tinh chỉnh thứ kk.

Ví dụ

radi.inpradi.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 ô (2,2)(2,2) từ 55 thành 99. 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à (2,2)(2,2).
    1 4 3
    6 9 2
  • Trong lần tinh chỉnh thứ hai, ta thay đổi mức độ phóng xạ của ô (1,3)(1,3) từ 33 thành 55. 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à (1,3)(1,3)(2,2)(2,2).
    1 4 5
    6 9 2
  • Trong lần tinh chỉnh thứ ba, ta thay đổi mức độ phóng xạ của ô (2,2)(2,2) từ 99 thành 1010. 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à (1,3)(1,3)(2,2)(2,2).
    1 4 5
    6 10 2

Ràng buộc

  • 40%40\% số test ứng với 40%40\% số điểm của bài toán thoả mãn: 1m×n1001 \le m \times n \le 1001q1001 \le q \le 100.
  • 24%24\% số test khác ứng với 24%24\% số điểm của bài thoả mãn: 1m×n50001 \le m\times n \le 50001q50001 \le q \le 5000.
  • 20%20\% số test khác ứng với 20%20\% số điểm của bài thoả mãn: 1m400; 1n4001 \le m \le 400; \ 1 \le n \le 4001q2×1051 \le q \le 2\times 10^5.
  • 16%16\% số test còn lại ứng với 16%16\% 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, ô (i,j)(i,j) được coi là phù hợp để khai thác radium nếu giá trị của aija_{ij} là lớn nhất ở hàng thứ ii và cũng lớn nhất ở cột thứ jj.

Mặt khác, tất cả các giá trị của aija_{ij} đề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:

  • r[i]r[i]: vị trí trong hàng của ô khai thác được ở hàng ii từ trái sang. Nếu trong hàng không có ô nào khai thác được thì r[i]=1r[i] = -1.
  • c[j]c[j]: vị trí trong cột của ô khai thác được ở cột jj từ trên xuống. Nếu trong cột không có ô nào khai thác được thì c[j]=1c[j]=-1.
  • rmax[i]\text{rmax}[i]: mức độ phóng xạ lớn nhất ở hàng ii.
  • cmax[j]\text{cmax}[j]: mức độ phóng xạ lớn nhất ở cột jj.

Từ đây ta duyệt từng ô và đếm xem ban đầu có bao nhiêu ô phù hợp để khai thác.

Ta có (i,j)(i,j) là ô phù hợp nếu:

a[i][j]=rmax[i]=cmax[j] a[i][j] = \text{rmax}[i] = \text{cmax}[j]

⇒ Kéo theo là r[i]=jr[i]=jc[j]=ic[j]=i.

Khi tinh chỉnh mức độ phóng xạ là xx tại ô (i,j)(i,j) thì có 2 trường hợp sau:

  • Trường hợp 1: Ô (i,j)(i,j) đã là ô có thể khai thác được (r[i]=jr[i]=jc[j]=ic[j]=i). Thì sau khi tinh chỉnh với mức độ phóng xạ lớn hơn thì (i,j)(i,j) 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: Ô (i,j)(i,j) 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: Ô (i,j)(i,j) trở thành ô khai thác được nếu:

      x>max(rmax[i],cmax[j]) x > \max(\text{rmax}[i], \text{cmax}[j])

      Lúc này ta cập nhật rmax[i]=cmax[j]=x\text{rmax}[i] = \text{cmax}[j] = x.

      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 ii và nhiều nhất 1 ô khai thác được ở cột jj 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 ii nghĩa là r[i]1r[i] \neq -1 và c[r[i]]1c[r[i]] \neq -1. 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 11.

      Tương tự, nếu có 1 ô khai thác được ở cột jj nghĩa là c[j]1c[j] \neq -1 và r[c[j]]1r[c[j]] \neq -1. 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 11.

      Bây giờ (i,j)(i,j) là ô khai thác được nên r[i]=jr[i]=j và c[j]=ic[j]=i.

    • Trường hợp 2.2: Ô (i,j)(i,j) vẫn chưa trở thành ô khai thác được vì không thoả mãn điều kiện: x>rmax[i]x > \text{rmax}[i] và x>cmax[j]x > \text{cmax}[j].

      • Nếu xx đều nhỏ hơn rmax[i]\text{rmax}[i] và cmax[j]\text{cmax}[j] thì không có điều gì thay đổi.
      • Nếu xx chỉ lớn hơn rmax[i]\text{rmax}[i], thì ô khai thác được ở hàng i (r[i]1, c[r[i]]1)i \ (r[i] \neq -1, \ c[r[i]] \neq -1) sẽ không còn khai thác được nữa. Số ô khai thác được giảm đi 11.
      • Nếu xx chỉ lớn hơn cmax[j]\text{cmax}[j], thì ô khai thác được ở cột j (c[j]1, r[c[j]]1)j \ (c[j] \neq -1, \ r[c[j]] \neq -1) sẽ không còn khai thác được nữa. Số ô khai thác được giảm đi 11.

Độ phức tạp là: O(mn+q)O(mn + q).

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 nn cột gạch, chiều cao cột thứ ii ban đầu bằng hih_i, chiều cao được đo bằng số viên gạch. Sau khi khôi phục, tất cả nn 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à aa.
  • 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à rr.
  • 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à mm.

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 00.

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 44 số nguyên n,a,r,m (1n105; 0a,r,m104)n,a,r,m \ (1 \le n \le 10^5; \ 0 \le a,r,m \le 10^4) 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 nn số nguyên h1,h2,...,hn (0hi109)h_1,h_2,...,h_n \ (0 \le h_i \le 10^9) 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.inprest.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ứ 33, chi phí cho thao tác này là 11. Bây giờ độ cao của các cột là: 5,5,4,6,55,5,4,6,5.
    • Tiếp tục đặt một viên gạch lên đỉnh cột thứ 33, chi phí cho thao tác này là 11. Bây giờ độ cao của các cột là: 5,5,5,6,55,5,5,6,5.
    • Dỡ một viên gạch khỏi đỉnh cột thứ 44, chi phí cho thao tác này là 22. Bây giờ độ cao của các cột là: 5,5,5,5,55,5,5,5,5.

    ⇒ 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à 1+1+2=41+1+2=4.

  • 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ứ 44 sang đỉnh cột thứ 33, chi phí cho thao tác này là 22. Bây giờ độ cao của các cột là: 5,5,4,5,55,5,4,5,5.
    • Đặt một viên gạch lên đỉnh cột thứ 33, chi phí cho thao tác này là 11. Bây giờ độ cao của các cột là: 5,5,5,5,55,5,5,5,5.

    ⇒ 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à 2+1=32+1=3.

Ràng buộc

  • 25%25\% số test ứng với 25%25\% số điểm của bài thoả mãn: a=ra = rma+rm \ge a + r.
  • 25%25\% số test khác ứng với 25%25\% số điểm của bài thoả mãn: 1n1031 \le n \le 10^30hi1040 \le h_i \le 10^4.
  • 25%25\% số test khác ứng với 25%25\% số điểm của bài thoả mãn: 0hi1060 \le h_i \le 10^6.
  • 25%25\% số test còn lại ứng với 25%25\% 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 ii. 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 ii, và tăng thêm số viên gạch ở những cột thấp hơn ii.

Lưu ý rằng, ta không được tạo thêm cột hay bỏ qua những cột có độ cao là 00.

Ta thấy rằng có 2 trường hợp là:

  1. a+rma+r\le m: 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.
  2. a+r>ma+r>m: 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í p (1n)p \ (1 \to n) sao cho: độ cao các cột từ 1p1 \to p đều i\le i, độ cao các cột từ p+1np+1 \to n đều >i> i. Tiếp đến ta sử dụng mảng cộng dồn pref[n]\text{pref}[n] để tính xem từ 1p1 \to p có tổng cộng bao nhiêu viên gạch.

Gọi AA 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 ii, ta có công thức:

A=p×ipref[p] A=p\times i - \text{pref}[p]

BB là tổng số viên gạch cần bớt đi ở những cột cao hơn ii, ta có công thức:

B=pref[n]pref[p](np)×i B = \text{pref}[n] - \text{pref}[p] - (n-p)\times i

Từ đây ta có 33 trường hợp khi khôi phục bức tường đến độ cao ii:

  1. Nếu A=BA=B thì chi phí khôi phục tối ưu sẽ là: A×min(a+r,m)A \times \min(a+r,m).
  2. Nếu A<BA<B 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 ii chỉ còn BAB - A, nên chi phí khôi phục là: A×min(a+r,m)+(BA)×RA \times \min(a+r,m) + (B-A) \times R.
  3. Nếu A>BA > B thì cũng tương tự, còn ABA-B viên gạch cần thêm ở những cột thấp hơn ii, nên chi phí khôi phục là: B×min(a+r,m)+(AB)×aB \times \min(a+r,m) + (A-B) \times a.

Ta đã thực hiện những công việc sau:

  • Sắp xếp: O(n×logn)O(n \times \log n),
  • Duyệt từng độ cao O(hmax)O(h_{\max}) với hmaxh_{\max} là hằng số \ge chiều cao lớn nhất của các cột ban đầu,
  • Tìm kiếm vị trí pp với O(logn)O(\log n),
  • Tính chi phí khôi phục trong O(1)O(1).

⇒ Độ phức tạp của thuật toán là:

O(n×logn+hmax×logn)=O(hmax×logn) O(n \times \log n + h_{\max} \times \log n) = O(h_{\max} \times \log n)

Tối ưu

Với h109h \le 10^9 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 ii, vậy nên ta phải tối ưu bằng cách chỉ duyệt những độ cao ii cần thiết.

Khi duyệt từng độ cao từ 0hmax0 \to h_{\max} 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 low=0low=0 và cận trên high=1010 (hmax)high=10^{10} \ (h_{\max}) để đả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 ii cần thiết.

Độ phức tạp của thuật toán chỉ còn: O(n×logn+loghmax×logn)=O(n×logn)O(n \times \log n + \log h_{\max} \times \log n) = O(n \times \log 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;
}