由树形解空间入手,深入分析回溯、动态规划、分治算法的共同点和不同点

一、回溯、动态规划、分治

其实这三个算法就可以直接认为是一类算法,它们的共同点都是会涉及到递归

更需要清楚明白的一点是,它们的解,都是基于一颗递归形式的树上得来的!也就是——回溯、动态规划、分治的解空间是 一棵树!!!(正是由于这种解的形式,它们都和递归分不开,因为递归就是适合解决这种树结构的求解问题!)

为什么这么说呢,下面我们来逐个分析一下!!!

1.1:回溯

1.1.1:核心思想:构建决策树,遍历路径,收集答案

  • 回溯:可以看作是暴力穷举,递归(eg:全排列问题);可能会有很多重复子问题(很多重复计算,因为子问题之间都有关联)。它的本质就是遍历一颗 决策树 (每个节点都是在做决策),然后全部遍历完成后将叶子节点上的答案进行 收集 起来,就能得到最终答案

    🌸回溯算法和 DFS 算法的细微差别是:回溯算法是在遍历 「树枝」 ,DFS 算法是在遍历 「节点」

    🍀站在回溯树的一个节点上,你只需要思考 3 个问题:
    1、路径:也就是已经做出的选择
    2、选择列表:也就是你当前可以做的选择。
    3、结束条件:也就是到达决策树底层,无法再做选择的条件。

    🪧代码框架:

    result = []
    def backtrack(路径, 选择列表):if 满足结束条件:result.add(路径)returnfor 选择 in 选择列表:做选择backtrack(路径, 选择列表)撤销选择
    

    其核心就是 for 循环里面的递归, 在递归调用之前在这个决策节点上「做选择」,在递归调用之后「撤销选择」

具体举例解释(求数字1,2,3的全排列):
在这里插入图片描述
比如在上图这样一颗二叉树,就是我们这个由回溯法求解全排列问题的解空间,我们的解是从这颗三叉树中得来的(包括动态规划和分治算法,它们的解空间本质上也是这样一颗多叉树,但是像动态规划可能存在重复子问题呀,会使用dp备忘录数组进行剪枝)。

而各个算法的关键以及区分点也就是

  1. 如何去构建这样一给解空间(一颗多叉树)——递归函数的定义
  2. 这个多叉树的节点含义是什么
  3. 如何基于这个解空间获得问题的答案。

上面两点既是这三个算法在求解问题流程上的共同点,而不同点是在于这两个流程具体实施的做法不同, 同样,对于这三个算法,只需要弄懂了以上几点,对折三个算法的理解、区别也就了然了。

再回到上面那个全排列问题和那个图,我们来逐一击破

  1. 🚉🚉🚉想要求解[1,2,3] 这三个全排列问题,很容易其实能想到上面那个树形结构,基于暴力穷举的想法,那么我们的二叉树其实就能构建好,很自然而然就能想象到下面这个图形(即使没有学过数据结构,但你的思路和想法的本质也一定是这样的)
    在这里插入图片描述

  2. 🚉🚉🚉基于回溯算法,每个节点其实都在做决策(这也是为什么把由回溯算法生成的解空间的树称为决策树)

    🍀站在回溯树的一个节点上,你只需要思考 3 个问题:
    1、路径:也就是已经做出的选择
    2、选择列表:也就是你当前可以做的选择。
    3、结束条件:也就是到达决策树底层,无法再做选择的条件。在这里插入图片描述
    比如上面那个图的2节点,你的 “路径” 就是[2], “选择列表” 是[1,3],“结束条件” 为:该决策节点的选择列表为空/

  3. 🚉🚉🚉得到这样一颗决策树的解空间后,我们就需要获得问题的答案了,做法是:回溯算法的这个回溯递归函数backtrack 其实就像是一个指针,游走在这颗树上,遍历完每条路径到叶子节点,这个路径上的值即为一个解,把所有路径遍历完成,即将所有答案收集,得到了问题的最终解!

