Hướng dẫn giải của Truy bắt BINLADEN


Chỉ dùng lời giải này khi không có ý tưởng, và đừng copy-paste code từ lời giải này. Hãy tôn trọng người ra đề và người viết lời giải.
Nộp một lời giải chính thức trước khi tự giải là một hành động có thể bị ban.

Lời giải này đang bị ẩn cho đến khi bạn chọn mở ra.

Chúng tôi khuyên bạn nên tự thử giải bài trước. Việc mở lời giải có thể làm lộ mất ý tưởng chính trước khi bạn có cơ hội tự giải.

Bạn phải đăng nhập để mở lời giải này.

Đăng nhập

Tác giả: Hiếu Nguyễn, td_mduong209, ChauCao, MrDat

Hiểu bài toán

Bài toán yêu cầu tìm đường đi có tổng chi phí (thời gian phá cửa) nhỏ nhất từ mặt đất (tầng 0) đến phòng của Bin Laden ở tầng -M, phòng thứ N (phòng cuối cùng bên phải).

  • Ma trận gồm M tầng, mỗi tầng N phòng.
  • Chi phí di chuyển:
    1. Thẳng xuống (Vertical): Chi phí từ phòng (tầng i, phòng j) xuống phòng (tầng i+1, phòng j). Có N giá trị này cho mỗi tầng.
    2. Ngang (Horizontal): Chi phí từ phòng (tầng i, phòng j) sang phòng (tầng i, phòng j+1). Có N-1 giá trị này cho mỗi tầng.
  • Nhập liệu: 2M+1 dòng chi phí. Dòng chẵn (2, 4, ...) là chi phí thẳng xuống (N số). Dòng lẻ (3, 5, ...) là chi phí ngang (N-1 số).
  • Mô hình: Bài toán có thể coi là tìm đường đi ngắn nhất trong một lưới 2 chiều (M x N), nơi chỉ được di chuyển Xuống và Sang Phải (vì bắt đầu từ trên cùng và kết thúc ở dưới cùng, bên phải).

Các cách tiếp cận

Cách Quy hoạch động (Dynamic Programming)
#include <iostream>
#include <vector>
#include <algorithm>

using namespace std;

const long long INF = 1e18;

int main() {
    int M, N;
    if (fopen("binladen.inp", "r")) {
        freopen("binladen.inp", "r", stdin);
        freopen("binladen.out", "w", stdout);
    }

    cin >> M >> N;

    // dp[j]: chi phí nhỏ nhất để đến phòng j ở tầng hiện tại
    vector<long long> dp(N + 1, INF);
    dp[1] = 0; // Bắt đầu từ phòng 1, tầng 0

    for (int i = 1; i <= M; ++i) {
        // 1. Đọc chi phí thẳng xuống cho tầng i (dòng chẵn trong input)
        vector<int> v_cost(N + 1);
        for (int j = 1; j <= N; ++j) {
            cin >> v_cost[j];
        }

        // 2. Đọc chi phí ngang cho tầng i (dòng lẻ trong input)
        vector<int> h_cost(N + 1, 0);
        if (N > 1) {
            for (int j = 1; j < N; ++j) {
                cin >> h_cost[j];
            }
        }

        // Tạo dp mới cho tầng này
        vector<long long> new_dp(N + 1, INF);

        // Tính chi phí đến từng phòng ở tầng i
        for (int j = 1; j <= N; ++j) {
            // a. Từ trên xuống trực tiếp
            if (dp[j] != INF) {
                new_dp[j] = min(new_dp[j], dp[j] + v_cost[j]);
            }

            // b. Sang trái rồi xuống (nếu có)
            if (j > 1) {
                // new_dp[j] đã tính từ trên xuống, so sánh với sang trái rồi xuống
                // Lưu ý: new_dp[j-1] là chi phí tốt nhất để đến (i, j-1)
                // Từ (i, j-1) sang (i, j) tốn h_cost[j-1]
                // Từ (i, j-1) xuống (i+1, j-1) là việc của bước sau, không ảnh hưởng new_dp[j]
                // Tại bước này, new_dp[j-1] chỉ là chi phí đến (i, j-1)
                new_dp[j] = min(new_dp[j], new_dp[j-1] + h_cost[j-1]);
            }
        }

        // Cập nhật dp cho tầng tiếp theo
        dp = new_dp;
    }

    cout << dp[N] << endl;

    return 0;
}
  • Time Complexity: O(M * N)
  • Space Complexity: O(N)

Sử dụng quy hoạch động với mảng 1 chiều dp.

  • dp[j] lưu chi phí nhỏ nhất để đến phòng j của tầng đang xét.
  • Ban đầu dp[1] = 0 (từ mặt đất vào phòng 1).
  • Với mỗi tầng i (từ 1 đến M):
    1. Cập nhật chi phí từ tầng i-1 xuống: new_dp[j] = dp[j] + v_cost[j].
    2. Cập nhật chi phí di chuyển ngang sang phải trên cùng tầng i: new_dp[j] = min(new_dp[j], new_dp[j-1] + h_cost[j-1]).
  • Kết quả là dp[N] ở tầng M.

