算法设计与分析之回溯法

news2024/10/7 16:25:55

文章目录

  • 1. 回溯法简介
    • 1.1 DFS的基本思想
    • 1.2 回溯法的基本思想
    • 1.3 回溯法和DFS的区别
    • 1.4 剪枝
  • 2. 01背包问题:子集树
    • 2.1 问题介绍
    • 2.2 解决思路
    • 2.3 算法实现
    • 2.4 如何优化
  • 3. 旅行商问题TSP:排序树
    • 3.1 问题介绍
    • 3.2 解决思路
    • 3.3 算法框架
    • 3.4 算法实现
  • 4. 总结

1. 回溯法简介

回溯法,又叫试探法,是一种寻找最优解暴力搜寻法。由于暴力,回溯法的时间复杂度较高,因此在比较一些数字较大的问题,比如最短路径问题等时,运行时间一般比较长。在回溯法中,**DFS(深度优先搜索)**是一种很重要的工具。

1.1 DFS的基本思想

(1)某一种可能情况向前探索,并生成一个子节点;

(2)过程中,一旦发现原来的选择不符合要求,就回溯至父亲结点,然后重新选择另一方向,再次生成子结点,继续向前探索;

(3)如此反复进行,直至求得最优解。

1.2 回溯法的基本思想

(1)针对具体问题,定义问题的解空间

(2)确定易于搜索的解空间结构(数据结构的选择);

(3)一般以DFS的方式搜索解空间;

(4)在搜索过程中,可以使用剪枝函数等来优化算法。(剪枝函数:用约束函数限界函数剪去得不到最优解的子树,统称为剪枝函数。)

解空间:顾名思义,就是一个问题的所有解的集合。(但这离我们要求的最优解还差很远。)

约束条件:有效解的要求,即题目的要求。

约束函数:减去不满足约束条件的子树的函数。

限界函数:去掉得不到最优解的结点的函数。

扩展结点:当前正在产生子结点的结点称为扩展结点。

回溯法处理的解空间类型主要分为以下两种:

  • 子集树:当所给问题是从集合中找出满足某种性质的子集时,相应的解空间树称为子集树。

  • 排列树:当所给问题事从集合中确定满足某种性质的排列时,相应的解空间树称为排列树。

1.3 回溯法和DFS的区别

DFS是一种遍历搜索图、树等数据结构的一种算法,更像一种工具;

而回溯法则是为了解决问题不断地生成又放弃一些解决方案(解空间在搜索问题的过程中动态产生是回溯法的一个重要特点),直至找到最优解或搜索完毕为止的一种方法,更像一种指导思想,在解空间中利用DFS进行全面的搜索。

1.4 剪枝

剪枝就是在搜索过程中利用过滤条件来剪去完全不用考虑(已经判断这条路走下去得不到最优解)的搜索路径,从而避免了一些不必要的搜索,优化算法求解速度,当然还必须得保证结果的正确性。

应用到回溯算法中,我们可以提前判断当前路径是否能产生结果集,如果否,就可以提前回溯。而这也叫做可行性剪枝

另外还有一种叫做最优性剪枝,每次记录当前得到的最优值,如果当前结点已经无法产生比当前最优解更优的解时,可以提前回溯。

然而,剪枝的过滤条件不好找,想通过剪枝优化来提高算法高效性,既要保证结果正确性,还要保证剪枝的准确性。

2. 01背包问题:子集树

2.1 问题介绍

01背包问题就是由子集树解决的一个经典问题。问题如下:

小明打算去拜访同学,他打算带一背包的巧克力作为礼物。他希望装进的巧克力总价值最高(这样可能比较好吃)。然而小明体力有限,巧克力包不能太重,只能有8kg。可供选择的巧克力如下:

序号品牌重量/kg价值
1费列罗445
2好时之点557
3德芙222
4Cudie(西班牙)111
5自制667

2.2 解决思路

在这里插入图片描述

