Skip to content

Olympic 11 Thành phố 2018 - 2019

Bài 1: Cắt chữ vi tính

Đề bài

\(n\) thí sinh tham dự kỳ thi Olympic TPHCM, đánh số báo danh từ 1 đến \(n\).

Ban tổ chức dự kiến số báo danh của từng thí sinh được cắt trên giấy decal bằng máy vi tính và dán vào vị trí chỗ ngồi của thí sinh.

Số báo danh của thí sinh được kết hợp từ các chữ số: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9. Chẳng hạn: số báo danh 113 được kết hợp từ 3 chữ số là 1, 1 và 3.

Nếu có 13 thí sinh thì số báo danh được đánh là: 1, 2, ..., 13. Như vậy, cần cắt 17 chữ số.

Ban tổ chức muốn biết tổng số các chữ số cần cắt trong kỳ thi này để có thể dự trù kinh phí. Bạn hãy viết chương trình giúp ban tổ chức.

Yêu cầu: Cho trước số thí sinh dự thi. Hãy tính số lượng chữ số cần cắt.

Dữ liệu vào: VITINH.INP

Gồm duy nhất một dòng ghi số nguyên dương \(n (1 \le n \le 10^16)\) là số thí sinh dự thi.

Kết quả: VITINH.OUT

Gồm một số nguyên duy nhất là số lượng chữ số cần cắt.

Ví dụ:

VITINH.INP VITINH.OUT
13 17

Bài giải đề xuất

Ý tưởng chính

Dùng vòng lặp để tính số chữ số cho từng phạm vi như bảng sau:

Bắt đầu Kết thúc (giới hạn trên) Số luỹ thừa của 10 Số lượng số Số chữ số Tổng số chữ số
1 9 \(10^1\) 9 1 9
10 99 \(10^2\) 90 2 180
100 999 \(10^3\) 900 3 2700
1000 9999 \(10^4\) 9000 4 36000

Viết chương trình

1. Khởi tạo

Khởi tạo các biến như bảng trên:

    // các số luỹ thừa của 10: 1, 10, 100, 1000, 10000, ...
    lli power_of_ten = 1;

    // giới hạn trên của phạm vi đang xét: 9, 99, 999, 9999, ...
    lli bound;

    // số chữ số trong phạm vi đang xét: 1, 2, 3, 4, ...
    lli number_of_digits = 1;

    // số lượng số trong phạm vi đang xét
    lli amount;
    # các số luỹ thừa của 10: 1, 10, 100, 1000, 10000, ...
    power_of_ten = 1

    # giới hạn trên của phạm vi đang xét: 9, 99, 999, 9999, ...
    bound = 0

    # số chữ số trong phạm vi đang xét: 1, 2, 3, 4, ...
    number_of_digits = 1

    # số lượng số trong phạm vi đang xét
    amount = 0

2. Xử lý lần lượt từng phạm vi

Áp dụng vòng lặp while để xử lý từng phạm vi. Trong mỗi phạm vi, cần chú ý giá trị của n so với giới hạn trên bound (là số kết thúc của mỗi phạm vi).

    while (true)
    {
        // Tính giới hạn trên của phạm vi đang xét: 9, 99, 999, 9999, ...
        bound = power_of_ten * 10 - 1;

        // So sánh n và giới hạn trên
        if (n >= bound)
        {
            // Tính số lượng số trong phạm vi đang xét: 9, 90, 900, 9000, ...
            amount = 9 * power_of_ten;

            // Lấy số lượng số x số chữ số, rồi cộng dồn vào kết quả
            total_digits += amount * number_of_digits;
        }
        else
        {
            // Tính số lượng số trong phạm vi đang xét
            amount = n - power_of_ten + 1;

            // Lấy số lượng số x số chữ số, rồi cộng dồn vào kết quả
            total_digits += amount * number_of_digits;

            // Thoát vòng lặp, kết thúc chương trình
            break;
        }

        // Tăng số chữ số để xét phạm vi tiếp theo
        number_of_digits += 1;

        // Nhân thêm 10
        power_of_ten *= 10;

        // Điều kiện dừng vòng lặp
        if (power_of_ten > n) break;
    }
    while (True):
        # Tính giới hạn trên của phạm vi đang xét: 9, 99, 999, 9999, ...
        bound = power_of_ten * 10 - 1

        # So sánh n và giới hạn trên
        if n >= bound:
            # Tính số lượng số trong phạm vi đang xét: 9, 90, 900, 9000, ...
            amount = 9 * power_of_ten

            # Lấy số lượng số x số chữ số, rồi cộng dồn vào kết quả
            total_digits += amount * number_of_digits
        else:
            # Tính số lượng số trong phạm vi đang xét: 9, 90, 900, 9000, ...
            amount = n - power_of_ten + 1

            # Lấy số lượng số x số chữ số, rồi cộng dồn vào kết quả
            total_digits += amount * number_of_digits

            # Thoát vòng lặp, kết thúc chương trình
            break

        # Tăng số chữ số để xét phạm vi tiếp theo
        number_of_digits += 1

        # Nhân thêm 10
        power_of_ten *= 10

        # Điều kiện dừng vòng lặp
        if power_of_ten > n:
            break

