【树形dp 换根法 BFS】2581. 统计可能的树根数目

本文涉及知识点

C++BFS算法
动态规划汇总
图论知识汇总
树形dp 换根法 BFS

LeetCode 2581. 统计可能的树根数目

Alice 有一棵 n 个节点的树,节点编号为 0 到 n - 1 。树用一个长度为 n - 1 的二维整数数组 edges 表示,其中 edges[i] = [ai, bi] ,表示树中节点 ai 和 bi 之间有一条边。
Alice 想要 Bob 找到这棵树的根。她允许 Bob 对这棵树进行若干次 猜测 。每一次猜测,Bob 做如下事情:
选择两个 不相等 的整数 u 和 v ,且树中必须存在边 [u, v] 。
Bob 猜测树中 u 是 v 的 父节点 。
Bob 的猜测用二维整数数组 guesses 表示,其中 guesses[j] = [uj, vj] 表示 Bob 猜 uj 是 vj 的父节点。
Alice 非常懒,她不想逐个回答 Bob 的猜测,只告诉 Bob 这些猜测里面 至少 有 k 个猜测的结果为 true 。
给你二维整数数组 edges ,Bob 的所有猜测和整数 k ,请你返回可能成为树根的 节点数目 。如果没有这样的树,则返回 0。
示例 1:
输入:edges = [[0,1],[1,2],[1,3],[4,2]], guesses = [[1,3],[0,1],[1,0],[2,4]], k = 3
输出:3
解释:
根为节点 0 ,正确的猜测为 [1,3], [0,1], [2,4]
根为节点 1 ,正确的猜测为 [1,3], [1,0], [2,4]
根为节点 2 ,正确的猜测为 [1,3], [1,0], [2,4]
根为节点 3 ,正确的猜测为 [1,0], [2,4]
根为节点 4 ,正确的猜测为 [1,3], [1,0]
节点 0 ,1 或 2 为根时,可以得到 3 个正确的猜测。
示例 2:
输入:edges = [[0,1],[1,2],[2,3],[3,4]], guesses = [[1,0],[3,4],[2,1],[3,2]], k = 1
输出:5
解释:
根为节点 0 ,正确的猜测为 [3,4]
根为节点 1 ,正确的猜测为 [1,0], [3,4]
根为节点 2 ,正确的猜测为 [1,0], [2,1], [3,4]
根为节点 3 ,正确的猜测为 [1,0], [2,1], [3,2], [3,4]
根为节点 4 ,正确的猜测为 [1,0], [2,1], [3,2]
任何节点为根,都至少有 1 个正确的猜测。
提示:
edges.length == n - 1
2 <= n <= 105
1 <= guesses.length <= 105
0 <= ai, bi, uj, vj <= n - 1
ai != bi
uj != vj
edges 表示一棵有效的树。
guesses[j] 是树中的一条边。
guesses 是唯一的。
0 <= k <= guesses.length

换根法

某棵有根树,根为root,某个儿子为child,则将根从root换成child后,除 r o o t ↔ c h i l d root\leftrightarrow child rootchild这条的边的父子关系发生变化外,其它都不边。
mGuesss[x] 记录猜测次数:x=Mask(r,v) = u*n+v
x1 = Mask(root,child)
x2 = Mask(child,root)
则 dp[child] = dp[root] - mGuesss[x1] + mGuress[x2]
分三步:
一,令root为0,计算m_dp[0]。
二,dfs各节点计算m_dp[cur]。
三,统计m_dp中为k的元素数量。

动态规划的状态表示

m_dp[cur]表示以cur为根据猜对父子关系的数量。
空间复杂度: O(n)

动态规划的转移方程

dp[child] = dp[root] - mGuesss[x1] + mGuress[x2]
单个状态的转移方程时间复杂度:O(1) 总时间复杂度:O(n)

动态规划的初始值

dp[0]先计算

动态规划的填表顺序

深度优先,广度优先也可以。

动态规划的返回值

cout(dp.being(),dp.end(),k)

代码(超时)

核心代码

