数据结构 | 图结构 | 最小生成树 | Kruskal Prim算法讲解

news2024/11/17 5:53:18

文章目录

    • 前言
    • Kruskal算法
    • Prim算法

前言

讲解之前,我们需要先明白连通图是指什么?连通图具有以一个顶点为起点可以到达该图中的任意一个顶点的特性,就算它们不直接相连,但是它们之间至少有一条可以递达的路径。并且连通图是针对无向图的,对于有向图,有个对应的概念——强连通图。而一张连通图的最小连通子图被称为生成树,因此生成树这个概念是对于无向图的,再细看这个概念,什么叫最小连通子图,这个最小体现在哪?一张连通图的连通子图不唯一,只要一张子图满足任意两顶点间连通的条件,它就是连通图的连通子图,从某种意义上说,连通图和连通子图拥有相同的顶点数量,这是一个默认前提,而最小就体现在:用最少的边数将图中的所有顶点连接,若图有n个顶点,那么最少可以用n-1个顶点连通这些顶点在这里插入图片描述
图片来自百度百科,上面这张图中,左上是一张连通图,该图有6个顶点,其他两张图是它的连通子图,它们用5条边将原图的6个顶点连接,使得任意两顶点间都存在一条路径能够递达,满足了连通的特性,所以它们是原图的连通子图,并且用了最少的边连接所有顶点,所以它们又是原图的最小连通子图,也叫生成树。继续补充生成树的概念:连通图的每一棵生成树都是原图的一个极大无环子图,也就是说生成树只要再加一条边,都将形成一条环路。但是只要任意删除一条边,生成树就不再满足连通的特性

这篇博客讲解的是最小生成树,那么这个最小又体现在哪呢?因为生成树也不唯一,从上面的例子就能看出来,我们可以用相同数量的边以不同的方式连接连通图所有的顶点,它们都是最小连通子图,也就是生成树。生成树的最小体现在边的数量最少,而最小生成树的最小体现在边的权值上,即数量相同的边,连接所有的顶点,但是这些边的权值加起来要最小。总结一下最小生成树的特征:

1,用n-1条边连接原图的n个顶点
2.这n-1条边的权值相加得到的值是所有可能中最小的
3.这n-1条边不能构成回路

Kruskal算法

理解了最小生成树的来由,现在的问题是怎样从不唯一的生成树中,找到一棵最小生成树?当然了,最小生成树也是不唯一的,数量相同的边,权值相加难免也会有相同的时候。

Kruskal算法使用了贪心思想,即每次从原图中挑一条权值最小的边,然后挑一条次小的,直到挑了n-1条边。此时最小生成树也就构建完成,这时就有一个问题,如果这n-1条边不能连接n个顶点怎么办?刚才说过了,对于生成树来说,少一条边就无法满足连接所有边的条件,所以我们要挑出n-1条边,不多不少,其次多一条边还会导致图中构成环路。反着理解,我们只要在挑选边的过程中保证每次添加的边不会使图构成环路,那么最后挑出的n-1条边就不会构成环路,并且保证这些边的权值最小,这不就构成了最小生成树吗?所以现在的问题就变成了要怎么保证每次选择的边不会构成环路?我们可以将连通的顶点放在一个集合中,每次选择边时,如果边的两个顶点都在一个集合中,说明这两个顶点已经连通,再选择一条边连接它们,不就构成了环路吗?所以此时不能选这条边,一开始每个顶点都是一个集合,随着边的添加,顶点就逐渐的汇成一个集合,直到最后一条边的添加,两个集合合并成为了一个集合,也就是说该集合中的顶点都是连通的。对于选择不同集合的顶点的子算法,我们使用并查集结构实现,由于上篇博客已经实现了并查集,这里就不再讲解这个结构,但是因为我实现的并查集是针对泛型的,而这里我们只需要针对整数(用数组下标做映射),使用泛型反而有点复杂,所以我把简化后的并查集贴出来

#pragma once
#include <iostream>
#include <unordered_map>
#include <vector>
#include <utility>
using namespace std;

class UnionFindSet
{
public:
	UnionFindSet(size_t n) 
	{
		_ufs.resize(n, -1);
	}

