动态规划解决的问题有
背包问题 01背包 多重背包 完全背包问题
打家劫舍
股票问题
子序列问题
动态规划的本质性解题步骤
1.dp数组的含义,以及下标的含义
//到底是定义一维的dp数组 还是二维的dp数组
求子序列 求背包 二维数组
i j都是什么意思?
dp[i][j];
2.递推公式 阅读题目,寻找关系。 //理解题目的大概意思
3.dp数组初始化 //这个好难,dp数组的初始化。
4.dp数组遍历顺序(关键)
5.打印dp数组
dp[i][j]=max(dp[i-1][j-weight[i]]+value[i],dp[i-1][j]);
//当前层是由上一层推到出来的
// 滚动数组将二维数组转为一维数组
//当前层是由上一层拷贝而来的,将上一层拷贝给当前层,把新的层覆盖到当前层
dp[j] 容量为j的背包能装的最大价值是多少?
一维dp数组
dp[j]=max(dp[j]//不妨物品i,将上一层数据拷贝下来了,dp[j-wegiht[i]+value[i]]);
//初始化
dp[0]=0; 背包容量为0的时候,装的价值为0;
//遍历顺序
一维数组遍历的顺序
for()
for(倒叙遍历)j=weight[i],j>=weight[i],j--;
//保证物品只添加了一次
//一维dp数组,每次重复利用的,比较讲究遍历的顺序。
//
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mZ32HGZR-1691505556706)(C:\Users\周鑫\AppData\Roaming\Typora\typora-user-images\image-20230708163034444.png)]
class Solution {
//思维转化,求出来的子集它的和等于数组总和的一半,那么求出总和然后再除以2,得到一半,即在这个背包里面找到价值为一半的石头,它的价值和体积是相同的。
public boolean canPartition(int[] nums) {
int sum= Arrays.stream(nums).sum();
//给定一个非空的数组 nums,是否可以将这个数组分割成两个子集
// 一个数组,里面的元素的值既是体积也是价值
// 首先求和,然后求出target,target就是最大的价值,这个背包的最大价值
int target=sum/2;
//剪枝优化,如果是奇数,那么就直接返回了
if (sum%2==1){
return false;
}
int dp[]=new int[target+1];
dp[0]=0;
for (int i = 0; i < nums.length; i++) {
//先遍历物品
for (int j = target; j >=nums[i] ; j--) {
//重后往前进行遍历,可以找到。
//每个数都使用一次
dp[j]=Math.max(dp[j],dp[j-nums[i]]+nums[i]);
//把上衣行复制下来的,本质没有改变。
}
if(dp[target] == target)
return true;
}
return dp[target] == target;
}
//给一个集合,把集合分为两个集合,使得两个子集的元素和相等
// 元素不能重复使用 01背包问题
}
和零一背包的区别在于每个物品可以使用无数次;
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-InjjRy8i-1691505556707)(C:\Users\周鑫\AppData\Roaming\Typora\typora-user-images\image-20230714092231195.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-h8zHn92t-1691505556708)(C:\Users\周鑫\AppData\Roaming\Typora\typora-user-images\image-20230714092638955.png)]
先遍历物品在遍历背包,结果是相同的。
滑动窗口
//算法实现的大致思路
int left=0,right=0;//双指针
while(letf<right&&right<s.size())
window.add(s[right]);
right++;
while(window needs shrink){
winodw.remove(s[left]);
letf++;
}
算法框架
/* 滑动窗口算法框架 */
void slidingWindow(String s) {
// 用合适的数据结构记录窗口中的数据
HashMap<Character, Integer> window = new HashMap<>();
int left = 0, right = 0;
while (right < s.length()) {
// c 是将移入窗口的字符
char c = s.charAt(right);
window.put(c, window.getOrDefault(c, 0) + 1);
// 增大窗口
right++;
// 进行窗口内数据的一系列更新
...
/*** debug 输出的位置 ***/
// 注意在最终的解法代码中不要 print
// 因为 IO 操作很耗时,可能导致超时
System.out.printf("window: [%d, %d)\n", left, right);
/********************/
// 判断左侧窗口是否要收缩
while (left < right && window needs shrink) {
// d 是将移出窗口的字符
char d = s.charAt(left);
window.put(d, window.get(d) - 1);
// 缩小窗口
left++;
// 进行窗口内数据的一系列更新
...
}
}
}
算法常用模板,直接在上面进行修改
public class 模板 {
public static void main(String[] args) {
}
public static String minWindow(String s,String t){
//s是原来的字串,t是目标字串
Map<Character,Integer> need=new HashMap<>();
Map<Character,Integer> window=new HashMap<>();
for (char c:t.toCharArray()) {
need.put(c, need.getOrDefault(c, 0) + 1);
//API getOrDefault的用法,如果存在返回对应的值,如果不存在返回-1;
}
int left=0,right=0; //左指针和右指针
int valid=0; //有效数字
int start=0,len=Integer.MAX_VALUE;
while (right<s.length()){
char c=s.charAt(right);
right++;
if (need.containsKey(c)){
window.put(c,window.getOrDefault(c,0)+1);
if (window.get(c).equals(need.get(c))){
valid++; //找到满足的数字
}
}
while(valid==need.size()){
if (right-left<len){
start=left;//从left开始
len=right-start;
}
char d =s.charAt(left);
left++;
if (need.containsKey(d)){
if (window.get(d).equals(need.get(d))){
valid--;
window.put(d,window.get(d)-1);
}
}
}
}
return len==Integer.MAX_VALUE?"":s.substring(start,start+left);
}
}
//理论上你可以设计两端都开或者两端都闭的区间,但设计为左闭右开区间是最方便处理的。因为这样初始化 left = right = 0 时区间 [0, 0) 中没有元素,但只要让 right 向右移动(扩大)一位,区间 [0, 1) 就包含一个元素 0 了。如果你设置为两端都开的区间,那么让 right 向右移动一位后开区间 (0, 1) 仍然没有元素;如果你设置为两端都闭的区间,那么初始区间 [0, 0] 就包含了一个元素。这两种情况都会给边界处理带来不必要的麻烦。
leetcode 76题最小覆盖字串
leetcode 567 字符串的排列
leetcode 438 找到字符串中所有的字母异位词
BigInteger 和BigDecimal;
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-edFGH31t-1691505556708)(C:\Users\周鑫\AppData\Roaming\Typora\typora-user-images\image-20230703102133493.png)]
在 java.math 包下面的。
public class 大数 {
public static void main(String[] args) {
BigInteger B1 =new BigInteger("123");
BigInteger B2 =new BigInteger("125");
System.out.println(B1.multiply(B2));
System.out.println(B1.add(B2));
System.out.println(B1.max(B2));
System.out.println(B2.subtract(B1));
System.out.println(B2.divide(B1));
}
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qPERDjcT-1691505556709)(C:\Users\周鑫\AppData\Roaming\Typora\typora-user-images\image-20230703102905433.png)]
二分法的原型 有序 搜索一个target 返回对应索引的值
二分最基础的框架
int binarySearch(int[] nums, int target) {
int left = 0, right = ...;
while(...) {
int mid = left + (right - left) / 2;
if (nums[mid] == target) {
...
} else if (nums[mid] < target) {
left = ...
} else if (nums[mid] > target) {
right = ...
}
}
return ...;
}
left + (right - left) / 2 就和 (left + right) / 2 的结果相同,但是有效防止了letf和right太大,直接相加导致溢出的情况。
二分查找的模板,以后都采用左闭右闭的思路
基础的查找
int binary_search(int[] nums, int target) {
int left = 0, right = nums.length - 1;
while(left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] < target) {
left = mid + 1;
} else if (nums[mid] > target) {
right = mid - 1;
} else if(nums[mid] == target) {
// 直接返回
return mid;
}
}
// 直接返回
return -1;
}
想左搜索,这是由于 12223,如果寻找2,右三个的下标都是2,左寻找,是找到左边界那个
int left_bound(int[] nums, int target) {
int left = 0, right = nums.length - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] < target) {
left = mid + 1;
} else if (nums[mid] > target) {
right = mid - 1;
} else if (nums[mid] == target) {
// 别返回,锁定左侧边界
right = mid - 1;
}
}
// 判断 target 是否存在于 nums 中
if (left < 0 || left >= nums.length) {
return -1;
}
// 判断一下 nums[left] 是不是 target
return nums[left] == target ? left : -1;
}
//最后判断 left 是否越界,这个很重要。
//向右进行搜索
int right_bound(int[] nums, int target) {
int left = 0, right = nums.length - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] < target) {
left = mid + 1;
} else if (nums[mid] > target) {
right = mid - 1;
} else if (nums[mid] == target) {
// 别返回,锁定右侧边界
left = mid + 1;
}
}
// 判断 target 是否存在于 nums 中
// if (left - 1 < 0 || left - 1 >= nums.length) {
// return -1;
// }
// 由于 while 的结束条件是 right == left - 1,且现在在求右边界
// 所以用 right 替代 left - 1 更好记
if (right < 0 || right >= nums.length) {
return -1;
}
return nums[right] == target ? right : -1;
}
leetcode 1011
在传送带上面包裹必须在days天内从一个港口运送到另一个港口,给出包裹的质量,包裹是依次运出的,给出天数,在这些天内都运出去,返回days天内将传送带上所有包裹去拿不运出去的最低能力
class Solution {
public int shipWithinDays(int[] weights, int days) {
//运输 涉及到赵左边的最小值或者右边的最大值问题,都可以采用二分查找来解决
//二分 letf 和right是如何确定的呢?
//days天运出,最多一天,最迟每天云一趟,那么最大承载量就是所有的和
// 最小的承载力就是最大的包裹的质量
int left=Arrays.stream(weights).max().getAsInt();
int right=Arrays.stream(weights).sum(); //求得和
while (left<right){
//往左搜索
int mid =left+(right-left)/2;
if (isC(weights,days,mid)) {
//搜索左边界的题目
//运的比较多了,我们要往小的进行寻找
right=mid;
}else{
//每天云的太少了
left=mid+1;
}
}
//不断的往左进行搜索的,结束的条件是left=right
return right;
}
public static boolean isC(int weights[],int Days,int H){
//如何判断呢
int count=1; //记录当前的天数
int leiji=0;
for (int i = 0; i < weights.length; i++) {
leiji+=weights[i];
if (leiji>H){
count++;
leiji=weights[i];
}
if (count>Days){
return false;//运载量是现实太小了根部运输不了
}
}
return true;
}
}
leetcode 875爱吃香蕉
有n堆香蕉,存在数组里面了,i下标对应的索引就是多少跟香蕉,警卫离开了,将在h小时后回来。
珂珂决定他吃香蕉的速度,一个小时吃掉k跟,如果这堆香蕉小于k跟,那么就直接吃掉,现在求h小时内吃掉所有香蕉的最小速度
//你可以一个小时内吃掉所有的香蕉,也可以旋转慢慢吃香蕉
确定边界 left是左边界,每个小时必须吃1个
right是右边界,每个小时最多全部吃完
这是第一版写的答案,没有处理好时间关系
int left=1;//不太好确定左边界
int right= Arrays.stream(piles).max().getAsInt();
//因为一个小时吃完,那么最快就吃一堆
//向左进行搜索的
while(left<right){
int mid=left+(right-left)/2;
if (isS(piles,h,mid)){
//符合的话,是向左进行逼近的
right=mid; //不断的向左进行收缩 这个速度是可以直接吃完的
}else {
left=mid+1;
}
}
return left;
}
public static boolean isS(int []piles,int h,int speed){
//如何判断是否满足呢?
//每一个小时吃一推,多了就不吃了
int time=1;
for (int i = 0; i < piles.length; i++) {
int temp=0;
//这是这一推的东西
time++;
temp=piles[i]-speed;
while (piles[i]>speed) {
time++;
//ear>s,ear-s第一次吃完,ear-s-s第二次吃完,
//下一次就再减去
temp=temp-speed;
if (temp<=0){
break;
}
}
//时间边了,就返回false
if (time>h){
return false;
}
}
return true;
}
//正确处理时间关系
class Solution {
public int minEatingSpeed(int[] piles, int h) {
int right=Arrays.stream(piles).max().getAsInt();
int left=1;
while (left<right){
int mid =left+(right-left)/2;
int costTime=0;
for (int pile:piles){
int currentTime=pile/mid;
if (mid*currentTime<pile){
currentTime++;
}
costTime+=currentTime;
}
if (costTime<=h){
//比给定的时间小,要继续向左搜索
right=mid;
}else{
left=mid+1;
//说明比时间大,速度要加快了
}
}
return left;
}
}
晚上看完 前缀和 和差分数组 和哈希表
明天早上看 滑动窗口和双指针
前缀和技巧 用于快速、频繁的计算一个索引区间的值之和
返回两个索引之间的值,一般方法
class NumArray {
private int[] nums;
public NumArray(int[] nums) {
this.nums = nums;
}
public int sumRange(int left, int right) {
int res = 0;
for (int i = left; i <= right; i++) {
res += nums[i];
}
return res;
}
}
//但是这样时间复杂度会特别的高。
//如何进行优化呢?
class NumArray {
// 前缀和数组
private int[] preSum;
/* 输入一个数组,构造前缀和 */
public NumArray(int[] nums) {
// preSum[0] = 0,便于计算累加和
preSum = new int[nums.length + 1];
// 计算 nums 的累加和
for (int i = 1; i < preSum.length; i++) {
preSum[i] = preSum[i - 1] + nums[i - 1];
}
}
/* 查询闭区间 [left, right] 的累加和 */
public int sumRange(int left, int right) {
return preSum[right + 1] - preSum[left];
}
}
//这样时间复杂度就是o1了
想要计算nums[i,j]的累加和,只要计算preSum[j+1]-preSum[i]即可;
前缀和主要适用的场景是原始数组不会被修改的情况下,频繁查询某个区间的累加和。
差分数组
应用场景是 频繁的原始数组的某个子区间的元素进行增减
比如要将区间nums[2,6]全部加1,再给nums[2,9]全部减3,最后nums是多少
如果用for循环去写的话,时间复杂度很高。
类似于前缀和数组
我们构造一个diff差分数组 diff就是nums[i]-nums[i-1];
leetcode 1109
右n个航班,它们分别重1-n进行编号,有一份航班表,表中第i条预定记录bookings[i]=[i,j,k] 意味着从i-j上每个航班预定了k个座位,实质上就是差分数组,对应的数组加1,我们可以构建一个差分数组
hash table 也叫做散列表
一般用来快速判断一个元素是否出现在集合里面
数组本身也是一个哈希表
leetcode 242 有效的字母异位词
给定两个字符串 s t,编写一个函数判断t是否是s的字母异位词
采用哈希表
字母对应26个,创建数组,对应的下标索引转换成字母,索引对应的值就是出现的次数
class Solution {
public boolean isAnagram(String s, String t) {
int [] record=new int[26]; //字母的长度为26.
for (int i = 0; i <s.length(); i++) {
record[s.charAt(i)-'a']++; //对应的下标++;
}
for (int i = 0; i <t.length() ; i++) {
record[t.charAt(i)-'a']--;
}
for(int count:record){
if (count!=0){
return false;
}
}
return true;
}
}
leetcode 349 求两个数组之间的交集
//方法一set集合
class Solution {
public int[] intersection(int[] nums1, int[] nums2) {
Integer> set2 =new HashSet<>();
for (int a:nums1){
set1.add(a);
}
for (int b:nums2){
if (set1.contains(b)){
set2.add(b);
}
}
//set2就是两个数组之间重复的元素,在将set转为数组
int arr [] =new int[set2.size()];
int j=0;
for (int arrs:set2){
arr[j++]=arrs;
}
return arr;
}
//数组
常用的hash结构
数组 集合 set 映射(散列表)map;
双指针算法
得到单链表的倒数第k个节点
7.4今天需要学习的内容时
滑动窗口 14届蓝桥杯省赛的真题 百度之星真题 两个小时用来看vue和element。
滑动窗口
就是 维护一个窗口,不断滑动,然后更新答案,时间复杂度是ON,比字符串算法要高效很多。 困难点 如何向窗口中添加新元素,如何缩小窗口
letf=0,right=0;
while()
window.add()
while(window need shrink){
window.remove();
left++;
}
//滑动窗口
//列题一 一个字符串 s和t,返回s中股改t所有字符的最小字串,如果s中不存在覆盖所有字符额字串,则返回空字符"";
滑动窗口就算结束了
螺旋矩阵 54;
晚上需要回看的题目 leetcode209
链表就是右节点组成的
虚拟头节点,可以避免处理空指针的情况,降低代码的复杂度
leetcode21 合并两个链表
leetcode22 和21题刚好相反,是分开一个链表
单链表的倒数第k给节点
//找到链表倒数第k个节点
链表有n个节点,倒数第k个节点就是n-k+1 个节点
但是现在不知道有多少个节点,如果先遍历一遍得到n的值,在遍历链表得到第n-k+1的值,相当于遍历的两次
先让p1走k步,还剩n-k步,再让一个p2指向head,p1和p2同时向前移动,p1走到链表的结尾空指针的时候前进了n-k步
p2也从head开始前进了n-k步,停留在n-k+1的节点上。刚好倒数第k个节点
class Solution {
public ListNode removeNthFromEnd(ListNode head, int n) {
ListNode dummy=new ListNode(-1);
dummy.next=head;
ListNode x =find(dummy,n+1);
//删除倒数第k个节点,需要找到倒数第k+1个节点,
// 让倒数k+1节点指向倒数k-1,相当于删除了倒数第k个节点
x.next=x.next.next; //删除中间的节点
return dummy.next;
}
public static ListNode find(ListNode head,int k){
//找到倒数第k个节点
ListNode p1=head;
for (int i = 0; i <k ; i++) {
p1=p1.next;
}
ListNode p2=head;
while (p1!=null){
p2=p2.next;
p1=p1.next;
}
return p2;
}
}
栈 先进后出的 入栈 和出栈的操作
压栈叫做 push 出栈叫做pop
实列 碗的摆放 羽毛球摆放
栈是一个特殊的结构,用链表和数组都可以实现
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vnSoXBvY-1691505556709)(C:\Users\周鑫\AppData\Roaming\Typora\typora-user-images\image-20230705084110234.png)]
栈常用操作的代码实现
import java.util.Arrays;
public class MyStack{
private int[] arr;
// size 记录栈中元素个数
private int size;
public MyStack(){
// 调用无参构造方法 默认最大容量12
this(12);
}
public MyStack(int MaxSize){
this.arr = new int[MaxSize];
}
// 入栈
public int push(int value){
if(this.size == arr.length){
// 栈满 ,需要扩容
int[] copyArr;
// 复制arr 数组并扩容一倍
copyArr = Arrays.copyOf(arr,2 * arr.length);
arr = copyArr;
}
//将元素添加到size位置
this.arr[size] = value;
// 元素个数加一
this.size++;
// 返回添加元素
return value;
}
// 出栈
public int pop(){
if(isEmpty()){
//没有元素
//抛出运行时异常,此处也可以自定义异常
throw new RuntimeException("栈中没有元素,不能出栈....");
}
// 获得栈顶元素
int value = this.arr[size - 1];
// size - 1 之后, 下一次插入时会覆盖原数据,利用覆盖替代删除
this.size--;
return value;
}
// 获取栈顶元素
public int peek(){
if(isEmpty()){
//没有元素
//抛出运行时异常,此处也可以自定义异常
throw new RuntimeException("栈中没有元素,不能出栈....");
}
return this.arr[this.size - 1];
}
//获取元素个数
public int getSize(){
return this.size;
}
//判断元素是否为空
public boolean isEmpty(){
return this.size == 0;
}
}
队列 只允许在一端进行操作,在另一端进行删除操作的特殊线性表,队列具有先进先出的特点
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XPsrn3HT-1691505556710)(C:\Users\周鑫\AppData\Roaming\Typora\typora-user-images\image-20230705084621325.png)]
生活中的列子,排队
linkedlist实现了queue队列的接口,可以通过linkedlist来实例化,
添加元素 offer 删除元素 poll;
栈一般采用链表作为底层结构
基本方法的实现代码
public class MyLinkedListQueue {
// 结点
static class Node {
public int val;
public Node next;
public Node(int val) {
this.val = val;
}
}
public Node head;
public Node last;
public int useSize;
public MyLinkedListQueue(){
head = null;
last = null;
}
public void offer(int val) {
Node tmp = new Node(val);
if(head == null){
head = tmp;
last = tmp;
}else{
last.next = tmp;
last = tmp;
}
useSize++;
}
public int poll() {
if(isEmpty()) {
throw new RuntimeException("队列为空");
}
int val = head.val;
head = head.next;
if(head == null){
last = null;
}
useSize--;
return 0;
}
public int peek() {
if(isEmpty()){
throw new RuntimeException("链表为空");
}
return head.val;
}
public int size() {
return useSize;
}
public boolean isEmpty(){
return useSize==0;
}
leetcode 232 用栈来模拟队列
我们知道 栈是先进后出的,队列是先进先出的,可以用两个栈来模拟队列
队列 1 2 3 4
栈一[1234
栈二 1234]
//代码
//使用两个栈来模拟队列的。
class MyQueue {
Stack<Integer> stackIn;
Stack<Integer> stackOut;
public MyQueue() {
stackIn= new Stack<>(); //栈1
stackOut=new Stack<>(); //栈2
}
public void push(int x) {
stackIn.push(x);
}
public int pop() {
dumpstackIn();
return stackOut.pop();
}
public int peek() {
dumpstackIn();
return stackOut.peek();
}
public boolean empty() {
return stackIn.isEmpty()&&stackOut.isEmpty(); //两个都为空,说明队列才为0
}
//实现很多类都需要一个函数,直接将这个函数抽象出来。
private void dumpstackIn(){
//
if (!stackOut.isEmpty()) return; //out里面还有值;
while (!stackIn.isEmpty()){
stackOut.push(stackIn.pop());
}
}
}
用一个队列来模拟栈
队列是先进先出的 leetcode 225
后 先
3 2 1 要来模拟栈,添加元素的时候,在末尾加一个,然后将前面的元素全部弹出来,即这个数就添加到了队首,就模拟了栈,即先进后出,即模拟了队列后进来的先出去。
class MyStack {
//用队列来模拟栈
Queue<Integer> queue;
public MyStack() {
queue=new LinkedList<>();
}
public void push(int x) {
queue.offer(x);
int size=queue.size();
while (size-- > 1)
{
queue.offer(queue.poll());
}
}
public int pop() {return queue.poll();
}
public int top() {
return queue.peek();
}
public boolean empty() {
return queue.isEmpty();
}
}
leetcode 20题
// 符号匹配,用栈来。
class Solution {
public boolean isValid(String s) {
//判断一个括号,用栈来写。
Stack<Character> stack =new Stack<>();
if (s.length()%2!=0){
return false;
}
//如果刚开始就是左括号,那么一定不对
for (int i = 0; i < s.length(); i++) {
char c=s.charAt(i);
if (c=='('||c=='{'||c=='['){
stack.push(c); //将字母放进去
}else{
if (c==')'||c==']'||c=='}'){
if (stack.size()==0){
return false; //想要出栈,一定要考虑长度是否为0,如果长度为0,直接返回false
}
char d=stack.peek(); //找出来d;
if (d=='('&&c==')'||d=='['&&c==']'||d=='{'&&c=='}'){
stack.pop();
}else {
return false;
}
}
}
}
if (stack.isEmpty()==true){
//为空
return true;
}else {
return false;
}
}
}
栈解决字符串问题
leetcode 1047;
class Solution {
public String removeDuplicates(String s) {
Stack<Character> stack =new Stack<>();
for (int i = 0; i < s.length(); i++) {
char c=s.charAt(i);
if (stack.isEmpty() ||stack.peek()!=c){
stack.push(c); //相邻之间不能相等,不相等进栈
}else{
stack.pop(); //如果有相等就出战
}
}
String str="";
while (!stack.isEmpty()){
str=stack.pop()+str; //拼接的时候注意顺序
}
return str;
}
}
还可以使用双指针的思想
class Solution {
public String removeDuplicates(String s) {
char[] ch = s.toCharArray(); // 将输入的字符串转换为字符数组
int fast = 0; // 快指针,用于遍历原始字符数组
int slow = 0; // 慢指针,用于指示处理后的字符数组的尾部位置
while (fast < s.length()) {
ch[slow] = ch[fast]; // 用快指针的值覆盖慢指针的值
if (slow > 0 && ch[slow] == ch[slow - 1]) { // 如果当前字符与前一个字符相同
slow--; // 慢指针退后一步,表示将当前重复的字符删除
} else {
slow++; // 否则,慢指针前进一步
}
fast++; // 快指针前进一步
}
return new String(ch, 0, slow); // 根据慢指针位置创建新的字符串,表示处理后的结果,即去除重复字符的字符串
}
}
leetocde973
class Solution {
public int[][] kClosest(int[][] points, int k) {
PriorityQueue<long[]> queue =new PriorityQueue<>((a,b)->Long.compare(b[0],a[0]));
//比较的就是距离远点的平方; //比较规则的时候,需要使用long的包装类
for (int i = 0; i < points.length; i++) {
int []cur=points[i]; //当前数组
//队列里面存放的是数组,数组的零索引是对应的平方和,1索引是对应的下标;
queue.add(new long[]{1L*cur[0]*cur[0]+cur[1]*cur[1],1l*i}); //将平方和加到队列中去, if(queue.size()>k){
}
//把对应的距离 和索引都加到数组中去,数组在加到队列中去
//一共需要k个,找一个出队列的条件
if (queue.size()>k){
//寻找k个元素,如果大于了k,就需要讲小的进行返回,规则就是小的在·前面
queue.poll();
}
}
int [][]ans=new int[k][];
int p=0;
while (!queue.isEmpty()) {
ans[p++] = points[(int)queue.poll()[1]];
}
return ans;
}
}
U307988 数组操作 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
//java代码
import java.util.PriorityQueue;
import java.util.Scanner;
public class Seg implements Comparable<Seg>{
int len,l,r;
public Seg(int len, int l, int r) {
this.len = len;
this.l = l;
this.r = r;
}
//比较多个内容,长度和左节点,就需要构建一个对象,实现comparable接口,重写compareTo方法
@Override
public int compareTo(Seg o) {
if (len==o.len){
return l-o.l; //返回一共数,升序排序
}else {
//长度不等,就看谁的长度大
return o.len-len;//进行降序排序
}
}
}
class m {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
int kase = scanner.nextInt();
while (kase-- > 0) {
int n = scanner.nextInt();
solve(n);
}
}
public static void solve(int n){
PriorityQueue<Seg> queue =new PriorityQueue<>();
queue.add(new Seg(n,1,n));
int []ans=new int[n+1]; //存储结果
int cnt=1;
while (!queue.isEmpty()){
Seg s=queue.poll();//出来的是长度最大,左端点最小的段
int mid=s.l+s.r; //取中间值
if (mid%2==1){
mid=(mid-1)>>1;
}else{
mid>>=1;
}
ans[mid]=cnt;//中间的数改变
cnt++; //还需要再++;
//都添加到优先队列中去,用规则进行比较,先看长度,长度相等比位置
//如果长度不等就看长度,先出来的都是靠左,或者是长度比较长的
if (mid-1>=s.l){
//说明左边还有0
queue.add(new Seg(mid-s.l,s.l,mid-1));
}
if (s.r >= mid + 1) {
queue.add(new Seg(s.r - mid, mid + 1, s.r));
}
}
for (int i = 1; i <= n; i++) {
System.out.print(ans[i] + " ");
}
System.out.println();
}
}
leetcode 347 前k个高频元素
//347
class Solution {
public int[] topKFrequent(int[] nums, int k) {
//一个元素出现的次数,可以想到用hash表来存放这个元素,需要k个,那么就用优先队列,将哈希表里面的元素都添加到优先队列中去,设置比较的规则;
Map<Integer, Integer> occurrences = new HashMap<Integer, Integer>();
//定义了一个hash表·
for (int num : nums) {
//讲数组中的元素·全部都·加入·到·哈希表中去
occurrences.put(num, occurrences.getOrDefault(num, 0) + 1);
}
// int[] 的第一个元素代表数组的值,第二个元素代表了该值出现的次数
PriorityQueue<int[]> queue = new PriorityQueue<int[]>(new Comparator<int[]>() {
public int compare(int[] m, int[] n) {
//比较的是后面出现的次数
return m[1] - n[1];
}
});
//lamada表达式还可以再进行简化书写
for (Map.Entry<Integer, Integer> entry : occurrences.entrySet()) {
//用entrySet来遍历集合,得到对应的键和值
//将值和键添加到优先队列中去,排序是按照右低到高来排序的
//当队列里面的数据都满了的时候,将队列里面最小的拿出来进行比较
//如果小于,如果队列里面小于count,那么就把这个出队,再把新的添加到优先队列中去
int num = entry.getKey(), count = entry.getValue();
if (queue.size() == k) {
if (queue.peek()[1] < count) {
queue.poll();
queue.offer(new int[]{num, count});
}
} else {
queue.offer(new int[]{num, count});
}
}
int[] ret = new int[k];
//需要取k个出来
for (int i = 0; i < k; ++i) {
//取出的来是对应map集合的键
ret[i] = queue.poll()[0];
}
return ret;
}
}
二叉树的遍历方式 dfs(前序 中序 后序) bfs层序;
二叉树的属性 最小深度 最大深度 平衡二叉树等
满二叉树 如果一棵二叉树只有度为0的结点和度为2的结点,并且度为0的结点在同一层上,则这棵二叉树为满二叉树。
完全二叉树 除了底层节点可能没有填满,其余每层节点数都到达了最大值,并且最下面一层节点都集中再该层最左边的若干位置;
二叉搜索树 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值;若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值;它的左、右子树也分别为二叉排序树
平衡二叉搜索树 AVL树,如果它是一棵空树,或者左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一颗平衡二叉树。
存储方式 链式存储就是用的指针 顺序存储方式用的数组
顺便存储 如果父节点的数组下标是i,那么左孩子就是2*i+1,右孩子是i*2+2;
二叉树遍历 前序遍历 中序遍历 后序遍历
深度优先搜索 dfs 栈,一般使用递归的方式,前序 中序 后序 都是递归的方式来遍历的
也可以用迭代法来实现,用递归解决都可以用迭代来解决。
广度优先搜索 bfs 一层一层的去遍历,再图论中使用的多。队列,迭代法。
前序遍历 中左右
中序遍历 左中右
后续遍历 左右中
遍历方式,递归方法进行遍历,
// 前序遍历·递归·LC144_二叉树的前序遍历
class Solution {
public List<Integer> preorderTraversal(TreeNode root) {
List<Integer> result = new ArrayList<Integer>();
preorder(root, result);
return result;
}
public void preorder(TreeNode root, List<Integer> result) {
if (root == null) {
return;
}
result.add(root.val);
preorder(root.left, result);
preorder(root.right, result);
}
}
// 中序遍历·递归·LC94_二叉树的中序遍历
class Solution {
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> res = new ArrayList<>();
inorder(root, res);
return res;
}
void inorder(TreeNode root, List<Integer> list) {
if (root == null) {
return;
}
inorder(root.left, list);
list.add(root.val); // 注意这一句
inorder(root.right, list);
}
}
// 后序遍历·递归·LC145_二叉树的后序遍历
class Solution {
public List<Integer> postorderTraversal(TreeNode root) {
List<Integer> res = new ArrayList<>();
postorder(root, res);
return res;
}
void postorder(TreeNode root, List<Integer> list) {
if (root == null) {
return;
}
postorder(root.left, list);
postorder(root.right, list);
list.add(root.val); // 注意这一句
}
}
//迭代法模拟递归,遍历二叉树
//普通方法遍历二叉树
// 采用栈来遍历
class Solution {
public List<Integer> preorderTraversal(TreeNode root) {
List<Integer> list =new ArrayList<>();
if (root==null){
return list;
}
Stack<TreeNode> stack =new Stack<TreeNode>();
stack.push(root);
while (!stack.isEmpty()){
TreeNode node =stack.pop();
list.add(node.val);
if (node.right!=null){
stack.push(node.right);
}
if (node.left!=null){
stack.push(node.left);
}
//第二次遍历的时候直接弹出来的是左节点,刚刚最后一次添加的左节点
}
return list;
}
}
//可以右前序遍历快速得到后序遍历的代码。
class Solution {
public List<Integer> postorderTraversal(TreeNode root) {
List<Integer> result = new ArrayList<>();
if (root == null){
return result;
}
Stack<TreeNode> stack = new Stack<>();
stack.push(root);
while (!stack.isEmpty()){
TreeNode node = stack.pop();
result.add(node.val);
if (node.left != null){
stack.push(node.left);
}
if (node.right != null){
stack.push(node.right);
}
}
Collections.reverse(result); //这个找出来的是中右左,犯规来就是左右中。达到的是后序遍历。
return result;
}
}
//迭代中序遍历
class Solution {
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> list=new ArrayList<>();
if (root==null){
return list;
}
Stack<TreeNode> stack =new Stack<TreeNode>();
TreeNode cur=root;
while (cur!=null||!stack.isEmpty()){
if (cur!=null){
//如何进行遍历
//从跟节点开始,这样的效率肯定不如递归遍历
stack.push(cur);
cur=cur.left;
//不断的向左进行遍历
}else{
//遇到null了
//弹出栈
cur= stack.pop();
list.add(cur.val);
//左中右的顺序进行遍历
cur=cur.right;
}
}
return list;
}
}
//中序遍历
class Solution {
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> result = new ArrayList<>();
if (root == null){
return result;
}
Stack<TreeNode> stack = new Stack<>();
TreeNode cur = root;
while (cur != null || !stack.isEmpty()){
if (cur != null){
stack.push(cur);
cur = cur.left;
}else{
cur = stack.pop();
result.add(cur.val);
cur = cur.right;
}
}
return result;
}
}
之前不管是递归或者是迭代遍历用栈,都是dfs,现在层序遍历是bfs;
层序遍历 广度优先搜索,相当于图论里面的广度优先搜索
//使用的是队列的结构
class Solution {
// 用bfs实现遍历二叉树,层序遍历二叉树
public List<List<Integer>> result = new ArrayList<List<Integer>>();
public List<List<Integer>> levelOrder(TreeNode root) {
checkFun02(root);
return result;
}
public void checkFun02(TreeNode node) {
if (node == null) return;
Queue<TreeNode> que = new LinkedList<TreeNode>();
que.offer(node);
while (!que.isEmpty()) {
List<Integer> itemList = new ArrayList<Integer>();
int len = que.size();
while (len > 0) {
TreeNode tmpNode = que.poll();
itemList.add(tmpNode.val);
if (tmpNode.left != null) que.offer(tmpNode.left);
if (tmpNode.right != null) que.offer(tmpNode.right);
len--;
}
result.add(itemList);
}
}
}
//bfs
class Solution {
public TreeNode invertTree(TreeNode root) {
//先遍历,然后遍历的时候完成交换
if (root==null) return null;
//用层序去遍历
Queue<TreeNode> queue =new LinkedList<>();
queue.offer(root);
while (!queue.isEmpty()){
//将队列中的第一个取出来
int len=queue.size();
while (len>0){
//每次要取出来
TreeNode poll = queue.poll();
reverse(poll);
if (poll.left!=null){
queue.offer(poll.left);
}
if (poll.right!=null){
queue.offer(poll.right);
}
len--;
}
}
return root;
}
public static void reverse(TreeNode t){ //实现翻转功能
{
TreeNode temp=t.left;
t.left=t.right;
t.right=temp;
}
}
}
//递归方法
//前序遍历 和后序遍历都可以
class Solution {
public TreeNode invertTree(TreeNode root) {
//得考虑一下
if (root==null){
return null;
}
//前序遍历 中左右
invertTree(root.right);
invertTree(root.left);
swap(root);
return root;
}
public void swap(TreeNode root){
TreeNode tmp=root.left;
root.left=root.right;
root.right=tmp;
}
}
class Solution {
public boolean isSymmetric(TreeNode root) {
return compare(root.left,root.right);
//智能使用后序
//不断收集左右孩子的信息 返回给上一个节点
// 左右 中
}
boolean compare(TreeNode left,TreeNode right){
if (left==null &&right!=null){
return false;
}
if (left!=null &&right==null){
return false;
}
if (left == null && right == null) {
return true;
}
if (left.val != right.val) {
return false;
}
boolean outside=compare(left.left,right.right);
//后序遍历
boolean inside=compare(left.right,right.left);
//左节点的左孩子 和右节点的右孩子
return outside&&inside;
}
}
深度和高度是不同的,高度是到叶子节点的距离,深度是到跟节点
求高度是从下网上遍历,后序遍历
求深度 可以是前序遍历 中左右,往前遍历一个就+1,往下遍历再加1;
//后序遍历,求得是高度
class solution {
/**
* 递归法
*/
//后序遍历的过程,左右中的遍历顺序
public int maxDepth(TreeNode root) {
if (root == null) {
return 0;
}
int leftDepth = maxDepth(root.left);
int rightDepth = maxDepth(root.right);
//左节点和右节点孩子最大的高度 然后+1;
return Math.max(leftDepth, rightDepth) + 1;
}
}
//前序遍历
class Solution {
//类似于回溯法
/**
* 递归法(求深度法)
*/
//定义最大深度
int maxnum = 0;
public int maxDepth(TreeNode root) {
ans(root,0);
return maxnum;
}
//递归求解最大深度
void ans(TreeNode tr,int tmp){
if(tr==null) return;
tmp++;
maxnum = maxnum<tmp?tmp:maxnum;
ans(tr.left,tmp);
ans(tr.right,tmp);
tmp--; //需要进行回溯
}
}
//思路最清晰的就是迭代法
class solution {
/**
* 迭代法,使用层序遍历
*/
public int maxDepth(TreeNode root) {
if(root == null) {
return 0;
}
Deque<TreeNode> deque = new LinkedList<>();
deque.offer(root);
int depth = 0;
while (!deque.isEmpty()) {
int size = deque.size();
depth++;
for (int i = 0; i < size; i++) {
TreeNode node = deque.poll();
if (node.left != null) {
deque.offer(node.left);
}
if (node.right != null) {
deque.offer(node.right);
}
}
}
return depth;
}
//bfs搜索
class Solution {
int minDepth(TreeNode root) {
if (root==null){
return 0;
}
//dfs 广度优先搜索
Queue<TreeNode> queue =new LinkedList<>();
queue.offer(root); //将根节点添加进来
int depth=1; //原始的深度
//用队列来模拟功能
while (!queue.isEmpty()){
int size=queue.size();
for (int i = 0; i <size ; i++) {
TreeNode cur = queue.poll();
if (cur.left==null&&cur.right==null){
//左右都为null,说明了是根节点了
return depth;
}
if (cur.left!=null){
queue.offer(cur.left);
}
if (cur.right!=null){
queue.offer(cur.right);
}
}
depth++;
}
return depth;
}
}
//递归方法
//后序求高度
class Solution {
/**
* 递归法,相比求MaxDepth要复杂点
* 因为最小深度是从根节点到最近**叶子节点**的最短路径上的节点数量
*/
public int minDepth(TreeNode root) {
if (root == null) {
return 0;
}
int leftDepth = minDepth(root.left);
int rightDepth = minDepth(root.right);
//如果左边为空,返回右子树的高度+1;
//后序遍历 ,左右中遍历的方式;
if (root.left == null) {
return rightDepth + 1;
}
if (root.right == null) {
return leftDepth + 1;
}
// 左右结点都不为null
return Math.min(leftDepth, rightDepth) + 1;
}
}
//bfs层序遍历
class Solution {
// 迭代法
public int countNodes(TreeNode root) {
if (root == null) return 0;
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
int result = 0;
while (!queue.isEmpty()) {
int size = queue.size();
while (size -- > 0) {
TreeNode cur = queue.poll();
result++;
if (cur.left != null) queue.offer(cur.left);
if (cur.right != null) queue.offer(cur.right);
}
}
return result;
}
}
//dfs
class Solution {
/**
* 针对完全二叉树的解法
*
* 满二叉树的结点数为:2^depth - 1
*/
public int countNodes(TreeNode root) {
if (root == null) return 0;
TreeNode left = root.left;
TreeNode right = root.right;
int leftDepth = 0, rightDepth = 0; // 这里初始为0是有目的的,为了下面求指数方便
while (left != null) { // 求左子树深度
left = left.left;
leftDepth++;
}
while (right != null) { // 求右子树深度
right = right.right;
rightDepth++;
}
if (leftDepth == rightDepth) {
return (2 << leftDepth) - 1; // 注意(2<<1) 相当于2^2,所以leftDepth初始为0
}
//如果不是局部满二叉树的化,就相当于是直接进行递归操作;
return countNodes(root.left) + countNodes(root.right) + 1;
}
}
//正常的递归
class Solution {
// 通用递归解法
//当成普通的二叉树去遍历
public int countNodes(TreeNode root) {
if(root == null) {
return 0;
}
//后序遍历,左右中;
return countNodes(root.left) + countNodes(root.right) + 1;
}
}
class Solution {
//用递归解决
public boolean hasPathSum(TreeNode root, int targetSum) {
//题目要求从根节点到叶子节点的路径,这条路径上所有节点相加等于目标和targetSum;
if (root==null){
return false;
}
//找到最后的叶子节点
if (root.left==null && root.right==null){
return targetSum==root.val;
}
return hasPathSum(root.left,targetSum-root.val) ||hasPathSum(root.right,targetSum-root.val);
//和答题那一题很相似
}
多天农民画
文化赋能 乡村振兴;
素材组需要剪辑一个视频,加分的一点,
网站的投稿;
图的表示方式 邻接矩阵 和临界表
P1294 高手去散步 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
找一个最长的方案
用邻接矩阵存储图;
从一个点开始寻找,不断的找下去
定义一个二维的矩阵,存储两个点之间距离;
for (int i = 1; i <= n; i++) {
x = scanner.nextInt();
y = scanner.nextInt();
c = scanner.nextInt();
Edge[x][y] = c;
Edge[y][x] = c;
}
// dfs代码
import java.util.*;
//所有的变量都是设置的全部变量
class Main {
static int[][] Edge;
static int n, s, an, t, ans;
static boolean[]b;
static int x, y, c;
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
t = scanner.nextInt();
n = scanner.nextInt();
Edge = new int[21][21];
b = new boolean[t+1]; //用布尔数组存放节点
for (int i = 1; i <= n; i++) {
x = scanner.nextInt();
y = scanner.nextInt();
c = scanner.nextInt();
Edge[x][y] = c;
Edge[y][x] = c;
}
for (int i = 1; i <= t; i++) {
//当前路径为ture
b[i] = true;
dfs(i, 0);
an = Math.max(an, ans);
//找了一次之后,需要访问的结果全部都归为false;
Arrays.fill(b, false);
}
System.out.println(an);
}
// static void dfs(int k, int su) {
// ans = Math.max(ans, su);
// for (int i = 1; i <= t; i++) {
//
// if (Edge[k][i] > 0 && b[i]==false) {
//
// b[i] = true;
// dfs(i, su + Edge[k][i]);
// b[i] = false;
// }
// }
// }
static void dfs(int k,int su){
ans=Math.max(ans,su);
for (int i = 0; i < t; i++) {
if (Edge[k][i]>0&&b[i]==false){
b[i]=true;
dfs(i,su+ Edge[k][i]);
b[i]=false; //撤销操作;
}
}
}
}
集合关系
自反 对称 传递 ;
并查集的基本操作,初始化init,查询find,合并unionn;
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xO4TShjb-1691505556711)(C:\Users\周鑫\AppData\Roaming\Typora\typora-user-images\image-20230723155321947.png)]
初始化,用一个数组fa[]来存储每一个元素的父节点
查询 find,寻找i的祖先,
合并操作,将i和j进行一共合并的操作,需要找到id的祖先,在找到j的祖先,将i的祖先指向j的祖先;
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NjjZHPzr-1691505556711)(C:\Users\周鑫\AppData\Roaming\Typora\typora-user-images\image-20230723160241626.png)]
路径压缩,原来是一很长的链表,变为了图;
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KGpuwuOS-1691505556712)(C:\Users\周鑫\AppData\Roaming\Typora\typora-user-images\image-20230723160544138.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uNuzyE6a-1691505556725)(C:\Users\周鑫\AppData\Roaming\Typora\typora-user-images\image-20230723161017940.png)]
//并查集的代码
import java.util.Scanner;
public class BING {
static int fa[];
public static void main(String[] args) {
int n ,m,x,y;
Scanner scanner =new Scanner(System.in);
n=scanner.nextInt();
m=scanner.nextInt();
fa=new int[n+1];
init(n);
for (int i = 1; i <=m; i++) {
x=scanner.nextInt();
y=scanner.nextInt();
union(x,y);
}
int q=scanner.nextInt();
for (int i = 0; i < q; i++) {
if (find(scanner.nextInt())!=find(scanner.nextInt())){
System.out.println("NO");
}else {
System.out.println("YES");
}
}
}
//路径压缩的代码
static void init(int n){
for (int i = 1; i <=n; i++) {
fa[i]=i;
}
}
static int find(int i){
if (fa[i]==i){
return i;
}
else{
fa[i]=find(fa[i]);//将i的父节点加入进来,进行递归
return fa[i];
}
}
//通过这一段代码,将所有的节点都指向了一个父节点
static void union(int i,int j){
int i_fa=find(i);
int j_fa=find(j);
fa[i_fa]=j_fa;
}
}
数据结构就都先了解一下,不用深挖数据结构得内容,直接过来看算法题,难度可能会有点大,但是必须得坚持下去。
回溯算法是自上而下的
回溯算法可以解决的问题 组合问题 分割问题 子集问题 排列问题 棋盘问题 递增子序列
组合问题 就是 给定两个整数n和k,返回1-n中所有可能的k个数的组合
输入 n=4,k=2; 输出 24 34 23 13 12 14;
抽象为树形的样子
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NbqGamGR-1691505556726)(C:\Users\周鑫\AppData\Roaming\Typora\typora-user-images\image-20230711150012046.png)]
回溯的模板内容
void backtracking(参数 n k startIndex){
if(终止条件)
存放结果;
return;
}
for(选择:本层集合中元素(树中节点孩子的数量就是集合的大小)){
处理节点;
backtracking(路径,选择列表); //递归
回溯,撤销处理的结果;
}
组合问题的代码
class Solution {
List<List<Integer>> result = new ArrayList<>(); //数组
LinkedList<Integer> path = new LinkedList<>(); //链表
// 定义了两个集合内容
public List<List<Integer>> combine(int n, int k) {
backTracking(n, k, 1); //索引位置
return result;
}
public void backTracking(int n,int k,int startIndex){
//回溯算法
if (path.size()==k){
result.add(new ArrayList<>(path));
return;
}
for (int i = startIndex; i <=n-(k-path.size())+1 ; i++) {
// k是需要这么多元素,比如需要3个。 path.size是已经添加进去的元素,k-path.size还需要添加的元素
// 最后+1 这个看做实列来
//这边进行剪枝优化,可以加大搜索的效率
path.add(i); //将当前元素加进来
backTracking(n,k,i+1);
path.removeLast(); //撤销操作
}
}
}
//leetcode submit region end(Prohibit modification and deletion)
组合问题|||
找出所有相加之和为n的k个数的组合,组合中只允许含有1-9的正整数,并且每种组合中不存在重复的数字
k=3 n=9 ; 和组合基本问题相同,引入一个sum变量,在回溯之前加上sum的值,回溯之后减去sum的值即可
结束的条件就是path.size==k,且sum==9;
class Solution {
public List<List<Integer>> result =new ArrayList<>();
public LinkedList<Integer> path=new LinkedList<>();
public List<List<Integer>> combinationSum3(int k, int n) {
combine(k,n,1,0);
return result;
}
public void combine(int k,int n,int startIndex,int sum){
if (sum>n){
return;
}
if (path.size()==k){
if (sum==n) result.add(new ArrayList<>(path));
return;
}
for (int i = startIndex; i <=9-(k-path.size())+1; i++) {
//有一剪枝优化的操作
//还需要进行剪枝的操作
path.add(i);
sum+=i;
combine(k,n,i+1,sum);
//回溯操作 不仅需要将集合里面的元素去除吊,还要将sum减去i;
path.removeLast();
//移除最后一个
sum-=i;
}
}
}
电话号码的字母组合
首先需要完成的是映射关系,字母和数字需要完成映射,可以用一个字符数组来解决,字符串操作可以想到的是StringBuilder
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NHkBGaXV-1691505556726)(C:\Users\周鑫\AppData\Roaming\Typora\typora-user-images\image-20230711152057254.png)]
数是这样的,取出abc中的字母,再去def中的字母,
代码
class Solution {
List<String> list =new ArrayList<>();
public List<String> letterCombinations(String digits) {
//需要解决的问题
// 数字和字母如何进行映射
//输入 1 * # 等等异常的情况
if (digits==null||digits.length()==0){
return list;
}
String [] numString={"", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"};
backTracking(digits,numString,0);
return list;
}
StringBuilder temp =new StringBuilder();
//如果这个放在里面的话,每一次回溯都会重新创建一个字符串,不太划算的。
public void backTracking(String digits,String []numString,int num){
//拼接字符串的
if (num==digits.length()){
list.add(temp.toString());
return;
}
String str=numString[digits.charAt(num)-'0']; //找到对应的字母了
//digits.charAt(num)-‘0’ 得到的是对应数组的下标,str代表数字对应的字符串
for (int i = 0; i < str.length() ; i++) {
temp.append(str.charAt(i));
backTracking(digits,numString,num+1);
temp.deleteCharAt(temp.length()-1); //删除最后一个
}
}
}
组合总和问题
1 是 给定一个无重复的元素,candidates中的数字可以无重复的选取使用
组合问题2 是给定一个数组candidates 和一个目标书target,找出candidates中所有可以使数字和为target的组合,数字都只能使用一次。数组里面元素是可以重复的
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bgVuDLVd-1691505556727)(C:\Users\周鑫\AppData\Roaming\Typora\typora-user-images\image-20230711152702638.png)]
先取 2 5 3 ,取2的可以继续取253, 取5的只能取53,不能再取2,如果取2了,求出来的结果会重复出现。
代码如下
需要先进行排序,便于后面进行剪枝操作,如果sum+candidates[i]>target了,说明没有必要再进行递归了,直接结束即可。
class Solution {
List<List<Integer>> result=new ArrayList<>();
List<Integer> path= new LinkedList<>();
public List<List<Integer>> combinationSum(int[] candidates, int target) {
//Arrays.sort(candidates);
backTracking(path,candidates,target,0,0);
return result;
}
public void backTracking(List<Integer>path,int []candidates,int target,int sum,int start){
if (sum>target){
return;
}
if (sum==target){
result.add(new ArrayList<>(path));
return; //结束了
}
for (int i = start; i <candidates.length; i++) {
sum+=candidates[i];
// if (sum>target){
// break;//剪枝优化
// }
path.add(candidates[i]);
backTracking(path,candidates,target,sum,i);//
//组合问题,对应的 start;
path.remove(path.size()-1);
sum-=candidates[i];
}
}
}
组合总和||的思路
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YFzN61PW-1691505556727)(C:\Users\周鑫\AppData\Roaming\Typora\typora-user-images\image-20230711153055250.png)]
需要进行树层去重,对于里面的元素是不能重复取得,使用过的元素不能重复选取,
组合问题抽象为树形结构,两个维度,一个是树枝上使用过,一个是树层上使用过。
树层上面已经取了1之后,就不能再取1了,不然就会造成重复的现象,这个叫做树层去重,用一个used[]数组来记录结果。
子集问题二 和上面组合问题一个是树枝去重 一个是树层去重
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bChxv79X-1691505556728)(C:\Users\周鑫\AppData\Roaming\Typora\typora-user-images\image-20230711153855335.png)]
子集问题,收获结果,要去每一个节点取收获,之前都是在终点进行收获结果。
nums[123]; 求子集不能出现重复的。给出的nums是不含有重复的;
class Solution {
//这两个设置的是全局变量
List<List<Integer>> result =new ArrayList<>();
LinkedList<Integer> path =new LinkedList<>();
public List<List<Integer>> subsets(int[] nums) {
//如果startindex>=nums.size;
// 此时已经到了叶子节点,需要进行return;
dfs(0,nums);
return result;
}
public void dfs(int startIndex,int [] nums){
result.add (new ArrayList<>(path));
if (startIndex>=nums.length){
return;
}
//需要startIndex索引,控制位置。
for (int i = startIndex; i < nums.length; i++)
{
path.add(nums[i]);
dfs(i+1,nums);
//撤销上一次的操作
path.removeLast();
}
}
}
**输入:**nums = [1,2,2] 输出:[[],[1],[1,2],[1,2,2],[2],[2,2]] 给出的元素是有重复元素的,
用一个used数组,防止出现重复,used数组还需要进行排序,是为了让相同的元素埃在一起
用startIndex往后进行取元素,需要进行数层的去重,如果元素相同,前面已经用过了,后面就不能再用了;
树层进行去重
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9WrQ54Lz-1691505556728)(C:\Users\周鑫\AppData\Roaming\Typora\typora-user-images\image-20230719151544419.png)]
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedList;
//leetcode submit region begin(Prohibit modification and deletion)
class Solution {
List<List<Integer>> result =new ArrayList<>();
LinkedList<Integer> path =new LinkedList<>();
boolean [] used;
public List<List<Integer>> subsetsWithDup(int[] nums) {
if (nums.length==0){
result.add(path);
return result;
}
Arrays.sort(nums);
used=new boolean[nums.length];
dfs(nums,0);
return result;
}
public void dfs(int []nums,int startIndex){
result.add(new ArrayList<>(path)); //将每一个节点上面的元素都添加进来
//和之前不同的是,需要收获每一个节点的元素
//结束条件还没有确定呢
if (startIndex>=nums.length){
//到结尾了
return;
}
for (int i = startIndex; i < nums.length; i++) {
//需要进行数层的去重
if (i>0&&nums[i]==nums[i-1]&&used[i-1]==false){
//刚刚出现了失误的情况
continue;//直接跳过;
}
path.add(nums[i]);
used[i]=true;
dfs(nums,i+1);
path.removeLast();
used[i]=false;
}
}
}
全排列问题
给定一个没有重复的数字序列,返回所有可能的排列
输入: [1,2,3]
输出: [ [1,2,3], [1,3,2], [2,1,3], [2,3,1], [3,1,2], [3,2,1] ]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-I9iV2BWY-1691505556729)(C:\Users\周鑫\AppData\Roaming\Typora\typora-user-images\image-20230711154624917.png)]
全排列问题 2,给定一个可能包含重复数字的序列nums,返回所有不重复的全排列
应该是树层进行去重
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-f2rbAk07-1691505556729)(C:\Users\周鑫\AppData\Roaming\Typora\typora-user-images\image-20230711155156611.png)]
组合问题和排列问题都是在树形结构的叶子节点收集结果,子集问题是取树上所有节点的结果
深度搜索 dfs 广度搜索 bfs 回溯 剪枝
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zyqKPB7M-1691505556729)(C:\Users\周鑫\AppData\Roaming\Typora\typora-user-images\image-20230711164315303.png)]
搜索bfs
bfs 常见的场景,问题的本质是让你在一幅图中找到从start到target的最近距离
要说框架的话,我们先举例一下 BFS 出现的常见场景好吧,问题的本质就是让你在一幅「图」中找到从起点 start 到终点 target 的最近距离,这个例子听起来很枯燥,但是 BFS 算法问题其实都是在干这个事儿,把枯燥的本质搞清楚了,再去欣赏各种问题的包装才能胸有成竹嘛。
这个广义的描述可以有各种变体,比如走迷宫,有的格子是围墙不能走,从起点到终点的最短距离是多少?如果这个迷宫带「传送门」可以瞬间传送呢?
再比如说两个单词,要求你通过某些替换,把其中一个变成另一个,每次只能替换一个字符,最少要替换几次?
再比如说连连看游戏,两个方块消除的条件不仅仅是图案相同,还得保证两个方块之间的最短连线不能多于两个拐点。你玩连连看,点击两个坐标,游戏是如何判断它俩的最短连线有几个拐点的?
再比如……
净整些花里胡哨的,本质上看这些问题都没啥区别,就是一幅「图」,让你从一个起点,走到终点,问最短路径。这就是 BFS 的本质,框架搞清楚了直接默写就好。
bfs的框架
// 计算从起点 start 到终点 target 的最近距离
int BFS(Node start, Node target) {
Queue<Node> q; // 核心数据结构
Set<Node> visited; // 避免走回头路
q.offer(start); // 将起点加入队列
visited.add(start);
while (q not empty) {
int sz = q.size();
/* 将当前队列中的所有节点向四周扩散 */
for (int i = 0; i < sz; i++) {
Node cur = q.poll();
/* 划重点:这里判断是否到达终点 */
if (cur is target)
return step;
/* 将 cur 的相邻节点加入队列 */
for (Node x : cur.adj()) {
if (x not in visited) {
q.offer(x);
visited.add(x);
}
}
}
}
// 如果走到这里,说明在图中没有找到目标节点
}
岛屿问题的核心是用bfs/dfs算法遍历二维数组;
岛屿问题的小模版
// 方向数组,分别代表上、下、左、右
int[][] dirs = new int[][]{{-1,0}, {1,0}, {0,-1}, {0,1}};
void dfs(int[][] grid, int i, int j, boolean[][] visited) {
int m = grid.length, n = grid[0].length;
if (i < 0 || j < 0 || i >= m || j >= n) {
// 超出索引边界
return;
}
if (visited[i][j]) {
// 已遍历过 (i, j)
return;
}
// 进入节点 (i, j)
visited[i][j] = true;
// 递归遍历上下左右的节点
for (int[] d : dirs) {
int next_i = i + d[0];
int next_j = j + d[1];
dfs(grid, next_i, next_j, visited);
}
// 离开节点 (i, j)
}
class Solution {
// 主函数,计算岛屿数量
int numIslands(char[][] grid) {
int res = 0;
int m = grid.length, n = grid[0].length;
// 遍历 grid
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
//如果开始遇到一个岛屿了,开始dfs淹没岛屿
if (grid[i][j] == '1') {
// 每发现一个岛屿,岛屿数量加一
res++;
// 然后使用 DFS 将岛屿淹了
dfs(grid, i, j);
}
}
}
return res;
}
// 从 (i, j) 开始,将与之相邻的陆地都变成海水
void dfs(char[][] grid, int i, int j) {
int m = grid.length, n = grid[0].length;
if (i < 0 || j < 0 || i >= m || j >= n) {
// 超出索引边界
return;
}
if (grid[i][j] == '0') {
// 已经是海水了
return;
}
// 将 (i, j) 变成海水
grid[i][j] = '0';
// 淹没上下左右的陆地
//四个方向进行运动;
dfs(grid, i + 1, j);
dfs(grid, i, j + 1);
dfs(grid, i - 1, j);
dfs(grid, i, j - 1);
}
}
类似于求岛屿的数量问题,只不过在每一次遍历的时候需要更新当时的最大面积即可;
放入的元素是有序的,要么单调递减,要么单调递增;
递增:从栈底到栈顶的数据是从小到大的
递减:从栈底到栈顶的数据是从大到小的;
单调栈的伪代码
stack<int> st;
//此处一般需要给数组最后添加结束标志符,具体下面例题会有详细讲解
for (遍历这个数组)
{
if (栈空 || 栈顶元素大于等于当前比较元素)
{
入栈;
}
else
{
while (栈不为空 && 栈顶元素小于当前元素)
{
栈顶元素出栈;
更新结果;
}
当前数据入栈;
}
}
数学逻辑问题
leetcode 292
leetcode 319
leetcode 877
877
class Solution {
public int bulbSwitch(int n) {
return (int)Math.sqrt(n);
//刚开始都是关闭的,如果某一栈灯是最后点亮的,那么他一定要被按奇数次开关
//比如有六栈灯,需要进行六次操作,
// 对于第六个灯会被按多少次呢?
// 1 2 3 6都会被按
// 求16的平方跟,因为最后会有4栈灯亮着
// 第1 4 9 16;
//一般情况下一个数的因子都是成对出现的
//比如3=1*3
//只有n的平方是只出现了一次的 4=2*2,那么这样就相当于操作了奇数次
//最后就一定会亮着了,需要对一个数求它的平方根;
}
}
局部最优推导全局最优
455 分发饼干
//打印十进制的二进制位数
public class 位运算 {
public static void main(String[] args) {
Scanner sc= new Scanner(System.in);
solve(sc.nextInt());
}
public static void solve(int num){
//1->0000 0000 0000 0000 0000 0000 0000 0001
for (int i = 31; i >=0 ; i--) {
//将十进制转为二进制;
System.out.print((num&(1<