2070.每一个查询的最大美丽值

目标

给你一个二维整数数组 items ,其中 items[i] = [pricei, beautyi] 分别表示每一个物品的 价格 和 美丽值 。

同时给你一个下标从 0 开始的整数数组 queries 。对于每个查询 queries[j] ,你想求出价格小于等于 queries[j] 的物品中,最大的美丽值 是多少。如果不存在符合条件的物品,那么查询的结果为 0 。

请你返回一个长度与 queries 相同的数组 answer,其中 answer[j]是第 j 个查询的答案。

示例 1:

输入:items = [[1,2],[3,2],[2,4],[5,6],[3,5]], queries = [1,2,3,4,5,6]
输出:[2,4,5,5,6,6]
解释:
- queries[0]=1 ,[1,2] 是唯一价格 <= 1 的物品。所以这个查询的答案为 2 。
- queries[1]=2 ,符合条件的物品有 [1,2] 和 [2,4] 。
  它们中的最大美丽值为 4 。
- queries[2]=3 和 queries[3]=4 ,符合条件的物品都为 [1,2] ,[3,2] ,[2,4] 和 [3,5] 。
  它们中的最大美丽值为 5 。
- queries[4]=5 和 queries[5]=6 ,所有物品都符合条件。
  所以,答案为所有物品中的最大美丽值,为 6 。

示例 2:

输入:items = [[1,2],[1,2],[1,3],[1,4]], queries = [1]
输出:[4]
解释:
每个物品的价格均为 1 ,所以我们选择最大美丽值 4 。
注意,多个物品可能有相同的价格和美丽值。

示例 3:

输入:items = [[10,1000]], queries = [5]
输出:[0]
解释:
没有物品的价格小于等于 5 ,所以没有物品可以选择。
因此,查询的结果为 0 。

说明:

  • 1 <= items.length, queries.length <= 10^5
  • items[i].length == 2
  • 1 <= pricei, beautyi, queries[j] <= 10^9

思路

有一个二维数组 items,其元素 items[i] = [pricei, beautyi] 表示 item 的价格与美丽值。有一个查询数组,每一次查询的目标是找出价格小于等于 queries[i] 的物品中的最大美丽值。

为了求得答案,需要知道小于 queries[i] 都有哪些物品,然后从中找出最大美丽值。

先将 items 按照价格从小到大排序,二分查找 queries[i] 的上界下标 end。剩下的问题是找出 [0, end] 范围内的最大美丽值,这些值是固定的,可以预处理。

代码


/**
 * @date 2025-03-09 22:44
 */
public class MaximumBeauty2070 {

    public int[] maximumBeauty(int[][] items, int[] queries) {
        int n = items.length;
        Arrays.sort(items, (a, b) -> a[0] - b[0]);
        int[] maxBeauty = new int[n];
        maxBeauty[0] = items[0][1];
        for (int i = 1; i < n; i++) {
            maxBeauty[i] = Math.max(maxBeauty[i - 1], items[i][1]);
        }
        int[] res = new int[queries.length];
        for (int i = 0; i < queries.length; i++) {
            int upperbound = bs(queries[i], items);
            if (upperbound >= 0 && upperbound < n) {
                res[i] = maxBeauty[upperbound];
            }
        }
        return res;
    }

    public int bs(int target, int[][] items) {
        int n = items.length;
        int left = 0, right = n - 1;
        int mid = left + (right - left) / 2;
        while (left <= right) {
            if (items[mid][0] > target) {
                right = mid - 1;
            } else {
                left = mid + 1;
            }
            mid = left + (right - left) / 2;
        }
        return right;
    }

}

性能

2234.花园的最大总美丽值

目标

Alice 是 n 个花园的园丁,她想通过种花,最大化她所有花园的总美丽值。

给你一个下标从 0 开始大小为 n 的整数数组 flowers ,其中 flowers[i] 是第 i 个花园里已经种的花的数目。已经种了的花 不能 移走。同时给你 newFlowers ,表示 Alice 额外可以种花的 最大数目 。同时给你的还有整数 target ,full 和 partial 。

如果一个花园有 至少 target 朵花,那么这个花园称为 完善的 ,花园的 总美丽值 为以下分数之 和 :

  • 完善 花园数目乘以 full.
  • 剩余 不完善 花园里,花的 最少数目 乘以 partial 。如果没有不完善花园,那么这一部分的值为 0 。

