2920.收集所有金币可获得的最大积分

目标

有一棵由 n 个节点组成的无向树,以 0 为根节点,节点编号从 0 到 n - 1 。给你一个长度为 n - 1 的二维 整数 数组 edges ,其中 edges[i] = [ai, bi] 表示在树上的节点 ai 和 bi 之间存在一条边。另给你一个下标从 0 开始、长度为 n 的数组 coins 和一个整数 k ,其中 coins[i] 表示节点 i 处的金币数量。

从根节点开始,你必须收集所有金币。要想收集节点上的金币,必须先收集该节点的祖先节点上的金币。

节点 i 上的金币可以用下述方法之一进行收集:

  • 收集所有金币,得到共计 coins[i] - k 点积分。如果 coins[i] - k 是负数,你将会失去 abs(coins[i] - k) 点积分。
  • 收集所有金币,得到共计 floor(coins[i] / 2) 点积分。如果采用这种方法,节点 i 子树中所有节点 j 的金币数 coins[j] 将会减少至 floor(coins[j] / 2) 。

返回收集 所有 树节点的金币之后可以获得的最大积分。

示例 1:

输入:edges = [[0,1],[1,2],[2,3]], coins = [10,10,3,3], k = 5
输出:11
解释:
使用第一种方法收集节点 0 上的所有金币。总积分 = 10 - 5 = 5 。
使用第一种方法收集节点 1 上的所有金币。总积分 = 5 + (10 - 5) = 10 。
使用第二种方法收集节点 2 上的所有金币。所以节点 3 上的金币将会变为 floor(3 / 2) = 1 ,总积分 = 10 + floor(3 / 2) = 11 。
使用第二种方法收集节点 3 上的所有金币。总积分 =  11 + floor(1 / 2) = 11.
可以证明收集所有节点上的金币能获得的最大积分是 11 。 

示例 2:

输入:edges = [[0,1],[0,2]], coins = [8,4,4], k = 0
输出:16
解释:
使用第一种方法收集所有节点上的金币,因此,总积分 = (8 - 0) + (4 - 0) + (4 - 0) = 16 。

说明:

  • n == coins.length
  • 2 <= n <= 10^5
  • 0 <= coins[i] <= 10^4
  • edges.length == n - 1
  • 0 <= edges[i][0], edges[i][1] < n
  • 0 <= k <= 10^4

提示:

  • Let dp[x][t] be the maximum points we can get from the subtree rooted at node x and the second operation has been used t times in its ancestors.
  • Note that the value of each node <= 104, so when t >= 14 dp[x][t] is always 0.
  • General equation will be: dp[x][t] = max((coins[x] >> t) - k + sigma(dp[y][t]), (coins[x] >> (t + 1)) + sigma(dp[y][t + 1])) where nodes denoted by y in the sigma, are the direct children of node x.

思路

定义 dp[x][t] 表示 x 的祖先节点已经使用了 t 次方法二,当前节点及其子树所能获得的最大积分,最终结果为 dp[0][0]。状态转移方程为 dp[x][t] = Math.max(Σdp[c][t] + (coins[x] >> t) - k, Σdp[c][t + 1] + (coins[x] >> (t + 1))),其中 c 是当前节点的直接孩子节点。

代码


/**
 * @date 2025-01-23 10:20
 */
public class MaximumPoints2920 {

    public int maximumPoints(int[][] edges, int[] coins, int k) {
        int n = coins.length;
        // 建图
        List<Integer>[] g = new ArrayList[n];
        for (int i = 0; i < n; i++) {
            g[i] = new ArrayList<>();
        }
        for (int[] edge : edges) {
            int from = edge[0];
            int to = edge[1];
            g[from].add(to);
            g[to].add(from);
        }
        // bfs 获得层级结构
        List<List<Integer>> depthNodes = new ArrayList<>();
        List<Integer> l = new ArrayList<>();
        l.add(0);
        depthNodes.add(l);
        Queue<Integer> q = new ArrayDeque<>();
        q.offer(0);
        while (!q.isEmpty()) {
            int size = q.size();
            List<Integer> list = new ArrayList<>();
            for (int i = 0; i < size; i++) {
                Integer from = q.poll();
                for (Integer to : g[from]) {
                    list.add(to);
                    g[to].removeIf(next -> next.equals(from));
                }
            }
            if (list.size() > 0) {
                depthNodes.add(list);
                q.addAll(list);
            }
        }
        // 初始化叶子节点的分数
        int[][] dp = new int[n][15];
        int depth = depthNodes.size();
        for (Integer leaf : depthNodes.get(depth - 1)) {
            for (int t = 0; t < 14; t++) {
                dp[leaf][t] = Math.max((coins[leaf] >> t) - k, (coins[leaf] >> (t + 1)));
            }
        }
        // 自底向上计算子树分数和的最大值
        for (int d = depth - 2; d >= 0; d--) {
            List<Integer> nodes = depthNodes.get(d);
            for (Integer cur : nodes) {
                for (int t = 0; t < 14; t++) {
                    int sum0 = 0;
                    int sum1 = 0;
                    for (Integer child : g[cur]) {
                        sum0 += dp[child][t];
                        sum1 += dp[child][t + 1];
                    }
                    dp[cur][t] = Math.max(sum0 + (coins[cur] >> t) - k, sum1 + (coins[cur] >> (t + 1)));
                }
            }
        }
        return dp[0][0];
    }

}

