Leetcode_find_k_pairs_smallest_sum

博客提到利用堆来维护当前最小值,还表示要学习他人代码风格,涉及数据结构中堆的应用及代码学习方面的信息技术内容。

利用堆,维护当前最小值。

vector<vector<int>> kSmallestPairs(vector<int>& nums1, vector<int>& nums2, int k) {
        auto cmp = [](pair<int, int> x, pair<int, int> y){
            return x.first + x.second < y.first + y.second;};
        priority_queue<pair<int, int>, vector<pair<int, int>>, decltype(cmp)> heap(cmp);
        vector<vector<int>> result;
        for(int i = 0; i < nums1.size() && i < k; i++){
            for(int j=0; j < nums2.size() && j < k; j++){
                auto tmp = make_pair(nums1[i], nums2[j]);
                if(heap.size() < k){
                    heap.push(tmp);
                } else {
                    if(!cmp(heap.top(), tmp)){
                        heap.pop();
                        heap.push(tmp);
                    }
                }
            }
        }
        
        while(!heap.empty()){
            result.push_back({heap.top().first, heap.top().second});
            heap.pop();
        }
        return result;
        
    }

一个别人的写法,学习一下代码风格

 

vector<vector<int>> kSmallestPairs(vector<int>& nums1, vector<int>& nums2, int k) {
        if (nums1.empty() || nums2.empty())
            return vector< vector<int> >();
        
        using Pair = pair<int, int>;
        
        auto cmp_func = [&](Pair a, Pair b){ 
            return (nums1[a.first] + nums2[a.second]) > 
                (nums1[b.first] + nums2[b.second]);
        };
        
        using Pri = priority_queue<Pair, vector<Pair>,  decltype(cmp_func)>;
        
        Pri minheap(cmp_func);
        
        vector< vector<int> > result;
        
        for (int i = 0; i < nums1.size() && i < k; i++)
        {
            minheap.push({i, 0});
        }
        
        while (result.size() < k && !minheap.empty())
        {
            auto min = minheap.top(); 
            minheap.pop();
            result.push_back({nums1[min.first], nums2[min.second]});
            
            if (min.second < nums2.size() - 1)
            {
                minheap.push({min.first, min.second + 1});
            }
        }
        
        return result;
    }

 

上述代码的目的是:给定两个长度为 $ n $ 的数组 $ a $ 和 $ b $,求出所有 $ a_i + b_j $(共 $ n^2 $ 个和)中最小的 $ n $ 个值,并按升序输出。 我们来**逐行分析代码逻辑**,然后给出**时间复杂度分析**,并指出其问题所在。 --- ### ✅ 代码功能简述: 1. 使用两个优先队列(堆): - `q_min`:小根堆,用于最后输出结果。 - `q_max`:大根堆,维护当前已知的最小的 $ n $ 个数(通过保留较小的数,弹出较大的数)。 2. 初始时将 `a[1]+b[1]` 加入两个堆。 3. 双重循环遍历所有 $ i,j \in [1,n] $: - 跳过 `(1,1)`(已处理) - 计算 `s = a[i] + b[j]` - 如果 `q_max.size() < n`,就把 `s` 插入堆 - 否则,如果 `q_max.top() > s`,说明当前最大值比新值大,可以替换掉最大值以保持最小的 $ n $ 个数 - 弹出 `q_max` 的堆顶,并插入 `s` - **关键点:有一个 `else break;`** 4. 最后从小根堆 `q_min` 中依次输出前 $ n $ 小的值。 --- ### ❗️核心问题:`else break;` 这是本代码最关键的错误或误解之处。 ```cpp else break; ``` 这个 `break` 只跳出内层 `j` 循环。但它的逻辑是:如果 `q_max.top() <= s`,就 `break`。这意味着一旦遇到一个不值得加入的和,就直接跳出整个 `j` 循环 —— 这在一般情况下是**完全错误的**! #### 为什么错? 因为数组 `a` 和 `b` 并未排序!即使 `a[i] + b[j]` 当前较大,后面的 `b[j+1], ..., b[n]` 仍可能很小,导致 `a[i] + b[j']` 更小。 举个例子: ```cpp a = [100, 1], b = [100, 1] n = 2 ``` 当 `i=1`, 外层循环处理 `a[1]=100`: - j=1: `100+100=200` → 已初始化 - j=2: `100+1=101` → 假设堆里已经有更小的元素(比如 `1+1=2`),那么 `q_max.top()` 可能是 10,此时 `101 > 10`,进入 `else break;`,于是跳出了 j 循环。 但是这会导致漏掉很多组合,尤其是当 `i=2` 时还没处理完。 而且更重要的是:**外层循环对每个 i 都要完整地跑完内层 j 循环才能枚举所有 n² 种情况**。 所以这个 `break` 是致命的剪枝错误 —— 它破坏了正确性。 --- ### 🔍 正确思路应是什么? 若想从 $ n^2 $ 个和中选出最小的 $ n $ 个,常见优化方法是: 1. 先对 `a` 和 `b` 排序; 2. 使用堆进行“多路归并”思想:初始把 `a[0]+b[0], a[0]+b[1], ..., a[0]+b[n-1]` 放进堆; 3. 每次取出最小值,再扩展下一个可能的候选; 4. 类似于 LeetCode 第373题、第378题的做法。 但此代码既没有排序,也没有使用索引去重机制,还用了错误的 `break`,因此**不能保证正确性**。 --- ### ⏱️ 时间复杂度分析 尽管逻辑错误,我们仍可分析其最坏运行时间。 ```cpp for(int i=1; i<=n; i++) // 外层 n 次 { for(int j=1; j<=n; j++) // 内层 n 次 { if(...) continue; int s = a[i] + b[j]; if (size < n) ... else if (top > s) { pop & push } else break; // ❗️这里可能导致提前退出 } } ``` #### 最坏情况下的执行次数: - 外层:$ O(n) $ - 内层:理想情况下 $ O(n) $,但由于 `else break;`,实际执行次数依赖于数据顺序。 但由于 `a` 和 `b` 未排序,`a[i]+b[j]` 在 `j` 上不是单调的,所以 `break` 的条件不可靠,也无法保证性能提升。 因此,在最坏情况下(例如所有 `a[i]+b[j]` 都小于当前堆顶),不会触发 `break`,内层始终执行 $ n $ 次。 → 总共进行 $ n^2 $ 次判断和可能的堆操作。 每次堆操作(`push`, `pop`)的时间复杂度是 $ O(\log n) $,因为堆中最多有 $ n $ 个元素。 #### 所以总时间复杂度为: $$ \boxed{O(n^2 \log n)} $$ --- ### 🧠 为什么堆大小限制为 n? - `q_max` 是大根堆,维护目前已见的最小的 $ n $ 个数中的最大值。 - 当已有 $ n $ 个数后,只有比堆顶小的新数才可能进入。 - 这是一种典型的 Top-K 维护策略。 但由于 `break` 的存在,后续更小的值可能被跳过,所以无法正确维护。 --- ### ✅ 如何修复? 正确的做法如下(建议): 1. 对 `a` 和 `b` 数组排序; 2. 使用优先队列(最小堆)存储三元组 `(sum, i, j)`; 3. 初始加入 `(a[0]+b[0], 0, 0), (a[0]+b[1], 0, 1), ...` 或仅 `(a[0]+b[0])` 然后逐步扩展; 4. 使用 visited 数组防止重复插入; 5. 取出前 $ n $ 个最小的即可。 参考题目:[LeetCode 373. Find K Pairs with Smallest Sums](https://leetcode.com/problems/find-k-pairs-with-smallest-sums/) --- ### ✅ 示例修正代码(正确版): ```cpp #include <bits/stdc++.h> using namespace std; typedef long long ll; vector<ll> smallestSums(vector<ll>& a, vector<ll>& b, int n) { sort(a.begin(), a.end()); sort(b.begin(), b.end()); priority_queue<tuple<ll, int, int>, vector<tuple<ll, int, int>>, greater<>> pq; vector<vector<bool>> vis(n, vector<bool>(n, false)); pq.push({a[0] + b[0], 0, 0}); vis[0][0] = true; vector<ll> res; while (res.size() < n && !pq.empty()) { auto [sum, i, j] = pq.top(); pq.pop(); res.push_back(sum); if (i + 1 < n && !vis[i+1][j]) { pq.push({a[i+1] + b[j], i+1, j}); vis[i+1][j] = true; } if (j + 1 < n && !vis[i][j+1]) { pq.push({a[i] + b[j+1], i, j+1}); vis[i][j+1] = true; } } return res; } ``` 该算法时间复杂度为 $ O(n \log n) $,远优于原代码。 --- ### ❌ 原代码总结 | 项目 | 内容 | |------|------| | 功能目标 | 输出 $ a_i + b_j $ 中最小的 $ n $ 个值 | | 实际效果 | ❌ 不正确,因 `else break;` 提前终止内层循环 | | 时间复杂度 | $ O(n^2 \log n) $(最坏) | | 空间复杂度 | $ O(n) $(堆只存最多 $ n $ 个元素) | | 是否可行 | ❌ 错误剪枝导致遗漏合法解 | --- ###
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值