1338.数组大小减半

目标

给你一个整数数组 arr。你可以从中选出一个整数集合,并删除这些整数在数组中的每次出现。

返回 至少 能删除数组中的一半整数的整数集合的最小大小。

示例 1:

输入:arr = [3,3,3,3,5,5,5,2,2,7]
输出:2
解释:选择 {3,7} 使得结果数组为 [5,5,5,2,2]、长度为 5(原数组长度的一半)。
大小为 2 的可行集合有 {3,5},{3,2},{5,2}。
选择 {2,7} 是不可行的,它的结果数组为 [3,3,3,3,5,5,5],新数组长度大于原数组的二分之一。

示例 2:

输入:arr = [7,7,7,7,7,7]
输出:1
解释:我们只能选择集合 {7},结果数组为空。

说明:

  • 1 <= arr.length <= 10^5
  • arr.length 为偶数
  • 1 <= arr[i] <= 10^5

思路

从整数数组中选出一个元素集合,使该集合中元素在原数组中的出现次数超过原数组长度的一半,求集合大小的最小值。

统计每个元素的出现次数,将出现次数从大到小排序,然后开始选元素直到满足题目条件。

代码


/**
 * @date 2024-12-15 0:17
 */
public class MinSetSize1338 {

    public int minSetSize(int[] arr) {
        int n = arr.length;
        int[] cnt = new int[100001];
        for (int i : arr) {
            cnt[i]++;
        }
        PriorityQueue<Integer> q = new PriorityQueue<>((a, b) -> b - a);
        for (int i : cnt) {
            if (i > 0) {
                q.offer(i);
            }
        }
        int res = 0;
        int l = 0;
        while (!q.isEmpty()) {
            l += q.poll();
            res++;
            if (l >= n / 2) {
                break;
            }
        }
        return res;
    }
}

性能

2931.购买物品的最大开销

目标

给你一个下标从 0 开始大小为 m * n 的整数矩阵 values ,表示 m 个不同商店里 m * n 件不同的物品。每个商店有 n 件物品,第 i 个商店的第 j 件物品的价值为 values[i][j] 。除此以外,第 i 个商店的物品已经按照价值非递增排好序了,也就是说对于所有 0 <= j < n - 1 都有 values[i][j] >= values[i][j + 1]

每一天,你可以在一个商店里购买一件物品。具体来说,在第 d 天,你可以:

  • 选择商店 i 。
  • 购买数组中最右边的物品 j ,开销为 values[i][j] * d 。换句话说,选择该商店中还没购买过的物品中最大的下标 j ,并且花费 values[i][j] * d 去购买。

注意,所有物品都视为不同的物品。比方说如果你已经从商店 1 购买了物品 0 ,你还可以在别的商店里购买其他商店的物品 0 。

请你返回购买所有 m * n 件物品需要的 最大开销 。

示例 1:

输入:values = [[8,5,2],[6,4,1],[9,7,3]]
输出:285
解释:第一天,从商店 1 购买物品 2 ,开销为 values[1][2] * 1 = 1 。
第二天,从商店 0 购买物品 2 ,开销为 values[0][2] * 2 = 4 。
第三天,从商店 2 购买物品 2 ,开销为 values[2][2] * 3 = 9 。
第四天,从商店 1 购买物品 1 ,开销为 values[1][1] * 4 = 16 。
第五天,从商店 0 购买物品 1 ,开销为 values[0][1] * 5 = 25 。
第六天,从商店 1 购买物品 0 ,开销为 values[1][0] * 6 = 36 。
第七天,从商店 2 购买物品 1 ,开销为 values[2][1] * 7 = 49 。
第八天,从商店 0 购买物品 0 ,开销为 values[0][0] * 8 = 64 。
第九天,从商店 2 购买物品 0 ,开销为 values[2][0] * 9 = 81 。
所以总开销为 285 。
285 是购买所有 m * n 件物品的最大总开销。

示例 2:

输入:values = [[10,8,6,4,2],[9,7,5,3,2]]
输出:386
解释:第一天,从商店 0 购买物品 4 ,开销为 values[0][4] * 1 = 2 。
第二天,从商店 1 购买物品 4 ,开销为 values[1][4] * 2 = 4 。
第三天,从商店 1 购买物品 3 ,开销为 values[1][3] * 3 = 9 。
第四天,从商店 0 购买物品 3 ,开销为 values[0][3] * 4 = 16 。
第五天,从商店 1 购买物品 2 ,开销为 values[1][2] * 5 = 25 。
第六天,从商店 0 购买物品 2 ,开销为 values[0][2] * 6 = 36 。
第七天,从商店 1 购买物品 1 ,开销为 values[1][1] * 7 = 49 。
第八天,从商店 0 购买物品 1 ,开销为 values[0][1] * 8 = 64 。
第九天,从商店 1 购买物品 0 ,开销为 values[1][0] * 9 = 81 。
第十天,从商店 0 购买物品 0 ,开销为 values[0][0] * 10 = 100 。
所以总开销为 386 。
386 是购买所有 m * n 件物品的最大总开销。