Mã nguồn

Code đầy đủ được đặt tại GitHub.

Bài 2: Chi phí giao thông

Đề bài

Một mạng lưới giao thông gồm \(N\) nút được đánh số \(1..N\), và \(M\) con đường hai chiều nối các các nút với nhau.

Mỗi nút giao thông \(I\) có một chi phí hành trình là \(T_i\). Chi phí lưu thông trên mỗi con đường nối hai nút khác nhau \(I\)\(K\)\(L_{ik} = L_{ki}\).

Một người muốn di chuyển từ nút giao thông này đến nút giao thông khác sẽ chịu chi phí bằng tổng chi phí lưu thông của các con đường đã đi qua, cộng với chi phí hành trình lớn nhất trong số các nút giao thông đã gặp. Chi phí này quá lớn đối với một người, vì vậy họ phải tính toán trước chi phí phải trả ít nhất khi di chuyển để quyết định xem có nên đi hay không.

Yêu cầu: Cho nhu cầu di chuyển giữa \(R\) cặp nút. Hãy tính chi phí nhỏ nhất phải trả cho việc di chuyển giữa mỗi cặp nút giao thông này.

Dữ liệu vào: CHIPHI.INP

  • Dòng đầu tiên gồm 3 số nguyên: \(N (1 \le N \le 250)\), \(M (1 \le M \le 10000)\)\(R (1 \le R \le 10000)\).
  • \(N\) dòng tiếp theo, mỗi dòng ghi một số nguyên \(T (1 \le T \le 100000)\) tương ứng chi phí hành trình của một nút giao thông \(1..N\).
  • \(M\) dòng tiếp theo mỗi dòng ghi 3 số nguyên dương: \(I, K, L (I \neq K, 1 \le L \le 100000)\) biểu diễn việc di chuyển trên con đường hai chiều nối hai nút giao thông \(I\)\(K\), sẽ mất chi phí lưu thông là \(L\).
  • \(R\) dòng tiếp theo, mỗi dòng ghi 2 số nguyên: \(U, V (U \neq V)\) biểu diễn nhu cầu di chuyển từ nút giao thông U đến nút giao thông V.

Kết quả: CHIPHI.OUT

Gồm \(R\) dòng, mỗi dòng là một số nguyên tương ứng với chi phí nhỏ nhất khi di chuyển giữa một cặp nút.

Ví dụ:

CHIPHI.INP CHIPHI.OUT
5 7 2
2
5
3
3
4
1 2 3
1 3 2
2 5 3
5 3 1
5 4 1
2 4 3
3 4 4
1 4
2 3
8
9

Giải thích:

Mạng lưới giao thông trên gồm: 5 nút, 7 con đường hai chiều và 2 cặp nút giao thông.

Chi phí cụ thể như sau:

  • Chi phí hành trình: \(T_1 = 2, T_2 = 5, T_3 = 3, T_4 = 3, T_5 = 4\).
  • Chi phí lưu thông: \(L_{12} = 3, L_{13} = 2, L_{25} = 3, L_{53} = 1, L_{54} = 1, L_{24} = 3, L_{34} = 4\).

Nhu cầu di chuyển giữa hai cặp nút: từ nút 1 đến nút 4, từ nút 2 đến nút 3.

Cách tính chi phí nhỏ nhất như sau:

  • Để đi từ nút 1 đến nút 4 mất chi phí ít nhất thì cần phải đi qua các nút theo trình tự: \(1 - 3 - 5 - 4\). Chi phí là \((L_{13} + L_{53} + L_{54}) + max\{T_1, T_2, T_3, T_4\}\), tương ứng \((2 + 1 + 1) + 4 = 8\).
  • Để đi từ nút 2 đến nút 3 mất chi phí ít nhất thì cần phải đi qua các nút theo trình tự: \(2 - 5- 3\). Chi phí là \((L_{25} + L_{53}) + max\{T_2, T_5, T_3\}\), tương ứng \((3 + 1) + 5 = 9\).

