782.变为棋盘

目标

一个 n x n 的二维网络 board 仅由 0 和 1 组成 。每次移动,你能交换任意两列或是两行的位置。

返回 将这个矩阵变为 “棋盘” 所需的最小移动次数 。如果不存在可行的变换,输出 -1。

“棋盘” 是指任意一格的上下左右四个方向的值均与本身不同的矩阵。

示例 1:

输入: board = [[0,1,1,0],[0,1,1,0],[1,0,0,1],[1,0,0,1]]
输出: 2
解释:一种可行的变换方式如下,从左到右:
第一次移动交换了第一列和第二列。
第二次移动交换了第二行和第三行。

示例 2:

输入: board = [[0, 1], [1, 0]]
输出: 0
解释: 注意左上角的格值为0时也是合法的棋盘.

示例 3:

输入: board = [[1, 0], [1, 0]]
输出: -1
解释: 任意的变换都不能使这个输入变为合法的棋盘。

说明:

  • n == board.length
  • n == board[i].length
  • 2 <= n <= 30
  • board[i][j] 将只包含 0或 1

思路

有一个 n x n 的网格,仅由 01 组成。每次移动可以交换网格的任意两行或两列。求将网格变为棋盘的最小移动次数,如果无法变为棋盘返回 -1。所谓棋盘,指的是任意格子的值与它周围四个格子的值不同。

首先需要分析出可以转变为棋盘的条件,棋盘只有两种类型的行,它们互反,且这两种行的个数相差至多为 1。对于每种类型的行,0 的个数和 1 的个数相差至多也是 1

然后需要求出当前行与列转化为 010101…… 或者 101010…… 所需的移动次数。求出位置不同的个数 diff,如果 n 为偶数,最小交换次数为 Math.min(diff, n - diff),如果 n 为奇数,最小交换次数为 diff/2

代码


/**
 * @date 2024-12-09 8:46
 */
public class MovesToChessboard782 {

    public int movesToChessboard(int[][] board) {
        int n = board.length;
        int[] firstRow = board[0];
        int[] firstCol = new int[n];
        int[] firstRowCnt = new int[2];
        int[] firstColCnt = new int[2];
        for (int i = 0; i < n; i++) {
            firstRowCnt[board[0][i]]++;
            firstColCnt[board[i][0]]++;
            firstCol[i] = board[i][0];
        }

        // 第一行或者第一列0与1的个数之差超过1就无法转化为棋盘。因为最终棋盘的行与列都是 101010…… 或 010101…… 的形式
        if (Math.abs(firstRowCnt[0] - firstRowCnt[1]) > 1 || Math.abs(firstColCnt[0] - firstColCnt[1]) > 1) {
            return -1;
        }

        // 判断行是否完全相同或者完全相反,这里使用异或运算来判断,如果equal == 0 说明完全相同,opposite == 0 说明完全相反,如果都不等于0说明无法转为棋盘
        for (int i = 1; i < n; i++) {
            int equal = 0;
            int opposite = 0;
            for (int j = 0; j < board[i].length; j++) {
                int tmp = board[i][j] ^ board[0][j];
                equal += tmp;
                opposite += 1 ^ tmp;
            }
            if (equal != 0 && opposite != 0) {
                return -1;
            }

        }

        // 经过前面的判断,第一行与第一列的第一个格子的值一定是相同的
        return minMove(firstRow, firstRowCnt) + minMove(firstCol, firstColCnt);
    }

    public int minMove(int[] arr, int[] cnt) {
        int n = arr.length;
        int start = cnt[1] > cnt[0] ? 1 : 0;
        int diff = 0;
        // 计算与棋盘的差异,我们移动一次可以减少两个 diff,因此最终结果需要除以2
        for (int i = 0; i < n; i++) {
            // i % 2 ^ start 的作用是确定 1010…… 还是 0101……,如果不考虑start 那么序列是 0101,如果需要以1开头,就需要 异或 1
            diff += i % 2 ^ start ^ arr[i];
        }
        // 这里考虑的是 n 的奇偶性,如果n为奇数,那么必定 abs(cnt[1] - cnt[0]) == 1,多的那一个一定要放在第一个位置,移动的方案只有一种
        // 如果为偶数,最终可以是 0101…… 或者 1010……,取移动次数最小的
        return n % 2 != 0 ? diff / 2 : Math.min(diff, n - diff) / 2;
    }

}

性能

3251.单调数组对的数目II

目标

给你一个长度为 n 的 正 整数数组 nums 。

如果两个 非负 整数数组 (arr1, arr2) 满足以下条件,我们称它们是 单调 数组对:

  • 两个数组的长度都是 n 。
  • arr1 是单调 非递减 的,换句话说 arr1[0] <= arr1[1] <= ... <= arr1[n - 1] 。
  • arr2 是单调 非递增 的,换句话说 arr2[0] >= arr2[1] >= ... >= arr2[n - 1] 。
  • 对于所有的 0 <= i <= n - 1 都有 arr1[i] + arr2[i] == nums[i] 。

