Hướng dẫn giải của Ma trận Mirko


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, haidang3004, zuanopro, super_god

Hiểu bài toán

Bài toán yêu cầu tìm số dòng tối đa có thể xóa từ trên xuống (bắt đầu từ dòng 1) của một ma trận ký tự sao cho sau khi xóa, các cột của ma trận còn lại vẫn phân biệt nhau (không có hai cột nào giống nhau). Nói cách khác, ta cần tìm số dòng đầu tiên cần giữ lại (ký hiệu là dòng bắt đầu từ k) sao cho các cột từ k đến M phân biệt nhau. Ta cần tối thiểu hóa k (vì số dòng xóa = k - 1 nếu k là chỉ số dòng bắt đầu, hoặc k nếu k là số dòng xóa). Dễ thấy nếu xóa k dòng, ta xét M - k dòng cuối.

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

Cách Brute Force (Tìm kiếm tuyến tính)
#include <bits/stdc++.h>
using namespace std;

int m, n;
string a[1005];

bool check(int rows_to_delete) {
    int start_row = rows_to_delete; // Dòng bắt đầu (0-indexed)
    if (start_row >= m) return true; // Xóa hết hoặc chỉ còn 1 dòng

    set<string> col_signatures;
    for (int j = 0; j < n; ++j) {
        string col_str = "";
        for (int i = start_row; i < m; ++i) {
            col_str += a[i][j];
        }
        if (col_signatures.count(col_str)) return false;
        col_signatures.insert(col_str);
    }
    return true;
}

int main() {
    if (!(cin >> m >> n)) return 0;
    for (int i = 0; i < m; ++i) cin >> a[i];

    // Tìm số dòng xóa lớn nhất
    // Thử xóa từ 0 dòng đến m-1 dòng
    int ans = 0;
    for (int k = 0; k < m; ++k) {
        if (check(k)) {
            ans = k;
        } else {
            break; // Nếu k dòng không được thì k+1 cũng không được
        }
    }
    cout << ans << endl;
    return 0;
}
  • Time Complexity: O(M^2 * N)
  • Space Complexity: O(M * N)

Ta thử từng số dòng xóa k từ 0 tăng dần. Với mỗi k, ta kiểm tra tất cả các cột. Mỗi cột được tạo thành xâu ký tự từ các dòng còn lại (từ k đến M-1). Ta dùng set để kiểm tra xem có hai cột nào trùng nhau không. Nếu không trùng, ta cập nhật kết quả. Vì M, N <= 1000, độ phức tạp khoảng 10^9 (nếu M=N=1000), quá chậm cho trường hợp xấu nhất nhưng có thể chạy nhanh trong một số trường hợp test yếu. Tuy nhiên, giải pháp này chỉ phù hợp khi M nhỏ (thực tế chỉ chạy được ~100).

Cách Tối ưu với Băm (Hashing) và Binary Search
#include <iostream>
#include <vector>
#include <set>
#include <string>
using namespace std;

bool check_columns(const vector<string>& matrix, int rowsToDelete) {
    int M = matrix.size();
    int N = matrix[0].size(); 
    set<vector<char>> seen_columns; 
    for (int col = 0; col < N; col++) {
        vector<char> current_column;
        for (int row = rowsToDelete; row < M; row++) {
            current_column.push_back(matrix[row][col]);
        }
        if (seen_columns.count(current_column)) {
            return false;
        }
        seen_columns.insert(current_column);
    }
    return true; 
}

int main() {
    int M, N;
    cin >> M >> N;
    vector<string> matrix(M);
    for (int i = 0; i < M; i++) {
        cin >> matrix[i];
    }
    int left = 0, right = M - 1;
    int maxDelete = 0;
    while (left <= right) {
        int mid = (left + right) / 2;
        if (check_columns(matrix, mid)) {
            maxDelete = mid; 
            left = mid + 1; // Thử xóa nhiều hơn
        } else {
            right = mid - 1; // Phải xóa ít hơn
        }
    }
    cout << maxDelete << endl;
    return 0;
}
  • Time Complexity: O(M * N * log M)
  • Space Complexity: O(M * N)

Giả thuyết: Nếu ta có thể xóa k dòng và giữ lại M-k dòng cuối cùng để các cột phân biệt, thì ta cũng có thể xóa k-1 dòng (vì chỉ thêm 1 dòng vào đầu, các cột trở nên khác biệt hơn hoặc vẫn khác biệt). Do đó, tính chất này có tính chất đơn điệu. Ta có thể sử dụng kỹ thuật Binary Search trên số dòng cần xóa (từ 0 đến M-1). Với mỗi mid, ta kiểm tra xem mid dòng đầu có thể bị xóa không bằng hàm check_columns. Hàm check_columns hiện tại vẫn tạo vector cho mỗi cột, tốn O(M*N) bộ nhớ và thời gian. Độ phức tạp tổng thể là O(M * N * log M).

Cách Mã hóa Cột bằng Hashing (Optimized)
#include <bits/stdc++.h>
using namespace std;

int m, n, t, ok;
long long f[1001][1001], mod = 1e9 + 7;
char a[1001][1001];

