枚举–前缀和

# 超时

“`C++
class Solution {
public:
int specialTriplets(vector& nums) {
int n = nums.size();
long long ans = 0;
const int MOD = 1e9 + 7;

unordered_map cnt;
for (int i = 0; i < n; i++) { cnt[nums[i]]++; } for (int k = 1; k < n - 1; k++) { int b = nums[k]; int num_a = 0; int num_c = 0; for (int i = 0; i < k; i++) { if (nums[i] == b * 2) num_a++; } num_c = cnt[b * 2] - num_a; if (b == 0) num_c--; ans = (ans + (num_a * num_c) % MOD) % MOD; } return ans; } }; ``` # 答案 ```C++ class Solution { public: int specialTriplets(vector& nums) {
constexpr int MOD = 1e9 + 7;

unordered_map suf;
for (auto x : nums)
{
suf[x]++;
}

long long ans = 0;
unordered_map pre;
for (int x : nums)
{
suf[x]–;
ans += 1LL * pre[x * 2] * suf[x * 2];
pre[x]++;
}

return ans % MOD;
}
};
“`

### 1. 核心差异:时间复杂度

这是导致“超时”的根本原因。

– **超时代码 (TLE)**:
– **复杂度**:$O(N^2)$。
– **原因**:外层循环枚举中间点 `k`,内层循环 `for (int i = 0; i < k; i++)` 每次都要重新扫描一遍左边的数组来统计 `num_a`。当 $N=10^5$ 时,计算量达到 $10^{10}$ 级别,远超通常的 $10^8$ 限制。 - **答案代码 (AC)**: - **复杂度**:$O(N)$。 - **原因**:去掉了内层循环。利用**哈希表(或数组)实时维护**前缀信息。每移动一步,只需要 $O(1)$ 的时间更新 `pre` 和 `suf`,不需要回头重新扫描。 ------ ### 2. 优化逻辑:由“静态统计”转向“动态维护” 做笔记时,重点记录这个思维转变过程: - **暴力思维 (TLE)**: - “我现在到了第 `k` 个位置,我不记得前面发生了什么,所以我得回头**重新数一遍**前面有多少个符合条件的数。” - **优化思维 (AC)**: - “我一路走过来,用一个小本子 (`pre` map) **随手记下**刚才见过的数字。当我走到第 `k` 个位置时,不用回头,直接查小本子就知道前面有多少个符合条件的数。” ------ ### 3. 代码实现细节对比 (笔记重点) 请在笔记中着重标注 **AC 代码** 的这三个步骤,这是处理“三元组问题”的标准操作流: #### A. 预处理 (Pre-processing) 先把所有元素放入“右侧池子” (`suf`)。 ```C++ // 初始化:假设指针在最左边,所有元素都在右边 (suffix) for (auto x : nums) suf[x]++; ``` #### B. 核心循环三部曲 (The 3-Step Loop) 这是优化的精髓,顺序绝对不能乱: 1. **移出右侧**:当前遍历到的元素 `x` 也就是中间元素 `nums[j]`,它不再属于“未来”了,所以从 `suf` 中减去。 2. **计算贡献**:利用当前的 `pre` (左侧历史) 和 `suf` (右侧剩余) 直接计算答案。 3. **加入左侧**:当前元素 `x` 处理完毕,对于下一个元素来说,`x` 变成了“历史”,所以加入 `pre`。 ```C++ for (int x : nums) { // 1. 现任变前任:它不再是右边的元素了 suf[x]--; // 2. 搞事情:左边有多少个目标 * 右边有多少个目标 // 这里的 x 就是中间那个数,我们要找的是左右两边等于 x*2 的数 ans += 1LL * pre[x * 2] * suf[x * 2]; // 3. 记录历史:把它加入左边的记录,供后面的人查询 pre[x]++; } ``` ------ ### 4. 笔记总结模板 (Copy 此处) > **【算法笔记】三元组问题 ($i < j < k$) 通用解法** >
> **场景**:在数组中寻找满足特定数值关系的三元组 (如 `nums[i] == a, nums[j] == b, nums[k] == c`)。
>
> **核心技巧**:**枚举中间元素 `j`**。
>
> – 固定中间元素 `j`,问题转化为:“在 `j` 左边找有多少个 `a`,在 `j` 右边找有多少个 `c`”。
> – 答案 = $\sum (\text{count\_left}[a] \times \text{count\_right}[c])$。
>
> **数据结构**:
>
> – `pre` Map/Array:维护 `0` 到 `j-1` 的频率(动态增加)。
> – `suf` Map/Array:维护 `j+1` 到 `n-1` 的频率(动态减少)。
>
> **防坑点**:
>
> 1. **初始化**:`suf` 先填满,`pre` 为空。
> 2. **更新顺序**:先 `suf[x]–` (当前元素移出右边),再计算 `ans`,最后 `pre[x]++` (当前元素加入左边)。
> 3. **数据溢出**:乘积可能超过 `int`,累加 `ans` 时必须强转 `long long` (即 `1LL * …`)。

### 5. 额外提示 (Corner Case)

在你的 TLE 代码中,有一行处理非常棘手:

“`C++
if (b == 0) num_c–;
“`

这是因为在暴力法中,你是用 `总数 – 左边 = 右边` 来算右边的。如果目标数是 0,且当前中间数也是 0,`2*b` 还是 0,你会把**当前这个 0** 也算进右边去,所以要减掉。

**AC 代码的优势**:通过 `suf[x]–` 在计算前就把当前元素移除了,**天然避免了**这个问题,不需要特殊判断 0 的情况,代码更简洁、不易出错。

发表评论