Hướng dẫn giải của Bài toán chiếc ba lô


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, Vanhoang0802, hoasentrangcm

Hiểu bài toán

Bài toán này là một biến thể của bài toán cái túi (Knapsack) với các ràng buộc phụ thuộc. Có N món đồ, mỗi món có khối lượng wi và giá trị vi. Sức chứa balo là W. Ràng buộc là một số đồ vật chỉ có thể được chọn nếu một đồ vật khác (đồ vật cha) đã được chọn trước đó. Ràng buộc này tạo thành một cấu trúc cây (hoặc rừng cây), trong đó đồ vật i phụ thuộc vào đồ vật d_i (cha). Mục tiêu là chọn một tập hợp đồ vật con (tập hợp này phải hợp lệ về mặt phụ thuộc) sao cho tổng khối lượng không vượt quá W và tổng giá trị là lớn nhất.

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

Cách Quy hoạch động trên cây với mảng 1 chiều (Tree Knapsack)
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;

const int MAXN = 50010;
const int MAXW = 60010;

int n, W;
vector<int> adj[MAXN];
int w[MAXN], v[MAXN];
int dp[MAXN][MAXW];
int sz[MAXN];

// Tính kích thước subtree cho mỗi node
void get_sizes(int u) {
    sz[u] = 1;
    for (int v : adj[u]) {
        get_sizes(v);
        sz[u] += sz[v];
    }
}

void dfs(int u) {
    // Base case: Mảng dp[u] đã được khởi tạo với 0 (hoặc -Inf)
    // Ban đầu, xem như chưa chọn gì, dp[u][0] = 0, các trạng thái khác -Inf
    // Tuy nhiên, để đơn giản, ta coi dp[u] là DP của subtree rooted at u
    // Logic: Duyệt qua các con, merge kết quả vào cha.

    for (int child : adj[u]) {
        dfs(child);
        // Merge subtree của child vào u
        // Đây là thuật toán 'Knapsack on tree' kinh điển
        for (int j = W; j >= 0; j--) {
            // Nếu ta đã có j khối lượng ở node u (từ các con trước đó hoặc chính nó)
            // Giờ thử thêm subtree của child
             for (int k = 0; k <= j; k++) {
                 // k là khối lượng từ subtree child
n                 // j - k là khối lượng còn lại
                 if (dp[u][j - k] != -1 && dp[child][k] != -1) {
                     dp[u][j] = max(dp[u][j], dp[u][j - k] + dp[child][k]);
                 }
             }
        }
    }

    // Cuối cùng, thử chọn chính node u
    // Chỉ cần lùi mảng 1 bước cho khối lượng của u
    for (int j = W; j >= w[u]; j--) {
        if (dp[u][j - w[u]] != -1) {
            dp[u][j] = max(dp[u][j], dp[u][j - w[u]] + v[u]);
        }
    }
}

int main() {
    cin >> n >> W;
    for (int i = 1; i <= n; i++) {
        int p;
        cin >> p;
        if (p != 0) adj[p].push_back(i);
    }
    for (int i = 1; i <= n; i++) cin >> w[i];
    for (int i = 1; i <= n; i++) cin >> v[i];

    // Khởi tạo DP
    memset(dp, -1, sizeof(dp));
    for (int i = 0; i <= n; i++) dp[i][0] = 0;

    // Tạo node ảo là root của tất cả các node không có cha
    // Hoặc duyệt qua các node gốc
    vector<int> roots;
    for (int i = 1; i <= n; i++) {
        if (adj[i].size() > 0 && count(adj, adj+n, i) == 0) roots.push_back(i);
    }
    // Code mẫu này giả định cấu trúc forest, cần xử lý root ảo
    // Đơn giản hơn: Solution 1 dùng node 0 làm root cho mọi item có d_i = 0
    return 0;
}
  • Time Complexity: ~O(N * W^2)~ (Quá chậm, chỉ để minh họa logic cơ bản)
  • Space Complexity: O(N * W)

Đây là cách tiếp cận cơ bản nhất của DP trên cây. Đối với mỗi node, ta tính toán các khả năng chọn các đồ vật trong subtree của nó. Ta duyệt qua các con của một node, kết hợp DP của các con vào node đó. Cuối cùng, ta thử chọn chính node đó. Cách này rất trực quan nhưng độ phức tạp ~O(N * W^2)~ do việc merge mảng 2 chiều thô sơ, dẫn đến TLE với giới hạn đề bài.