class CNeiBo
{
public:	static vector<vector<int>> Two(int n, vector<vector<int>>& edges, bool bDirect, int iBase = 0) {vector<vector<int>>  vNeiBo(n);for (const auto& v : edges){vNeiBo[v[0] - iBase].emplace_back(v[1] - iBase);if (!bDirect){vNeiBo[v[1] - iBase].emplace_back(v[0] - iBase);}}return vNeiBo;}	static vector<vector<std::pair<int, int>>> Three(int n, vector<vector<int>>& edges, bool bDirect, int iBase = 0){vector<vector<std::pair<int, int>>> vNeiBo(n);for (const auto& v : edges){vNeiBo[v[0] - iBase].emplace_back(v[1] - iBase, v[2]);if (!bDirect){vNeiBo[v[1] - iBase].emplace_back(v[0] - iBase, v[2]);}}return vNeiBo;}static vector<vector<int>> Grid(int rCount, int cCount, std::function<bool(int, int)> funVilidCur, std::function<bool(int, int)> funVilidNext){vector<vector<int>> vNeiBo(rCount * cCount);auto Move = [&](int preR, int preC, int r, int c){if ((r < 0) || (r >= rCount)){return;}if ((c < 0) || (c >= cCount)){return;}if (funVilidCur(preR, preC) && funVilidNext(r, c)){vNeiBo[cCount * preR + preC].emplace_back(r * cCount + c);}};for (int r = 0; r < rCount; r++){for (int c = 0; c < cCount; c++){Move(r, c, r + 1, c);Move(r, c, r - 1, c);Move(r, c, r, c + 1);Move(r, c, r, c - 1);}}return vNeiBo;}static vector<vector<int>> Mat(vector<vector<int>>& neiBoMat){vector<vector<int>> neiBo(neiBoMat.size());for (int i = 0; i < neiBoMat.size(); i++){for (int j = i + 1; j < neiBoMat.size(); j++){if (neiBoMat[i][j]){neiBo[i].emplace_back(j);neiBo[j].emplace_back(i);}}}return neiBo;}
};class Solution {
public:int rootCount(vector<vector<int>>& edges, vector<vector<int>>& guesses, int k) {m_c = edges.size() + 1;m_dp.resize(m_c);m_vNeiBo = CNeiBo::Two(m_c, edges, false);for (const auto& v : guesses) {m_mGuess[Mask(v[0], v[1])]++;}m_dp[0] = DFS1(0, -1);DFS2(0, -1);return count_if(m_dp.begin(), m_dp.end(), [&](int i) {return i >= k; });}int DFS1(int cur, int par) {int ret = 0;if (-1 != par) { ret += m_mGuess[Mask(par, cur)];}for (const auto& next : m_vNeiBo[cur]) {if (next == par) { continue; }ret += DFS1(next, cur);}return ret;}void DFS2(int cur, int par) {if (-1 != par) {m_dp[cur] = m_dp[par];m_dp[cur] -= m_mGuess[Mask(par, cur)];m_dp[cur] += m_mGuess[Mask(cur, par)];}	for (const auto& next : m_vNeiBo[cur]) {if (next == par) { continue; }DFS2(next, cur);}}long long Mask(long long par, int cur) { return m_c * par + cur; }int m_c;vector<int> m_dp;unordered_map<long long, int> m_mGuess;vector < vector <int>> m_vNeiBo;
};

单元测试