请你返回 Alice 种最多 newFlowers 朵花以后,能得到的 最大 总美丽值。

示例 1:

输入:flowers = [1,3,1,1], newFlowers = 7, target = 6, full = 12, partial = 1
输出:14
解释:Alice 可以按以下方案种花
- 在第 0 个花园种 2 朵花
- 在第 1 个花园种 3 朵花
- 在第 2 个花园种 1 朵花
- 在第 3 个花园种 1 朵花
花园里花的数目为 [3,6,2,2] 。总共种了 2 + 3 + 1 + 1 = 7 朵花。
只有 1 个花园是完善的。
不完善花园里花的最少数目是 2 。
所以总美丽值为 1 * 12 + 2 * 1 = 12 + 2 = 14 。
没有其他方案可以让花园总美丽值超过 14 。

示例 2:

输入:flowers = [2,4,5,3], newFlowers = 10, target = 5, full = 2, partial = 6
输出:30
解释:Alice 可以按以下方案种花
- 在第 0 个花园种 3 朵花
- 在第 1 个花园种 0 朵花
- 在第 2 个花园种 0 朵花
- 在第 3 个花园种 2 朵花
花园里花的数目为 [5,4,5,5] 。总共种了 3 + 0 + 0 + 2 = 5 朵花。
有 3 个花园是完善的。
不完善花园里花的最少数目为 4 。
所以总美丽值为 3 * 2 + 4 * 6 = 6 + 24 = 30 。
没有其他方案可以让花园总美丽值超过 30 。
注意,Alice可以让所有花园都变成完善的,但这样她的总美丽值反而更小。

说明:

  • 1 <= flowers.length <= 10^5
  • 1 <= flowers[i], target <= 10^5
  • 1 <= newFlowers <= 10^10
  • 1 <= full, partial <= 10^5

思路

有一个整数数组 flowersflowers[i] 表示下标为 i 的花园种植的花的数目。花园的美丽值通过以下方式计算:

  • 花园中花的数目大于等于 target,称该花园是完善的,美丽值 = 所有完善花园的个数 * full
  • 花园中花的数目小于 target,美丽值 = 所有花园中花的最小数目 * partial

Alice 可以向任意花园中种花,新种的花的数目不超过 newFlowers,求花园的最大美丽值。

// todo

代码

性能

2597.美丽子集的数目

目标

给你一个由正整数组成的数组 nums 和一个 正 整数 k 。

如果 nums 的子集中,任意两个整数的绝对差均不等于 k ,则认为该子数组是一个 美丽 子集。

返回数组 nums 中 非空 且 美丽 的子集数目。

nums 的子集定义为:可以经由 nums 删除某些元素(也可能不删除)得到的一个数组。只有在删除元素时选择的索引不同的情况下,两个子集才会被视作是不同的子集。

示例 1:

输入:nums = [2,4,6], k = 2
输出:4
解释:数组 nums 中的美丽子集有:[2], [4], [6], [2, 6] 。
可以证明数组 [2,4,6] 中只存在 4 个美丽子集。

示例 2:

输入:nums = [1], k = 1
输出:1
解释:数组 nums 中的美丽数组有:[1] 。
可以证明数组 [1] 中只存在 1 个美丽子集。

说明:

  • 1 <= nums.length <= 18
  • 1 <= nums[i], k <= 1000

思路

返回数组的美丽子序列个数,美丽指子序列中任意两个元素的差的绝对值不等于 k

由于数组长度不大,可以回溯枚举子序列,并针对每一子序列,两两比较,判断差的绝对值是否等于 k

更好的处理方法是使用哈希表记录元素的出现次数,回溯枚举子序列时直接判断当前元素加减 k 的绝对值是否存在于哈希表中。

代码


/**
 * @date 2025-03-07 0:09
 */
public class BeautifulSubsets2597 {

    public int beautifulSubsets(int[] nums, int k) {
        return dfs(0, new ArrayList<>(), nums, k);
    }

    public int dfs(int index, List<Integer> path, int[] nums, int k) {
        if (index == nums.length) {
            return path.size() > 0 ? 1 : 0;
        }
        int res = 0;
        res += dfs(index + 1, path, nums, k);
        for (Integer num : path) {
            if (Math.abs(num - nums[index]) == k) {
                return res;
            }
        }
        path.add(nums[index]);
        res += dfs(index + 1, path, nums, k);
        path.remove(path.size() - 1);
        return res;
    }

}