因为我们考虑的是找子集,所以每个物品只有选与不选两种状态,因此解空间是一个二叉树。在这个树中,每一层的边表示对一个物品的选择与否。如上图所示,选择第一层点0与左边点1间的边,表示选择1号物品,也就是选择左子树走下去;如果不选择1号物品入包,则进入右子树,选择右边点1。那么,一共有n件物品,就有n层的边,n+1层点。最后一层的每一个叶结点分别表示一种选择法,一共有2n个叶结点,即解空间中共有2n种解,我们要在这些叶结点中选择最佳结点。

我们先给出利用回溯法搜索子树集的伪代码框架:

void search(层数)
{
	if(搜索到最底层)
		打印出结果解;
	else 
		for(遍历当前层解)
		{
			if(合适解)
				继续搜索;
			撤消当前状态的影响; //回溯
		}
}

回溯法讲究“暴力”。从暴力的角度思考,想把所有的尽量装满背包的搭配都找出来,需要标记每一种装法(每一个解)最大value,从而找到最优解。我们从第一种巧克力开始装,然后找下一个,判断能否装入,再递归,到达边界,比较,记录较优解,回溯,继续往下找……循环。从子集树的角度将,我们优先选择走左子树,也就是入包;当走到叶结点或不符合约束的重量条件时,回溯到父结点,进入右结点,最后遍历全树。

判断能否装入后可以用一个book数组来标记是否选择入包。

2.3 算法实现

由以上思路写出01背包问题的算法如下:

//01背包问题-回溯法-子集树 
#include <iostream>

int n, bag_v, bag_w;
int bag[100], x[100], w[100], val[100];

//search递归函数,当前节点背包的价值为cur_v(current value),重量为cur_w(current weight)
void search(int cur, int cur_v, int cur_w)
{
    if(cur > n) //判断子集树的边界   
    {
        if(cur_v > bag_v) //子集树对应的背包价值 是否超过了 最大价值
        {
            bag_v = cur_v; //得到最大价值
            for(int i = 1; i <= n; i++)      
                bag[i] = x[i]; //x表示当前子集树各物品是否被选中,将选中的物品存入bag中 
        }
    }
    else 
        for(int j = 0; j <= 1; j++) //遍历当前解层:j 代表是否选择该物品
        {
            x[cur] = j;      
            if(cur_w + x[cur]*w[cur] <= bag_w) //满足重量约束,继续向前寻找配对 
            {
                cur_w += w[cur]*x[cur];
                cur_v += val[cur]*x[cur];
                search(cur + 1, cur_v, cur_w); //递归,下一层物品 
                //清除痕迹,回溯上一层 
                cur_w -= w[cur]*x[cur];   
                cur_v -= val[cur]*x[cur];
                x[cur] = 0;
            }
        }
}

int main()
{
    int i;
    bag_v = 0; //初始化背包最大价值
    
    //输入数据 
    std::cout << "请输入背包最大容量:" << std::endl;
    std::cin >> bag_w;
    std::cout << "请输入物品个数:" << std::endl;
    std::cin >> n;
    std::cout << "请依次输入物品的重量:" << std::endl;
    for(i = 1; i <= n; i++) 
        std::cin >> w[i];
    std::cout << "请依次输入物品的价值:" << std::endl;
    for(i = 1; i <= n; i++) 
        std::cin >> val[i]; 
    search(1, 0, 0);
    
    std::cout << "最大价值为:" << std::endl;
    std::cout << bag_v << std::endl;
    std::cout << "物品的编号依次为:" << std::endl;

    for(i = 1; i <= n; i++)
        if(bag[i] == 1) 
            std::cout << i << " ";
    std::cout << std::endl;
    
    return 0;
}

输出如下:

PS E:\Code\VSCode\Learning\build> .\main.exe
请输入背包最大容量:
8
请输入物品个数:
5
请依次输入物品的重量:
4 5 2 1 6
请依次输入物品的价值:
45 57 22 11 67
最大价值为:
90
物品的编号依次为:
2 3 4

2.4 如何优化

我们可以用一个上界函数bound():当前价值+剩余容量可容纳的最大价值,去和目前的背包最大价值(也就是最优解)比较,如果bound()更小,那就没有继续搜索的意义了,剪去左子树,即不选择当前物品,进入右子树。

因为物品只有选与不选2个决策,而总共有n个物品,所以时间复杂度为O(2n)。因为递归栈最多达到n层,而且存储所有物品的信息也只需要常数个一维数组,所以最终的空间复杂度为O(n)。

那么,我们如何计算这个“剩余容量可容纳的最大价值”呢?首先,我们先将物品按照其单位重量价值从大到小排序,此后就按照顺序考虑各个物品。代码如下:

if(cur_w+w[cur]<=bag_w) //将物品cur放入背包,搜索左子树,即选择当前物品 
{
	cur_w+=w[cur]; //同步更新当前背包的重量
	cur_v+=val[cur]; //同步更新当前背包的总价值
	put[cur]=1;
	search(cur+1,cur_v,cur_w); //深度搜索进入下一层
	cur_w-=w[cur]; //回溯复原
	cur_v-=val[cur]; //回溯复原
}
if(bound(cur+1,cur_v,cur_w)>bag_v) //如若符合条件则搜索右子树,即不选择当前物品 
{
	put[cur]=0;
	search(cur+1,cur_v,cur_w);
}
  • 当i<=n,重量超过限制时,leftw为负,我们得到的是一个达不到的理想最大价值,因为此时最后放入的物品单位价值较高,但无法完全塞进书包,我们就去掉多余的部分,只取一部分该物体入包。当然,这是做不到的。因此计算出的值是一个达不到的理想值。

  • 当i>n,重量未超过限制时,则是可达到的最大价值。

这样就解释了这个上界函数的优化。可以看出,这是一个最优性剪枝优化,判断当前结点是否有机会产生更优解。

优化后的算法如下:

#include <iostream>

int n, bag_v, bag_w;
int bag[100], put[100], w[100], val[100], order[100];
double perp[100]; 

//按照单位重量价值排序,这里用冒泡 
void bubblesort()
{
    int i,j;
    int temporder = 0;
    double temp = 0.0;
 
    for(i = 1;i <= n; i++)
        perp[i] = val[i] / w[i]; //计算单位价值(单位重量的物品价值)
    for(i = 1; i <= n - 1; i++)
    {
        for(j = i + 1; j <= n; j++)
            if(perp[i] < perp[j]) //冒泡排序perp[], order[], sortv[], sortw[]
        {
            temp = perp[i];  //冒泡对perp[]排序交换 
            perp[i] = perp[i];
            perp[j] = temp;
 
            temporder = order[i]; //冒泡对order[]交换 
            order[i] = order[j];
            order[j] = temporder;
 
            temp = val[i]; //冒泡对val[]交换 
            val[i] = val[j];
            val[j] = temp;
 
            temp = w[i]; //冒泡对w[]交换 
            w[i] = w[j];
            w[j] = temp;
        }
    }
}

//计算上界函数,功能为剪枝
double bound(int i, int cur_v, int cur_w)
{   //判断当前背包的总价值cur_v + 剩余容量可容纳的最大价值 <= 当前最优价值
    double leftw = bag_w - cur_w; //剩余背包容量
    double b = cur_v; //记录当前背包的总价值cur_v,最后求上界
    //以物品单位重量价值递减次序装入物品
    while(i <= n && w[i] <= leftw)
    {
        leftw -= w[i];
        b += val[i];
        i++;
    }
    //装满背包
    if(i <= n)
        b += val[i] / w[i] * leftw;
    return b; //返回计算出的上界
}