	size_t find_root(size_t index)   // 查找index下标的根元素,返回其下标
	{
		size_t root = index;
		while (_ufs[root] >= 0)     // 根元素在并查集中的值是负数
		{
			root = _ufs[root];
		}

		while (_ufs[index] >= 0)
		{
			size_t parent = _ufs[index];  // 先保存其双亲,以对其双亲也进行路径压缩
			_ufs[index] = root;           // 路径压缩
			index = parent;
		}

		return root;
	}


	bool set_test(size_t data1, size_t data2)         // 判断两个元素是否在同一集合中
	{
		return find_root(data1) == find_root(data2); // 返回两元素的根元素下标是否相等
	}

	// 连接两个集合
	void set_union(size_t data1, size_t data2)
	{
		// 找到它们的根元素下标
		size_t root1 = find_root(data1);
		size_t root2 = find_root(data2);

		if (root1 != root2) // 不同集合才能合并
		{
			// 假设root1为根元素的集合元素个数更多,如果它的元素更少,交换
			// 注意根元素在并查集中存储的是负数
			if (_ufs[root1] > _ufs[root2])
			{
				swap(root1, root2);
			}

			_ufs[root1] += _ufs[root2]; // 数值的累加,维护集合的个数
			_ufs[root2] = root1;        // 将小集合作为大集合的子集,保存大集合根元素的下标
		}
	}

	size_t set_size() // 返回并查集中树的个数
	{
		// 遍历数组,只有有值小于0就说明它是一个根元素,树的个数加1
		size_t ret = 0;
		for (size_t i = 0; i < _ufs.size(); ++i)
		{
			if (_ufs[i] < 0)
			{
				ret++;
			}
		}
		return ret;
	}
private:
	vector<int> _ufs;				// 保存元素之间关系的数组
};

有了并查集,我们只需要在选择边时,判断边的两个顶点是否处于同一集合中,如果处于同一集合,不选择这条边。如果不处于,选择这条边并将这两个顶点放入同一集合,关于集合的操作我们通过并查集提供的接口实现。

而对于每次的选择,我们要怎么快速的选到最小权值的边呢?由于顶点之间的关系都是用邻接矩阵或者邻接表保存的(具体实现可以看这篇博客),这样的结构只适合用来查询,想要找出其中最小权值的边就会比较复杂,所以这里得额外地再使用一个结构,保存边的信息(起点和终点,权值),用来排序的数组也好,直接使用优先级队列创建小堆也行。这里我就使用优先级队列,排序其实也行,这个看具体使用场景。

struct edge
{
	size_t _srci;
	size_t _deti;
	W _w;

	edge(size_t srci, size_t deti, W w)
		:_srci(srci)
		, _deti(deti)
		, _w(w)
	{}

	bool operator>(const edge& x)
	{
		return _w > x._w;
	}
};

typedef graph<V, W, MAX_W, Direction> self;
const W& kruskal(self& min_tree) // 接收一个graph类型的对象引用,将其修改为最小生成树
{
	// 先初始化优先级队列
	priority_queue<edge, vector<W>, greater<W>> minque;
	// 遍历邻接矩阵,将图的边保存到优先级队列中
	// 但是要注意,由于生成树是无向图,所以只需要遍历一半的矩阵,另一半是重复的
	for (size_t i = 0; i < _matrix.size(); ++i)
	{
		// 注意j < i,控制只遍历一半的矩阵
		for (size_t j = 0; j < i; ++j)
		{
			if (_matrix[i][j] != MAX_W)
			{
				minque.push(edge(i, j, _matrix[i][j]));
			}
		}
	}
}

先给出一部分实现,因为优先级队列需要传仿函数,缺省的仿函数是less,建立的是大堆,其仿函数的实现大概是这样的

class <template T>
struct less
{
	bool operator()(const T& left, const T& right)
	{
		return left < right;
		// 如果是greater类
		// return left > right;
	}
};

可以看到它重载了()操作符,priority_queue使用时会用less类创建一个对象,比如

less compare;

遇到需要比较的情况,比如要比较left和right两个数,就使用compare对象

compare(left, right);