请你返回所有 单调 数组对的数目。

由于答案可能很大,请你将它对 109 + 7 取余 后返回。

示例 1:

输入:nums = [2,3,2]
输出:4
解释:
单调数组对包括:
([0, 1, 1], [2, 2, 1])
([0, 1, 2], [2, 2, 0])
([0, 2, 2], [2, 1, 0])
([1, 2, 2], [1, 1, 0])

示例 2:

输入:nums = [5,5,5,5]
输出:126

说明:

  • 1 <= n == nums.length <= 2000
  • 1 <= nums[i] <= 1000

思路

有一个长度为 n 正整数数组 nums,可以将其拆成两个数组 arr1 arr2,使之满足 arr1[i] + arr2[i] == nums[i]。问 有多少种拆分方法使得 arr1 非递减 且 arr2 非递增。

与昨天的 3250.单调数组对的数目I 相比,nums[i] 的最大值从 50 变成了 1000。时间复杂度大概为 O(n*m^2),mnums[i] 的最大值,如果还沿用昨天的解法就会超时。

先将昨天的题目改写为动态规划,定义 dp[i][j] 表示最后一个元素为 j,长度为 i + 1 的满足条件的 arr1 个数。由于 arr1 是非递减的,如果最后一个元素为 arr1[i] = j 那么倒数第二个元素arr1[i - 1] <= j。同时我们还要考虑到 arr2 非递增,即 arr2[i - 1] >= arr2[i]nums[i - 1] - arr1[i - 1] >= nums[i] - arr1[i]arr1[i - 1] <= nums[i - 1] - nums[i] + arr1[i]。综上,arr1[i - 1] <= Math.min(j, nums[i - 1] - nums[i] + j)

经过上面的分析,dp[i][j] = Σdp[i - 1][k],其中 k ∈ [0, Math.min(j, nums[i - 1] - nums[i] + j)]。这样写会超时,针对每个 j,我们会进行多次重复的计算。

d = nums[i - 1] - nums[i],当 d >= 0 时,上界为 j,否则上界为 j + d

考虑 nums[i - 1] < nums[i],即 d < 0

  • arr1[i - 1] = j 时,令arr2[i - 1] = nums[i - 1] - j = a
  • arr1[i] = j 时,arr2[i] = nums[i] - arr1[i] = nums[i - 1] - d - j = a - dd < 0

也就是说,当 arr1[i] 的取值与上一层一样时,arr2[i] 比上一层的值大了 |d|。为了使第 iarr2 非递增,那么 arr1 的取值只能从 |d| 开始。

它们之间的约束关系是这样的,当 nums[i] 变大,arr1i 层取 j 时,arr2 的第 i 层比上一层增大了 |d|,这时我们必须舍弃 [0, |d|) 的取值,因为它必定大于上一层 arr2 的最大值。然后考虑第 i 层的 arr1[|d|, nums[i]] 的情况,由于第 i 层的 arr2 相比第 i - 1 层增大了 |d|,因此需要减小第 i - 1 层的 arr1,使第 i - 1 层的 arr2 增大。所以第 i 层的 j 对应第 i - 1 层的 j - |d|

dp[i][j] 的取值类似前缀和,只不过有约束条件,并不是所有值都合法。考虑简单的情况 nums[0] == nums[1] && i == 1,

  • 当 j == 0 时,dp[1][0] = dp[0][0] = 1
  • 当 j == 1 时,上一层(i == 0) arr1 可以取 0、1,dp[1][1] = dp[1][0] + dp[0][1] = 2
  • 当 j == 2 时,上一层(i == 0) arr1 可以取 0、1、2,dp[1][2] = dp[1][1] + dp[0][2] = 3

因此我们有 dp[i][j] = (dp[i][j - 1] + dp[i - 1][j - d])

代码


/**
 * @date 2024-11-29 9:39
 */
public class CountOfPairs3251 {

    public static int MOD = 1000000007;

    public int countOfPairs(int[] nums) {
        int res = 0;
        int n = nums.length;
        int[][] dp = new int[n][1001];
        for (int i = 0; i <= nums[0]; i++) {
            dp[0][i] = 1;
        }
        for (int i = 1; i < n; i++) {
            int d = Math.max(nums[i] - nums[i - 1], 0);
            for (int j = d; j <= nums[i]; j++) {
                if (j == 0) {
                    dp[i][j] = dp[i - 1][0] % MOD;
                } else {
                    dp[i][j] = (dp[i][j - 1] + dp[i - 1][j - d]) % MOD;
                }
            }
        }
        for (int i : dp[n - 1]) {
            res = (res + i) % MOD;
        }
        return res;
    }

}

性能

3233.统计不是特殊数字的数字数量

目标

给你两个 正整数 l 和 r。对于任何数字 x,x 的所有正因数(除了 x 本身)被称为 x 的 真因数。