性能

2588.统计美丽子数组数目

目标

给你一个下标从 0 开始的整数数组nums 。每次操作中,你可以:

  • 选择两个满足 0 <= i, j < nums.length 的不同下标 i 和 j 。
  • 选择一个非负整数 k ,满足 nums[i] 和 nums[j] 在二进制下的第 k 位(下标编号从 0 开始)是 1 。
  • 将 nums[i] 和 nums[j] 都减去 2^k 。

如果一个子数组内执行上述操作若干次后,该子数组可以变成一个全为 0 的数组,那么我们称它是一个 美丽 的子数组。

请你返回数组 nums 中 美丽子数组 的数目。

子数组是一个数组中一段连续 非空 的元素序列。

示例 1:

输入:nums = [4,3,1,2,4]
输出:2
解释:nums 中有 2 个美丽子数组:[3,1,2] 和 [4,3,1,2,4] 。
- 按照下述步骤,我们可以将子数组 [3,1,2] 中所有元素变成 0 :
  - 选择 [3, 1, 2] 和 k = 1 。将 2 个数字都减去 21 ,子数组变成 [1, 1, 0] 。
  - 选择 [1, 1, 0] 和 k = 0 。将 2 个数字都减去 20 ,子数组变成 [0, 0, 0] 。
- 按照下述步骤,我们可以将子数组 [4,3,1,2,4] 中所有元素变成 0 :
  - 选择 [4, 3, 1, 2, 4] 和 k = 2 。将 2 个数字都减去 22 ,子数组变成 [0, 3, 1, 2, 0] 。
  - 选择 [0, 3, 1, 2, 0] 和 k = 0 。将 2 个数字都减去 20 ,子数组变成 [0, 2, 0, 2, 0] 。
  - 选择 [0, 2, 0, 2, 0] 和 k = 1 。将 2 个数字都减去 21 ,子数组变成 [0, 0, 0, 0, 0] 。

示例 2:

输入:nums = [1,10,4]
输出:0
解释:nums 中没有任何美丽子数组。

说明:

  • 1 <= nums.length <= 10^5
  • 0 <= nums[i] <= 10^6

思路

有一个下标从 0 开始的数组 nums,每次操作可以任选两个不同下标的元素,如果它们在二进制下的第 k 位是 1,那么将这两个元素减去 2^k,即把这两个元素的第 k 位置 0。如果对 nums 的子数组执行任意次操作后可以将子数组所有元素变为 0,称该子数组为 美丽子数组。返回数组 nums 的美丽子数组数目。

通过分析可以知道,累加美丽子数组中所有元素在二进制下每个bit位上 1 的个数,可以发现每个位置上 1 的个数都是偶数。可以通过异或运算来判断是否是美丽子数组。

代码


/**
 * @date 2025-03-06 8:37
 */
public class BeautifulSubarrays2588 {

    public long beautifulSubarrays(int[] nums) {
        int n = nums.length;
        int[] prefix = new int[n + 1];
        Map<Integer, List<Integer>> map = new HashMap<>();
        map.computeIfAbsent(prefix[0], x -> new ArrayList<>()).add(0);
        for (int i = 1; i <= n; i++) {
            prefix[i] = nums[i - 1] ^ prefix[i - 1];
            map.computeIfAbsent(prefix[i], x -> new ArrayList<>()).add(i);
        }
        long res = 0;
        for (List<Integer> value : map.values()) {
            int size = value.size();
            res += (size - 1L) * size / 2;
        }
        return res;
    }

}

性能

1328.破坏回文串

目标

给你一个由小写英文字母组成的回文字符串 palindrome ,请你将其中 一个 字符用任意小写英文字母替换,使得结果字符串的 字典序最小 ,且 不是 回文串。

请你返回结果字符串。如果无法做到,则返回一个 空串 。

如果两个字符串长度相同,那么字符串 a 字典序比字符串 b 小可以这样定义:在 a 和 b 出现不同的第一个位置上,字符串 a 中的字符严格小于 b 中的对应字符。例如,"abcc” 字典序比 "abcd" 小,因为不同的第一个位置是在第四个字符,显然 'c' 比 'd' 小。

