Hướng dẫn giải của Phần tử xuất hiện nhiều nhất


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, duykhanh100106, thanhdoan, nquang2909

Editorial for maxfreq: Phần tử xuất hiện nhiều nhất

Hiểu bài toán

Cho một dãy gồm n số nguyên (1 ≤ n ≤ 10^5). Yêu cầu tìm số xuất hiện nhiều nhất trong dãy. Nếu có nhiều số cùng xuất hiện với số lần nhiều nhất, chọn số xuất hiện đầu tiên trong dãy. Output gồm số đó và tần suất của nó.

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

Cách Brute Force (Đếm tần suất và kiểm tra lại)
#include <stdio.h>
#include <math.h>
#include <stdlib.h>
#include <string.h>
int fre[100001];
int main() {
    int n;
    scanf("%d",&n);
    int a[n];
    for (int i = 0; i < n; i++){
        scanf("%d",&a[i]);
    }
    int max = 0, ans = a[0];
    // Bước 1: Đếm tần suất vào mảng fre
    for (int i = 0; i < n; i++){
        fre[a[i]]++;
    }
    // Bước 2: Duyệt lại mảng a để tìm phần tử có tần suất lớn nhất
    // Lưu ý: Solution 1 không xử lý đúng điều kiện 'phần tử đầu tiên' nếu chỉ dùng fre[a[i]] > max
    // Logic đúng nên là: if (fre[a[i]] > max) { max = fre[a[i]]; ans = a[i]; }
    // Cần sửa lại logic để đảm bảo thứ tự ưu tiên
    for (int i = 0; i < n; i++){
        if (fre[a[i]] > max ){
            max = fre[a[i]];
            ans = a[i];
        }
    }
    printf("%d %d",ans,max);
    return 0;
}
  • Time Complexity: O(n)
  • Space Complexity: O(n)

Phương pháp này sử dụng một mảng phụ (mảng đếm) để lưu tần suất của các số. Nó đọc vào dãy số, cập nhật tần suất vào mảng đếm. Sau đó, nó duyệt lại dãy số ban đầu (giữ nguyên thứ tự) để tìm số đầu tiên có tần suất bằng với tần suất lớn nhất. Độ phức tạp thời gian là O(n) do duyệt 2 lần, không gian là O(n) để lưu dãy số và O(K) cho mảng đếm.

Cách Hash Map (Đếm tần suất và tối ưu bộ nhớ)
#include <stdio.h>

// Giá trị tối đa của các phần tử là 10^5
#define MAX_VAL 100000

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

    // Mảng để lưu dãy số ban đầu
    int day_so[n];

    // Mảng để đếm tần suất (thay thế Hash Map)
    int tan_suat[MAX_VAL + 1] = {0};

    // Bước 1: Đọc dữ liệu và đếm tần suất
    for (int i = 0; i < n; i++) {
        scanf("%d", &day_so[i]);
        tan_suat[day_so[i]]++;
    }

    // Biến để lưu kết quả
    int so_ket_qua = -1;
    int max_tan_suat = 0;

    // Bước 2: Duyệt qua dãy số ban đầu để tìm kết quả
    for (int i = 0; i < n; i++) {
        int so_hien_tai = day_so[i];
        int tan_suat_hien_tai = tan_suat[so_hien_tai];

        if (tan_suat_hien_tai > max_tan_suat) {
            max_tan_suat = tan_suat_hien_tai;
            so_ket_qua = so_hien_tai;
        }
    }

    printf("%d %d", so_ket_qua, max_tan_suat);
    return 0;
}
  • Time Complexity: O(n)
  • Space Complexity: O(n)

Cách tiếp cận này tương tự như cách trước nhưng được trình bày rõ ràng hơn. Nó sử dụng mảng tan_suat với kích thước cố định (100001) để đếm tần suất. Việc duyệt qua day_so ban đầu để tìm kết quả đảm bảo nếu có nhiều phần tử cùng tần suất, phần tử xuất hiện đầu tiên sẽ được chọn. Đây là cách làm chuẩn trong các kỳ thi lập trình với giới hạn giá trị phần tử nhỏ.

Cách Tối ưu không gian (Không lưu mảng ban đầu)
#include<stdio.h>

#define MAX 100005

