目录
第一题
题目来源
题目内容
解决方法
方法一:过滤和排序
方法二:迭代
第二题
题目来源
题目内容
解决方法
方法一:回溯算法
方法二:动态规划
方法三:DFS+剪枝
方法四:动态规划+状态压缩
方法五:广度优先搜索(BFS)
第三题
题目来源
题目内容
解决方法
方法一:回溯算法
方法二:记忆化搜索
方法三:递归加剪枝
1333. 餐厅过滤器 - 力扣(LeetCode)
这道题的思路和算法如下:
首先,将二维数组 restaurants 转换为餐馆对象列表。每个餐馆对象包含 id、rating、veganFriendly、price 和 distance 五个属性。
使用过滤器条件对餐馆列表进行过滤:
对过滤后的餐馆列表进行排序:
返回排序后的餐馆id列表。
import java.util.*;
class Restaurant {
int id;
int rating;
int veganFriendly;
int price;
int distance;
public Restaurant(int id, int rating, int veganFriendly, int price, int distance) {
this.id = id;
this.rating = rating;
this.veganFriendly = veganFriendly;
this.price = price;
this.distance = distance;
}
}
class Solution {
public List filterRestaurants(int[][] restaurants, int veganFriendly, int maxPrice, int maxDistance) {
List restaurantList = new ArrayList<>();
// 将二维数组转换为餐馆对象列表
for (int[] res : restaurants) {
restaurantList.add(new Restaurant(res[0], res[1], res[2], res[3], res[4]));
}
// 使用过滤器条件进行过滤
return restaurantList.stream()
.filter(r -> veganFriendly == 0 || r.veganFriendly == veganFriendly)
.filter(r -> r.price <= maxPrice && r.distance <= maxDistance)
.sorted((r1, r2) -> {
if (r1.rating == r2.rating) {
return r2.id - r1.id;
} else {
return r2.rating - r1.rating;
}
})
.map(r -> r.id)
.collect(Collectors.toList());
}
}
复杂度分析:
因此,总的时间复杂度为 O(nlogn)。
空间复杂度方面,需要额外使用一个餐馆对象列表来存储转换后的餐馆信息,所以空间复杂度为 O(n),其中 n 是餐馆的数量。
综上所述,该算法的时间复杂度为 O(nlogn),空间复杂度为 O(n)。
LeetCode运行结果:
思路和算法如下:
import java.util.ArrayList;
import java.util.List;
public class Solution {
public List filterRestaurants(int[][] restaurants, int veganFriendly, int maxPrice, int maxDistance) {
List filtered = new ArrayList<>();
// 过滤符合条件的整型数组
for (int[] res : restaurants) {
if ((veganFriendly == 1 && res[2] == 0) || res[3] > maxPrice || res[4] > maxDistance) {
continue;
}
filtered.add(res);
}
// 根据评分和 id 进行排序
filtered.sort((r1, r2) -> {
if (r1[1] == r2[1]) {
return r2[0] - r1[0];
} else {
return r2[1] - r1[1];
}
});
// 提取餐馆 id 并返回
List result = new ArrayList<>();
for (int[] res : filtered) {
result.add(res[0]);
}
return result;
}
}
复杂度分析:
假设 restaurants 数组的长度为 n。
综上所述,该算法的时间复杂度为 O(nlogn)。
对于空间复杂度,主要取决于存储过程中的辅助数据结构的大小。在这种情况下,使用一个辅助数据结构(例如优先队列或链表)来存储过滤和排序后的餐馆信息。假设过滤后符合条件的餐馆数量为 m,则辅助数据结构的大小为 O(m)。因此,算法的空间复杂度为 O(m)。
LeetCode运行结果:
39. 组合总和 - 力扣(LeetCode)
这个问题可以使用回溯算法来解决。回溯算法是一种通过不断尝试所有可能的选择,并在遇到不满足条件的情况时进行回退,继续尝试其他选择的算法。具体的思路如下:
这样,就可以找到所有满足条件的组合了。注意,为了避免重复的组合出现,我们在每次递归调用时,从当前位置开始选择元素,而不是从0开始选择。这样可以保证每个元素只能使用一次,避免重复的组合。另外,对于给定的输入,保证和为目标值的不同组合数少于150个,所以回溯算法是可行的,不会超时。
import java.util.ArrayList;
import java.util.List;
class Solution {
public List> combinationSum(int[] candidates, int target) {
List> result = new ArrayList<>();
List path = new ArrayList<>();
backtrack(candidates, target, 0, path, result);
return result;
}
private void backtrack(int[] candidates, int target, int start, List path, List> result) {
if (target < 0) {
return;
}
if (target == 0) {
result.add(new ArrayList<>(path));
return;
}
for (int i = start; i < candidates.length; i++) {
path.add(candidates[i]);
backtrack(candidates, target - candidates[i], i, path, result);
path.remove(path.size() - 1);
}
}
}
复杂度分析:
回溯算法的时间复杂度和空间复杂度通常是难以精确计算的,因为它涉及到了所有可能的组合情况。但可以从一个大致的角度进行分析。
设候选数组的长度为 n,目标值为 target。
时间复杂度:
空间复杂度:
综上所述,回溯算法的时间复杂度为 O(n^target),空间复杂度为 O(target)。在实际应用中,需要根据具体问题规模和约束条件综合考虑算法的效率和可用性。
LeetCode运行结果:
除了回溯算法,也可以使用动态规划来解决这个问题。
在动态规划方法中,我们使用一个二维数组dp来保存每个目标值对应的所有组合。dp[i]中存储的是目标值为i时的所有组合。
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
class Solution {
public List> combinationSum(int[] candidates, int target) {
Arrays.sort(candidates); // 对数组进行排序
List>> dp = new ArrayList<>();
// 初始化dp数组
for (int i = 0; i <= target; i++) {
dp.add(new ArrayList<>());
}
// 遍历每个目标值
for (int i = 1; i <= target; i++) {
List> combinations = new ArrayList<>();
// 遍历每个候选数字
for (int j = 0; j < candidates.length && candidates[j] <= i; j++) {
if (candidates[j] == i) {
combinations.add(Arrays.asList(candidates[j]));
} else {
for (List prev : dp.get(i - candidates[j])) {
if (candidates[j] <= prev.get(0)) {
List combination = new ArrayList<>();
combination.add(candidates[j]);
combination.addAll(prev);
combinations.add(combination);
}
}
}
}
dp.set(i, combinations);
}
return dp.get(target);
}
}
复杂度分析:
LeetCode运行结果:
除了回溯算法和动态规划之外,还可以使用DFS+剪枝的方法来解决这个问题。
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
class Solution {
public List> combinationSum(int[] candidates, int target) {
Arrays.sort(candidates); // 对数组进行排序
List> combinations = new ArrayList<>();
dfs(candidates, target, 0, new ArrayList<>(), combinations);
return combinations;
}
/**
* DFS+剪枝求解组合
* @param candidates 候选数字数组
* @param target 目标值
* @param start 从候选数字数组的哪个位置开始搜索
* @param combination 当前的组合
* @param combinations 结果集
*/
private void dfs(int[] candidates, int target, int start, List combination, List> combinations) {
if (target == 0) { // 如果目标值为0,说明已经找到一个组合
combinations.add(new ArrayList<>(combination));
return;
}
for (int i = start; i < candidates.length && candidates[i] <= target; i++) {
// 剪枝操作:如果当前候选数字已经比目标值大了,就没有继续搜索的必要了
if (candidates[i] > target) {
break;
}
combination.add(candidates[i]); // 将当前候选数字加入到组合中
dfs(candidates, target - candidates[i], i, combination, combinations); // 继续搜索,从当前候选数字开始搜索
combination.remove(combination.size() - 1); // 回溯操作:将当前候选数字移除组合
}
}
}
在DFS+剪枝方法中,我们使用一个dfs函数来实现搜索。首先对候选数字进行排序,然后从0位置开始搜索。遍历候选数字数组,如果当前候选数字不大于目标值,将其加入到组合中。然后,将目标值减去当前候选数字,继续从当前位置开始搜索,直到目标值为0。如果目标值为0,说明已经找到一个组合,将其加入到combinations列表中。 回溯操作时,将当前候选数字从组合中移除,继续搜索下一个候选数字。
可以看出,与回溯算法相比,DFS+剪枝方法会在搜索过程中进行一些剪枝操作,从而减少了不必要的递归调用,提高了效率。
复杂度分析:
需要注意的是,虽然DFS+剪枝方法可以在搜索过程中减少不必要的递归调用,但在最坏情况下,仍然需要遍历所有可能的组合,因此其时间复杂度与回溯算法相同。空间复杂度方面,DFS+剪枝方法与回溯算法相比,由于没有维护二维数组dp,所以空间复杂度略低一些。
LeetCode运行结果:
使用Java的动态规划+状态压缩方法来解决组合总和问题。
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class Solution {
public List> combinationSum(int[] candidates, int target) {
Arrays.sort(candidates); // 对数组进行排序
int[] dp = new int[target + 1]; // 动态规划数组
List> combinations = new ArrayList<>();
dp[0] = 1; // 初始化dp[0]为1
for (int candidate : candidates) {
for (int i = candidate; i <= target; i++) {
if (dp[i - candidate] > 0) {
// 如果dp[i - candidate]不为0,说明可以通过当前候选数字得到目标值i
dp[i] += dp[i - candidate]; // 更新dp[i]
}
}
}
if (dp[target] == 0) {
// 如果dp[target]为0,说明无法得到目标值target,直接返回空列表
return combinations;
}
dfs(candidates, target, 0, new ArrayList<>(), combinations, dp); // 使用DFS搜索符合条件的组合
return combinations;
}
private void dfs(int[] candidates, int target, int start, List combination, List> combinations, int[] dp) {
if (target == 0) { // 如果目标值为0,说明已经找到一个组合
combinations.add(new ArrayList<>(combination));
return;
}
for (int i = start; i < candidates.length && target >= candidates[i]; i++) {
if (dp[target - candidates[i]] > 0) {
combination.add(candidates[i]); // 将当前候选数字加入到组合中
dfs(candidates, target - candidates[i], i, combination, combinations, dp); // 继续搜索,从当前候选数字开始搜索
combination.remove(combination.size() - 1); // 回溯操作:将当前候选数字移除组合
}
}
}
}
该方法的核心思想是使用动态规划数组dp记录每个目标值对应的组合数。首先初始化dp[0]为1,表示目标值为0时有一种组合方式。然后,按照候选数字的顺序更新dp数组,遍历候选数字数组,并从候选数字开始更新dp数组中的对应位置。如果dp[i - candidate]不为0,则说明可以通过当前候选数字得到目标值i,因此更新dp[i] += dp[i - candidate]。
在DFS过程中,我们根据dp数组进行剪枝操作,只选择能够达到目标值的候选数字进行搜索。如果dp[target]为0,说明无法得到目标值target,直接返回空列表。否则,继续使用DFS搜索符合条件的组合。
复杂度分析:
时间复杂度:
空间复杂度:
综上所述,使用动态规划+状态压缩方法解决组合总和问题的时间复杂度为O(target * n),空间复杂度为O(target)。在实际应用中,如果目标值较大,则算法的效率会受到限制。因此,对于较大的目标值,可能需要考虑其他优化方法或策略来提高算法效率。
LeetCode运行结果:
使用Java的广度优先搜索(BFS)方法来解决组合总和问题。
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
public class Solution {
public List> combinationSum(int[] candidates, int target) {
Arrays.sort(candidates); // 对数组进行排序
List> combinations = new ArrayList<>();
Queue queue = new LinkedList<>(); // 使用队列保存搜索状态
// 初始化队列,将初始状态(目标值、路径、起始位置)加入队列
queue.offer(new State(target, new ArrayList<>(), 0));
while (!queue.isEmpty()) {
State state = queue.poll(); // 取出当前状态
if (state.target == 0) { // 如果目标值为0,说明已经找到一个符合条件的组合
combinations.add(state.path);
continue;
}
for (int i = state.start; i < candidates.length && candidates[i] <= state.target; i++) {
List newPath = new ArrayList<>(state.path);
newPath.add(candidates[i]); // 将当前候选数字加入到路径中
// 将更新后的状态(目标值、路径、起始位置)加入队列
queue.offer(new State(state.target - candidates[i], newPath, i));
}
}
return combinations;
}
// 定义状态类
private class State {
int target; // 目标值
List path; // 当前路径
int start; // 起始位置
public State(int target, List path, int start) {
this.target = target;
this.path = path;
this.start = start;
}
}
}
该方法使用广度优先搜索(BFS)进行搜索。使用队列保存搜索状态,每次从队列中取出一个状态进行拓展,直到队列为空。
在每一次拓展过程中,依次选择候选数字,并判断是否满足以下条件:
由于每个位置的数字可以重复使用,因此每次拓展时仍可以选择当前候选数字及之后的数字。这样可以保证得到所有可能的组合。
复杂度分析:
设 n 为候选数字数组 candidates 的长度,m 为目标值 target 的大小,k 为结果列表 combinations 的平均长度(即每个组合的平均元素个数)。
时间复杂度:
空间复杂度:
总结起来,使用广度优先搜索(BFS)的组合总和问题算法的时间复杂度为 O(nlogn + kn),空间复杂度为 O(n + m + k^2)。
LeetCode运行结果:
40. 组合总和 II - 力扣(LeetCode)
算法思路:
class Solution {
public List> combinationSum2(int[] candidates, int target) {
Arrays.sort(candidates); // 先排序
List> res = new ArrayList<>();
backtrack(res, new ArrayList<>(), candidates, target, 0);
return res;
}
private void backtrack(List> res, List temp, int[] candidates, int target, int start) {
if (target == 0) {
res.add(new ArrayList<>(temp));
} else if (target > 0) {
for (int i = start; i < candidates.length; i++) {
// 如果当前数字和前一个数字相等,则跳过,避免重复
if (i > start && candidates[i] == candidates[i - 1]) {
continue;
}
temp.add(candidates[i]);
backtrack(res, temp, candidates, target - candidates[i], i + 1);
temp.remove(temp.size() - 1);
}
}
}
}
复杂度分析:
算法的时间复杂度取决于两个因素,候选数字数组的长度(n)和目标数 target 的大小(m)。
空间复杂度主要取决于递归过程中使用的栈空间和结果列表的空间。
需要注意的是,排序操作会占用一定的时间和空间,但由于排序只需要进行一次,所以对于整体的时间和空间复杂度影响较小。
LeetCode运行结果:
除了回溯算法,还可以使用记忆化搜索(Memoization Search)来解决这个问题。
记忆化搜索的思路类似于回溯算法。在递归过程中,为了避免重复计算某些值,我们可以使用一个记忆化数组 memo,记录每个状态的计算结果。在递归过程中,如果遇到一个已经计算过的状态,则直接返回其对应的计算结果,避免重复计算。
class Solution {
public List> combinationSum2(int[] candidates, int target) {
Arrays.sort(candidates);
Map>> memo = new HashMap<>();
return search(candidates, target, 0, memo);
}
private List> search(int[] candidates, int target, int start, Map>> memo) {
String key = target + "," + start;
if (memo.containsKey(key)) {
return memo.get(key);
}
List> res = new ArrayList<>();
if (target == 0) {
res.add(new ArrayList<>());
} else if (target > 0) {
for (int i = start; i < candidates.length && candidates[i] <= target; i++) {
if (i > start && candidates[i] == candidates[i - 1]) {
continue; // 避免重复计算相同的数字
}
List> subRes = search(candidates, target - candidates[i], i + 1, memo);
for (List subList : subRes) {
List tempList = new ArrayList<>();
tempList.add(candidates[i]);
tempList.addAll(subList);
res.add(tempList);
}
}
}
memo.put(key, res);
return res;
}
}
复杂度分析:
对于记忆化搜索的复杂度分析,我们可以将其分为两个部分来看:计算每个状态的时间复杂度和总状态数。
综上所述,记忆化搜索的时间复杂度为 O(n * 2^n),其中 n 是候选数字数组的长度。空间复杂度为 O(n * m),其中 n 是候选数字数组的长度,m 是目标数 target 的大小。记忆化数组 memo 的空间复杂度为 O(n * m)。这里的空间复杂度主要取决于 memo 数组的大小。同时,递归调用的栈空间复杂度也为 O(n),因为在最坏情况下,回溯的深度可能达到 n 级别。
LeetCode运行结果:
除了回溯算法和记忆化搜索,还可以使用递归加剪枝的方法来解决该问题。这种方法类似于回溯算法,但通过一些条件判断来减少搜索的分支。
在递归加剪枝的方法中,我们首先对候选数字数组 candidates 进行排序。然后,使用回溯的方式从候选数字数组中选择数字,并进行相应的剪枝操作。
在每一轮的选择中,我们从 start 位置开始遍历候选数字数组。如果当前数字和上一个数字相同且不是起始位置,则跳过,以避免重复组合的产生。
对于每个选择,我们有两个分支:
当 remain 等于 0 时,表示已经找到一个合法的组合,将其加入到结果列表 result 中。
该方法通过剪枝操作,减少了不必要的搜索分支,提高了效率。
class Solution {
public List> combinationSum2(int[] candidates, int target) {
Arrays.sort(candidates);
List> result = new ArrayList<>();
List temp = new ArrayList<>();
backtrack(result, temp, candidates, target, 0);
return result;
}
private void backtrack(List> result, List temp, int[] candidates, int remain, int start) {
if (remain == 0) {
result.add(new ArrayList<>(temp));
return;
}
for (int i = start; i < candidates.length; i++) {
if (i > start && candidates[i] == candidates[i - 1]) {
continue;
}
if (candidates[i] <= remain) {
temp.add(candidates[i]);
backtrack(result, temp, candidates, remain - candidates[i], i + 1);
temp.remove(temp.size() - 1);
} else {
break; // 剩余的数字已经超过目标值,不再搜索
}
}
}
}
复杂度分析:
时间复杂度和空间复杂度与回溯算法相同,为 O(n * 2^n) 和 O(n)。
LeetCode运行结果: