代码随想录 刷题记录-13 回溯(2)组合问题

news2024/11/29 2:35:39

在这里涉及到的回溯中的抽象树,都是“选哪一个元素”的思想。

1.第77题. 组合

回溯法就用递归来解决嵌套层数的问题

把组合问题抽象为如下树形结构:

可以看出这棵树,一开始集合是 1,2,3,4, 从左向右取数,取过的数,不再重复取。

第一次取1,集合变为2,3,4 ,因为k为2,我们只需要再取一个数就可以了,分别取2,3,4,得到集合[1,2] [1,3] [1,4],以此类推。

每次从集合中选取元素,可选择的范围随着选择的进行而收缩,调整可选择的范围

图中可以发现n相当于树的宽度,k相当于树的深度

那么如何在这个树上遍历,然后收集到我们要的结果集呢?

图中每次搜索到了叶子节点,我们就找到了一个结果

相当于只需要把达到叶子节点的结果收集起来,就可以求得 n个数中k个数的组合集合。

回溯法三部曲

  • 递归函数的返回值以及参数

在这里要定义两个全局变量,一个用来存放符合条件单一结果,一个用来存放符合条件结果的集合。

函数里一定有两个参数,既然是集合n里面取k个数,那么n和k是两个int型的参数。

然后还需要一个参数,为int型变量startIndex,这个参数用来记录本层递归的中,集合从哪里开始遍历,startIndex 是为了防止出现重复的组合

那么整体代码如下:

vector<vector<int>> result; // 存放符合条件结果的集合
vector<int> path; // 用来存放符合条件单一结果
void backtracking(int n, int k, int startIndex)
  • 回溯函数终止条件

什么时候到达所谓的叶子节点了呢?

path这个数组的大小如果达到k,说明我们找到了一个子集大小为k的组合了,在图中path存的就是根节点到叶子节点的路径。

此时用result二维数组,把path保存起来,并终止本层递归。

所以终止条件代码如下:

if (path.size() == k) {
    result.push_back(path);
    return;
}
  • 单层搜索的过程

回溯法的搜索过程就是一个树型结构的遍历过程,在如下图中,可以看出for循环用来横向遍历,递归的过程是纵向遍历

在该层要对每一个可能的方向进行试探,并作现场恢复。

代码如下:

for (int i = startIndex; i <= n; i++) { // 控制树的横向遍历
    path.push_back(i); // 处理节点
    backtracking(n, k, i + 1); // 递归:控制树的纵向遍历,注意下一层搜索要从i+1开始
    path.pop_back(); // 回溯,撤销处理的节点
}

可以看出backtracking(递归函数)通过不断调用自己一直往深处遍历,总会遇到叶子节点,遇到了叶子节点就要返回。

完整代码如下:

class Solution {
    List<Integer> path;
    List<List<Integer>> res;
    public List<List<Integer>> combine(int n, int k) {
        path = new ArrayList<>();
        res = new ArrayList<>();

        dfs(n,k,1);
        return res;
    }

    public void dfs(int n, int k, int startIndex){
        if(path.size()==k){
            res.add(new ArrayList<>(path));
            return;
        }
        for(int i=startIndex ; i <= n ; i++){
            path.add(i);
            dfs(n,k,i+1);
            path.remove(path.size()-1);
        }
    }
}

回顾框架:

void backtracking(参数) {
    if (终止条件) {
        存放结果;
        return;
    }

    for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
        处理节点;
        backtracking(路径,选择列表); // 递归
        回溯,撤销处理结果
    }

剪枝优化

我们说过,回溯法虽然是暴力搜索,但也有时候可以有点剪枝优化一下的。

在遍历的过程中有如下代码:

for (int i = startIndex; i <= n; i++) {
    path.push_back(i);
    backtracking(n, k, i + 1);
    path.pop_back();
}

举一个例子,n = 4,k = 4的话,那么第一层for循环的时候,从元素2开始的遍历都没有意义了。 在第二层for循环,从元素3开始的遍历都没有意义了。

可以剪枝的地方就在递归中每一层的for循环所选择的起始位置

如果for循环选择的起始位置之后的元素个数 已经不足 我们需要的元素个数了,那么就没有必要搜索了

接下来看一下优化过程如下:

  1. 已经选择的元素个数:path.size();

  2. 还需要的元素个数为: k - path.size();

  3. 在集合n中至多要从该起始位置 : n - (k - path.size()) + 1,开始遍历