说明:

  • 1 <= m == values.length <= 10
  • 1 <= n == values[i].length <= 10^4
  • 1 <= values[i][j] <= 10^6
  • values[i] 按照非递增顺序排序。

思路

m 个商店,每个商店里有 n 个商品。values[i][j] 表示商店 i 中商品 j 的价值,values[i] 非严格递减。从第一天开始,在第 d 天,我们可以选择一个商店 i,花费 d * values[i][j] 购买剩余的商品中价值中最小的商品 j。返回购买完所有商店的所有商品所需的最大开销。

要使开销最大,我们应该优先购买价值最小的商品,因为开销有天数加成,价值越大,在后面买翻倍后开销更大。由于每个商店里的商品是非严格递减的,每次从所有商店末尾取价值最小的商品,实际上就是按照所有商店的所有商品的价值从小到大取。

我们可以使用最小堆维护每个商店的最后一个商品,堆中元素记录商店编号 i,与商品下标 j

如果是求最小开销呢?

如果没有限制购买规则,显然先购买高价值商品花费最小。但是题目规定每一次任选一个商店从其剩余商品中的最后一个(剩余商品中价值最低的)商品购买。

我们应该从 左边 开始优先购买价值 最小 的商品,然后将顺序 反转 就得到了从右购买的序列,按照这个顺序计算购买的开销。

注意:不能从右边选最大的买,比如:

100000 90000 1
100    10    2

如果从右边取最大的,购买顺序为 2 10 100 1 90000 100000,这显然不是最小开销。而从左边取最小的买,顺序为 100 10 2 100000 90000 1,反转得到 1 90000 100000 2 10 100 按此顺序购买花费最小。

代码


/**
 * @date 2024-12-12 14:11
 */
public class MaxSpending2931 {

    /**
     * 13ms
     * 直接合并为一维数组后排序
     * 时间复杂度 O(mnlog(mn))
     * 执行快是因为不用维护堆以及操作堆中数据的值,每次计算的复杂度小于维护数据结构的复杂度
     */
    public long maxSpending_v2(int[][] values) {
        int m = values.length;
        int n = values[0].length;
        long res = 0L;
        long d = 1L;
        int[] v = new int[m * n];
        for (int i = 0; i < m; i++) {
            System.arraycopy(values[i], 0, v, i * n, n);
        }
        Arrays.sort(v);
        for (int i : v) {
            res += d++ * i;
        }
        return res;
    }

    /**
     * 37ms
     * 时间复杂度 O(mnlog(m))
     */
    public long maxSpending(int[][] values) {
        int m = values.length;
        int n = values[0].length;
        PriorityQueue<int[]> q = new PriorityQueue<>(m, (a, b) -> a[0] - b[0]);
        for (int i = 0; i < m; i++) {
            q.offer(new int[]{values[i][n - 1], i, n - 1});
        }
        long res = 0L;
        long d = 1L;
        while (!q.isEmpty()) {
            int[] goods = q.poll();
            int storeIndex = goods[1];
            res += d++ * goods[0];
            int p = --goods[2];
            if (p >= 0) {
                q.offer(new int[]{values[storeIndex][p], storeIndex, p});
            }
        }
        return res;
    }

}

性能

743.网络延迟时间

目标

有 n 个网络节点,标记为 1 到 n。

给你一个列表 times,表示信号经过 有向 边的传递时间。 times[i] = (ui, vi, wi),其中 ui 是源节点,vi 是目标节点, wi 是一个信号从源节点传递到目标节点的时间。

现在,从某个节点 K 发出一个信号。需要多久才能使所有节点都收到信号?如果不能使所有节点收到信号,返回 -1 。

示例 1:

输入:times = [[2,1,1],[2,3,1],[3,4,1]], n = 4, k = 2
输出:2

示例 2:

输入:times = [[1,2,1]], n = 2, k = 1
输出:1

示例 3:

输入:times = [[1,2,1]], n = 2, k = 2
输出:-1

说明:

  • 1 <= k <= n <= 100
  • 1 <= times.length <= 6000
  • times[i].length == 3
  • 1 <= ui, vi <= n
  • ui != vi
  • 0 <= wi <= 100
  • 所有 (ui, vi) 对都 互不相同(即,不含重复边)

思路

