【LeetCode每日一题】:2033(贪心+快速排序魔改)

题号2033

  • [🌟 LeetCode 2033 题解:Minimum Operations to Make a Uni-Value Grid](#🌟 LeetCode 2033 题解:Minimum Operations to Make a Uni-Value Grid)
    • [1. 题目分析(先讲人话版)](#1. 题目分析(先讲人话版))
    • [2. 贪心结论:选中位数最优](#2. 贪心结论:选中位数最优)
      • [2.1 为什么是中位数?(直观证明)](#2.1 为什么是中位数?(直观证明))
      • [2.2 稍微严格一点的证明](#2.2 稍微严格一点的证明)
    • [3. 可行性判断(非常关键)](#3. 可行性判断(非常关键))
    • [4. 算法流程](#4. 算法流程)
    • [5. 我的排序递进过程:从"能分区"到"能找第 k 个"](#5. 我的排序递进过程:从“能分区”到“能找第 k 个”)
    • [5.1 第一阶段:中间值分区法(我最初的写法)](#5.1 第一阶段:中间值分区法(我最初的写法))
      • 排序部分代码
      • [🌈 示意图(只分区,不归位)](#🌈 示意图(只分区,不归位))
      • [✅ 优点](#✅ 优点)
      • [⚠️ 缺点](#⚠️ 缺点)
    • [5.2 第二阶段:首元素基准 + 填坑法(可定位)](#5.2 第二阶段:首元素基准 + 填坑法(可定位))
      • 排序部分代码
      • [🌈 示意图(填坑)](#🌈 示意图(填坑))
      • [✅ 优点](#✅ 优点)
      • [⚠️ 缺点](#⚠️ 缺点)
    • [5.3 第三阶段:中位基准 + 填坑法(我最后采用)](#5.3 第三阶段:中位基准 + 填坑法(我最后采用))
      • 排序部分代码
      • [🌈 图解流程](#🌈 图解流程)
      • [✅ 优点](#✅ 优点)
      • [⚠️ 缺点](#⚠️ 缺点)
    • [6. 最简单方案:直接用 STL `nth_element`](#6. 最简单方案:直接用 STL nth_element)
    • [7. 三种手写分区方法对比](#7. 三种手写分区方法对比)
    • [8. 一句总结](#8. 一句总结)

🌟 LeetCode 2033 题解:Minimum Operations to Make a Uni-Value Grid

原题链接:https://leetcode.cn/problems/minimum-operations-to-make-a-uni-value-grid/description/?envType=daily-question&envId=2026-04-28


1. 题目分析(先讲人话版)

我们每次操作可以对某个数 +x 或 -x,目标是让整个二维网格都变成同一个值,问最少操作次数。

核心观察有两步:

  1. 先把二维数组拉平成一维数组,问题不变。
  2. 如果最终都要变成同一个目标值 t,总代价就是:
    • sum(|a[i] - t| / x)(前提是每个 a[i] 都能通过 ±x 到达 t)

于是题目变成:

  • 能不能到达同一个值?
  • 如果能,选哪个目标值 t 让总代价最小?

2. 贪心结论:选中位数最优

把数组记作 a,并按升序排好(或者只找中位数,不一定全排)。

我们要最小化:

F(t) = Σ |a[i] - t|

这是经典结论:绝对值距离和在中位数处最小。

2.1 为什么是中位数?(直观证明)

  • 当 t 往右移动一点:
    • 左边元素到 t 的距离变大
    • 右边元素到 t 的距离变小
  • 只有当"左边个数"和"右边个数"尽量平衡时,总代价最小。
  • 这个平衡点正是中位数位置。

2.2 稍微严格一点的证明

设数组升序后为 b[0..n-1]。考虑 t 每增加 1 时 F(t) 的变化量:

  • 对每个 b[i] <= t,贡献 +1
  • 对每个 b[i] > t,贡献 -1

所以"斜率"大致是:

count_left - count_right

当 t 在中位数左边,右侧更多,斜率负,F(t) 还在下降;

当 t 在中位数右边,左侧更多,斜率正,F(t) 开始上升;

因此最小值出现在中位数处(偶数长度时是中间区间,取任一中位数都行)。


3. 可行性判断(非常关键)

因为每次只能加减 x,一个数的模 x 余数不会变。

所以要想最终全相等,所有元素必须满足:

a[i] % x 都相同。

只要有一个不同,直接返回 -1。


4. 算法流程

  1. 拉平二维数组到 nums。
  2. 同时检查所有元素 % x 是否一致,不一致返回 -1。
  3. 找 nums 的中位数 med(nth_element 或 quickselect)。
  4. 答案是 sum(|v - med|) / x。

时间复杂度:

  • nth_element 平均 O(N),N = n*m
  • 总体平均 O(N)

空间复杂度:

  • O(N)(存拉平数组)

5. 我的排序递进过程:从"能分区"到"能找第 k 个"

同样是"取中间值",为什么有的分区只能分两边,有的却能直接定位第 k 小?


5.1 第一阶段:中间值分区法(我最初的写法)

排序部分代码

cpp 复制代码
auto _sort=[&](int left,int right)->int
{
    if(left >= right) return left;

    int l = left;
    int r = right;
    int base = nums[(l + r) / 2];

    while(l < r)
    {
        while(l < r && nums[r] >= base) r--;
        while(l < r && nums[l] <= base) l++;
        if(l < r) 
        {
	        swap(nums[l], nums[r]);
	    }
    }
    return l;
};

🌈 示意图(只分区,不归位)

text 复制代码
初始: [ 9, 1, 8, 3, 7, 2, 6 ]
base = 中间值 3

过程后可能变成:
[ 2, 1, 3, 8, 7, 9, 6 ]
          ^
         l/r 相遇点

✅ 优点

  • 代码短
  • 做"粗分区"很快

⚠️ 缺点

  • 返回下标不保证是基准最终位置
  • 只能分区间,不能稳定定位第 k 小

5.2 第二阶段:首元素基准 + 填坑法(可定位)

排序部分代码

cpp 复制代码
auto _sort=[&](int left,int right)->int
{
    int base = nums[left];
    int l = left, r = right;

    while (l < r)
    {
        while (l < r && nums[r] >= base) r--;
        nums[l] = nums[r];

        while (l < r && nums[l] <= base) l++;
        nums[r] = nums[l];
    }

    nums[l] = base;
    return l;
};

🌈 示意图(填坑)

text 复制代码
base 挖坑:
[ (base), 5, 2, 9, 1, 7 ]
   ^ left 坑

从右找 < base 的数填左坑,再从左找 > base 的数填右坑...

最终:
[ 1, 2, (base), 9, 5, 7 ]
          ^
      base 最终位置

✅ 优点

  • 返回下标就是基准最终位置
  • 能稳定用于 quickselect

⚠️ 缺点

  • 如果始终取首元素,遇到坏序列时退化风险高

5.3 第三阶段:中位基准 + 填坑法(我最后采用)

排序部分代码

cpp 复制代码
auto _sort=[&](int left,int right)->int
{
    int mid = (left + right) / 2;
    int base = nums[mid];
    swap(nums[left], nums[mid]);

    int l = left, r = right;
    while (l < r)
    {
        while (l < r && nums[r] >= base) r--;
        nums[l] = nums[r];

        while (l < r && nums[l] <= base) l++;
        nums[r] = nums[l];
    }

    nums[l] = base;
    return l;
};

🌈 图解流程

text 复制代码
步骤A:选中位基准
[ 9, 4, 7, 3, 8, 2, 6 ]
          ^ mid, base=3

步骤B:换到 left,开始挖坑
[ 3, 4, 7, 9, 8, 2, 6 ]
  ^ left 坑

步骤C:填坑循环后
[ 2, 3, 7, 9, 8, 4, 6 ]
     ^ base 归位(可用于 quickselect)

✅ 优点

  • 比固定首元素更不容易退化
  • 比只交换不归位更适合找第 k 小

⚠️ 缺点

  • 代码复杂度高于直接用 STL

6. 最简单方案:直接用 STL nth_element

如果只是为了拿中位数,工程上最推荐直接用 STL。

cpp 复制代码
int k = nums.size() / 2;
nth_element(nums.begin(), nums.begin() + k, nums.end());
int med = nums[k];

它的含义是:

  • nums[k] 会是"如果整体排序后"第 k 个元素
  • 左边都 <= nums[k],右边都 >= nums[k]
  • 但左右两边内部不保证有序

这正好满足本题需求。


7. 三种手写分区方法对比

方法 能否把基准放到最终位置 能否直接用于找第k小 实现复杂度 退化风险
中间值交换分区(第一版) ❌ 不能保证 ⚠️ 不稳 🙂 简单 中等
首元素基准填坑(第二版) ✅ 可以 ✅ 稳定 😐 中等 偏高
中位基准 + 填坑(第三版) ✅ 可以 ✅ 稳定 😐 中等 较低

8. 一句总结

这题本质是"同余可达性 + 中位数最小绝对偏差"。

排序/快选只是为了高效拿到中位数;想省心就直接 nth_element。

相关推荐
和裕2 分钟前
全纸结构重型纸箱能否满足 1 吨以上设备出口熏蒸豁免要求?通关合规性全解析
大数据·运维·网络·人工智能·算法
tangguofeng35 分钟前
不带头结点的链队操作集(不含InitFlag)(C语言版)
算法
苏打豆2 小时前
CRISP源码阅读——基于ROS2力矩反馈控制器与速度加速度零空间
c++·线性代数·算法·矩阵·机器人
蛋蛋的就会顺顺的2 小时前
hot100——矩阵
java·数据结构·算法·leetcode·力扣
老歌老听老掉牙2 小时前
麻花钻切屑形态演变的力学机制与临界条件分析
python·算法·钻头
zhangfeng11333 小时前
国产GPU/AI算力芯片现状(修订完整版 · 截至2026年9月
人工智能·算法·ai编程·npu
学代码的CJY3 小时前
从代码出发理解时间复杂度与空间复杂度
数据结构·算法
白帽攻防录4 小时前
SRC 挖洞:WSO2 JWT 算法混淆绕过深度复盘,CVE-2026-5430 不支持的算法怎么变成管理员
网络·算法·安全·网络安全·jwt
小O的算法实验室4 小时前
IEEE TEVC,基于精确与DQN群智能调度方法用于多目标异构USV
算法
可乐ea5 小时前
AI Agent 工具调用准确性评测:选择错误与参数错误分开测
大数据·人工智能·算法·大模型·工具调用·ai智能体·agent评测