template<class T1, class T2>
void AssertEx(const T1& t1, const T2& t2)
{Assert::AreEqual(t1, t2);
}template<class T>
void AssertEx(const vector<T>& v1, const vector<T>& v2)
{Assert::AreEqual(v1.size(), v2.size());for (int i = 0; i < v1.size(); i++){Assert::AreEqual(v1[i], v2[i]);}
}template<class T>
void AssertV2(vector<vector<T>> vv1, vector<vector<T>> vv2)
{sort(vv1.begin(), vv1.end());sort(vv2.begin(), vv2.end());Assert::AreEqual(vv1.size(), vv2.size());for (int i = 0; i < vv1.size(); i++){AssertEx(vv1[i], vv2[i]);}
}namespace UnitTest
{vector<vector<int>> edges, guesses;int k;TEST_CLASS(UnitTest){public:TEST_METHOD(TestMethod0){edges = { {0,1},{1,2},{1,3},{4,2} }, guesses = { {1,3},{0,1},{1,0},{2,4} }, k = 3;auto res = Solution().rootCount(edges, guesses, k);AssertEx(3, res);}TEST_METHOD(TestMethod1){edges = { {0,1},{1,2},{2,3},{3,4} }, guesses = { {1,0},{3,4},{2,1},{3,2} }, k = 1;auto res = Solution().rootCount(edges, guesses, k);AssertEx(5, res);}TEST_METHOD(TestMethod2){edges ={ {1,0},{2,1},{2,3},{4,0},{5,2},{6,1},{0,7},{1,8},{9,6},{10,4},{11,10},{12,8},{8,13},{14,4},{15,9},{9,16},{3,17},{4,18},{6,19},{20,13},{21,20},{19,22},{23,3},{24,0},{25,14},{17,26},{27,3},{3,28},{29,3},{4,30},{31,9},{0,32},{33,12},{34,14},{27,35},{35,36},{37,33},{38,18},{6,39} };guesses ={ {13,8},{4,18},{37,33},{4,30},{1,8},{3,17},{25,14},{0,1},{27,35},{21,20},{6,1},{26,17},{1,2},{8,13},{22,19},{30,4},{4,0},{2,5},{14,4},{9,6},{19,22},{16,9},{5,2},{29,3},{34,14},{8,1},{11,10},{15,9},{10,4},{35,27},{3,27},{33,12},{14,34},{32,0},{14,25},{39,6},{7,0},{4,10},{0,32},{23,3},{20,21},{24,0},{0,7},{1,0},{3,28},{6,9},{8,12},{18,4},{1,6},{2,1},{2,3},{3,29},{9,16},{17,26},{35,36},{13,20},{10,11},{18,38},{3,23},{0,24},{33,37},{12,33},{3,2},{20,13},{17,3} };k =	29;auto res = Solution().rootCount(edges, guesses, k);AssertEx(40, res);}};
}

DFS非常容易超时

DFS稍稍复杂,leetcode就容易超时。
所以:
一,计算出临接表。
二,DFS各节点层次。
三,计算出各节点的孩子。
四,BFS各节点。由于每个节点顶多一个父亲,所以无需判断节点是否重复访问。

class CNeiBo
{
public:	static vector<vector<int>> Two(int n, vector<vector<int>>& edges, bool bDirect, int iBase = 0) {vector<vector<int>>  vNeiBo(n);for (const auto& v : edges){vNeiBo[v[0] - iBase].emplace_back(v[1] - iBase);if (!bDirect){vNeiBo[v[1] - iBase].emplace_back(v[0] - iBase);}}return vNeiBo;}	static vector<vector<std::pair<int, int>>> Three(int n, vector<vector<int>>& edges, bool bDirect, int iBase = 0){vector<vector<std::pair<int, int>>> vNeiBo(n);for (const auto& v : edges){vNeiBo[v[0] - iBase].emplace_back(v[1] - iBase, v[2]);if (!bDirect){vNeiBo[v[1] - iBase].emplace_back(v[0] - iBase, v[2]);}}return vNeiBo;}static vector<vector<int>> Grid(int rCount, int cCount, std::function<bool(int, int)> funVilidCur, std::function<bool(int, int)> funVilidNext){vector<vector<int>> vNeiBo(rCount * cCount);auto Move = [&](int preR, int preC, int r, int c){if ((r < 0) || (r >= rCount)){return;}if ((c < 0) || (c >= cCount)){return;}if (funVilidCur(preR, preC) && funVilidNext(r, c)){vNeiBo[cCount * preR + preC].emplace_back(r * cCount + c);}};for (int r = 0; r < rCount; r++){for (int c = 0; c < cCount; c++){Move(r, c, r + 1, c);Move(r, c, r - 1, c);Move(r, c, r, c + 1);Move(r, c, r, c - 1);}}return vNeiBo;}static vector<vector<int>> Mat(vector<vector<int>>& neiBoMat){vector<vector<int>> neiBo(neiBoMat.size());for (int i = 0; i < neiBoMat.size(); i++){for (int j = i + 1; j < neiBoMat.size(); j++){if (neiBoMat[i][j]){neiBo[i].emplace_back(j);neiBo[j].emplace_back(i);}}}return neiBo;}
};class CDFSLeveChild
{
public:CDFSLeveChild(const vector<vector <int>>& vNeiBo,int root=0):m_vNeiBo(vNeiBo), Leve(m_vLeve){m_vLeve.resize(m_vNeiBo.size());DFS(root, -1);};const vector<int>& Leve;vector<vector<int>> Child() const{vector<vector <int>> vChild(m_vNeiBo.size());for (int i = 0; i < m_vNeiBo.size(); i++) {for (const auto& next : m_vNeiBo[i]) {if (m_vLeve[next] < m_vLeve[i]) { continue; }vChild[i].emplace_back(next);}}return vChild;}
protected:void DFS(int cur, int par) {if (-1 != par) { m_vLeve[cur] = m_vLeve[par] + 1; }for (const auto& next : m_vNeiBo[cur]) {if (next == par) { continue; }DFS(next, cur);}}vector<int> m_vLeve;const vector<vector <int>>& m_vNeiBo;
};
class Solution {
public:int rootCount(vector<vector<int>>& edges, vector<vector<int>>& guesses, int k) {m_c = edges.size() + 1;m_dp.resize(m_c);		m_vNeiBo = CNeiBo::Two(m_c, edges, false);		auto vChilds = CDFSLeveChild(m_vNeiBo).Child();for (const auto& v : guesses) {m_mGuess[Mask(v[0], v[1])]++;}for (int par = 0; par < m_c; par++) {for (int& child : vChilds[par]) {m_dp[0] += m_mGuess[Mask(par, child)];}}	queue<int> que;		que.emplace(0);while (que.size()) {int cur = que.front();que.pop();for (const auto& child : vChilds[cur]) {m_dp[child] = m_dp[cur];m_dp[child] -= m_mGuess[Mask(cur, child)];m_dp[child] += m_mGuess[Mask(child, cur)];que.emplace(child);}}return count_if(m_dp.begin(), m_dp.end(), [&](int i) {return i >= k; });}long long Mask(long long par, int cur) { return m_c * par + cur; }int m_c;vector<int> m_dp;unordered_map<long long, int> m_mGuess;vector < vector <int>> m_vNeiBo;	
};

