3254.长度为K的子数组的能量值I

目标

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

一个数组的 能量值 定义为:

  • 如果 所有 元素都是依次 连续 且 上升 的,那么能量值为 最大 的元素。
  • 否则为 -1 。

你需要求出 nums 中所有长度为 k 的 子数组 的能量值。

请你返回一个长度为 n - k + 1 的整数数组 results ,其中 results[i] 是子数组 nums[i..(i + k - 1)] 的能量值。

示例 1:

输入:nums = [1,2,3,4,3,2,5], k = 3
输出:[3,4,-1,-1,-1]
解释:
nums 中总共有 5 个长度为 3 的子数组:
[1, 2, 3] 中最大元素为 3 。
[2, 3, 4] 中最大元素为 4 。
[3, 4, 3] 中元素 不是 连续的。
[4, 3, 2] 中元素 不是 上升的。
[3, 2, 5] 中元素 不是 连续的。

示例 2:

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

示例 3:

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

说明:

  • 1 <= n == nums.length <= 500
  • 1 <= nums[i] <= 10^5
  • 1 <= k <= n

思路

有一个整数数组 nums,如果其子数组中的元素是连续且递增的,即公差为 1 的数列,定义子数组的能量值为子数组的最大元素,否则能量为 -1。返回所有长度为 k 的子数组的能量值。

显然需要使用滑动窗口,关键在于元素移入移出后如何判断是否连续且递增。我们可以使用队列记录不满足规则元素的 前一个 元素下标。当元素移入窗口时,判断元素与前一个元素是否满足规则,不满足则将前一个元素下标加入队列。窗口滑动时,先判断被移出的元素下标是否与队首相同,如果相同将队首元素删除。最后判断队列是否为空,如果为空则能量为新加入的元素,否则为 -1

官网题解使用一个计数器记录连续递增的元素个数,初始化结果数组元素为 -1,当计数器大于等于 k 时,记录 i - k + 1 位置上的能量值为当前元素,否则将计数器重置为 1。

代码


/**
 * @date 2024-11-06 5:50
 */
public class ResultsArray3254 {

    public int[] resultsArray(int[] nums, int k) {
        int n = nums.length;
        if (k == 1) {
            return nums;
        }
        int[] res = new int[n - k + 1];
        Queue<Integer> q = new ArrayDeque<>();
        int l = 0, i = 0;
        for (int r = 1; r < n; r++) {
            int prev = r - 1;
            if (nums[r] - nums[prev] != 1) {
                q.offer(prev);
            }
            if (r - l + 1 > k) {
                if (!q.isEmpty() && l == q.peek()) {
                    q.poll();
                }
                l++;
            }
            if (r - l + 1 == k) {
                if (q.isEmpty()) {
                    res[i++] = nums[r];
                } else {
                    res[i++] = -1;
                }
            }
        }
        return res;
    }

}

性能

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

}

性能

633.平方数之和

目标

给定一个非负整数 c ,你要判断是否存在两个整数 a 和 b,使得 a^2 + b^2 = c 。

示例 1:

输入:c = 5
输出:true
解释:1 * 1 + 2 * 2 = 5

示例 2:

输入:c = 3
输出:false

说明:

  • 0 <= c <= 2^31 - 1

思路

判断给定的整数是否是两个整数的平方和。

我们可以先从 0 开始计算每一个整数的平方和,直到平方和小于 Integer.MAX_VALUE, 不超过 10^5 个,将它保存到集合中,这一部分计算可以预处理。

然后我们还是同样的方法计算平方和 square,然后判断 c - square 是否在集合中,直到 square > c

代码


/**
 * @date 2024-11-04 0:17
 */
public class JudgeSquareSum633 {

    public static Set<Integer> set = new HashSet<>(100000);

    static {
        long i = 0;
        long square = 0;
        while (true) {
            square = i * i;
            if (square > Integer.MAX_VALUE) {
                break;
            }
            set.add((int) square);
            i++;
        }
    }

    public boolean judgeSquareSum(int c) {
        long i = 0;
        long square = 0;
        while (true) {
            square = i * i;
            if (square > c) {
                break;
            } else if (set.contains((int) (c - square))) {
                return true;
            }
            i++;
        }
        return false;
    }

}

性能

638.大礼包

目标

在 LeetCode 商店中, 有 n 件在售的物品。每件物品都有对应的价格。然而,也有一些大礼包,每个大礼包以优惠的价格捆绑销售一组物品。

给你一个整数数组 price 表示物品价格,其中 price[i] 是第 i 件物品的价格。另有一个整数数组 needs 表示购物清单,其中 needs[i] 是需要购买第 i 件物品的数量。