如果一个数字恰好仅有两个 真因数,则称该数字为 特殊数字。例如:

  • 数字 4 是 特殊数字,因为它的真因数为 1 和 2。
  • 数字 6 不是 特殊数字,因为它的真因数为 1、2 和 3。

返回区间 [l, r] 内 不是 特殊数字 的数字数量。

示例 1:

输入: l = 5, r = 7
输出: 3
解释:
区间 [5, 7] 内不存在特殊数字。

示例 2:

输入: l = 4, r = 16
输出: 11
解释:
区间 [4, 16] 内的特殊数字为 4 和 9。

说明:

  • 1 <= l <= r <= 10^9

提示:

  • A special number must be a square of a prime number.
  • We need to find all primes in the range [sqrt(l), sqrt(r)].
  • Use sieve to find primes till sqrt(10^9).

思路

返回给定区间 [l, r] 内不是特殊数字的数字个数,所谓特殊数字指除了它本身恰好有两个正因数的数字。

一看到数据范围是 10^9 就不可能使用使用暴力解法去判断每一个数字是不是 lr 的因数,要么使用二分要么预处理。特殊数字是确定的,它除了自身以外只有两个因数,1 一定是一个,即除了 1 和它本身只有一个因数。看了提示说特殊数字是质数的平方,我们需要找到所有在 [√l, √r] 范围内的质数,可以预处理 √10^9 内的质数。二分查找 [l, r] 范围内质数的个数,然后减掉即可。

埃氏筛的基本思想是创建一个 boolean[] 标记查询范围内的数是否为质数,初始时均标记为 true。从 2 开始遍历(01 后面直接过滤掉了),直到 i < √endi * i < end。在循环内部,如果当前值是质数,则将 i * i,i * (i + 1),i * (i + 2),…… 标记为非质数。比如在 2 的循环内,所有大于 2 的偶数都被标为非质数,以此类推,像筛子一样将质数筛选出来。

// todo 最后也可以使用前缀和计算个数

代码


/**
 * @date 2024-11-22 9:07
 */
public class NonSpecialCount3233 {

    public static int[] primes;

    static {
        List<Integer> primeList = new ArrayList<>();
        int end = (int) Math.ceil(Math.sqrt(1000000001));
        boolean[] isPrime = new boolean[end + 1];
        Arrays.fill(isPrime, true);
        for (int i = 2; i * i <= end; i++) {
            if (isPrime[i]) {
                for (int j = i * i; j <= end; j += i) {
                    isPrime[j] = false;
                }
            }
        }
        // 前面没有将 isPrime[0] isPrime[1] 置为false,这里从2开始
        for (int i = 2; i <= end; i++) {
            if (isPrime[i]) {
                primeList.add(i);
            }
        }
        int cnt = primeList.size();
        primes = new int[cnt];
        for (int i = 0; i < cnt; i++) {
            primes[i] = primeList.get(i);
        }
    }

    public int nonSpecialCount_v1(int l, int r) {
        int ceilLeft = (int) Math.ceil(Math.sqrt(l));
        int right = (int) Math.sqrt(r);
        if (ceilLeft > right) {
            return r - l + 1;
        }
        int a = Arrays.binarySearch(primes, ceilLeft);
        if (a < 0) {
            // 获取插入点,说明原来该位置的值大于ceilLeft
            a = -a - 1;
        }
        int b = Arrays.binarySearch(primes, right);
        if (b < 0) {
            // 这里多减了1,因为插入点是第一个大于right的位置,减1则小于 right
            b = -b - 2;
        }
//        return r - l + 1 - (b - a + 1);
        return r - l - b + a;
    }

}

性能

3222.求出硬币游戏的赢家

目标

给你两个 正 整数 x 和 y ,分别表示价值为 75 和 10 的硬币的数目。

Alice 和 Bob 正在玩一个游戏。每一轮中,Alice 先进行操作,Bob 后操作。每次操作中,玩家需要拿出价值 总和 为 115 的硬币。如果一名玩家无法执行此操作,那么这名玩家 输掉 游戏。

两名玩家都采取 最优 策略,请你返回游戏的赢家。

示例 1:

输入:x = 2, y = 7
输出:"Alice"
解释:
游戏一次操作后结束:
Alice 拿走 1 枚价值为 75 的硬币和 4 枚价值为 10 的硬币。

示例 2:

输入:x = 4, y = 11
输出:"Bob"
解释:
游戏 2 次操作后结束:
Alice 拿走 1 枚价值为 75 的硬币和 4 枚价值为 10 的硬币。
Bob 拿走 1 枚价值为 75 的硬币和 4 枚价值为 10 的硬币。

说明:

  • 1 <= x, y <= 100

思路

有价值 75 的硬币 x 个,价值 10 的硬币 y 个。AliceBob 轮流进行操作,每一次操作需要从中取出价值 115 的硬币,如果无法执行此操作则玩家输掉游戏。如果 Alice 先进行操作,最后的赢家是谁?

