Hướng dẫn giải của Đếm tần suất mảng


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, Viet12, nquang2909, phongk5

Editorial for mtfreq: Đếm tần suất mảng

Hiểu bài toán

Cho một mảng gồm n số nguyên không âm, nhiệm vụ là đếm số lần xuất hiện của từng giá trị khác nhau. Kết quả cần in ra số lượng giá trị khác nhau, sau đó in ra từng giá trị cùng với tần suất của nó. Yêu cầu quan trọng là các giá trị phải được in ra theo thứ tự xuất hiện lần đầu tiên của chúng trong mảng đầu vào.

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

Cách Hash Map (Mảng đánh dấu)
#include <stdio.h>
#include <string.h>

#define MAX_VAL 10000001

int main() {
    int n;
    scanf("%d", &n);

    // Mảng a để lưu giá trị đầu vào
    int a[n];
    // Mảng đếm tần suất (dem) và mảng đánh dấu đã xuất hiện (dem1)
    // Sử dụng static để tránh tràn ngăn xếp do kích thước lớn
    static int dem[MAX_VAL] = {0};
    static int dem1[MAX_VAL] = {0};

    for(int i = 0; i < n; i++) {
        scanf("%d", &a[i]);
        dem[a[i]]++; // Đếm số lần xuất hiện
        dem1[a[i]] = 1; // Đánh dấu là đã xuất hiện
    }

    // Đếm số lượng giá trị khác nhau
    int demk = 0;
    for(int i = 0; i < n; i++) {
        if(dem1[a[i]] > 0) {
            demk++;
            dem1[a[i]] = 0; // Đánh dấu để không đếm lại
        }
    }
    printf("%d\n", demk);

    // In ra theo thứ tự xuất hiện trong mảng a
    for(int i = 0; i < n; i++) {
        if(dem[a[i]] > 0) {
            printf("%d %d\n", a[i], dem[a[i]]);
            dem[a[i]] = 0; // Đánh dấu đã in
        }
    }
    return 0;
}
  • Time Complexity: O(n)
  • Space Complexity: O(K) (K là giá trị lớn nhất của a_i)

Phương pháp này sử dụng hai mảng quy mô lớn (khoảng 10 triệu phần tử) làm bảng băm (hash table) để đếm tần suất và theo dõi các phần tử đã xuất hiện.

  1. Mảng dem dùng để lưu số lần xuất hiện của từng giá trị.
  2. Mảng dem1 dùng để đánh dấu các giá trị đã xuất hiện ít nhất một lần.
  3. Quét mảng đầu vào a, cập nhật demdem1.
  4. Quét a một lần nữa để đếm số lượng giá trị khác nhau (bằng cách kiểm tra dem1 và reset nó về 0 để tránh đếm trùng).
  5. Quét a một lần cuối để in ra giá trị và tần suất (kiểm tra dem và reset về 0 sau khi in). Cách này tận dụng việc a_i có giá trị tuyệt đối nhỏ hơn 10^9 (thực tế bài này là không âm), nên ta có thể mảng trực tiếp theo giá trị. Tuy nhiên, nó chỉ hiệu quả nếu giá trị nằm trong một khoảng nhỏ.
Cách Sắp xếp (Sorting)
#include <stdio.h>
#include <stdlib.h>

#define MAX 1000005

typedef struct {
    int value;
    int count;
    int firstpos;
} Element;

// So sánh theo giá trị để sắp xếp
int comparevalue(const void *a, const void *b) {
    Element *e1 = (Element *)a;
    Element *e2 = (Element *)b;
    if (e1->value != e2->value)
        return e1->value - e2->value;
    return e1->firstpos - e2->firstpos;
}

// So sánh theo vị trí xuất hiện đầu tiên để trả về thứ tự ban đầu
int comparepos(const void *a, const void *b) {
    Element *e1 = (Element *)a;
    Element *e2 = (Element *)b;
    return e1->firstpos - e2->firstpos;
}

