本文介绍一种线性扫描结合频次统计的算法,用于在数组中找出满足“移除一个元素后各数字出现次数均相等”的最长前缀长度,时间复杂度 O(n),空间复杂度 O(n)。

本文介绍一种线性扫描结合频次统计的算法,用于在数组中找出满足“移除一个元素后各数字出现次数均相等”的最长前缀长度,时间复杂度 O(n),空间复杂度 O(n)。

“无聊”(boring)前缀的定义是:该前缀中恰好存在一个元素,将其删除后,剩余所有不同数字的出现频次完全相同。例如 [1,2,3,1,2,2,3,3,3,1](长度为10)是无聊的——删去一个 3 后,1、2、3 均出现 3 次。

要高效判断每个前缀 a[0..i] 是否为无聊前缀,关键在于动态维护频次分布的核心统计量,而非对每个前缀重新计数(那将导致 O(n²) 复杂度)。我们需要在遍历过程中实时更新以下四个变量:

这些变量可在每次插入 arr[i] 时 O(1) 更新(通过先减旧频次影响、再加新频次影响实现)。

一个前缀 a[0..i] 是无聊的,当且仅当以下任一条件成立(覆盖所有合法删元情形):

  1. 所有元素频次均为 1 → 删任意一个,其余全为 1
    ⇒ maxFreq == 1
  2. 仅有一个元素频次为 maxFreq,其余全为 maxFreq − 1,且 maxFreq > 1
    ⇒ countMax == 1 && (distinct - 1) * (maxFreq - 1) + maxFreq == i + 1 && countMaxMinusOne == distinct - 1
    (总长度 = 1 个高频元 + 其余 distinct−1 个 maxFreq−1 元)
  3. 仅有一个元素频次为 1,其余全为同一频次 f > 1
    ⇒ countMax == distinct - 1 && countMaxMinusOne == 1 && maxFreq * (distinct - 1) + 1 == i + 1
    (即删掉那个只出现 1 次的元素,剩下 distinct−1 种数各出现 maxFreq 次)

实际编码中,条件2和3可更简洁地用频次映射验证,但上述逻辑已足够指导实现。以下是完整 Java 实现:

import java.util.*;

public class BoringPrefix {
    public static int findMaxBoringPrefixLength(int[] arr) {
        if (arr == null || arr.length < 2) return 0;

        Map<Integer, Integer> freq = new HashMap<>(); // 数字 → 当前频次
        int distinct = 0, maxFreq = 0, countMax = 0, countMaxMinusOne = 0;
        int result = 0;

        for (int i = 0; i < arr.length; i++) {
            int x = arr[i];
            int oldFreq = freq.getOrDefault(x, 0);
            int newFreq = oldFreq + 1;
            freq.put(x, newFreq);

            // 更新 distinct
            if (oldFreq == 0) distinct++;

            // 从旧频次组中移除 x 的影响
            if (oldFreq > 0) {
                if (oldFreq == maxFreq) countMax--;
                if (oldFreq == maxFreq - 1) countMaxMinusOne--;
            }

            // 加入新频次组的影响
            if (newFreq > maxFreq) {
                // 新频次打破纪录:maxFreq 升级
                maxFreq = newFreq;
                countMax = 1;
                countMaxMinusOne = (oldFreq > 0) ? (oldFreq == maxFreq - 1 ? 1 : 0) : 0;
            } else if (newFreq == maxFreq) {
                countMax++;
            } else if (newFreq == maxFreq - 1) {
                countMaxMinusOne++;
            }

            // 检查是否为 boring 前缀
            boolean isBoring = false;

            // Case 1: 所有频次为 1
            if (maxFreq == 1) {
                isBoring = true;
            }
            // Case 2: 一个元素频次为 maxFreq,其余为 maxFreq-1
            else if (countMax == 1 && countMaxMinusOne == distinct - 1) {
                isBoring = true;
            }
            // Case 3: 一个元素频次为 1,其余为 maxFreq(此时 maxFreq >= 2)
            else if (countMax == distinct - 1 && 
                     freq.values().stream().filter(f -> f == 1).count() == 1) {
                isBoring = true;
            }

            if (isBoring) result = i + 1; // 记录最长有效前缀长度
        }

        return result;
    }

    // 示例调用
    public static void main(String[] args) {
        int[] arr = {1, 2, 3, 1, 2, 2, 3, 3, 3, 1, 4, 4, 5};
        System.out.println(findMaxBoringPrefixLength(arr)); // 输出: 10
    }
}

⚠️ 注意事项:

总结:识别“无聊前缀”的本质是识别频次分布的特殊平衡态。通过精心设计的四变量状态机,我们能在 O(n) 时间内完成全部判断,适用于 n ≤ 10⁵ 规模的实际场景。

本文转载于:互联网 如有侵犯,请联系zhengruancom@outlook.com删除。
免责声明:正软商城发布此文仅为传递信息,不代表正软商城认同其观点或证实其描述。