所以优化之后的for循环是:

for (int i = startIndex; i <= n - (k - path.size()) + 1; i++) // i为本次搜索的起始位置

优化后整体代码如下:

class Solution {
    List<Integer> path;
    List<List<Integer>> res;
    public List<List<Integer>> combine(int n, int k) {
        path = new ArrayList<>();
        res = new ArrayList<>();

        dfs(n,k,1);
        return res;
    }

    public void dfs(int n, int k, int startIndex){
        if(path.size()==k){
            res.add(new ArrayList<>(path));
            return;
        }
        for(int i=startIndex ; i <= n-(k- path.size())+1 ; i++){
            path.add(i);
            dfs(n,k,i+1);
            path.remove(path.size()-1);
        }
    }
}

2.216.组合总和III

本题就是在[1,2,3,4,5,6,7,8,9]这个集合中找到和为n的k个数的组合。

相对于77. 组合 (opens new window),无非就是多了一个限制,本题是要找到和为n的k个数的组合,而整个集合已经是固定的了[1,...,9]。

回溯三部曲

  • 确定递归函数参数

和77. 组合 (opens new window)一样,依然需要一维数组path来存放符合条件的结果,二维数组result来存放结果集。

这里我依然定义path 和 result为全局变量。

vector<vector<int>> result; // 存放结果集
vector<int> path; // 符合条件的结果

接下来还需要如下参数:

  • targetSum(int)目标和,也就是题目中的n。
  • k(int)就是题目中要求k个数的集合。
  • sum(int)为已经收集的元素的总和,也就是path里元素的总和。
  • startIndex(int)为下一层for循环搜索的起始位置。

代码如下:

vector<vector<int>> result;
vector<int> path;
void backtracking(int targetSum, int k, int sum, int startIndex)

其实这里sum这个参数也可以省略,每次targetSum减去选取的元素数值,然后判断如果targetSum为0了,说明收集到符合条件的结果了,这里为了直观便于理解,还是加一个sum参数。

  • 确定终止条件

k其实就已经限制树的深度,因为就取k个元素,树再往下深了没有意义。

终止代码如下:

if (path.size() == k) {
    if (sum == targetSum) result.push_back(path);
    return; // 如果path.size() == k 但sum != targetSum 直接返回
}
  • 单层搜索过程

本题和77. 组合 (opens new window)区别之一就是集合固定的就是9个数[1,...,9],所以for循环固定i<=9

处理过程就是 path收集每次选取的元素,相当于树型结构里的边,sum来统计path里元素的总和。

代码如下:

for (int i = startIndex; i <= 9; i++) {
    sum += i;
    path.push_back(i);
    backtracking(targetSum, k, sum, i + 1); // 注意i+1调整startIndex
    sum -= i; // 回溯
    path.pop_back(); // 回溯
}

最后代码如下:

class Solution {
    List<Integer> path;
    List<List<Integer>> res;
    int sum;
    public List<List<Integer>> combinationSum3(int k, int n) {
        path = new ArrayList<>();
        res = new ArrayList<>();
        dfs(k,n,1);
        return res;
    }
    
    public void dfs(int k , int n, int startIndex){
        if(sum > n) return ; 
        if(path.size() == k ){
            if(sum == n){
                res.add(new ArrayList<>(path));
            }
            return ; 
        }
        for(int i=startIndex ; i<=9 - (k - path.size()) + 1 ;i++){  //  9 - (k - path.size()) 为剪枝操作
            sum+=i;
            path.add(i);
            dfs(k,n,i+1);
            path.remove(path.size()-1);
            sum-=i;
        }
    }
    
   

}

3.17.电话号码的字母组合

思路

组合问题,嵌套for循环的思路 ,考虑回溯法。

1.参数 : 字符串、index

2.返回条件 index == 字符串.size()

3.层逻辑 遍历 字符串.getIndex(index) 下的所有字母

数字和字母如何映射

使用 map 或定义一个二维数组

List<String> res;
    StringBuilder path;
    String[] mapping = {"","","abc","def","ghi","jkl","mno","pqrs","tuv","wxyz"};
    public List<String> letterCombinations(String digits) {
        res = new ArrayList<>();
        path = new StringBuilder();
        if(digits.isEmpty()) return res;
        dfs(digits,0);
        return  res;
    }

    public void dfs(String digits, int index){
        if(index == digits.length()){
            res.add(new String(path));
            return ;
        }
        int digit = digits.charAt(index) - '0';
        for(char c : mapping[digit].toCharArray()){
            path.append(c);
            dfs(digits,index+1);
            path.deleteCharAt(path.length()-1);
        }
    }

总结

回溯法抽象为树形结构后,其遍历过程就是:for循环横向遍历,递归纵向遍历,回溯不断调整结果集

4.39. 组合总和

思路

本题和77.组合 (opens new window),216.组合总和III (opens new window)的区别是:本题没有数量要求,可以无限重复,但是有总和的限制,所以间接的也是有个数的限制。

注意图中叶子节点的返回条件,因为本题没有组合数量要求,仅仅是总和的限制,所以递归没有层数的限制,只要选取的元素总和超过target,就返回!

而在77.组合 (opens new window)和216.组合总和III (opens new window)中都可以知道要递归K层,因为要取k个元素的组合。

回溯三部曲

  • 递归函数参数

这里依然是定义两个全局变量,二维数组result存放结果集,数组path存放符合条件的结果。(这两个变量可以作为函数参数传入)

首先是题目中给出的参数,集合candidates, 和目标值target。

此外我还定义了int型的sum变量来统计单一结果path里的总和,其实这个sum也可以不用,用target做相应的减法就可以了,最后如何target==0就说明找到符合的结果了,但为了代码逻辑清晰,我依然用了sum。

本题还需要startIndex来控制for循环的起始位置,对于组合问题,什么时候需要startIndex呢?

我举过例子,如果是一个集合来求组合的话,就需要startIndex,例如:77.组合 (opens new window),216.组合总和III (opens new window)。

如果是多个集合取组合,各个集合之间相互不影响,那么就不用startIndex,例如:17.电话号码的字母组合(opens new window)

注意以上只是说求组合的情况,如果是排列问题,又是另一套分析的套路,后面在讲解排列的时候会重点介绍

代码如下:

vector<vector<int>> result;
vector<int> path;
void backtracking(vector<int>& candidates, int target, int sum, int startIndex)
  • 递归终止条件
if (sum > target) {
    return;
}
if (sum == target) {
    result.push_back(path);
    return;
}
  • 单层搜索的逻辑

单层for循环依然是从startIndex开始,搜索candidates集合。

本题元素为可重复选取的

for (int i = startIndex; i < candidates.size(); i++) {
    sum += candidates[i];
    path.push_back(candidates[i]);
    backtracking(candidates, target, sum, i); // 关键点:不用i+1了,表示可以重复读取当前的数
    sum -= candidates[i];   // 回溯
    path.pop_back();        // 回溯
}

完整代码如下:

class Solution {
    List<List<Integer>> res;
    List<Integer> path;
    int sum = 0;
    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        Arrays.sort(candidates);
        res = new ArrayList<>();
        path  = new ArrayList<>();
        dfs(candidates,target,0);
        return res;
    }

    public void dfs(int[] candidates,int target,int startIndex){
        if(sum == target){
            res.add(new ArrayList<>(path));
        }
        // if(sum > target) return ; 也可以这里剪枝
        for(int i = startIndex ; i < candidates.length && sum + candidates[i] <= target ; i++){//剪枝操作
            sum += candidates[i];
            path.add(candidates[i]);
            dfs(candidates,target,i);
            path.remove(path.size()-1);
            sum-= candidates[i];
        }
    }
}

5.40.组合总和II

这道题目和39.组合总和 (opens new window)如下区别:

  1. 本题candidates 中的每个数字在每个组合中只能使用一次。
  2. 本题数组candidates的元素是有重复的,而39.组合总和 (opens new window)是无重复元素的数组candidates

如果把所有组合求出来,再用set或者map去重,则很容易超时。

所以要在搜索的过程中就去掉重复组合。

所谓去重,其实就是使用过的元素不能重复选取。 

组合问题可以抽象为树形结构,那么“使用过”在这个树形结构上是有两个维度的,一个维度是同一树枝上使用过,一个维度是同一树层上使用过。

题目要求元素在同一个组合内是可以重复的,怎么重复都没事,但两个组合不能相同。

所以我们要去重的是同一树层上的“使用过”,同一树枝上的都是一个组合里的元素,不用去重

为了理解去重我们来举一个例子,candidates = [1, 1, 2], target = 3,(方便起见candidates已经排序了)