由于compare对象实现了()的重载,所以程序会调用这个重载函数,返回left < right的比较情况,如果这两个操作数是内置类型,int,char,那么left < right这个表达式是成立的,但是两个操作数是自定义类型呢?这个比较计算就无法进行,因此我们需要在自定义类型中对<操作符进行重载,以支持这个比较计算。所以我在edge中实现了operator>以支持edge类型的比较计算。以下是Kruskal的全部实现

struct edge
{
	size_t _srci;
	size_t _deti;
	W _w;

	edge(size_t srci, size_t deti, W w)
		:_srci(srci)
		, _deti(deti)
		, _w(w)
	{}

	bool operator>(const edge& x) const
	{
		return _w > x._w;
	}
};

typedef graph<V, W, MAX_W, Direction> self;
W kruskal(self& min_tree) // 接收一个graph类型的对象引用,将其修改为最小生成树
{
	// 先初始化优先级队列
	priority_queue<edge, vector<edge>, greater<edge>> minque;
	// 遍历邻接矩阵,将图的边保存到优先级队列中
	// 但是要注意,由于生成树是无向图,所以只需要遍历一半的矩阵,另一半是重复的
	for (size_t i = 0; i < _matrix.size(); ++i)
	{
		// 注意j < i,控制只遍历一半的矩阵
		for (size_t j = 0; j < i; ++j)
		{
			if (_matrix[i][j] != MAX_W)
			{
				minque.push(edge(i, j, _matrix[i][j]));
			}
		}
	}

	// 最小生成树的初始化,初始化矩阵,顶点集合与映射表
	min_tree._vertex = _vertex;
	min_tree._index_map = _index_map;
	min_tree._matrix.resize(_vertex.size());
	for (size_t i = 0; i < _vertex.size(); ++i)
	{
		min_tree._matrix[i].resize(_vertex.size(), MAX_W);
	}

	// 并查集的初始化
	UnionFindSet ufs(_vertex.size()); 
	// 返回值创建
	W ret = W();

	// 当并查集中只有一个集合,说明所有的顶点都连接在了一起
	// 此时结束循环,最小生成树构建完成
	while (ufs.set_size() != 1)
	{
		// 取出最小的边
		edge min_edge = minque.top();
		minque.pop();
		// 判断边的顶点是否处于同一集合中
		if (ufs.set_test(min_edge._srci, min_edge._deti) == false)
		{
			// 若不在一个集合中,添加这条边然后将两顶点放入同一集合
			min_tree._add_edge(min_edge._srci, min_edge._deti, min_edge._w);
			ufs.set_union(min_edge._srci, min_edge._deti);
			ret += min_edge._w;
		
			// for test
			cout << _vertex[min_edge._srci] << "->" << _vertex[min_edge._deti] << ':' << _matrix[min_edge._srci][min_edge._deti] << endl;
		}
	}
	return ret;
}

其中使用到了上篇博客模拟实现的图结构,但是有一个接口发生了变化add_edge,该接口接收两个顶点的值,与边的权值,将顶点的值转换成下标后将两点连接,由于Kruskal算法中已经算出了顶点映射的下标,所以这里不用调该接口,我们将其结构进一步封装,得到一个接收顶点下标的接口_add_edge,使程序调用该接口就好了

void _add_edge(size_t srci, size_t deti, const W& w)
{
	// 检查邻接矩阵是否初始化,因为默认构造函数并不会初始化矩阵
	if (_matrix.size() != _vertex.size())
	{
		_matrix.resize(_vertex.size());
		for (size_t i = 0; i < _matrix.size(); ++i)
		{
			// 用最大值初始化矩阵
			_matrix[i].resize(_vertex.size(), MAX_W);
		}
	}

	_matrix[srci][deti] = w;
	// 如果是无向图,镜像也要添加边
	if (Direction == false)
	{
		_matrix[deti][srci] = w;
	}
}

// 边的添加
void add_edge(const V& src, const V& det, const W& w)
{
	// 需要进入邻接表,将顶点转换成下标
	size_t src_index = get_index(src);
	size_t det_index = get_index(det);

	// 顶点存在的判断
	if (src_index == -1 || det_index == -1)
	{
		throw invalid_argument("顶点不存在");
	}

	_add_edge(src_index, det_index, w);
}