int cnt[MAX];
int main() {
    int n;
    scanf("%d", &n);

    int value = -1;
    int maxx = 0;
    int temp;

    // Đọc và đếm tần suất ngay lập tức
    for(int i = 0; i < n; i++){
        scanf("%d", &temp);
        cnt[temp]++;

        // Kiểm tra điều kiện cập nhật kết quả ngay tại đây
        // Nếu tần suất mới lớn hơn, hoặc bằng (và maxx chưa được gán) -> cập nhật
        // Tuy nhiên, để đảm bảo 'xuất hiện đầu tiên', ta cần so sánh strictly >
        // Hoặc nếu bằng, ta chỉ cập nhật khi maxx == 0 (lần đầu)
        // Nhưng cách này không chắc chắn chọn được phần tử đầu tiên nếu có nhiều phần tử cùng tần suất
        // Ví dụ: [1, 2, 2, 1] -> cnt[1]=2, cnt[2]=2. 
        // Nếu duyệt: i=0 (val=1) -> maxx=2. i=1 (val=2) -> maxx=2 (không đổi). i=2 (val=2) -> không đổi. i=3 (val=1) -> không đổi. 
        // Kết quả là 1 (đúng).
        // Nhưng nếu: [2, 1, 1, 2] -> i=0 (val=2) -> maxx=1. i=1 (val=1) -> maxx=2. -> ans=1. i=2 (val=1) -> maxx=2. -> ans=1. i=3 (val=2) -> maxx=2 (bằng). -> ans vẫn là 1. -> Đúng.
        // Rủi ro: Nếu có 2 số cùng tần suất max, số nào xuất hiện sau mà làm tăng tần suất lên bằng max thì số đó sẽ được chọn nếu logic là >=. Nhưng nếu logic là > thì sẽ giữ nguyên số đầu tiên đạt max.
        // Logic dưới đây là: Nếu tần suất hiện tại LỚN HƠN max hiện tại -> Cập nhật. Điều này đảm bảo số xuất hiện đầu tiên có tần suất MAX sẽ được giữ lại.
        if (cnt[temp] > maxx) {
            maxx = cnt[temp];
            value = temp;
        }
    }
    printf("%d %d\n", value, maxx);
    return 0;
}
  • Time Complexity: O(n)
  • Space Complexity: O(K) ~ O(100000)

Đây là biến thể tối ưu nhất. Thay vì lưu mảng a gồm n phần tử (tốn O(n)), ta chỉ cần lưu mảng đếm tần suất. Trong cùng một vòng lặp đọc dữ liệu, ta vừa đếm tần suất, vừa kiểm tra điều kiện cập nhật kết quả. Điều kiện cnt[temp] > maxx (nghiêm ngặt) đảm bảo rằng nếu một số mới có cùng tần suất với max hiện tại, nó sẽ không thay thế số cũ. Do đó, số được chọn là số đầu tiên trong dãy đạt được tần suất cao nhất.

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

Cách tiếp cận Time Space Tên
1 O(n) O(n) Brute Force (Đếm tần suất và kiểm tra lại)
2 O(n) O(n) Hash Map (Đếm tần suất và tối ưu bộ nhớ)
3 O(n) O(K) ~ O(100000) Tối ưu không gian (Không lưu mảng ban đầu)

Bài học kinh nghiệm

  • Độ lớn của các phần tử a_i (≤ 10^5) nhỏ, cho phép sử dụng mảng tĩnh (mảng đếm) thay cho Hash Map.
  • Để thỏa mãn điều kiện 'phần tử đầu tiên', cần ưu tiên duyệt theo thứ tự xuất hiện của dãy số ban đầu hoặc sử dụng phép so sánh nghiêm ngặt (>) thay vì (>=) khi cập nhật kết quả.
  • Có thể tối ưu bộ nhớ bằng cách không lưu trữ dãy số ban đầu mà xử lý luồng dữ liệu ngay khi đọc vào.

Lỗi thường gặp

  • Dùng phép so sánh >= khi cập nhật kết quả trong vòng lặp duyệt lại mảng ban đầu: Nếu dùng >=, phần tử xuất hiện sau cùng có thể ghi đè lên phần tử xuất hiện trước đó, vi phạm yêu cầu 'phần tử đầu tiên'.
  • Quên khai báo mảng đếm có kích thước đủ lớn (cần ít nhất 100001 phần tử để chứa giá trị 100000).
  • Biến lặp và biến giá trị có thể trùng tên gây lỗi logic.

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.