Bài giải đề xuất

Ý tưởng chính

Áp dụng thuật toán Floyd-Warshall vì thuật toán này có thể tìm được chi phí nhỏ nhất giữa các cặp nút, giúp nhanh chóng trả lời một loạt các truy vấn (một loạt cặp nút) mà không mất công chạy lại thuật toán cho từng cặp.

Viết chương trình

Bước 0:

Bước 0.1. Khai báo các biến liên quan.

typedef long long int lli;

const lli INF = 1e18;

struct node
{
    int id;
    int travel_cost;
};

// số nút, số nút, số truy vấn
int n, m, r;

// Mảng lưu nút và chi phí hành trình T
vector<lli> T;

// Mảng lưu nút và chi phí hành trình T để tìm giá trị lớn nhất
vector<node> nodes;

// Mảng lưu chi phí lưu thông L giữa hai nút
vector<vector<lli>> L;

// Mảng lưu các truy vấn (là các cặp nút)
vector<pair<int, int>> queries;

// mảng lưu kết quả tổng chi phí nhỏ nhất giữa hai cặp nút (dành cho các truy vấn)
vector<vector<lli>> results;
INF = int(1e18)

class node:
    def __init__(self, id, travel_cost):
        self.id = id
        self.travel_cost = travel_cost

# số nút, số nút, số truy vấn
n = m = r = 0

# Mảng lưu nút và chi phí hành trình T
T = []

# Mảng lưu nút và chi phí hành trình T để tìm giá trị lớn nhất
nodes = []

# Mảng lưu chi phí lưu thông L giữa hai nút
L = []

# Mảng lưu các truy vấn (là các cặp nút)
queries = []

# mảng lưu kết quả tổng chi phí nhỏ nhất giữa hai cặp nút (dành cho các truy vấn)
results = []

Bước 0.2. Đọc dữ liệu vào các biến.

    cin >> n >> m >> r;

    // Đọc chi phí hành trình T
    T.resize(n + 1);
    nodes.resize(n);
    for (int i = 0; i < n; ++i)
    {
        nodes[i].id = i + 1;
        cin >> nodes[i].travel_cost;
        T[i + 1] = nodes[i].travel_cost;
    }

    // Khởi tạo mảng chi phí lưu thông L
    L.resize(n + 1, vector<lli>(n + 1, INF));

    // Khởi tạo mảng kết quả
    results.resize(n + 1, vector<lli>(n + 1, INF));

    // Khởi tạo cho trường hợp một nút đi đến chính nó
    for (int i = 1; i < n + 1; ++i)
    {
        L[i][i] = 0;
        results[i][i] = T[i];
    }

    // Đọc chi phí lưu thông l giữa hai nút i và k
    int i, k;
    lli l, initial_total_cost;
    for (int u = 0; u < m; ++u)
    {
        cin >> i >> k >> l;

        L[i][k] = l;
        L[k][i] = l;

        // Tạm tính tổng chi phí ban đầu 
        initial_total_cost = l + max(T[i], T[k]);
        results[i][k] = initial_total_cost;
        results[k][i] = initial_total_cost;
    }

    // Khởi tạo mảng các truy vấn
    queries.resize(r);

    // Đọc các cặp nút cần truy vấn
    int u_query, v_query;
    for (int i = 0; i < r; ++i)
    {
        cin >> u_query >> v_query;
        queries[i] = {u_query, v_query};
    }
    with open(input_file, 'r') as f:
        n, m, r = map(int, f.readline().split())

        # Đọc chi phí hành trình T
        T = [0] * (n + 1)
        nodes = [node(0, 0) for _ in range(n)]

        for i in range(n):
            nodes[i].id = i + 1
            nodes[i].travel_cost = int(f.readline())
            T[i + 1] = nodes[i].travel_cost

        # Khởi tạo mảng chi phí lưu thông L
        L = [[INF for _ in range(n + 1)] for _ in range(n + 1)]

        # Khởi tạo mảng kết quả
        results = [[INF for _ in range(n + 1)] for _ in range(n + 1)]

        # Khởi tạo cho trường hợp một nút đi đến chính nó
        for i in range(1, n + 1):
            L[i][i] = 0
            results[i][i] = T[i]

        # Đọc chi phí lưu thông l giữa hai nút i và k
        for u in range(m):
            i, k, l = map(int, f.readline().split())

            L[i][k] = l
            L[k][i] = l

            # Tạm tính tổng chi phí ban đầu 
            initial_total_cost = l + max(T[i], T[k])
            results[i][k] = initial_total_cost
            results[k][i] = initial_total_cost

        # Khởi tạo mảng các truy vấn
        queries = [(0, 0)] * r

        # Đọc các cặp nút cần truy vấn
        for i in range(r):
            u_query, v_query = map(int, f.readline().split())
            queries[i] = (u_query, v_query)

Bước 0.3. Đối với C++, viết hàm so sánh để hỗ trợ cho hàm sort() sắp xếp các nút theo thứ tự chi phí hành trình T tăng dần.

bool compare(const node &a, const node &b)
{
    return a.travel_cost < b.travel_cost;
}

Bước 1: Sắp xếp các nút theo chi phí hành trình tăng dần

Ta cần sắp xếp mảng nodes theo chi phí hành trình T tăng dần, vì khi duyệt các nút trung gian k (nút trung gian theo nghĩa của thuật toán Floyd-Warshall), ta luôn có được chi phí T là lớn nhất.

Nói cách khác, khi vòng lặp duyệt đến nút k (được lấy từ mảng nodes đã sắp xếp), thì T[k] là chi phí hành trình lớn nhất trong số các nút trung gian mà thuật toán đã xem xét tính đến thời điểm đó. Việc này nhằm bảo đảm thao tác cập nhật tổng chi phí results tại mỗi thời điểm là đúng đắn.

    // Sắp xếp các nút theo chi phí hành trình tăng dần
    sort(nodes.begin(), nodes.end(), compare);
    # Sắp xếp các nút theo chi phí hành trình tăng dần
    nodes.sort(key=lambda node: node.travel_cost)

Bước 2: Thực thi Floyd-Warshall

    // nút trung gian k và phí hành trình t tại nút k 
    int k, tk;

    // Duyệt từng nút theo thứ tự vừa sắp xếp ở trên
    for (int p = 0; p < n; ++p)
    {
        // Xét nút trung gian k và chi phí hành trình tại k
        k = nodes[p].id;
        tk = nodes[p].travel_cost; 

        // Duyệt từng cặp nút u - v
        for (int u = 1; u < n + 1; ++u)
        {
            for (int v = 1; v < n + 1; ++v)
            {
                // 1. Tính chi phí lưu thông khi đi qua nút trung gian k
                if (L[u][k] != INF && L[k][v] != INF)
                {
                    L[u][v] = min(L[u][v], L[u][k] + L[k][v]);
                }

                // 2. Tính tổng chi phí kết quả tốt nhất (nếu có đường đi từ u đến v)
                if (L[u][v] != INF)
                {
                    results[u][v] = min(results[u][v], L[u][v] + max({T[u], T[v], T[k]}));
                }
            }
        }

        // Bảo đảm tính đối xứng cho mảng results sau mỗi bước k
        for (int u = 1; u < n + 1; ++u)
        {
            for (int v = u + 1; v < n + 1; ++v)
            {
                results[u][v] = min(results[u][v], results[v][u]);
                results[v][u] = results[u][v];
            }
        }
    }
    # nút trung gian k và phí hành trình t tại nút k 
    k = tk = 0

    # Duyệt từng nút theo thứ tự vừa sắp xếp ở trên
    for p in range(n):
        # Xét nút trung gian k và chi phí hành trình tại k
        k = nodes[p].id
        tk = nodes[p].travel_cost

        # Duyệt từng cặp nút u - v
        for u in range(1, n + 1):
            for v in range(1, n + 1):
                # 1. Tính chi phí lưu thông khi đi qua nút trung gian k
                if L[u][k] != INF and L[k][v] != INF:
                    L[u][v] = min(L[u][v], L[u][k] + L[k][v])

                # 2. Tính tổng chi phí kết quả tốt nhất (nếu có đường đi từ u đến v)
                if L[u][v] != INF:
                    results[u][v] = min(results[u][v], L[u][v] + max(T[u], T[v], T[k]))

            # Bảo đảm tính đối xứng cho mảng results sau mỗi bước k
            for u in range(1, n + 1):
                for v in range(1, n + 1):
                    results[u][v] = min(results[u][v], results[v][u])
                    results[v][u] = results[u][v]

Mã nguồn

Code đầy đủ được đặt tại GitHub.