回溯法复习(总结篇)

news2024/12/23 6:38:04

根据课本上的学习要点梳理,“通用解题法”,可以系统的搜索一个问题的所有解、任一解,他是一个既带有系统性(暴力遍历)又带有跳跃性(剪枝)的搜索算法。

理解回溯法和深度优先搜索策略

 回溯的本质就是递归,就是一种搜索方式,在问题的解空间树中,按深度优先搜索策略,从根节点出发搜索解空间树,算法搜索到解空间的一个节点时,先判断该节点是否包含问题的解,如果肯定不包含,跳过该节点的子树(剪枝),逐层向其祖先节点回溯;否则进入该子树,继续按深度优先搜索下一层节点(因此我们必须明确每一层节点的含义)。

回溯法求问题的一个解时,只要搜索到一个解,就可以结束了。

回溯法求问题的全部解时,需要回溯到根,并且根节点的所有子树都要被搜索到才能结束。

回溯法解决的问题都可以抽象为树形结构,是的,我指的是所有回溯法的问题都可以抽象为树形结构!因为回溯法解决的都是在集合中递归查找子集,集合的大小就构成了树的宽度,递归的深度,都构成的树的深度。递归就要有终止条件,所以必然是一棵高度有限的树(N叉树)。

回溯法的效率如何呢?他虽然很难,但并不是什么高效的算法,回溯的本质是穷举,穷举所有的可能,然后选出我们想要的答案。如果想要回溯法更加高效一些,那就根据题目的意思加一些剪枝操作,但还是该不了穷举的本质。虽然他效率不高,但很多问题不用回溯法根本写不出来。

回溯法,一般可以解决如下几种问题:

  • 组合问题:N个数里面按一定规则找出k个数的集合
  • 切割问题:一个字符串按一定规则有几种切割方式
  • 子集问题:一个N个数的集合里有多少符合条件的子集
  • 排列问题:N个数按一定规则全排列,有几种排列方式
  • 棋盘问题:N皇后,解数独等等

子集问题和排列问题如何区分? 组合不强调元素的顺序,而排列会强调元素的顺序。

掌握用回溯法解题的算法框架 

 1)递归回溯

2)迭代回溯

3)子集树算法框架

4)排列树算法框架

递归回溯三部曲

  • 回溯函数模板返回值以及参数
  • 回溯函数的终止条件
  • 回溯搜索的单层遍历逻辑

1、void backtrack(参数)回溯算法中函数返回值一般为void。再来看一下参数,因为回溯算法需要的参数可不像二叉树递归的时候那么容易一次性确定下来,所以一般是先写逻辑,然后需要什么参数,就填什么参数。

2、遍历树形结构需要终止条件,达到了终止条件,一般就是搜索到叶子节点了,也就找到了一个满足条件的答案,把这个答案存放起来,并结束本层递归

终止条件的伪代码如下:

if(终止条件){
    存放结果;
    return;
}

3、回溯搜索的遍历过程

回溯法一般书在集合中递归搜索,集合的大小构成了树的宽度,递归的深度构成了树的深度

(集合的大小和孩子的数量是相等的)for循环可以理解是横向遍历,backtracking(递归)就是纵向遍历,这样就把这棵树全遍历完了,一般来说,搜索叶子节点就是找的其中一个结果了。

伪代码如下:

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

 整体的代码模板可以写成这样:

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

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

递归回溯的代码框架

 迭代回溯代码框架

 子集树与排列树

子集树的典型代表就是子集问题、组合问题(0-1背包、最优装载、最大团、符号三角形--有点坑、n后问题、图的m着色问题)。这类子集树通常有2^n个节点。

排列树的典型代表就是排列问题(旅行售货员,批处理作业调度、圆排列、电路板排列),这类排列树通常有n!个节点。

子集树的一般算法可以描述为:

 排列树的一般算法可以描述为:

 范例学习回溯法的设计策略

范例1--组合问题1(代码随想录的搬运工):

主要是这个归一化的解题模板和思路值得学习;

给定两个整数 n 和 k,返回 1 ... n 中所有可能的 k 个数的组合。

示例:
输入: n = 4, k = 2
输出:
[2,4],[3,4],[2,3],[1,2],[1,3],[1,4]