在这里插入图片描述
最后是测试程序,先给出一个连通图,我们要找出该连通图的最小生成树
在这里插入图片描述

#include "Graph.hpp"
#include "UnionFindset.hpp"

int main()
{
	matrix::graph<char, int, INT_MAX, false> g;
	g.add_tex('a');
	g.add_tex('b');
	g.add_tex('c');
	g.add_tex('d');
	g.add_tex('e');
	g.add_tex('f');
	g.add_tex('g');
	g.add_tex('h');
	g.add_tex('i');

	g.add_edge('a', 'b', 4);
	g.add_edge('a', 'h', 8);
	g.add_edge('b', 'h', 11);
	g.add_edge('b', 'c', 8);
	g.add_edge('h', 'i', 7);
	g.add_edge('i', 'c', 2);
	g.add_edge('h', 'g', 1);
	g.add_edge('i', 'g', 6);
	g.add_edge('c', 'f', 4);
	g.add_edge('g', 'f', 2);
	g.add_edge('c', 'd', 7);
	g.add_edge('d', 'f', 14);
	g.add_edge('d', 'e', 9);
	g.add_edge('e', 'f', 10);

	matrix::graph<char, int, INT_MAX, false> min_tree;
	size_t ret = g.kruskal(min_tree);
	cout << "最小生成树的权值:" << ret << endl;
	return 0;
}

在这里插入图片描述
(图片来自《算法导论》)我在添加边时将这条边的起点,终点以及权值进行了打印,读者可以通过打印的顺序与图片进行对照,看看两者构建的最小生成树是否相同

可以看到最小生成树不是唯一的,《算法导论》给出的结果选择了a->h这条边,而我们的算法选择了b->c这条边,虽然边不一样,但是权值是相同的,不影响程序最后的结果。经过测试Kruskal算法实现完成

Prim算法

与Kruskal的思想类似,它们都是贪心思想,不同的是两者贪心的范围不同,Kruskal的贪心是从全局入手,每次选择的都是整张图中权值最小的边,而Prim呢?Prim从局部入手,这个局部就体现在Prim需要接收一个顶点值,作为算法的起点,从这个顶点入手,从依附于该顶点的边中选择权值最小的那个。所以说Prim的贪是着眼于当前,从当前拥有的顶点出发,从依附于它们的边中选择权值最小的那个,而Kruskal的贪则是着眼于全局,从所有的未选择的边中选择权值最小的那个,这就是两个算法的本质区别。

因为Prim是从依附于当前顶点的边中选择权值最小的那个,那么Prim所拥有的顶点与依附于该顶点的边就需要被记录,这里可以使用一个bool数组记录已经拥有的顶点,可以用优先级队列存储依附于当前顶点的边。一开始,以调用者传入的顶点为起点,将顶点值转换成数组下标,修改bool数组中该下标的bool值为true,表示当前连接了这个顶点,然后遍历邻接矩阵(邻接表)找出依附于该顶点的边并放入优先级队列中,与Kruskal一样,优先级队列是一个小堆,此时Prim的初始化工作完成。要注意的是:虽然无向图的边没有方向,但是我们可以人为的指定方向(注意这只是为了方便算法的实现),以拥有的顶点为起点,向外的顶点(未拥有的顶点)为终点。

初始化完成后,我们选择堆顶的边(局部权值最小),添加到最小生成树上,但是要注意保证权值最小的同时,这条边的终点不能是我们已经拥有的顶点,否则将构成环路。边的起点是我们已经拥有的顶点,边的终点是我们未拥有的顶点,在这个前提下保证边的权值是最小的。就这样我们从一个顶点开始一步步的向外连接其他顶点,直到所有顶点被连接,此时的边的数量为n-1,根据这个条件我们结束循环,最小生成树的构建也随之完成了