每次操作需要取 1x4y,可以直接模拟,直到 x < 1y < 4

这道题也有数学解法,输赢实际上取决于可以进行的操作次数。总共可以进行的操作次数是 Math.min(x,y/4),如果为偶数 Bob 胜,奇数 Alice 胜。

代码


/**
 * @date 2024-11-05 9:14
 */
public class LosingPlayer3222 {

    public String losingPlayer_v1(int x, int y) {
        return Math.min(x, y / 4) % 2 == 0 ? "Bob" : "Alice";
    }

    public String losingPlayer(int x, int y) {
        int cnt = 0;
        while (x >= 1 && y >= 4) {
            x--;
            y -= 4;
            cnt++;
        }
        return cnt % 2 == 0 ? "Bob" : "Alice";
    }

}

性能

3200.三角形的最大高度

目标

给你两个整数 red 和 blue,分别表示红色球和蓝色球的数量。你需要使用这些球来组成一个三角形,满足第 1 行有 1 个球,第 2 行有 2 个球,第 3 行有 3 个球,依此类推。

每一行的球必须是 相同 颜色,且相邻行的颜色必须 不同。

返回可以实现的三角形的 最大 高度。

示例 1:

输入: red = 2, blue = 4
输出: 3
解释:
上图显示了唯一可能的排列方式。

示例 2:

输入: red = 2, blue = 1
输出: 2
解释:
上图显示了唯一可能的排列方式。

示例 3:

输入: red = 1, blue = 1
输出: 1

示例 4:

输入: red = 10, blue = 1
输出: 2
解释:
上图显示了唯一可能的排列方式。

说明:

  • 1 <= red, blue <= 100

思路

有红球 red 个,蓝球 blue 个,使用这两种球组成三角形,要求每一行只能由同一种颜色的球组成,且相邻行球的颜色不同,问三角形的最大高度。

三角形第 i 行球的个数为 i,奇数行的总个数为 1 + 3 + 5 + …… 偶数行的总个数为 2 + 4 + 6 + ……,根据等差数列求和公式,奇数行所需球的总个数为 oddRowCnt^2,偶数行所需球的总个数为 evenRowCnt^2 + evenRowCnt

假设 red 放在奇数行,代入可得 redOddRowCnt = sqrt(red),如果放在偶数行,则 redEvenRowCnt = (sqrt(1 + 4 * red) - 1) / 2。同理可求出 blueOddRowCnt blueEvenRowCnt

接下来分两种情况讨论:

  1. 红色球放第一行:如果 |redOddRowCnt - blueEvenRowCnt| > 1,取 Math.min(redOddRowCnt, blueEvenRowCnt) * 2 + 1,否则取 Math.min(redOddRowCnt, blueEvenRowCnt) * 2
  2. 蓝色球放第一行:如果 |blueOddRowCnt - redEvenRowCnt| > 1,取 Math.min(blueOddRowCnt, redEvenRowCnt) * 2 + 1,否则取 Math.min(blueOddRowCnt, redEvenRowCnt) * 2

取上面两种情况的最大值即可。

代码


/**
 * @date 2024-10-15 9:25
 */
public class MaxHeightOfTriangle3200 {

    public int maxHeightOfTriangle(int red, int blue) {
        int redOddRowCnt = (int) Math.sqrt(red);
        int redEvenRowCnt = (int) ((Math.sqrt(1 + 4 * red) - 1) / 2);
        int blueOddRowCnt = (int) Math.sqrt(blue);
        int blueEvenRowCnt = (int) ((Math.sqrt(1 + 4 * blue) - 1) / 2);
        int r, b;
        if (redOddRowCnt - blueEvenRowCnt >= 1) {
            r = 2 * blueEvenRowCnt + 1;
        } else {
            r = 2 * redOddRowCnt;
        }
        if (blueOddRowCnt - redEvenRowCnt >= 1) {
            b = 2 * redEvenRowCnt + 1;
        } else {
            b = 2 * blueOddRowCnt;
        }
        return Math.max(r, b);
    }

}

性能

1884.鸡蛋掉落-两枚鸡蛋

目标

给你 2 枚相同 的鸡蛋,和一栋从第 1 层到第 n 层共有 n 层楼的建筑。

已知存在楼层 f ,满足 0 <= f <= n ,任何从 高于 f 的楼层落下的鸡蛋都 会碎 ,从 f 楼层或比它低 的楼层落下的鸡蛋都 不会碎 。

每次操作,你可以取一枚 没有碎 的鸡蛋并把它从任一楼层 x 扔下(满足 1 <= x <= n)。如果鸡蛋碎了,你就不能再次使用它。如果某枚鸡蛋扔下后没有摔碎,则可以在之后的操作中 重复使用 这枚鸡蛋。

请你计算并返回要确定 f 确切的值 的 最小操作次数 是多少?

示例 1:

输入:n = 2
输出:2
解释:我们可以将第一枚鸡蛋从 1 楼扔下,然后将第二枚从 2 楼扔下。
如果第一枚鸡蛋碎了,可知 f = 0;
如果第二枚鸡蛋碎了,但第一枚没碎,可知 f = 1;
否则,当两个鸡蛋都没碎时,可知 f = 2。

示例 2:

输入:n = 100
输出:14
解释:
一种最优的策略是:
- 将第一枚鸡蛋从 9 楼扔下。如果碎了,那么 f 在 0 和 8 之间。将第二枚从 1 楼扔下,然后每扔一次上一层楼,在 8 次内找到 f 。总操作次数 = 1 + 8 = 9 。
- 如果第一枚鸡蛋没有碎,那么再把第一枚鸡蛋从 22 层扔下。如果碎了,那么 f 在 9 和 21 之间。将第二枚鸡蛋从 10 楼扔下,然后每扔一次上一层楼,在 12 次内找到 f 。总操作次数 = 2 + 12 = 14 。
- 如果第一枚鸡蛋没有再次碎掉,则按照类似的方法从 34, 45, 55, 64, 72, 79, 85, 90, 94, 97, 99 和 100 楼分别扔下第一枚鸡蛋。
不管结果如何,最多需要扔 14 次来确定 f 。

说明:

  • 1 <= n <= 1000

思路

有一个 1 ~ n 层楼的建筑,存在一个楼层 f,任何大于 f 层落下的鸡蛋都会摔碎。现在有两个鸡蛋,每次操作可以从任意楼层向下扔鸡蛋,如果鸡蛋碎了则无法再使用,求确定 f 值的最小操作次数。

为了确保能够找到 f,如果第一个尝试的鸡蛋碎了,那么另一个鸡蛋只能从已知的安全楼层一层一层向上尝试。

观察示例2,可以从 n 开始 减 1 2 3 …… i 直到小于等于零,返回 i - 1即可。

看了题解,这样做可行的逻辑是这样的:

假设已知最小操作次数 k,我们扔第一枚鸡蛋选第几层?显然,应该选第 k 层,因为如果第一枚鸡蛋碎了,只需要从 1 ~ k - 1 枚举即可。

如果第一枚鸡蛋没碎,那么下一次选第几层?现在还剩下 k - 1 次尝试,所以应该选 k + 1 + (k - 2) = k + (k - 1) 层,因为如果在该层扔鸡蛋碎了,只需从 k + 1 ~ k + k - 2 枚举即可,共 k - 2 次,再加上前面尝试的 2 次,总次数为 k

以此类推,我们可以确定总层数 n = k + (k - 1) + (k - 2) + …… + 2 + 1 = k * (k + 1)/2,解方程得 k = (sqrt(1+8*n) - 1)/2,结果需要向上取整。

代码


/**
 * @date 2024-10-13 19:30
 */
public class TwoEggDrop1884 {

    public int twoEggDrop_v1(int n) {
        return (int) Math.ceil((Math.sqrt(1 + 8 * n) - 1) / 2);
    }

    public int twoEggDrop(int n) {
        int i = 1;
        while (n > 0){
            n -= i++;
        }
        return i - 1;
    }
}

性能

3102.最小化曼哈顿距离

目标

给你一个下标从 0 开始的数组 points ,它表示二维平面上一些点的整数坐标,其中 points[i] = [xi, yi] 。

两点之间的距离定义为它们的 曼哈顿距离。两个单元格 (xi, yi) 和 (xj, yj) 之间的曼哈顿距离为 |xi - xj| + |yi - yj|。

请你恰好移除一个点,返回移除后任意两点之间的 最大 距离可能的 最小 值。

示例 1:

输入:points = [[3,10],[5,15],[10,2],[4,4]]
输出:12
解释:移除每个点后的最大距离如下所示:
- 移除第 0 个点后,最大距离在点 (5, 15) 和 (10, 2) 之间,为 |5 - 10| + |15 - 2| = 18 。
- 移除第 1 个点后,最大距离在点 (3, 10) 和 (10, 2) 之间,为 |3 - 10| + |10 - 2| = 15 。
- 移除第 2 个点后,最大距离在点 (5, 15) 和 (4, 4) 之间,为 |5 - 4| + |15 - 4| = 12 。
- 移除第 3 个点后,最大距离在点 (5, 15) 和 (10, 2) 之间的,为 |5 - 10| + |15 - 2| = 18 。
在恰好移除一个点后,任意两点之间的最大距离可能的最小值是 12 。

示例 2:

输入:points = [[1,1],[1,1],[1,1]]
输出:0
解释:移除任一点后,任意两点之间的最大距离都是 0 。

说明:

  • 3 <= points.length <= 10^5
  • points[i].length == 2
  • 1 <= points[i][0], points[i][1] <= 10^8