示例 1:

输入:palindrome = "abccba"
输出:"aaccba"
解释:存在多种方法可以使 "abccba" 不是回文,例如 "zbccba", "aaccba", 和 "abacba" 。
在所有方法中,"aaccba" 的字典序最小。

示例 2:

输入:palindrome = "a"
输出:""
解释:不存在替换一个字符使 "a" 变成非回文的方法,所以返回空字符串。

说明:

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

思路

有一个回文字符串,可以修改其中一个字符使其不是回文,求修改后字典序最小的字符串。如果无法破会回文串,返回空串。

根据题意,字符串长度为 1 直接返回空串。

要使字典序最小,只需使字符串最左边的字符为 a,但是需考虑能否破坏回文串。于是就有了贪心解法,从左到右将对应位置上的字符修改为 a,判断是否是回文,不是回文直接返回。如果均为 a,将最后一个字符替换为 b

注意不要改中间的字符,它不会破坏回文串。

代码


/**
 * @date 2025-03-05 0:06
 */
public class BreakPalindrome1328 {

    public String breakPalindrome(String palindrome) {
        int n = palindrome.length();
        if (n == 1) {
            return "";
        }
        int l = 0, r = n - 1;
        char[] chars = palindrome.toCharArray();
        while (l < r) {
            if (chars[r] != 'a') {
                chars[l] = 'a';
                return new String(chars);
            }
            l++;
            r--;
        }
        chars[n - 1] = 'b';
        return new String(chars);
    }

}

性能

1745.分割回文串IV

目标

给你一个字符串 s ,如果可以将它分割成三个 非空 回文子字符串,那么返回 true ,否则返回 false 。

当一个字符串正着读和反着读是一模一样的,就称其为 回文字符串 。

示例 1:

输入:s = "abcbdd"
输出:true
解释:"abcbdd" = "a" + "bcb" + "dd",三个子字符串都是回文的。

示例 2:

输入:s = "bcbddxy"
输出:false
解释:s 没办法被分割成 3 个回文子字符串。

说明:

  • 3 <= s.length <= 2000
  • s​​​​​​ 只包含小写英文字母。

思路

判断能否将给定字符串分割成三个非空回文子串。

核心逻辑:

  • 计算所有子串是否是回文。
  • 从起点 i = 0 开始,枚举所有终点 j,如果是 s[i~j] 是回文,k-- 接着以 j + 1 为起点继续递归搜索。
  • 结束条件是 i == s.length() || k < 0,如果 k == 0 返回 true。

代码


/**
 * @date 2025-03-04 15:58
 */
public class CheckPartitioning1745 {

    public boolean checkPartitioning(String s) {
        int n = s.length();
        boolean[][] isPalindrome = new boolean[n][n];
        for (boolean[] row : isPalindrome) {
            Arrays.fill(row, true);
        }
        for (int i = n - 2; i >= 0; i--) {
            for (int j = i + 1; j < n; j++) {
                isPalindrome[i][j] = s.charAt(i) == s.charAt(j) && isPalindrome[i + 1][j - 1];
            }
        }
        char[][] mem = new char[n][4];
        return dfs(0, 3, isPalindrome, s, mem);
    }

    public boolean dfs(int i, int k, boolean[][] isPalindrome, String s, char[][] mem) {
        int n = s.length();
        if (i == n || k < 0) {
            return k == 0;
        }
        if (mem[i][k] != '\u0000') {
            return mem[i][k] == 'T';
        }
        boolean res = false;
        for (int j = i; j < n; j++) {
            if (isPalindrome[i][j]) {
                res = res || dfs(j + 1, k - 1, isPalindrome, s, mem);
            }
        }
        mem[i][k] = res ? 'T' : 'F';
        return res;
    }

}

性能

1278.分割回文串III

目标

给你一个由小写字母组成的字符串 s,和一个整数 k。

请你按下面的要求分割字符串:

  • 首先,你可以将 s 中的部分字符修改为其他的小写英文字母。
  • 接着,你需要把 s 分割成 k 个非空且不相交的子串,并且每个子串都是回文串。

请返回以这种方式分割字符串所需修改的最少字符数。

示例 1:

输入:s = "abc", k = 2
输出:1
解释:你可以把字符串分割成 "ab" 和 "c",并修改 "ab" 中的 1 个字符,将它变成回文串。

