3153.所有数对中数位不同之和

目标

你有一个数组 nums ,它只包含 正 整数,所有正整数的数位长度都 相同 。

两个整数的 数位不同 指的是两个整数 相同 位置上不同数字的数目。

请你返回 nums 中 所有 整数对里,数位不同之和。

示例 1:

输入:nums = [13,23,12]
输出:4
解释:
计算过程如下:
- 13 和 23 的数位不同为 1 。
- 13 和 12 的数位不同为 1 。
- 23 和 12 的数位不同为 2 。
所以所有整数数对的数位不同之和为 1 + 1 + 2 = 4 。

示例 2:

输入:nums = [10,10,10,10]
输出:0
解释:
数组中所有整数都相同,所以所有整数数对的数位不同之和为 0 。

说明:

  • 2 <= nums.length <= 10^5
  • 1 <= nums[i] < 10^9
  • nums 中的整数都有相同的数位长度。

思路

有一个数组其元素的数位长度相同,针对数组中所有可能的数对组合(即任选两个元素),比较其数位不同的个数并累加求和。

C(n,2) = n(n-1)/2 时间复杂度为 O(n^2),再加上数位比较,暴力枚举会超时。

考虑到所有元素的数位 digitLength 相同,那么可以统计所有元素该位上数字出现次数,时间复杂度为 O(digitLength*n)。然后逐数位比较,即在 0~9 之间组合,组合数等于对应数位数字出现次数的乘积。外层数位循环最大为10(可以忽略掉出现次数为0的),内层 0~9 组合数最多(10*9/2),总循环次数最大450,没有增大数据规模。

代码


/**
 * @date 2024-08-30 9:33
 */
public class SumDigitDifferences3153 {

    public long sumDigitDifferences(int[] nums) {
        int n = nums.length;
        int first = nums[0];
        int digitsLength = 0;
        while (first > 0) {
            digitsLength++;
            first /= 10;
        }
        int[][] cnt = new int[digitsLength][10];
        for (int i = 0; i < n; i++) {
            int d = 0;
            int num = nums[i];
            while (num > 0) {
                cnt[d++][num % 10]++;
                num /= 10;
            }
        }
        long res = 0L;
        for (int[] digit : cnt) {
            for (int i = 0; i < 10; i++) {
                if (digit[i] == 0) {
                    continue;
                }
                for (int j = i + 1; j < 10; j++) {
                    res += (long) digit[i] * digit[j];
                }
            }
        }
        return res;
    }

}

性能

3144.分割字符频率相等的最少子字符串

目标

给你一个字符串 s ,你需要将它分割成一个或者更多的 平衡 子字符串。比方说,s == "ababcc" 那么 ("abab", "c", "c") ,("ab", "abc", "c") 和 ("ababcc") 都是合法分割,但是 ("a", "bab", "cc") ,("aba", "bc", "c") 和 ("ab", "abcc") 不是,不平衡的子字符串用粗体表示。

请你返回 s 最少 能分割成多少个平衡子字符串。

注意:一个 平衡 字符串指的是字符串中所有字符出现的次数都相同。

示例 1:

输入:s = "fabccddg"
输出:3
解释:
我们可以将 s 分割成 3 个子字符串:("fab, "ccdd", "g") 或者 ("fabc", "cd", "dg") 。

示例 2:

输入:s = "abababaccddb"
输出:2
解释:
我们可以将 s 分割成 2 个子字符串:("abab", "abaccddb") 。

说明:

  • 1 <= s.length <= 1000
  • s 只包含小写英文字母。

提示:

  • Let dp[i] be the minimum number of partitions for the prefix ending at index i + 1.
  • dp[i] can be calculated as the min(dp[j]) over all j such that j < i and word[j+1…i] is valid.

思路

将字符串划分成子字符串,要求子字符串中每个字符的出现次数相同,求划分后子字符串的最少个数。

想了一会没有头绪,暴力求解该如何做?枚举所有可能的划分?分情况讨论,如果划分为两部分有n-1种分发,如果是三部分有C(n-1,2)种,以此类推。这种枚举显然是不可能的。

考虑贪心算法尽可能多的合并?对应示例2,如果前面尽可能多的合并了 ababab 后面的 ac cd db 就要拆开了,并不是最小的。

如何判断给定区间上字符串是平衡的?还是要遍历计数,然后再来一次循环判断个数是否相同。

先将字符串划分为出现次数都是1的子串,然后再进行合并?如何合并?记录 dp[start][end] ?状态如何转移?

看了题解需要使用动态规划,dp[i] 表示 子串 [0 ~ i) 的最小平衡子串的个数。状态转移方程为 for (int j = i; j >= 0; j--) { if (isBlance(word[j, i])) { dp[i+1] = Math.min(dp[j] + 1, dp[i+1])} }

关键点是使用不同字符的种类乘以最大个数直接判断给定区间是否是平衡的,使用循环会超时。

代码


/**
 * @date 2024-08-28 10:29
 */
public class MinimumSubstringsInPartition3144 {

    public int minimumSubstringsInPartition_v1(String s) {
        int n = s.length();
        int[] dp = new int[n + 1];
        Arrays.fill(dp, Integer.MAX_VALUE);
        dp[0] = 0;
        int[] cnt = new int[26];
        for (int i = 0; i < n; i++) {
            Arrays.fill(cnt, 0);
            int k = 0, max = 0;
            for (int j = i; j >= 0; j--) {
                int index = s.charAt(j) - 'a';
                k += ++cnt[index] == 1 ? 1 : 0;
                max = Math.max(cnt[index], max);
                if (k * max == i - j + 1) {
                    dp[i + 1] = Math.min(dp[j] + 1, dp[i + 1]);
                }
            }
        }
        return dp[n];
    }

}

性能

690.员工的重要性

目标

你有一个保存员工信息的数据结构,它包含了员工唯一的 id ,重要度和直系下属的 id 。

给定一个员工数组 employees,其中:

  • employees[i].id 是第 i 个员工的 ID。
  • employees[i].importance 是第 i 个员工的重要度。
  • employees[i].subordinates 是第 i 名员工的直接下属的 ID 列表。

给定一个整数 id 表示一个员工的 ID,返回这个员工和他所有下属的重要度的 总和。

示例 1:

输入:employees = [[1,5,[2,3]],[2,3,[]],[3,3,[]]], id = 1
输出:11
解释:员工 1 自身的重要度是 5 ,他有两个直系下属 2 和 3 ,而且 2 和 3 的重要度均为 3 。因此员工 1 的总重要度是 5 + 3 + 3 = 11 。

示例 2:

输入:employees = [[1,2,[5]],[5,-3,[]]], id = 5
输出:-3
解释:员工 5 的重要度为 -3 并且没有直接下属。因此,员工 5 的总重要度为 -3。

说明:

  • 1 <= employees.length <= 2000
  • 1 <= employees[i].id <= 2000
  • 所有的 employees[i].id 互不相同。
  • -100 <= employees[i].importance <= 100
  • 一名员工最多有一名直接领导,并可能有多名下属。
  • employees[i].subordinates 中的 ID 都有效。

思路

有一个数据结构 Employee,属性有 id、重要性、下属id集合。现在要查找给定id员工的重要性,即自身及下属的重要性之和。

直接使用map映射id与员工对象,使用dfs或者bfs搜索并累加重要性即可。

代码


/**
 * @date 2024-08-26 14:59
 */
public class GetImportance690 {
    class Employee {
        public int id;
        public int importance;
        public List<Integer> subordinates;
    }

    public int getImportance(List<Employee> employees, int id) {
        Employee root = null;
        Map<Integer, Employee> map = new HashMap<>();
        for (Employee employee : employees) {
            map.put(employee.id, employee);
        }
        int res = 0;
        Queue<Employee> q = new ArrayDeque<>(employees.size());
        q.offer(map.get(id));
        while (!q.isEmpty()) {
            int size = q.size();
            for (int i = 0; i < size; i++) {
                Employee node = q.poll();
                res += node.importance;
                for (Integer subordinate : node.subordinates) {
                    q.offer(map.get(subordinate));
                }
            }
        }
        return res;
    }
}

性能

698.划分为k个相等的子集

目标

给定一个整数数组 nums 和一个正整数 k,找出是否有可能把这个数组分成 k 个非空子集,其总和都相等。

示例 1:

输入: nums = [4, 3, 2, 3, 5, 2, 1], k = 4
输出: True
说明: 有可能将其分成 4 个子集(5),(1,4),(2,3),(2,3)等于总和。

示例 2:

输入: nums = [1,2,3,4], k = 3
输出: false

说明:

  • 1 <= k <= len(nums) <= 16
  • 0 < nums[i] < 10000
  • 每个元素的频率在 [1,4] 范围内

思路

给定一个数组 nums,判断能否将其划分为 k 个非空子集,要求每个子集的和都相等。

与数组中元素顺序无关可以排序。数组元素的和是可以枚举的,它一定大于等于数组中的最大元素。和确定了之后就可以从后向前判断能否组成这个和,如果不能则立即返回。

// todo

代码

性能

3133.数组最后一个元素的最小值

目标

给你两个整数 n 和 x 。你需要构造一个长度为 n 的 正整数 数组 nums ,对于所有 0 <= i < n - 1 ,满足 nums[i + 1] 大于 nums[i] ,并且数组 nums 中所有元素的按位 AND 运算结果为 x 。

返回 nums[n - 1] 可能的 最小 值。

示例 1

输入:n = 3, x = 4
输出:6
解释:
数组 nums 可以是 [4,5,6] ,最后一个元素为 6 。

示例 2

输入:n = 2, x = 7
输出:15
解释:
数组 nums 可以是 [7,15] ,最后一个元素为 15 。

说明

  • 1 <= n, x <= 10^8

思路

构造一个长度为 n 的单调递增数组 nums,要求数组所有元素按位与的结果为正整数 x,返回 nums[n - 1] 的最小值。

要使元素按位与的结果为 x,那么 x 中bit位为1的位置在元素中也必须为1。我们可以找到 x 中bit位为0的位置,从最低位开始置1,如果还不凑不够数组个数就将最高位置1,重新从最低位开始置1。

刚开始是按照上面的思路模拟着写的,非常容易出错。调试了半天发现,其实就是将 n - 1 的二进制表示填入 x 的二进制表示中的bit位为0的位置,如果不够就借用高位的0。之所以是 n -1 是因为 x 本身也算一个元素,0 ~ n - 1n 个元素。

官网题解也正是这个思路,不过实现简洁多了。主要是刚开始没有想清楚,其实根本不用讨论低位/高位,直接填就行了。

代码


/**
 * @date 2024-08-22 10:12
 */
public class MinEnd3133 {

    public long minEnd(int n, int x) {
        List<Integer> zeroIndex = new ArrayList<>(63);
        int i = 0;
        int tmp = x;
        // 记录0的位置
        while (tmp > 0) {
            if ((tmp & 1) == 0) {
                zeroIndex.add(i);
            }
            tmp >>= 1;
            i++;
        }
        long res = x;
        int zeroCnt = zeroIndex.size();
        long highBitsCnt;
        // 如果不含0,那么只能填高位
        if (zeroCnt == 0) {
            // 这个不应该全填1,应填n-1的二进制表示
            highBitsCnt = n - 1;
            while (highBitsCnt > 0) {
                if ((highBitsCnt & 1) == 1) {
                    res |= 1L << i;
                }
                i++;
                highBitsCnt >>= 1;
            }
            return res;
        }
        // 如果含0则计算其能表示的数字个数,下面是快速幂
        long oneLoopElementCnt = 1;
        tmp = zeroCnt;
        int base = 2;
        while (tmp > 0) {
            if ((tmp & 1) == 1) {
                oneLoopElementCnt = oneLoopElementCnt * base;
            }
            base *= base;
            tmp >>= 1;
        }
        // 低位需要填的个数
        long lowBitsCnt = n % oneLoopElementCnt - 1;
        if (lowBitsCnt == -1) {
            // 如果为-1,表示能够整除,将低位的0全置为1
            for (Integer index : zeroIndex) {
                res |= 1L << index;
            }
            // 如果低位能够填满数组则无需填高位
            if (n <= oneLoopElementCnt) {
                highBitsCnt = 0;
            } else {
                // 高位的计数需减1,是因为将低位全填1,这时已经完成了一个loop,例如n=4,x=2,oneLoopElementCnt 为2,n / oneLoopElementCnt = 2, (10 11) 110 111,我们只需要填充一个高位
                highBitsCnt = n / oneLoopElementCnt - 1;
            }
        } else {
            // 否则将需要填在低位的个数填入低位的0
            int j = 0;
            while (lowBitsCnt > 0) {
                if ((lowBitsCnt & 1) == 1) {
                    res |= 1L << zeroIndex.get(j);
                }
                j++;
                lowBitsCnt >>= 1;
            }
            // 如果低位能够填满数组则无需填高位
            if (n <= oneLoopElementCnt) {
                highBitsCnt = 0;
            } else {
                // 这里不需要减1,是因为n不能整除oneLoopElementCnt,多余位已经被舍去了,例如n=3,x=2,n / oneLoopElementCnt = 1
                highBitsCnt = n / oneLoopElementCnt;
            }
        }

        // 填充高位个数
        while (highBitsCnt > 0) {
            if ((highBitsCnt & 1) == 1) {
                // 出错点:如果写成1,会溢出,必须加上L
                res |= 1L << i;
            }
            i++;
            highBitsCnt >>= 1;
        }

        return res;
    }

}

性能

3007.价值和小于等于K的最大数字

目标

给你一个整数 k 和一个整数 x 。整数 num 的价值是它的二进制表示中在 x2x3x …… 等位置处 set bit(指在某数的二进制表示中值为 1 的二进制位) 的数目(从最低有效位开始)。下面的表格包含了如何计算价值的例子。

x num Binary Representation Price
1 13 000001101 3
2 13 000001101 1
2 233 011101001 3
3 13 000001101 1
3 362 101101010 2

num累加价值 是从 1num 的数字的 价值。如果 num 的累加价值小于或等于 k 则被认为是 廉价 的。

请你返回 最大 的廉价数字。

示例 1:

输入:k = 9, x = 1
输出:6
解释:由下表所示,6 是最大的廉价数字。
x num Binary Representation Price Accumulated Price
1 1 001 1 1
1 2 010 1 2
1 3 011 2 4
1 4 100 1 5
1 5 101 2 7
1 6 110 2 9
1 7 111 3 12

示例 2:

输入:k = 7, x = 2
输出:9
解释:由下表所示,9 是最大的廉价数字。
x num Binary Representation Price Accumulated Price
2 1 0001 0 0
2 2 0010 1 1
2 3 0011 1 2
2 4 0100 0 2
2 5 0101 0 2
2 6 0110 1 3
2 7 0111 1 4
2 8 1000 1 5
2 9 1001 1 6
2 10 1010 2 8

说明:

  • 1 <= k <= 10^15
  • 1 <= x <= 8

提示:

  • Binary search the answer.
  • In each step of the binary search you should calculate the number of the set bits in the ith position. Then calculate the sum of them.

思路

给定步长x,数字的价值定义为从最低有效位开始,以步长x遍历数字的二进制表示,并记录bit为1的个数。数字n的累加价值定义为 1 ~ n 的价值之和。给定数字k,求累加价值不超过k的最大数字。

先尝试模拟暴力求解。注意到返回值是 long 类型,说明n的规模超过了10^9,即便是 O(n) 的解法也会超时,并且每个数字都要循环bit位计数,肯定超时。我们需要要找一种 O(logn) 的解法。

提示说使用二分查找,问题的关键是如何直接计算出给定数字的累加价值。

// todo

代码

性能

3137.K周期字符串需要的最少操作次数

目标

给你一个长度为 n 的字符串 word 和一个整数 k ,其中 k 是 n 的因数。

在一次操作中,你可以选择任意两个下标 i 和 j,其中 0 <= i, j < n ,且这两个下标都可以被 k 整除,然后用从 j 开始的长度为 k 的子串替换从 i 开始的长度为 k 的子串。也就是说,将子串 word[i..i + k - 1] 替换为子串 word[j..j + k - 1] 。

返回使 word 成为 K 周期字符串 所需的 最少 操作次数。

如果存在某个长度为 k 的字符串 s,使得 word 可以表示为任意次数连接 s ,则称字符串 word 是 K 周期字符串 。例如,如果 word == "ababab",那么 word 就是 s = "ab" 时的 2 周期字符串 。

示例 1:

输入:word = "leetcodeleet", k = 4
输出:1
解释:可以选择 i = 4 和 j = 0 获得一个 4 周期字符串。这次操作后,word 变为 "leetleetleet" 。

示例 2:

输入:word = "leetcoleet", k = 2
输出:3
解释:可以执行以下操作获得一个 2 周期字符串。

i j word
0 2 etetcoleet
4 0 etetetleet
6 0 etetetetet

说明:

  • 1 <= n == word.length <= 10^5
  • 1 <= k <= word.length
  • k 能整除 word.length 。
  • word 仅由小写英文字母组成。

思路

给定字符串 word 以及周期 k(k 为 word 长度的因数,即可以被 word 长度整除),将字符串分为 word.length()/k 份,每次操作可以将其中一份替换为其它份字符串。问最少需要多少次操作,可以使每份字符串都相等。

用份数减去出现次数最多的子串即可。

代码


/**
 * @date 2024-08-17 20:44
 */
public class MinimumOperationsToMakeKPeriodic3137 {

    /**
     * 使用substring效率更高
     * 循环步长为k
     */
    public int minimumOperationsToMakeKPeriodic(String word, int k) {
        int n = word.length();
        int max = 0;
        int parties = n / k;
        // 优化点:指定初始容量
        Map<String, Integer> map = new HashMap<>(parties);
        // 出错点,这里循环条件使 <= n,而非小于n,因为我们使用的使substring,不包括结束index
        for (int i = k; i <= n; i += k) {
            String period = word.substring(i - k, i);
            // 优化点:这里可以直接拿到cnt,不用后面再从map里get
            int cnt = map.merge(period, 1, Integer::sum);
            max = Math.max(cnt, max);
        }
        return parties - max;
    }

}

性能

50.快速幂

目标

实现 pow(x, n) ,即计算 x 的整数 n 次幂函数(即,x^n)。

示例 1:

输入:x = 2.00000, n = 10
输出:1024.00000

示例 2:

输入:x = 2.10000, n = 3
输出:9.26100

示例 3:

输入:x = 2.00000, n = -2
输出:0.25000
解释:2^-2 = 1/2^2 = 1/4 = 0.25

说明:

  • -100.0 < x < 100.0
  • -2^31 <= n <= 2^31-1
  • n 是一个整数
  • 要么 x 不为零,要么 n > 0 。
  • -10^4 <= x^n <= 10^4

思路

通常求 x^n 最直接的算法是迭代相乘,时间复杂度为 O(n)。但是当 n 超过 10^6 就需要考虑 O(logn) 的算法了。

快速幂的核心思想是根据幂次 power 的二进制表示来确定 对应的指数 是否需要计入结果。

比如 2^13, 幂次 13 的二进制表示为 1101,而 2^13 = 2^1 * 2^4 * 2^8,其中 1、4、8 刚好对应幂次的二进制表示中bit为 1 所代表的数字。我们可以循环将幂右移 1 位,直到幂为 0,并在循环中累计 base = base * base,当相应 bit 位为 1 时将 base 计入结果即可。

代码

/**
 * @date 2024-08-16 10:19
 */
public class MyPow50 {

    public double myPow(double x, int n) {
        long cnt = n >= 0 ? n : -((long) n);
        double res = 1.0;
        while (cnt > 0) {
            if ((cnt & 1) == 1) {
                res = res * x;
            }
            x = x * x;
            cnt = cnt >> 1;
        }
        return n == 0 ? 1 : n > 0 ? res : 1 / res;
    }

}

性能

3148.矩阵中的最大得分

目标

给你一个由 正整数 组成、大小为 m x n 的矩阵 grid。你可以从矩阵中的任一单元格移动到另一个位于正下方或正右侧的任意单元格(不必相邻)。从值为 c1 的单元格移动到值为 c2 的单元格的得分为 c2 - c1 。

你可以从 任一 单元格开始,并且必须至少移动一次。

返回你能得到的 最大 总得分。

示例 1:

输入:grid = [[9,5,7,3],[8,9,6,1],[6,7,14,3],[2,5,3,1]]
输出:9
解释:从单元格 (0, 1) 开始,并执行以下移动:
- 从单元格 (0, 1) 移动到 (2, 1),得分为 7 - 5 = 2 。
- 从单元格 (2, 1) 移动到 (2, 2),得分为 14 - 7 = 7 。
总得分为 2 + 7 = 9 。

示例 2:

输入:grid = [[4,3,2],[3,2,1]]
输出:-1
解释:从单元格 (0, 0) 开始,执行一次移动:从 (0, 0) 到 (0, 1) 。得分为 3 - 4 = -1 。

说明:

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

思路

有一个二维矩阵 grid,可以从任意格子出发向下或右移动(不必相邻),移动的得分为元素值之差 to - from,求最大的得分数。

首先想到动态规划,关键点是想清楚最大得分其实就是以当前元素为左上顶点(不包括其自身),以 m - 1, n - 1 为右下顶点的矩形中的最大值减去当前元素值。如果把状态定义错误还是会超时的,比如当前元素出发所能取得的最大值,需要依次向下/向右比较,时间复杂度 O(m * n * (m +n))m = 1000 n = 95,循环次数 10^5 * (10^3 + 95) 超时,耗时878 ms,下午又提交了几次,耗时 1200 ~ 1300 ms。参考特殊数组II 中的时间复杂度分析。对于O(n)的算法,10^8 差不多就是极限了,单个用例在1s左右,多个肯定超时。

代码

/**
 * @date 2024-08-15 0:22
 */
public class MaxScore3148 {

    /**
     * 修改了dp的定义,表示以i,j为起点到右下的矩形的最大值
     * 执行通过
     */
    public int maxScore_v2(List<List<Integer>> grid) {
        int m = grid.size();
        int n = grid.get(0).size();
        int[][] dp = new int[m][n];
        int res = Integer.MIN_VALUE;
        dp[m - 1][n - 1] = grid.get(m - 1).get(n - 1);
        for (int i = n - 2; i >= 0; i--) {
            int cur = grid.get(m - 1).get(i);
            // 注意这里使用的是上一个位置为顶点的矩形中的最大值
            res = Math.max(dp[m - 1][i + 1] - cur, res);
            // 如果先进行这个计算,然后再进行上面的计算,就有会出现不移动的情况,但积分只有移动才能取得,可以是负值
            dp[m - 1][i] = Math.max(cur, dp[m - 1][i + 1]);
        }
        for (int i = m - 2; i >= 0; i--) {
            int cur = grid.get(i).get(n - 1);
            res = Math.max(dp[i + 1][n - 1] - cur, res);
            dp[i][n - 1] = Math.max(cur, dp[i + 1][n - 1]);
        }

        for (int i = m - 2; i >= 0; i--) {
            for (int j = n - 2; j >= 0; j--) {
                int cur = grid.get(i).get(j);
                res = Math.max(dp[i + 1][j] - cur, res);
                res = Math.max(dp[i][j + 1] - cur, res);
                dp[i][j] = Math.max(cur, dp[i + 1][j]);
                dp[i][j] = Math.max(dp[i][j], dp[i][j + 1]);
            }
        }
        return res;
    }

    /**
     * 560 / 564 超时  m = 1000  n = 95
     * O(m * n * (m +n)) 循环次数 10^5 * (10^3 + 95)
     * 878 ms
     */
    public int maxScore_v1(List<List<Integer>> grid) {
        int m = grid.size();
        int n = grid.get(0).size();
        int[][] dp = new int[m][n];
        for (int[] row : dp) {
            Arrays.fill(row, Integer.MIN_VALUE);
        }
        dp[m - 1][n - 1] = 0;
        int res = Integer.MIN_VALUE;
        for (int i = n - 2; i >= 0; i--) {
            for (int j = i + 1; j < n; j++) {
                int diff = grid.get(m - 1).get(j) - grid.get(m - 1).get(i);
                if (dp[m - 1][j] > 0) {
                    dp[m - 1][i] = Math.max(diff + dp[m - 1][j], dp[m - 1][i]);
                }
                dp[m - 1][i] = Math.max(diff, dp[m - 1][i]);
            }
            res = Math.max(dp[m - 1][i], res);
        }
        for (int i = m - 2; i >= 0; i--) {
            for (int j = i + 1; j < m; j++) {
                int diff = grid.get(j).get(n - 1) - grid.get(i).get(n - 1);
                if (dp[j][n - 1] > 0) {
                    dp[i][n - 1] = Math.max(diff + dp[j][n - 1], dp[i][n - 1]);
                }
                dp[i][n - 1] = Math.max(diff, dp[i][n - 1]);
            }
            res = Math.max(dp[i][n - 1], res);
        }

        for (int i = m - 2; i >= 0; i--) {
            for (int j = n - 2; j >= 0; j--) {
                for (int h = i + 1; h < m; h++) {
                    int rowDiff = grid.get(h).get(j) - grid.get(i).get(j);
                    if (dp[h][j] > 0) {
                        dp[i][j] = Math.max(rowDiff + dp[h][j], dp[i][j]);
                    }
                    dp[i][j] = Math.max(rowDiff, dp[i][j]);
                }
                for (int k = j + 1; k < n; k++) {
                    int colDiff = grid.get(i).get(k) - grid.get(i).get(j);
                    if (dp[i][k] > 0) {
                        dp[i][j] = Math.max(colDiff + dp[i][k], dp[i][j]);
                    }
                    dp[i][j] = Math.max(colDiff, dp[i][j]);
                }
                res = Math.max(dp[i][j], res);
            }
        }
        return res;
    }

}

性能

3152.特殊数组II

目标

如果数组的每一对相邻元素都是两个奇偶性不同的数字,则该数组被认为是一个 特殊数组 。

有一个整数数组 nums 和一个二维整数矩阵 queries,对于 queries[i] = [fromi, toi],请你帮助检查子数组 nums[fromi..toi] 是不是一个 特殊数组 。

返回布尔数组 answer,如果 nums[fromi..toi] 是特殊数组,则 answer[i] 为 true ,否则,answer[i] 为 false 。

示例 1:

输入:nums = [3,4,1,2,6], queries = [[0,4]]
输出:[false]
解释:
子数组是 [3,4,1,2,6]。2 和 6 都是偶数。

示例 2:

输入:nums = [4,3,1,6], queries = [[0,2],[2,3]]
输出:[false,true]
解释:
子数组是 [4,3,1]。3 和 1 都是奇数。因此这个查询的答案是 false。
子数组是 [1,6]。只有一对:(1,6),且包含了奇偶性不同的数字。因此这个查询的答案是 true。

说明:

  • 1 <= nums.length <= 10^5
  • 1 <= nums[i] <= 10^5
  • 1 <= queries.length <= 10^5
  • queries[i].length == 2
  • 0 <= queries[i][0] <= queries[i][1] <= nums.length - 1

思路

类似于特殊数组I,只不过是判断子数组是否是特殊数组。我们可以记录奇偶性相同的下标,如果 queries[i] = [fromi, toi] 包含其中的下标对返回false。但是如果所有元素的奇偶性都相同,下标对有n个,查询也是n次,O(n^2) 对于 10^5 的数据规模会超时。

我们可以将问题转化一下,由于仅考虑相邻元素的奇偶性,将数组中的偶数元素都替换为0,奇数元素都替换为1,这样0与1交替出现的是特殊数组。使用前缀和判断不了是否交替出现,只能初步排除一些区间。

之所以超时是因为进行了许多重复判断,我们想直接判断查询区间是否包含奇偶相同的下标对。可以使用二分查找,如果查出的from,to下标相同,说明不相交。

这里比较坑的一点是,题目中没有说明如果查询区间只包含一个值视为奇偶性不同。

log2(10^5) ≈ 16.6 时间复杂度为O(nlogn),耗时30ms,说明10^6的数据规模,O(n)的解法不会超时,以后多留意一下。

看了题解,其实可以使用前缀和,只不过不是将原数组转为0或1计算前缀和,而是定义数组diff,原数组nums相邻元素奇偶性相同为0,不同为1。对应给定的区间,我们就可以根据diff的前缀和判断是否含有奇偶性相同的元素。时间复杂度为O(n),耗时3ms。

也有使用动态规划求解的,记录最近一次奇偶性相同的位置。时间复杂度为O(n),耗时3ms。

代码

/**
 * @date 2024-08-14 9:58
 */
public class IsArraySpecial3152 {

    public boolean[] isArraySpecial_v1(int[] nums, int[][] queries) {
        int n = nums.length;
        List<Integer> from = new ArrayList<>();
        List<Integer> to = new ArrayList<>();
        for (int i = 1; i < n; i++) {
            int j = i - 1;
            if (nums[i] % 2 == nums[j] % 2) {
                from.add(j);
                to.add(i);
            }
        }
        int l = from.size();
        int[] fromArray = new int[l];
        int[] toArray = new int[l];
        for (int i = 0; i < l; i++) {
            fromArray[i] = from.get(i);
            toArray[i] = to.get(i);
        }
        int length = queries.length;
        boolean[] res = new boolean[length];
        Arrays.fill(res, true);
        for (int i = 0; i < length; i++) {
            int[] query = queries[i];
            if (query[0] == query[1]) {
                // 如果查询区间相同认为是特殊区间
                res[i] = true;
                continue;
            }
            int fromIndex = Arrays.binarySearch(fromArray, query[0]);
            if (fromIndex >= 0) {
                // 如果需要查询的数组包含了query[0],由于前面判断了相等的情况,
                // 执行到这里query[1] > query[0],而对应的toIndex 为 fromIndex + 1,
                // 推出 query[1] >= toIndex,说明包含奇偶性相同的下标对,
                res[i] = false;
                continue;
            }
            int toIndex = Arrays.binarySearch(toArray, query[1]);
            if (fromIndex != toIndex) {
                // 执行到这里,fromIndex < 0,如果 toIndex >= 0,由于 query[0] < query[1],推出 query[0] <= fromIndex
                // 如果 toIndex < 0,说明查询区间开始与结束下标都没有找到,
                // 如果插入位置不同,说明也包含了奇偶性相同的元素
                res[i] = false;
            }
        }
        return res;
    }

}

性能