性能

2218.从栈中取出K个硬币的最大面值和

目标

一张桌子上总共有 n 个硬币 栈 。每个栈有 正整数 个带面值的硬币。

每一次操作中,你可以从任意一个栈的 顶部 取出 1 个硬币,从栈中移除它,并放入你的钱包里。

给你一个列表 piles ,其中 piles[i] 是一个整数数组,分别表示第 i 个栈里 从顶到底 的硬币面值。同时给你一个正整数 k ,请你返回在 恰好 进行 k 次操作的前提下,你钱包里硬币面值之和 最大为多少 。

示例 1:

输入:piles = [[1,100,3],[7,8,9]], k = 2
输出:101
解释:
上图展示了几种选择 k 个硬币的不同方法。
我们可以得到的最大面值为 101 。

示例 2:

输入:piles = [[100],[100],[100],[100],[100],[100],[1,1,1,1,1,1,700]], k = 7
输出:706
解释:
如果我们所有硬币都从最后一个栈中取,可以得到最大面值和。

说明:

  • n == piles.length
  • 1 <= n <= 1000
  • 1 <= piles[i][j] <= 10^5
  • 1 <= k <= sum(piles[i].length) <= 2000

思路

有 n 个栈,栈中有 piles[i].length 个硬币,每次操作可以从任意栈顶取一个硬币,求 k 次操作取得的硬币和的最大值。

定义 dp[i][j] 表示 从前 i + 1 个栈中最多取 j 个 所能得到的最大值,与 0 - 1 背包不同的是我们需要枚举从当前栈取 1 至 x 枚硬币的最大值,状态转移方程为 dp[i][j] = Math.max(dp[i][j], Math.max(dp[i - 1][j], dp[i - 1][j - x] + prefix[i][x])),外层 max 求的是当前栈每种取法的最大值,第二个参数的 max 求的是当前栈 不取 或者 取 x 枚硬币对应的最大值。由于是从栈顶取,我们使用前缀和 prefix 记录每个栈从栈顶到底的硬币和。

代码


/**
 * @date 2025-01-21 13:46
 */
public class MaxValueOfCoins2218 {

    public int maxValueOfCoins(List<List<Integer>> piles, int k) {
        int n = piles.size();
        int[][] prefix = new int[n][];
        // 计算前缀和
        for (int i = 0; i < n; i++) {
            List<Integer> pile = piles.get(i);
            prefix[i] = new int[pile.size() + 1];
            for (int j = 1; j <= pile.size(); j++) {
                prefix[i][j] = prefix[i][j - 1] + pile.get(j - 1);
            }
        }
        // 定义dp[i][j] 表示 从前 i + 1 个栈中最多取 j 个 所能得到的最大值
        int[][] dp = new int[n][k + 1];
        // 初始化,从第一个栈最多取 k 个
        for (int j = 1; j <= k; j++) {
            int length = piles.get(0).size();
            if (j <= length) {
                dp[0][j] = prefix[0][j];
            } else {
                dp[0][j] = dp[0][length];
            }
        }
        for (int i = 1; i < n; i++) {
            // 枚举右边界
            int length = piles.get(i).size();
            for (int j = 1; j <= k; j++) {
                // j 表示从前 i + 1 个栈中总共取 j 个
                for (int x = 1; x <= length && j >= x; x++) {
                    // 表示从当前栈中取 x 个
                    dp[i][j] = Math.max(dp[i][j], Math.max(dp[i - 1][j], dp[i - 1][j - x] + prefix[i][x]));
                }
            }
        }
        return dp[n - 1][k];
    }

}