提示:

  • Notice that the Manhattan distance between two points [xi, yi] and [xj, yj] is max({xi - xj + yi - yj, xi - xj - yi + yj, - xi + xj + yi - yj, - xi + xj - yi + yj}).
  • If you replace points as [xi - yi, xi + yi] then the Manhattan distance is max(max(xi) - min(xi), max(yi) - min(yi)) over all i.
  • After those observations, the problem just becomes a simulation. Create multiset of points [xi - yi, xi + yi], you can iterate on a point you might remove and get the maximum Manhattan distance over all other points.

思路

有一些二维平面上的整数坐标点,移除其中一个点,求任意两点之间曼哈顿距离最大值的最小值。

n个点之间的曼哈顿距离有C(n,2) = n * (n - 1) / 2 个,移除其中一个点,最大值可能发生变化,需要找到其中最小的。

问题的关键是如何计算n个点之间曼哈顿距离的最大值,如果暴力计算,找出最大值的时间复杂度是 O(n^2),还要依次移除一个点找到其中的最小值,综合起来时间复杂度是 O(n^3)。点的个数最多 10^5,暴力求解会超时。

我么需要考虑曼哈顿距离有什么特性,找出最大值需要每一个都遍历吗?移除一个点对最大值有什么影响,需要重新计算吗?

看了题目的提示是使用变量替换将问题转化:两点(xi, yi) 与 (xj, yj) 之间的曼哈顿距离为|xi - xj| + |yi - yj|,去掉绝对值即 max({xi - xj + yi - yj, xi - xj - yi + yj, - xi + xj + yi - yj, - xi + xj - yi + yj}),令 ui = xi - yi, vi = xi + yi,则点 (xi, yi) 与 (xj, yj) 之间的曼哈顿距离转换为 max({vi - vj, ui - uj, uj - ui, vj - vi}) = max({|vi - vj|, |ui- uj|})对于所有的 i,j,曼哈顿距离的最大值为 max({max(vi) - min(vi), max(ui) - min(ui)})。这样找出曼哈顿距离最大值的时间复杂度减少为 O(n)

于是,我们只需要将坐标点转化为 (ui, vi),依次去除某个坐标点,再找出 u、v 的最大值与最小值即可。试了一下,O(n^2)的时间复杂度仍会超时。

我们可以记录一下vi、ui取最大最小值时的坐标,然后排除这几个坐标点后找出其中的最小值,最坏的情况即坐标没有重复,时间复杂度为O(4n)。

我也尝试了记录次大与次小值,试图将复杂度降为 O(n),但需要考虑去除掉相应坐标点后,另一个坐标是否会受到影响。例如,max({max(vi) - min(vi), max(ui) - min(ui)}) 假如我们删去了vi最大的坐标,取次大的vi,但是后面ui的最大与最小是否受到影响?

因此我们不仅需要一次遍历获取到最大与次大,还要再次遍历比较当前值是否是最大或最小,如果是则取次大或次小。需要注意,这里的最大与次大以及最小与次小的值可以相同。更进一步,如果我们同时保存了x、y 取最大、最小的四个坐标,我们只需排除这4个坐标即可,其它坐标对最大距离没有影响。

看了官网题解,转换后的距离称为切比雪夫距离。事实上,max({|vi - vj|, |ui - uj|}) 计算的是 (vi, ui) (vj, uj) 两点曼哈顿距离投影到 x 轴(|vi - vj|) 和 y 轴(|ui - uj|)的线段长度的最大值,即切比雪夫距离。注意本题中的u、v对应题解中v、u,并且使用的是x-y,而非y-x,不过对于求解没有什么影响,无非是关于坐标轴镜像了一下。

代码

/**
 * @date 2024-07-09 10:20
 */
public class MinimumDistance3102 {

    public int minimumDistance(int[][] points) {
        for (int[] point : points) {
            int u = point[0] - point[1];
            int v = point[0] + point[1];
            point[0] = u;
            point[1] = v;
        }
        int n = points.length;
        int res = Integer.MAX_VALUE;
        int maxX = Integer.MIN_VALUE;
        int minX = Integer.MAX_VALUE;
        int maxY = Integer.MIN_VALUE;
        int minY = Integer.MAX_VALUE;
        int maxXIndex = -1, minXIndex = -1, maxYIndex = -1, minYIndex = -1;
        List<Integer> exclude = new ArrayList<>();
        for (int j = 0; j < n; j++) {
            if (points[j][0] > maxX) {
                maxX = points[j][0];
                maxXIndex = j;
            }
            if (points[j][0] < minX) {
                minX = points[j][0];
                minXIndex = j;
            }
            if (points[j][1] > maxY) {
                maxY = points[j][1];
                maxYIndex = j;
            }
            if (points[j][1] < minY) {
                minY = points[j][1];
                minYIndex = j;
            }
        }
        exclude.add(maxXIndex);
        exclude.add(minXIndex);
        exclude.add(maxYIndex);
        exclude.add(minYIndex);
        for (Integer i : exclude) {
            maxX = Integer.MIN_VALUE;
            minX = Integer.MAX_VALUE;
            maxY = Integer.MIN_VALUE;
            minY = Integer.MAX_VALUE;
            for (int j = 0; j < n; j++) {
                if (i != j) {
                    maxX = Math.max(maxX, points[j][0]);
                    minX = Math.min(minX, points[j][0]);
                    maxY = Math.max(maxY, points[j][1]);
                    minY = Math.min(minY, points[j][1]);
                }
            }
            res = Math.min(res, Math.max(maxX - minX, maxY - minY));
        }
        return res;
    }
}