有一个 n 个节点的有向图 ,节点标记为 1 ~ n,求从其中某个节点 k 出发访问到所有其它节点的最短时间。

即从 k 出发求出到达所有其它节点的最短路径,然后取其中的最 值。

Floyd 算法的基本思想是动态规划。定义 dp[i][j] 表示从节点 i 到 节点 j 的最短路径,对于所有其它中间节点 m,更新 dp[i][j] = Math.min(dp[i][j], dp[i][m] + dp[m][j]),时间复杂度 O(n^3)。

如果 i -> j 有直接的通路则初始化 dp[i][j] 为路径的权值,否则为 INF

但是本题不需要其它起点的最短路径,因此可以使用 Dijkstra 算法、Bellman-Ford 算法 或者 SPFA 算法。

图的表示可以使用邻接矩阵、邻接表、前向星、链式前向星等结构。

代码


/**
 * @date 2024-11-25 9:08
 */
public class NetworkDelayTime743 {

    public int networkDelayTime(int[][] times, int n, int k) {
        int[][] dp = new int[n + 1][n + 1];
        for (int[] cost : dp) {
            Arrays.fill(cost, 20000);
        }
        for (int[] edge : times) {
            dp[edge[0]][edge[1]] = edge[2];
        }
        for (int m = 1; m <= n; m++) {
            for (int i = 1; i <= n; i++) {
                for (int j = 1; j <= n; j++) {
                    if (i == j || i == m || j == m) {
                        continue;
                    }
                    dp[i][j] = Math.min(dp[i][j], dp[i][m] + dp[m][j]);
                }
            }
        }
        int res = -1;
        for (int i = 1; i <= n; i++) {
            if (i == k) {
                continue;
            }
            res = Math.max(dp[k][i], res);
        }
        return res == 20000 ? -1 : res;
    }

}

性能

3244.新增道路查询后的最短距离II

目标

给你一个整数 n 和一个二维整数数组 queries。

有 n 个城市,编号从 0 到 n - 1。初始时,每个城市 i 都有一条单向道路通往城市 i + 1( 0 <= i < n - 1)。

queries[i] = [ui, vi] 表示新建一条从城市 ui 到城市 vi 的单向道路。每次查询后,你需要找到从城市 0 到城市 n - 1 的最短路径的长度。

所有查询中不会存在两个查询都满足 queries[i][0] < queries[j][0] < queries[i][1] < queries[j][1]

返回一个数组 answer,对于范围 [0, queries.length - 1] 中的每个 i,answer[i] 是处理完前 i + 1 个查询后,从城市 0 到城市 n - 1 的最短路径的长度。

示例 1:

输入: n = 5, queries = [[2, 4], [0, 2], [0, 4]]
输出: [3, 2, 1]
解释:
新增一条从 2 到 4 的道路后,从 0 到 4 的最短路径长度为 3。
新增一条从 0 到 2 的道路后,从 0 到 4 的最短路径长度为 2。
新增一条从 0 到 4 的道路后,从 0 到 4 的最短路径长度为 1。

示例 2:

输入: n = 4, queries = [[0, 3], [0, 2]]
输出: [1, 1]
解释:
新增一条从 0 到 3 的道路后,从 0 到 3 的最短路径长度为 1。
新增一条从 0 到 2 的道路后,从 0 到 3 的最短路径长度仍为 1。

说明:

  • 3 <= n <= 10^5
  • 1 <= queries.length <= 10^5
  • queries[i].length == 2
  • 0 <= queries[i][0] < queries[i][1] < n
  • 1 < queries[i][1] - queries[i][0]
  • 查询中不存在重复的道路。
  • 不存在两个查询都满足 i != j 且 queries[i][0] < queries[j][0] < queries[i][1] < queries[j][1]

思路

n 个城市,刚开始每一个城市 i 有一条单项道路通向城市 i + 1,有一个二维数组 queriesqueries[i] 表示增加一条从 queries[i][0]queries[i][1] 的单项道路,返回 answer 数组,answer[i] 表示增加了 queries[i] 之后从城市 0 到城市 n - 1 的最短路径。增加的路径保证不会出现相交的情况。

比昨天的题 3243.新增道路查询后的最短距离I 多了一个条件,新增的路径不会交叉。昨天首先考虑的就是今天这个思路,因为起点与终点是固定的,可以通过查询区间的关系来减小最短路径。当时考虑的差分数组解决不了区间包含的关系。

定义区间数组 intervalinterval[i] = j, 表示 i -> j 有直达道路。如果发现查询的路径 [l, r] 已经被包含,直接略过。否则,循环记录区间 (l, r) 内的路径数,保存下一跳的城市 next = interval[l],将 interval[l] 置为 r,l = next 直到 l >= r。注意最后一跳到 r 是没有计数的,相当于减去了将前面多余的步数,与直达的效果一样。 interval[l] = r 是第一次进入循环必须进行的操作,幸运的是它并不影响我们标记其它区间内的元素,当有更大的查询路径时,直接在 l 处就跳过了。