性能

3287.求出数组中最大序列值

目标

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

定义长度为 2 * x 的序列 seq 的 值 为:

  • (seq[0] OR seq[1] OR ... OR seq[x - 1]) XOR (seq[x] OR seq[x + 1] OR ... OR seq[2 * x - 1]).

请你求出 nums 中所有长度为 2 * k 的 子序列 的 最大值 。

示例 1:

输入:nums = [2,6,7], k = 1
输出:5
解释:
子序列 [2, 7] 的值最大,为 2 XOR 7 = 5 。

示例 2:

输入:nums = [4,2,5,6,7], k = 2
输出:2
解释:
子序列 [4, 5, 6, 7] 的值最大,为 (4 OR 5) XOR (6 OR 7) = 2 。

说明:

  • 2 <= nums.length <= 400
  • 1 <= nums[i] < 27
  • 1 <= k <= nums.length / 2

思路

// todo

代码

性能

3298.统计重新排列后包含另一个字符串的子字符串数目II

目标

给你两个字符串 word1 和 word2 。

如果一个字符串 x 重新排列后,word2 是重排字符串的 前缀,那么我们称字符串 x 是 合法的 。

请你返回 word1 中 合法 子字符串 的数目。

注意 ,这个问题中的内存限制比其他题目要 小 ,所以你 必须 实现一个线性复杂度的解法。

示例 1:

输入:word1 = "bcca", word2 = "abc"
输出:1
解释:
唯一合法的子字符串是 "bcca" ,可以重新排列得到 "abcc" ,"abc" 是它的前缀。

示例 2:

输入:word1 = "abcabc", word2 = "abc"
输出:10
解释:
除了长度为 1 和 2 的所有子字符串都是合法的。

示例 3:

输入:word1 = "abcabc", word2 = "aaabc"
输出:0

说明:

  • 1 <= word1.length <= 10^6
  • 1 <= word2.length <= 10^4
  • word1 和 word2 都只包含小写英文字母。

思路

参考 3297.统计重新排列后包含另一个字符串的子字符串数目I,本题内存限制小,必须使用线性复杂度的解法。

代码


/**
 * @date 2025-01-09 14:28
 */
public class ValidSubstringCount3297 {

    public long validSubstringCount_v1(String word1, String word2) {
        int[] cnt1 = new int[26];
        int[] cnt2 = new int[26];
        char[] chars1 = word1.toCharArray();
        char[] chars2 = word2.toCharArray();
        for (char c : chars1) {
            cnt1[c - 'a']++;
        }
        for (char c : chars2) {
            cnt2[c - 'a']++;
        }
        for (int i = 0; i < 26; i++) {
            if (cnt1[i] < cnt2[i]) {
                return 0;
            }
        }
        int n = word1.length();
        int r = n - 1;
        while (--cnt1[chars1[r] - 'a'] >= cnt2[chars1[r] - 'a']) {
            r--;
        }
        long res = n - r;
        cnt1[chars1[r++] - 'a']++;
        for (int i = 0; i < n - word2.length(); i++) {
            int c = chars1[i] - 'a';
            cnt1[c]--;
            while (r < n && cnt1[c] < cnt2[c]) {
                cnt1[chars1[r++] - 'a']++;
            }
            if (cnt1[c] >= cnt2[c]) {
                res += n - r + 1;
            } else {
                break;
            }
        }
        return res;
    }

}

性能

3219.切蛋糕的最小总开销II

目标

有一个 m x n 大小的矩形蛋糕,需要切成 1 x 1 的小块。

给你整数 m ,n 和两个数组:

  • horizontalCut 的大小为 m - 1 ,其中 horizontalCut[i] 表示沿着水平线 i 切蛋糕的开销。
  • verticalCut 的大小为 n - 1 ,其中 verticalCut[j] 表示沿着垂直线 j 切蛋糕的开销。

一次操作中,你可以选择任意不是 1 x 1 大小的矩形蛋糕并执行以下操作之一:

  1. 沿着水平线 i 切开蛋糕,开销为 horizontalCut[i] 。
  2. 沿着垂直线 j 切开蛋糕,开销为 verticalCut[j] 。

每次操作后,这块蛋糕都被切成两个独立的小蛋糕。

每次操作的开销都为最开始对应切割线的开销,并且不会改变。

请你返回将蛋糕全部切成 1 x 1 的蛋糕块的 最小 总开销。

