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. 核心问题与瓶颈
- 目标:不断合并相邻且和最小的一对数,直到数组非递减。
- 策略 :
- 不实际移动数组 :使用链表思想。通过
TreeSet<Integer> idx存储当前依然存在 的元素下标。下标 i i i 的下一个元素不再是 i + 1 i+1 i+1,而是idx.higher(i)。这样"删除"元素只需 O ( log N ) O(\log N) O(logN)。 - 快速获取最小对 :使用
TreeSet<Pair> pairs维护所有相邻对的和。利用TreeSet的自动排序功能,pollFirst()可以在 O ( log N ) O(\log N) O(logN) 时间内拿到当前和最小的相邻对。 - 动态维护逆序对数量 :使用变量
dec记录当前数组中逆序对(相邻且前>后)的数量 。- 当
dec == 0时,数组已经有序,循环终止。 - 每次合并操作只影响局部(当前对的前后关系),可以在常数时间内更新
dec,避免了 O ( N ) O(N) O(N) 的全局检查。
- 当
- 不实际移动数组 :使用链表思想。通过
2. 算法逻辑
-
初始化:
pairs:存入初始所有相邻对的和与左下标。idx:存入初始所有下标0到n-1。arr:复制原数组(使用long防止溢出)。dec:统计初始的相邻逆序对数量。
-
主循环 (
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) 生成,加入。
- 旧关系: a r r [ p r e ] arr[pre] arr[pre] vs a r r [ i ] arr[i] arr[i] 被破坏。如果原先逆序,
- 右邻居关系 (
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]) 生成,加入。
- 旧关系: a r r [ n x t ] arr[nxt] arr[nxt] vs a r r [ n x t n x t ] arr[nxtnxt] arr[nxtnxt] 被破坏。如果原先逆序,
- 内部关系 :原先 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],则
- 执行删除 :更新
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)
- 预处理 :初始化
pairs和idx,耗时 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)。
参考灵神