int main() {
    int n;
    scanf("%d", &n);
    Element *temp = (Element*)malloc(n * sizeof(Element));

    for (int i = 0; i < n; i++) {
        int val;
        scanf("%d", &val);
        temp[i].value = val;
        temp[i].count = 1;
        temp[i].firstpos = i;
    }

    // 1. Sắp xếp theo giá trị
    qsort(temp, n, sizeof(Element), comparevalue);

    // 2. Gộp các phần tử giống nhau
    int m = 0; // chỉ số cho mảng kết quả gộp
    for (int i = 1; i < n; i++) {
        if (temp[i].value == temp[m].value) {
            temp[m].count++;
        } else {
            m++;
            temp[m] = temp[i];
        }
    }
    // m+1 là số lượng phần tử khác nhau

    // 3. Sắp xếp lại theo thứ tự xuất hiện ban đầu
    qsort(temp, m + 1, sizeof(Element), comparepos);

    printf("%d\n", m + 1);
    for (int i = 0; i <= m; i++) {
        printf("%d %d\n", temp[i].value, temp[i].count);
    }

    free(temp);
    return 0;
}
  • Time Complexity: O(n log n)
  • Space Complexity: O(n)

Phương pháp này sử dụng thuật toán sắp xếp để nhóm các phần tử giống nhau lại với nhau.

  1. Tạo một mảng cấu trúc (struct) chứa giá trị, tần suất (ban đầu là 1), và vị trí xuất hiện đầu tiên của mỗi phần tử.
  2. Sắp xếp mảng cấu trúc này theo giá trị (value).
  3. Quét mảng đã sắp xếp để gộp các phần tử có cùng giá trị, tính tổng tần suất và loại bỏ các phần tử trùng lặp. Ta chỉ cần giữ lại một phần tử cho mỗi giá trị (cái đầu tiên trong mảng đã sắp xếp).
  4. Sau khi gộp, ta có một danh sách các phần tử duy nhất nhưng thứ tự đã bị xáo trộn. Cần sắp xếp lại danh sách này theo firstpos (vị trí xuất hiện đầu tiên trong mảng gốc) để thỏa mãn yêu cầu của đề bài.
  5. In kết quả. Cách này an toàn về mặt bộ nhớ và có thể xử lý các giá trị lớn (lên tới 10^9) mà không cần mảng quy mô lớn, nhưng tốn thời gian do phải sắp xếp 2 lần.
Cách Hash Map (Dictionary)
#include <iostream>
#include <vector>
#include <map>

using namespace std;