核心点是如何表示区间,如何判断区间是否重合,如何针对大区间减少的路径计数。

代码


/**
 * @date 2024-11-20 10:10
 */
public class ShortestDistanceAfterQueries3244 {

    public int[] shortestDistanceAfterQueries(int n, int[][] queries) {
        int ql = queries.length;
        int[] res = new int[ql];
        int[] interval = new int[n - 1];
        Arrays.setAll(interval, i -> i + 1);
        int shortestPath = n - 1;
        for (int i = 0; i < ql; i++) {
            int l = queries[i][0];
            int r = queries[i][1];
            while (interval[l] < r) {
                int next = interval[l];
                interval[l] = r;
                l = next;
                shortestPath--;
            }
            res[i] = shortestPath;
        }
        return res;
    }

}

性能

910.最小差值II

目标

给你一个整数数组 nums,和一个整数 k 。

对于每个下标 i(0 <= i < nums.length),将 nums[i] 变成 nums[i] + k 或 nums[i] - k 。

nums 的 分数 是 nums 中最大元素和最小元素的差值。

在更改每个下标对应的值之后,返回 nums 的最小 分数 。

示例 1:

输入:nums = [1], k = 0
输出:0
解释:分数 = max(nums) - min(nums) = 1 - 1 = 0 。

示例 2:

输入:nums = [0,10], k = 2
输出:6
解释:将数组变为 [2, 8] 。分数 = max(nums) - min(nums) = 8 - 2 = 6 。

示例 3:

输入:nums = [1,3,6], k = 3
输出:3
解释:将数组变为 [4, 6, 3] 。分数 = max(nums) - min(nums) = 6 - 3 = 3 。

说明:

  • 1 <= nums.length <= 10^4
  • 0 <= nums[i] <= 10^4
  • 0 <= k <= 10^4

思路

这道题与 908.最小差值I 的区别是对于每个元素操作是 必选的,增加或减少的值 固定k 而不是区间 [-k, k]

每个元素都可以取 nums[i] + knums[i] - k,如果枚举所有可能的数组,然后再取最大最小值差的最小值显然是不可能的。不考虑重复元素的情况下,可能的数组有 2^n 种。

可以先排序,增大小的值,减少大的值,枚举二者的边界,比如前 i 个元素 [0, i - 1]k,剩余元素减 k,这时上界为 Math.max(nums[i - 1] + k, nums[n - 1] - k) ,下界为 Math.min(nums[0] + k, nums[i] - k),取上下界的最小值即可。

代码


/**
 * @date 2024-10-21 9:57
 */
public class SmallestRangeII910 {

    public int smallestRangeII(int[] nums, int k) {
        Arrays.sort(nums);
        int n = nums.length;
        int res = nums[n - 1] - nums[0];
        for (int i = 1; i < n; i++) {
            int max = Math.max(nums[i - 1] + k, nums[n - 1] - k);
            int min = Math.min(nums[0] + k, nums[i] - k);
            res = Math.min(res, max - min);
        }
        return res;
    }

}

性能

871.最低加油次数

目标

汽车从起点出发驶向目的地,该目的地位于出发位置东面 target 英里处。

沿途有加油站,用数组 stations 表示。其中 stations[i] = [positioni, fueli] 表示第 i 个加油站位于出发位置东面 positioni 英里处,并且有 fueli 升汽油。

假设汽车油箱的容量是无限的,其中最初有 startFuel 升燃料。它每行驶 1 英里就会用掉 1 升汽油。当汽车到达加油站时,它可能停下来加油,将所有汽油从加油站转移到汽车中。

为了到达目的地,汽车所必要的最低加油次数是多少?如果无法到达目的地,则返回 -1 。

注意:如果汽车到达加油站时剩余燃料为 0,它仍然可以在那里加油。如果汽车到达目的地时剩余燃料为 0,仍然认为它已经到达目的地。

示例 1:

输入:target = 1, startFuel = 1, stations = []
输出:0
解释:可以在不加油的情况下到达目的地。

示例 2:

输入:target = 100, startFuel = 1, stations = [[10,100]]
输出:-1
解释:无法抵达目的地,甚至无法到达第一个加油站。

示例 3:

输入:target = 100, startFuel = 10, stations = [[10,60],[20,30],[30,30],[60,40]]
输出:2
解释:
出发时有 10 升燃料。
开车来到距起点 10 英里处的加油站,消耗 10 升燃料。将汽油从 0 升加到 60 升。
然后,从 10 英里处的加油站开到 60 英里处的加油站(消耗 50 升燃料),
并将汽油从 10 升加到 50 升。然后开车抵达目的地。
沿途在两个加油站停靠,所以返回 2 。

说明:

  • 1 <= target, startFuel <= 10^9
  • 0 <= stations.length <= 500
  • 1 <= positioni < positioni+1 < target
  • 1 <= fueli < 10^9

思路

已知汽车油耗为 1 英里/升,现在想要开车前往 target 英里外的目的地,出发时车中有 startFuel 升油,沿途有 stations.length 个加油站,stations[i][0] 表示加油站 i 距出发地的距离,stations[i][1] 表示加油站 i 的汽油总量。假设汽车油箱无限大,求到达目的地的最少加油次数,如果无法到达返回 -1

可以将出发点、加油站、目的地画到数轴上,由于油耗为 1 英里/升,有多少升油就可以到达多远的距离。那么问题转化为从 n 个加油站中选取最少的个数,使油箱油量大于等于 target。要使选择的加油站最少,那么应该首选油量多的加油站,前提是能够抵达该加油站。我们可以使用大顶堆维护加油站油量,将能够抵达的加油站放入其中,然后将堆顶的油加入油箱,将新的能够抵达的加油站放入堆中,直到油箱中的油量大于等于 target

代码


/**
 * @date 2024-10-07 21:09
 */
public class MinRefuelStops871 {

    public int minRefuelStops(int target, int startFuel, int[][] stations) {
        PriorityQueue<Integer> q = new PriorityQueue<>((a, b) -> b - a);
        int n = stations.length;
        int i = 0;
        int res = 0;
        int fuel = startFuel;
        while (fuel < target) {
            while (i < n && fuel >= stations[i][0]) {
                q.offer(stations[i++][1]);
            }
            if (!q.isEmpty()) {
                fuel += q.poll();
                res++;
                if (fuel >= target) {
                    return res;
                }
            } else if (i == n || fuel < stations[i][0]) {
                // 没有剩余的加油站或者无法抵达
                return -1;
            }
        }
        return res;
    }

}

性能

2207.字符串中最多数目的子序列

目标

给你一个下标从 0 开始的字符串 text 和另一个下标从 0 开始且长度为 2 的字符串 pattern ,两者都只包含小写英文字母。

你可以在 text 中任意位置插入 一个 字符,这个插入的字符必须是 pattern[0] 或者 pattern[1] 。注意,这个字符可以插入在 text 开头或者结尾的位置。

请你返回插入一个字符后,text 中最多包含多少个等于 pattern 的 子序列 。

子序列 指的是将一个字符串删除若干个字符后(也可以不删除),剩余字符保持原本顺序得到的字符串。

示例 1:

输入:text = "abdcdbc", pattern = "ac"
输出:4
解释:
如果我们在 text[1] 和 text[2] 之间添加 pattern[0] = 'a' ,那么我们得到 "abadcdbc" 。那么 "ac" 作为子序列出现 4 次。
其他得到 4 个 "ac" 子序列的方案还有 "aabdcdbc" 和 "abdacdbc" 。
但是,"abdcadbc" ,"abdccdbc" 和 "abdcdbcc" 这些字符串虽然是可行的插入方案,但是只出现了 3 次 "ac" 子序列,所以不是最优解。
可以证明插入一个字符后,无法得到超过 4 个 "ac" 子序列。

示例 2:

输入:text = "aabb", pattern = "ab"
输出:6
解释:
可以得到 6 个 "ab" 子序列的部分方案为 "aaabb" ,"aaabb" 和 "aabbb" 。

说明:

  • 1 <= text.length <= 10^5
  • pattern.length == 2
  • text 和 pattern 都只包含小写英文字母。

思路

已知字符串 pattern 包含两个小写英文字符,可以将其中的一个字符插入字符串 text,求插入后最多可以得到多少个个等于 pattern 的子序列。

刚开始想的是根据乘法原理来做,统计字符串中这两个字符出现的次数,cnt1cnt2,插入出现次数较小的字符,可以使子序列个数增加最多。子序列个数为 Math.max(cnt1, cnt2) * (Math.min(cnt1, cnt2) + 1)

但是这个想法忽略了这种情况,考虑字符串 mpmpcnt1 = 2, cnt2 = 2,但是子序列个数并不是 4,而是 3,需要减去第二个 m 之前的 p 的个数。我们不妨以 pattern[0] 为终点,统计它前面的 pattern[1] 的个数,将其从结果中减去。