还有一个数组 special 表示大礼包,special[i] 的长度为 n + 1 ,其中 special[i][j] 表示第 i 个大礼包中内含第 j 件物品的数量,且 special[i][n] (也就是数组中的最后一个整数)为第 i 个大礼包的价格。

返回 确切 满足购物清单所需花费的最低价格,你可以充分利用大礼包的优惠活动。你不能购买超出购物清单指定数量的物品,即使那样会降低整体价格。任意大礼包可无限次购买。

示例 1:

输入:price = [2,5], special = [[3,0,5],[1,2,10]], needs = [3,2]
输出:14
解释:有 A 和 B 两种物品,价格分别为 ¥2 和 ¥5 。 
大礼包 1 ,你可以以 ¥5 的价格购买 3A 和 0B 。 
大礼包 2 ,你可以以 ¥10 的价格购买 1A 和 2B 。 
需要购买 3 个 A 和 2 个 B , 所以付 ¥10 购买 1A 和 2B(大礼包 2),以及 ¥4 购买 2A 。

示例 2:

输入:price = [2,3,4], special = [[1,1,0,4],[2,2,1,9]], needs = [1,2,1]
输出:11
解释:A ,B ,C 的价格分别为 ¥2 ,¥3 ,¥4 。
可以用 ¥4 购买 1A 和 1B ,也可以用 ¥9 购买 2A ,2B 和 1C 。 
需要买 1A ,2B 和 1C ,所以付 ¥4 买 1A 和 1B(大礼包 1),以及 ¥3 购买 1B , ¥4 购买 1C 。 
不可以购买超出待购清单的物品,尽管购买大礼包 2 更加便宜。

说明:

  • n == price.length == needs.length
  • 1 <= n <= 6
  • 0 <= price[i], needs[i] <= 10
  • 1 <= special.length <= 100
  • special[i].length == n + 1
  • 0 <= special[i][j] <= 50
  • 生成的输入对于 0 <= j <= n - 1 至少有一个 special[i][j] 非零。

思路

有一个购物清单 needneed[i] 表示需要购买商品 i 的数量,price[i] 表示商品 i 的单价,此外还有一组大礼包 specialspecial[j][i] 表示大礼包 j 中包含的第 i 件商品的数量,并且 specal[j][n] 表示该大礼包的价格。求购买 need 清单中的商品最少花费多少钱,我们可以购买大礼包任意次,但是购买的总数量不能超过需求的数量,尽管可能价格更低。

完全背包问题是物品有无限个,背包容量有限,求能装下的最大价值/最小价值。如果将题目中的清单视为多个背包容量,单买物品 i,以及购买大礼包 j 中的商品 i 视为不同的商品,那么我们求的是装满所有背包的最小价值。问题在于,大礼包不光有商品 i,还有其它商品,如何处理?

网友题解将单买也看成大礼包,只不过其它商品数量为 0,这样可以统一处理大礼包。

// todo

代码

性能

3226.使两个整数相等的位更改次数

目标

给你两个正整数 n 和 k。

你可以选择 n 的 二进制表示 中任意一个值为 1 的位,并将其改为 0。

返回使得 n 等于 k 所需要的更改次数。如果无法实现,返回 -1。

示例 1:

输入: n = 13, k = 4
输出: 2
解释:
最初,n 和 k 的二进制表示分别为 n = (1101)2 和 k = (0100)2,
我们可以改变 n 的第一位和第四位。结果整数为 n = (0100)2 = k。

示例 2:

输入: n = 21, k = 21
输出: 0
解释:
n 和 k 已经相等,因此不需要更改。

示例 3:

输入: n = 14, k = 13
输出: -1
解释:
无法使 n 等于 k。

说明:

  • 1 <= n, k <= 10^6

思路

有两个整数 nk,每次操作可以将 n 的二进制位从 1 改为 0,求使 n 等于 k 所需的操作次数,如果无法实现返回 -1。

注意到这两个整数最大为 10^6,而 2^20 = 1048576,因此最高 bit 位不会超过 20

依次比较这两个数的第 19 位到第 0 位:

  • 如果相等则跳过
  • 如果 n 的 bit 位为 0 则返回 -1,因为这时 k 对应位置的 bit 位为 1,无法通过操作使之相等
  • 否则累加操作次数

官网题解提供了另一种解法,将每个 bit 为 1 的位置视为一个元素,如果可以通过操作将 n 变为 k, 说明 k 的 bit 1 的集合是 n 的 bit 1 集合的子集。因此 n & k = k,这时我们需要统计 nk bit 位不同的个数,直接使用异或运算统计 bit 1 的个数即可。

return (n & k) == k ? Integer.bitCount(n ^ k) : -1;

代码


/**
 * @date 2024-11-02 5:00
 */
public class MinChanges3226 {

    public int minChanges(int n, int k) {
        int res = 0;
        for (int i = 19; i >= 0; i--) {
            int bitn = n & 1 << i;
            int bitk = k & 1 << i;
            if (bitn == bitk) {
                continue;
            }
            if (bitn == 0) {
                return -1;
            } else {
                res++;
            }
        }
        return res;
    }
}

性能

3259.超级饮料的最大强化能量

目标

来自未来的体育科学家给你两个整数数组 energyDrinkA 和 energyDrinkB,数组长度都等于 n。这两个数组分别代表 A、B 两种不同能量饮料每小时所能提供的强化能量。

你需要每小时饮用一种能量饮料来 最大化 你的总强化能量。然而,如果从一种能量饮料切换到另一种,你需要等待一小时来梳理身体的能量体系(在那个小时里你将不会获得任何强化能量)。

返回在接下来的 n 小时内你能获得的 最大 总强化能量。

注意 你可以选择从饮用任意一种能量饮料开始。

示例 1:

输入:energyDrinkA = [1,3,1], energyDrinkB = [3,1,1]
输出:5
解释:
要想获得 5 点强化能量,需要选择只饮用能量饮料 A(或者只饮用 B)。

示例 2:

输入:energyDrinkA = [4,1,1], energyDrinkB = [1,1,3]
输出:7
解释:
第一个小时饮用能量饮料 A。
切换到能量饮料 B ,在第二个小时无法获得强化能量。
第三个小时饮用能量饮料 B ,并获得强化能量。

说明:

  • n == energyDrinkA.length == energyDrinkB.length
  • 3 <= n <= 10^5
  • 1 <= energyDrinkA[i], energyDrinkB[i] <= 10^5

思路

energyDrinkAenergyDrinkB 两个数组,表示饮料 AB 在第 i 小时可以提供的能量,现在需要每一小时饮用饮料 AB 来获取能量,可以暂停一小时来切换饮料,求能够获得的最大能量。

定义 dp[i][j] 表示第 i 小时选择饮料 j 所能获取的最大能量,假设 j = 0 表示饮料 Aj = 1 表示饮料 B,那么状态转移方程为 dp[i][0] = Math.max(dp[i - 1][1], dp[i - 1][0] + energyDrinkA)dp[i][1] = Math.max(dp[i - 1][0], dp[i - 1][1] + energyDrinkB),最终返回 Math.max(dp[n - 1][0], dp[n - 1][1])。初始条件 dp[0][0] = energyDrinkA[0] dp[0][1] = energyDrinkA[1]

由于只与前面的状态有关,因此可以进行存储优化,使用两个变量保存前一个小时饮用 A B 的最大能量即可。

代码


/**
 * @date 2024-11-01 0:37
 */
public class MaxEnergyBoost3259 {

    public long maxEnergyBoost_v1(int[] energyDrinkA, int[] energyDrinkB) {
        int n = energyDrinkA.length;
        long prevA = energyDrinkA[0];
        long prevB = energyDrinkB[0];
        for (int i = 1; i < n; i++) {
            long curA = Math.max(prevB,  prevA + energyDrinkA[i]);
            long curB = Math.max(prevA,  prevB + energyDrinkB[i]);
            prevA = curA;
            prevB = curB;
        }
        return Math.max(prevA, prevB);
    }

}

性能

3165.不包含相邻元素的子序列的最大和

目标

给你一个整数数组 nums 和一个二维数组 queries,其中 queries[i] = [posi, xi]。

对于每个查询 i,首先将 nums[posi] 设置为 xi,然后计算查询 i 的答案,该答案为 nums 中 不包含相邻元素 的 子序列 的 最大 和。

返回所有查询的答案之和。

由于最终答案可能非常大,返回其对 10^9 + 7 取余 的结果。

子序列 是指从另一个数组中删除一些或不删除元素而不改变剩余元素顺序得到的数组。

示例 1:

输入:nums = [3,5,9], queries = [[1,-2],[0,-3]]
输出:21
解释:
执行第 1 个查询后,nums = [3,-2,9],不包含相邻元素的子序列的最大和为 3 + 9 = 12。
执行第 2 个查询后,nums = [-3,-2,9],不包含相邻元素的子序列的最大和为 9 。

示例 2:

输入:nums = [0,-1], queries = [[0,-5]]
输出:0
解释:
执行第 1 个查询后,nums = [-5,-1],不包含相邻元素的子序列的最大和为 0(选择空子序列)。