W Prim(self& min_tree, const V& start)
{
	// 先将顶点值转换为下标
	size_t starti = get_index(start);
	if (starti == -1)
	{
		throw invalid_argument("顶点不存在");
	}

	// 最小生成树的初始化,初始化矩阵,顶点集合与映射表
	min_tree._vertex = _vertex;
	min_tree._index_map = _index_map;
	min_tree._matrix.resize(_vertex.size());
	for (size_t i = 0; i < _vertex.size(); ++i)
	{
		min_tree._matrix[i].resize(_vertex.size(), MAX_W);
	}

	// bool数组的构建与初始化
	size_t vertex_size = _vertex.size();
	vector<bool> vertex_had(vertex_size, false);
	// 起点的标记
	vertex_had[starti] = true;

	// 创建优先级队列,存储当前可以选择的边
	priority_queue<edge, vector<edge>, greater<edge>> min_que;
	// 优先级队列初始化
	for (size_t i = 0; i < vertex_size; ++i)
	{
		// 将依附于顶点的边存储到优先级队列
		if (_matrix[starti][i] != MAX_W)
		{
			min_que.push(edge(starti, i, _matrix[starti][i]));
		}
	}
	// 至此初始化完成,要开始循环以构建最小生成树了
	
	// 一些关键变量的创建
	size_t finish = 0;
	W ret = W();
	// 当还未连接所有顶点时,重复循环
	while (finish != vertex_size - 1)
	{
		// 拿到最小权值边
		edge min_edge = min_que.top();
		min_que.pop();
		// 判断可以选择的最小权值边是否构成了环路
		if (!vertex_had[min_edge._deti])
		{
			// 未构成环路,为最小生成树添加这条边
			min_tree._add_edge(min_edge._srci, min_edge._deti, min_edge._w);
			vertex_had[min_edge._deti] = true;
			// 循环变量的维护
			finish++;
			// 返回值的维护,权值的记录
			ret += min_edge._w;

			// for test
			cout << _vertex[min_edge._srci] << "->" << _vertex[min_edge._deti] << ':' << _matrix[min_edge._srci][min_edge._deti] << endl;
		}
		// 若构成环路,直接进行下次最小权值边的选择
		else
		{
			continue;
		}
		// 添加依附于新拥有顶点的边
		// 注意添加的边的起点是新拥有的顶点,终点是未拥有的顶点
		for (size_t i = 0; i < vertex_size; ++i)
		{
			if (_matrix[min_edge._deti][i] != MAX_W && !vertex_had[i])
			{
				min_que.push(edge(min_edge._deti, i, _matrix[min_edge._deti][i]));
			}
		}
	} // end of while
	return ret;
}

以上是Prim算法的实现,由于之前已经讲解过了大致的逻辑,并且代码也有详细的注释,这里就不再赘述其中的细节,直接进行程序的测试

#include "Graph.hpp"
#include "UnionFindset.hpp"

int main()
{
	matrix::graph<char, int, INT_MAX, false> g;
	g.add_tex('a');
	g.add_tex('b');
	g.add_tex('c');
	g.add_tex('d');
	g.add_tex('e');
	g.add_tex('f');
	g.add_tex('g');
	g.add_tex('h');
	g.add_tex('i');

	g.add_edge('a', 'b', 4);
	g.add_edge('a', 'h', 8);
	g.add_edge('b', 'h', 11);
	g.add_edge('b', 'c', 8);
	g.add_edge('h', 'i', 7);
	g.add_edge('i', 'c', 2);
	g.add_edge('h', 'g', 1);
	g.add_edge('i', 'g', 6);
	g.add_edge('c', 'f', 4);
	g.add_edge('g', 'f', 2);
	g.add_edge('c', 'd', 7);
	g.add_edge('d', 'f', 14);
	g.add_edge('d', 'e', 9);
	g.add_edge('e', 'f', 10);

	matrix::graph<char, int, INT_MAX, false> min_tree;
	size_t ret = g.Prim(min_tree, 'a');
	cout << "最小生成树的权值:" << ret << endl;
		
	return 0;
}

在这里插入图片描述

与Kruskal算法举的例子一样,这次我们用Prim算法求其最小生成树,读者可以通过打印的顺序体会Prim算法的实现过程。


至此最小生成树的两种算法讲解完成,虽然实现过程略有些复杂,但是两种的指导思想——贪心,却很好理解,所以在复杂算法的实现之前,我们需要考虑清楚算法的大概实现逻辑,将贪心的思想具体化,最后才是写代码完成细节的实现

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

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

相关文章

Mysql 中的日期时间函数汇总