其实这里还有一个漏洞,应该考虑 pattern 这两个字符相同的情况。c(n,2) = n(n - 1)/2,注意实际计算的时候应该将 n1,因为我们插入了一个字符,即 cnt1 * (cnt1 + 1) / 2

还是存在一个问题,比如 text 中的字符与 pattern 都是由一个相同的字符组成,那么 cnt1 最大值为 10^5,在乘法计算时就会溢出,需要使用long类型。

官网题解的思路是先计算不插入字符时子序列的个数,遇到 pattern[1] 就累加 pattern[0],同时记录二者的出现次数:

  • 如果 pattern[1] 出现次数更多,可以在开头插入 pattern[0] 使得子序列个数增加 pattern[1]
  • 如果 pattern[0] 出现次数更多,可以在结尾插入 pattern[1] 使得子序列个数增加 pattern[0]

最后直接将累加结果加上 Math.max(cnt1, cnt2) 即可。

代码


/**
 * @date 2024-09-24 8:54
 */
public class MaximumSubsequenceCount2207 {
    public long maximumSubsequenceCount(String text, String pattern) {
        long cnt1 = 0, cnt2 = 0;
        char c1 = pattern.charAt(0), c2 = pattern.charAt(1);
        char[] chars = text.toCharArray();
        long sub = 0;
        for (char c : chars) {
            if (c1 == c) {
                cnt1++;
                sub += cnt2;
            } else if (c2 == c) {
                cnt2++;
            }
        }
        if (c1 == c2) {
            return cnt1 * (cnt1 + 1) / 2;
        }
        return Math.max(cnt1, cnt2) * (Math.min(cnt1, cnt2) + 1) - sub;
    }

}

性能

LCP40.心算挑战

目标

「力扣挑战赛」心算项目的挑战比赛中,要求选手从 N 张卡牌中选出 cnt 张卡牌,若这 cnt 张卡牌数字总和为偶数,则选手成绩「有效」且得分为 cnt 张卡牌数字总和。 给定数组 cards 和 cnt,其中 cards[i] 表示第 i 张卡牌上的数字。 请帮参赛选手计算最大的有效得分。若不存在获取有效得分的卡牌方案,则返回 0。

示例 1:

输入:cards = [1,2,8,9], cnt = 3
输出:18
解释:选择数字为 1、8、9 的这三张卡牌,此时可获得最大的有效得分 1+8+9=18。

示例 2:

输入:cards = [3,3,1], cnt = 1
输出:0
解释:不存在获取有效得分的卡牌方案。

说明:

  • 1 <= cnt <= cards.length <= 10^5
  • 1 <= cards[i] <= 1000

思路

从数组中选cnt个元素,找出最大的偶数和,如果不存在偶数和返回0。

如果cnt为偶数,那么直接取最大的cnt个即可,如果cnt为奇数,取最大的cnt-1个,然后再从大到小判断和是否为偶数即可。 与cnt的奇偶性无关,它也不能保证cnt个元素相加和为偶数,必须要奇数元素的个数为偶数才行。

应该使用动态规划来做了,O(cnt * n)超出时间限制。

尝试根据数组元素奇偶性分组,然后分情况讨论如何选取。这里的限制条件是必须选取偶数个奇数元素,并且使得和最大。

这道题标成简单真的搞人心态啊,还是做的太少了。

代码

/**
 * @date 2024-08-01 20:24
 */
public class MaxmiumScoreLCP40 {