int main() {
    ios::sync_with_stdio(0);
    cin.tie(0);
    cin >> m >> n;
    // Đọc dữ liệu, lưu theo cột hoặc hàng đều được, code gốc đọc ngược để tính hash từ dưới lên
    // Ở đây f[i][j] là hash của cột j考虑 các dòng từ 1 đến i (theo input reversed)
    // Thực tế input là m dòng, code đọc từ m về 1.
    for (int i = m; i >= 1; i--) {
        for (int j = 1; j <= n; j++) {
            cin >> a[i][j];
        }
    }

    // Tính hash prefix cho các cột
    // f[i][j] = hash của cột j từ dòng 1 đến i (dòng 1 là dòng cuối cùng của input)
    // Công thức: hash = (hash_truoc * 26 + val) % mod
    for (int i = 1; i <= m; i++) {
        for (int j = 1; j <= n; j++) {
            f[i][j] = (f[i - 1][j] * 26 + a[i][j] - 'a' + 1) % mod;
        }
    }

    // Tìm dòng bắt đầu (từ dưới lên)
    // t là dòng bắt đầu (1-indexed)
    t = 1;
    while (t <= m) {
        ok = 1;
        // Kiểm tra tất cả các cột có hash bằng nhau tại dòng t hay không
        // Nếu có, nghĩa là các cột từ dòng t trở lên (tới m) bị trùng?
        // Không, logic code này đang kiểm tra các cột tại "điểm cắt".
        // Thực ra code này đang tìm dòng bắt đầu sao cho các cột phân biệt.
        // Nếu f[t][j] == f[t][i] thì cột i và j từ dòng t đến m là giống nhau.
        for (int i = 1; (i <= n) && (ok); i++)
            for (int j = i + 1; (j <= n) && (ok); j++)
                if (f[t][j] == f[t][i]) {
                    t++; // Nếu trùng, thử bắt đầu từ dòng tiếp theo
                    ok = 0;
                }
        if (ok) break; // Không còn trùng lặp, tìm được t
    }
    // t là số dòng được giữ lại từ dưới lên (tức là dòng t, t+1, ..., m)
    // Nếu input đọc ngược, t là chỉ số dòng thực tế.
    // Số dòng xóa được là m - t.
    cout << (m - t);
}
  • Time Complexity: O(M * N)
  • Space Complexity: O(M * N)

Đây là cách hiệu quả nhất. Thay vì so sánh các xâu trực tiếp, ta băm các cột. Dựa vào tính chất đơn điệu, ta chỉ cần tìm dòng bắt đầu t sao cho các cột từ t đến M phân biệt. Ta có thể dùng mảng f[i][j] để lưu giá trị băm của cột j xét từ dòng i đến M. Tuy nhiên, code mẫu lại tính hash theo chiều ngược lại (từ dưới lên) để có thể tính nhanh prefix hash. Logic của code mẫu (Solution 2, 3) là:

  1. Đọc dữ liệu và lưu ngược (dòng 1 là dòng cuối của input).
  2. Tính f[i][j] là hash của cột j từ dòng 1 đến i (trong dữ liệu đã đọc ngược). Tương đương với hash của cột j từ dòng M-i+1 đến M trong dữ liệu gốc.
  3. Duyệt t từ 1 đến M. Tại mỗi t, f[t][j] chính là hash của cột j từ dòng t đến M (dữ liệu gốc).
  4. Nếu tại t, tất cả các hash f[t][j] đều phân biệt, thì t là số dòng từ dưới lên cần giữ lại. Số dòng xóa là M - t. Vì ta muốn xóa nhiều dòng nhất, ta tìm t nhỏ nhất thỏa mãn.
  5. Code thực hiện tăng t lên cho đến khi tìm được t thỏa mãn. Nếu t=1 thỏa mãn, xóa M-1 dòng. Nếu t=M thỏa mãn, xóa 0 dòng.

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

Cách tiếp cận Time Space Tên
1 O(M^2 * N) O(M * N) Brute Force (Tìm kiếm tuyến tính)
2 O(M * N * log M) O(M * N) Tối ưu với Băm (Hashing) và Binary Search
3 O(M * N) O(M * N) Mã hóa Cột bằng Hashing (Optimized)

Bài học kinh nghiệm

  • Tính chất đơn điệu: Nếu các cột phân biệt khi xóa k dòng đầu, thì chúng cũng phân biệt khi xóa k-1 dòng đầu (vì thêm dữ liệu vào đầu làm các cột khác nhau hơn). Do đó ta có thể dùng Binary Search hoặc duyệt tuyến tính để tìm số dòng xóa lớn nhất.
  • Thay vì so sánh trực tiếp các chuỗi ký tự (tốn O(M) cho mỗi cặp), ta có thể băm chuỗi hoặc dùng trie. Tuy nhiên, với ràng buộc M, N <= 1000, cách hiệu quả nhất là băm các cột và kiểm tra sự phân biệt bằng set hoặc mảng.
  • Có thể tối ưu không gian bằng cách chỉ cần hash dòng hiện tại và dòng trước đó nếu chỉ cần kiểm tra điều kiện.

Lỗi thường gặp

  • Lầm tưởng rằng chỉ cần kiểm tra các dòng đầu tiên bị xóa là đủ (ví dụ: dòng 1 và dòng 2). Phải kiểm tra toàn bộ các cột còn lại.
  • Độ phức tạp O(MNK) với K là số lần thử (Binary Search) có thể chậm nếu không dùng hashing. Việc tạo xâu con cho mỗi cột trong vòng lặp Binary Search có thể tốn bộ nhớ và thời gian.
  • Logic của Solution 2/3 hơi ngược về chỉ số dòng nếu không đọc kỹ. Họ đọc dữ liệu từ m về 1 vào a[i], sau đó f[i][j] lưu hash từ 1 đến i. Nếu t=1 thì hash là cả cột (từ 1 đến m tương đương m đến 1 trong input).

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.