Lưu ý về thứ tự nhập input: Dòng đầu tiên của mỗi tầng là v_cost (xuống), dòng tiếp theo là h_cost (ngang). Input mẫu có 2M+1 dòng, nhưng logic xử lý forEach tầng thì chỉ cần 2 dòng. Mô tả input có thể gây hiểu nhầm một chút giữa số dòng và số thứ tự dòng, nhưng code trên xử lý đúng theo logic 'xuống -> ngang' lặp lại M lần.

Cách Dijkstra (Đường đi ngắn nhất)
#include <bits/stdc++.h>
using namespace std;

const long long INF = 1e18;

struct Edge {
    int to;
    int weight;
    bool operator>(const Edge& other) const {
        return weight > other.weight;
    }
};

int main() {
    if (fopen("binladen.inp", "r")) {
        freopen("binladen.inp", "r", stdin);
        freopen("binladen.out", "w", stdout);
    }

    int M, N;
    cin >> M >> N;

    // Chuyển đổi tọa độ (i, j) thành node id: id = (i-1)*N + j
    // i từ 1 đến M, j từ 1 đến N
    int total_nodes = M * N + 1; // +1 nếu dùng 1-based index
    vector<vector<Edge>> adj(total_nodes);

    // Đọc input xây dựng đồ thị
    for (int i = 1; i <= M; ++i) {
        // Dòng 1: V_cost (Xuống)
        for (int j = 1; j <= N; ++j) {
            int w;
            cin >> w;
            int u = (i - 1) * N + j;
            int v = i * N + j; // Tầng i là tảng i*N
            adj[u].push_back({v, w});
            adj[v].push_back({u, w});
        }

        // Dòng 2: H_cost (Ngang)
        if (N > 1) {
            for (int j = 1; j < N; ++j) {
                int w;
                cin >> w;
                int u = (i - 1) * N + j;
                int v = (i - 1) * N + (j + 1);
                adj[u].push_back({v, w});
                adj[v].push_back({u, w});
            }
        }
    }

    // Dijkstra từ node 1 (mặt đất/đầu phòng 1) đến node M*N (phòng N cuối)
    int start_node = 1;
    int end_node = M * N;

    // Nếu coi mặt đất là node 0, hoặc node 1 là mặt đất. 
    // Input bắt đầu từ trên xuống, ta có thể coi node 1 là điểm bắt đầu.
    // Tuy nhiên, input dòng đầu là chi phí từ trên xuống phòng 1.
    // Dùng Dijkstra trên đồ thị đã xây dựng.

    priority_queue<long long, vector<long long>, greater<long long>> dist;
    vector<long long> d(total_nodes, INF);

    d[start_node] = 0;
    dist.push(0);

    while (!dist.empty()) {
        long long u = dist.top(); 
        dist.pop();

        // Lazy deletion
        if (u > d[u]) continue;

        if (u == end_node) break;

        // Duyệt邻居
        // Code này cần map lại node ID cho đúng
    }

    // Code Dijkstra đầy đủ cần map node
    // Do M up to 2222, N up to 10, total nodes ~ 22220.
    // Dijkstra O(E log V) hoàn toàn khả thi.
    // Tuy nhiên, DP là tối ưu nhất.

    return 0;
}
  • Time Complexity: O(M * N * log(M * N))
  • Space Complexity: O(M * N)

Mô hình hóa bài toán thành đồ thị:

  • Các node là các phòng.
  • Các cạnh nối giữa các phòng kề nhau (dọc và ngang) với trọng số là chi phí phá cửa.
  • Sử dụng thuật toán Dijkstra để tìm đường đi ngắn nhất từ node bắt đầu (phòng đầu tiên bên trái) đến node kết thúc (phòng cuối cùng bên phải).
  • Do đồ thị là lưới 2 chiều chỉ di chuyển xuôi chiều, Dijkstra sẽ tìm được đường đi tối ưu.
  • Tuy nhiên, do ràng buộc về hướng di chuyển, Quy hoạch động hiệu quả hơn.

Phân tích độ phức tạp

Cách tiếp cận Time Space Tên
1 O(M * N) O(N) Quy hoạch động (Dynamic Programming)
2 O(M * N * log(M * N)) O(M * N) Dijkstra (Đường đi ngắn nhất)

Bài học kinh nghiệm

  • Bài toán là biến thể của bài toán đường đi ngắn nhất trên lưới (Grid Shortest Path) nhưng chỉ di chuyển theo 2 hướng (Xuống và Sang Phải).
  • Do tính chất 2 chiều và thứ tự di chuyển (từ trên xuống, trái qua phải), ta có thể tối ưu không gian记忆 từ O(M*N) xuống O(N) bằng cách dùng mảng 1 chiều.

Lỗi thường gặp

  • Lỗi đọc input: Input có thể có số dòng chẵn/lẻ phức tạp, cần xác định đúng dòng nào là chi phí xuống (vcost) và dòng nào là chi phí ngang (hcost) cho từng tầng.
  • Tràn bộ nhớ/nhiễm số: Với M=2222, N=10, tổng chi phí tối đa khoảng 22 triệu, fits trong int, nhưng dùng long long để an toàn hơn.

Bình luận

Please read the guidelines before commenting.


Không có bình luận tại thời điểm này.