说明:

  • 1 <= nums.length <= 5 * 10^4
  • -10^5 <= nums[i] <= 10^5
  • 1 <= queries.length <= 5 * 10^4
  • queries[i] == [posi, xi]
  • 0 <= posi <= nums.length - 1
  • -10^5 <= xi <= 10^5

思路

// todo

代码

性能

3216.交换后字典序最小的字符串

目标

给你一个仅由数字组成的字符串 s,在最多交换一次 相邻 且具有相同 奇偶性 的数字后,返回可以得到的 字典序 最小的字符串。

如果两个数字都是奇数或都是偶数,则它们具有相同的奇偶性。例如,5 和 9、2 和 4 奇偶性相同,而 6 和 9 奇偶性不同。

示例 1:

输入: s = "45320"
输出: "43520"
解释:
s[1] == '5' 和 s[2] == '3' 都具有相同的奇偶性,交换它们可以得到字典序最小的字符串。

示例 2:

输入: s = "001"
输出: "001"
解释:
无需进行交换,因为 s 已经是字典序最小的。

说明:

  • 2 <= s.length <= 100
  • s 仅由数字组成。

思路

有一个仅由数字组成的字符串 s,最多可以执行一次操作,交换字符串中相邻并且奇偶性相同的元素,返回可以得到的字典序最小的字符串。

实际上就是交换第一个满足 i < j && s.charAt(i) - '0' > s.charAt(j) - '0 && (s.charAt(i) - '0') % 2 == (s.charAt(j) - '0') % 2 的相邻字符。

注意到 0 的 ASCII码为 48,数字的 ASCII码的奇偶性与数字本身的奇偶性相同,并且数字大的对应的 ASCII 码也大,不影响判断,可以不用减字符 '0'。

代码


/**
 * @date 2024-10-30 0:11
 */
public class GetSmallestString3216 {

    public String getSmallestString(String s) {
        char[] chars = s.toCharArray();
        int n = s.length();
        int prev = s.charAt(0);
        for (int i = 1; i < n; i++) {
            int cur = s.charAt(i);
            if (prev > cur && prev % 2 == cur % 2) {
                chars[i] = s.charAt(i - 1);
                chars[i - 1] = s.charAt(i);
                break;
            }
            prev = cur;
        }
        return new String(chars);
    }

}

性能

3211.生成不含相邻零的二进制字符串

目标

给你一个正整数 n。

如果一个二进制字符串 x 的所有长度为 2 的 子字符串 中包含 至少 一个 "1",则称 x 是一个 有效 字符串。

返回所有长度为 n 的 有效 字符串,可以以任意顺序排列。

示例 1:

输入: n = 3
输出: ["010","011","101","110","111"]
解释:
长度为 3 的有效字符串有:"010"、"011"、"101"、"110" 和 "111"。

示例 2:

输入: n = 1
输出: ["0","1"]
解释:
长度为 1 的有效字符串有:"0" 和 "1"。

说明:

  • 1 <= n <= 18

思路

示例2让人困惑,字符串 0 并没有长度为 2 的子字符串,更别提包含至少一个 1 了,但它是有效字符串。

还是按照题目名称来做吧,生成长度为 n,不含相邻零的二进制字符串。直接回溯即可。

官网题解还提出了一种位运算的解法,主要思想就是将 二进制字符串 视为 数字的二进制表示,问题转化为 0 ~ 2^n - 1 的数字中不含相邻零的个数。由于超出 n 的位数不在我们的考虑范围,为了简化处理,可以直接将数字的低 n 位取反,x ^ ((1 << n) - 1))1 << n0 开始计向左移 n 位,再减 1,得到低 n 位全为 1 的数字,对它取异或相当于低 n 位取反。问题转换为 数字中是否存在连续的 1。针对每一个数字,无需遍历每一位,直接使用 x & (x >> 1) 是否等于 0 来判断是否含有相邻的 1。相当于将每一位与它前面的位相与,如果存在相邻的 1 就会存在某个位相与的结果为 1 使最终结果不为 0

代码


/**
 * @date 2024-10-29 0:23
 */
public class ValidStrings3211 {
    List<String> res;
    char[] path;
    int n;

    public List<String> validStrings(int n) {
        res = new ArrayList<>();
        path = new char[n];
        this.n = n;
        backTracing('1', 0);
        return res;
    }

    public void backTracing(char prev, int i) {
        if (i == n) {
            res.add(new String(path));
            return;
        }
        path[i] = '1';
        int next = i + 1;
        backTracing('1', next);
        if (prev != '0') {
            path[i] = '0';
            backTracing('0', next);
        }
    }
}

性能

685.冗余连接II

目标

在本问题中,有根树指满足以下条件的 有向 图。该树只有一个根节点,所有其他节点都是该根节点的后继。该树除了根节点之外的每一个节点都有且只有一个父节点,而根节点没有父节点。

输入一个有向图,该图由一个有着 n 个节点(节点值不重复,从 1 到 n)的树及一条附加的有向边构成。附加的边包含在 1 到 n 中的两个不同顶点间,这条附加的边不属于树中已存在的边。

结果图是一个以边组成的二维数组 edges 。 每个元素是一对 [ui, vi],用以表示 有向 图中连接顶点 ui 和顶点 vi 的边,其中 ui 是 vi 的一个父节点。

返回一条能删除的边,使得剩下的图是有 n 个节点的有根树。若有多个答案,返回最后出现在给定二维数组的答案。

示例 1:

输入:edges = [[1,2],[1,3],[2,3]]
输出:[2,3]

示例 2:

输入:edges = [[1,2],[2,3],[3,4],[4,1],[1,5]]
输出:[4,1]

说明:

  • n == edges.length
  • 3 <= n <= 1000
  • edges[i].length == 2
  • 1 <= ui, vi <= n

思路

有一颗 n 个节点的树,节点编号 1 ~ n。使用 edges 表示向树中两个没有直接连接的节点之间加一条边之后的边的集合,找出一条可以删除的边使得 edges 变为一颗有 n 个节点的树。如果有多种选择,返回 edges 中最后出现的那个,即下标最大的边。与 冗余连接 不同的是 edges有向边 的集合。

如果直接使用昨天无向图寻找环的做法会有两个问题:

  • 无法处理 a -> b, b -> a 的情况,因为在无向图中为了防止环,直接回避了这种情况
  • 并不是删去环上任意一条边都可以的,因为边是有向的,如果某个节点出现两个父节点,那么一定要删去以该节点为终点的边

官网题解使用的还是并查集。// todo

代码


/**
 * @date 2024-10-28 8:51
 */
public class FindRedundantDirectedConnection685 {

    List<Integer>[] g;
    Set<Integer> loop;
    List<Integer> path;
    int start;
    int end;

    public int[] findRedundantDirectedConnection(int[][] edges) {
        int n = edges.length;
        g = new List[n + 1];
        for (int i = 0; i <= n; i++) {
            g[i] = new ArrayList<>();
        }
        int[] degree = new int[n + 1];
        Set<Integer> e = new HashSet<>(n);
        int end = -1;
        int[] self = null;
        for (int[] edge : edges) {
            int from = edge[0];
            int to = edge[1];
            int fromto = from << 10 | to;
            int tofrom = to << 10 | from;
            if (e.contains(fromto)) {
                self = new int[]{from, to};
            }
            e.add(fromto);
            e.add(tofrom);
            g[from].add(to);
            g[to].add(from);
            if (degree[to] == 1) {
                end = to;
            } else {
                degree[to]++;
            }
        }

        if (self != null) {
            if (end == -1) {
                for (int i = n - 1; i >= 0; i--) {
                    if ((self[0] == edges[i][0] && edges[i][1] == self[1])
                            || (self[0] == edges[i][1] && edges[i][0] == self[1])) {
                        return edges[i];
                    }
                }
            } else {
                return new int[]{self[0] == end ? self[1] : self[0], end};
            }

        }

        loop = new HashSet<>(n);
        path = new ArrayList<>();
        loop.add(1);
        path.add(1);
        dfs(0, 1);
        loop = new HashSet<>();
        for (int i = path.size() - 1; i >= 0; i--) {
            loop.add(path.get(i));
            if (start == path.get(i)) {
                break;
            }
        }
        if (end == -1) {
            for (int i = n - 1; i >= 0; i--) {
                if (loop.contains(edges[i][0]) && loop.contains(edges[i][1])) {
                    return edges[i];
                }
            }
        } else {
            for (int i = n - 1; i >= 0; i--) {
                if (edges[i][1] == end && loop.contains(edges[i][0])) {
                    return edges[i];
                }
            }
        }

        return null;
    }

    private boolean dfs(int parent, int current) {
        for (Integer next : g[current]) {
            if (next == parent) {
                continue;
            }
            if (loop.contains(next)) {
                start = next;
                return true;
            } else {
                loop.add(next);
                path.add(next);
                if (dfs(current, next)) {
                    return true;
                }
                path.remove(path.size() - 1);
                loop.remove(next);
            }
        }
        return false;
    }

}

性能