日期和时间函数MySQL中内置了大量的日期和时间函数&#xff0c;能够灵活、方便地处理日期和时间数据&#xff0c;本节就简单介绍一下MySQL中内置的日期和时间函数。1 CURDATE()函数CURDATE()函数用于返回当前日期&#xff0c;只包含年、月、日部分&#xff0c;格式为YYYY-MM-D…

【Unity3D小工具】Unity3D中实现仿真时钟、表盘、仿原神时钟

推荐阅读 CSDN主页GitHub开源地址Unity3D插件分享简书地址我的个人博客 大家好&#xff0c;我是佛系工程师☆恬静的小魔龙☆&#xff0c;不定时更新Unity开发技巧&#xff0c;觉得有用记得一键三连哦。 一、前言 今天实现一个时钟工具&#xff0c;其实在之前已经完成了一个简…

P3366 【模板】最小生成树

题目描述 如题&#xff0c;给出一个无向图&#xff0c;求出最小生成树&#xff0c;如果该图不连通&#xff0c;则输出 orz。 输入格式 第一行包含两个整数 &#xfffd;,&#xfffd;N,M&#xff0c;表示该图共有 &#xfffd;N 个结点和 &#xfffd;M 条无向边。 接下来 &…

【K8S系列】Pod重启策略及重启可能原因

目录 1 重启策略 1.1 Always 1.2 OnFailure 1.3 Nerver 1.4 yaml示例 2 Pod常见异常状态 2.1 Pending状态 2.2 Waiting/ContainerCreating状态 2.3 CrashLoopBackOff状态 2.4 ImagePullBackOff状态 2.5 Error状态 2.6 其他状态说明 tips: 3.自动重启的可能原…

【HBase——陌陌海量存储案例】5. Apache Phoenix快速入门

6.3 快速入门 6.3.1 需求 本次的小DEMO&#xff0c;我们沿用之前的订单数据集。我们将使用Phoenix来创建表&#xff0c;并进行数据增删改查操作。 列名说明id订单IDstatus订单状态money支付金额pay_way支付方式IDuser_id用户IDoperation_time操作时间category商品分类 6.3.…

Ruoyi-Cloud框架学习-【06 新增业务项目】

新建模块 结构查看 新增子模块&#xff0c;按照若依赖的约定&#xff0c;避免后续出现问题&#xff0c;当然一通百通&#xff0c;也可以按照自己的思路配置&#xff0c;修改对应的配置即可。 后端项目结构如下&#xff0c;通过观察&#xff0c;我们需要新增自己的模块在ruoy…

git reset

reset三种模式区别和使用场景区别&#xff1a;--hard&#xff1a;重置位置的同时&#xff0c;直接将 working Tree工作目录、 index 暂存区及 repository 都重置成目标Reset节点的內容,所以效果看起来等同于清空暂存区和工作区。--soft&#xff1a;重置位置的同时&#xff0c;保…

Java后端项目排错经验分享

导致错误的原因有很多&#xff0c;最常见的无非也就那么几种&#xff1a; 1、粗心导致的格式问题以及代码多写少写错写字母的问题 2、代码逻辑问题 3、框架版本不匹配问题 无论是哪种问题&#xff0c;排查错误的方式最好最便捷的方式有调试日志&#xff0c;那么如果是线上问…

【头歌】循环队列及链队列的基本操作

第1关&#xff1a;循环队列的基本操作任务描述本关任务是实现循环队列的基本操作函数&#xff0c;以实现判断队列是否为满、是否为空、求队列元素个数、进队和出队等功能。相关知识队列的基本概念队列&#xff08;简称队&#xff09;也是一种运算受限的线性表&#xff0c;在这种…

Kubernetes集群搭建 (未完待续)

Kubernetes集群搭建 目录 前言前期准备K8S集群安装 虚拟机设置安装K8S集群k8s部署Nginx 附录1 Docker安装附录2 yum k8s 问题附录3 k8s start问题附录4 k8s master init 前言 本文指定Docker与K8s版本&#xff0c;保证兼容性&#xff0c;可供参考 Docker‐ce‐3:19.03.9‐3…

推荐7个高质量图片素材网站