    public int maxmiumScore_v2(int[] cards, int cnt) {
        int res = 0;
        PriorityQueue<Integer> odd = new PriorityQueue<>((a, b) -> b - a);
        PriorityQueue<Integer> even = new PriorityQueue<>((a, b) -> b - a);
        // 将奇偶元素分组
        for (int card : cards) {
            if (card % 2 == 0) {
                even.offer(card);
            } else {
                odd.offer(card);
            }
        }
        // 如果只能选一个元素,那么直接从偶数队列取,否则返回0
        if (cnt == 1 && even.size() > 0) {
            return even.poll();
        } else if (cnt == 1) {
            return 0;
        }
        int oddCnt = 0;
        int lastOdd = 0;
        int lastEven = 0;
        // 从奇偶队列中,从大到小取cnt-1个,留一个后面分情况讨论
        while (cnt > 1) {
            if (!odd.isEmpty() && !even.isEmpty() && odd.peek() > even.peek()) {
                lastOdd = odd.poll();
                res += lastOdd;
                oddCnt++;
            } else if (!odd.isEmpty() && !even.isEmpty()) {
                lastEven = even.poll();
                res += lastEven;
            } else if (!odd.isEmpty()) {
                lastOdd = odd.poll();
                res += lastOdd;
                oddCnt++;
            } else if (!even.isEmpty()) {
                lastEven = even.poll();
                res += lastEven;
            }
            cnt--;
        }
        if (oddCnt % 2 == 1 && !odd.isEmpty()) {
            // 如果选择了奇数个奇数元素,并且奇数队列还有剩余元素
            if (even.size() >= 2) {
                // 如果偶数队列有超过两个,并且这两个之和大于最近已选的奇数元素+奇数队列的元素
                int tmp = even.poll();
                tmp += even.poll();
                if (tmp > lastOdd + odd.peek()) {
                    // 将上一个奇数选择删除,选两个偶数
                    res += tmp - lastOdd;
                } else {
                    // 否则选一个奇数
                    res += odd.poll();
                }
            } else {
                // 如果没有多余的偶数元素,直接选一个奇数
                res += odd.poll();
            }
        } else if (oddCnt % 2 == 1 && even.size() >= 2) {
            // 如果已经选了奇数个奇数元素,并且没有多余的奇数元素了,那么回退,选两个偶数
            res -= lastOdd;
            res += even.poll();
            res += even.poll();
        } else if (oddCnt % 2 == 0) {
            // 如果奇数元素选了偶数个,那么只需再选一个偶数,或者删掉一个偶数,选两个奇数
            if (odd.size() >= 2 && lastEven != 0) {
                // 如果奇数存在两个,且之前选过偶数
                int tmp = odd.poll();
                tmp += odd.poll();
                if (even.size() == 0 || tmp > lastEven + even.peek()) {
                    // 如果没有多余的偶数,或者两个奇数之和大于之前选择的偶数+偶数队列元素
                    // 选两个奇数
                    res += tmp - lastEven;
                } else {
                    // 选一个偶数
                    res += even.poll();
                }
            } else {
                // 如果没有多余2个的奇数了,只能选一个偶数
                if (even.size() == 0) {
                    return 0;
                } else {
                    res += even.poll();
                }
            }
        } else {
            // 如果已经选了奇数个奇数元素,并且没有多余的奇数元素了,并且偶数个数不足2个,返回0
            res = 0;
        }
        return res;
    }

}

性能

3111.覆盖所有点的最少矩形数目

目标

给你一个二维整数数组 point ,其中 points[i] = [xi, yi] 表示二维平面内的一个点。同时给你一个整数 w 。你需要用矩形 覆盖所有 点。

每个矩形的左下角在某个点 (x1, 0) 处,且右上角在某个点 (x2, y2) 处,其中 x1 <= x2 且 y2 >= 0 ,同时对于每个矩形都 必须 满足 x2 - x1 <= w 。

如果一个点在矩形内或者在边上,我们说这个点被矩形覆盖了。

请你在确保每个点都 至少 被一个矩形覆盖的前提下,最少 需要多少个矩形。

注意:一个点可以被多个矩形覆盖。

示例 1:

输入:points = [[2,1],[1,0],[1,4],[1,8],[3,5],[4,6]], w = 1
输出:2
解释:
上图展示了一种可行的矩形放置方案:
一个矩形的左下角在 (1, 0) ,右上角在 (2, 8) 。
一个矩形的左下角在 (3, 0) ,右上角在 (4, 8) 。

示例 2:

输入:points = [[0,0],[1,1],[2,2],[3,3],[4,4],[5,5],[6,6]], w = 2
输出:3
解释:
上图展示了一种可行的矩形放置方案:
一个矩形的左下角在 (0, 0) ,右上角在 (2, 2) 。
一个矩形的左下角在 (3, 0) ,右上角在 (5, 5) 。
一个矩形的左下角在 (6, 0) ,右上角在 (6, 6) 。

示例 3:

输入:points = [[2,3],[1,2]], w = 0
输出:2
解释:
上图展示了一种可行的矩形放置方案:
一个矩形的左下角在 (1, 0) ,右上角在 (1, 2) 。
一个矩形的左下角在 (2, 0) ,右上角在 (2, 3) 。

说明:

  • 1 <= points.length <= 10^5
  • points[i].length == 2
  • 0 <= xi == points[i][0] <= 10^9
  • 0 <= yi == points[i][1] <= 10^9
  • 0 <= w <= 10^9
  • 所有点坐标 (xi, yi) 互不相同。

思路

有一个二维数组表示平面中的点,给定一个整数w表示矩形x方向的最大宽度,矩形的左下角在x坐标轴上某个点,右上角在第一象限某点。问覆盖坐标平面上所有点最少需要多少个矩形,在矩形边上也算被覆盖了。

由于y轴方向没有限制,我们只考虑x轴方向,可以将坐标平面中的点按照x轴坐标大小排序,然后按w宽度分组计数即可。

最快的写法是将二维坐标转成了一维的x坐标,排序只需要寻址一次。