思考:直接的暴力解法,首先想到的就是for循环,几个k几个for循环,n = 100, k = 3 那么就三层for循环,问题在于这个k到底是多少呢?我们没办法写出k个for循环,回溯搜索法来了,虽然回溯法也是暴力,但至少能写出来,不像for循环嵌套k层让人绝望。

用回溯法就很明智,第i层就是取了第i个数,横向遍历的就是集合的大小,注意:[1,2]=[2,1],所以啊,从左向右取数字,取过的数,就不用重复取了。横向遍历的时候,集合是越来越小的。

然后把达到叶子节点的结果收集起来,就可以求得 n个数中k个数的组合集合。

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

定义两个全局变量,一个存放符合条件的单层结果,一个用来存放符合条件的结果的集合

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

 startIndex来记录下一层递归,搜索的起始位置,防止出现重复的组合,搜索过的前面的元素,就不需要再搜索了,一共有n个元素,

void backtracking(int n, int k, int startIndex) 
  • 回溯函数终止条件

path数组的大小达到k,说明找到了一个子集大小为k的组合

if (path.size() == k) {
    result.push_back(path);
    return;
}
  • 单层搜索逻辑

如此我们才遍历完图中的这棵树?

for循环每次从startIndex开始遍历,然后用path保存取到的节点i。

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

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

backtracking的下面部分就是回溯的操作了,撤销本次处理的结果。

剪枝优化:

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

所以可以将循环遍历从i=startindex 到 i=n,修改为:

for (int i = startIndex; i <= n - (k - path.size()) + 1; i++) // i为本次搜索的起始位置
  1. 已经选择的元素个数:path.size();

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

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

为什么有个+1呢,因为包括起始位置,我们要是一个左闭的集合。

举个例子,n = 4,k = 3, 目前已经选取的元素为0(path.size为0),n - (k - 0) + 1 即 4 - ( 3 - 0) + 1 = 2。

从2开始搜索都是合理的,可以是组合[2, 3, 4]。

这里大家想不懂的话,建议也举一个例子,就知道是不是要+1了。

范例1--总结

组合问题是回溯法解决的经典问题,我们开始的时候给大家列举一个很形象的例子,就是n为100,k为50的话,直接想法就需要50层for循环。

从而引出了回溯法就是解决这种k层for循环嵌套的问题。

然后进一步把回溯法的搜索过程抽象为树形结构,可以直观的看出搜索的过程。

接着用回溯法三部曲,逐步分析了函数参数、终止条件和单层搜索的过程。

剪枝优化要根据题目的意思来,最好是画画图理解

范例2--组合问题2(代码随想录的搬运工)

找出所有相加之和为 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]]

与范例1的区别在于:组合中只允许含有 1 - 9 的正整数,k相当于树的深度,9(因为整个集合就是9个数)就是树的宽度。例如 k = 2,n = 4的话,就是在集合[1,2,3,4,5,6,7,8,9]中求 k(个数) = 2, n(和) = 4的组合。

  • 确定递归函数参数

vector<vector<int>> result; // 存放结果集

vector<int> path; // 符合条件的结果

targetSum(int)目标和,也就是题目中的n。

k(int)就是题目中要求k个数的集合。

sum(int)为已经收集的元素的总和,也就是path里元素的总和。

tartIndex(int)为下一层for循环搜索的起始位置。

  • 确定终止条件

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

回溯过程和处理过程是一一对应的,处理有加号,回溯的时候就有减号

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(); // 回溯
}

剪枝优化:跟范例1一样,for循环的范围也可以剪枝,i <= 9 - (k - path.size()) + 1就可以了。

范例3--子集问题(代码随想录的搬运工)

给定一组不含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。

说明:解集不能包含重复的子集。

示例: 输入: nums = [1,2,3] 输出: [ [3],   [1],   [2],   [1,2,3],   [1,3],   [2,3],   [1,2],   [] ]

分析:求子集问题和组合问题不一样,组合问题和分割问题都是收集树的叶子节点,但是子集问题是收集树的所有节点,其实子集也是一种组合问题,因为它的集合是无序的,子集{1,2} 和 子集{2,1}是一样的。那么既然是无序,取过的元素不会重复取,写回溯算法的时候,for就要从startIndex开始,而不是从0开始!

有同学问了,什么时候for可以从0开始呢?求排列问题的时候,就要从0开始,因为集合是有序的,{1, 2} 和{2, 1}是两个集合,排列问题我们后续的文章就会讲到的。

  • 递归函数参数

全局变量数组path为子集收集元素,二维数组result存放子集组合。(也可以放到递归函数参数里),递归函数参数在上面讲到了,需要startIndex,确保不会被重复选择。

  • 递归终止条件

当startindex已经大于数组长度,就终止了,没有元素可以选择了

if (startIndex >= nums.size()) {
    return;
}
  • 单层搜索逻辑

求取子集问题不需要任何剪枝,因为子集问题就是要遍历整棵树

for (int i = startIndex; i < nums.size(); i++) {
    path.push_back(nums[i]);    // 子集收集元素
    backtracking(nums, i + 1);  // 注意从i+1开始,元素不重复取
    path.pop_back();            // 回溯
}

其实这些问题都是由标准模板的题目,要弄清楚子集问题、组合问题和分割问题的区别,组合问题分割问体都是手机树形结构的叶子节点的结果,子集问题是收集树中所有节点的结果。

范例4--全排列(代码随想录的搬运工)

给定一个 没有重复 数字的序列,返回其所有可能的全排列。

示例:

  • 输入: [1,2,3]
  • 输出: [ [1,2,3], [1,3,2], [2,1,3], [2,3,1], [3,1,2], [3,2,1] ]

这是典型的排列树,我们就不按照代码随想录的那一套写了

  • 递归函数参数

vector<vector<int>> result;存放结果

定义 t 为需要排列的第t个数,也就是递归深度(解空间树的层数)

定义 n 为需要排列的总个数

定义vector<int> nums为原始的输入

 void backtrack(int t,int n,vector<int>& nums)
  • 递归终止条件
if(t==n){
    result.push_back(nums);
    return;
}
  • 单层搜索逻辑
 for(int i=t;i<n;i++){
    swap(nums[t],nums[i]);
    backtrack(t+1,n,nums);
    swap(nums[t],nums[i]);
}

swap函数:

 void swap(int &a,int &b){
    int tmp=a;
    a=b;
    b=tmp;
}

按照排列树的代码写起来很简单,其余排列树也是根据题目要求有着不同的变化。

代码随想录官方的思路也值得学习:

 递归函数的参数:排列是有序的,[1,2]和[2,1]是两个集合,和之前分析的组合不同的地方,可以看出元素1在[1,2]中已经使用过了,但是在[2,1]中还要在使用一次1,所以处理排列问题就不用使用startIndex了。但排列问题需要一个used数组,标记已经选择的元素。

递归终止条件:当收集元素的数组path的大小达到和nums数组一样大的时候,说明找到了一个全排列,也表示到达了叶子节点。

单层搜索逻辑:因为排列问题,每次都要从头开始搜索,例如元素1在[1,2]中已经使用过了,但是在[2,1]中还要再使用一次1。而used数组,其实就是记录此时path里都有哪些元素使用了,一个排列里一个元素只能使用一次

代码如下:

class Solution {
public:
    vector<vector<int>> result;
    vector<int> path;
    void backtracking (vector<int>& nums, vector<bool>& used) {
        // 此时说明找到了一组
        if (path.size() == nums.size()) {
            result.push_back(path);
            return;
        }
        for (int i = 0; i < nums.size(); i++) {
            if (used[i] == true) continue; // path里已经收录的元素,直接跳过
            used[i] = true;
            path.push_back(nums[i]);
            backtracking(nums, used);
            path.pop_back();
            used[i] = false;
        }
    }
    vector<vector<int>> permute(vector<int>& nums) {
        result.clear();
        path.clear();
        vector<bool> used(nums.size(), false);
        backtracking(nums, used);
        return result;
    }
};

大家此时可以感受出排列问题的不同:

  • 每层都是从0开始搜索而不是startIndex
  • 需要used数组记录path里都放了哪些元素了

范例5--棋盘问题--解数独(代码随想录的搬运工)

一个数独的解法需遵循如下规则: 数字 1-9 在每一行只能出现一次。 数字 1-9 在每一列只能出现一次。 数字 1-9 在每一个以粗实线分隔的 3x3 宫内只能出现一次。 空白格用 '.' 表示。

提示:

  • 给定的数独序列只包含数字 1-9 和字符 '.' 。
  • 你可以假设给定的数独只有唯一解。
  • 给定数独永远是 9x9 形式的

解题步骤与分析

N皇后问题是因为每一行、每一列只会放置一个皇后,for循环只需要遍历列的位置就行了,因为我们可以确定每一行放置一个皇后。

解数独不一样,棋盘的每一个位置都要放一个数字(而N皇后是一行只放一个皇后),并检查数字是否合法,解数独的树形结构要比N皇后更宽更深

  • 递归函数以及参数

递归函数的返回值需要是bool类型,为什么呢?

因为解数独找到一个符合的条件(就在树的叶子节点上)立刻就返回,相当于找从根节点到叶子节点一条唯一路径,所以需要使用bool返回值。

代码如下:

bool backtracking(vector<vector<char>>& board)

找到一个解就行了

  • 递归终止条件

本题递归不用终止条件,解数独是要遍历整个树形结构寻找可能的叶子节点就立刻返回。

不用终止条件会不会死循环?

递归的下一层的棋盘一定比上一层的棋盘多一个数,等数填满了棋盘自然就终止(填满当然好了,说明找到结果了),所以不需要终止条件!

  • 单层遍历逻辑

那么有没有永远填不满的情况呢?

我们需要的是一个二维的递归(也就是两个for循环嵌套着递归)

一个for循环遍历棋盘的行,一个for循环遍历棋盘的列,一行一列确定下来之后,递归遍历这个位置放9个数字的可能性!

bool backtracking(vector<vector<char>>& board) {
    for (int i = 0; i < board.size(); i++) {        // 遍历行
        for (int j = 0; j < board[0].size(); j++) { // 遍历列
            if (board[i][j] != '.') continue;
            // (i, j) 这个位置放k是否合适
            for (char k = '1'; k <= '9'; k++) {     
                if (isValid(i, j, k, board)) {
                    board[i][j] = k;                // 放置k
                     // 如果找到合适一组立刻返回
                    if (backtracking(board)) return true;
                    board[i][j] = '.';              // 回溯,撤销k
                }
            }
            // 9个数都试完了,都不行,那么就返回false
            return false;                           
        }
    }
    // 遍历完没有返回false,说明找到了合适棋盘位置了
    return true; 
}

注意这里return false的地方,这里放return false 是有讲究的

因为如果一行一列确定下来了,这里尝试了9个数都不行,说明这个棋盘找不到解决数独问题的解!

那么会直接返回, 这也就是为什么没有终止条件也不会永远填不满棋盘而无限递归下去。

判断棋盘是否合法

判断棋盘是否合法有如下三个维度:

  • 同行是否重复
  • 同列是否重复
  • 9宫格里是否重复

代码如下:

bool isValid(int row, int col, char val, vector<vector<char>>& board) {
    for (int i = 0; i < 9; i++) { // 判断行里是否重复
        if (board[row][i] == val) {
            return false;
        }
    }
    for (int j = 0; j < 9; j++) { // 判断列里是否重复
        if (board[j][col] == val) {
            return false;
        }
    }
    int startRow = (row / 3) * 3;
    int startCol = (col / 3) * 3;
    for (int i = startRow; i < startRow + 3; i++) { // 判断9方格里是否重复
        for (int j = startCol; j < startCol + 3; j++) {
            if (board[i][j] == val ) {
                return false;
            }
        }
    }
    return true;
}

解数独很难,数独的搜索过程有两个循环,一个for循环遍历棋盘的行,一个for循环遍历棋盘的列,一行一列确定下来之后,遍历这个位置(i,j)放9个数字的可能性。return true和return false也很有讲究,如果尝试了9个数字都不行,说明棋盘找不到解了,就应该return false。判断棋盘合法性的代码也很难很长,所以需要单独写,这样方便检查也方便看。

总结

回溯法其实很多都是按照模板来套用,跟暴力搜索类似,最好剪枝和判断是否满足题目条件的时候单独写一个函数,一来看起来好看,二来好修改,杂糅在一起反而感觉一团糟。

