https://leetcode.cn/circle/discuss/v2RXSN/
技巧指一些比较套路的算法,包括双指针、滑动窗口、二分答案、前缀和、差分、回溯、前后缀分解、二进制枚举、贡献法等。
这些技巧相对容易掌握,无论是求职面试还是周赛,都是经常考察的,可以优先学习这些内容。
题目 | 难度 | 备注 |
---|---|---|
2730. 找到最长的半重复子字符串 | 1502 | 双指针 |
2698. 求一个整数的惩罚数 | 1679 | 回溯 |
2563. 统计公平数对的数目 | 1721 | 排序+二分查找 |
2653. 滑动子数组的美丽值 | 1785 | 滑动窗口 |
2718. 查询后矩阵的和 | 1769 | 倒序处理 |
2576. 求出最多标记下标 | 1843 | 二分答案/双指针 |
2537. 统计好子数组的数目 | 1892 | 双指针 |
2602. 使数组元素全部相等的最少操作次数 | 1903 | 排序+前缀和+二分查找 |
2594. 修车的最少时间 | 1915 | 二分答案 |
2681. 英雄的力量 | 2060 | 贡献法 |
2555. 两个线段获得的最多奖品 | 2081 | 双指针 |
2560. 打家劫舍 IV | 2081 | 二分答案+DP/贪心 |
2616. 最小化数对的最大差值 | 2155 | 二分答案+贪心 |
2528. 最大化城市的最小供电站数目 | 2236 | 二分答案+前缀和+差分+贪心 |
2565. 最少得分子序列 | 2432 | 前后缀分解 |
2552. 统计上升四元组 | 2433 | 有技巧的枚举 |
难度中等9
给你一个下标从 0 开始的字符串 s
,这个字符串只包含 0
到 9
的数字字符。
如果一个字符串 t
中至多有一对相邻字符是相等的,那么称这个字符串 t
是 半重复的 。例如,0010
、002020
、0123
、2002
和 54944
是半重复字符串,而 00101022
和 1101234883
不是。
请你返回 s
中最长 半重复 子字符串的长度。
一个 子字符串 是一个字符串中一段连续 非空 的字符。
示例 1:
输入:s = "52233"
输出:4
解释:最长半重复子字符串是 "5223" ,子字符串从 i = 0 开始,在 j = 3 处结束。
示例 2:
输入:s = "5494"
输出:4
解释:s 就是一个半重复字符串,所以答案为 4 。
示例 3:
输入:s = "1111111"
输出:2
解释:最长半重复子字符串是 "11" ,子字符串从 i = 0 开始,在 j = 1 处结束。
提示:
1 <= s.length <= 50
'0' <= s[i] <= '9'
题解:同向双指针
class Solution {
public int longestSemiRepetitiveSubstring(String s) {
int ans = 1;
int left = 0;
int cnt = 0; // cnt 记录窗口内半重复字符的出现次数
for(int right = 1; right < s.length(); right++){
if(s.charAt(right) == s.charAt(right-1))
cnt += 1;
while(cnt > 1){
left += 1;
if(s.charAt(left) == s.charAt(left-1))
cnt -= 1;
}
ans = Math.max(ans, right - left + 1);
}
return ans;
}
}
难度中等21
给你一个正整数 n
,请你返回 n
的 惩罚数 。
n
的 惩罚数 定义为所有满足以下条件 i
的数的平方和:
1 <= i <= n
i * i
的十进制表示的字符串可以分割成若干连续子字符串,且这些子字符串对应的整数值之和等于 i
。示例 1:
输入:n = 10
输出:182
解释:总共有 3 个整数 i 满足要求:
- 1 ,因为 1 * 1 = 1
- 9 ,因为 9 * 9 = 81 ,且 81 可以分割成 8 + 1 。
- 10 ,因为 10 * 10 = 100 ,且 100 可以分割成 10 + 0 。
因此,10 的惩罚数为 1 + 81 + 100 = 182
示例 2:
输入:n = 37
输出:1478
解释:总共有 4 个整数 i 满足要求:
- 1 ,因为 1 * 1 = 1
- 9 ,因为 9 * 9 = 81 ,且 81 可以分割成 8 + 1 。
- 10 ,因为 10 * 10 = 100 ,且 100 可以分割成 10 + 0 。
- 36 ,因为 36 * 36 = 1296 ,且 1296 可以分割成 1 + 29 + 6 。
因此,37 的惩罚数为 1 + 81 + 100 + 1296 = 1478
提示:
1 <= n <= 1000
class Solution {
public int punishmentNumber(int n) {
int ans = 0;
for(int i = 1; i <= n; i++){
if(dfs(0, i, String.valueOf(i*i))) ans += i * i;
}
return ans;
}
public boolean dfs(int i, int sum, String s){
if(sum == 0 && i == s.length()) return true;
if(sum < 0 || i == s.length()) return false;
boolean res = false;
for(int j = i; j < s.length() && !res; j++){
res |= dfs(j+1, sum - Integer.valueOf(s.substring(i, j+1)), s);
}
return res;
}
}
python
class Solution:
def punishmentNumber(self, n: int) -> int:
def dfs(i, sum: int, s: str) -> bool:
if sum == 0 and i == len(s): return True
if sum < 0 or i == len(s): return False
for j in range(i, len(s)):
if dfs(j+1, sum - int(s[i:j+1]), s):
return True
return False
ans = 0
for num in range(1, n+1):
if dfs(0, num, str(num * num)):
ans += num * num
return ans
难度中等34
给你一个下标从 0 开始、长度为 n
的整数数组 nums
,和两个整数 lower
和 upper
,返回 公平数对的数目 。
如果 (i, j)
数对满足以下情况,则认为它是一个 公平数对 :
0 <= i < j < n
,且lower <= nums[i] + nums[j] <= upper
示例 1:
输入:nums = [0,1,7,4,4,5], lower = 3, upper = 6
输出:6
解释:共计 6 个公平数对:(0,3)、(0,4)、(0,5)、(1,3)、(1,4) 和 (1,5) 。
示例 2:
输入:nums = [1,7,9,2,5], lower = 11, upper = 11
输出:1
解释:只有单个公平数对:(2,3) 。
提示:
1 <= nums.length <= 105
nums.length == n
-109 <= nums[i] <= 109
-109 <= lower <= upper <= 109
class Solution {
// 可以看到要求与元素顺序无关,可以先排序
// 枚举左端点,寻找右端点区间
// lower <= nums[i] + nums[j] <= upper
// lower - nums[i] <= nums[j] <= upper - nums[i]
public long countFairPairs(int[] nums, int lower, int upper) {
Arrays.sort(nums);
long ans = 0l;
int n = nums.length;
// 0, 1, 4, 4, 5, 7
for(int i = 0; i < n; i++){
// left : 在[i+1,n)区间中找到第一个大于等于lower - nums[i]的元素下标
int left = lowerBound(nums, i+1, lower - nums[i]);
// right : 在[i+1,n)区间中找到最后一个大于upper - nums[i]的元素下标
// 最后一个大于upper - nums[i] = 大于等于upper - nums[i]+1的第一个元素下标 - 1
int right = lowerBound(nums, i+1, upper - nums[i] + 1) - 1;
ans += right - left + 1;
//
}
return ans;
}
// lower_bound : 返回大于等于key的第一个元素下标
public int lowerBound(int[] nums, int left, int target){
int right = nums.length;
while(left < right){
int mid = (left + right) >> 1;
if(nums[mid] < target) left = mid + 1;
else right = mid;
}
return left;
}
}
python
class Solution:
def countFairPairs(self, nums: List[int], lower: int, upper: int) -> int:
nums.sort()
ans = 0
for i, x in enumerate(nums):
left = bisect_left(nums, lower - x, i+1, len(nums))
right = bisect_left(nums, upper - x + 1, i+1, len(nums)) - 1
ans += right - left + 1
return ans
难度中等27
给你一个长度为 n
的整数数组 nums
,请你求出每个长度为 k
的子数组的 美丽值 。
一个子数组的 美丽值 定义为:如果子数组中第 x
小整数 是 负数 ,那么美丽值为第 x
小的数,否则美丽值为 0
。
请你返回一个包含 n - k + 1
个整数的数组,依次 表示数组中从第一个下标开始,每个长度为 k
的子数组的 美丽值 。
示例 1:
输入:nums = [1,-1,-3,-2,3], k = 3, x = 2
输出:[-1,-2,-2]
解释:总共有 3 个 k = 3 的子数组。
第一个子数组是 [1, -1, -3] ,第二小的数是负数 -1 。
第二个子数组是 [-1, -3, -2] ,第二小的数是负数 -2 。
第三个子数组是 [-3, -2, 3] ,第二小的数是负数 -2 。
示例 2:
输入:nums = [-1,-2,-3,-4,-5], k = 2, x = 2
输出:[-1,-2,-3,-4]
解释:总共有 4 个 k = 2 的子数组。
[-1, -2] 中第二小的数是负数 -1 。
[-2, -3] 中第二小的数是负数 -2 。
[-3, -4] 中第二小的数是负数 -3 。
[-4, -5] 中第二小的数是负数 -4 。
示例 3:
输入:nums = [-3,1,2,-3,0,-3], k = 2, x = 1
输出:[-3,0,-3,-3,-3]
解释:总共有 5 个 k = 2 的子数组。
[-3, 1] 中最小的数是负数 -3 。
[1, 2] 中最小的数不是负数,所以美丽值为 0 。
[2, -3] 中最小的数是负数 -3 。
[-3, 0] 中最小的数是负数 -3 。
[0, -3] 中最小的数是负数 -3 。
提示:
n == nums.length
1 <= n <= 105
1 <= k <= n
1 <= x <= k
-50 <= nums[i] <= 50
class Solution {
// 注意到值域 -50 <= nums[i] <= 50,可以使用计数排序思想来寻找第 x 小的整数
// 从小到大遍历窗口内的计数数组,当遍历个数 >= x 时,说明找到了第x小的数
public int[] getSubarrayBeauty(int[] nums, int k, int x) {
int n = nums.length;
int[] ans = new int[n-k+1];
int[] cnt = new int[105];
int diff = 50; // 因为取值有负数,所以统一加上偏移量
for(int i = 0; i < k-1; i++)
cnt[nums[i] + diff] += 1;
for(int i = k-1; i < n; i++){
cnt[nums[i] + diff] += 1;
int t = 0, j = 0; // t 从小到大统计窗口内元素的出现个数; j第x小的元素在cnt数组中的下标
for(; j <= 50; j++){
t += cnt[j];
if(t >= x) break;
}
cnt[nums[i - k + 1] + diff] -= 1;
ans[i - k + 1] = j - diff < 0 ? j - diff : 0;
}
return ans;
}
}
python
难度中等29
给你一个整数 n
和一个下标从 0 开始的 二维数组 queries
,其中 queries[i] = [typei, indexi, vali]
。
一开始,给你一个下标从 0 开始的 n x n
矩阵,所有元素均为 0
。每一个查询,你需要执行以下操作之一:
typei == 0
,将第 indexi
行的元素全部修改为 vali
,覆盖任何之前的值。typei == 1
,将第 indexi
列的元素全部修改为 vali
,覆盖任何之前的值。请你执行完所有查询以后,返回矩阵中所有整数的和。
class Solution {
// 如果操作的是行,那么需要知道:
// 1. 这一行之前有没有被操作过,
// 2. 这一行有多少列之前操作过
public long matrixSumQueries(int n, int[][] queries) {
long ans = 0;
Set<Integer>[] vis = new Set[]{new HashSet<>(), new HashSet<>()};
for(int i = queries.length-1; i >= 0; i--){
int[] q = queries[i];
int type = q[0], index = q[1], val = q[2];
if(!vis[type].contains(index)){ // 后面(>i)没有对这一行/列的操作
// 这一行/列还剩下 n-vis[type^1].size() 个可以填入的格子
ans += (long)(n - vis[type ^ 1].size()) * val;
vis[type].add(index);
}
}
return ans;
}
}
难度中等40
给你一个下标从 0 开始的整数数组 nums
。
一开始,所有下标都没有被标记。你可以执行以下操作任意次:
i
和 j
,满足 2 * nums[i] <= nums[j]
,标记下标 i
和 j
。请你执行上述操作任意次,返回 nums
中最多可以标记的下标数目。
示例 1:
输入:nums = [3,5,2,4]
输出:2
解释:第一次操作中,选择 i = 2 和 j = 1 ,操作可以执行的原因是 2 * nums[2] <= nums[1] ,标记下标 2 和 1 。
没有其他更多可执行的操作,所以答案为 2 。
示例 2:
输入:nums = [9,2,5,4]
输出:4
解释:第一次操作中,选择 i = 3 和 j = 0 ,操作可以执行的原因是 2 * nums[3] <= nums[0] ,标记下标 3 和 0 。
第二次操作中,选择 i = 1 和 j = 2 ,操作可以执行的原因是 2 * nums[1] <= nums[2] ,标记下标 1 和 2 。
没有其他更多可执行的操作,所以答案为 4 。
示例 3:
输入:nums = [7,6,8]
输出:0
解释:没有任何可以执行的操作,所以答案为 0 。
提示:
1 <= nums.length <= 105
1 <= nums[i] <= 109
方法一:同向双指针
class Solution {
public int maxNumOfMarkedIndices(int[] nums) {
int n = nums.length;
Arrays.sort(nums);
int left = 0, right = n / 2;
int ans = 0;
while(left < n / 2 && right < n){
if((long)(2 * nums[left]) <= (long)nums[right]){
left += 1;
right += 1;
ans += 2;
}else right += 1;
}
return ans;
}
}
python
class Solution:
def maxNumOfMarkedIndices(self, nums: List[int]) -> int:
nums.sort()
n = len(nums)
ans = 0
left, right = 0, n // 2
while left < n // 2 and right < n:
if (nums[left] * 2) <= nums[right]:
left += 1
right += 1
ans += 2
else:
right += 1
return ans
方法二:二分答案
class Solution {
/**
二分答案:如果可以匹配 k 对,那么也可以匹配
public int maxNumOfMarkedIndices(int[] nums) {
Arrays.sort(nums);
int n = nums.length;
int left = -1, right = n / 2;
while(left < right){
int mid = (left + right + 1) >> 1;
if(check(nums, mid)) left = mid;
else right = mid - 1;
}
return right * 2;
}
public boolean check(int[] nums, int k){
int n = nums.length;
for(int i = 0; i < k; i++){
if((long)(2 * nums[i]) > (long)(nums[n - k + i]))
return false;
}
return true;
}
}
python
class Solution:
def maxNumOfMarkedIndices(self, nums: List[int]) -> int:
nums.sort()
left, right = 0, len(nums) // 2 + 1
while left + 1 < right:
k = (left + right) // 2
if all(nums[i] * 2 <= nums[i - k] for i in range(k)):
left = k
else:
right = k
return left * 2
难度中等34
给你一个整数数组 nums
和一个整数 k
,请你返回 nums
中 好 子数组的数目。
一个子数组 arr
如果有 至少 k
对下标 (i, j)
满足 i < j
且 arr[i] == arr[j]
,那么称它是一个 好 子数组。
子数组 是原数组中一段连续 非空 的元素序列。
示例 1:
输入:nums = [1,1,1,1,1], k = 10
输出:1
解释:唯一的好子数组是这个数组本身。
示例 2:
输入:nums = [3,1,4,3,2,2,4], k = 2
输出:4
解释:总共有 4 个不同的好子数组:
- [3,1,4,3,2,2] 有 2 对。
- [3,1,4,3,2,2,4] 有 3 对。
- [1,4,3,2,2,4] 有 2 对。
- [4,3,2,2,4] 有 2 对。
提示:
1 <= nums.length <= 105
1 <= nums[i], k <= 109
方法一:双指针
关键是窗口中的对数如何计算
class Solution {
// 子数组统计问题,常用双指针(不定长滑动窗口)实现
public long countGood(int[] nums, int k) {
Map<Integer, Integer> cnt = new HashMap<>();
long ans = 0;
int left = 0, pairs = 0;
for(int x : nums){
pairs += cnt.getOrDefault(x, 0);
cnt.merge(x, 1, Integer::sum); // 移入右端点
// [0,right], [1,right], ..., [left, right] 这些都是好子数组,一共有 left+1 个。
ans += left;
while(pairs >= k){
pairs -= cnt.merge(nums[left++], -1, Integer::sum);
ans++;
}
}
return ans;
}
}
写法二:
class Solution {
// 子数组统计问题,常用双指针(不定长滑动窗口)实现
public long countGood(int[] nums, int k) {
Map<Integer, Integer> cnt = new HashMap<>();
long ans = 0;
int left = 0, pairs = 0;
for(int x : nums){
pairs += cnt.getOrDefault(x, 0);
cnt.merge(x, 1, Integer::sum); // 移入右端点
while(pairs - cnt.get(nums[left]) + 1 >= k){
pairs -= cnt.merge(nums[left++], -1, Integer::sum); // 移出左端点
}
if(pairs >= k) ans += left + 1;
}
return ans;
}
}
难度中等25
给你一个正整数数组 nums
。
同时给你一个长度为 m
的整数数组 queries
。第 i
个查询中,你需要将 nums
中所有元素变成 queries[i]
。你可以执行以下操作 任意 次:
1
。请你返回一个长度为 m
的数组 answer
,其中 answer[i]
是将 nums
中所有元素变成 queries[i]
的 最少 操作次数。
注意,每次查询后,数组变回最开始的值。
示例 1:
输入:nums = [3,1,6,8], queries = [1,5]
输出:[14,10]
解释:第一个查询,我们可以执行以下操作:
- 将 nums[0] 减小 2 次,nums = [1,1,6,8] 。
- 将 nums[2] 减小 5 次,nums = [1,1,1,8] 。
- 将 nums[3] 减小 7 次,nums = [1,1,1,1] 。
第一个查询的总操作次数为 2 + 5 + 7 = 14 。
第二个查询,我们可以执行以下操作:
- 将 nums[0] 增大 2 次,nums = [5,1,6,8] 。
- 将 nums[1] 增大 4 次,nums = [5,5,6,8] 。
- 将 nums[2] 减小 1 次,nums = [5,5,5,8] 。
- 将 nums[3] 减小 3 次,nums = [5,5,5,5] 。
第二个查询的总操作次数为 2 + 4 + 1 + 3 = 10 。
示例 2:
输入:nums = [2,9,6,3], queries = [10]
输出:[20]
解释:我们可以将数组中所有元素都增大到 10 ,总操作次数为 8 + 1 + 4 + 7 = 20 。
提示:
n == nums.length
m == queries.length
1 <= n, m <= 105
1 <= nums[i], queries[i] <= 109
题解:排序 + 前缀和 + 二分
class Solution {
// 排序 + 前缀和 + 二分
public List<Long> minOperations(int[] nums, int[] queries) {
List<Long> ans = new ArrayList<>();
Arrays.sort(nums);
int n = nums.length;
long[] sum = new long[n+1];
for(int i = 0; i < n; i++)
sum[i+1] = sum[i] + nums[i];
for(int q : queries){
int left = 0, right = n;
// 二分找到第一个大于等于 q 的元素下标
while(left < right){
int mid = (left + right) >> 1;
if(nums[mid] < q) left = mid + 1;
else right = mid;
}
// 在left=right左边的都比q小,右边的都比q大
// 用目标和-前缀和求左边的操作数 用前缀和-目标和求右边的操作数
long curl = (long)q * (long)right - sum[right]; //左边: 目标和 - 前缀和
long curr = sum[n] - sum[right] - (long)q * (long)(n-right); // 右边:前缀和 - 目标和
ans.add(curl + curr);
}
return ans;
}
}
难度中等22
给你一个整数数组 ranks
,表示一些机械工的 能力值 。ranksi
是第 i
位机械工的能力值。能力值为 r
的机械工可以在 r * n2
分钟内修好 n
辆车。
同时给你一个整数 cars
,表示总共需要修理的汽车数目。
请你返回修理所有汽车 最少 需要多少时间。
**注意:**所有机械工可以同时修理汽车。
示例 1:
输入:ranks = [4,2,3,1], cars = 10
输出:16
解释:
- 第一位机械工修 2 辆车,需要 4 * 2 * 2 = 16 分钟。
- 第二位机械工修 2 辆车,需要 2 * 2 * 2 = 8 分钟。
- 第三位机械工修 2 辆车,需要 3 * 2 * 2 = 12 分钟。
- 第四位机械工修 4 辆车,需要 1 * 4 * 4 = 16 分钟。
16 分钟是修理完所有车需要的最少时间。
示例 2:
输入:ranks = [5,1,8], cars = 6
输出:16
解释:
- 第一位机械工修 1 辆车,需要 5 * 1 * 1 = 5 分钟。
- 第二位机械工修 4 辆车,需要 1 * 4 * 4 = 16 分钟。
- 第三位机械工修 1 辆车,需要 8 * 1 * 1 = 8 分钟。
16 分钟时修理完所有车需要的最少时间。
提示:
1 <= ranks.length <= 105
1 <= ranks[i] <= 100
1 <= cars <= 106
题解:二分答案
class Solution {
public long repairCars(int[] ranks, int cars) {
long left = 0, right = (long)1e18;
while(left < right){
long mid = (left + right) >> 1;
if(!check(ranks, cars, mid)) left = mid + 1;
else right = mid;
}
return left;
}
// n 个机械工能否在 times 时间内修好 cars 辆车
// 在timee时间内,查看每个机械工最多能修多少辆车,求sum,查看能否超过cars
public boolean check(int[] ranks, int cars, long times){
int cnt = 0;
for(int rank : ranks){
cnt += (int)Math.sqrt(times / rank);
if(cnt >= cars) return true;
}
return false;
}
}
难度困难16
给你一个下标从 0 开始的整数数组 nums
,它表示英雄的能力值。如果我们选出一部分英雄,这组英雄的 力量 定义为:
i0
,i1
,… ik
表示这组英雄在数组中的下标。那么这组英雄的力量为 max(nums[i0],nums[i1] ... nums[ik])2 * min(nums[i0],nums[i1] ... nums[ik])
。请你返回所有可能的 非空 英雄组的 力量 之和。由于答案可能非常大,请你将结果对 109 + 7
取余。
示例 1:
输入:nums = [2,1,4]
输出:141
解释:
第 1 组:[2] 的力量为 22 * 2 = 8 。
第 2 组:[1] 的力量为 12 * 1 = 1 。
第 3 组:[4] 的力量为 42 * 4 = 64 。
第 4 组:[2,1] 的力量为 22 * 1 = 4 。
第 5 组:[2,4] 的力量为 42 * 2 = 32 。
第 6 组:[1,4] 的力量为 42 * 1 = 16 。
第 7 组:[2,1,4] 的力量为 42 * 1 = 16 。
所有英雄组的力量之和为 8 + 1 + 64 + 4 + 32 + 16 + 16 = 141 。
示例 2:
输入:nums = [1,1,1]
输出:7
解释:总共有 7 个英雄组,每一组的力量都是 1 。所以所有英雄组的力量之和为 7 。
提示:
1 <= nums.length <= 105
1 <= nums[i] <= 109
题解:
元素的顺序不影响答案,先排序。
枚举 x=nums[i]
作为最大值。
x
一个数的力量为 x^ 3
。(单个数的力量是自身的三次方)
其余不超过 nums[i]
的数字作为最小值时,贡献是多少?
例如有 a,b,c,d,e
五个数,且顺序从小到大。
枚举到 d
时:
a 作为最小值,中间的 b 和 c 可选可不选,一共有 2^2 种方案,所以 a 的贡献是 a⋅2^2
。
b 作为最小值,中间的 c 可选可不选,一共有 2^ 1种方案,所以 b 的贡献是 b⋅2^1
。
c 作为最小值,一共有 2^0 种方案,所以 c 的贡献是 c⋅2^0
。
这些元素作为最小值的贡献为a⋅2^2 + b⋅2^1 + c⋅2^0
,记作 s。
那么 d 及其左侧元素对答案的贡献为d^3 +d^2⋅s=d^2⋅(d+s)
继续,枚举到 e 时:a,b,c,d 作为最小值的贡献为a⋅2^3+b⋅2^2+c⋅2^1+d⋅2^0 = 2⋅s+d
上式为新的 s,即s=2⋅s+nums[i]
利用这一递推式,就可以 O(1) 地计算出排序后的每个 nums[i] 左侧元素作为最小值的贡献。
class Solution {
/**
贡献法,
*/
private static final int MOD = (int)1e9+7;
public int sumOfPower(int[] nums) {
Arrays.sort(nums);
long ans = 0, s = 0;
for(long x : nums){
ans = (ans + x*x % MOD *(x + s)) % MOD; // 中间模一次防止溢出
s = (s * 2 + x) % MOD;
}
return (int)ans;
}
}
专题训练:贡献法
难度中等34
在 X轴 上有一些奖品。给你一个整数数组 prizePositions
,它按照 非递减 顺序排列,其中 prizePositions[i]
是第 i
件奖品的位置。数轴上一个位置可能会有多件奖品。再给你一个整数 k
。
你可以选择两个端点为整数的线段。每个线段的长度都必须是 k
。你可以获得位置在任一线段上的所有奖品(包括线段的两个端点)。注意,两个线段可能会有相交。
k = 2
,你可以选择线段 [1, 3]
和 [2, 4]
,你可以获得满足 1 <= prizePositions[i] <= 3
或者 2 <= prizePositions[i] <= 4
的所有奖品 i 。请你返回在选择两个最优线段的前提下,可以获得的 最多 奖品数目。
示例 1:
输入:prizePositions = [1,1,2,2,3,3,5], k = 2
输出:7
解释:这个例子中,你可以选择线段 [1, 3] 和 [3, 5] ,获得 7 个奖品。
示例 2:
输入:prizePositions = [1,2,3,4], k = 0
输出:2
解释:这个例子中,一个选择是选择线段 [3, 3] 和 [4, 4] ,获得 2 个奖品。
提示:
1 <= prizePositions.length <= 105
1 <= prizePositions[i] <= 109
0 <= k <= 109
prizePositions
有序非递减。题解:https://leetcode.cn/problems/maximize-win-from-two-segments/solution/tong-xiang-shuang-zhi-zhen-ji-lu-di-yi-t-5hlh/
class Solution {
public int maximizeWin(int[] prizePositions, int k) {
int ans = 0, n = prizePositions.length;
// 定义pre[i+1] 表示以p[i]为线段右端点时覆盖了至多多少奖品
// 初始pre[0] = 0
// 转移:pre[right+1] = max(pre[right], right-left+1)
// 那么答案(两条线段覆盖多少奖品)为 max(right - left + 1 + pre[left])
int[] pre = new int[n + 1];
int left = 0;
for(int right = 0; right < n; right++){
while(prizePositions[left] < prizePositions[right]-k)
left++;
ans = Math.max(ans, right - left + 1 + pre[left]);
pre[right+1] = Math.max(pre[right], right - left + 1);
}
return ans;
}
}
python
class Solution:
def maximizeWin(self, prizePositions: List[int], k: int) -> int:
ans, n = 0, len(prizePositions)
pre = [0 for _ in range(n+1)]
left = 0
for right in range(n):
while prizePositions[left] < prizePositions[right] - k:
left += 1
ans = max(ans, right - left + 1 + pre[left])
pre[right+1] = max(pre[right], right - left + 1)
return ans
难度中等54
沿街有一排连续的房屋。每间房屋内都藏有一定的现金。现在有一位小偷计划从这些房屋中窃取现金。
由于相邻的房屋装有相互连通的防盗系统,所以小偷 不会窃取相邻的房屋 。
小偷的 窃取能力 定义为他在窃取过程中能从单间房屋中窃取的 最大金额 。
给你一个整数数组 nums
表示每间房屋存放的现金金额。形式上,从左起第 i
间房屋中放有 nums[i]
美元。
另给你一个整数 k
,表示窃贼将会窃取的 最少 房屋数。小偷总能窃取至少 k
间房屋。
返回小偷的 最小 窃取能力。
示例 1:
输入:nums = [2,3,5,9], k = 2
输出:5
解释:
小偷窃取至少 2 间房屋,共有 3 种方式:
- 窃取下标 0 和 2 处的房屋,窃取能力为 max(nums[0], nums[2]) = 5 。
- 窃取下标 0 和 3 处的房屋,窃取能力为 max(nums[0], nums[3]) = 9 。
- 窃取下标 1 和 3 处的房屋,窃取能力为 max(nums[1], nums[3]) = 9 。
因此,返回 min(5, 9, 9) = 5 。
示例 2:
输入:nums = [2,7,9,3,1], k = 2
输出:2
解释:共有 7 种窃取方式。窃取能力最小的情况所对应的方式是窃取下标 0 和 4 处的房屋。返回 max(nums[0], nums[4]) = 2 。
提示:
1 <= nums.length <= 105
1 <= nums[i] <= 109
1 <= k <= (nums.length + 1)/2
方法一:二分答案 + DP
class Solution {
public int minCapability(int[] nums, int k) {
int mx = 0;
for(int num : nums) mx = Math.max(mx, num);
int left = 0, right = mx;
while(left < right){
int mid = (left + right) >> 1;
// 如果不能偷mid金额,那么(0,mid]都不能偷,left = mid + 1
if(!check(nums, k, mid)) left = mid + 1;
else right = mid;
}
return right;
}
public boolean check(int[] nums, int k, int maxval){
int n = nums.length;
// 定义 f[i] 表示 前i间 能偷的最少房屋数
int[] f = new int[n+2];
for(int i = 0; i < n; i++){
f[i+2] = Math.max(f[i+1], f[i] + (nums[i] <= maxval ? 1 : 0));
if(f[i+1] >= k)
return true;
}
return f[n+1] >= k;
}
}
难度中等13
给你一个下标从 0 开始的整数数组 nums
和一个整数 p
。请你从 nums
中找到 p
个下标对,每个下标对对应数值取差值,你需要使得这 p
个差值的 最大值 最小。同时,你需要确保每个下标在这 p
个下标对中最多出现一次。
对于一个下标对 i
和 j
,这一对的差值为 |nums[i] - nums[j]|
,其中 |x|
表示 x
的 绝对值 。
请你返回 p
个下标对对应数值 最大差值 的 最小值 。
示例 1:
输入:nums = [10,1,2,7,1,3], p = 2
输出:1
解释:第一个下标对选择 1 和 4 ,第二个下标对选择 2 和 5 。
最大差值为 max(|nums[1] - nums[4]|, |nums[2] - nums[5]|) = max(0, 1) = 1 。所以我们返回 1 。
示例 2:
输入:nums = [4,2,1,2], p = 1
输出:0
解释:选择下标 1 和 3 构成下标对。差值为 |2 - 2| = 0 ,这是最大差值的最小值。
提示:
1 <= nums.length <= 105
0 <= nums[i] <= 109
0 <= p <= (nums.length)/2
class Solution {
// 最小化最大值 ==> 二分答案
public int minimizeMax(int[] nums, int p) {
Arrays.sort(nums);
int left = 0, right = (int)1e9;
while(left < right){
int mid = (left + right) >> 1;
if(!check(nums, p, mid)) left = mid + 1;
else right = mid;
}
return right;
}
// 查看是否存在 p对 最大差值不超过 k 的数对
public boolean check(int[] nums, int p, int k){
int cnt = 0;
int i = 1, n = nums.length;
while(i < n){
if(nums[i] - nums[i-1] <= k){
i += 2;
cnt += 1;
}else i += 1;
if(cnt == p) return true;
}
return cnt >= p;
}
}
最小化最大值
最大化最小值
难度困难28
给你一个下标从 0 开始长度为 n
的整数数组 stations
,其中 stations[i]
表示第 i
座城市的供电站数目。
每个供电站可以在一定 范围 内给所有城市提供电力。换句话说,如果给定的范围是 r
,在城市 i
处的供电站可以给所有满足 |i - j| <= r
且 0 <= i, j <= n - 1
的城市 j
供电。
|x|
表示 x
的 绝对值 。比方说,|7 - 5| = 2
,|3 - 10| = 7
。一座城市的 电量 是所有能给它供电的供电站数目。
政府批准了可以额外建造 k
座供电站,你需要决定这些供电站分别应该建在哪里,这些供电站与已经存在的供电站有相同的供电范围。
给你两个整数 r
和 k
,如果以最优策略建造额外的发电站,返回所有城市中,最小供电站数目的最大值是多少。
这 k
座供电站可以建在多个城市。
示例 1:
输入:stations = [1,2,4,5,0], r = 1, k = 2
输出:5
解释:
最优方案之一是把 2 座供电站都建在城市 1 。
每座城市的供电站数目分别为 [1,4,4,5,0] 。
- 城市 0 的供电站数目为 1 + 4 = 5 。
- 城市 1 的供电站数目为 1 + 4 + 4 = 9 。
- 城市 2 的供电站数目为 4 + 4 + 5 = 13 。
- 城市 3 的供电站数目为 5 + 4 = 9 。
- 城市 4 的供电站数目为 5 + 0 = 5 。
供电站数目最少是 5 。
无法得到更优解,所以我们返回 5 。
示例 2:
输入:stations = [4,4,4,4], r = 0, k = 3
输出:4
解释:
无论如何安排,总有一座城市的供电站数目是 4 ,所以最优解是 4 。
提示:
n == stations.length
1 <= n <= 105
0 <= stations[i] <= 105
0 <= r <= n - 1
0 <= k <= 109
class Solution {
public long maxPower(int[] stations, int r, int k) {
long left = 0, right = (long)1e15;
while (left < right) {
long mid = (left + right + 1) >> 1;
if (!check(stations, r, k, mid))
right = mid - 1;
else
left = mid;
}
return right;
}
// 将 i 位置供电站stations[i] 化为 区间[i-r, i+r] 的供电站数量,使用差分数组来维护区间更新后的值
// 查看在 至多增加k个供电站 的条件下,每个城市最小供电站数目能否>=mnpower
// 特别的,从左往右遍历,当 i-r 位置城市供电站数目 < mnpower时,则需要在 r位置 建造供电站
public boolean check(int[] stations, int r, int k, long mnpower) {
int n = stations.length;
long cnt = 0; // 额外建造供电站的数量
long[] diff = new long[n + r + 1];
for (int i = 0; i < r; i++) {
diff[0] += stations[i];
diff[i + r + 1] -= stations[i];
}
long s = 0; // 差分数组的当前值(第 i-r 位置上供电站的数目)
for (int i = r; i < n + r; i++) {
if (i < n) {
diff[i - r] += stations[i];
diff[i + r + 1] -= stations[i];
}
s += diff[i - r]; // 恢复 i-r 位置供电站数量
if (s < mnpower) {
long add = mnpower - s;
cnt += add;
if (cnt > k)
return false;
s += add;
if (i < n)
diff[i + r + 1] -= add;
}
}
return true;
}
}
难度困难27
给你两个字符串 s
和 t
。
你可以从字符串 t
中删除任意数目的字符。
如果没有从字符串 t
中删除字符,那么得分为 0
,否则:
left
为删除字符中的最小下标。right
为删除字符中的最大下标。字符串的得分为 right - left + 1
。
请你返回使 t
成为 s
子序列的最小得分。
一个字符串的 子序列 是从原字符串中删除一些字符后(也可以一个也不删除),剩余字符不改变顺序得到的字符串。(比方说 "ace"
是 "***a\***b***c\***d***e\***"
的子序列,但是 "aec"
不是)。
示例 1:
输入:s = "abacaba", t = "bzaa"
输出:1
解释:这个例子中,我们删除下标 1 处的字符 "z" (下标从 0 开始)。
字符串 t 变为 "baa" ,它是字符串 "abacaba" 的子序列,得分为 1 - 1 + 1 = 1 。
1 是能得到的最小得分。
示例 2:
输入:s = "cde", t = "xyz"
输出:3
解释:这个例子中,我们将下标为 0, 1 和 2 处的字符 "x" ,"y" 和 "z" 删除(下标从 0 开始)。
字符串变成 "" ,它是字符串 "cde" 的子序列,得分为 2 - 0 + 1 = 3 。
3 是能得到的最小得分。
提示:
1 <= s.length, t.length <= 105
s
和 t
都只包含小写英文字母。class Solution {
public int minimumScore(String S, String T) {
char[] s = S.toCharArray(), t = T.toCharArray();
int n = s.length, m = t.length;
// 定义 pre[i] 为 s[:i] 对应的 t 的最长前缀的结束下标。
// 定义 suf[i] 为 s[i:] 对应的 t 的最长后缀的开始下标。
int[] suf = new int[n+1];
suf[n] = m;
for(int i = n-1, j = m-1; i >= 0; --i){
if(j >= 0 && s[i] == t[j]) --j;
suf[i] = j + 1;
}
int ans = suf[0]; // 删除t[:suf[0]]
if(ans == 0) return 0;
// 代码实现时,使用变量 j 来代替pre数组
for(int i = 0, j = 0; i < n; i++){
if(s[i] == t[j]) // 注意 j 不会等于 m,因为上面 suf[0]>0 表示 t 不是 s 的子序列
ans = Math.min(ans, suf[i+1] - ++j); // ++j 后,删除 t[j:suf[i+1]]
}
return ans;
}
}
难度困难24
给你一个长度为 n
下标从 0 开始的整数数组 nums
,它包含 1
到 n
的所有数字,请你返回上升四元组的数目。
如果一个四元组 (i, j, k, l)
满足以下条件,我们称它是上升的:
0 <= i < j < k < l < n
且nums[i] < nums[k] < nums[j] < nums[l]
。示例 1:
输入:nums = [1,3,2,4,5]
输出:2
解释:
- 当 i = 0 ,j = 1 ,k = 2 且 l = 3 时,有 nums[i] < nums[k] < nums[j] < nums[l] 。
- 当 i = 0 ,j = 1 ,k = 2 且 l = 4 时,有 nums[i] < nums[k] < nums[j] < nums[l] 。
没有其他的四元组,所以我们返回 2 。
示例 2:
输入:nums = [1,2,3,4]
输出:0
解释:只存在一个四元组 i = 0 ,j = 1 ,k = 2 ,l = 3 ,但是 nums[j] < nums[k] ,所以我们返回 0 。
提示:
4 <= nums.length <= 4000
1 <= nums[i] <= nums.length
nums
中所有数字 互不相同 ,nums
是一个排列。class Solution {
// nums[i] < nums[k] < nums[j] < nums[l]
// 枚举 j 和 k 这两个中间的,会更容易计算。
// 需要计算:
// 在 k 右侧的比 nums[j] 大的元素个数,记作 great[k][nums[j]];
// 在 j 左侧的比 nums[k] 小的元素个数,记作 less[j][nums[k]]。
// 对于固定的 j 和 k,根据乘法原理,对答案的贡献为less[j][nums[k]]⋅great[k][nums[j]]
public long countQuadruplets(int[] nums) {
int n = nums.length;
int[][] great = new int[n][n+1];
// 倒序遍历nums, 设x < nums[k+1], 对于 x,大于它的数的个数+1,即 great[k][x]+1
for(int k = n-2; k >= 2; k--){
great[k] = great[k+1].clone();
for(int x = nums[k+1] - 1; x > 0; x--)
great[k][x]++; // x < nums[k+1],对于 x,大于它的数的个数 +1
}
long ans = 0;
int[] less = new int[n+1];
for(int j = 1; j < n-2; j++){
for(int x = nums[j-1]+1; x <= n; x++)
less[x]++; // x > nums[j-1],对于 x,小于它的数的个数 +1
for (int k = j + 1; k < n - 1; k++)
if (nums[j] > nums[k])
ans += less[nums[k]] * great[k][nums[j]];
}
return ans;
}
}
周赛常客。如果你很难想出状态转移方程,以及递推的顺序,可以先从记忆化搜索开始思考,然后转换到递推上。
记忆化搜索像是自动挡,无需思考递推顺序,边界条件也容易确认;而递推像是手动挡,需要仔细确认递推的顺序以及初始值。但记忆化搜索并不是万能的,某些题目递推的写法可以结合数据结构等来优化时间复杂度。
题目 | 难度 | 备注 |
---|---|---|
2606. 找到最大开销的子字符串 | 1422 | 最大子数组和 |
2707. 字符串中的额外字符 | 1736 | 线性 DP |
2585. 获得分数的方法数 | 1910 | 背包 |
2547. 拆分数组的最小代价 | 2020 | 划分型 DP |
2597. 美丽子集的数目 | 2023 | 回溯/动态规划 |
2581. 统计可能的树根数目 | 2228 | 换根 DP |
2646. 最小化旅行的价格总和 | 2238 | 树形 DP |
2719. 统计整数数目 | 2355 | 数位 DP |
2713. 矩阵中严格递增的单元格数 | 2387 | DP+优化 |
2538. 最大价值和与最小价值和的差值 | 2398 | 树形 DP |
2572. 无平方子集计数 | 2420 | 0-1背包/子集状压DP |
2742. 给墙壁刷油漆 | 2425 | 线性DP/0-1背包 |
2617. 网格图中最少访问的格子数 | 2581 | 线段树/单调栈优化DP |
难度中等7
给你一个字符串 s
,一个字符 互不相同 的字符串 chars
和一个长度与 chars
相同的整数数组 vals
。
子字符串的开销 是一个子字符串中所有字符对应价值之和。空字符串的开销是 0
。
字符的价值 定义如下:
'a'
的价值为 1
,'b'
的价值为 2
,以此类推,'z'
的价值为 26
。chars
中的位置为 i
,那么它的价值就是 vals[i]
。请你返回字符串 s
的所有子字符串中的最大开销。
示例 1:
输入:s = "adaa", chars = "d", vals = [-1000]
输出:2
解释:字符 "a" 和 "d" 的价值分别为 1 和 -1000 。
最大开销子字符串是 "aa" ,它的开销为 1 + 1 = 2 。
2 是最大开销。
示例 2:
输入:s = "abc", chars = "abc", vals = [-1,-1,-1]
输出:0
解释:字符 "a" ,"b" 和 "c" 的价值分别为 -1 ,-1 和 -1 。
最大开销子字符串是 "" ,它的开销为 0 。
0 是最大开销。
提示:
1 <= s.length <= 105
s
只包含小写英文字母。1 <= chars.length <= 26
chars
只包含小写英文字母,且 互不相同 。vals.length == chars.length
-1000 <= vals[i] <= 1000
class Solution {
// 子数组的DP问题(代表:53. 最大子数组和)
public int maximumCostSubstring(String s, String chars, int[] vals) {
Map<Character, Integer> cost = new HashMap<>();
for(int i = 0; i < chars.length(); i++){
cost.put(chars.charAt(i), vals[i]);
}
for(int i = 0; i < 26; i++){
if(cost.containsKey((char)(i + 'a'))) continue;
cost.put((char)(i + 'a'), i+1);
}
int ans = 0;
int cur = 0;
for(int i = 0; i < s.length(); i++){
cur = Math.max(cur + cost.get(s.charAt(i)), 0);
ans = Math.max(ans, cur);
}
return ans;
}
}
难度中等22
给你一个下标从 0 开始的字符串 s
和一个单词字典 dictionary
。你需要将 s
分割成若干个 互不重叠 的子字符串,每个子字符串都在 dictionary
中出现过。s
中可能会有一些 额外的字符 不在任何子字符串中。
请你采取最优策略分割 s
,使剩下的字符 最少 。
示例 1:
输入:s = "leetscode", dictionary = ["leet","code","leetcode"]
输出:1
解释:将 s 分成两个子字符串:下标从 0 到 3 的 "leet" 和下标从 5 到 8 的 "code" 。只有 1 个字符没有使用(下标为 4),所以我们返回 1 。
示例 2:
输入:s = "sayhelloworld", dictionary = ["hello","world"]
输出:3
解释:将 s 分成两个子字符串:下标从 3 到 7 的 "hello" 和下标从 8 到 12 的 "world" 。下标为 0 ,1 和 2 的字符没有使用,所以我们返回 3 。
提示:
1 <= s.length <= 50
1 <= dictionary.length <= 50
1 <= dictionary[i].length <= 50
dictionary[i]
和 s
只包含小写英文字母。dictionary
中的单词互不相同。class Solution {
Set<String> set;
String s;
int[] cache;
public int minExtraChar(String s, String[] dictionary) {
set = new HashSet<>();
for(String d : dictionary) set.add(d);
this.s = s;
int n = s.length();
cache = new int[n+1];
Arrays.fill(cache, -1);
return dfs(n);
}
// 定义 dfs(i) 表示分割 s[:i] 剩下的最少字符
// 转移 对于 第 i 个字符,有选和不选两种情况
// 选(分割): dfs(i) = dfs(j) , s[j:i] 在字典中
// 不选(不分割)): dfs(i) = dfs(i-1)+1
// 取所有情况最小值
// 递归边界: dfs(0) = 0
// 递归入口: dfs(len(s))
public int dfs(int i){
if(i == 0) return 0;
if(cache[i] >= 0) return cache[i];
int res = i; // 最坏情况是s[:i]都不选,最大值即i
// 分割
for(int j = 0; j < i; j++){
if(set.contains(s.substring(j, i)))
res = Math.min(res, dfs(j));
}
// 不分割
res = Math.min(res, dfs(i-1)+1);
return cache[i] = res;
}
}
记忆化搜索转递推
class Solution {
public int minExtraChar(String s, String[] dictionary) {
Set<String> set = new HashSet<>();
for(String d : dictionary) set.add(d);
int n = s.length();
int[] f = new int[n+1];
for(int i = 0; i <= n; i++)
f[i] = i; // 最坏情况是s[:i]都不选,最大值即i
for(int i = 0; i <= n; i++){
// 分割
for(int j = 0; j < i; j++){
if(set.contains(s.substring(j, i)))
f[i] = Math.min(f[i], f[j]);
}
// 不分割
if(i > 0)
f[i] = Math.min(f[i], f[i-1]+1);
}
return f[n];
}
}
难度困难24
考试中有 n
种类型的题目。给你一个整数 target
和一个下标从 0 开始的二维整数数组 types
,其中 types[i] = [counti, marksi]
表示第 i
种类型的题目有 counti
道,每道题目对应 marksi
分。
返回你在考试中恰好得到 target
分的方法数。由于答案可能很大,结果需要对 109 +7
取余。
注意,同类型题目无法区分。
3
道同类型题目,那么解答第 1
和第 2
道题目与解答第 1
和第 3
道题目或者第 2
和第 3
道题目是相同的。示例 1:
输入:target = 6, types = [[6,1],[3,2],[2,3]]
输出:7
解释:要获得 6 分,你可以选择以下七种方法之一:
- 解决 6 道第 0 种类型的题目:1 + 1 + 1 + 1 + 1 + 1 = 6
- 解决 4 道第 0 种类型的题目和 1 道第 1 种类型的题目:1 + 1 + 1 + 1 + 2 = 6
- 解决 2 道第 0 种类型的题目和 2 道第 1 种类型的题目:1 + 1 + 2 + 2 = 6
- 解决 3 道第 0 种类型的题目和 1 道第 2 种类型的题目:1 + 1 + 1 + 3 = 6
- 解决 1 道第 0 种类型的题目、1 道第 1 种类型的题目和 1 道第 2 种类型的题目:1 + 2 + 3 = 6
- 解决 3 道第 1 种类型的题目:2 + 2 + 2 = 6
- 解决 2 道第 2 种类型的题目:3 + 3 = 6
示例 2:
输入:target = 5, types = [[50,1],[50,2],[50,5]]
输出:4
解释:要获得 5 分,你可以选择以下四种方法之一:
- 解决 5 道第 0 种类型的题目:1 + 1 + 1 + 1 + 1 = 5
- 解决 3 道第 0 种类型的题目和 1 道第 1 种类型的题目:1 + 1 + 1 + 2 = 5
- 解决 1 道第 0 种类型的题目和 2 道第 1 种类型的题目:1 + 2 + 2 = 5
- 解决 1 道第 2 种类型的题目:5
示例 3:
输入:target = 18, types = [[6,1],[3,2],[2,3]]
输出:1
解释:只有回答所有题目才能获得 18 分。
提示:
1 <= target <= 1000
n == types.length
1 <= n <= 50
types[i].length == 2
1 <= counti, marksi <= 50
class Solution {
private static final int MOD = (int)1e9+7;
int[][] types, cache;
public int waysToReachTarget(int target, int[][] types) {
this.types = types;
int n = types.length;
cache = new int[n][target+1];
for(int i = 0; i < n; i++)
Arrays.fill(cache[i], -1);
return dfs(n-1, target);
}
// 定义 dfs(i, score) 表示解答前 i 种题目,得分为 score 的方法数
// 转移 枚举第 i 种题目答对 1题....counti题
// 递归边界
// dfs(i < 0, score = 0) = 1
// dfs(i < 0, score != 0) = 0
// if(score < 0) return 0;
// 递归入口 dfs(len(types)-1, target)
public int dfs(int i, int score){
if(i < 0 && score == 0) return 1;
if(i < 0 && score != 0) return 0;
if(score < 0) return 0;
if(cache[i][score] >= 0) return cache[i][score];
int res = 0;
int count = types[i][0], mark = types[i][1];
for(int k = 0; k <= count; k++){
res = (res + dfs(i-1, score - (k*mark))) % MOD;
}
return cache[i][score] = res % MOD;
}
}
记忆化搜索转递推
class Solution {
/** 一比一翻译 递归入口:dfs(n-1, target)
==> 最外层循环 for i in range(n),第二层循环 for score in range(target+1)
计算状态时还有一层循环 for k in range(count)
把题目看成物品,把得分看成背包,则问题转化为,选择k个物品,塞入背包中,恰好装满的方案数
*/
private static final int MOD = (int)1e9+7;
public int waysToReachTarget(int target, int[][] types) {
int n = types.length;
int[][] f = new int[n+1][target+1];
f[0][0] = 1; // 恰好得到 target 分的方法数,只有f[0][0]才计算方案
for(int i = 0; i < n; i++){ // 先遍历物品
int count = types[i][0], mark = types[i][1];
for(int j = 0; j <= target; j++){ // 再遍历背包
for(int k = 0; k <= count && (k * mark) <= j; k++){
f[i+1][j] = (f[i+1][j] + f[i][j - k * mark]) % MOD;
}
}
}
return f[n][target];
}
}
难度困难25
给你一个整数数组 nums
和一个整数 k
。
将数组拆分成一些非空子数组。拆分的 代价 是每个子数组中的 重要性 之和。
令 trimmed(subarray)
作为子数组的一个特征,其中所有仅出现一次的数字将会被移除。
trimmed([3,1,2,4,3,4]) = [3,4,3,4]
。子数组的 重要性 定义为 k + trimmed(subarray).length
。
[1,2,3,3,3,4,4]
,trimmed([1,2,3,3,3,4,4]) = [3,3,3,4,4]
。这个子数组的重要性就是 k + 5
。找出并返回拆分 nums
的所有可行方案中的最小代价。
子数组 是数组的一个连续 非空 元素序列。
示例 1:
输入:nums = [1,2,1,2,1,3,3], k = 2
输出:8
解释:将 nums 拆分成两个子数组:[1,2], [1,2,1,3,3]
[1,2] 的重要性是 2 + (0) = 2 。
[1,2,1,3,3] 的重要性是 2 + (2 + 2) = 6 。
拆分的代价是 2 + 6 = 8 ,可以证明这是所有可行的拆分方案中的最小代价。
示例 2:
输入:nums = [1,2,1,2,1], k = 2
输出:6
解释:将 nums 拆分成两个子数组:[1,2], [1,2,1] 。
[1,2] 的重要性是 2 + (0) = 2 。
[1,2,1] 的重要性是 2 + (2) = 4 。
拆分的代价是 2 + 4 = 6 ,可以证明这是所有可行的拆分方案中的最小代价。
示例 3:
输入:nums = [1,2,1,2,1], k = 5
输出:10
解释:将 nums 拆分成一个子数组:[1,2,1,2,1].
[1,2,1,2,1] 的重要性是 5 + (3 + 2) = 10 。
拆分的代价是 10 ,可以证明这是所有可行的拆分方案中的最小代价。
提示:
1 <= nums.length <= 1000
0 <= nums[i] < nums.length
1 <= k <= 109
class Solution {
// 划分出第一个子数组,问题变成一个规模更小的子问题
// 由于[划分出长为 x 和 y 的子数组]和[划分出长为 y 和 x 的子数组]之后,剩余的子问题是相同的,因此这题适合用动态规划解决。
/*
* 划分型dp(优化) + 实时维护频率数组 state 和 出现 > 1 次的元素个数 trim
* 定义:f[i] 表示 [0,i-1] 的子数组的拆分数组的最小代价
* 状态转移:f[i] = k + min{f[j] + trim} , j=[0,i-1]
* 答案:f[n]
*/
public int minCost(int[] nums, int k) {
int n = nums.length;
// 定义 f[i] 表示拆分数组 nums[:i] 的最小代价
int[] f = new int[n+1];
int[] state = new int[n];
for(int i = 0; i < n; i++){
Arrays.fill(state, 0);
int trim = 0, mn = Integer.MAX_VALUE;
for(int j = i; j >= 0; j--){
int x = nums[j];
if(state[x] == 0){ // 首次出现
state[x] = 1;
}else if(state[x] == 1){ // 第二次出现(不再唯一)
state[x] = 2;
trim += 2;
}else{
trim += 1;
}
mn = Math.min(mn, f[j] + trim); // unique唯一元素不加入重要性的统计中
}
f[i+1] = k + mn; // mn 为拆分
}
return f[n];
}
}
难度中等31
给你一个由正整数组成的数组 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 <= 20
1 <= nums[i], k <= 1000
方法一:回溯 + 转换
在枚举 78.子集 的基础上加个判断。
在选择 x = nums[i]
的时候,如果之前选过 x+k
或 x-k
,则不能选,否则可以选。
class Solution:
# 枚举i选还是不选
def beautifulSubsets(self, nums: List[int], k: int) -> int:
ans = -1 # 去掉空集
cnt = [0] * (max(nums) + 2 * k)
def dfs(i: int) -> None:
if i == len(nums):
nonlocal ans
ans += 1
return
# 不选
dfs(i+1)
# 选
x = nums[i]
if cnt[x - k] == 0 and cnt[x + k] == 0:
cnt[x] += 1
dfs(i + 1)
cnt[x] -= 1
dfs(0)
return ans
class Solution:
# 枚举选哪个
def beautifulSubsets(self, nums: List[int], k: int) -> int:
ans = -1 # 去掉空集
cnt = [0] * (max(nums) + 2 * k)
def dfs(i: int) -> None:
nonlocal ans
ans += 1
if i == len(nums):
return
for j in range(i, len(nums)):
x = nums[j]
if cnt[x - k] == 0 and cnt[x + k] == 0:
cnt[x] += 1
dfs(j+1)
cnt[x] -= 1
dfs(0)
return ans
难度困难34
Alice 有一棵 n
个节点的树,节点编号为 0
到 n - 1
。树用一个长度为 n - 1
的二维整数数组 edges
表示,其中 edges[i] = [ai, bi]
,表示树中节点 ai
和 bi
之间有一条边。
Alice 想要 Bob 找到这棵树的根。她允许 Bob 对这棵树进行若干次 猜测 。每一次猜测,Bob 做如下事情:
u
和 v
,且树中必须存在边 [u, v]
。u
是 v
的 父节点 。Bob 的猜测用二维整数数组 guesses
表示,其中 guesses[j] = [uj, vj]
表示 Bob 猜 uj
是 vj
的父节点。
Alice 非常懒,她不想逐个回答 Bob 的猜测,只告诉 Bob 这些猜测里面 至少 有 k
个猜测的结果为 true
。
给你二维整数数组 edges
,Bob 的所有猜测和整数 k
,请你返回可能成为树根的 节点数目 。如果没有这样的树,则返回 0
。
示例 1:
输入:edges = [[0,1],[1,2],[1,3],[4,2]], guesses = [[1,3],[0,1],[1,0],[2,4]], k = 3
输出:3
解释:
根为节点 0 ,正确的猜测为 [1,3], [0,1], [2,4]
根为节点 1 ,正确的猜测为 [1,3], [1,0], [2,4]
根为节点 2 ,正确的猜测为 [1,3], [1,0], [2,4]
根为节点 3 ,正确的猜测为 [1,0], [2,4]
根为节点 4 ,正确的猜测为 [1,3], [1,0]
节点 0 ,1 或 2 为根时,可以得到 3 个正确的猜测。
提示:
edges.length == n - 1
2 <= n <= 105
1 <= guesses.length <= 105
0 <= ai, bi, uj, vj <= n - 1
ai != bi
uj != vj
edges
表示一棵有效的树。guesses[j]
是树中的一条边。guesses
是唯一的。0 <= k <= guesses.length
class Solution {
private List<Integer>[] g;
private Set<Long> s = new HashSet<>();
private int k, ans, cnt0;
public int rootCount(int[][] edges, int[][] guesses, int k) {
this.k = k;
g = new ArrayList[edges.length+1];
Arrays.setAll(g, e -> new ArrayList<>());
for(int[] e : edges){
int x = e[0], y = e[1];
g[x].add(y);
g[y].add(x); // 建图
}
for(int[] e : guesses){ // guesses 转换成哈希表,加快查询速度
s.add((long) e[0] << 32 | e[1]); // 两个 4 字节数压缩成一个 8 字节数Long
}
dfs(0, -1);
reroot(0,-1,cnt0);
return ans;
}
private void dfs(int x, int fa){
for(int y : g[x]){
if(y != fa){
if(s.contains((long) x << 32 | y)) // 以 0 为根时,猜对了
cnt0++;
dfs(y,x);
}
}
}
private void reroot(int x, int fa, int cnt){
if(cnt >= k) ans++;// 此时 cnt 就是以 x 为根时的猜对次数
for(int y : g[x]){
if(y != fa){
int c = cnt;
if(s.contains((long) x << 32 | y)) c--;// 原来是对的,现在错了
if(s.contains((long) y << 32 | x)) c++;// 原来是对的,现在错了
reroot(y,x,c);
}
}
}
}
难度困难31
现有一棵无向、无根的树,树中有 n
个节点,按从 0
到 n - 1
编号。给你一个整数 n
和一个长度为 n - 1
的二维整数数组 edges
,其中 edges[i] = [ai, bi]
表示树中节点 ai
和 bi
之间存在一条边。
每个节点都关联一个价格。给你一个整数数组 price
,其中 price[i]
是第 i
个节点的价格。
给定路径的 价格总和 是该路径上所有节点的价格之和。
另给你一个二维整数数组 trips
,其中 trips[i] = [starti, endi]
表示您从节点 starti
开始第 i
次旅行,并通过任何你喜欢的路径前往节点 endi
。
在执行第一次旅行之前,你可以选择一些 非相邻节点 并将价格减半。
返回执行所有旅行的最小价格总和。
示例 1:
输入:n = 4, edges = [[0,1],[1,2],[1,3]], price = [2,2,10,6], trips = [[0,3],[2,1],[2,3]]
输出:23
解释:
上图表示将节点 2 视为根之后的树结构。第一个图表示初始树,第二个图表示选择节点 0 、2 和 3 并使其价格减半后的树。
第 1 次旅行,选择路径 [0,1,3] 。路径的价格总和为 1 + 2 + 3 = 6 。
第 2 次旅行,选择路径 [2,1] 。路径的价格总和为 2 + 5 = 7 。
第 3 次旅行,选择路径 [2,1,3] 。路径的价格总和为 5 + 2 + 3 = 10 。
所有旅行的价格总和为 6 + 7 + 10 = 23 。可以证明,23 是可以实现的最小答案。
示例 2:
输入:n = 2, edges = [[0,1]], price = [2,2], trips = [[0,0]]
输出:1
解释:
上图表示将节点 0 视为根之后的树结构。第一个图表示初始树,第二个图表示选择节点 0 并使其价格减半后的树。
第 1 次旅行,选择路径 [0] 。路径的价格总和为 1 。
所有旅行的价格总和为 1 。可以证明,1 是可以实现的最小答案。
提示:
1 <= n <= 50
edges.length == n - 1
0 <= ai, bi <= n - 1
edges
表示一棵有效的树price.length == n
price[i]
是一个偶数1 <= price[i] <= 1000
1 <= trips.length <= 100
0 <= starti, endi <= n - 1
方法一:树形DP,暴力DFS每一条路径,计算每一个节点的经过次数,然后写一个打家劫舍
class Solution {
/**
1. 计算每个点经过的次数 cnt(贡献法思想:计算每个点对答案能贡献多少)
2. 写一个树形DP求答案,知道了每个点会被经过多少次,把price[i] 更新成 price[i]*cnt[i]
问题就变成了计算减半后的price[i]之和的最小值
随便从一个点出发DFS,对节点 x 及其儿子 y,分类讨论:
如果 price[x] 不变,那么 price[y] 可以减半,也可以不变,取这两种情况的最小值
如果 price[x] 减半,那么 price[y] 只能不变
因此,子树 x 需要返回两个值:
price[x] 不变时的子树 x 的最小价值总和
price[x] 减半时的子树 x 的最小价值总和
*/
private List<Integer>[] g;
private int[] price, cnt;
private int end;
public int minimumTotalPrice(int n, int[][] edges, int[] price, int[][] trips) {
g = new ArrayList[n];
Arrays.setAll(g, e -> new ArrayList<>());
for(int[] e : edges){
int x = e[0], y = e[1];
g[x].add(y);
g[y].add(x);
}
this.price = price;
cnt = new int[n]; // 每个节点在路径上经过的次数
for(int[] t : trips){
end = t[1];
path(t[0], -1);
}
int[] p = dfs(0, -1);
return Math.min(p[0], p[1]);
}
// 找到从 x 到 end 的路径,将经过的节点次数cnt[i] + 1
public boolean path(int x, int fa){
if(x == end){ // 到达终点(注意树只有唯一的一条简单路径)
++cnt[x]; // 统计从 start 到 end 的路径上的点经过了多少次
return true; // 找到终点
}
for(int y : g[x])
if(y != fa && path(y, x)){
cnt[x]++; // 统计从 start 到 end 的路径上的点经过了多少次
return true; // 找到终点
}
return false;
}
// 类似 337. 打家劫舍Ⅲ
// 返回 (price[x] 不变时的子树 x 的最小价值总和, price[x] 减半时的子树 x 的最小价值总和)
public int[] dfs(int x, int fa){
int notHalve = price[x] * cnt[x]; // x 不变
int halve = notHalve / 2; // x 减半
for(int y : g[x]){
if(y != fa){
int[] p = dfs(y, x); // 计算 y 不变/减半的最小价值总和
notHalve += Math.min(p[0], p[1]); // x 不变,那么 y 可以不变,可以减半,取这两种情况的最小值
halve += p[0]; // x 减半,那么 y 只能不变
}
}
return new int[]{notHalve, halve};
}
}
方法二:树上差分 + Tarjan离线
难度困难25
给你两个数字字符串 num1
和 num2
,以及两个整数 max_sum
和 min_sum
。如果一个整数 x
满足以下条件,我们称它是一个好整数:
num1 <= x <= num2
min_sum <= digit_sum(x) <= max_sum
.请你返回好整数的数目。答案可能很大,请返回答案对 109 + 7
取余后的结果。
注意,digit_sum(x)
表示 x
各位数字之和。
示例 1:
输入:num1 = "1", num2 = "12", min_num = 1, max_num = 8
输出:11
解释:总共有 11 个整数的数位和在 1 到 8 之间,分别是 1,2,3,4,5,6,7,8,10,11 和 12 。所以我们返回 11 。
示例 2:
输入:num1 = "1", num2 = "5", min_num = 1, max_num = 5
输出:5
解释:数位和在 1 到 5 之间的 5 个整数分别为 1,2,3,4 和 5 。所以我们返回 5 。
提示:
1 <= num1 <= num2 <= 1022
1 <= min_sum <= max_sum <= 400
题解:数位DP:dfs(nums2) - dfs(nums1-1)
class Solution {
private static final int MOD = (int)1e9 + 7;
int min_sum, max_sum;
int[][] cache;
public int count(String num1, String num2, int min_sum, int max_sum) {
this.min_sum = min_sum;
this.max_sum = max_sum;
int m = num2.toCharArray().length;
cache = new int[m][max_sum + 1];
for(int i = 0; i < m; i++){
Arrays.fill(cache[i], -1);
}
char[] s2 = num2.toCharArray();
int a = dfs(0, 0, true, s2);
for(int i = 0; i < m; i++){
Arrays.fill(cache[i], -1);
}
char[] s1 = num1.toCharArray();
s1[s1.length - 1]--;
int b = dfs(0, 0, true, s1);
return (a - b + MOD) % MOD;
}
public int dfs(int i, int sum, boolean isLimit, char[] s){
if(i == s.length){
return sum >= min_sum ? 1 : 0;
}
if(!isLimit && cache[i][sum] >= 0) return cache[i][sum];
int res = 0;
int up = isLimit ? s[i] - '0' : 9;
for(int d = 0; d <= up; d++){
if((sum + d) <= max_sum)
res = (res + dfs(i+1, sum+d, isLimit & (d == up), s)) % MOD;
}
if(!isLimit) cache[i][sum] = res % MOD;
return res % MOD;
}
}
难度困难34
给你一个下标从 1 开始、大小为 m x n
的整数矩阵 mat
,你可以选择任一单元格作为 起始单元格 。
从起始单元格出发,你可以移动到 同一行或同一列 中的任何其他单元格,但前提是目标单元格的值 严格大于 当前单元格的值。
你可以多次重复这一过程,从一个单元格移动到另一个单元格,直到无法再进行任何移动。
请你找出从某个单元开始访问矩阵所能访问的 单元格的最大数量 。
返回一个表示可访问单元格最大数量的整数。
示例 3:
输入:mat = [[3,1,6],[-9,5,7]]
输出:4
解释:上图展示了从第 2 行、第 1 列的单元格开始,可以访问 4 个单元格。可以证明,无论从哪个单元格开始,最多只能访问 4 个单元格,因此答案是 4 。
提示:
m == mat.length
n == mat[i].length
1 <= m, n <= 105
1 <= m * n <= 105
-105 <= mat[i][j] <= 105
class Solution {
/**
从格子 mat[i][j] 出发往其他格子能走多少 等价于 从其他格子走到 mat[i][j] 能走多少
==> 定义 f[i][j] 表示到达mat[i][j]是所访问的单元格的最大数量
在枚举转移来源时,不需要知道具体从哪个单元格转移过来,只需要知道转移来源的f的最大值是多少
按照元素值从小到大计算,那么第 i 行比 mat[i][j] 小的f值都算出来了,比 mat[i][j] 大的还没计算
==> 对于第 i 行,相当于取这一行的 f 值的最大值,作为转移来源的值
用长为 m 的数组rowmax维护每一行的 f 值的最大值
用长为 n 的数组colmax维护每一列的 f 值的最大值
转移方程: f[i][j] = max(rowmax[i], colmax[j]) + 1
细节: 代码实现时,f[i][j] 可以忽略,因为只需要每行每列的 f 值的最大值
对于相同元素,全部计算出最大值后,再更新到rowmax和colmax中
*/
public int maxIncreasingCells(int[][] mat) {
Map<Integer, List<int[]>> g = new TreeMap<>();
int m = mat.length, n = mat[0].length;
for (int i = 0; i < m; i++)
for (int j = 0; j < n; j++)
// 相同元素放在同一组,统计位置
g.computeIfAbsent(mat[i][j], k -> new ArrayList<>()).add(new int[]{i, j});
int ans = 0;
int[] rowMax = new int[m], colMax = new int[n];
for(List<int[]> pos : g.values()){
int[] mx = new int[pos.size()]; // 先把最大值算出来,再更新 rowMax 和 colMax
for(int i = 0; i < pos.size(); i++){
// 因为是从小到大计算
// 所以可以保证 rowmax[i] 和 colmax[j] 中转移来源的值一定 < 当前位置pos[i][j]的值
mx[i] = Math.max(rowMax[pos.get(i)[0]], colMax[pos.get(i)[1]]) + 1;
ans = Math.max(ans, mx[i]);
}
for(int k = 0; k < pos.size(); k++){
int i = pos.get(k)[0], j = pos.get(k)[1];
rowMax[i] = Math.max(rowMax[i], mx[k]); // 更新第 i 行的最大 f 值
colMax[j] = Math.max(colMax[j], mx[k]); // 更新第 j 列的最大 f 值
}
}
return ans;
}
}
难度困难38
给你一个 n
个节点的无向无根图,节点编号为 0
到 n - 1
。给你一个整数 n
和一个长度为 n - 1
的二维整数数组 edges
,其中 edges[i] = [ai, bi]
表示树中节点 ai
和 bi
之间有一条边。
每个节点都有一个价值。给你一个整数数组 price
,其中 price[i]
是第 i
个节点的价值。
一条路径的 价值和 是这条路径上所有节点的价值之和。
你可以选择树中任意一个节点作为根节点 root
。选择 root
为根的 开销 是以 root
为起点的所有路径中,价值和 最大的一条路径与最小的一条路径的差值。
请你返回所有节点作为根节点的选择中,最大 的 开销 为多少。
示例 1:
输入:n = 6, edges = [[0,1],[1,2],[1,3],[3,4],[3,5]], price = [9,8,7,6,10,5]
输出:24
解释:上图展示了以节点 2 为根的树。左图(红色的节点)是最大价值和路径,右图(蓝色的节点)是最小价值和路径。
- 第一条路径节点为 [2,1,3,4]:价值为 [7,8,6,10] ,价值和为 31 。
- 第二条路径节点为 [2] ,价值为 [7] 。
最大路径和与最小路径和的差值为 24 。24 是所有方案中的最大开销。
提示:
1 <= n <= 105
edges.length == n - 1
0 <= ai, bi <= n - 1
edges
表示一棵符合题面要求的树。price.length == n
1 <= price[i] <= 105
题解:0x3f:https://leetcode.cn/problems/difference-between-maximum-and-minimum-price-sum/solution/by-endlesscheng-5l70/
1、由于价值都是正数,因此价值和最小的一条路径一定只有一个点。
2、根据提示 1,「价值和最大的一条路径与最小的一条路径的差值」等价于「去掉路径的一个端点」。
3、由于价值都是正数,一条路径能延长就尽量延长,这样路径和就越大,那么最优是延长到叶子。根据提示 2,问题转换成去掉一个叶子后的最大路径和(这里的叶子严格来说是度为 1 的点,因为根的度数也可能是 1)。
4、最大路径和是一个经典树形 DP 问题,类似「树的直径」。由于我们需要去掉一个叶子,那么可以让子树返回两个值:
对于当前节点,它有多颗子树,我们一颗颗 DFS,假设当前 DFS 完了其中一颗子树,它返回了「当前带叶子的路径和」和「当前不带叶子的路径和」,那么答案有两种情况:
然后更新「最大带叶子的路径和」和「最大不带叶子的路径和」。
最后返回「最大带叶子的路径和」和「最大不带叶子的路径和」,用来供父节点计算。
class Solution {
List<Integer>[] g;
int n;
long res;
int[] price;
public long maxOutput(int n, int[][] edges, int[] price) {
this.n = n;
g = new ArrayList[n];
Arrays.setAll(g, e -> new ArrayList<>());
for(int[] e : edges){
int x = e[0], y = e[1];
g[x].add(y);
g[y].add(x);
}
this.price = price;
dfs(0, -1);
return res;
}
// 返回带叶子的最大路径和,不带叶子的最大路径和
public long[] dfs(int x, int fa){
// s1 带叶子的最大路径和 ; s2 不带叶子的最大路径和
long p = price[x], maxS1 = p, maxS2 = 0;
for(int y : g[x]){
if(y != fa){
long[] result = dfs(y, x);
long s1 = result[0], s2 = result[1];
// 前面最大带叶子的路径和 + 当前不带叶子的路径和
// 前面最大不带叶子的路径和 + 当前带叶子的路径和
res = Math.max(res, Math.max(maxS1 + s2, maxS2 + s1));
// 这里加上 p 是因为 x 必然不是叶子
maxS1 = Math.max(maxS1, s1 + p);
maxS2 = Math.max(maxS2, s2 + p);
}
}
return new long[]{maxS1, maxS2};
}
}
难度中等31
给你一个正整数数组 nums
。
如果数组 nums
的子集中的元素乘积是一个 无平方因子数 ,则认为该子集是一个 无平方 子集。
无平方因子数 是无法被除 1
之外任何平方数整除的数字。
返回数组 nums
中 无平方 且 非空 的子集数目。因为答案可能很大,返回对 109 + 7
取余的结果。
nums
的 非空子集 是可以由删除 nums
中一些元素(可以不删除,但不能全部删除)得到的一个数组。如果构成两个子集时选择删除的下标不同,则认为这两个子集不同。
示例 1:
输入:nums = [3,4,4,5]
输出:3
解释:示例中有 3 个无平方子集:
- 由第 0 个元素 [3] 组成的子集。其元素的乘积是 3 ,这是一个无平方因子数。
- 由第 3 个元素 [5] 组成的子集。其元素的乘积是 5 ,这是一个无平方因子数。
- 由第 0 个和第 3 个元素 [3,5] 组成的子集。其元素的乘积是 15 ,这是一个无平方因子数。
可以证明给定数组中不存在超过 3 个无平方子集。
示例 2:
输入:nums = [1]
输出:1
解释:示例中有 1 个无平方子集:
- 由第 0 个元素 [1] 组成的子集。其元素的乘积是 1 ,这是一个无平方因子数。
可以证明给定数组中不存在超过 1 个无平方子集。
提示:
1 <= nums.length <= 1000
1 <= nums[i] <= 30
class Solution {
/**
把每个无平方因字数 SF 对应的质因子集合,表示成一个二进制数
枚举子集的元素乘积,把乘积看成一个背包,装能够组成元素乘积的数字
*/
private static final int[] PRIMES = new int[]{2, 3, 5, 7, 11, 13, 17, 19, 23, 29}; // 给出小于等于 30 的质数集合
private static final int MOD = (int) 1e9 + 7, MX = 30, N_PRIMES = PRIMES.length, M = 1 << N_PRIMES;
private static final int[] SF_TO_MASK = new int[MX + 1]; // SF_TO_MASK[i] 为 i 的质因子集合(用二进制表示)
// 对于每个整数,将其质因数集合用二进制表示出来
// SF_TO_MASK[i] 的二进制第 j 位为 1 表示 i 可以被 PRIMES[j] 整除
// SF_TO_MASK[i] 的值为 -1 表示 i 存在平方因子
static {
for (int i = 2; i <= MX; ++i) // 值域:1 <= nums[i] <= 30
for (int j = 0; j < N_PRIMES; ++j) {
int p = PRIMES[j];
if (i % p == 0) { // p 是 i 的因子
if (i % (p * p) == 0) { // 有平方因子
SF_TO_MASK[i] = -1;
break;
}
SF_TO_MASK[i] |= 1 << j; // 把 j 加到集合中
}
}
}
public int squareFreeSubsets(int[] nums) {
var f = new int[M]; // f[j] 表示恰好组成质数集合 j 的方案数
f[0] = 1; // 质数集合是空集的方案数为 1
for (int x : nums) { //
int mask = SF_TO_MASK[x]; // 计算当前数的质因数集合
if (mask >= 0) // x 是 SF , 当当前数不包含平方因子时才进行更新
for (int j = M - 1; j >= mask; --j)
if ((j | mask) == j) // mask 是 j 的子集, 如果 j 包含 mask 的质因数集合,则可以更新 f[j]
// 把mask装到能容纳 mask 的背包 j 中
f[j] = (f[j] + f[j ^ mask]) % MOD; // 不选 mask + 选 mask
}
// f 数组中的所有值加起来就是无平方非空子集的总数,因为空集不算,所以要减去 1
var ans = 0L;
for (int v : f) ans += v;
return (int) ((ans - 1) % MOD); // -1 去掉空集(nums 的空子集)
}
}
难度困难25
给你两个长度为 n
下标从 0 开始的整数数组 cost
和 time
,分别表示给 n
堵不同的墙刷油漆需要的开销和时间。你有两名油漆匠:
i
堵墙需要花费 time[i]
单位的时间,开销为 cost[i]
单位的钱。1
单位,开销为 0
。但是必须在付费油漆匠 工作 时,免费油漆匠才会工作。请你返回刷完 n
堵墙最少开销为多少。
示例 1:
输入:cost = [1,2,3,2], time = [1,2,3,2]
输出:3
解释:下标为 0 和 1 的墙由付费油漆匠来刷,需要 3 单位时间。同时,免费油漆匠刷下标为 2 和 3 的墙,需要 2 单位时间,开销为 0 。总开销为 1 + 2 = 3 。
示例 2:
输入:cost = [2,3,4,2], time = [1,1,1,1]
输出:4
解释:下标为 0 和 3 的墙由付费油漆匠来刷,需要 2 单位时间。同时,免费油漆匠刷下标为 1 和 2 的墙,需要 2 单位时间,开销为 0 。总开销为 2 + 2 = 4 。
提示:
1 <= cost.length <= 500
cost.length == time.length
1 <= cost[i] <= 106
1 <= time[i] <= 500
class Solution {
// 付费的油漆匠 选或不选,只有一个付费油漆匠,time[i]需要加起来
// 如果第 i 面墙分配给免费的油漆匠,那么消耗 1 单位的时间
// 定义dfs(i,j):刷完0 ~ i 的墙,且当前累计付费时间为j(已经赊给免费的时间),最少开销为多少
// 付费:dfs(i,j) = dfs(i-1, j+time[i]) + cost[i]
// 免费:dfs(i,j) = dfs(i-1, j-1)
// 转移:dfs(i,j) = min(dfs(i-1, j+time[i])+cost[i], dfs(i-1, j-1))
// 递归边界:dfs(-1, <0) = inf ; dfs(i,j) = 0 if j > i(时间超过了剩下需要刷的墙)
// 递归入口:dfs(n-1, 0)
int[] cost, time;
int[][] cache;
int n;
public int paintWalls(int[] cost, int[] time) {
this.cost = cost;
this.time = time;
n = cost.length;
cache = new int[n][2*n+1]; // 免费时长可以为负数,因此需要加偏移量
for(int i = 0; i < n; i++)
Arrays.fill(cache[i], -1);
return dfs(n-1, n); // 偏移量防止负数
}
// 免费时长为j,刷前i片墙需要的最小花费
public int dfs(int i, int j){
if(i < j-n) return 0; // 剩余所有墙都可以由免费油漆工刷
if(i < 0) return Integer.MAX_VALUE / 2;
if(cache[i][j] >= 0) return cache[i][j];
// 免费油漆工刷 dfs(i-1, j-1)
// 付费油漆工刷 dfs(i-1, j+time[i])+cost[i]
return cache[i][j] = Math.min(dfs(i-1, j+time[i])+cost[i], dfs(i-1, j-1));
}
}
难度困难17
给你一个下标从 0 开始的 m x n
整数矩阵 grid
。你一开始的位置在 左上角 格子 (0, 0)
。
当你在格子 (i, j)
的时候,你可以移动到以下格子之一:
j < k <= grid[i][j] + j
的格子 (i, k)
(向右移动),或者i < k <= grid[i][j] + i
的格子 (k, j)
(向下移动)。请你返回到达 右下角 格子 (m - 1, n - 1)
需要经过的最少移动格子数,如果无法到达右下角格子,请你返回 -1
。
示例 1:
输入:grid = [[3,4,2,1],[4,2,3,1],[2,1,0,0],[2,4,0,0]]
输出:4
解释:上图展示了到达右下角格子经过的 4 个格子。
提示:
m == grid.length
n == grid[i].length
1 <= m, n <= 105
1 <= m * n <= 105
0 <= grid[i][j] < m * n
grid[m - 1][n - 1] == 0
class Solution {
/**
(0, 0) -> (0, 1) -> (1, 1)
(0, 0) -> (1, 0) -> (1, 1)
有重叠子问题 -> 用动态规划来优化
dfs(0, 0) -> dfs(x, y)
g = grid[x][y]
dfs(i, j) 枚举k,计算: min(dfs(i, k), k in [j+1,j+g])
计算: min(dfs(k, j), k in [i+1,i+g])
优化前: O(mn log(m+n))
f[i][j] = min {
min(f[i][k], k in [j+1,j+g])
min(f[k][j], k in [i+1,i+g])
}
i和j倒序枚举
从右往左计算的时候,如果计算出f[i][j] <= 之前的数,那么之前的数就没用了
==> 单调栈 从底往顶,值和下标不断变大的单调栈
*/
public int minimumVisitedCells(int[][] grid) {
int m = grid.length, n = grid[0].length, mn = 0;
List<int[]>[] colSt = new ArrayList[n]; // 每列的单调栈
Arrays.setAll(colSt, e -> new ArrayList<int[]>());
for (int i = m - 1; i >= 0; --i) {
var st = new ArrayList<int[]>(); // 当前行的单调栈
for (int j = n - 1; j >= 0; --j) {
var st2 = colSt[j];
mn = Integer.MAX_VALUE;
int g = grid[i][j];
if (i == m - 1 && j == n - 1) // 特殊情况:已经是终点
mn = 0;
else if (g > 0) {
// 在单调栈上二分
int k = search(st, j + g);
if (k < st.size()) mn = Math.min(mn, st.get(k)[0]);
k = search(st2, i + g);
if (k < st2.size()) mn = Math.min(mn, st2.get(k)[0]);
}
if (mn == Integer.MAX_VALUE) continue;
++mn; // 加上 (i,j) 这个格子
// 插入单调栈
while (!st.isEmpty() && mn <= st.get(st.size() - 1)[0])
st.remove(st.size() - 1);
st.add(new int[]{mn, j});
while (!st2.isEmpty() && mn <= st2.get(st2.size() - 1)[0])
st2.remove(st2.size() - 1);
st2.add(new int[]{mn, i});
}
}
return mn < Integer.MAX_VALUE ? mn : -1;
}
// 见 https://www.bilibili.com/video/BV1AP41137w7/
private int search(List<int[]> st, int target) {
int left = -1, right = st.size(); // 开区间 (left, right)
while (left + 1 < right) { // 区间不为空
int mid = (left + right) >>> 1;
if (st.get(mid)[1] > target) left = mid; // 范围缩小到 (mid, right)
else right = mid; // 范围缩小到 (left, mid)
}
return right;
}
}
包含 DFS、BFS、拓扑排序、最短路等。
题目 | 难度 | 备注 |
---|---|---|
2641. 二叉树的堂兄弟节点 II | 1677 | BFS |
2684. 矩阵中移动的最大次数 | 1626 | 多源BFS/网格图DP |
2685. 统计完全连通分量的数量 | 1769 | DFS |
2642. 设计可以求最短路径的图类 | 1811 | Dijkstra/Floyd 模板 |
2608. 图中的最短环 | 1904 | BFS |
2662. 前往目标的最小代价 | 2154 | Dijkstra |
2577. 在网格图中访问一个格子的最少时间 | 2382 | Dijkstra/二分答案+BFS |
2603. 收集树中金币 | 2712 | 拓扑排序 |
2699. 修改图中的边权 | 2874 | Dijkstra |
难度中等9
给你一棵二叉树的根 root
,请你将每个节点的值替换成该节点的所有 堂兄弟节点值的和 。
如果两个节点在树中有相同的深度且它们的父节点不同,那么它们互为 堂兄弟 。
请你返回修改值之后,树的根 root
。
注意,一个节点的深度指的是从树根节点到这个节点经过的边数。
示例 1:
输入:root = [5,4,9,1,10,null,7]
输出:[0,0,0,7,7,null,11]
解释:上图展示了初始的二叉树和修改每个节点的值之后的二叉树。
- 值为 5 的节点没有堂兄弟,所以值修改为 0 。
- 值为 4 的节点没有堂兄弟,所以值修改为 0 。
- 值为 9 的节点没有堂兄弟,所以值修改为 0 。
- 值为 1 的节点有一个堂兄弟,值为 7 ,所以值修改为 7 。
- 值为 10 的节点有一个堂兄弟,值为 7 ,所以值修改为 7 。
- 值为 7 的节点有两个堂兄弟,值分别为 1 和 10 ,所以值修改为 11 。
提示:
[1, 105]
。1 <= Node.val <= 104
题解:站在父亲的视角上修改孩子的值 ==> BFS
class Solution {
public TreeNode replaceValueInTree(TreeNode root) {
if(root == null) return root;
root.val = 0;
List<TreeNode> dq = new ArrayList<>();
dq.add(root);
while(dq.size() > 0){
List<TreeNode> cur = new ArrayList<>();
int sum = 0;
for(TreeNode t : dq){
if(t.left != null){
sum += t.left.val;
cur.add(t.left);
}
if(t.right != null){
sum += t.right.val;
cur.add(t.right);
}
}
for(TreeNode t : dq){
int tot = (t.left == null ? 0 : t.left.val) + (t.right == null ? 0 : t.right.val);
if(t.left != null) t.left.val = sum - tot;
if(t.right != null) t.right.val = sum - tot;
}
dq = cur;
}
return root;
}
}
python
class Solution:
def replaceValueInTree(self, root: Optional[TreeNode]) -> Optional[TreeNode]:
if not root: return root
root.val = 0
q = [root]
while q:
cur = []
s = 0
for t in q:
if t.left:
s += t.left.val
cur.append(t.left)
if t.right:
s += t.right.val
cur.append(t.right)
for t in q:
cs = (t.left.val if t.left else 0) + (t.right.val if t.right else 0)
if t.left: t.left.val = s - cs
if t.right: t.right.val = s - cs
q = cur
return root
难度中等10
给你一个下标从 0 开始、大小为 m x n
的矩阵 grid
,矩阵由若干 正 整数组成。
你可以从矩阵第一列中的 任一 单元格出发,按以下方式遍历 grid
:
(row, col)
可以移动到 (row - 1, col + 1)
、(row, col + 1)
和 (row + 1, col + 1)
三个单元格中任一满足值 严格 大于当前单元格的单元格。返回你在矩阵中能够 移动 的 最大 次数。
示例 1:
输入:grid = [[2,4,3,5],[5,4,9,3],[3,4,2,11],[10,9,13,15]]
输出:3
解释:可以从单元格 (0, 0) 开始并且按下面的路径移动:
- (0, 0) -> (0, 1).
- (0, 1) -> (1, 2).
- (1, 2) -> (2, 3).
可以证明这是能够移动的最大次数。
提示:
m == grid.length
n == grid[i].length
2 <= m, n <= 1000
4 <= m * n <= 105
1 <= grid[i][j] <= 106
方法一:网格图DP
记忆化搜索
class Solution:
def maxMoves(self, grid: List[List[int]]) -> int:
m, n = len(grid), len(grid[0])
@cache
def dfs(i, j: int) -> int:
if j == n-1:
return 0
res = 0
for k in i-1, i, i+1:
if 0 <= k < m and grid[k][j+1] > grid[i][j]:
res = max(res, dfs(k, j+1) + 1)
return res
return max(dfs(k, 0) for k in range(m))
方法二:多源BFS
class Solution:
# 多源BFS,每轮向右搜索一列
def maxMoves(self, grid: List[List[int]]) -> int:
m, n = len(grid), len(grid[0])
q = range(m) # 从第0列开始,共m行个元素
vis = [-1] * m
for j in range(n - 1): # 遍历每一列
tmp = q
q = []
for i in tmp: # 每一列中还存在的行元素 i
for k in i - 1, i, i + 1:
if 0 <= k < m and vis[k] != j and grid[k][j+1] > grid[i][j]:
vis[k] = j # 时间戳标记,避免重复创建 vis 数组
q.append(k)
if len(q) == 0:
return j
return n-1
难度中等15
给你一个整数 n
。现有一个包含 n
个顶点的 无向 图,顶点按从 0
到 n - 1
编号。给你一个二维整数数组 edges
其中 edges[i] = [ai, bi]
表示顶点 ai
和 bi
之间存在一条 无向 边。
返回图中 完全连通分量 的数量。
如果在子图中任意两个顶点之间都存在路径,并且子图中没有任何一个顶点与子图外部的顶点共享边,则称其为 连通分量 。
如果连通分量中每对节点之间都存在一条边,则称其为 完全连通分量 。
提示:
1 <= n <= 50
0 <= edges.length <= n * (n - 1) / 2
edges[i].length == 2
0 <= ai, bi <= n - 1
ai != bi
题解:如何在一个dfs中计算完全联通分量的边个数和点个数?
class Solution:
# 完全联通分量,点的个数和边的个数的关系:e = v*(v-1) / 2
def countCompleteComponents(self, n: int, edges: List[List[int]]) -> int:
g = [[] for _ in range(n)]
for x, y in edges:
g[x].append(y)
g[y].append(x)
def dfs(x: int) -> int:
vis[x] = True
nonlocal v, e
v += 1
e += len(g[x])
for y in g[x]:
if not vis[y]:
dfs(y)
vis = [False] * n
ans = 0
for i in range(n):
if not vis[i]:
e = v = 0
dfs(i)
# 由于上面统计的时候,一条边统计了两次,所以代码中的判断条件是 e == v*(v-1)
ans += e == v*(v-1)
return ans
难度困难5
给你一个有 n
个节点的 有向带权 图,节点编号为 0
到 n - 1
。图中的初始边用数组 edges
表示,其中 edges[i] = [fromi, toi, edgeCosti]
表示从 fromi
到 toi
有一条代价为 edgeCosti
的边。
请你实现一个 Graph
类:
Graph(int n, int[][] edges)
初始化图有 n
个节点,并输入初始边。addEdge(int[] edge)
向边集中添加一条边,其中 edge = [from, to, edgeCost]
。数据保证添加这条边之前对应的两个节点之间没有有向边。int shortestPath(int node1, int node2)
返回从节点 node1
到 node2
的路径 最小 代价。如果路径不存在,返回 -1
。一条路径的代价是路径中所有边代价之和。提示:
1 <= n <= 100
0 <= edges.length <= n * (n - 1)
edges[i].length == edge.length == 3
0 <= fromi, toi, from, to, node1, node2 <= n - 1
1 <= edgeCosti, edgeCost <= 106
addEdge
至多 100
次。shortestPath
至多 100
次。方法一:Dijkstra算法
class Graph {
private static final int INF = Integer.MAX_VALUE / 2;
int n;
int[][] g;
public Graph(int n, int[][] edges) {
this.n = n;
g = new int[n][n];
for(int i = 0; i < n; i++) Arrays.fill(g[i], INF);
for(int[] e : edges){
int from = e[0], to = e[1], cost = e[2];
g[from][to] = cost;
}
}
public void addEdge(int[] edge) {
int from = edge[0], to = edge[1], cost = edge[2];
g[from][to] = cost;
}
public int shortestPath(int node1, int node2) {
int[] dis = new int[n];
Arrays.fill(dis, INF);
dis[node1] = 0;
boolean[] used = new boolean[n];
for(int i = 0; i < n; i++){
int x = -1;
for(int y = 0; y < n; y++){
if(!used[y] && (x == -1 || dis[y] < dis[x])){
x = y;
}
}
used[x] = true;
for(int y = 0; y < n; y++){
dis[y] = Math.min(dis[y], dis[x] + g[x][y]);
}
}
return dis[node2] == INF ? -1 : dis[node2];
}
}
方法二:Floyd算法
class Graph {
/*
Floyd定义:
f[k][i][j] 表示除了i 和 j 之外,从 i 到 j 的路径中间点上至多为 k 的时候,从 i 到 j 的最短路的长度
分类讨论:
从 i 到 j 的最短路中间至多为 k-1 ==>
从 i 到 j 的最短路中间至多为 k,说明 k 一定是中间节点
f[k][i][j] = min(f[k-1][i][j], f[k-1][i][k] + f[k-1][k][j])
维度优化: f[i][j] = min(f[i][j], f[i][k] + f[k][j])
提问: 为什么维度优化,这样做还是对的? - k表示路径中间至多为k,不包含端点
*/
private static final int INF = Integer.MAX_VALUE / 3;
int n;
int[][] g;
public Graph(int n, int[][] edges) {
this.n = n;
g = new int[n][n];
for(int i = 0; i < n; i++){
// 邻接矩阵(初始化为无穷大,表示 i 到 j 没有边)
Arrays.fill(g[i], INF);
g[i][i] = 0; // i->i 的路径初始化为0
}
for(int[] e : edges){
int from = e[0], to = e[1], cost = e[2];
g[from][to] = cost;
}
// Floyd维护最短路径
for(int k = 0; k < n; k++){
for(int i = 0; i < n; i++){
for(int j = 0; j < n; j++){
g[i][j] = Math.min(g[i][j], g[i][k] + g[k][j]);
}
}
}
}
public void addEdge(int[] edge) {
int x = edge[0], y = edge[1], cost = edge[2];
if(cost >= g[x][y]){
return; // 无需更新,因为目前从 x->y 最短路比新加入的点小
}
// 更新i->j的路径,从新加入的节点处转移
for(int i = 0; i < n; i++){
for(int j = 0; j < n; j++){
// 原来: i -> j
// 更新后: i -> x -> y -> j
g[i][j] = Math.min(g[i][j], g[i][x] + cost + g[y][j]);
}
}
}
public int shortestPath(int start, int end) {
int ans = g[start][end];
return ans < INF/ 3 ? ans : -1;
}
}
难度困难14
现有一个含 n
个顶点的 双向 图,每个顶点按从 0
到 n - 1
标记。图中的边由二维整数数组 edges
表示,其中 edges[i] = [ui, vi]
表示顶点 ui
和 vi
之间存在一条边。每对顶点最多通过一条边连接,并且不存在与自身相连的顶点。
返回图中 最短 环的长度。如果不存在环,则返回 -1
。
环 是指以同一节点开始和结束,并且路径中的每条边仅使用一次。
示例 1:
输入:n = 7, edges = [[0,1],[1,2],[2,0],[3,4],[4,5],[5,6],[6,3]]
输出:3
解释:长度最小的循环是:0 -> 1 -> 2 -> 0
提示:
2 <= n <= 1000
1 <= edges.length <= 1000
edges[i].length == 2
0 <= ui, vi < n
ui != vi
class Solution {
/**
环:从点 a 到点 b,有两条不同的简单路径,这两条简单路径就组成了一个环
BFS枚举起点 start,如果找到从 start 出发的两条不同的简单路径,能到达同一个点
那么当前的环就是最小的,后面继续找 只能找到更大的包含 start 的环
环长为 dis[a] + dis[b] + 1
问:为什么说发现一个已经入队的点,就说明有环?
答:这说明到同一个点有两条不同的路径,这两条路径组成了一个环。
*/
List<Integer>[] g;
int[] dis; // dis[i] 表示从 start 到 i 的最短路长度
public int findShortestCycle(int n, int[][] edges) {
g = new ArrayList[n];
Arrays.setAll(g, e -> new ArrayList<>());
for(int[] e : edges){
int x = e[0], y = e[1];
g[x].add(y);
g[y].add(x);
}
dis = new int[n];
int ans = Integer.MAX_VALUE;
for(int i = 0; i < n; i++){ // 枚举每个起点跑 BFS
ans = Math.min(ans, bfs(i));
}
return ans < Integer.MAX_VALUE ? ans : -1;
}
public int bfs(int start){
int ans = Integer.MAX_VALUE;
Arrays.fill(dis, -1);
dis[start] = 0;
Deque<int[]> dq = new ArrayDeque<>(); // 点,上一个点 (记录一下是谁让我入队的,以此来区分是否是不同路径)
dq.add(new int[]{start, -1});
while(!dq.isEmpty()){
int[] p = dq.poll();
int x = p[0], fa = p[1];
for(int y : g[x]){
if(dis[y] < 0){ // 第一次遇到
dis[y] = dis[x] + 1;
dq.add(new int[]{y, x});
}else if(y != fa)
ans = Math.min(ans, dis[x] + dis[y] + 1);
}
}
return ans;
}
}
难度中等28
给你一个数组 start
,其中 start = [startX, startY]
表示你的初始位置位于二维空间上的 (startX, startY)
。另给你一个数组 target
,其中 target = [targetX, targetY]
表示你的目标位置 (targetX, targetY)
。
从位置 (x1, y1)
到空间中任一其他位置 (x2, y2)
的代价是 |x2 - x1| + |y2 - y1|
。
给你一个二维数组 specialRoads
,表示空间中存在的一些特殊路径。其中 specialRoads[i] = [x1i, y1i, x2i, y2i, costi]
表示第 i
条特殊路径可以从 (x1i, y1i)
到 (x2i, y2i)
,但成本等于 costi
。你可以使用每条特殊路径任意次数。
返回从 (startX, startY)
到 (targetX, targetY)
所需的最小代价。
示例 1:
输入:start = [1,1], target = [4,5], specialRoads = [[1,2,3,3,2],[3,4,4,5,1]]
输出:5
解释:从 (1,1) 到 (4,5) 的最优路径如下:
- (1,1) -> (1,2) ,移动的代价是 |1 - 1| + |2 - 1| = 1 。
- (1,2) -> (3,3) ,移动使用第一条特殊路径,代价是 2 。
- (3,3) -> (3,4) ,移动的代价是 |3 - 3| + |4 - 3| = 1.
- (3,4) -> (4,5) ,移动使用第二条特殊路径,代价是 1 。
总代价是 1 + 2 + 1 + 1 = 5 。
可以证明无法以小于 5 的代价完成从 (1,1) 到 (4,5) 。
示例 2:
输入:start = [3,2], target = [5,7], specialRoads = [[3,2,3,4,4],[3,3,5,5,5],[3,4,5,6,6]]
输出:7
解释:最优路径是不使用任何特殊路径,直接以 |5 - 3| + |7 - 2| = 7 的代价从初始位置到达目标位置。
提示:
start.length == target.length == 2
1 <= startX <= targetX <= 105
1 <= startY <= targetY <= 105
1 <= specialRoads.length <= 200
specialRoads[i].length == 5
startX <= x1i, x2i <= targetX
startY <= y1i, y2i <= targetY
1 <= costi <= 105
class Solution {
public int minimumCost(int[] start, int[] target, int[][] specialRoads) {
long t = (long) target[0] << 32 | target[1];
// 由于 1 <= startY <= targetY <= 105 ,用map来记录 距离dis数组 和 访问vis数组
Map<Long, Integer> dis = new HashMap<>();
dis.put(t, Integer.MAX_VALUE);
dis.put((long) start[0] << 32 | start[1], 0);
Set<Long> vis = new HashSet<>();
for(;;){ // dijkstra 算法,找到dis中最短的点,用该点更新到其他点的距离
long v = -1;
int dv = -1;
for(Map.Entry<Long, Integer> e : dis.entrySet()){
if(!vis.contains(e.getKey()) && (dv < 0 || e.getValue() < dv)){
v = e.getKey();
dv = e.getValue();
}
}
if(v == t) return dv; // 找到了到终点的最短路
vis.add(v);
int vx = (int) (v >> 32), vy = (int) (v & Integer.MAX_VALUE);
// 用 点v 更新到终点的最短路
dis.merge(t, dv + (target[0] - vx + target[1] - vy), Math::min);
// 用 点v 更新到其他点的距离
for(int[] r : specialRoads){
//从位置 (x1, y1) 到空间中任一其他位置 (x2, y2) 的代价是 |x2 - x1| + |y2 - y1|
int d = dv + Math.abs(r[0] - vx) + Math.abs(r[1] - vy) + r[4];
long w = (long) r[2] << 32 | r[3];
if(d < dis.getOrDefault(w, Integer.MAX_VALUE))
dis.put(w, d);
}
}
}
}
难度困难32
给你一个 m x n
的矩阵 grid
,每个元素都为 非负 整数,其中 grid[row][col]
表示可以访问格子 (row, col)
的 最早 时间。也就是说当你访问格子 (row, col)
时,最少已经经过的时间为 grid[row][col]
。
你从 最左上角 出发,出发时刻为 0
,你必须一直移动到上下左右相邻四个格子中的 任意 一个格子(即不能停留在格子上)。每次移动都需要花费 1 单位时间。
请你返回 最早 到达右下角格子的时间,如果你无法到达右下角的格子,请你返回 -1
。
示例 1:
输入:grid = [[0,1,3,2],[5,1,2,5],[4,3,8,6]]
输出:7
解释:一条可行的路径为:
- 时刻 t = 0 ,我们在格子 (0,0) 。
- 时刻 t = 1 ,我们移动到格子 (0,1) ,可以移动的原因是 grid[0][1] <= 1 。
- 时刻 t = 2 ,我们移动到格子 (1,1) ,可以移动的原因是 grid[1][1] <= 2 。
- 时刻 t = 3 ,我们移动到格子 (1,2) ,可以移动的原因是 grid[1][2] <= 3 。
- 时刻 t = 4 ,我们移动到格子 (1,1) ,可以移动的原因是 grid[1][1] <= 4 。
- 时刻 t = 5 ,我们移动到格子 (1,2) ,可以移动的原因是 grid[1][2] <= 5 。
- 时刻 t = 6 ,我们移动到格子 (1,3) ,可以移动的原因是 grid[1][3] <= 6 。
- 时刻 t = 7 ,我们移动到格子 (2,3) ,可以移动的原因是 grid[2][3] <= 7 。
最终到达时刻为 7 。这是最早可以到达的时间。
提示:
m == grid.length
n == grid[i].length
2 <= m, n <= 1000
4 <= m * n <= 105
0 <= grid[i][j] <= 105
grid[0][0] == 0
题解:https://leetcode.cn/problems/minimum-time-to-visit-a-cell-in-a-grid/solution/er-fen-da-an-bfspythonjavacgo-by-endless-j10w/
class Solution {
// 无法到达右下角的情况:grid[0][1] > 1 或者 grid[1][0] > 1.
// 通过反复横跳, 出发时间应该为偶数
// 到达一个格子的时间的奇偶性是不变的,即 dis[i][j] 应该和 i+j 的奇偶性相同
private final static int[][] dirts = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}};
public int minimumTime(int[][] grid) {
int m = grid.length, n = grid[0].length;
if(grid[0][1] > 1 && grid[1][0] > 1)
return -1;
int[][] dis = new int[m][n];
for(int i = 0; i < m; i++)
Arrays.fill(dis[i], Integer.MAX_VALUE);
dis[0][0] = 0;
// dis x y : 按距离排序,确保每次从优先队列中取出的都是当前能够达到的最短路
PriorityQueue<int[]> pq = new PriorityQueue<>((a, b) -> a[0] - b[0]);
pq.add(new int[]{0, 0, 0});
for(;;){ // 只要等待,就一定可以到达终点
int[] p = pq.poll();
int d = p[0], i = p[1], j = p[2];
if(d > dis[i][j]) continue;
if(i == m-1 && j == n-1) // 找到终点,此时 d 一定是最短路
return d;
for(int[] q : dirts){ // 枚举周围四个格子
int x = i + q[0], y = j + q[1];
if(0 <= x && x < m && 0 <= y && y < n){
int nd = Math.max(d+1, grid[x][y]);
nd += (nd - x - y) % 2; // nd 必须和 x+y 同奇偶
if(nd < dis[x][y]){
dis[x][y] = nd; // 更新最短路
pq.add(new int[]{nd, x, y});
}
}
}
}
}
}
难度困难33
给你一个 n
个节点的无向无根树,节点编号从 0
到 n - 1
。给你整数 n
和一个长度为 n - 1
的二维整数数组 edges
,其中 edges[i] = [ai, bi]
表示树中节点 ai
和 bi
之间有一条边。再给你一个长度为 n
的数组 coins
,其中 coins[i]
可能为 0
也可能为 1
,1
表示节点 i
处有一个金币。
一开始,你需要选择树中任意一个节点出发。你可以执行下述操作任意次:
2
以内的所有金币,或者你需要收集树中所有的金币,并且回到出发节点,请你返回最少经过的边数。
如果你多次经过一条边,每一次经过都会给答案加一。
示例 1:
输入:coins = [1,0,0,0,0,1], edges = [[0,1],[1,2],[2,3],[3,4],[4,5]]
输出:2
解释:从节点 2 出发,收集节点 0 处的金币,移动到节点 3 ,收集节点 5 处的金币,然后移动回节点 2 。
提示:
n == coins.length
1 <= n <= 3 * 104
0 <= coins[i] <= 1
edges.length == n - 1
edges[i].length == 2
0 <= ai, bi < n
ai != bi
edges
表示一棵合法的树。class Solution {
// 拓扑排序,去掉没有金币的叶子
// 性质:如果所有在叶子上的金币都收集到了,那么顺路可以把不在叶子上的金币也收集到
// 再次拓扑排序,去掉两轮叶子
public int collectTheCoins(int[] coins, int[][] edges) {
int n = coins.length;
List<Integer>[] g = new ArrayList[n];
Arrays.setAll(g, e -> new ArrayList<>());
int[] deg = new int[n];
for(int[] e : edges){
int x = e[0], y = e[1];
g[x].add(y);
g[y].add(x);
deg[x]++;
deg[y]++;
}
int left_edges = n-1; // 剩余叶子的个数
// 用拓扑排序「剪枝」:去掉没有金币的子树
Deque<Integer> dq = new ArrayDeque<>();
for(int i = 0; i < n; i++)
if(deg[i] == 1 && coins[i] == 0) // 无金币叶子
dq.add(i);
while(!dq.isEmpty()){
int x = dq.poll();
left_edges -= 1;
for(int y : g[x]){
if(--deg[y] == 1 && coins[y] == 0)
dq.add(y);
}
}
// 再次拓扑排序,去掉两轮叶子,剩下的节点都是必须走的节点
for(int i = 0; i < n; i++)
if(deg[i] == 1 && coins[i] == 1) // 有金币的叶子
dq.add(i);
left_edges -= dq.size();
if(dq.size() <= 1) return 0; // 至多一个有金币的叶子,直接收集
while(!dq.isEmpty()){
int x = dq.poll();
for(int y : g[x]){
if(--deg[y] == 1){ // 如果有金币的叶子的邻居变成叶子了,就把它也去掉
left_edges -= 1;
}
}
}
return Math.max(left_edges * 2, 0); // 每条剩余的边走两次 = 节点走两次
}
}
难度困难102
给你一个 n
个节点的 无向带权连通 图,节点编号为 0
到 n - 1
,再给你一个整数数组 edges
,其中 edges[i] = [ai, bi, wi]
表示节点 ai
和 bi
之间有一条边权为 wi
的边。
部分边的边权为 -1
(wi = -1
),其他边的边权都为 正 数(wi > 0
)。
你需要将所有边权为 -1
的边都修改为范围 [1, 2 * 109]
中的 正整数 ,使得从节点 source
到节点 destination
的 最短距离 为整数 target
。如果有 多种 修改方案可以使 source
和 destination
之间的最短距离等于 target
,你可以返回任意一种方案。
如果存在使 source
到 destination
最短距离为 target
的方案,请你按任意顺序返回包含所有边的数组(包括未修改边权的边)。如果不存在这样的方案,请你返回一个 空数组 。
**注意:**你不能修改一开始边权为正数的边。
示例 1:
输入:n = 5, edges = [[4,1,-1],[2,0,-1],[0,3,-1],[4,3,-1]], source = 0, destination = 1, target = 5
输出:[[4,1,1],[2,0,1],[0,3,3],[4,3,1]]
解释:上图展示了一个满足题意的修改方案,从 0 到 1 的最短距离为 5 。
提示:
1 <= n <= 100
1 <= edges.length <= n * (n - 1) / 2
edges[i].length == 3
0 <= ai, bi < n
wi = -1
或者 1 <= wi <= 107
ai != bi
0 <= source, destination < n
source != destination
1 <= target <= 109
https://leetcode.cn/problems/modify-graph-edge-weights/solution/xiang-xi-fen-xi-liang-ci-dijkstrachou-mi-gv1m/
class Solution {
/**
无解的情况:将边权为-1的视为1,如果一次最短路下,从start到end的距离 > target,则一定无解
有解的情况,需要思考两个事情:
1. 按照什么顺序修改 边权为-1 的边
2. 如何使用代码实现
*/
public int[][] modifiedGraphEdges(int n, int[][] edges, int source, int destination, int target) {
List<int[]> g[] = new ArrayList[n];
Arrays.setAll(g, e -> new ArrayList<>());
for (int i = 0; i < edges.length; i++) {
int x = edges[i][0], y = edges[i][1];
g[x].add(new int[]{y, i});
g[y].add(new int[]{x, i}); // 建图,额外记录边的编号
}
var dis = new int[n][2];
for (int i = 0; i < n; i++)
if (i != source)
dis[i][0] = dis[i][1] = Integer.MAX_VALUE;
dijkstra(g, edges, destination, dis, 0, 0);
int delta = target - dis[destination][0];
if (delta < 0) // -1 全改为 1 时,最短路比 target 还大
return new int[][]{};
dijkstra(g, edges, destination, dis, delta, 1);
if (dis[destination][1] < target) // 最短路无法再变大,无法达到 target
return new int[][]{};
for (var e : edges)
if (e[2] == -1) // 剩余没修改的边全部改成 1
e[2] = 1;
return edges;
}
// 朴素 Dijkstra 算法
// 这里 k 表示第一次/第二次
private void dijkstra(List<int[]> g[], int[][] edges, int destination, int[][] dis, int delta, int k) {
int n = g.length;
var vis = new boolean[n];
for (; ; ) {
// 找到当前最短路,去更新它的邻居的最短路
// 根据数学归纳法,dis[x][k] 一定是最短路长度
int x = -1;
for (int i = 0; i < n; ++i)
if (!vis[i] && (x < 0 || dis[i][k] < dis[x][k]))
x = i;
if (x == destination) // 起点 source 到终点 destination 的最短路已确定
return;
vis[x] = true; // 标记,在后续的循环中无需反复更新 x 到其余点的最短路长度
for (var e : g[x]) {
int y = e[0], eid = e[1];
int wt = edges[eid][2];
if (wt == -1)
wt = 1; // -1 改成 1
if (k == 1 && edges[eid][2] == -1) {
// 第二次 Dijkstra,改成 w
int w = delta + dis[y][0] - dis[x][1];
if (w > wt)
edges[eid][2] = wt = w; // 直接在 edges 上修改
}
// 更新最短路
dis[y][k] = Math.min(dis[y][k], dis[x][k] + wt);
}
}
}
}