Cách Quy hoạch động Optimized (Kỹ thuật Merge Tree + Knapsack)
#include <iostream>
#include <algorithm>
#include <vector>
#include <cstring>
using namespace std;

const int MAXN = 50010;
const int MAXW = 60010;

int n, W;
vector<int> adj[MAXN];
int w[MAXN], v[MAXN];
int dp[MAXN][MAXW];
int sz[MAXN];

// Tính kích thước subtree để limit vòng lặp (Heuristic)
void get_sizes(int u) {
    sz[u] = 1;
    for (int v : adj[u]) {
        get_sizes(v);
        sz[u] += sz[v];
    }
}

void dfs(int u) {
    // Khởi tạo: Node u chưa được chọn, capacity 0 là 0, còn lại -Inf
    // Tuy nhiên, ta sẽ merge con trước

    for (int child : adj[u]) {
        dfs(child);
        // Merge con vào cha
        // Optimization: Chỉ duyệt j đến sz[u] * 200 (giả định max weight)
        // Nhưng đơn giản nhất là duyệt j từ W về 0
        for (int j = W; j >= 0; j--) {
            if (dp[u][j] == -1) continue;
            for (int k = 1; k <= sz[child] * 200 && j + k <= W; k++) {
                 if (dp[child][k] != -1) {
                    dp[u][j + k] = max(dp[u][j + k], dp[u][j] + dp[child][k]);
                 }
            }
        }
    }

    // Chọn node u
    for (int j = W; j >= w[u]; j--) {
        if (dp[u][j - w[u]] != -1)
            dp[u][j] = max(dp[u][j], dp[u][j - w[u]] + v[u]);
    }
}

int main() {
    cin >> n >> W;
    // Đọc input và xây dựng cây
    // ... (giống code mẫu)
    return 0;
}
  • Time Complexity: ~O(N * W * 200)~ (Vẫn quá chậm nếu không tối ưu đúng cách)
  • Space Complexity: O(N * W)

Cách này cải tiến một chút bằng cách dùng kích thước subtree để giới hạn vòng lặp, nhưng nhìn chung vẫn chưa tối ưu enough cho bài toán này vì W khá lớn và N lớn. Tuy nhiên, đây là bước đệm quan trọng để hiểu các giải thuật tối ưu hơn.

Cách Quy hoạch động theo DFS order (Duyệt theo thứ tự)
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;

int n, m;
vector<int> g[50010];
int w[50010], v[50010], sz[50010], o[50010];
int pool[61000010]; // Pool bộ nhớ khổng lồ
int t;

void dfs(int x) {
    for (auto u : g[x])
        dfs(u), sz[x] += sz[u];
    o[++t] = x; // Lưu thứ tự DFS pre-order
    ++sz[x];
}