void search(int cur, int cur_v, int cur_w)
{   //search递归函数,当前current节点的价值为current value,重量为current weight 
    if(cur > n) //判断边界   
    {
        if(cur_v > bag_v) //是否超过了最大价值
        {
            bag_v = cur_v; //得到最大价值
            for(int i = 1; i <= n; i++)      
                bag[order[i]] = put[i]; //put表示当前是否被选中,将选中的物品存入bag中 
        }
    }
    //如若左子节点可行,则直接搜索左子树
    //对于右子树,先计算上界函数,以判断是否将其减去
    if(cur_w + w[cur] <= bag_w) //将物品cur放入背包,搜索左子树,即选择当前物品 
    {
        cur_w += w[cur]; //同步更新当前背包的重量
        cur_v += val[cur]; //同步更新当前背包的总价值
        put[cur] = 1;
        search(cur + 1, cur_v, cur_w); //深度搜索进入下一层
        cur_w -= w[cur]; //回溯复原
        cur_v -= val[cur]; //回溯复原
    }
    if(bound(cur + 1, cur_v, cur_w) > bag_v) //如若符合条件则搜索右子树,即不选择当前物品 
    {
        put[cur] = 0;
        search(cur + 1, cur_v, cur_w);
    }
}

int main()
{
    int i;
    bag_v = 0; //初始化背包最大价值
    //输入数据 
    std::cout << "请输入背包最大容量:" << std::endl;;
    std::cin >> bag_w;
    std::cout << "请输入物品个数:" << std::endl;
    std::cin >> n;
    std::cout << "请依次输入物品的重量:" << std::endl;
    for(i = 1; i <= n; i++) 
        std::cin >> w[i];
    std::cout << "请依次输入物品的价值:" << std::endl;
    for(i = 1; i <= n; i++) 
        std::cin >> val[i];
    for(i = 1; i <= n; i++) //新增的order数组,存储初始编号 
        order[i] = i;
    search(1, 0, 0);
    
    std::cout << "最大价值为:" << std::endl;
    std::cout << bag_v << std::endl;
    std::cout << "物品的编号依次为:" << std::endl;

    for(i = 1; i <= n; i++)
        if(bag[i] == 1) 
            std::cout << i << " ";
    std::cout << std::endl;
    
    return 0;
}

3. 旅行商问题TSP:排序树

3.1 问题介绍

小明在去同学那前想了一想,准备顺便拜访各高校的高中同学。他打算从本校出发,途径高中同学所在的一些高校,最终回到自己学校。小舟很懒,希望只走最短的路,同时不想在一个学校玩第二次,因为他们不是主要目标。如何制定一个旅行方案?

乍一看这个题目是不是和最短路径问题很像?但很可惜的是,最短路径不要求通过每一个点,还是有所不同。

3.2 解决思路

排列树与子集树最大的区别在于,子集树的解是无序的子集,而排列树的解则包含整个集合的所有元素,我们从暴力的原则出发,将元素进行全排列

在这里插入图片描述

{ } 外的数表示已经排好序,{ } 内的数表示尚未排序。

在排序树中,每一层选择一个数字排到队尾,因此对一个n元素的集合,树的第一层将有n个子结点,表示可选n个数放在队伍的第一个位置,一次分叉比前一次减少一个(因为已经确定了一个位置的元素);树共有n+1层(图中省略了最后一层),表示选择n次;叶结点共有**n!**个,表示组合数A,全排列共有n!种情形(因此时间复杂度也是n!)。

在这个问题中,我们的解空间就是所有城市的全排列,即走过每一个城市的顺序,因此可以用排序树来考虑这个问题。

3.3 算法框架

void backtrack(int t)
{
    if(t > n)
        output(x);
    else
    {
        for(int i = t; i <= n; i++)
        {
            swap(x[t], x[i]);
            if(constraint(t) && bound(t))
                backtrack(t+1);
            swap(x[i],x[t]);
        }
    }
}

这里的swap是一个交换函数,对于一个排列,只要交换任意两数后就是一个新排列。constraint()和bound())分别是约束条件限定函数(用于剪枝优化)。

为什么要用swap来交换,而不是把数据放入新数组啦等等什么别的操作呢?这是因为,当我们在原先存储数据的数组x内进行交换时,我们把排好序的元素放到了数组的前面,留下的数据则是未排序的。这样在我们进行for循环的时候就能从t开始,同时避免了重复遇到排过序的数,也不需要book记录等多余的代码。