🍊🍊🍊! 注意:实际过程中,肯定不是像上面讲的那样,先把树构建好了,然后去在这棵树上求解,反而是2、3两步同时进行的过程,一边构建这个解空间树,一边求解的过程,上面那么讲也只是为了好理解。比如再回到全排列问题,这个回溯递归函数backtrack 像一个指针,在构建树,维护每个节点的属性(路径、选择列表)时,还需要保证走到叶子节点时收集由这条路径得到的答案。


1.1.2:求解答案:如何遍历树(和DFS的区别)

其实呢,弄懂上面三点,你对整个回溯算法的理解其实已经就差不多了,这时基于宏观理解上面,再深入到回溯算法本身的特性上,进一步,从第三点,遍历树,寻找答案,那么具体该如何遍历呢?

对于回溯算法决策树的遍历的框架如下:

void traverse(TreeNode* root) {for (TreeNode* child : root->childern) {// 前序位置需要的操作traverse(child);// 后序位置需要的操作}
}

在这里插入图片描述

这里有一点需要注意的是,这个遍历框架和DFS遍历树的框架不太一样,多叉树的DFS遍历代码如下:

/* 多叉树遍历框架 */
void traverse(TreeNode* root) {if (root == nullptr) return;// 前序位置for (TreeNode* child : root->children) {traverse(child);}// 后序位置

可以看到,DFS的前序遍历位置和后序位置在for循环遍历子节点外面,而回溯算法遍历树的前序位置和后续位置在遍历子节点的for循环之内。

其实在1.1.1小节讲到回溯算法的核心思想开头就已经提到:

🌸回溯算法和 DFS 算法的细微差别是:回溯算法是在遍历 「树枝」 ,DFS 算法是在遍历 「节点」

也就是说,回溯算法更关注的是边,它遍历的对象是边,而非节点,它所遍历的就是root节点下的边,而DFS遍历的就是root这个节点,两者关注点不一样,你想遍历哪个,就按照对应的代码进行书写。

在这里插入图片描述

⭐它们的区别可以这样反映的代码上,可以更直观的理解!


// DFS 算法,关注点在节点
void traverse(TreeNode* root) {if (root == nullptr) return;printf("进入节点 %s", root);for (TreeNode* child : root->children) {traverse(child);}printf("离开节点 %s", root);
}// 回溯算法,关注点在树枝
void backtrack(TreeNode *root) {if (root == nullptr) return;for (TreeNode* child : root->children) {// 做选择printf("从 %s 到 %s", root, child);backtrack(child);// 撤销选择printf("从 %s 到 %s", child, root);}
}

到上面,我们基本上弄懂了了回溯的遍历思路以及和DFS遍历思路侧重点的不同,那么下面我们再回过头来看回溯算法的决策树和遍历过程:

⭐⭐⭐⭐⭐ 「路径」和「选择」是每个节点的属性,函数在树上游走要正确处理节点的属性,那么就要在这两个特殊时间点搞点动作:

  • 前序遍历的代码在进入某一个节点(做决策)之前的那个时间点执行:进行当前决策节点的 【做选择】
  • 后序遍历代码在离开某个节点之后的那个时间点执行:【撤销选择】,即撤销当前这个节点的当前这个选择(为了从选择列表中选取下一个选择做准备)
    在这里插入图片描述

那么现在,对核心框架中的这段代码我们应该有更好的理解了:

for 选择 in 选择列表:# 做选择将该选择从选择列表移除(表示做这个选择)路径.add(选择)#基于这个选择往后面的子节点进行树枝遍历backtrack(路径, 选择列表)# 撤销选择(为从选择列表中做出下一个选择做准备)路径.remove(选择)将该选择再加入选择列表

他是对于当前非结束决策节点的操作,本质是在决策树上遍历当前节点下面的边,从而在遍历(也是构建)决策树的过程中收集答案。

1.1.3:学习代码实现细节:例题:N排列

1:题目描述

题目链接:N排列
在这里插入图片描述

2:解题思路

其实整体的解题思路在上面解题思路中已经讲解了,这里以例题形式更详细解释一下详细代码的实现过程:

还是以[1,2,3]为例,整个解空间为以下树的形式
在这里插入图片描述

🪧 Tips: 这里的每个节点可以有一个used数组属性,其实这是【选择列表】的抽象,表示当前还有哪些元素没用,从而得到剩余集合:真正的选择列表。而【排列】也就是指遍历的到当前决策节点处得到的【路径】在这里插入图片描述
即:没有显式记录「选择列表」,而是通过 used 数组排除已经存在 track 中的元素,从而推导出当前的选择列表:在这里插入图片描述

3:⭐代码实现:回溯三部曲

🪧回顾:核心代码框架:

	result = []def backtrack(路径, 选择列表):if 满足结束条件:result.add(路径)returnfor 选择 in 选择列表:做选择backtrack(路径, 选择列表)撤销选择

有了解题思路,代码框架,我们具体实现代码时,其实就是对代码框架进行填充,完善,把不确定的部分给实现了,其实总结下来这个完善过程就是以下的递归三部曲:

  1. 递归函数的定义(明确这个递归函数的作用,包括参数:路径、选择列表)——明确决策节点的两个属性:路径、选择列表

      这里我们递归函数的定义就是对

    vector<vector<int>> result; //毋庸置疑,是用于在递归遍历过程中收集答案的oid backtrack(vector<int>& nums, vector<int>& track, vector<bool>& used)
    
  2. 递归终止条件: 选择列表为空,而我们的选择列表并不是一个真实存在的列表,而是numsused数组共同体现的一个东西,判断终止也可以用以下代码判断:路径的大小和nums大小相同,其实本质也就是说选择列表中的元素已经用完了

      // 触发结束条件if (track.size() == nums.size()) {res.push_back(track);return;}
    
  3. 求解答案单层搜索的逻辑(回溯遍历):这个其实就是指backtracking这个函数在这个树结构上“游走”的过程,即遍历树的过程,这在`1.1.2·小节中已经讲过,不再重复

      	for (int i = 0; i < nums.size(); i++) {// 排除不合法的选择if (used[i]) {//nums[i] 已经在 track中,跳过continue;}// 做选择track.push_back(nums[i]);used[i] = true;// 进入下一层决策树backtrack(nums, track, used);// 取消选择track.pop_back();used[i] = false;}
    

✅完整代码:

class Solution {
public:vector<vector<int>> result; //全局变量,存储在回溯过程中收集的最终结果//定义回溯代码(核心框架)void backtracking(vector<int>&nums, vector<bool>& used, vector<int>& track){//某条回溯路径的终止条件if(track.size() == nums.size() ){result.push_back(track);return;}//从选择列表里做选择for(int i = 0; i < nums.size(); i ++){if(used[i]){continue;}//做选择used[i] = true;track.push_back(nums[i]);//进入下一层决策树backtracking(nums, used, track);//撤销选择(为下一个选择做准备)used[i] = false;track.pop_back();}}vector<vector<int>> permute(vector<int>& nums) {result.clear();vector<int> track; //记录当前路径vector<bool> used (nums.size(), false); // 结合nums数组共同作用为【选择列表】,true代表当前元素已经被选择backtracking(nums, used, track); // 执行核心算法逻辑:回溯return result; //返回最终结果}
};

1.2:动态规划

1.2.1:核心思想

  • 动态规划:一般是来求最值/最优值问题;存在最优子结构(问题的最优解由子问题的最优解推导而来)即基于子问题最优解进行选择,由子问题 推导 出问题的最终最优解。

关于动态规划的思想篇我推荐看东哥的这篇:动态规划解题套路框架

当然,只有真正实践了,刷题了,才能更好的去理解一个算法,围绕动态规划的解题五部曲,我也写了几篇博客:

  • 【Leetcode每日一刷】动态规划算法: 62. 不同路径、63. 不同路径 II
  • 【Leetcode每日一刷】动态规划:509. 斐波那契数、322. 零钱兑换、300. 最长递增子序列
  • 【Leetcode每日一刷】动态规划:931. 下降路径最小和

在这篇文章中,我依然想从这三个角度,来解释这些算法:

而各个算法的关键以及区分点也就是

  1. 如何去构建这样一给解空间(一颗多叉树)——递归函数的定义
  2. 这个多叉树的节点含义是什么
  3. 如何基于这个解空间获得问题的答案。

这里,我也会从一个例题入手:322. 零钱兑换去讲解,建立在我们已经有前面几篇博客的知识基础上,去从这三个角度去深挖动态规划的本质(这也是在前面几篇博客里没有讲到的)。
在这里插入图片描述
关于怎么做这个题,可以看我前面的题解博客,现在我们从上面三个角度,去反思一下:

  1. 动态规划的树形解空间:在这里插入图片描述
    和回溯算法一样,动态规划也是一个树形的解空间,它的构建方法是:由状态转移方程,将原问题分解成规模更小的子问题(最优子结构),由子问题去 推导(这个推导很值得深究) 出问题的解。

  2. 通过前面对回溯算法的学习,我们知道,回溯的决策树上的点代表一个选择(决策),它有两个属性:路径&选择列表。同样,对于动态规划的解空间,它的节点的含义很简单:基于原问题所划分的小规模子问题的解

  3. 如何基于这个解空间去获得问题的答案?说白了,就是我们获得了这样一棵树,答案也是我们通过处理这个树,取得来的。对于回溯,它的做法是收集决策树的子节点路径。而对于动态规划呢?其实也就是我们要深究上面第一点所说的这个【推导】的含义,具体是什么?其实就是状态转移方程(下面的节点推导出上面节点的值),但其本质呢?其实本质就是对树上节点的处理,这个处理,是基于节点的,对于动态规划,它的处理策略就是:在这里插入图片描述
    其实你看,就是那个状态转移方程,由子问题(子节点)推导而来。

1.2.2:分析:重叠子问题和子问题独立性?

OK,首先我们来说说子问题的重叠,动态规划的一大特点就是消除了子问题的重叠性,这个是毋庸置疑的。下面这张图其实是给出在暴力递归下的树形解空间:
在这里插入图片描述
可以看到,对于节点5,其实在构造这个抽象的树形解空间时,它被计算了多次,而动态规划对此的改进就是:备忘录、DP数组。本质就是在追求 “如何聪明地穷举” 。用空间换时间的思路。

那么子问题的独立性呢?我在不同的地方看到不同的解释,可能你在学习的时候也会有疑惑。后来我才发现,这两个“独立”并不是一个含义,接下来我将逐个讲解。

  • 子问题相互独立,是最优子结构的前提——>是可以用DP求解的前提

      这里我们用一个例子来说明:比如说,假设你考试,每门科目的成绩都是互相独立的。你的原问题是考出最高的总成绩,那么你的子问题就是要把语文考到最高,数学考到最高…… 为了每门课考到最高,你要把每门课相应的选择题分数拿到最高,填空题分数拿到最高…… 当然,最终就是你每门课都是满分,这就是最高的总成绩。
      得到了正确的结果:最高的总成绩就是总分。因为这个过程符合最优子结构,「每门科目考到最高」这些子问题是互相独立,互不干扰的。
      但是,如果加一个条件:你的语文成绩和数学成绩会互相制约,不能同时达到满分,数学分数高,语文分数就会降低,反之亦然。
      这样的话,显然你能考到的最高总成绩就达不到总分了,按刚才那个思路就会得到错误的结果。因为「每门科目考到最高」的子问题并不独立,语文数学成绩户互相影响,无法同时最优,所以最优子结构被破坏。
      回到凑零钱问题,为什么说它符合最优子结构呢?假设你有面值为 1, 2, 5 的硬币,你想求 amount = 11 时的最少硬币数(原问题),如果你知道凑出 amount = 10, 9, 6 的最少硬币数(子问题),你只需要把子问题的答案加一(再选一枚面值为 1, 2, 5 的硬币),求个最小值,就是原问题的答案。因为硬币的数量是没有限制的,所以子问题之间没有相互制约,是互相独立的。

  • 子问题不独立:这里的“独立”是指,子问题之间具有相互推导关系你看下面这个图dp[11]可以由下面的三个孩子根据状态方程推得,而dp[10]是也可以由它下面三个孩子根据状态方程推得…其实这个不独立主要是和下面要讲的分治算法作比较而提出的不独立,和上面那个独立是两个概念。这
    在这里插入图片描述

1.3:分治

1.3.1:核心思想

  • 分治:分治则是,原问题的解本身就是可以分解为与原问题相同但规模更小的子问题,然后 合并 子问题,从而得到原问题的解。

最典型的分治算法就是归并排序了,核心逻辑如下:

void sort(int[] nums, int lo, int hi) {int mid = (lo + hi) / 2;/****** 分 ******/// 对数组的两部分分别排序sort(nums, lo, mid);sort(nums, mid + 1, hi);/****** 治 ******/// 合并两个排好序的子数组merge(nums, lo, mid, hi);
}

「对数组排序」是一个可以运用分治思想的算法问题,只要我先把数组的左半部分排序,再把右半部分排序,最后把两部分合并,不就是对整个数组排序了吗?

同样的,我们再从三个视角对这个算法进行审视(以归并排序为例)在这里插入图片描述

  1. 分治算法的树形结构:和动态规划其实有的像的是,分治算法树形结构也是构建的核心也是:将原问题分解为规模更小的子问题,但是这个分并不基于什么状态转移方程,因为它的子问题之间没有什么关系(也就是上面在动态规划里讲的:子问题之间的推导关系)——所以由于这种情况的存在,分治也不存在什么DP里的重叠子问题!
  2. 每个节点的含义:这个就和动态规划一样了:原问题较小规模的子问题的解
  3. 如何基于这个树形解空间来求得问题的解?其实对于分治,很明显的一点就是:合并,没错。其实你看处理位置上,和动态规划一样,就是处理方法不一样。动态规划采用状态转移方程(即:推导)进行处理,而分治采用的是合并!在这里插入图片描述

1.3.2:例题:棋盘覆盖问题

1:题目描述

在这里插入图片描述

2:解题思路

这题就是我觉得比起知道用分治怎么解,更难的是想到应该用分治!

直接上图吧!

在这里插入图片描述
分析:

  1. 首先,当k>0时,将 2 k × 2 k 2^k×2^k 2k×2k的棋盘可以等分为四个规模更小且与原问题类型一致的问题:4个 2 k − 1 × 2 k − 1 2^{k-1}×2^{k-1} 2k1×2k1的棋盘覆盖问题,其中有三个不存在特殊方格,可以使用一个L型先进行覆盖,那么其覆盖到这3个子棋盘的会和的位置,作为该子问题的特殊方格
  2. 经过上面处理,我们成功地获得了4个 2 k − 1 × 2 k − 1 2^{k-1}×2^{k-1} 2k1×2k1的棋盘覆盖子问题。递归地使用这种分割策略(也就是分治里的【分】),直到棋盘为1×1则终止(递归必需的终止条件)
  3. 再合并结果(其实不用显示合并,只用在递归遍历时把结果填入,最终全部遍历完,就是已经是合并后的结果了)

实现:

  1. 明确递归函数的定义是什么,相信并且利用好函数的定义!
<
//tr,tc(top_row,top_col):代表棋盘左上角坐标
//sr,sc(special_row, special_col):代表特殊点坐标
//size:棋盘的大小//递归
void chessBoard(int tr, int tc, int sr, int sc, int size);
  1. 递归函数的实现:这题很简单,就是怎么分!进行四个判断即可!
    每次判断是基于分割后的四个小方块进行判断,判断特殊方格是否在内。判断方法是根据分割后小棋盘左上角的坐标和特殊棋盘的坐标进行比较。若在里面则直接接着递归下去,如果不在呢,根据没有特殊方格的三个子棋盘的位置不同,将汇合处标记为特殊位置。

✅完整代码

#include <stdio.h>
#include <stdlib.h>
#include <cstring>    // Include for memsetint nCount = 0;
int Matrix[100][100];void chessBoard(int tr, int tc, int sr, int sc, int size);int main()
{int size,r,c,row,col;std::memset(Matrix,0,sizeof(Matrix));    // Use std:: prefix for memsetscanf("%d",&size);scanf("%d%d",&row,&col);chessBoard(0,0,row,col,size);for (r = 0; r < size; r++){for (c = 0; c < size; c++){printf("%2d ",Matrix[r][c]);}printf("\n");}return 0;
}void chessBoard(int tr, int tc, int sr, int sc, int size)
{//tr and tc represent the top left corner's coordinate of the matrixif (1 == size) return;int s,t;s = size/2; //The number of grid the matrix's edget = ++ nCount;//locate the special  grid on bottom right cornerif (sr < tr + s && sc < tc +s){chessBoard(tr,tc,sr,sc,s);}else{Matrix[tr+s-1][tc+s-1] = t;chessBoard(tr,tc,tr+s-1,tc+s-1,s);}//locate the special  grid on bottom left cornerif (sr < tr + s && sc >= tc + s ){chessBoard(tr,tc+s,sr,sc,s);}else{Matrix[tr+s-1][tc+s] = t;chessBoard(tr,tc+s,tr+s-1,tc+s,s);}//locate the special  grid on top right cornerif (sr >= tr + s && sc < tc + s){chessBoard(tr+s,tc,sr,sc,s);}else{Matrix[tr+s][tc+s-1] = t;chessBoard(tr+s,tc,tr+s,tc+s-1,s);}//locate the special  grid on top left cornerif (sr >= tr + s && sc >= tc + s){chessBoard(tr+s,tc+s,sr,sc,s);}else{Matrix[tr+s][tc+s] = t;chessBoard(tr+s,tc+s,tr+s,tc+s,s);}}

二、总结

回溯、动态规划、分治可以认为是一类算法,其实不用严格的将其分为太开,它们的共同点是解空间都是一棵多叉树,获得解也是在这个多叉树上进行操作

只需要从这三个视角对其有一个直观认识即可

而各个算法的关键以及区分点也就是

  1. 如何去构建这样一给解空间(一颗多叉树)——递归函数的定义
  2. 这个多叉树的节点含义是什么
  3. 如何基于这个解空间获得问题的答案。

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

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Python深度学习基于Tensorflow(6)神经网络基础

文章目录 使用Tensorflow解决XOR问题激活函数正向传播和反向传播解决过拟合权重正则化Dropout正则化批量正则化 BatchNormal权重初始化残差连接 选择优化算法传统梯度更新算法动量算法NAG算法AdaGrad算法RMSProp算法Adam算法如何选择优化算法 使用tf.keras构建神经网络使用Sequ…

漫谈音频深度伪造技术

作为人工智能时代的新型媒体合成技术&#xff0c;深度伪造技术近年来在网络媒体中的涉及领域越发广泛、出现频次越发频繁。据路透社报道&#xff0c;2023年&#xff0c;社交媒体网站上发布50万个深度伪造的语音和视频。 1、深度伪造技术的五个方面 音频深度伪造技术&#xff…

3D模型实时变形算法

最近&#xff0c;在尝试渲染一些奇怪的形状后&#xff0c;我陷入了计算机图形学的困境。事实证明&#xff0c;对于我试图解决的具体问题&#xff0c;没有现有的选项完全适合我想要做的事情。几周后&#xff0c;我终于带着一些答案再次浮出水面&#xff0c;写了很多行代码&#…

【项目学习01_2024.05.02_Day04】

学习笔记 4 课程分类查询4.1需求分析4.2 接口定义4.3 接口开发4.3.1 树型表查询4.3.2 开发Mapper 4 课程分类查询 4.1需求分析 有课程分类的需求 course_category课程分类表的结构 这张表是一个树型结构&#xff0c;通过父结点id将各元素组成一个树。 利用mybatis-plus-gen…

(✌)粤嵌—2024/5/8—随机链表的复制

代码实现&#xff1a; struct Node* copyRandomList(struct Node *head) {if (head NULL) {return NULL;}// 第一步&#xff1a;复制节点&#xff0c;并将复制节点插入到原节点的旁边struct Node *p head;while (p) {struct Node *temp malloc(sizeof(*temp));temp->val …

网络编程入门之UDP编程

欢迎各位帅哥美女来捧场&#xff0c;本文是介绍UDP网络编程。在这里&#xff0c;你会见到最详细的教程&#xff1b;细致到每一行代码&#xff0c;每一个api的由来和使用它的目的等。 目录 1.UDP相关API 1.1.两个类 1.2.两个类中的方法 2.UDP编程 2.1.大体框架 2.2.内容构…

知识图谱:人工智能的“核心驱动力”

知识图谱&#xff1a;人工智能的“核心驱动力” 一、人工智能与知识图谱二、知识图谱的定义与重要性三、知识图谱工程师的薪资情况四、知识图谱的应用领域六、知识图谱的未来展望七、总结 一、人工智能与知识图谱 人工智能&#xff08;AI&#xff09;作为21世纪的前沿技术&…

【四、性能测试】性能测试基础与几个重要的概念

你好&#xff0c;我是山茶&#xff0c;一个95后在职程序员。也是一个目标跟 1000 程序员探索出 AI 测试 副业之路的 bro&#xff0c;欢迎跟我一起沟通交流&#xff01; 一、什么是性能测试&#xff1f; 性能测试是通过自动化的测试工具模拟多种正常、峰值以及异常负载条件来…

JRT失控处理打印和演示

基于JRT完备的脚本化和打印基础&#xff0c;基于JRT的业务可以轻松的实现想要的打效果&#xff0c;这次以质控图的失控处理打印和月报打印来分享基于JRT的打印业务实现。 演示视频链接 失控报告打印 失控处理打印的虚拟M import JRT.Core.DataGrid.GridDto; import JRT.Co…

Bugku Crypto 部分题目简单题解

抄错的字符 题目描述&#xff1a; 老师让小明抄写一段话&#xff0c;结果粗心的小明把部分数字抄成了字母&#xff0c;还因为强迫症把所有字母都换成大写。你能帮小明恢复并解开答案吗&#xff1a; QWIHBLGZZXJSXZNVBZW 观察疑似base64解码&#xff0c;尝试使用cyberchef解码…

【OceanBase 系列】—— OceanBase v4.3 特性解读:查询性能提升之利器列存储引擎

原文链接&#xff1a;OceanBase 社区 对于分析类查询&#xff0c;列存可以极大地提升查询性能&#xff0c;也是 OceanBase 做好 HTAP 和 OLAP 的一项不可缺少的特性。本文介绍 OceanBase 列存的实现特色。 OceanBase从诞生起就一直坚持LSM-Tree架构&#xff0c;不断打磨功能支…

Google Earth Engine谷歌地球引擎计算遥感影像在每个8天间隔内的多年平均值

本文介绍在谷歌地球引擎&#xff08;Google Earth Engine&#xff0c;GEE&#xff09;中&#xff0c;求取多年时间中&#xff0c;遥感影像在每1个8天时间间隔内的多年平均值的方法。 本文是谷歌地球引擎&#xff08;Google Earth Engine&#xff0c;GEE&#xff09;系列教学文章…