示例 1:

输入:m = 3, n = 2, horizontalCut = [1,3], verticalCut = [5]
输出:13
解释:
沿着垂直线 0 切开蛋糕,开销为 5 。
沿着水平线 0 切开 3 x 1 的蛋糕块,开销为 1 。
沿着水平线 0 切开 3 x 1 的蛋糕块,开销为 1 。
沿着水平线 1 切开 2 x 1 的蛋糕块,开销为 3 。
沿着水平线 1 切开 2 x 1 的蛋糕块,开销为 3 。
总开销为 5 + 1 + 1 + 3 + 3 = 13 。

示例 2:

输入:m = 2, n = 2, horizontalCut = [7], verticalCut = [4]
输出:15
解释:
沿着水平线 0 切开蛋糕,开销为 7 。
沿着垂直线 0 切开 1 x 2 的蛋糕块,开销为 4 。
沿着垂直线 0 切开 1 x 2 的蛋糕块,开销为 4 。
总开销为 7 + 4 + 4 = 15 。

说明:

  • 1 <= m, n <= 10^5
  • horizontalCut.length == m - 1
  • verticalCut.length == n - 1
  • 1 <= horizontalCut[i], verticalCut[i] <= 10^3

思路

有一块 m x n 的蛋糕,horizontalCut[i] 表示水平切第 i 行的开销,verticalCut[i] 表示垂直切第 i 列的开销。求将蛋糕切成 1 x 1 小块的最小代价。

切蛋糕的最小总开销I 相比数据范围扩大了,返回值是 long 型。

代码


/**
 * @date 2024-12-26 16:38
 */
public class MinimumCost3219 {

    public long minimumCost(int m, int n, int[] horizontalCut, int[] verticalCut) {
        int horizontalPart = m;
        int verticalPart = n;
        Arrays.sort(horizontalCut);
        Arrays.sort(verticalCut);
        int h = 0;
        int v = 0;
        long res = 0;
        while (h < m - 1 || v < n - 1) {
            int hcost = h >= m - 1 ? Integer.MAX_VALUE : horizontalCut[h];
            int vcost = v >= n - 1 ? Integer.MAX_VALUE : verticalCut[v];
            if (hcost < vcost) {
                res += verticalPart * hcost;
                horizontalPart--;
                h++;
            } else {
                res += horizontalPart * vcost;
                verticalPart--;
                v++;
            }
        }
        return res;
    }

}

性能

3292.形成目标字符串需要的最少字符串数II

目标

给你一个字符串数组 words 和一个字符串 target。

如果字符串 x 是 words 中 任意 字符串的 前缀,则认为 x 是一个 有效 字符串。

现计划通过 连接 有效字符串形成 target ,请你计算并返回需要连接的 最少 字符串数量。如果无法通过这种方式形成 target,则返回 -1。

示例 1:

输入: words = ["abc","aaaaa","bcdef"], target = "aabcdabc"
输出: 3
解释:
target 字符串可以通过连接以下有效字符串形成:
words[1] 的长度为 2 的前缀,即 "aa"。
words[2] 的长度为 3 的前缀,即 "bcd"。
words[0] 的长度为 3 的前缀,即 "abc"。

示例 2:

输入: words = ["abababab","ab"], target = "ababaababa"
输出: 2
解释:
target 字符串可以通过连接以下有效字符串形成:
words[0] 的长度为 5 的前缀,即 "ababa"。
words[0] 的长度为 5 的前缀,即 "ababa"。

示例 3:

输入: words = ["abcdef"], target = "xyz"
输出: -1

说明:

  • 1 <= words.length <= 100
  • 1 <= words[i].length <= 5 * 10^4
  • 输入确保 sum(words[i].length) <= 10^5.
  • words[i] 只包含小写英文字母。
  • 1 <= target.length <= 5 * 10^4
  • target 只包含小写英文字母。

思路

// todo

代码

性能

1847.最近的房间

目标

一个酒店里有 n 个房间,这些房间用二维整数数组 rooms 表示,其中 rooms[i] = [roomIdi, sizei] 表示有一个房间号为 roomIdi 的房间且它的面积为 sizei 。每一个房间号 roomIdi 保证是 独一无二 的。

同时给你 k 个查询,用二维数组 queries 表示,其中 queries[j] = [preferredj, minSizej] 。第 j 个查询的答案是满足如下条件的房间 id :

  • 房间的面积 至少 为 minSizej ,且
  • abs(id - preferredj) 的值 最小 ,其中 abs(x) 是 x 的绝对值。