3.4 算法实现

//旅行商问题-回溯法-排序树 
#include <iostream>
 
int n, t;
int dis[100][100], x[100], bestroad[100]; 
int cur_dis, bestdis;
const int INF=99999;

void swap(int& a, int& b)  //swap函数,交换 
{
	int temp;
	temp = a;
	a = b;
	b = temp;
}
 
void backtrack(int t)   
{
	if (t == n)
	{ 	//判断边界。很长的判断,不能到自己或到不了,要比当前最优解短 
		if (dis[x[n - 1]][x[n]] != 0 && dis[x[n]][1] != 0 &&(cur_dis + dis[x[n - 1]][x[n]] + dis[x[n]][1] < bestdis || bestdis == 0)) 
		{  	//记录最优路径,最优距离 
			for (int j = 1; j <= n; j++)
				bestroad[j] = x[j];
			bestdis = cur_dis + dis[x[n-1]][x[n]] + dis[x[n]][1];
			return;
		}
	}
	else
	{
		for (int j=t;j<= n; j++)
		{
			if(dis[x[t]][x[j]]!=0&& (cur_dis + dis[x[t - 1]][x[t]] + dis[x[t]][1] < bestdis || bestdis == 0))
			{
				swap(x[t], x[j]);
				cur_dis += dis[x[t]][x[t-1]];
				backtrack(t+1);
				//回溯 
				cur_dis -= dis[x[t]][x[t-1]];
				swap(x[t], x[j]);
			}
		}
	}
 }
 
int main()
{
	int i, j, m, a, b, c;

	std::cout << "输入城市数:" << std::endl;
	std::cin >> n; 
	std::cout << "输入路径数:" << std::endl; 
	std::cin >> m;
	//初始化邻接矩阵
	for(i = 1; i <= n; i++)
		for(j = 1; j <= n; j++)
			dis[i][j] = 0;  
	std::cout << "输入路径与距离:" << std::endl;

	//读入城市之间的距离
	for(i = 1; i <= m; i++)
	{ 
		std::cin >> a >> b >> c;
		dis[a][b] = dis[b][a] = c; //无向图,两边都记录 
	}
	for(i = 1; i <= n; i++)
		x[i] = i;
		
	backtrack(2);      
	std::cout << "最佳路径为:";
	for (i = 1; i <= n; i++)
			std::cout << bestroad[i] << " --> ";
	std::cout << "1" << std::endl;
	std::cout << "最短距离为:" << bestdis;

	return 0;
 }
 

输出如下:

PS E:\Code\VSCode\Learning\build> ."E:/Code/VSCode/Learning/build/main.exe"
输入城市数:
4
输入路径数:
6
输入路径与距离:
1 2 30
1 3 6
1 4 4
2 3 5
2 4 10
3 4 20
最佳路径为:1 --> 4 --> 2 --> 3 --> 1
最短距离为:25

注意:

  • 不同于最短路径,这里我们把**INF(即无路径连通)与0(即自身)**放在一起处理,因为他们都不需要swap。

  • 我们用t==n,而不是t>=n,是为了防止数组下表越界

4. 总结

  • 回溯法作为一种极暴力的搜索法,其时间复杂度是极高的,子集树大概是2n,排序树大概是n!,所以处理大的问题不太给力。但作为回报,它能给出真正的最优解。

  • 回溯法的子集树和排序树,可以处理两类问题,求子集最优和排序最优。

  • 想要利用剪枝函数优化是非常困难的。

参考文章:程序猿声:【算法学习】再谈回溯法

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

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

相关文章

项目一点点记录

kafka发布通知 kafka是消息队列&#xff0c;kafka采用发布订阅模式进行消息的生产与消费。在项目中&#xff0c;我们采用spring来整合kafka&#xff0c; 通过定义事件event来封装 点赞、关注、评论三类事件&#xff0c;event实体中有 事件主题topic&#xff0c;当前用户id&…