强调一下,树层去重的话,需要对数组排序!

选择过程树形结构如图所示:

个人理解,这里为什么要对树层去重:在数组排序后,假若元素 a1 已经在前面出现过了,现在又在同一树层出现了元素 a2 ,且 a2 == a1, 所有从a2出发的可能的情况,都可以从a1出发 经过 若干 选或不选得到。同时为了避免a2和a1之间出现其他元素,需要a2和a1之间只有可能有和它们相等的元素,这也是对树层去重要先排序的原因。

如果前面a1 used == true 的情况下,再出现a2,则是在树枝上,不去重。

回溯三部曲

  • 递归函数参数

与39.组合总和 (opens new window)套路相同,此题还需要加一个bool型数组used,用来记录同一树枝上的元素是否使用过。

这个集合去重的重任就是used来完成的。

代码如下:

vector<vector<int>> result; // 存放组合集合
vector<int> path;           // 符合条件的组合
void backtracking(vector<int>& candidates, int target, int sum, int startIndex, vector<bool>& used) {
  • 递归终止条件

与39.组合总和 (opens new window)相同,终止条件为 sum > target 和 sum == target

代码如下:

if (sum > target) { // 这个条件其实可以省略
    return;
}
if (sum == target) {
    result.push_back(path);
    return;
}

sum > target 这个条件其实可以省略,因为在递归单层遍历的时候,会有剪枝的操作,下面会介绍到。

  • 单层搜索的逻辑

这里与39.组合总和 (opens new window)最大的不同就是要去重了。

前面我们提到:要去重的是“同一树层上的使用过”,如何判断同一树层上元素(相同的元素)是否使用过了呢。

如果candidates[i] == candidates[i - 1] 并且 used[i - 1] == false,就说明:前一个树枝,使用了candidates[i - 1],也就是说同一树层使用过candidates[i - 1]

此时for循环里就应该做continue的操作。

我在图中将used的变化用橘黄色标注上,可以看出在candidates[i] == candidates[i - 1]相同的情况下:

  • used[i - 1] == true,说明同一树枝candidates[i - 1]使用过
  • used[i - 1] == false,说明同一树层candidates[i - 1]使用过

可能有的录友想,为什么 used[i - 1] == false 就是同一树层呢,因为同一树层,used[i - 1] == false 才能表示,当前取的 candidates[i] 是从 candidates[i - 1] 回溯而来的。

class Solution {
    List<List<Integer>> res = new ArrayList<>();
    List<Integer> path = new ArrayList<>();
    boolean[] used;
    int sum =0;

    public List<List<Integer>> combinationSum2(int[] candidates, int target) {
        Arrays.sort(candidates);
        used = new boolean[candidates.length];
        dfs(candidates,target,sum,0);
        return res;
    }

    public void dfs(int[] candidates, int target, int sum , int startIndex){
        if(sum == target){
            res.add(new ArrayList<>(path));
            return;
        }
        for(int i=startIndex ; i < candidates.length && sum <= target; i++){
            if( i != 0 && candidates[i] == candidates[i-1] && used[i-1] == false) continue;
            used[i] = true;
            path.add(candidates[i]);
            sum += candidates[i];
            dfs(candidates,target,sum,i+1);
            sum-= candidates[i];
            path.remove(path.size()-1);
            used[i]=false;
        }
    }
}

补充

这里直接用startIndex来去重也是可以的, 就不用used数组了。

chatGPT的建议:used 数组的使用问题used 数组的使用不当。used 数组通常用于防止在同一层的递归中使用相同的元素,但这里其实不需要 used 数组,因为我们只要在遍历数组时跳过前一个未被选中的相同元素即可。

class Solution {
    List<List<Integer>> res = new ArrayList<>();
    List<Integer> path = new ArrayList<>();
    int sum =0;

    public List<List<Integer>> combinationSum2(int[] candidates, int target) {
        Arrays.sort(candidates);
        dfs(candidates,target,sum,0);
        return res;
    }

    public void dfs(int[] candidates, int target, int sum , int startIndex){
        if(sum == target){
            res.add(new ArrayList<>(path));
            return;
        }
        for(int i=startIndex ; i < candidates.length && sum <= target; i++){
            if( i > startIndex && candidates[i] == candidates[i-1] ) continue;
            path.add(candidates[i]);
            sum += candidates[i];
            dfs(candidates,target,sum,i+1);
            sum-= candidates[i];
            path.remove(path.size()-1);
        }
    }
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2062256.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

探索Python交互式编程的新境界:Python-prompt-toolkit的魔法

文章目录 探索Python交互式编程的新境界&#xff1a;Python-prompt-toolkit的魔法背景&#xff1a;为何选择Python-prompt-toolkit&#xff1f;Python-prompt-toolkit是什么&#xff1f;如何安装Python-prompt-toolkit&#xff1f;简单使用&#xff1a;Python-prompt-toolkit的…

MongoDB Compass初体验

入坑Mongodb也好多年了&#xff0c;客户端一直都是使用的Robomongo&#xff0c;后改名为Robo 3T了&#xff0c;现在又改名为Studio 3T&#xff0c;还分了免费版和付费版。 最近换了新电脑&#xff0c;需要重新安装Mongodb的客户端&#xff0c;加上公司对安装软件的各种限制&…

国内首颗ASIL D级高端旗舰级R52+内核车规MCU发布,中国汽车芯片强势崛起

8月21日&#xff0c;在2024紫光同芯合作伙伴大会上&#xff0c;紫光同芯正式发布第二代THA6系列高端旗舰级新品THA6412。该芯片在安全性、可靠性、算力、实时性等方面全方位升级&#xff0c;是继今年7月紫光同芯发布THA6206芯片后&#xff0c;又一款通过ASIL D产品认证的旗舰级…

GDB的基本使用(1)

我有话说 因为时间和精力原因&#xff0c;本文写的虎头蛇尾了&#xff0c;除了启动调试与程序执行以外只有少量截图演示&#xff0c;只是简单的说明。如果有需要可以联系我&#xff0c;我有时间的话会把演示补上&#xff0c;谢谢理解。 启动调试与程序执行 启动调试并传递参数…

小白快速上手 SRC漏洞挖掘科普攻略!零基础入门到精通,收藏这一篇就够了

前言 随着网络安全的快速发展&#xff0c;黑客攻击的手段也越来越多样化&#xff0c;因此SRC漏洞挖掘作为一种新的网络安全技术&#xff0c;也在不断发展和完善。那么&#xff0c;作为一个网安小白如果想要入门SRC漏洞挖掘&#xff0c;需要掌握哪些知识呢&#xff1f;以下是本…

css属性 clip-path切割多边形polygon

如果我们要把一个矩形切割成如图所示&#xff0c;可以使用 clip-path来做切割 clip-path&#xff1a;polygon&#xff08;x1 y1&#xff0c;x2 y2&#xff09;里面的参数是切割后每个顶点的坐标&#xff0c;坐标的原点是div的左上角&#xff0c;每个顶点的坐标如下&#xff1a…

海山数据库(He3DB)源码详解:事务源码执行过程

海山数据库(He3DB)源码详解&#xff1a;事务源码执行过程 本文介绍了He3DB数据库在DDL和DML操作过程中&#xff0c;两种事务在底层源码执行流程细节。 操作过程 1、启动数据库并进入GDB 首先&#xff0c;在终端A启动一个测试用的数据库test&#xff0c;并查看当前测试用的t…

【Qt】输入类控件QLineEdit

目录 输入类控件QLineEdit 例子&#xff1a;录入个人信息 例子&#xff1a;使用正则表达式验证输入框的数据 例子&#xff1a;验证俩次输入密码一致 例子&#xff1a;切换显示代码 输入类控件QLineEdit QLineEdit 用来表示单行输入框&#xff0c;可以输入一段文本&#xf…

ubuntu18.04更改系统语言及换源的方法步骤

ubuntu的虚拟机不知道第几次被玩崩溃了&#xff0c;无奈只好重装&#xff0c;这里记录下更改语言和换源的操作步骤。 一、更改系统语言为简体中文 1&#xff0c;点击虚拟机右上角的开始按钮&#xff0c;选择设置。 2&#xff0c;在左侧选项中选择Region & Language,再选择…

day28-测试自动化之Requests库的发送请求、响应内容、Cookie和session

目录 一、发送请求 1.1.GET请求 1).作用 2).步骤 3).响应对象 4).代码 5).带参代码 1.2.POST请求 1).作用 2).应用 3).参数 4).代码&#xff08;地址已失效&#xff09; 5).扩展 1.3.PUT请求 1).作用 2).应用 3).参数 4).响应 5).代码&#xff08;地址已失效&#xff09; 1.4.D…

