【LeetCode 每日一题】3510. 移除最小数对使数组有序 II

Problem: 3510. 移除最小数对使数组有序 II

文章目录

  • 整体思路
      • [1. 核心问题与瓶颈](#1. 核心问题与瓶颈)
      • [2. 算法逻辑](#2. 算法逻辑)
  • 完整代码
  • 时空复杂度
      • [1. 时间复杂度: O ( N log ⁡ N ) O(N \log N) O(NlogN)](#1. 时间复杂度: O ( N log ⁡ N ) O(N \log N) O(NlogN))
      • [2. 空间复杂度: O ( N ) O(N) O(N)](#2. 空间复杂度: O ( N ) O(N) O(N))

整体思路

1. 核心问题与瓶颈

  • 目标:不断合并相邻且和最小的一对数,直到数组非递减。
  • 策略
    1. 不实际移动数组 :使用链表思想。通过 TreeSet<Integer> idx 存储当前依然存在 的元素下标。下标 i i i 的下一个元素不再是 i + 1 i+1 i+1,而是 idx.higher(i)。这样"删除"元素只需 O ( log ⁡ N ) O(\log N) O(logN)。
    2. 快速获取最小对 :使用 TreeSet<Pair> pairs 维护所有相邻对的和。利用 TreeSet 的自动排序功能,pollFirst() 可以在 O ( log ⁡ N ) O(\log N) O(logN) 时间内拿到当前和最小的相邻对。
    3. 动态维护逆序对数量 :使用变量 dec 记录当前数组中逆序对(相邻且前>后)的数量
      • dec == 0 时,数组已经有序,循环终止。
      • 每次合并操作只影响局部(当前对的前后关系),可以在常数时间内更新 dec,避免了 O ( N ) O(N) O(N) 的全局检查。

2. 算法逻辑

  1. 初始化

    • pairs:存入初始所有相邻对的和与左下标。
    • idx:存入初始所有下标 0n-1
    • arr:复制原数组(使用 long 防止溢出)。
    • dec:统计初始的相邻逆序对数量。
  2. 主循环 (while dec > 0)

    • 取最小对 :从 pairs 弹出和最小的对 (s, i)。 i i i 是左下标,nxt = idx.higher(i) 是右下标。
    • 合并逻辑 :我们要把 i i i 和 n x t nxt nxt 合并,结果存在 i i i 位置, n x t nxt nxt 被移除。新值为 s = a r r [ i ] + a r r [ n x t ] s = arr[i] + arr[nxt] s=arr[i]+arr[nxt]。
    • 更新逆序计数 (dec)
      • 内部关系 :原先 a r r [ i ] arr[i] arr[i] 和 a r r [ n x t ] arr[nxt] arr[nxt] 的关系消失了。如果之前 a r r [ i ] > a r r [ n x t ] arr[i] > arr[nxt] arr[i]>arr[nxt],则 dec--
      • 左邻居关系 (pre) :如果存在左邻居 pre
        • 旧关系: a r r [ p r e ] arr[pre] arr[pre] vs a r r [ i ] arr[i] arr[i] 被破坏。如果原先逆序,dec--
        • 新关系: a r r [ p r e ] arr[pre] arr[pre] vs s s s 建立。如果新关系逆序,dec++
        • 更新 pairs :旧的对 ( a r r [ p r e ] + a r r [ i ] ) (arr[pre] + arr[i]) (arr[pre]+arr[i]) 失效,移除;新的对 ( a r r [ p r e ] + s ) (arr[pre] + s) (arr[pre]+s) 生成,加入。
      • 右邻居关系 (nxtnxt) :如果存在右邻居的右邻居 nxtnxt(即 n x t nxt nxt 的右边)。
        • 旧关系: a r r [ n x t ] arr[nxt] arr[nxt] vs a r r [ n x t n x t ] arr[nxtnxt] arr[nxtnxt] 被破坏。如果原先逆序,dec--
        • 新关系: s s s vs a r r [ n x t n x t ] arr[nxtnxt] arr[nxtnxt] 建立。如果新关系逆序,dec++
        • 更新 pairs :旧的对 ( a r r [ n x t ] + a r r [ n x t n x t ] ) (arr[nxt] + arr[nxtnxt]) (arr[nxt]+arr[nxtnxt]) 失效,移除;新的对 ( s + a r r [ n x t n x t ] ) (s + arr[nxtnxt]) (s+arr[nxtnxt]) 生成,加入。
    • 执行删除 :更新 arr[i] = s,从 idx 中移除 nxt
    • 计数ans++

完整代码

java 复制代码
import java.util.TreeSet;

class Solution {
    // 记录类:存储相邻两数之和 s,以及该对左边元素的下标 i
    private record Pair(long s, int i) {
    }

    public int minimumPairRemoval(int[] nums) {
        int n = nums.length;
        
        // 1. 优先队列 (TreeSet) 维护当前所有的相邻对
        // 排序规则:按和 s 从小到大排序;和相同时按索引 i 从小到大排序
        TreeSet<Pair> pairs = new TreeSet<>((a, b) -> a.s != b.s ? Long.compare(a.s, b.s) : a.i - b.i);
        
        // 2. 统计初始逆序对数量,并初始化 pairs
        int dec = 0; // dec: decreasing pairs count
        for (int i = 0; i < n - 1; i++) {
            int x = nums[i];
            int y = nums[i + 1];
            if (x > y) {
                dec++;
            }
            pairs.add(new Pair((long)x + y, i));
        }
        
        // 3. 链表模拟 (TreeSet) 维护当前有效的下标
        // 这样可以快速找到某个下标的前驱和后继
        TreeSet<Integer> idx = new TreeSet<>();
        for (int i = 0; i < n; i++) {
            idx.add(i);
        }
        
        // 使用 long 数组存储当前值,防止加法溢出
        long[] arr = new long[n];
        for (int i = 0; i < n; i++) {
            arr[i] = nums[i];
        }

        int ans = 0;
        
        // 4. 主循环:只要还存在逆序对,就继续合并
        while (dec > 0) {
            ans++;

            // 取出和最小的相邻对
            Pair p = pairs.pollFirst();
            long s = p.s;  // 合并后的新值
            int i = p.i;   // 左下标

            // 找到右下标 (被合并并移除的那个)
            int nxt = idx.higher(i);
            
            // --- 更新 dec 计数 (内部) ---
            // 这一对被合并了,它们之间的逆序关系自然消失
            if (arr[i] > arr[nxt]) {
                dec--;
            }

            // --- 处理左邻居 (pre) ---
            Integer pre = idx.lower(i);
            if (pre != null) {
                // 1. 撤销旧的逆序关系
                if (arr[pre] > arr[i]) {
                    dec--;
                }
                // 2. 建立新的逆序关系 (pre 与 合并后的新值 s)
                if (arr[pre] > s) {
                    dec++;
                }
                // 3. 更新 pairs 集合
                // 移除旧对 (pre, i),添加新对 (pre, 新的i)
                // 注意:这里必须精确构造出旧的 Pair 对象才能删除
                pairs.remove(new Pair(arr[pre] + arr[i], pre));
                pairs.add(new Pair(arr[pre] + s, pre));
            }

            // --- 处理右邻居的右邻居 (nxtnxt) ---
            Integer nxtnxt = idx.higher(nxt);
            if (nxtnxt != null) {
                // 1. 撤销旧的逆序关系 (nxt 与 nxtnxt)
                if (arr[nxt] > arr[nxtnxt]) {
                    dec--;
                }
                // 2. 建立新的逆序关系 (合并后的新值 s 与 nxtnxt)
                if (s > arr[nxtnxt]) {
                    dec++;
                }
                // 3. 更新 pairs 集合
                // 移除旧对 (nxt, nxtnxt),添加新对 (新的i, nxtnxt)
                pairs.remove(new Pair(arr[nxt] + arr[nxtnxt], nxt));
                pairs.add(new Pair(s + arr[nxtnxt], i));
            }

            // --- 执行合并 ---
            // 更新 i 位置的值为和
            arr[i] = s;
            // 从有效下标集合中移除 nxt,相当于链表中删除了节点
            idx.remove(nxt);
        }  
        return ans;    
    }
}

时空复杂度

假设数组初始长度为 N N N。

1. 时间复杂度: O ( N log ⁡ N ) O(N \log N) O(NlogN)

  • 预处理 :初始化 pairsidx,耗时 O ( N log ⁡ N ) O(N \log N) O(NlogN)。
  • 循环
    • 在最坏情况下,循环可能执行 N N N 次(合并到只剩一个)。
    • 每次循环内部操作:
      • pairs.pollFirst(): O ( log ⁡ N ) O(\log N) O(logN)。
      • idx.higher(), idx.lower(): O ( log ⁡ N ) O(\log N) O(logN)。
      • pairs.remove(), pairs.add(): O ( log ⁡ N ) O(\log N) O(logN)。
      • idx.remove(): O ( log ⁡ N ) O(\log N) O(logN)。
    • 所有操作都是对数级别的。
  • 总计 : O ( N log ⁡ N ) O(N \log N) O(NlogN)。

2. 空间复杂度: O ( N ) O(N) O(N)

  • 计算依据
    • pairs (TreeSet) 存储 O ( N ) O(N) O(N) 个对象。
    • idx (TreeSet) 存储 O ( N ) O(N) O(N) 个整数。
    • arr 数组存储 N N N 个 long。
  • 结论 : O ( N ) O(N) O(N)。

参考灵神

相关推荐
C+++Python2 小时前
C++ vector
开发语言·c++·算法
2401_841495642 小时前
【LeetCode刷题】K 个一组翻转链表
数据结构·python·算法·leetcode·链表·翻转链表·迭代翻转
zhangrelay2 小时前
如何更环保(更省钱)的使用各类电子耗材/消耗品/易损件~电池为例
linux·笔记·学习
dustcell.2 小时前
高级课前复习2--RHCSA
linux·运维·服务器
胖少年3 小时前
Ubuntu 24.04 LTS apt autoremove 误删依赖致程序崩溃 解决与预防笔记
linux·笔记·ubuntu
Shea的笔记本3 小时前
MindSpore实战笔记:Pix2Pix图像转换复现全记录
笔记·算法·机器学习·web3
清酒难咽3 小时前
算法案例之蛮力法
c++·经验分享·算法
想逃离铁厂的老铁3 小时前
Day50 >> 98、可达路径 + 广度优先搜索理论基础
算法·深度优先·图论
Controller-Inversion3 小时前
k8s服务部署相关问题
linux·容器·kubernetes