进一步优化

可以用数组代码映射,算法方向,总共2n-2条边。假定根为0的树。
如果这条边是 子节点执行父节点,则此边数是child。如果方向相反则是n + child。
运行速度大约提高了20%。

class Solution {
public:int rootCount(vector<vector<int>>& edges, vector<vector<int>>& guesses, int k) {m_c = edges.size() + 1;m_dp.resize(m_c);	vector<int> vGuess(m_c * 2);m_vNeiBo = CNeiBo::Two(m_c, edges, false);	CDFSLeveChild dfs(m_vNeiBo);auto vChilds = dfs.Child();auto Mask = [&](int par, int child) {if (dfs.Leve[par] < dfs.Leve[child]) {return child;}return par + m_c;};for (const auto& v : guesses) {vGuess[Mask(v[0], v[1])]++;}for (int par = 0; par < m_c; par++) {for (int& child : vChilds[par]) {m_dp[0] += vGuess[Mask(par, child)];}}	queue<int> que;		que.emplace(0);while (que.size()) {int cur = que.front();que.pop();for (const auto& child : vChilds[cur]) {m_dp[child] = m_dp[cur];m_dp[child] -= vGuess[Mask(cur, child)];m_dp[child] += vGuess[Mask(child, cur)];que.emplace(child);}}return count_if(m_dp.begin(), m_dp.end(), [&](int i) {return i >= k; });}int m_c;vector<int> m_dp;	vector < vector <int>> m_vNeiBo;	
};

DFS序+差分数组

root和它的某个后代childchild换根。则到 c h i l d c h i l d ↔ r o o t childchild\leftrightarrow root childchildroot这条路径上的边都反转。可以用差分数组。
childchild和它的祖先不是连续的,但他们的DFS序是连续的。
此方案不好理解,实现也不简单。备用。

扩展阅读

视频课程

先学简单的课程,请移步CSDN学院,听白银讲师(也就是鄙人)的讲解。
https://edu.csdn.net/course/detail/38771

如何你想快速形成战斗了,为老板分忧,请学习C#入职培训、C++入职培训等课程
https://edu.csdn.net/lecturer/6176

相关推荐