代码

/**
 * @date 2024-07-31 9:12
 */
public class MinRectanglesToCoverPoints3111 {

    public int minRectanglesToCoverPoints_v1(int[][] points, int w) {
        int n = points.length;
        int[] x = new int[n];
        for (int i = 0; i < n; i++) {
            x[i] = points[i][0];
        }
        Arrays.sort(x);
        int res = 0;
        int r = -1;
        for (int i = 0; i < n; i++) {
            if (x[i] > r) {
                res++;
                r = x[i] + w;
            }
        }
        return res;
    }

    public int minRectanglesToCoverPoints(int[][] points, int w) {
        Arrays.sort(points, (a, b) -> a[0] - b[0]);
        int n = points.length;
        int res = 0;
        int l = 0;
        for (int i = 1; i < n; i++) {
            if (points[i][0] - points[l][0] > w) {
                res++;
                l = i;
            }
        }
        // res 计数的是之前的矩形个数,+1表示将最后的矩形计入
        return res + 1;
    }
}

性能

3106.满足距离约束且字典序最小的字符串

目标

给你一个字符串 s 和一个整数 k 。

定义函数 distance(s1, s2) ,用于衡量两个长度为 n 的字符串 s1 和 s2 之间的距离,即:

  • 字符 'a' 到 'z' 按 循环 顺序排列,对于区间 [0, n - 1] 中的 i ,计算所有「 s1[i] 和 s2[i] 之间 最小距离」的 和 。

例如,distance("ab", "cd") == 4 ,且 distance("a", "z") == 1 。

你可以对字符串 s 执行 任意次 操作。在每次操作中,可以将 s 中的一个字母 改变 为 任意 其他小写英文字母。

返回一个字符串,表示在执行一些操作后你可以得到的 字典序最小 的字符串 t ,且满足 distance(s, t) <= k 。

示例 1:

输入:s = "zbbz", k = 3
输出:"aaaz"
解释:在这个例子中,可以执行以下操作:
将 s[0] 改为 'a' ,s 变为 "abbz" 。
将 s[1] 改为 'a' ,s 变为 "aabz" 。
将 s[2] 改为 'a' ,s 变为 "aaaz" 。
"zbbz" 和 "aaaz" 之间的距离等于 k = 3 。
可以证明 "aaaz" 是在任意次操作后能够得到的字典序最小的字符串。
因此,答案是 "aaaz" 。

示例 2:

输入:s = "xaxcd", k = 4
输出:"aawcd"
解释:在这个例子中,可以执行以下操作:
将 s[0] 改为 'a' ,s 变为 "aaxcd" 。
将 s[2] 改为 'w' ,s 变为 "aawcd" 。
"xaxcd" 和 "aawcd" 之间的距离等于 k = 4 。
可以证明 "aawcd" 是在任意次操作后能够得到的字典序最小的字符串。
因此,答案是 "aawcd" 。

示例 3:

输入:s = "lol", k = 0
输出:"lol"
解释:在这个例子中,k = 0,更改任何字符都会使得距离大于 0 。
因此,答案是 "lol" 。

说明:

  • 1 <= s.length <= 100
  • 0 <= k <= 2000
  • s 只包含小写英文字母。

思路

定义两个由 a - z 组成的长度相等字符串之间的距离为相应位置上字母的最小距离,比如 az 的最小距离为1,ao 之间的距离为12,而非14,an 的距离为13,即距离的最大值为13。因此两字符相减 diff 如果大于13,应取 26 - diff

a b c d e f g h i j k l m

n o p q r s t u v w x y z

现在给定k表示字符串距离,需要我们找出与给定字符串距离为k的字典序最小的字符串。所谓字典序,先取第一个字符比较它在字母表中的排序,如果相等则比较下一个。

因此我们可以遍历给定的字符串的每一个字符,在距离范围内优先将其改为 a,如果距离还有多余,则继续改下一个字母,如果距离不够直接向下取剩余距离的字符。由于字母是首尾相接的,需要考虑是从哪边计算距离最短。第一行从前计算,第二行从后计算,或者两者都计算,比较大小决定如何处理。

代码

/**
 * @date 2024-07-27 22:22
 */
public class GetSmallestString3106 {

    public String getSmallestString(String s, int k) {
        char[] chars = s.toCharArray();
        int n = s.length();
        int i = 0;
        while (k > 0 && i < n) {
            char c = chars[i];
            int dist = Math.min('z' - c + 1, c - 'a');
            if (k < dist) {
                chars[i] = (char) (c - k);
                break;
            }
            chars[i] = 'a';
            k -= dist;
            i++;
        }

        return new String(chars);
    }

}

性能