int main() {
    std::ios::sync_with_stdio(false);
    std::cin.tie(nullptr);
    std::cin >> n >> m;

    // Xây dựng cây
    for (int i = 1, x; i <= n; i++) {
        std::cin >> x;
        g[x].push_back(i);
    }

    for (int i = 1; i <= n; i++) std::cin >> w[i];
    for (int i = 1; i <= n; i++) std::cin >> v[i];

    // Duyệt DFS từ root 0 (node ảo)
    dfs(0);

    // Ép kiểu mảng 1 chiều thành mảng 2D: f[i][j]
    // i là số lượng item đã xét, j là khối lượng
    int (*f)[m + 1] = decltype(f)(pool);

    // Khởi tạo mảng DP, mặc định 0
    // f[0][0] = 0 là mặc định

    for (int i = 1; i <= n; i++) {
        int x = o[i]; // Item thứ i trong thứ tự DFS
        int size_subtree = sz[x];
        int parent_idx = i - size_subtree; // Vị trí của cha trong thứ tự DFS

        // Rule 1: Nếu không chọn item x
        // Trạng thái f[i][j] có thể kế thừa từ f[i-1][j]
        // Nhưng do cách tổ chức DP theo cây, ta cần copy từ state của cha + các con đã xử lý
        // Code tối ưu:
        // Nếu không chọn x, kết quả tương tự như khi chưa xét subtree x
        // f[i][j] = f[parent_idx][j]
        for (int j = 0; j < m + 1; j++) {
             f[i][j] = f[parent_idx][j];
        }

        // Rule 2: Nếu có chọn item x
        // Chỉ xét các khối lượng có thể chứa w[x]
        // Cập nhật: f[i][j] = max(f[i][j], f[i-1][j - w[x]] + v[x])
        // Tuy nhiên, biến thể cây đặc biệt này có một công thức gom nhóm:
        // f[i][j] = max(f[parent_idx][j], f[i-1][j - w[x]] + v[x])
        // Tuy nhiên, code mẫu dùng:k
        // f[i][j] = max(f[parent_idx][j], f[i-1][j - w[x]] + v[x])
        // Cụ thể code mẫu:

        for (int j = w[x]; j <= m; j++) {
            f[i][j] = std::max(f[i - sz[x]][j], f[i - 1][j - w[x]] + v[x]);
        }

        // Giải thích chi tiết hơn:
        // f[i][j] là DP sau khi đã xét xong subtree của node thứ i trong order.
        // f[i - sz[x]][j] là DP của node cha (và các phần tử trước đó).
        // f[i - 1][j - w[x]] là DP của node ngay trước đó (đã được xử lý trong cùng subtree).
    }

    std::cout << *std::max_element(f[n], f[n] + m + 1);
    return 0;
}
  • Time Complexity: O(N * W)
  • Space Complexity: O(N * W)

Đây là phương pháp tối ưu nhất được trình bày trong giải pháp 1. Ý tưởng là:

  1. Xây dựng cây phụ thuộc, thêm node 0 làm cha của mọi node không phụ thuộc ai.
  2. Thực hiện DFS để lấy ra một thứ tự (pre-order) của các node.
  3. Định nghĩa DP[i][j]: tổng giá trị lớn nhất khi xét i node đầu tiên trong thứ tự với sức chứa j.
  4. Với mỗi node x ở vị trí i:
    • Nếu không chọn x: Trạng thái DP[i][j] phải bằng chính xác DP[i - sz[x]][j] (trạng thái của node cha). Điều này đúng vì nếu không chọn x, ta không thể chọn bất kỳ node nào trong subtree của x, nên ta bỏ qua cả khối subtree này.
    • Nếu chọn x: Ta có thể chọn x, và các node trước đó. Công thức là DP[i][j] = max(DP[i][j], DP[i-1][j - w[x]] + v[x]).
    • Code gộp hai trường hợp: max(f[i - sz[x]][j], f[i - 1][j - w[x]] + v[x]).

Cách này biến bài toán cây thành một bài toán quy hoạch động tuyến tính, tận dụng ưu điểm của Knapsack kinh điển.

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

Cách tiếp cận Time Space Tên
1 ~O(N * W^2)~ (Quá chậm, chỉ để minh họa logic cơ bản) O(N * W) Quy hoạch động trên cây với mảng 1 chiều (Tree Knapsack)
2 ~O(N * W * 200)~ (Vẫn quá chậm nếu không tối ưu đúng cách) O(N * W) Quy hoạch động Optimized (Kỹ thuật Merge Tree + Knapsack)
3 O(N * W) O(N * W) Quy hoạch động theo DFS order (Duyệt theo thứ tự)

Bài học kinh nghiệm

  • Biến đổi bài toán cây thành bài toán quy hoạch động tuyến tính bằng cách sử dụng thứ tự DFS (pre-order).
  • Công thức gom nhóm f[i][j] = max(f[i - sz[x]][j], f[i - 1][j - w[x]] + v[x]) là chìa khóa để xử lý phụ thuộc tree-structure.
  • Giới hạn N * W <= 6 * 10^7 cho phép giải thuật O(N * W) chạy trong thời gian chấp nhận được.

Lỗi thường gặp

  • Lỗi tràn mảng bộ nhớ khi khai báo dp[N][W] vì N*W có thể lên tới 300 triệu phần tử. Cần dùng mảng 1 chiều lớn hoặc pool memory như trong code mẫu.
  • Quên xử lý node ảo (root 0) dẫn đến việc bỏ sót các node gốc của rừng cây.
  • Sai lệch trong việc tính toán kích thước subtree (sz) hoặc thứ tự duyệt (order), dẫn đến việc cập nhật DP sai.

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.