示例 2:

输入:s = "aabbc", k = 3
输出:0
解释:你可以把字符串分割成 "aa"、"bb" 和 "c",它们都是回文串。

示例 3:

输入:s = "leetcode", k = 8
输出:0

说明:

  • 1 <= k <= s.length <= 100
  • s 中只含有小写英文字母。

思路

将字符串 s 分割成 k 个非空回文子串,允许修改字符中的任意字符,求修改字符的最小次数。

需要将字符串分割成 k 份并且每一份都是回文。我们需要暴力枚举所有可能的分法,并求得每种分法的修改次数,取其最小值。

核心逻辑:

  • 计算所有子串变为回文的修改次数,ops[i][j] = s.charAt(i) == s.charAt(j) ? ops[i + 1][j - 1] : ops[i + 1][j - 1] + 1;。由于需要从 i + 1 转移过来,所以外层倒序。初始化数组所有值为 0,外层从倒数第二个开始,内层从 i + 1 开始。
  • 从起点 i = 0 开始,枚举所有终点 j,无论 s[i~j] 是否是回文,我们直接加上 ops[i][j]k-- 接着以 j + 1 为起点继续递归搜索。
  • 结束条件:
    • i 未到结尾 k 先减为 0,不符合要求,返回 INF,可以取 0x3f3f3f3f
    • i == s.length(),如果 k == 0 返回 0,否则说明分割的子串没有 k 个,不符合题意返回 INF

代码


/**
 * @date 2025-03-03 8:52
 */
public class PalindromePartition1278 {

    public int palindromePartition(String s, int k) {
        int n = s.length();
        int[][] ops = new int[n][n];
        for (int i = n - 2; i >= 0; i--) {
            for (int j = i + 1; j < n; j++) {
                ops[i][j] = s.charAt(i) == s.charAt(j) ? ops[i + 1][j - 1] : ops[i + 1][j - 1] + 1;
            }
        }
        int[][] mem = new int[n][k + 1];
        for (int[] row : mem) {
            Arrays.fill(row, -1);
        }
        return dfs(0, k, s, ops, mem);
    }

    public int dfs(int i, int k, String s, int[][] ops, int[][] mem) {
        int n = s.length();
        if (i == n) {
            return k > 0 ? 0x3f3f3f3f : 0;
        }
        if (k == 0) {
            return 0x3f3f3f3f;
        }
        if (mem[i][k] != -1) {
            return mem[i][k];
        }
        int res = Integer.MAX_VALUE;
        for (int j = i; j < n; j++) {
            res = Math.min(res, ops[i][j] + dfs(j + 1, k - 1, s, ops, mem));
        }
        mem[i][k] = res;
        return res;
    }

}

性能

132.分割回文串II

目标

给你一个字符串 s,请你将 s 分割成一些子串,使每个子串都是回文串。

返回符合要求的 最少分割次数 。

示例 1:

输入:s = "aab"
输出:1
解释:只需一次分割就可将 s 分割成 ["aa","b"] 这样两个回文子串。

示例 2:

输入:s = "a"
输出:0

示例 3:

输入:s = "ab"
输出:1

说明:

  • 1 <= s.length <= 2000
  • s 仅由小写英文字母组成

思路

计算将字符串分割成回文子串的最小分割次数。

定义 dp[i] 表示前 i + 1 个字符的最小分割次数,如果 [0, i] 个字符已经是回文,无需切割,切割次数为 0。否则,需要枚举起点 j,如果 [j, i] 是回文,dp[i] = Math.min(dp[j - 1] + 1)

预处理 [i, j] 是否是回文,isPalindrome[i][j] = s.charAt(i) == s.charAt(j) && isPalindrome[i + 1][j - 1],由于状态由 i + 1 转换而来,外层要倒序,内层正序或倒序都可以。

代码


/**
 * @date 2025-03-02 0:10
 */
public class MinCut132 {