有关分割和去重的问题,需要明白如何模拟分割线,根据模板和题意来写,比动态规划简单多了。

明天就是除夕了,兔年快乐,心想事成,如愿以偿。 

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

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

相关文章

Kafka入门与核心概念

前言在我们开发过程中&#xff0c;有一些业务功能比较耗时&#xff0c;但是又不是很重要的核心功能&#xff0c;最典型的场景就是注册用户以后发送激活邮件分为两步1&#xff1a;向数据库插入一条数据2&#xff1a;向注册用户发送邮件第2步其实并不是核心功能&#xff0c;但是发…

SpringMVC-拦截器

1&#xff0c;pringMVC-拦截器 对于拦截器这节的知识&#xff0c;我们需要学习如下内容: 拦截器概念入门案例拦截器参数拦截器工作流程分析 1.1 拦截器概念 讲解拦截器的概念之前&#xff0c;我们先看一张图: (1)浏览器发送一个请求会先到Tomcat的web服务器 (2)Tomcat服务…

字节青训前端笔记 | 响应式系统与 React

本节课为前端框架 React 的基础课程讲解 React的设计思路 UI编程的特点 状态更新的时候&#xff0c;UI不会自动更新&#xff0c;需要手动调用DOM接口进行更新欠缺基本的代码层面的封装和隔离&#xff0c;代码层面没有组件化UI之间的数据依赖关系&#xff0c;需要手动维护&am…

【程序人生 | 价值扳机】你的寒假自律第一步(建议收藏)

&#x1f482;作者简介&#xff1a; THUNDER王&#xff0c;一名热爱财税和SAP ABAP编程以及热爱分享的博主。目前于江西师范大学会计专业大二本科在读&#xff0c;阿里云社区专家博主&#xff0c;华为云社区云享专家&#xff0c;CSDN SAP应用技术领域新兴创作者。   在学习工…

小技巧:Excel顽固的名称、引用冲突的解决

共享编辑、跨文档引用的不便 Excel 的共享文档多人编辑、跨文档引用等功能似乎很美好&#xff0c;实际用下来很成问题。 首先共享文档虽然用约定按标注责任人的方式由不同的人编辑不同的行避免的多人同时编辑一行的冲突&#xff0c;但是这通常是个大文件&#xff0c;经常(大概…

函数 C语言】

函数的声明和定义 函数间调用关系是&#xff0c;由于函数调用其他函数&#xff0c;替他函数也可以互相调用&#xff0c;同一个函数可以被一个或多个函数调用任意次。 先声明&#xff0c;后调用。 #include <stdio.h> //去标准库下找文件 #include "stdio.h"…

分件操作和IO

1.文件的路径如图:当我们打开此电脑后(windows系统),上面会显示我们当前的位置&#xff0c;点击之后会出现如图片中的一段字符&#xff0c;这段字符代表着当前你所处位置的路径。最开头的D&#xff1a;/d&#xff1a;是盘符&#xff0c;后面会用斜杠‘/’或者反斜杠‘\’分开,该…

MFC的使用

1.初使用的简单代码该程序包含两个代码 头文件mfc.h和mfc.cpp文件头文件mfc.h#pragma once #include<afxwin.h>class MyApp:public CWinApp//CWinApp应用程序类 { public://程序入口virtual BOOL InitInstance(); }; class MyFrame : public CFrameWnd//继承窗口框架类a …

Java_Git:2. 使用git管理文件版本

目录 1 创建版本库 1.1 使用GitBash 1.2 使用TortoiseGit 2 添加文件 2.1 添加文件过程 2.2 工作区和暂存区 3 修改文件 3.1 提交修改 3.2 查看修改历史 3.3 差异比较 3.4 还原修改 4 删除文件 5 案例&#xff1a;将java工程提交到版本库 5.1 复制文件到工作目录 …

论文阅读:Boosting 3D Object Detection by Simulating Multimodality on Point Clouds

Boosting 3D Object Detection by Simulating Multimodality on Point CloudsResponse DistillationSparse-Voxel DistillationVoxel-to-Point DistillationInstance DistillationlossExperiments稠密&#xff08;多模态、多帧&#xff09;信息->稀疏&#xff08;单模态点云…