性能

2938.区分黑球与白球

目标

桌子上有 n 个球,每个球的颜色不是黑色,就是白色。

给你一个长度为 n 、下标从 0 开始的二进制字符串 s,其中 1 和 0 分别代表黑色和白色的球。

在每一步中,你可以选择两个相邻的球并交换它们。

返回「将所有黑色球都移到右侧,所有白色球都移到左侧所需的 最小步数」。

示例 1:

输入:s = "101"
输出:1
解释:我们可以按以下方式将所有黑色球移到右侧:
- 交换 s[0] 和 s[1],s = "011"。
最开始,1 没有都在右侧,需要至少 1 步将其移到右侧。

示例 2:

输入:s = "100"
输出:2
解释:我们可以按以下方式将所有黑色球移到右侧:
- 交换 s[0] 和 s[1],s = "010"。
- 交换 s[1] 和 s[2],s = "001"。
可以证明所需的最小步数为 2 。

示例 3:

输入:s = "0111"
输出:0
解释:所有黑色球都已经在右侧。

说明:

  • 1 <= n == s.length <= 10^5
  • s[i] 不是 '0',就是 '1'

思路

有一个数组,其元素值不是0就是1,现在需要将所有的1都移到右边,每一步可以选择相邻的两个元素交换其位置,问移动的最小步数。

从左向右遍历数组元素,如果值为1就累加cnt,如果值为0就将移动步数加上 cnt。简单来说就是遇到1就合并,记录其个数,遇到0就整体移动 res += cnt。每次移动都贪心地将0移至其最终位置上。

有网友提到可以使用归并排序记录逆序对。

还有网友是基于下标和计算的。因为最终0都在右边,其下标和可以通过等差数列求和得到。我们只需在遍历过程中记录0的个数,并累加0的下标,然后与最终状态的下标和相减即可。

代码

package medium;

/**
 * @date 2024-06-06 0:03
 */
public class MinimumSteps2938 {

    /**
     * 将黑球视为一个整体,遇到黑球则合并到一起增加其权重,这样就可以视为将一个带权黑球从左移到右,每一步都是必要的。
     * 这其实也算是在移动的过程中统计逆序对的个数
     */
    public long minimumSteps(String s) {
        long res = 0;
        int n = s.length();
        int i = 0;
        long cnt = 0;
        while (i < n) {
            if (s.charAt(i) == '0') {
                // 遇到0就移动  累加移动步数,可以使用双指针优化
                res += cnt;
            } else {
                // 遇到1则合并
                cnt++;
            }
            i++;
        }
        return res;
    }

    /**
     * 优化
     * 使用双指针可以减少累加次数
     */
    public long minimumSteps_v1(String s) {
        long res = 0;
        int n = s.length();
        int i = 0;
        // left指向1的位置,如果第一值是0,那么left与i一起右移
        // 如果第一个值是1,仅移动i,当遇到0时,左侧1的个数就是i-left
        // 本来从下标left到i元素个数为 i - left + 1,由于i指向的不是1,所以不用加1
        int left = 0;
        while (i < n) {
            if (s.charAt(i) == '0') {
                res += i - left;
                left++;
            }
            i++;
        }
        return res;
    }
}

性能

1103.分糖果II

目标

排排坐,分糖果。

我们买了一些糖果 candies,打算把它们分给排好队的 n = num_people 个小朋友。

给第一个小朋友 1 颗糖果,第二个小朋友 2 颗,依此类推,直到给最后一个小朋友 n 颗糖果。

然后,我们再回到队伍的起点,给第一个小朋友 n + 1 颗糖果,第二个小朋友 n + 2 颗,依此类推,直到给最后一个小朋友 2 * n 颗糖果。

重复上述过程(每次都比上一次多给出一颗糖果,当到达队伍终点后再次从队伍起点开始),直到我们分完所有的糖果。注意,就算我们手中的剩下糖果数不够(不比前一次发出的糖果多),这些糖果也会全部发给当前的小朋友。

返回一个长度为 num_people、元素之和为 candies 的数组,以表示糖果的最终分发情况(即 ans[i] 表示第 i 个小朋友分到的糖果数)。

示例 1:

输入:candies = 7, num_people = 4
输出:[1,2,3,1]
解释:
第一次,ans[0] += 1,数组变为 [1,0,0,0]。
第二次,ans[1] += 2,数组变为 [1,2,0,0]。
第三次,ans[2] += 3,数组变为 [1,2,3,0]。
第四次,ans[3] += 1(因为此时只剩下 1 颗糖果),最终数组变为 [1,2,3,1]。