这期给大家分享7个免费、高质量图片素材网站&#xff0c;而且无版权&#xff0c;不用担心侵权&#xff01;建议收藏。 1、潮点视频 https://shipin520.com/shipin-tp/0-1329-0-0-0-0-0-0-0-1.html?from_code2510 网站主要以提供高质量视频素材为主&#xff0c;但也有大量的配…

基于FPGA的啸叫检测与抑制系统设计与实现verilog

针对啸叫的问题,本文设计了一种基于移频算法的啸叫抑制器。采用高性能音频编解码芯片对音频信号采样,移频器对采样所得的音频信号进行移频,移频频率范围为0~8Hz,再用音频芯片输出,采用FFT算法计算出啸叫点频率值,显示在LCD1602上,此移频器可达到快速有效的啸叫抑制效果。…

【JVM】垃圾回收之对象已死

文章目录1. 概述2. 引用计数算法3. 可达性分析算法4. 引用的分类4.1 软引用的应用本文参考&#xff1a;深入理解Java虚拟机&#xff1a;JVM高级特性与最佳实践&#xff08;第3版&#xff09; 1. 概述 总所周知&#xff0c;垃圾收集器的任务就是将“死去”的对象回收。 那么&a…

自动驾驶感知——毫米波雷达

文章目录1. 雷达的基本概念1.1 毫米波雷达分类1.2 信息的传输1.3 毫米波雷达的信号频段1.4 毫米波雷达工作原理1.4.1 毫米波雷达测速测距的数学原理1.4.2 毫米波雷达测角度的数学原理1.4.3 硬件接口1.4.4 关键零部件1.4.5 数据的协议与格式1.5 车载毫米波雷达的重要参数1.6 车载…

94. BERT以及BERT代码实现

1. NLP里的迁移学习 使用预训练好的模型来抽取词、句子的特征 例如word2vec 或语言模型 不更新预训练好的模型需要构建新的网络来抓取新任务需要的信息 Word2vec忽略了时序信息&#xff0c;语言模型只看了一个方向Word2vec只是抽取底层的信息&#xff0c;作为embedding层&…

数据太多?3款免费数据分析软件,分分钟解决

本文分享下我在做数据分析时用过的几个简单易上手的数据可视化软件。 先放上目录&#xff1a; 数据统计收集类——简道云数据图表美化类——图表秀数据开发类——Echart 01 简道云 https://www.jiandaoyun.com/ 适用于&#xff1a;想要“简单易上手”适合业务人员&#xff0…

TF-A源码移植的过程

1.解压标准 tf-a 源码包&#xff1a; tar xfz tf-a-stm32mp-2.2.r2-r0.tar.gz 2.将 ST 官方补丁文件打到 tf-a 源码中&#xff1a; 3.配置交叉编译工具链&#xff1a; 进入~/FSMP1A/tf-a-stm32mp-2.2.r2-r0$ 目录下&#xff0c;打开Makefile.sdk 将如下内容进行更改 4.复制设…

【前端设计】监控顺序返回型总线超时的计时器模块设计

前言 总线超时检查机制是系统中必要的模块设计&#xff0c;用于在总线无法返回response时能够及时上报中断。从理论上分析&#xff0c;如果总线发生了诸如挂死或者物理损坏等超时行为&#xff0c;无论计时器上报timeout的时间偏大还是偏小&#xff0c;都是一定可以上报中断的。…

Xilinx MicroBlaze系列教程(适用于ISE和Vivado开发环境)

本文是Xilinx MicroBlaze系列教程的第0篇文章。这个系列文章是我个人最近两年使用Xilinx MicroBlaze软核的经验和笔记,以Xilinx ISE 14.7和Spartan-6,以及Vivado 2018.3和Artix-7为例,介绍MicroBlaze软核、AXI系列IP核的软硬件使用,希望能帮助到更多的人。 MicroBlaze是Xil…

什么是有限元分析?能用来干什么

您是否想过工程师和制造商如何测试他们设计的耐用性、强度和安全性&#xff1f;如果您看过汽车广告&#xff0c;您可能会相信工程师和设计师不断地破坏他们的产品以测试其强度。您可能会得出结论&#xff0c;制造商会重复此过程&#xff0c;直到设计能够承受巨大的损坏并达到可…