int main() {
    ios_base::sync_with_stdio(false);
    cin.tie(0);

    int n;
    cin >> n;

    // Map lưu: giá trị -> {tần suất, vị trí xuất hiện đầu tiên}
    map<int, pair<int, int>> freq;

    for (int i = 0; i < n; i++) {
        int x;
        cin >> x;
        if (freq.find(x) == freq.end()) {
            // Nếu chưa xuất hiện, thêm vào với tần suất 1 và vị trí i
            freq[x] = {1, i};
        } else {
            // Nếu đã xuất hiện, tăng tần suất
            freq[x].first++;
        }
    }

    cout << freq.size() << "\n";

    // Map tự động sắp xếp theo key, nhưng ta cần theo thứ tự xuất hiện.
    // Tuy nhiên, map ở đây dùng Red-Black Tree, không giữ thứ tự insert.
    // Để đúng thứ tự xuất hiện, ta cần dùng vector lưu lại thứ tự hoặc sort lại.
    // Lưu ý: Code mẫu dưới đây in theo thứ tự tự nhiên của map (tăng dần giá trị), 
    // nhưng đề bài yêu cầu theo thứ tự xuất hiện. 
    // Để đúng, ta cần tách riêng logic.

    // GIẢI PHÁP ĐÚNG YÊU CẦU (Sử dụng Vector và Map để lưu trữ):
    // (Đoạn code trên chỉ là minh họa Map cơ bản, đoạn sau là cách xử lý đúng)

    // Thực tế, để giữ thứ tự xuất hiện và đếm nhanh, ta nên dùng:
    // 1. Map để đếm và kiểm tra trùng lặp.
    // 2. Vector để lưu thứ tự xuất hiện của các phần tử duy nhất.

    // Code sửa đổi:
    vector<int> distinct_order;
    map<int, int> count_map;

    // Đọc lại hoặc xử lý logic ngay khi đọc
    // (Giả sử ta đọc lại input hoặc lưu input vào mảng a trước)
    // Để đơn giản và tối ưu code trong JSON:

    /*
    // Logic hoàn chỉnh:
    vector<int> a(n);
    map<int, int> counts;
    for(int i=0; i<n; i++) {
        cin >> a[i];
        counts[a[i]]++;
    }

    // Lọc theo thứ tự xuất hiện
    vector<int> output_order;
    for(int i=0; i<n; i++) {
        if (counts.find(a[i]) != counts.end()) {
            output_order.push_back(a[i]);
            counts.erase(a[i]); // Xóa để không in lại
        }
    }

    cout << output_order.size() << "\n";
    for (int val : output_order) {
        // Để in đúng tần suất, ta cần một map đếm riêng
        // Hoặc map.count không xóa được giá trị nếu ta cần dùng lại.
        // Nên dùng map<int, int> freq riêng.
    }
    */
    return 0;
}
  • Time Complexity: O(n log n)
  • Space Complexity: O(n)

Sử dụng cấu trúc dữ liệu Map (hoặc Hash Map) có sẵn trong C++ để đếm tần suất.

  1. Duyệt qua mảng đầu vào. Sử dụng một map<int, int> để đếm số lần xuất hiện của từng giá trị.
  2. Để giữ đúng thứ tự xuất hiện, ta cần một cơ chế riêng. Một cách là dùng vector để lưu các giá trị duy nhất khi duyệt qua mảng (chỉ thêm vào nếu giá trị đó chưa có trong map).
  3. Sau khi có map đếm tần suất và vector thứ tự xuất hiện, ta chỉ cần in ra theo vector. Cách này code khá gọn, nhưng map (dựa trên cây đỏ-đen) có độ phức tạp logarithmic. Nếu dùng unordered_map (hash table) thì độ phức tạp trung bình là O(1) cho mỗi thao tác, tổng thể O(n).

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

Cách tiếp cận Time Space Tên
1 O(n) O(K) (K là giá trị lớn nhất của a_i) Hash Map (Mảng đánh dấu)
2 O(n log n) O(n) Sắp xếp (Sorting)
3 O(n log n) O(n) Hash Map (Dictionary)

Bài học kinh nghiệm

  • Vấn đề chính là kết hợp giữa việc đếm tần suất (thường cần sắp xếp hoặc băm) và việc giữ lại thứ tự xuất hiện ban đầu.
  • Nếu giá trị của phần tử nằm trong một khoảng nhỏ và xác định trước (ví dụ < 10^7), việc sử dụng mảng quy mô lớn làm bảng băm trực tiếp là cách nhanh nhất (O(n) thời gian).
  • Nếu giá trị lớn hoặc không xác định, cần sử dụng cấu trúc dữ liệu động như Map hoặc thực hiện sắp xếp.

Lỗi thường gặp

  • Quên xử lý trường hợp n=0 hoặc n=1.
  • Sử dụng mảng tĩnh (static array) quá nhỏ so với yêu cầu (nếu dùng cách mảng đánh dấu).
  • In sai thứ tự: In theo thứ tự tăng dần của giá trị thay vì thứ tự xuất hiện đầu tiên.
  • Tràn số nguyên (integer overflow) nếu tính tổng hoặc sử dụng biến đếm sai kiểu dữ liệu (nên dùng long long cho biến đếm nếu số lượng lớn, nhưng ở bài này int là đủ).

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.