    public int minCut(String s) {
        int n = s.length();
        boolean[][] isPalindrome = new boolean[n][n];
        for (boolean[] row : isPalindrome) {
            Arrays.fill(row, true);
        }
        for (int i = n - 2; i >= 0; i--) {
            for (int j = i + 1; j < n; j++) {
                isPalindrome[i][j] = s.charAt(i) == s.charAt(j) && isPalindrome[i + 1][j - 1];
            }
        }
        int[] dp = new int[n];
        for (int i = 0; i < n; i++) {
            if (isPalindrome[0][i]) {
                continue;
            }
            int res = Integer.MAX_VALUE;
            for (int j = 1; j <= i; j++) {
                if (isPalindrome[j][i]) {
                    res = Math.min(res, dp[j - 1] + 1);
                }
            }
            dp[i] = res;
        }
        return dp[n - 1];
    }

}

性能

131.分割回文串

目标

给你一个字符串 s,请你将 s 分割成一些子串,使每个子串都是 回文串 。返回 s 所有可能的分割方案。

示例 1:

输入:s = "aab"
输出:[["a","a","b"],["aa","b"]]

示例 2:

输入:s = "a"
输出:[["a"]]

说明:

  • 1 <= s.length <= 16
  • s 仅由小写英文字母组成

思路

返回将字符串划分为回文子串的所有可能方案。

定义 dp[i] 表示将前 i + 1 个字符划分为回文子串的所有可能方案。dp[i] 可以取 dp[i - 1] 每种方案的最后一或两个回文,判断能否与当前字符合并成新的回文。

代码


/**
 * @date 2025-03-01 20:16
 */
public class Partition131 {

    public List<List<String>> partition(String s) {
        int n = s.length();
        List<List<String>>[] dp = new List[n];
        for (int i = 0; i < n; i++) {
            dp[i] = new ArrayList<>();
        }
        char[] chars = s.toCharArray();
        dp[0].add(new ArrayList<>());
        dp[0].get(0).add((String.valueOf(chars[0])));
        for (int i = 1; i < n; i++) {
            dp[i] = new ArrayList<>();
            for (List<String> list : dp[i - 1]) {
                // 首先将单个字符加入
                List<String> tmp = new ArrayList<>(list);
                tmp.add(String.valueOf(chars[i]));
                dp[i].add(tmp);
                String cur = String.valueOf(chars[i]);
                // 判断能与最后一个回文合并成新的回文
                if (list.get(list.size() - 1).equals(cur)) {
                    tmp = new ArrayList<>(list.subList(0, list.size() - 1));
                    tmp.add(list.get(list.size() - 1) + cur);
                    dp[i].add(tmp);
                }
                // 判断能否与后两个回文合并成新的回文
                if (list.size() > 1 && list.get(list.size() - 2).equals(cur)) {
                    tmp = new ArrayList<>(list.subList(0, list.size() - 2));
                    tmp.add(list.get(list.size() - 2) + list.get(list.size() - 1) + cur);
                    dp[i].add(tmp);
                }
            }
        }
        return dp[n - 1];
    }

}

性能

时间复杂度 O(n * 2^n),假设每个字符之间都有一个逗号,考虑选或不选共有 2^(n - 1) 种划分。前 i + 1 个 字符有 2^i 种划分, 等比数列求和得到 2^n - 1

2353.设计食物评分系统

目标

设计一个支持下述操作的食物评分系统:

  • 修改 系统中列出的某种食物的评分。
  • 返回系统中某一类烹饪方式下评分最高的食物。

实现 FoodRatings 类:

  • FoodRatings(String[] foods, String[] cuisines, int[] ratings) 初始化系统。食物由 foods、cuisines 和 ratings 描述,长度均为 n 。
  • foods[i] 是第 i 种食物的名字。
  • cuisines[i] 是第 i 种食物的烹饪方式。
  • ratings[i] 是第 i 种食物的最初评分。
  • void changeRating(String food, int newRating) 修改名字为 food 的食物的评分。
  • String highestRated(String cuisine) 返回指定烹饪方式 cuisine 下评分最高的食物的名字。如果存在并列,返回 字典序较小 的名字。

注意,字符串 x 的字典序比字符串 y 更小的前提是:x 在字典中出现的位置在 y 之前,也就是说,要么 x 是 y 的前缀,或者在满足 x[i] != y[i] 的第一个位置 i 处,x[i] 在字母表中出现的位置在 y[i] 之前。

示例:

输入
["FoodRatings", "highestRated", "highestRated", "changeRating", "highestRated", "changeRating", "highestRated"]
[[["kimchi", "miso", "sushi", "moussaka", "ramen", "bulgogi"], ["korean", "japanese", "japanese", "greek", "japanese", "korean"], [9, 12, 8, 15, 14, 7]], ["korean"], ["japanese"], ["sushi", 16], ["japanese"], ["ramen", 16], ["japanese"]]
输出
[null, "kimchi", "ramen", null, "sushi", null, "ramen"]

解释
FoodRatings foodRatings = new FoodRatings(["kimchi", "miso", "sushi", "moussaka", "ramen", "bulgogi"], ["korean", "japanese", "japanese", "greek", "japanese", "korean"], [9, 12, 8, 15, 14, 7]);
foodRatings.highestRated("korean"); // 返回 "kimchi"
                                    // "kimchi" 是分数最高的韩式料理,评分为 9 。
foodRatings.highestRated("japanese"); // 返回 "ramen"
                                      // "ramen" 是分数最高的日式料理,评分为 14 。
foodRatings.changeRating("sushi", 16); // "sushi" 现在评分变更为 16 。
foodRatings.highestRated("japanese"); // 返回 "sushi"
                                      // "sushi" 是分数最高的日式料理,评分为 16 。
foodRatings.changeRating("ramen", 16); // "ramen" 现在评分变更为 16 。
foodRatings.highestRated("japanese"); // 返回 "ramen"
                                      // "sushi" 和 "ramen" 的评分都是 16 。
                                      // 但是,"ramen" 的字典序比 "sushi" 更小。

说明:

  • 1 <= n <= 2 * 10^4
  • n == foods.length == cuisines.length == ratings.length
  • 1 <= foods[i].length, cuisines[i].length <= 10
  • foods[i]、cuisines[i] 由小写英文字母组成
  • 1 <= ratings[i] <= 10^8
  • foods 中的所有字符串 互不相同
  • 在对 changeRating 的所有调用中,food 是系统中食物的名字。
  • 在对 highestRated 的所有调用中,cuisine 是系统中 至少一种 食物的烹饪方式。
  • 最多调用 changeRating 和 highestRated 总计 2 * 10^4 次

思路

设计一个食物评分系统,返回指定类别评分最高的食物,支持修改食物的评分。

要知道类别中评分最高的食物,优先队列/TreeSet 的元素应为 (rating, food) 键值对,根据评分从大到小排序,如果评分相同根据食物的字典序排列。

修改食物评分后需要更新对应类别的评分排名,因此需要维护 (food, cuisine) 的映射关系。如果使用懒加载,还需要记录食物最新的评分,维护 (food, rating)。如果使用红黑树,需要根据更新前的评分删除树中数据,同样需要维护 (food, rating)

有人使用优先队列超时是因为删除元素的复杂度是 O(n)。考虑使用懒删除或者使用 有序集合 TreeSet。有序集合查找最大/最小节点的复杂度是 O(logn),最大/小节点是最右/左叶子节点,查找复杂度是树的高度。

代码


/**
 * @date 2025-02-28 0:10
 */
public class FoodRatings {

    Map<String, PriorityQueue<String[]>> map;
    Map<String, String> foodMap;
    Map<String, Integer> ratingMap;

    public FoodRatings(String[] foods, String[] cuisines, int[] ratings) {
        int n = foods.length;
        map = new HashMap<>(n);
        foodMap = new HashMap<>(n);
        ratingMap = new HashMap<>(n);
        for (int i = 0; i < n; i++) {
            foodMap.put(foods[i], cuisines[i]);
            ratingMap.put(foods[i], ratings[i]);
            map.putIfAbsent(cuisines[i], new PriorityQueue<>((a, b) -> {
                int compare = Integer.parseInt(b[0]) - Integer.parseInt(a[0]);
                if (compare != 0) {
                    return compare;
                }
                return a[1].compareTo(b[1]);
            }));
            map.get(cuisines[i]).offer(new String[]{String.valueOf(ratings[i]), foods[i]});
        }
    }

    public void changeRating(String food, int newRating) {
        ratingMap.put(food, newRating);
        map.get(foodMap.get(food)).offer(new String[]{String.valueOf(newRating), food});
    }

    public String highestRated(String cuisine) {
        PriorityQueue<String[]> q = map.get(cuisine);
        while (Integer.parseInt(q.peek()[0]) != ratingMap.get(q.peek()[1])) {
            q.poll();
        }
        return q.peek()[1];
    }
}

性能