【华为上机真题 2023】数组去重和排序 (华为机考真题)

&#x1f388; 作者&#xff1a;Linux猿 &#x1f388; 简介&#xff1a;CSDN博客专家&#x1f3c6;&#xff0c;华为云享专家&#x1f3c6;&#xff0c;Linux、C/C、云计算、物联网、面试、刷题、算法尽管咨询我&#xff0c;关注我&#xff0c;有问题私聊&#xff01; &…

2023/1/20 ES6基本语法学习

1 let 命令的解读 1 let 声明变量&#xff0c;没有变量提升 <script>// 不存在变量提升console.log(variable)let variable zhaoshuai-lc </script>2 作用域 全局作用域函数作用域&#xff1a;function() {}块级作用域&#xff1a;{} let 是一个块作用域 <…

Spring中涉及的设计模式总结

Spring中涉及的设计模式总结 1.简单工厂(非23种设计模式中的一种) 实例化阶段主要是通过反射或者CGLIB对bean进行实例化&#xff0c;在这个阶段Spring又给我们暴露了很多的扩展点&#xff1a; 实现方式&#xff1a;BeanFactory。Spring中的BeanFactory就是简单工厂模式的体现…

【设计模式】结构型模式·装饰者模式

学习汇总入口【23种设计模式】学习汇总(数万字讲解体系思维导图) 写作不易&#xff0c;如果您觉得写的不错&#xff0c;欢迎给博主来一波点赞、收藏~让博主更有动力吧&#xff01; 一.概述 在不改变现有对象结构的情况下&#xff0c;动态地给该对象增加一些职责&#xff08;即增…

2022年房地产投资退出方法和工具研究报告

第一章 房地产投资概况 房地产商品既是人们日常“衣食住行”中的一种必需品&#xff0c;又因保值增值的功能而具有很好的投资品属性。房地产投资是以房地产为对象&#xff0c;为获得预期效益而对土地和房地产开发、房地产经营&#xff0c;以及购置房地产等进行的投资。 房地产…

深度学习 GNN图神经网络(二)PyTorch Geometric(PyG)安装

一、前言 我们使用torch_geometric库来实现图神经网络的编码&#xff0c;因为它与PyTroch天然集成。本文介绍了PyTorch Geometric&#xff08;PyG&#xff09;的安装与测试。 二、安装 首先打开官方的安装说明文档&#xff1a;https://pytorch-geometric.readthedocs.io/en/…

蓝桥杯 stm32 LCD显示及 数据格式化

文章代码使用 HAL 库。 文章目录前言一、LCD 原理图&#xff1a;二、LCD 基本函数&#xff1a;1.LCD 清屏函数&#xff1a;LCD_Clear ( u16 Color )&#xff1b;2. 显示一行字符串&#xff1a;LCD_DisplayStringLine(u8 Line, u8 *ptr)&#xff1b;3.设置字符背景色&#xff1a…

switch自制软件开发环境搭建

参考: https://switch.homebrew.guide/ https://switchbrew.org/wiki/Main_Page https://www.bilibili.com/video/BV133411Q77X/?spm_id_from333.788&vd_sourcec5c272e9490d8bf475c8204462fc26e7 1.开发环境 开发机 -> 虚拟机 ubuntu22.04 设备 -> 破解switch 大…

Ubuntu20.04系统WineHQ7.0安装微信

提供3种Ubuntu系统安装微信的方法&#xff0c;在Ubuntu20.04上验证都ok。1.WineHQ7.0安装微信&#xff1a;ubuntu20.04安装最新版微信--可以支持微信最新版&#xff0c;但是适配的不是特别好&#xff1b;比如WeChartOCR.exe 报错。2. 原生微信安装&#xff1a;linux系统下的微信…

[电商实时数仓] 数据仓库建模过程分析

文章目录1.数据仓库概述1.1 数据仓库概念1.2 数据仓库核心架构2.数据仓库建模概述2.1 数据仓库建模的意义2.2 数据仓库建模方法论2.2.1 ER模型2.2.2 维度模型3.维度建模理论之事实表3.1 事实表概述3.2 事实表分类3.3 事务事实表4.维度建模理论之维度表5.数据仓库设计5.1 数据仓…