怎么给PDF添加图片水印?其实很简单,看这篇就会了!

许多人都意识到版权问题的重要性&#xff0c;尽管在日常生活中我们可能很少遇到&#xff0c;但在办公和学习中却经常涉及到此类问题。例如&#xff0c;我们辛辛苦苦制作的PDF文件&#xff0c;如何确保不被他人盗用呢?这就涉及到如何为PDF添加图片水印的问题&#xff0c;相当于…

无向图G的广度优先搜索和深度优先搜索以及完整程序

图的遍历算法有两种&#xff1a;广度优先搜索和深度优先搜索 一.广度优先搜索类似于层次遍历&#xff0c;需要借助辅助队列 空间复杂度为O(|V|);空间复杂度由辅助队列大小决定 时间复杂度为O(|V||E|) 为避免同一顶点被多次访问&#xff0c;设计visited[]来标记顶点 二.深度…

MyBatis 从初识到掌握

目录 今日良言&#xff1a;与其抱怨于黑暗&#xff0c;不如提灯向前行 一、初识MyBatis 1.MyBatis定义 2.为什么要学习MyBatis 3.MyBatis的创建 二、MyBatis的相关操作 1.增删改查操作 2.动态SQL使用 今日良言&#xff1a;与其抱怨于黑暗&#xff0c;不如提灯向前行 一…

UE4/5 通过Control rig的FullBody【蜘蛛模型,不用basic ik】

目录 根设置 FullBody IK 额外骨设置 ​编辑 晃动效果 根设置 第一步你需要准备一个蜘蛛模型&#xff0c;不论是官方示例或者是epic上购买的模型 然后我用的是epic上面购买的一个眼球蜘蛛&#xff1a; 第一步&#xff0c;我们从根创建一个空项【这个记得脱离父子级到root之…

SQLServer 2016 R2数据库新建、附加、分离、备份、还原、复制等基本操作

一、打开Microsoft SQL Server Management Studio 在桌面上找到图标&#xff0c;双击运行 打开Microsoft SQL Server Management Studio 17 输入服务器名称&#xff0c;选择SQL Server 身份验证&#xff0c;sa和sa密码&#xff0c;可以勾选记住密码&#xff0c;以便以后的登录…

分享基于安卓项目的单元测试总结

前言&#xff1a; 负责公司的单元测试体系的搭建&#xff0c;大约有一两个月的时间了&#xff0c;从最初的框架的调研&#xff0c;到中期全员的培训&#xff0c;以及后期对几十个项目单元测试的引入和推进&#xff0c;也算是对安卓的单元测试有了一些初步的收获以及一些新的认…

【雕爷学编程】Arduino动手做(131)---跑马灯矩阵键盘模块

37款传感器与执行器的提法&#xff0c;在网络上广泛流传&#xff0c;其实Arduino能够兼容的传感器模块肯定是不止这37种的。鉴于本人手头积累了一些传感器和执行器模块&#xff0c;依照实践出真知&#xff08;一定要动手做&#xff09;的理念&#xff0c;以学习和交流为目的&am…

线性代数基础--矩阵

矩阵 矩阵是由排列在矩形阵列中的数字或其他数学对象组成的表格结构。它由行和列组成&#xff0c;并且在数学和应用领域中广泛使用。 基本概念 元素&#xff1a;矩阵中的每个数字称为元素。元素可以是实数、复数或其他数学对象。 维度&#xff1a;矩阵的维度表示矩阵的行数和…

vtk创建点

使用vtk库创建三维空间中的点 引言开发环境示例一项目结构实现代码 运行效果示例二项目结构实现代码 运行效果总结 引言 本文仅适合初学者。 本文不提供vtk动态库的生成&#xff0c;以及在QtCreator中的引进vtk时的配置。 本文先由示例一开始&#xff0c;然后再在示例一的基础…

aws使用外部 ID对其他账号授权