如果差的绝对值有 相等 的,选择 最小 的 id 。如果 没有满足条件的房间 ,答案为 -1 。

请你返回长度为 k 的数组 answer ,其中 answer[j] 为第 j 个查询的结果。

示例 1:

输入:rooms = [[2,2],[1,2],[3,2]], queries = [[3,1],[3,3],[5,2]]
输出:[3,-1,3]
解释:查询的答案如下:
查询 [3,1] :房间 3 的面积为 2 ,大于等于 1 ,且号码是最接近 3 的,为 abs(3 - 3) = 0 ,所以答案为 3 。
查询 [3,3] :没有房间的面积至少为 3 ,所以答案为 -1 。
查询 [5,2] :房间 3 的面积为 2 ,大于等于 2 ,且号码是最接近 5 的,为 abs(3 - 5) = 2 ,所以答案为 3 。

示例 2:

输入:rooms = [[1,4],[2,3],[3,5],[4,1],[5,2]], queries = [[2,3],[2,4],[2,5]]
输出:[2,1,3]
解释:查询的答案如下:
查询 [2,3] :房间 2 的面积为 3 ,大于等于 3 ,且号码是最接近的,为 abs(2 - 2) = 0 ,所以答案为 2 。
查询 [2,4] :房间 1 和 3 的面积都至少为 4 ,答案为 1 因为它房间编号更小。
查询 [2,5] :房间 3 是唯一面积大于等于 5 的,所以答案为 3 。

说明:

  • n == rooms.length
  • 1 <= n <= 10^5
  • k == queries.length
  • 1 <= k <= 10^4
  • 1 <= roomIdi, preferredj <= 10^7
  • 1 <= sizei, minSizej <= 10^7

思路

有一个数组 roomsrooms[i][0] 表示第 i 个房间编号,房间编号不重复,rooms[i][1] 表示第 i 个房间大小。有一个查询数组 queriesqueries[j][0] 表示第 j 个查询期望的房间编号queries[j][1] 表示第 j 个查询最小的房间大小。返回查询数组对应的结果数组,查询结果为房间编号,该房间的面积至少为 queries[j][1],且房间编号与 queries[j][0] 的距离最小,如果存在距离相等的情况,取房间编号最小的。

首先按房间大小排序,大小相同的按编号排序。对于每个查询首先二分查找出第一个大于 queries[j][1] 的房间在数组中的位置,接下来需要从该位置往后计算距离 queries[j][0] 最近的房间编号。

// todo 官网题解 Bentley Ottmann, Sparse Table 倍增 RMQ,Range Maximum/Minimum Query

代码


/**
 * @date 2024-12-16 16:23
 */
public class ClosestRoom1847 {

    public int[] closestRoom(int[][] rooms, int[][] queries) {
        Arrays.sort(rooms, (a, b) -> {
            int compare = a[1] - b[1];
            if (compare != 0) {
                return compare;
            }
            return a[0] - b[0];
        });
        int n = rooms.length;
        int k = queries.length;
        int[] res = new int[k];
        int i = 0;
        for (int[] query : queries) {
            int minAreaRoomIndex = lowerBound(rooms, 0, n - 1, query[1]);
            if (minAreaRoomIndex == n) {
                res[i++] = -1;
                continue;
            }
            int dist = Integer.MAX_VALUE;
            int roomId = Integer.MAX_VALUE;
            for (int j = minAreaRoomIndex; j < n; j++) {
                int tmp = Math.abs(query[0] - rooms[j][0]);
                if (tmp < dist) {
                    dist = tmp;
                    roomId = rooms[j][0];
                } else if (tmp == dist) {
                    roomId = Math.min(rooms[j][0], roomId);
                }
            }
            res[i++] = roomId;
        }

        return res;
    }

    public int lowerBound(int[][] rooms, int l, int r, int target) {
        int m = l + ((r - l) >> 1);
        while (l <= r) {
            if (rooms[m][1] >= target) {
                r = m - 1;
            } else {
                l = m + 1;
            }
            m = l + ((r - l) >> 1);
        }
        return l;
    }

}

性能

3266.K次乘运算后的最终数组II

目标

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

你需要对 nums 执行 k 次操作,每次操作中:

  • 找到 nums 中的 最小 值 x ,如果存在多个最小值,选择最 前面 的一个。
  • 将 x 替换为 x * multiplier 。