我想对大家说的话
《喜缺全书算法册》以原理、正确性证明、总结为主。
按类别查阅鄙人的算法文章,请点击《算法与数据汇总》。
有效学习:明确的目标 及时的反馈 拉伸区(难度合适) 专注
闻缺陷则喜(喜缺)是一个美好的愿望,早发现问题,早修改问题,给老板节约钱。
子墨子言之:事无终始,无务多业。也就是我们常说的专业的人做专业的事。
如果程序是一条龙,那算法就是他的是睛

测试环境

操作系统:win7 开发环境: VS2019 C++17
或者 操作系统:win10 开发环境: VS2022 C++17
如无特殊说明,本算法用**C++**实现。

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

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

相关文章

将知乎专栏文章转换为 Markdown 文件保存到本地

一、参考内容 参考知乎文章代码 | 将知乎专栏文章转换为 Markdown 文件保存到本地&#xff0c;利用代码为GitHub&#xff1a;https://github.com/chenluda/zhihu-download。 二、步骤 1.首先安装包flask、flask-cors、markdownify 2. 运行app.py 3.在浏览器中打开链接&…

嵌入式学习——数据结构(哈希、排序)——day50

1. 查找二叉树、搜索二叉树、平衡二叉树 2. 哈希表——人的身份证——哈希函数 3. 哈希冲突、哈希矛盾 4. 哈希代码 4.1 创建哈希表 4.2 5. 算法设计 5.1 正确性 5.2 可读性&#xff08;高内聚、低耦合&#xff09; 5.3 健壮性 5.4 高效率&#xff08;时间复杂度&am…

Visual Studio开发环境搭建

原文&#xff1a;https://blog.c12th.cn/archives/25.html Visual Studio开发环境搭建 测试&#xff1a;笔记本原装操作系统&#xff1a;Windows 10 家庭中文版 资源分享链接&#xff1a;提取码&#xff1a;qbt2 注意事项&#xff1a;注意查看本地硬盘是否够用&#xff0c;建议…

2. 数据结构分析即索引库的crud

1. 数据库脚本 DROP TABLE IF EXISTS tb_hotel; CREATE TABLE tb_hotel (id bigint(0) NOT NULL,name varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT COMMENT 酒店名称,address varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_090…

运营管理和服务支撑阶段

我前面的所有设备都部署好了&#xff0c;现在就需要运营管理和服务支撑 遇到问题了迅速解决&#xff0c;避免风险扩大 我们也可以给客户提供上面的服务&#xff0c;提高客户的预警能力&#xff0c;安全风险处理能力 我们不仅提供设备&#xff0c;还提供服务 我们公司成立了安…

【算法专题--链表】两两交换链表中的节点 -- 高频面试题(图文详解,小白一看就懂!!!)

目录 一、前言 二、题目描述 三、解题方法 ⭐双指针 -- 采用哨兵位头节点 &#x1f95d; 什么是哨兵位头节点&#xff1f; &#x1f34d; 解题思路 &#x1f34d; 案例图解 四、总结与提炼 五、共勉 一、前言 两两交换链表中的节点 这道题&#xff0c;可以说…

K8S - 在集群内反向代理外部资源 - headless service 的使用

在上一篇文章中 K8S - 理解ClusterIP - 集群内部service之间的反向代理和loadbalancer 介绍了 ClusterIP 的主要作用 : 在k8s 集群内部 代理 内部的多实例 service 但是ClusterIP 还有1个变种 -> 无头服务 (headless service) 它用于代理集群外部的 ip资源 当然代理外部资…

AI应用带你玩系列之SadTalker

前段时间我刷微信视频&#xff0c;我无意间点开了一个&#xff0c;画面缓缓展开&#xff0c;是一幅精致的水墨画&#xff0c;画中人物皆是古代装束&#xff0c;衣袂飘飘&#xff0c;仿佛能闻到墨香。然而&#xff0c;这宁静的画面突然被打破了&#xff0c;画中的人物开始动了起…

初识 SpringMVC,运行配置第一个Spring MVC 程序

1. 初识 SpringMVC&#xff0c;运行配置第一个Spring MVC 程序 文章目录 1. 初识 SpringMVC&#xff0c;运行配置第一个Spring MVC 程序1.1 什么是 MVC 2. Spring MVC 概述2.1 Spring MVC 的作用&#xff1a; 3. 运行配置第一个 Spring MVC 程序3.1 第一步&#xff1a;创建Mave…

鸿蒙开发系统基础能力:【@ohos.faultLogger (故障日志获取)】

故障日志获取 说明&#xff1a; 本模块首批接口从API version 8开始支持。后续版本的新增接口&#xff0c;采用上角标单独标记接口的起始版本。 导入模块 import faultLogger from ohos.faultLoggerFaultType 故障类型枚举。 系统能力&#xff1a; 以下各项对应的系统能力…

利用MSSQL模拟提权

点击星标&#xff0c;即时接收最新推文 本文选自《内网安全攻防&#xff1a;红队之路》 扫描二维码五折购书 利用MSSQL模拟提权 在MS SQL数据库&#xff0c;可以使用EXECUTE AS语句&#xff0c;以其他用户的上下文执行SQL查询。需要注意的是只有明确授予模拟&#xff08;Impers…

vuex的深入学习[基于vuex3]----篇(二)

store对象的创建 store的传递图 创建语句索引 创建vuex的语句为new Vuex.Store({…})Vuex的入口文件是index.js,store是index.js导出的store类store类是store.js文件中定义的。 Store的构造函数constructor 判断vuex是否被注入&#xff0c;就是将vue挂载在window对象上&am…

Java | Leetcode Java题解之第169题多数元素

题目&#xff1a; 题解&#xff1a; class Solution {public int majorityElement(int[] nums) {int count 0;Integer candidate null;for (int num : nums) {if (count 0) {candidate num;}count (num candidate) ? 1 : -1;}return candidate;} }

TLS握手中的RTT

文章目录 TLS 1.2 握手过程中的 RTT 次数TLS 1.3 1-RTT 初次TLS1.3 0-RTT 握手过程总结 TLS 1.2 握手过程中的 RTT 次数 TLS 1.2 握手通常需要2 RTT 才能完成。具体步骤如下&#xff1a; 第一次 RTT&#xff1a; 客户端发送 ClientHello&#xff1a;客户端生成一个随机数&…

26.3 Django路由层

1. 路由作用 在Django中, URL配置(通常称为URLconf)是定义网站结构的基础, 它充当着Django所支撑网站的目录. URLconf是一个映射表, 用于将URL模式(patterns)映射到Python的视图函数或类视图上. 这种映射机制是Django处理HTTP请求的基础, 它决定了当客户端发送请求时, Django如…

消息认证码解析

1. 什么是消息认证码 消息认证码(Message Authentication Code)是一种确认完整性并进行认证的技术&#xff0c;取三个单词的首字母&#xff0c;简称为MAC。 消息认证码的输入包括任意长度的消息和一个发送者与接收者之间共享的密钥&#xff0c;它可以输出固定长度的数据&#x…

AIGC-Animate Anyone阿里的图像到视频 角色合成的框架-论文解读

Animate Anyone: Consistent and Controllable Image-to-Video Synthesis for Character Animation 论文:https://arxiv.org/pdf/2311.17117 网页:https://humanaigc.github.io/animate-anyone/ MOTIVATION 角色动画的目标是将静态图像转换成逼真的视频&#xff0c;这在在线零…

ip地址怎么写才是的对的?合法ip地址正确的格式

IP地址怎么写才是的对的&#xff1f;在互联网的世界里&#xff0c;IP地址就像是我们生活中的门牌号码&#xff0c;它是每个设备在网络中的唯一标识。正确的书写IP地址对于确保网络通信的顺畅至关重要。本文将带您了解合法IP地址的正确格式与书写规范&#xff0c;并深入探讨其在…

css如何动态累计数字?

导读&#xff1a;css如何动态累计数字&#xff1f;用于章节目录的序列数生成&#xff0c;用css的计数器实现起来比 js方式更简单&#xff01; 伪元素 ::after ::before伪元素设置content 可以在元素的首部和尾部添加内容&#xff0c;我们要在元素的首部添加序列号&#xff0c…

Spring AI 介绍以及与 Spring Boot 项目整合

Spring AI 项目旨在简化使用 Spring Boot 开发包含人工智能功能的应用程序&#xff0c;提供抽象和支持多种模型提供商及矢量数据库提供商。 Spring AI 的功能特点 支持主流模型提供商&#xff1a;如 OpenAI、Microsoft、Amazon、Google 和 Huggingface 等。支持多种模型类型&a…