【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 的中位数 mednth_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 分钟前
在Matlab中绘制质点运动轨迹图
开发语言·学习·算法·matlab·信息可视化
爱炼丹的James5 分钟前
第三章 搜索和图论
数据结构·算法·图论
菜菜笔记6 分钟前
【无标题】
算法
努力努力再努力wz11 分钟前
【QT入门系列】QWidget 六大常用属性详解:windowOpacity、cursor、font、focus、toolTip 与 styleSheet
android·开发语言·数据结构·c++·qt·mysql·算法
Gauss松鼠会18 分钟前
GaussDB(DWS) 资源监控Topsql
java·网络·数据库·算法·oracle·性能优化·gaussdb
夏日听雨眠18 分钟前
数据结构(快速排序)
java·数据结构·算法
薇茗20 分钟前
【初阶数据结构】 升沉有序的平仄 排序 3
c语言·开发语言·数据结构·算法·排序算法·文件归并排序
薇茗22 分钟前
【初阶数据结构】 升沉有序的平仄 排序 2
c语言·数据结构·算法·排序算法·快排精讲
AI科技星26 分钟前
强哥德巴赫猜想(1+1)终极证明(2026 年5月 21 日)
开发语言·人工智能·算法·计算机视觉·量子计算
人道领域27 分钟前
【LeetCode刷题日记】654.最大二叉树:递归算法详解
java·算法·leetcode