示例 2:

输入:candies = 10, num_people = 3
输出:[5,2,3]
解释:
第一次,ans[0] += 1,数组变为 [1,0,0]。
第二次,ans[1] += 2,数组变为 [1,2,0]。
第三次,ans[2] += 3,数组变为 [1,2,3]。
第四次,ans[0] += 4,最终数组变为 [5,2,3]。

说明:

  • 1 <= candies <= 10^9
  • 1 <= num_people <= 1000

思路

现在有一些糖果要分给一排小朋友,第一个小朋友分1个,第二个分2个,依次类推,即分配的糖果总比上一次分的多一个,如果不够就剩多少分多少。如果一轮下来没有分完,就接着从第一个小朋友开始依次分配。问最终每个小朋友能够分得的糖果数量。

我们可以很容易地模拟这个分配过程,记录当前分配的糖果数量,并计算剩余糖果数量直到0。

网友最快的题解使用了数学公式,使时间复杂度从O(num_people+sqrt(candies)) 降为O(num_people)。

代码

/**
 * @date 2024-06-03 0:06
 */
public class DistributeCandies1103 {

    public int[] distributeCandies_v1(int candies, int num_people) {
        int[] res = new int[num_people];
        // 足量发放人次,解不等式:(n+1)n/2 <= candies
        int n = (int) ((Math.sqrt(candies * 8F + 1) - 1) / 2);
        // 足量发放的糖果数量,num_people <= 1000,其实不会溢出
        int distributed = (int) ((1L + n) * n / 2);
        // 剩余糖数量
        int remainder = candies - distributed;
        // 所有小朋友都得到糖果的轮次
        int allGetLoops = n / num_people;
        // 最后一轮获得糖果的人数
        int lastLoopNum = n % num_people;
        for (int i = 0; i < num_people; i++) {
            // 收到糖果的次数 = 所有小朋友都得到糖果的轮次 + 最后一轮是否发放
            int times = (lastLoopNum > i ? 1 : 0) + allGetLoops;
            // 等差数列求和,公差为 num_people,a1 = i + 1,n = times;
            res[i] = ((times - 1) * num_people + i * 2 + 2) * times / 2;
        }
        // 如果是最后一个,将剩余的全部分配
        res[lastLoopNum] += remainder;
        return res;
    }

    public int[] distributeCandies(int candies, int num_people) {
        int[] res = new int[num_people];
        int cnt = 1;
        while (candies > 0) {
            for (int i = 0; i < num_people && candies > 0; i++) {
                int num = Math.min(cnt++, candies);
                candies -= num;
                res[i] += num;
            }
        }
        return res;
    }

}

性能

2928.给小朋友们分糖果I

目标

给你两个正整数 n 和 limit 。

请你将 n 颗糖果分给 3 位小朋友,确保没有任何小朋友得到超过 limit 颗糖果,请你返回满足此条件下的 总方案数 。

示例 1:

输入:n = 5, limit = 2
输出:3
解释:总共有 3 种方法分配 5 颗糖果,且每位小朋友的糖果数不超过 2 :(1, 2, 2) ,(2, 1, 2) 和 (2, 2, 1) 。

示例 2:

输入:n = 3, limit = 3
输出:10
解释:总共有 10 种方法分配 3 颗糖果,且每位小朋友的糖果数不超过 3 :(0, 0, 3) ,(0, 1, 2) ,(0, 2, 1) ,(0, 3, 0) ,(1, 0, 2) ,(1, 1, 1) ,(1, 2, 0) ,(2, 0, 1) ,(2, 1, 0) 和 (3, 0, 0) 。

说明:

  • 1 <= n <= 50
  • 1 <= limit <= 50

思路

n 颗糖果分给 3 位小朋友,每个小朋友分到的糖果数量不超过 limit,求分配的方案数。注意,糖果必须分完,比如示例 1,不存在分得糖果数量为 0 的情况。

  • 第一个小朋友分到的糖果数为 a ∈ [0, Math.min(n, limit)]
  • 第二个小朋友分到的糖果数为 b ∈ [Math.max(0, n - a - limit), Math.min(n - a, limit)]
  • 第三个小朋友分到的糖果数为 n - a - b

代码

/**
 * @date 2024-06-01 15:59
 */
public class DistributeCandies2928 {
    public int distributeCandies(int n, int limit) {
        int res = 0;
        // 为第一个小朋友分配有0~limit种可能
        for (int i = 0; i <= limit; i++) {
            // 这时为第二个小朋友可以有n - i种可能,这里不要忘了不能超过limit
            for (int j = 0; j <= Math.min(limit, n - i); j++) {
                // 第三个小朋友,获取剩下的糖果,但不能超过limit
                if(n - i - j <= limit){
                    res++;
                }
            }
        }
        return res;
    }
}

性能