DC-2综合渗透,rbash逃逸,git提权,wordpress靶场渗透教程

前言 博客主页&#xff1a;【h0ck1r丶羽】的公众号~~ ​ 本文主要讲解了渗透测试中的完整渗透测试流程&#xff0c;主要介绍了【wpscan】、【cewl】、【rbash逃逸】的使用技巧&#xff0c;靶场为vulnhub的机器大家可以自行下载&#xff0c;如果文章哪有不对&#xff0c;还请师…

大模型在应用开发安全左移实践

1.应用开发安全左移势在必行 近年来&#xff0c;应用系统被入侵或敏感信息泄漏类的安全事件时有发生&#xff0c;大部分安全事件的根本原因是应用软件设计或实现中存在安全漏洞。由于软件安全性问题导致各种信息泄密、信息被篡改、网络服务中断的事件频发&#xff0c;给企业和…

如何基于 langchain与 LLM 构建自己的知识库系统

如何基于 langchain与 LLM 构建自己的知识库系统 前些时候字节上了自己的扣子&#xff0c;用来构建我们自己的 agent AI 实战&#xff1a;手把手教你使用「扣子/coze」来搭建个人blog知识库 - 掘金 (juejin.cn)[1] 受到启发&#xff0c;想在本地或者自己的服务器上面搭建一个知…

