2033.获取单值网格的最小操作数

目标

给你一个大小为 m x n 的二维整数网格 grid 和一个整数 x 。每一次操作,你可以对 grid 中的任一元素 加 x 或 减 x 。

单值网格 是全部元素都相等的网格。

返回使网格化为单值网格所需的 最小 操作数。如果不能,返回 -1 。

示例 1:

输入:grid = [[2,4],[6,8]], x = 2
输出:4
解释:可以执行下述操作使所有元素都等于 4 : 
- 2 加 x 一次。
- 6 减 x 一次。
- 8 减 x 两次。
共计 4 次操作。

示例 2:

输入:grid = [[1,5],[2,3]], x = 1
输出:5
解释:可以使所有元素都等于 3 。

示例 3:

输入:grid = [[1,2],[3,4]], x = 2
输出:-1
解释:无法使所有元素相等。

说明:

  • m == grid.length
  • n == grid[i].length
  • 1 <= m, n <= 10^5
  • 1 <= m * n <= 10^5
  • 1 <= x, grid[i][j] <= 10^4

思路

有一个 m x n 矩阵,每次操作可以将矩阵中的任意元素 +x 或者 -x,求使得矩阵所有元素相等所需的最小操作,如果不能返回 -1

所有元素对 x 取模余数相同才有解,因为操作无法改变这个余数,而目标是将元素变成同一个数。

如果有解,可以将共同的余数减去,将每个元素都除以 x,然后令 x = 1,问题变成找到一个值,使得所有元素到这个值的距离之和最小。

这是一个 L1 优化问题,它的解是中位数,可以先排序,再取中间的元素即可。

代码


/**
 * @date 2026-04-28 9:35
 */
public class MinOperations2033 {

    public int minOperations(int[][] grid, int x) {
        int m = grid.length;
        int n = grid[0].length;
        int[] arr = new int[m * n];
        int rem = grid[0][0] % x;
        int k = 0;
        for (int[] row : grid) {
            for (int e : row) {
                if (e % x != rem) {
                    return -1;
                }
                arr[k++] = e / x;
            }
        }
        Arrays.sort(arr);
        int median = arr[arr.length / 2];
        int res = 0;
        for (int num : arr) {
            res += Math.abs(num - median);
        }
        return res;
    }
}

性能

LCP24.数字游戏

目标

小扣在秋日市集入口处发现了一个数字游戏。主办方共有 N 个计数器,计数器编号为 0 ~ N-1。每个计数器上分别显示了一个数字,小扣按计数器编号升序将所显示的数字记于数组 nums。每个计数器上有两个按钮,分别可以实现将显示数字加一或减一。小扣每一次操作可以选择一个计数器,按下加一或减一按钮。

主办方请小扣回答出一个长度为 N 的数组,第 i 个元素(0 <= i < N)表示将 0~i 号计数器 初始 所示数字操作成满足所有条件 nums[a]+1 == nums[a+1],(0 <= a < i) 的最小操作数。回答正确方可进入秋日市集。

由于答案可能很大,请将每个最小操作数对 1,000,000,007 取余。

思路

老实说这道题我花了好一会儿才弄清楚题目要求,最后解法还因为超时没有通过。

先解释一下目标吧,实际上需要求解的是每一个位置上的局部最优解。所谓局部指仅考虑当前位置及之前的所有位置的操作次数。所谓最优是指初始序列达到满足条件的状态(公差为1的等差数列)时所做操作总和的最小值。如果有N个计数器,那么就有N个满足条件的序列,有可能前面操作最少的序列并不是后面操作最少的序列,即前面位置的总操作次数可能并不是当前最优解的一部分。可以参考上面图片的示例3。

我的思路就是直接暴力破解,循环遍历输入的数组,在第i次遍历中,分别将(0 ~ i-1)位置上的元素作为基点k,左侧的序列依次减1,右侧的依次加1。在第k个序列中求解(0 ~ i-1)位置上操作次数的累加和。在第j个位置上的操作次数为 nums[k]-(k-j)。

代码

public static int[] numsGame2(int[] nums) {
    int[] res = new int[nums.length];
    // 累加k序列的操作总和
    int[] acc = new int[nums.length];
    for (int i = 1; i < nums.length; i++) {
        for (int k = 0; k <= i; k++) {
            int temp = acc[k];
            // 增加一些判断跳过一些计算
            if(temp >= res[i] && res[i] != 0){
                continue;
            }
            // 这里看似套了3个循环,但其实只有在k序列第一次出现时才会循环i次,那么前面的i-1次只需从acc中累计即可,实际时间复杂度是O(n^2)
            for (int j = temp == 0 ? 0 : i; j <= i; j++) {
                temp += Math.abs(nums[j] - (nums[k] - (k - j)));
            }
            acc[k] = temp;
            if (k == 0) {
                res[i] = temp;
            } else {
                res[i] = Math.min(res[i], temp);
            }
        }
        res[i] = res[i] % 1000000007;
    }
    return res;
}
acc a b c d
acc[0] 0 |b-a-1| |c-a-2| |d-a-3\
acc[1] |a-b+1| 0 |c-b-1| |d-b-2\
acc[2] |a-c+2| |b-c+1| 0 |d-c-1\
acc[3] |a-d+3| |b-d+2| |c-d+1| 0

如上表所示,如果序列是[a,b,c,d],基点k等于c的时候,前2次从acc累加,最后一次需要重新累加。时间复杂度为O(n^2)。这和我们常见的外层循环N次,内层循环N次不同。循环的计算次数序列为1、3、5、7...,根据等差数列求和公式:Sn = n × a1 + (n × (n-1)/2) × d,当 d=2a1=1 时,Sn = n^2

性能

由于题目给出的数组最大长度是10^5,暴力求解是不可行的。也尝试了增加条件判断跳过一些计算但还是无法降低问题的规模。于是就开始怀疑是否前面的计算结果是否与后面的计算有关联,可以简化后面的计算?更优的算法复杂度应为O(nlogn)O(logn)O(n)。我思考了很久,应该是存在知识盲区了。我去看了答案,涉及到求中位数。其实一开始有想过中位数,方差均值这些,但是没想到和这个最优解有什么关系。今天没时间了,抽空再看看吧。