k 次操作以后,你需要将 nums 中每一个数值对 10^9 + 7 取余。

请你返回执行完 k 次乘运算以及取余运算之后,最终的 nums 数组。

示例 1:

输入:nums = [2,1,3,5,6], k = 5, multiplier = 2
输出:[8,4,6,5,6]
解释:
操作 结果
1 次操作后 [2, 2, 3, 5, 6]
2 次操作后 [4, 2, 3, 5, 6]
3 次操作后 [4, 4, 3, 5, 6]
4 次操作后 [4, 4, 6, 5, 6]
5 次操作后 [8, 4, 6, 5, 6]
取余操作后 [8, 4, 6, 5, 6]

示例 2:

输入:nums = [100000,2000], k = 2, multiplier = 1000000
输出:[999999307,999999993]
解释:
操作 结果
1 次操作后 [100000, 2000000000]
2 次操作后 [100000000000, 2000000000]
取余操作后 [999999307, 999999993]

说明:

  • 1 <= nums.length <= 10^4
  • 1 <= nums[i] <= 10^9
  • 1 <= k <= 10^9
  • 1 <= multiplier <= 10^6

思路

有一个数组 nums,我们需要执行 k 次操作,每次操作选择数组中最小元素 min,并将它的值替换为 min * multiplier,返回最终的数组。数据范围比 3264.K次乘运算后的最终数组I 大,multiplier 也大,会溢出,需要进行取余运算。

首先 k 最大 10^9,还沿用昨天模拟的解法会超时。更重要的是,由于乘积很大,我们只能在队列中保存取余后的数据,如果还按找之前模拟来取最小元素就不对了。

我们发现,当执行一些次操作之后,所有元素都会被乘以 multiplier,当 k / n 比较大时,我们可以使用快速幂先计算出 multiplierk/n 幂,然后再与元素相乘。

关键在于何时开始使用上面的思路来计算,考虑 1 2 4 8 16multiplier2,k 为 20

2   2   4   8   16
4   2   4   8   16
4   4   4   8   16
8   4   4   8   16
8   8   4   8   16
8   8   8   8   16
16  8   8   8   16
16  16  8   8   16
16  16  16  8   16
16  16  16  16  16

可以发现 当前数组 最小值 乘以 multiplier 大于 原数组 元素的 最大值 时,后面再乘以 multiplier 就是每一个元素执行一次了。

因此我们需要先使用堆模拟前面几次操作,直到满足上面的条件。注意:堆中数据不能取模,满足条件之前堆中数据使用 long 型不会溢出。

代码


/**
 * @date 2024-12-14 10:31
 */
public class GetFinalState3266 {

    public int[] getFinalState(int[] nums, int k, int multiplier) {
        if (multiplier == 1) {
            return nums;
        }
        int mod = 1000000007;
        int n = nums.length;
        long[] mul = new long[n];
        for (int i = 0; i < n; i++) {
            mul[i] = nums[i];
        }
        PriorityQueue<Integer> q = new PriorityQueue<>((a, b) -> {
            long compare = mul[a] - mul[b];
            if (compare != 0) {
                return (int) compare;
            }
            return a - b;
        });
        long max = 0;
        for (int i = 0; i < n; i++) {
            q.offer(i);
            max = Math.max(max, nums[i]);
        }
        int i = 0;
        for (; i < k; i++) {
            if (mul[q.peek()] * (long) multiplier > max) {
                break;
            }
            Integer index = q.poll();
            mul[index] = mul[index] * multiplier;
            q.offer(index);
        }
        int remainder = k - i;
        if (remainder >= n) {
            long pow = pow(multiplier, remainder / n);
            for (int j = 0; j < n; j++) {
                Integer index = q.poll();
                int rem = remainder % n;
                mul[index] = (int) ((mul[index] * pow % mod * (j < rem ? (long) multiplier : 1)) % mod);
            }
        } else {
            for (int j = 0; j < remainder; j++) {
                Integer index = q.poll();
                mul[index] = (int) ((mul[index] * (long) multiplier) % mod);
                q.offer(index);
            }
        }
        for (int j = 0; j < n; j++) {
            nums[j] = (int) mul[j];
        }
        return nums;
    }

    public long pow(long base, int exponent) {
        long res = 1;
        int mod = 1000000007;
        while (exponent > 0) {
            if ((exponent & 1) == 1) {
                res = res * base % mod;
            }
            base = base * base % mod;
            exponent >>= 1;
        }
        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;
    }

}

性能

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;
    }

}

性能