点击前往授权,进入控制台 https://signin.aws.amazon.com/signin?redirect_urihttps%3A%2F%2Fconsole.aws.amazon.com%2Fconsole%2Fhome%3FhashArgs%3D%2523%26isauthcode%3Dtrue%26state%3DhashArgsFromTB_eu-north-1_f2d9c316b93c0026&client_idarn%3Aaws%3Asignin%3A%…

Glassdoor美国公司员工及面试者评价数据

一、数据简介 除了股东、债权人、政府等外部利益相关者外&#xff0c;员工的利益更应该得到公司的恰当保护&#xff0c;因为员工才是公司创造价值的真正主体。提高企业在产品市场的竞争力&#xff0c;首先就是要提高员工对企业的满意度&#xff0c;只有员工的满意度更高、幸福感…

7个技巧,助你同时轻松管理和跟踪多个项目

仅仅想到要兼顾这么多重要的职责&#xff0c;就会让许多专业的项目经理感到焦虑。当涉及多个项目的多种项目管理工具的处理&#xff0c;即使对于了解项目管理的项目经理来说&#xff0c;也是一项艰巨的任务&#xff0c;而对于在这个领域没有经过适当培训的人来说&#xff0c;这…

强化学习从基础到进阶--案例与实践[7.1]:深度确定性策略梯度DDPG算法、双延迟深度确定性策略梯度TD3算法详解项目实战

【强化学习原理项目专栏】必看系列&#xff1a;单智能体、多智能体算法原理项目实战、相关技巧&#xff08;调参、画图等、趣味项目实现、学术应用项目实现 专栏详细介绍&#xff1a;【强化学习原理项目专栏】必看系列&#xff1a;单智能体、多智能体算法原理项目实战、相关技巧…

计算机网络—数据链路层

文章目录 数据链路层服务差错编码多路访问协议信道划分随机访问MAC协议 数据链路层服务 该层中的帧数据结构&#xff1a; 帧头部会因为不同的局域网协议而不同&#xff0c;因此会在另一篇博文中继续介绍不同的帧数据报&#xff0c;不在本博文介绍。&#xff08;不过除了PPP协…

Docker学习笔记11

Docker容器镜像&#xff1a; 1&#xff09;docker client 向docker daemon发起创建容器的请求&#xff1b; 2&#xff09;docker daemon查找本地有客户端需要的镜像&#xff1b; 3&#xff09;如无&#xff0c;docker daemon则到容器的镜像仓库中下载客户端需要的镜像&#…

线性代数基础--向量

目录 向量的概念 基本概念 抽象概念 向量的意义 几何意义 物理意义 欧式空间 特点和性质 行向量与列向量 行向量 列向量 两者的关系 向量的基本运算与范数 向量的基本运算 向量的加法 数乘运算&#xff08;实数与向量相乘&#xff09; 转置 向量的范数 向量…

echart 设置柱状图y轴最大刻度

start 最近接到需求希望柱状图 y轴最大高度可以略高一些&#xff1b;柱状图的数据能展示在柱状图的上方 记录一下相关配置项 解决方案 官方文档说明 https://echarts.apache.org/zh/option.html#xAxis.max 效果 代码 {key: business,title: {text: 业务领域分类,textSt…

DAY32:回溯算法(七)全排列+全排列Ⅱ(排列问题)

文章目录 46.全排列思路树形图used数组的作用 伪代码完整版时间复杂度总结 47.全排列Ⅱ思路树形图 完整版时间复杂度总结 46.全排列 给定一个不含重复数字的数组 nums &#xff0c;返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。 示例 1&#xff1a; 输入&#xf…

C#和LABVIEW的对决:哪种上位机编程语言更适合你?

今天&#xff0c;我们将谈论主流的上位机编程语言。你听说过C#和LABVIEW吗&#xff1f;它们是的上位机编程语言&#xff0c;C#作为自动化主流编程语言特别受欢迎&#xff0c;LABVIEW用于自动化测试&#xff0c; 首先&#xff0c;我们来了解C#语言。C#是一种文本语言&#xff0c…