机器学习第五十二周周报 Distribution Shift of GNN

文章目录 week52 Distribution Shift of GNN摘要Abstract一、文献阅读1. 题目2. Abstract3. 预测标准3.1 问题提出3.2 图结构3.3 分布转移 4. 文献解读4.1 Introduction4.2 创新点4.3 实验过程 5. 结论 二、若依框架1. 框架概述2. 核心功能 3. 技术栈4. 框架特点优缺点小结参考…

写作小白2024年逆袭,AI写作工具top4的正确打开方式

以前&#xff0c;人们总觉得写文章、编故事是人特有的本事&#xff0c;机器肯定搞不定。但现在&#xff0c;AI越来越牛&#xff0c;这些AI写作工具&#xff0c;用上了深度学习、自然语言处理这些高科技&#xff0c;能模仿人的思路来写东西。它们不仅能帮人写&#xff0c;有时候…

AI大模型日报#0822:OpenAI推GPT-4o微调服务、混元大模型负责人专访

导读&#xff1a;AI大模型日报&#xff0c;爬虫LLM自动生成&#xff0c;一文览尽每日AI大模型要点资讯&#xff01;目前采用“文心一言”&#xff08;ERNIE-4.0-8K-latest&#xff09;、“智谱AI”&#xff08;glm-4-0520&#xff09;生成了今日要点以及每条资讯的摘要。欢迎阅…

APO 集成生态exporter一键完成指标采集

Metrics 作为可观测性领域的三大支柱之一&#xff0c;Metrics数据采集显得尤为重要。传统的prometheus工具采集指标&#xff0c;需要指定路径抓取&#xff0c;当指标越来越多配置会显得复杂。同时prometheus只能采集指定的指标&#xff0c;当用户需要节点系统相关、中间件等指标…

虚幻5|制作一个木桩,含血量及伤害数字

一&#xff0c;基础设置 1.创建Actor蓝图类 2.编辑胶囊体和网格体的碰撞预设 3.打开敌人的角色蓝图&#xff0c;编辑飙血特效 二&#xff0c;创建敌人血量的ui&#xff0c;命名为敌人血量&#xff0c;如下 1. 2&#xff0c;打开后&#xff0c;添加一个画布画板和进度条&#…

为什么有了session和cookie还要使用JWT

session和cookie 为什么要使用session和cookie,他的流程是怎么样的 因为浏览器是无状态的,相当于每一次访问都是一次全新的访问,我们一般是登录来进行校验获取他的全部信息,所以会出现这个情况 这样当然也可以使用,但是很繁琐,并且影响性能,所以出现了session和cookie,他会生…

MDN边看边记

css css应用于文档的三种方法&#xff1a;使用外部样式表、使用内部样式表和使用内联样式。 css的层叠&#xff08;cascade&#xff09;和优先级&#xff08;specificity&#xff09;&#xff1a; 层叠&#xff1a; 在css中&#xff0c;顺序很重要&#xff0c;当应用两条同级…