2022-12-01
【知识点】回溯
(1)基本内容
回溯算法是一种纯暴力搜索方式,虽然是纯暴力,但是有些问题能用暴力解出来就很不错了。它能解决组合、分割、子集、排列(与组合问题相比是有序的)和棋盘等问题。回溯与递归共生,有递归就有回溯,在二叉树的一些题目中看似只有递归,但是回溯是存在的,只是没有去用它而已。回溯法可以抽象为树形结构,如下图所示,横向for循环遍历,纵向递归。
(2)回溯三部曲
// 返回值参数、终止条件、单层搜索逻辑
void backtracking(参数) { //回溯法递归中一般没有返回值,但参数一般较多
if (终止条件) {
存放结果;
return;
}
for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
处理节点;
backtracking(路径,选择列表); // 递归
回溯,撤销处理结果
}
}
1.【77】组合(中)
题目链接: 组合问题
题目描述:
给定两个整数n
和 k
,返回范围 [1, n]
中所有可能的k
个数的组合。返回顺序任意。
涉及知识点: 回溯【组合问题】、剪枝
思路: 题目比较简单,直接套用回溯法模板就可以解题,是回溯法的简单实践。
另外可以在原生的回溯法上进行剪枝优化。所谓剪枝就是不去遍历不满足情况的分支,比如组合问题中n = 4,k = 4,那么第一层for循环的时候,从元素2开始的遍历都没有意义了,因为以2开头的组合中最多只有三个数,就把这些分支都“剪掉”,它优化的是单层搜索中的逻辑。
代码:
// 原生版本
class Solution {
LinkedList<Integer> arr = new LinkedList<>();
List<List<Integer>> res = new ArrayList<>();
public List<List<Integer>> combine(int n, int k) {
backTracing(1,n,k);
return res;
}
public void backTracing(int startIndex,int n, int k){
if(arr.size()==k){
// 注意这里要用new ArrayList
res.add(new ArrayList<>(arr));
return;
}
for(int i = startIndex;i<=n;i++){
arr.add(i);
backTracing(i+1,n,k);
arr.removeLast();
}
return;
}
}
// 剪枝优化
// 注意:剪枝优化也无法改变其暴力搜索的本质
// 在arr中已经搜到了arr.size()个元素的情况下,
// 至多从n-(k-arr.size())+1的位置开始搜索是满足条件的,即完成剪枝操作
for(int i = startIndex;i<=n-(k-arr.size())+1;i++){ //剪枝操作只在遍历边界上不同,其他一样
arr.add(i);
backTracing(i+1,n,k);
arr.removeLast();
}
2.【216】组合总和 III(中)
题目链接: 组合总和问题
题目描述:
找出所有相加之和为 n 的 k 个数的组合。组合中只允许含有 1 - 9 的正整数,并且每种组合中不存在重复的数字。
说明: 所有数字都是正整数。解集不能包含重复的组合。
示例 1
: 输入: k = 3, n = 7 输出: [[1,2,4]]
示例 2
: 输入: k = 3, n = 9 输出: [[1,2,6], [1,3,5], [2,3,4]
涉及知识点: 回溯【组合问题】、剪枝
思路: 是原生组合问题的衍生,多了一个求和限制的条件,整体思路大差不差,但是这里的剪枝可以在两个地方进行:第一是判断当前总和是否已经超过目标和,超过就return;二是和组合问题一样,限制边界条件。
代码:
class Solution {
List<List<Integer>> res = new ArrayList<>();
LinkedList<Integer> path = new LinkedList<>();
int sum = 0;
public List<List<Integer>> combinationSum3(int k, int n) {
backTracing(1,k,n);
return res;
}
public void backTracing(int startIndex, int k,int n){
if(path.size()==k){
if(sum == n){
res.add(new ArrayList(path));
return;
}else{
return;
}
}
// 剪枝操作1:比较当前和与目前和大小
if(sum>n){
return;
}
for(int i = startIndex;i<=10-(k-path.size());i++){ //剪枝操作2:限制边界
path.add(i);
sum+=i;
backTracing(i+1,k,n);
path.removeLast();
sum-=i;
}
return;
}
}
3.【17】电话号码的字母组合(中)
题目链接: 根据电话号码进行字母组合
题目描述:
给定一个仅包含数字 2-9
的字符串,返回所有它能表示的字母组合。答案可以按任意顺序返回。
给出数字到字母的映射如下(与电话按键相同)。注意: 1 不对应任何字母。
涉及知识点: 回溯【组合问题】
思路: 这个题稍微有一点点绕,要先用数组或map等形式建立起数字和字母的映射关系,记为dict,根据传入的digitis,如"23",要先找到数字2和3在dict中对应的有哪些字母,然后用for循环去遍历2对应的字母,在递归中去遍历3对应的字母。这和前面在一个集合中进行组合的问题不太一样,在同一个组合中需要有一个startIndex来标记当前遍历元素的下一个位置,这里则是需要一个index来标记遍历到了第几个数字,而对于每个数字对应的字母集合则可以从头遍历到尾。
代码:
class Solution {
List<String> res = new ArrayList<>(); //放结果
StringBuilder temp = new StringBuilder(); //用来临时存放字母组合
// 需要用到数组进行映射
String[] dict = {"","","abc","def","ghi","jkl","mno","pqrs","tuv","wxyz"};
public List<String> letterCombinations(String digits) {
backTracing(digits,0);
return res;
}
public void backTracing(String digits,int index){
/*注意:这里写成如下形式是错误的,会导致digits=""的测试用例无法通过
if(digits == ""){
return;
}
*/
if(digits == null || digits.length() == 0){
return;
}
if(index==digits.length()){
res.add(temp.toString());
return;
}
String letters = dict[digits.charAt(index)-'0'];
for(int i=0;i<letters.length();i++){
temp.append(letters.charAt(i));
backTracing(digits,index+1);
temp.deleteCharAt(temp.length()-1);
}
}
}
4.【39】组合总和(中)
题目链接: 组合总和问题
题目描述:
给你一个 无重复元素 的整数数组 candidates
和一个目标整数 target
,找出 candidates
中可以使数字和为目标数 target
的 所有不同组合 ,并以列表形式返回。你可以按任意顺序返回这些组合。candidates
中的同一个数字可以无限制重复被选取 。如果至少一个数字的被选数量不同,则两种组合是不同的。 对于给定的输入,保证和为target
的不同组合数少于 150 个。
涉及知识点: 回溯【组合问题】
思路: 本题的解法也是基本套用回溯算法的模板,和组合总和 III相比,本题没有对元素个数进行限制,因此只能靠元素加和sum来控制递归深度。另一点不同在于本题允许元素重复,那么每次递归的时候都是从头递归,这点是靠startIndex来控制的,比如说当前for循环遍历到数组的第i个元素,那么本次循环的递归中传入的startIndex就是i。
代码:
class Solution {
List<List<Integer>> res = new ArrayList<>();
LinkedList<Integer> path = new LinkedList<>();
int sum = 0;
public List<List<Integer>> combinationSum(int[] candidates, int target) {
backTracing(candidates,target,0);
return res;
}
public void backTracing(int[] candidates, int target, int startIndex){
/*注意这里判断sum>target一定不能忘写,第一遍就是没写这个条件,会导致栈溢出*/
if(sum>target){
return;
}
if(sum==target){
res.add(new ArrayList(path));
return;
}
for(int i=index;i<candidates.length;i++){
path.add(candidates[i]);
sum+=candidates[i];
backTracing(candidates,target,i);
sum-=candidates[i];
path.removeLast();
}
return;
}
}
5.【40】组合总和 II(中)
题目链接: 组合总和问题(组合不能重复)
题目描述:
给定一个候选人编号的集合candidates
和一个目标数target
,找出candidates
中所有可以使数字和为 target
的组合。candidates
中的每个数字在每个组合中只能使用 一次 。
注意: 解集不能包含重复的组合。
涉及知识点: 回溯【组合问题】、去重
思路: 本题的关键点和难点在于去重操作,总体上有两种方法,在去重操作前要先对原数组进行排序,确保相同的元素是相邻的。
第一种方法是借助used数组,将已经遍历过的点对应的used数组中的值置1,作为标记,那么在树层遍历中(即for循环的横向遍历),如果当前i对应的元素等于i-1对应的元素(n>0情况下)&&used[i-1]==0,这说明将要进行的是树层去重操作,如果满足前述条件,则continue。这里需要used[i-1]==0的原因是防止在树枝上进行去重,因为在树枝上,第i-1个元素一定是used,而在树枝上的元素重复是允许的,所以要加上这个限制条件防止误去重。
第二种方法不再借助used数组,直接利用startIndex进行判断,当i大于startIndex时,说明是在进行树层遍历,这种写法更简洁,但需要好好理解一下。
下图给出了树层遍历和树枝遍历的区别:
代码:
// 使用used数组的方法
class Solution {
List<List<Integer>> res = new ArrayList<>();
LinkedList<Integer> path = new LinkedList<>();
int[] used;
int sum = 0;
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
// 去重操作前要对数组排序
used = new int[candidates.length];
Arrays.sort(candidates);
backTracing(candidates, target, 0);
return res;
}
public void backTracing(int[] candidates, int target, int startIndex){
if(sum>target){
return;
}
if(sum==target){
res.add(new ArrayList(path));
return;
}
for(int i = startIndex;i<candidates.length;i++){
// 利用used数组判断该元素是否重复
if(i>0&&candidates[i]==candidates[i-1]&&used[i-1]==0){
continue;
}
path.add(candidates[i]);
sum += candidates[i];
used[i]=1;
backTracing(candidates,target,i+1);
path.removeLast();
sum-=candidates[i];
used[i] = 0;
}
}
}
// 不使用used数组
// 其主要的不同点在于判断元素是否重复那里
if(i>startIndex&&candidates[i]==candidates[i-1]){
continue;
}
2022-12-02
6.【131】分割回文串(中)
题目链接: 分割回文串问题
题目描述:
给你一个字符串 s,请你将 s 分割成一些子串,使每个子串都是回文串
。返回 s 所有可能的分割方案。回文串
是正着读和反着读都一样的字符串。
涉及知识点: 回溯【分割问题】、回文串判断
思路: 分割问题和前面组合问题的遍历过程是类似的,不同的是分割问题中的startIndex要作为分割线,[startIndex,i]是当前子串的区间,想清楚这一点题就能写出来了。另外,判断回文数可以用双指针,也很简单。
代码:
class Solution {
List<List<String>> res = new ArrayList<>();
LinkedList<String> path = new LinkedList<>();
public List<List<String>> partition(String s) {
backTracing(s,0);
return res;
}
public void backTracing(String s,int startIndex){
if(startIndex>=s.length()){
res.add(new ArrayList(path));
return;
}
for(int i = startIndex;i<s.length();i++){
// 子串所在的区间为[startIndex,i]
if(isPalin(s,startIndex,i)){
String str = s.substring(startIndex, i + 1);
path.add(str);
}else{
continue;
}
backTracing(s,i+1); //这里传的是i+1,不是startIndex+1
path.removeLast();
}
}
// 判断是否回文
public boolean isPalin(String s,int start,int end){
for(int i = start, j = end;i<j;i++,j--){
if(s.charAt(i)!=s.charAt(j)){
return false;
}
}
return true;
}
}
7.【93】复原 IP 地址(中)
题目链接: 复原 IP 地址问题
题目描述:
有效 IP 地址
正好由四个整数(每个整数位于 0 到 255 之间组成,且不能含有前导 0)
,整数之间用 ‘.’ 分隔。例如:“0.1.2.201” 和 “192.168.1.1” 是 有效 IP 地址,但是 “0.011.255.245”、“192.168.1.312” 和 “192.168@1.1” 是 无效 IP 地址。
给定一个只包含数字的字符串 s ,用以表示一个 IP 地址,返回所有可能的有效 IP 地址,这些地址可以通过在 s 中插入 ‘.’ 来形成。你不能重新排序或删除 s 中的任何数字。你可以按任何顺序返回答案。
涉及知识点: 回溯【分割问题】、字符串操作
思路: 本题和上一个分割问题是类似的,但这里不同的是要用加“.”
的次数来控制递归结束,因为是四个子串,所以“.”
的数量为3,在加“.”
的同时用一个pointSum
来进行计数,当达到3时就说明到了终止条件,注意:这里还要再判断一下剩下的子串是否满足IP地址的要求。此外,本题里涉及到对字符串的一些拼接操作。
代码:
// 这种写法并不是最佳的,代码随想录上有时间复杂度更低的算法,是利用StringBuilder做的。
class Solution {
List<String> res = new ArrayList<>();
int pointSum = 0;
public List<String> restoreIpAddresses(String s) {
if(s.length()>12){ //相当于剪枝操作
return res;
}
backTracing(s,0);
return res;
}
public void backTracing(String s, int startIndex){
if(pointSum==3){
if(isIP(s,startIndex,s.length()-1)){
res.add(s);
}
return;
}
for(int i = startIndex;i<s.length();i++){
if(isIP(s,startIndex,i)){
//重构字符串
s = s.substring(0, i + 1)+"."+s.substring(i+1);
pointSum++;
backTracing(s,i+2);
s= s.substring(0,i + 1)+ s.substring(i +2);
pointSum--;
}else{
break;
}
}
}
public boolean isIP(String s,int start,int end){
int sum = 0;
if(start>end){
return false;
}
if(s.charAt(start)=='0'&&start!=end){
return false;
}
for(int i=start;i<=end;i++){
if(s.charAt(i)>'9'||s.charAt(i)<'0'){
return false;
}
sum =sum*10+(s.charAt(i)-'0');
if(sum>255){
return false;
}
}
return true;
}
}
2022-12-03
8.【73】子集(中)
题目链接: 子集问题
题目描述:
给你一个整数数组nums
,数组中的元素互不相同
。返回该数组所有可能的子集(幂集)。
解集不能包含重复的子集
。你可以按任意顺序返回解集。
涉及知识点: 回溯【子集问题】
思路: 本题前面那些回溯问题的不同在于收获结果是在每一个节点,而不是在叶子节点,明白了这个道理解题就比较容易了。不过要明确收获结果的位置,如果加了终止添加,那么要写在终止条件上面。注: 本题的终止条件也可以不写,因为有for循环控制着,但要在循环后面写return。
代码:
class Solution {
List<List<Integer>> res = new ArrayList<>();
LinkedList sub = new LinkedList<>();
public List<List<Integer>> subsets(int[] nums) {
backTracing(nums,0);
return res;
}
public void backTracing(int[] nums, int startIndex){
res.add(new ArrayList(sub));
if(startIndex==nums.length){ //这个条件可以不写
return;
}
for(int i=startIndex;i<nums.length;i++){
sub.add(nums[i]);
backTracing(nums,i+1);
sub.removeLast();
}
return;
}
}
9.【90】子集 II(中)
题目链接: 子集问题进阶
题目描述:
给你一个整数数组nums
,其中可能包含重复元素
,请你返回该数组所有可能的子集(幂集)。
解集不能包含重复的子集
。返回的解集中,子集可以按任意顺序排列。
涉及知识点: 回溯【子集问题】、去重
思路: 本题和上一个子集问题的解题方式类似,只是要考虑树层遍历中不能重,去重思路参考组合总和 II 。
代码:
class Solution {
List<List<Integer>> res = new ArrayList<>();
LinkedList<Integer> sub = new LinkedList<>();
public List<List<Integer>> subsetsWithDup(int[] nums) {
// 先排序
Arrays.sort(nums);
backTracing(nums,0);
return res;
}
public void backTracing(int[] nums,int startIndex){
res.add(new ArrayList(sub));
if(startIndex==nums.length){
return;
}
for(int i = startIndex;i<nums.length;i++){
if(i>0&&nums[i]==nums[i-1]&&i>startIndex){
continue;
}
sub.add(nums[i]);
backTracing(nums,i+1);
sub.removeLast();
}
}
}
100题纪念(2022-12-03)
10.【491】递增子序列(中)
题目链接: 递增子序列问题
题目描述:
给你一个整数数组nums
,找出并返回所有该数组中不同的递增子序列,递增子序列中至少有两个元素
。你可以按 任意顺序 返回答案。
数组中可能含有重复元素
,如出现两个整数相等,也可以视作递增序列的一种特殊情况。
涉及知识点: 回溯【子集问题】、去重
思路: 本题的去重思路和前面的思路不太一样,因为这里的序列顺序是给定的不能随意更改,所有就不能先排序再去看当前值是否和前一个值相同。所以考虑在每一树层借助HashMap去记录前面出现过的元素,如果当前层已经出现过就跳过本轮循环,进入下一个。
代码:
class Solution {
List<List<Integer>> res = new ArrayList<>();
LinkedList<Integer> sub = new LinkedList<>();
public List<List<Integer>> findSubsequences(int[] nums) {
backTracing(nums,0);
return res;
}
public void backTracing(int[] nums,int startIndex){
if(sub.size()>1){
res.add(new ArrayList(sub));
}
HashMap<Integer,Integer> map = new HashMap<>();
for(int i = startIndex;i<nums.length;i++){
if(!sub.isEmpty() && nums[i]< sub.getLast()){ //不符合递增序列的情况
continue;
}
// getOrDefault(Object,V)方法,如果map中有对应的key,则会返回其value,
// 否则返回一个“默认值”
if(map.getOrDefault( nums[i],0 ) >=1){ //元素重复的情况
continue;
}
map.put(nums[i],map.getOrDefault( nums[i],0 )+1);
sub.add(nums[i]);
backTracing(nums,i+1);
sub.removeLast();
}
}
}
2022-12-05
11.【46】全排列(中)
题目链接: 全排列问题
题目描述:
给定一个不含重复数字的数组nums
,返回其所有可能的全排列
。你可以按任意顺序返回答案。
涉及知识点: 回溯【排列问题】
思路: 排列与组合的不同之处在于:排列是有顺序的,且排列中可以回过头去取前面还没用过的元素,只是在一条树枝路径上不能重复取已经取过的元素。排列问题用一个used数组来辅助比较好,用来标识哪些元素已经用过了。
代码:
class Solution {
List<List<Integer>> res = new ArrayList<>();
LinkedList<Integer> path = new LinkedList<>();
public List<List<Integer>> permute(int[] nums) {
// 标识元素是否取过
int[] used = new int[nums.length];
backTracing(nums,used);
return res;
}
public void backTracing(int[] nums,int[] used){
if(path.size()==nums.length){
res.add(new ArrayList(path));
return;
}
for(int i = 0;i<nums.length;i++){
if(used[i]!=0){
continue;
}
path.add(nums[i]);
used[i]=1;
backTracing(nums,used);
used[i]=0;
path.removeLast();
}
}
}
12.【46】全排列 II(中)
题目链接: 全排列问题进阶
题目描述:
给定一个可包含重复数字的序列 nums
,按任意顺序 返回所有不重复的全排列
涉及知识点: 回溯【排列问题】、去重
思路: 本题和上题相比多了一个去重的操作,去重的思路和前面组合问题中的一样,也是先排序,然后借助used数组在树层上去重
代码:
class Solution {
List<List<Integer>> res = new ArrayList<>();
LinkedList<Integer> path = new LinkedList<>();
public List<List<Integer>> permuteUnique(int[] nums) {
int[] used = new int[nums.length];
Arrays.sort(nums);
backTracing(nums,used);
return res;
}
public void backTracing(int[] nums,int[] used){
if(path.size()==nums.length){
res.add(new ArrayList(path));
return;
}
for(int i = 0;i<nums.length;i++){
if(used[i]!=0||(i>0&&nums[i]==nums[i-1]&&used[i-1]==0)){
continue;
}
path.add(nums[i]);
used[i]=1;
backTracing(nums,used);
used[i]=0;
path.removeLast();
}
}
}
13.【51】N皇后(难)
题目链接: N皇后问题
题目描述:
按照国际象棋的规则,皇后可以攻击与之处在同一行或同一列或同一斜线上的棋子。
n 皇后问题 研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。
(即:同一行同一列同一对角线都不能出现两个皇后)
给你一个整数 n ,返回所有不同的 n 皇后问题 的解决方案。
每一种解法包含一个不同的 n 皇后问题 的棋子放置方案,该方案中 ‘Q’ 和 ‘.’ 分别代表了皇后和空位。
涉及知识点: 回溯、N皇后条件判断
思路: N皇后问题的核心还是在于回溯,我觉得比较难的部分在于字符串的相关操作以及判断N皇后条件,前者是因为对字符串和列表的相关方法不熟练,后者则是因为涉及对角线的包括了45度和135度,不细心的情况下很容易漏掉。
代码:
class Solution {
List<List<String>> res = new ArrayList<>();
public List<List<String>> solveNQueens(int n) {
char[][] chessboard = new char[n][n];
for (char[] c : chessboard) {
Arrays.fill(c, '.');
}
backTracing(n, 0, chessboard);
return res;
}
public void backTracing(int n,int row,char[][] chessboard){
if(row==n){
List<String> list = new ArrayList<>();
for(char[] c:chessboard){
list.add(String.copyValueOf(c));
}
res.add(list);
return;
}
for(int col = 0;col<n;col++){
if(isValid(chessboard,row,col,n)){
chessboard[row][col]='Q';
backTracing(n,row+1,chessboard);
chessboard[row][col] = '.';
}
}
}
public boolean isValid(char[][] chessboard,int row,int col,int n){
for(int i = 0;i<row;i++){
if(chessboard[i][col]=='Q'){
return false;
}
}
// 检查45度对角
for(int i = row-1 ,j = col+1; i>=0&&j<n;i--,j++){
if(chessboard[i][j]=='Q'){
return false;
}
}
// 检查135度对角
for(int i = row-1,j=col-1;i>=0&&j>=0;i--,j--){
if(chessboard[i][j]=='Q'){
return false;
}
}
return true;
}
}
14.【37】解数独(难)
题目链接: 解数独
题目描述:
编写一个程序,通过填充空格来解决数独问题。
数独的解法需 遵循如下规则:
数字 1-9 在每一行只能出现一次。
数字 1-9 在每一列只能出现一次。
数字 1-9 在每一个以粗实线分隔的 3x3 宫内只能出现一次。(请参考示例图)
数独部分空格内已填入了数字,空白格用 ‘.’ 表示。
涉及知识点: 回溯、二维递归
思路: 这道题和N皇后隐隐约约有点像,但实际上,N皇后只需要每行确定一个位置,而数独问题需要在每一个位置都找到能填入的数,所以要用到一个二维循环来遍历每个位置[row,col]
,再用一个循环确定当前位置的值能填几,确定之后进行递归,这里的下一层实际上是“确定了[row,col]位置的数值后对应的棋盘”
。另外,本题中只要找到一种数独填法就可以,所以递归中要有一个boolean类型返回值,一旦找到解就立刻向上返回。
代码:
class Solution {
public void solveSudoku(char[][] board) {
backTracing(board);
}
public boolean backTracing(char[][] board){
// 二维循环用来遍历每个位置位置
for(int row = 0;row<9;row++){
for(int col = 0;col<9;col++){
if(board[row][col]!='.'){
continue;
}
// 用来确定该位置的数
for(char k = '1'; k<='9';k++){
if(isValid(k,row,col,board)){
board[row][col]=k;
if(backTracing(board)){
return true;
}
board[row][col]='.';
}
}
return false;
}
}
return true;
}
public boolean isValid(char ch,int row,int col,char[][] board){
// 验证列是否出现过
for(int i = 0;i<9;i++){
if(board[i][col]==ch){
return false;
}
}
// 验证行是否出现过
for(int j = 0;j<9;j++){
if(board[row][j]==ch){
return false;
}
}
// 验证3*3宫内
int a = (row/3)*3;
int b = (col/3)*3;
for(int i = a;i<a+3;i++){
for(int j = b;j<b+3;j++)
if(board[i][j]==ch){
return false;
}
}
return true;
}
}
2022-12-06
【知识点】贪心理论
基本思想
贪心算法的本质是选择每一阶段的局部最优,从而达到全局最优。贪心算法并没有固定的套路,需要靠自己手动模拟,如果模拟可行,并且举不出反例,那就就可以试一试贪心策略,如果不可行,可能需要动态规划。
15.【455】分发饼干(易)
题目链接: 分发饼干问题
题目描述:
假设你是一位很棒的家长,想要给你的孩子们一些小饼干。但是,每个孩子最多只能给一块饼干。
对每个孩子 i
,都有一个胃口值 g[i]
,这是能让孩子们满足胃口的饼干的最小尺寸;并且每块饼干 j
,都有一个尺寸 s[j]
。如果 s[j] >= g[i]
,我们可以将这个饼干j
分配给孩子 i
,这个孩子会得到满足。你的目标是尽可能满足越多数量的孩子,并输出这个最大数值。
涉及知识点: 贪心算法
思路: 本题比较简单,适合用来入门贪心算法,这里的局部最优就是大饼干喂给胃口大的(或是小饼干喂给小胃口的),全局最优就是喂饱尽可能多的小孩。
利用排序+双指针就能很方便解出。
代码:
class Solution {
public int findContentChildren(int[] g, int[] s) {
int sum = 0;
int sPoint = 0;
Arrays.sort(g);
Arrays.sort(s);
for(int i = 0;i<s.length;i++){
// 小饼干喂饱小胃口
if(s[i]>=g[sPoint]){
sum+=1;
sPoint+=1;
if(sPoint==g.length){
break;
}
}
}
return sum;
}
}
16.【1005】K 次取反后最大化的数组和(易)
题目链接: K次反转
题目描述:
给你一个整数数组nums
和一个整数k
,按以下方法修改该数组:
选择某个下标 i
并将 nums[i]
替换为 -nums[i]
。
重复这个过程恰好 k
次。可以多次选择同一个下标 i
。
以这种方式修改数组后,返回数组 可能的最大和 。
涉及知识点: 贪心算法、IntStream
思路: 本题也不难,本题的局部最优是让绝对值大的负数变为正数,当前数值达到最大,整体最优:整个数组和达到最大。
最开始想的有点绕:先将数组从小到大排列,然后统计负数个数negLen,与k比较,如果k-negLen>0且为偶数,则将所有负值转为正值相加;若k-negLen>0且为奇数,则选出绝对值最小的值,将其置为负数;若k-negLen<0,则对前k个绝对值最大的负值变正。这种方法虽然能写,但是要考虑很多细节,提交了好几次才勉强通过后面看了代码随想录的代码,里面是按数值的绝对值大小从大到小排序的,这样在从头遍历的时候,在k>0的情况下,遇到负数就将其变正,这样就能实现局部最优,遍历完成后再看剩余的k是否为奇数,如果是则选排序后数值的最后一个元素(即绝对值最小的数字)将其置为负。代码随想录中用到了IntStream
来进行排序,是之前没有接触过的东西。
代码:
// 笨方法
class Solution {
public int largestSumAfterKNegations(int[] nums, int k) {
int sum = 0;
Arrays.sort(nums);
int negLen = 0;
for(int i = 0;i<nums.length;i++){
if(nums[i]>0){
break;
}
negLen++;
}
int diff = k-negLen;
if(diff>0){
if(diff%2==0){
for(int i = 0;i<nums.length;i++){
sum += Math.abs(nums[i]);
}
}else{
int min = 0;
if(negLen==0){
min = nums[0];
}else{
if(negLen<nums.length){
min = Math.abs(nums[negLen-1])<nums[negLen]?Math.abs(nums[negLen-1]):nums[negLen];
}else{
min = Math.abs(nums[negLen-1]);
}
}
for(int i = 0;i<nums.length;i++){
sum += Math.abs(nums[i]);
}
sum-=2*min;
}
}else{
for(int i = 0;i<k;i++){
sum +=Math.abs(nums[i]);
}
for(int i = k;i<nums.length;i++){
sum+=nums[i];
}
}
return sum;
}
}
// 代码随想录方法
class Solution {
public int largestSumAfterKNegations(int[] nums, int k) {
nums = IntStream.of(nums)
.boxed()
.sorted((o1, o2) -> Math.abs(o2) - Math.abs(o1))
.mapToInt(Integer::intValue).toArray();
for(int i = 0;i<nums.length;i++){
if(nums[i]<0&&k>0){
nums[i] = -nums[i];
k--;
}
}
if(k%2==1){
nums[nums.length-1] = -nums[nums.length-1];
}
return Arrays.stream(nums).sum();
}
}
2022-12-07
17.【322】重新安排行程 (难)
题目链接: 飞机航班问题
题目描述:
给你一份航线列表tickets
,其中 tickets[i] = [fromi, toi]
表示飞机出发和降落的机场地点。请你对该行程进行重新规划排序。
所有这些机票都属于一个从 JFK
(肯尼迪国际机场)出发的先生,所以该行程必须从 JFK
开始。如果存在多种有效的行程,请你按字典排序返回最小的行程组合。
例如,行程 [“JFK”, “LGA”] 与 [“JFK”, “LGB”] 相比就更小,排序更靠前。
假定所有机票至少存在一种合理的行程。且所有的机票 必须都用一次 且 只能用一次。
涉及知识点: 回溯、深搜
思路: 这道题据说是图论的深度优先搜索中使用回溯的例子,但是现在对图论还不太了解,所以还是暂时以回溯会重点。这道题重点在于对容器的使用上,要合理建立航程起点和终点的映射关系,代码中,Map<String,Map<String,Integer>> map
记录航程起点和终点及终点出现次数,其中的Map<String,Integer>
即对应着代码中用Map<String,Integer> temp
来记录航程终点及其在当前起点下的出现次数。整体思路基本上还是回溯的思想。
代码:
class Solution {
Map<String,Map<String,Integer>> map;
Deque<String> res;
public List<String> findItinerary(List<List<String>> tickets) {
map = new HashMap<String, Map<String, Integer>>();
res = new LinkedList<>();
for(List<String> ticket:tickets){
Map<String,Integer> temp;
if(map.containsKey(ticket.get(0))){
temp = map.get(ticket.get(0));
temp.put(ticket.get(1),temp.getOrDefault(ticket.get(1),0)+1);
}else{
temp = new TreeMap<>(); //要注意需要排序
temp.put(ticket.get(1),1);
}
map.put(ticket.get(0),temp);
}
res.add("JFK");
backTracing(tickets.size());
return new ArrayList(res);
}
public boolean backTracing(int ticketNum){
// 当结果长度等于航班数+1时,说明达到终止条件
if(res.size()==ticketNum+1){
return true;
}
String last = res.getLast();
if(map.containsKey(last)){
for(Map.Entry<String,Integer> target: map.get(last).entrySet()){
int count = target.getValue();
if(count>0){
res.add(target.getKey());
target.setValue(count-1);
if(backTracing(ticketNum)){
return true;
}
target.setValue(count);
res.removeLast();
}
}
}
return false;
}
}
18.【806】柠檬水找零(易)
题目链接: 柠檬水找零问题
题目描述:
在柠檬水摊上,每一杯柠檬水的售价为 5
美元。顾客排队购买你的产品,(按账单 bills
支付的顺序)一次购买一杯。
每位顾客只买一杯柠檬水,然后向你付 5
美元、10
美元或 20
美元。你必须给每个顾客正确找零,也就是说净交易是每位顾客向你支付 5
美元。
注意,一开始你手头没有任何零钱。
给你一个整数数组 bills
,其中 bills[i]
是第 i
位顾客付的账。如果你能给每位顾客正确找零,返回 true
,否则返回 false
。
涉及知识点: 贪心
思路: 这道题的策略比较简单,所以看起来只要把这个过程模拟出来就行,但是这里也体现着贪心的思想,因为5美元适用的情况多,所以要优先消耗10美元而不是5美元,则局部最优:遇到账单20,优先消耗美元10,完成本次找零。全局最优:完成全部账单的找零
。
代码:
class Solution {
public boolean lemonadeChange(int[] bills) {
int five = 0;
int ten = 0;
for(int i = 0;i<bills.length;i++){
if(i==0&&bills[i]!=5){
return false;
}
if(bills[i]==5){
five++;
}else if(bills[i]==10){
five--;
ten++;
if(five<0){
return false;
}
}else{
if(ten-1<0){
five -=3;
}else{
ten--;
five--;
}
if(five<0){
return false;
}
}
}
return true;
}
}
19.【376】摆动序列(中)
题目链接: 摆动序列问题
题目描述:
如果连续数字之间的差严格地在正数和负数之间交替,则数字序列称为摆动序列
。第一个差(如果存在的话)可能是正数或负数。仅有一个元素或者含两个不等元素的序列也视作摆动序列。
- 例如,
[1, 7, 4, 9, 2, 5]
是一个 摆动序列 ,因为差值(6, -3, 5, -7, 3)
是正负交替出现的。 - 相反,
[1, 4, 7, 2, 5]
和[1, 7, 4, 5, 5]
不是摆动序列,第一个序列是因为它的前两个差值都是正数,第二个序列是因为它的最后一个差值为零。
子序列 可以通过从原始序列中删除一些(也可以不删)元素来获得,剩下的元素保持其原始顺序。
给你一个整数数组 nums
,返回 nums
中作为 摆动序列 的 最长子序列的长度
。
涉及知识点: 贪心
思路: 本题的局部最优是删除单调坡度上的节点(不包括单调坡度两端的节点);整体最优是整个序列有最多的局部峰值,从而达到最长摆动序列。
本题的贪心思路是比较符合直觉的,主要是自己实现的代码写的比较复杂且结果不对,还是要多锻炼。另外,本题还可以用动态规划来解,但是现在还不太会动态规划,等到动态规划章节再看。
代码:
// 代码随想录的贪心代码
class Solution {
public int wiggleMaxLength(int[] nums) {
int count = 1;
int preDiff = 0;
int curDiff = 0;
for(int i = 1; i < nums.length; i++){
curDiff = nums[i]-nums[i-1];
if(curDiff>0&&preDiff<=0||curDiff<0&&preDiff>=0){
count++;
preDiff = curDiff;
}
}
return count;
}
}
20.【738】单调递增的数字(中)
题目链接: 单调递增数字
题目描述:
当且仅当每个相邻位数上的数字 x
和 y
满足 x <= y
时,我们称这个整数是单调递增的。
给定一个整数 n
,返回 小于或等于 n 的最大数字
,且数字呈 单调递增
。
涉及知识点: 贪心
思路: 本题的局部最优:遇到strNum[i - 1] > strNum[i]的情况,让strNum[i - 1]--,然后strNum[i]给为9,可以保证这两位变成最大单调递增整数。全局最优:得到小于等于N的最大单调递增的整数。
把整数的每位拆开成字符可以方便操作,从后向前遍历,依次比较与前一位数的大小,如果小于前一位,那么前一位的值要减1,当前位的值要在后面变为9,记下当前下标作为“变9”的起始下标,重复上述过程。
代码:
// 代码随想录的贪心代码
class Solution {
public int monotoneIncreasingDigits(int n) {
String s = String.valueOf(n);
char[] chars = s.toCharArray();
int start = s.length();
for(int i = s.length()-1; i > 0; i--){
if(chars[i]<chars[i-1]){
chars[i-1] -= 1;
start = i;
}
}
for(int i = start; i < s.length(); i++){
chars[i] = '9';
}
return Integer.parseInt(String.valueOf(chars));
}
}
2022-12-08
21.【122】买卖股票的最佳时机 II (中)
题目链接: 买卖股票问题
题目描述:
给你一个整数数组 prices
,其中 prices[i]
表示某支股票第 i 天的价格。
在每一天,你可以决定是否购买和/或出售股票。你在任何时候最多只能持有一股股票。你也可以先购买,然后在同一天出售。返回你能获得的最大利润。
涉及知识点: 贪心
思路: 这道题只需要返回能获得的最大利润,其实可以将从第i天
到第j天
获得的利润分解,即prices[j]-prices[i]=prices[j]-prices[j-1]+prices[j-1]-prices[j-2]+...+prices[i+1]-prices[i]
,因此只要保证每次交易都是正利润即可,局部最优:收集每天的正利润,全局最优:求得最大利润。
也可以用动态规划。
代码:
// 贪心
class Solution {
public int maxProfit(int[] prices) {
// 只要收集正利润即可
List<Integer> income = new ArrayList<>();
int sum = 0;
for(int i = 1;i<prices.length;i++){
income.add(prices[i]-prices[i-1]);
}
for(int i = 0;i<income.size();i++){
if(income.get(i)>0){
sum+=income.get(i);
}
}
return sum;
}
}
22.【714】 买卖股票的最佳时机含手续费 (中)
题目链接: 买卖股票含手续费问题
题目描述:
给定一个整数数组 prices
,其中 prices[i]
表示第 i
天的股票价格;整数 fee
代表了交易股票的手续费用。
你可以无限次地完成交易,但是你每笔交易都需要付手续费。如果你已经购买了一个股票,在卖出它之前你就不能再继续购买股票了。返回获得利润的最大值。
注意:这里的一笔交易指买入持有并卖出股票的整个过程,每笔交易你只需要为支付一次手续费。
涉及知识点: 贪心
**思路:**这道题其实用动态规划更好理解,用贪心的话会有一点点绕。用prices[i]+fee
作为第i
天的买入价格buy,如果遇到prices[j]+fee<buy
,则令buy =prices[j]+fee
,即选择在第j
天买比在第i
天买更能获得大的收益。若prices[j]>buy
,说明在第j
天卖出是可以获得正利润的,但是不是最大的还不知道,所以这里有一个“反悔操作”
,即可以假定在第j
天卖出,然后以prices[j]
作为新的buy
(这里不用加fee
是因为只是假设要卖,不是真卖,所以相当于fee
在前面已经出过了),然后观望后面的价格,如果prices[k]
比buy大,则说明后面卖利润更高,不用加fee
代表着可以撤销在第j
天的卖出操作,放到第k天卖,重复上述过程。
反悔操作:prices[k]-(prices[i]+fee) = prices[k]-prices[j]+prices[j]-(prices[i]+fee)
代码:
class Solution {
public int maxProfit(int[] prices, int fee) {
int buy = prices[0]+fee;
int sum = 0;
for(int i = 0;i<prices.length;i++){
if(prices[i]+fee<buy){
buy = prices[i]+fee;
}else if(prices[i]>buy){
sum += prices[i]-buy;
buy = prices[i]; //反悔操作
}
}
return sum;
}
}
23.【135】分发糖果 (难)
题目链接: 分发糖果问题
题目描述:
n
个孩子站成一排。给你一个整数数组 ratings
表示每个孩子的评分。
你需要按照以下要求,给这些孩子分发糖果:
- 每个孩子至少分配到
1
个糖果。 - 相邻两个孩子评分更高的孩子会获得更多的糖果。
请你给每个孩子分发糖果,计算并返回需要准备的 最少糖果数目 。
涉及知识点: 贪心
思路: 这道题不能想着一次遍历就同时兼顾左右相邻的孩子,最开始就是想兼顾所以钻进牛角尖。应该从前向后遍历来比较每个孩子的评分是否大于其左边孩子,然后再从后向前遍历比较每个孩子的评分是否大于其右边孩子,这样就可以全面考虑到左右的情况。
代码:
class Solution {
// 要确定一个方向的糖果数量后再确定另一边,不然会顾不过来
public int candy(int[] ratings) {
int sum = 0;
int[] candys = new int[ratings.length];
candys[0] = 1;
// 前向遍历,看右边孩子评分是否比当前孩子高
for(int i = 0;i<ratings.length-1;i++){
if(ratings[i]<ratings[i+1]){
candys[i+1] = candys[i]+1;
}else{
candys[i+1] = 1;
}
}
// 后向遍历,看左边孩子评分是否比当前孩子高
for(int i = ratings.length-1;i>0;i--){
if(ratings[i]<ratings[i-1]){
if(candys[i-1]<candys[i]+1){
candys[i-1]=candys[i]+1;
}
}
}
for(int candy:candys){
sum += candy;
}
return sum;
}
}
24.【406】根据身高重建队列 (中)
题目链接: 根据身高重建队列
题目描述:
假设有打乱顺序的一群人站成一个队列,数组people
表示队列中一些人的属性(不一定按顺序)。每个people[i] = [hi, ki]
表示第 i
个人的身高为 hi
,前面正好有 ki
个身高大于或等于hi
的人。
请你重新构造并返回输入数组 people
所表示的队列。返回的队列应该格式化为数组 queue
,其中 queue[j] = [hj, kj]
是队列中第 j
个人的属性(queue[0]
是排在队列前面的人)。
涉及知识点: 贪心
思路: 这道题理解起来稍微有点绕,和分糖果题类似,也是涉及到两个维度hi, ki
,如果同时兼顾就会顾此失彼,所以还是要先从一个维度出发。这里要先从身高hi
出发从高到底排序(身高相同时ki
小的站前面),这样排序后,后面的人就算站到前面人前面,也不会对前面人的属性产生影响,于是就可以按他们的ki
进行插空,这样可以保证前面有ki
个身高大于等于他的人。
代码:
class Solution {
public int[][] reconstructQueue(int[][] people) {
// 按身高从大到小排,若身高相同k小的站前面
Arrays.sort(people, (a, b) -> {
if (a[0] == b[0]) return a[1] - b[1];
return b[0] - a[0];
});
LinkedList<int[]> que = new LinkedList<>();
for(int[] p:people){
que.add(p[1],p);
}
return que.toArray(new int[people.length][]);
}
}
2022-12-09
25.【55】跳跃游戏 (中)
题目链接: 跳跃游戏
题目描述:
给定一个非负整数数组 nums
,你最初位于数组的第一个下标 数组中的每个元素代表你在该位置可以跳跃的最大长度。
判断你是否能够到达最后一个下标。
涉及知识点: 贪心
思路: 这道题如果拘泥于每次跳几步的问题就想偏了,其实不需要具体看走几步,只需要看每次能走的最大范围,最后看最大范围是否包括终点位置即可。局部最优解:每次取最大跳跃步数(取最大覆盖范围),整体最优解:最后得到整体最大覆盖范围,看是否能到终点。
代码:
class Solution {
// 不用拘泥于跳几步,只用看能跳的范围有没有覆盖到终点
public boolean canJump(int[] nums) {
if (nums.length == 1) {
return true;
}
int end = 0;
for(int i = 0;i<=end;i++){
if(nums[i]+i>end){
end = nums[i]+i;
}
if(end>=nums.length-1){
return true;
}
}
return false;
}
}