数据结构与算法(四) 广度优先搜索

news2024/10/3 4:39:10

本篇文章继续来学习广度优先搜索算法(Broad-First-Search,BFS)


1、本质

广度优先搜索本质上还是遍历整个搜索空间,找到给定问题的解

实际上也是一种暴力搜索算法,不过其中的实现细节和优化细节还是值得探讨的


与深度优先算法略有不同,广度优先搜索是同时推进各搜索路径的(雨露均沾)

下面以图的遍历作为例子,直观上感受下广度优先搜索是一个怎么样的搜索过程

在这里插入图片描述

假设初始节点为 1,目标节点为 9,则具体遍历步骤如下:

当前节点当前节点的相邻节点已被访问的节点
1 (初始)2341
234561234
56789123456
789 (目标)/123456789

2、核心

广度优先搜索的实现过程很容易理解,直观上感受就是每次从搜索边界向周围扩散一圈

更具体来说,我们需要维护一组节点,每次取出全部节点对其进行处理,然后将相邻的节点重新加入维护

直至遍历完所有节点、或者到达目标节点、或者满足结束条件时才停止上述过程


对标深度优先搜索,广度优先搜索也有几个核心的概念,分别是:

  • 当前节点集合:目前维护的节点集合,是上一步的相邻节点
  • 相邻节点集合:当前节点的相邻节点,是下一步的当前节点
  • 结束条件:表示遍历到哪些节点停止
  • 约束条件:表示哪些节点不可以遍历

对比深度优先搜索,广度优先搜索可以更快地找到抵达目标节点的最短路径

但是作为代价,广度优先搜索具有更高的空间复杂度,因此在使用过程中,需要根据实际情况进行选择


3、框架

广度优先搜索算法可以使用队列实现,大致过程描述如下

首先将初始节点加入队列,然后开始循环,直至队列为空

每次循环从队列取出所有节点并对其处理,然后再将每个节点的相邻节点加入队列

在这个过程中,还需要使用集合来标记已被处理过的元素,避免重复访问

具体的代码框架如下:

def bfs(初始节点):
	初始化步数为零
	初始化队列为空
	初始化集合为空

	将初始节点加入队列
	将初始节点加入集合

	while 队列不为空:

		for _ in 队列.长度():
        	元素 = 队列.出队()

			if  元素能满足结束条件:
				表示当前元素合法,返回对应结果
			if  元素不满足约束条件:
				表示当前元素非法,跳过当前元素

			for 相邻元素 in 元素.相邻元素():
				if  相邻元素不在集合中:
					队列.入队(相邻元素)
					集合.加入(相邻元素)

		步数 += 1

4、例题

(1)二叉树的层序遍历 | leetcode102

给定二叉树的根节点,返回节点值的层序遍历

class Solution {
public:
    vector<vector<int>> levelOrder(TreeNode* root) {
        // 特判
        vector<vector<int>> ans;
        if (!root) {
            return ans;
        }
        // 初始化队列
        queue <TreeNode*> q;
        q.push(root);
        // 每次取出队列中的所有节点进行处理
        // 然后把这些节点的左右节点加入队列
        // 直至队列为空时结束
        while (!q.empty()) {
            ans.push_back(vector<int>());
            int num = q.size();
            while (num-- > 0) {
                // 当前元素出队
                TreeNode* node = q.front(); q.pop();
                // 当前元素加入结果
                ans.back().push_back(node -> val);
                // 相邻元素入队
                if (node -> left ) { q.push(node -> left ); }
                if (node -> right) { q.push(node -> right); }
            }
        }
        // 返回结果
        return ans;
    }
};

(2)二叉树的最小深度 | leetcode111

给定二叉树的根节点,返回最小深度,最小深度是从根节点到最近叶子节点的最短路径上的节点数量

class Solution {
public:
    int minDepth(TreeNode* root) {
        // 特判
        if (!root) {
            return 0;
        }
        // 初始化步数
        int step = 1;
        // 初始化队列
        queue <TreeNode*> q;
        q.push(root);
        // 每次取出队列中的所有节点进行处理
        // 然后把这些节点的左右节点加入队列
        while (!q.empty()) {
            int num = q.size();
            while (num-- > 0) {
                // 当前元素出队
                TreeNode* node = q.front(); q.pop();
                // 如果满足结束条件,即到达叶节点
                // 那么返回对应结果
                if (
                    node -> left  == nullptr &&
                    node -> right == nullptr
                ) {
                    return step;
                }
                // 相邻元素入队
                if (node -> left ) { q.push(node -> left ); }
                if (node -> right) { q.push(node -> right); }
            }
            // 步数加一
            step++;
        }
        return -1;
    }
};

(3)二叉树的右侧视图 | leetcode199

给定二叉树的根节点,想象自己站在树的右侧,按照从顶部到底部的顺序,返回从右侧看到的节点值

class Solution {
public:
    vector<int> rightSideView(TreeNode* root) {
        // 特判
        vector<int> ans;
        if (!root) {
            return ans;
        }
        // 初始化队列
        queue <TreeNode*> q;
        q.push(root);
        // 每次取出队列中的所有节点进行处理
        // 然后把这些节点的左右节点加入队列
        // 直至队列为空时结束
        while (!q.empty()) {
            int num = q.size();
            while (num-- > 0) {
                // 当前元素出队
                TreeNode* node = q.front(); q.pop();
                // 如果当前节点符合要求,即该节点为最右侧节点
                // 那么将该节点加入结果
                if (num == 0) {
                    ans.push_back(node -> val);
                }
                // 相邻元素入队
                if (node -> left ) { q.push(node -> left ); }
                if (node -> right) { q.push(node -> right); }
            }
        }
        // 返回结果
        return ans;
    }
};

(4)围绕的区域 | leetcode130

给定一个 m × n 字符矩阵,由字符 XO 组成,找到所有被 X 围绕的 O,并且将这些 O 转变成 X

基本思路:

  1. 题目中说,任何边界上的 O 都不会填充为 X,因为这些 O 没有被 X 所围绕
  2. 换句话说,与边界相连的 O 都继续保持为 O,而其它的 O 则会被 X 所填充

解题思路:

  1. 找出边界上的 O
  2. 找出与上述的 O 相连的 O
  3. 将上述找到的 O 填充为 A
  4. 遍历矩阵,将 O 修改为 X,将 A 修改为 O

注意:

  1. 上述的第二步,既可以使用深度优先搜索实现,也可以使用广度优先搜索实现
  2. 另外,本题可通过直接修改数组作为访问标志,因此无需额外的哈希集合记录
// 广度优先搜索

class Solution {
public:
    const int dx[4] = {1, -1, 0, 0};
    const int dy[4] = {0, 0, 1, -1};

    void bfs(vector<vector<char>>& board, int x, int y, int m, int n) {
        // 特判
        if (board[x][y] != 'O') {
            return;
        }
        // 初始化队列
        queue<pair<int, int>> q;
        q.emplace(x, y);
        board[x][y] = 'A';
        // 依次取出队列中的节点进行处理
        // 把这些节点的相邻节点加入队列
        // 直至队列为空时结束
        while (!q.empty()) {
            // 当前元素出队
            int ax = q.front().first;
            int ay = q.front().second;
            q.pop();
            // 相邻元素入队
            for (int k = 0; k < 4; k++) {
                int bx = ax + dx[k];
                int by = ay + dy[k];
                // 判断相邻元素是否合法
                if  (
                    bx < 0 ||
                    by < 0 ||
                    bx >= m ||
                    by >= n ||
                    board[bx][by] != 'O'
                ) {
                    continue;
                }
                // 如果相邻元素合法,则入队
                q.emplace(bx, by);
                board[bx][by] = 'A';
            }
        }
    }

    void solve(vector<vector<char>>& board) {
        // 特判
        int m = board.size();
        if (m <= 2) {
            return;
        }
        int n = board[0].size();
        if (n <= 2) {
            return;
        }
        // 1. 找出边界上的 'O'
        // 2. 找出与上述的 'O' 相连的 'O' (广度优先搜索)
        // 3. 将上述找到的 'O' 填充为 'A'
        for (int i = 1; i <= m - 2; i++) {
            bfs(board, i, 0, m, n);
            bfs(board, i, n - 1, m, n);
        }
        for (int j = 0; j <= n - 1; j++) {
            bfs(board, 0, j, m, n);
            bfs(board, m - 1, j, m, n);
        }
        // 4. 遍历矩阵,将 'O' 修改为 'X',将 'A' 修改为 'O'
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (board[i][j] == 'O') {
                    board[i][j]  = 'X';
                }
                if (board[i][j] == 'A') {
                    board[i][j]  = 'O';
                }

            }
        }
    }
};
// 深度优先搜索

class Solution {
public:
    void dfs(vector<vector<char>>& board, int x, int y, int m, int n) {
        if  (
            x < 0 ||
            y < 0 ||
            x >= m ||
            y >= n ||
            board[x][y] != 'O'
        ) {
            return;
        }
        board[x][y] = 'A';
        dfs(board, x + 1, y, m, n);
        dfs(board, x - 1, y, m, n);
        dfs(board, x, y + 1, m, n);
        dfs(board, x, y - 1, m, n);
    }

    void solve(vector<vector<char>>& board) {
        // 特判
        int m = board.size();
        if (m <= 2) {
            return;
        }
        int n = board[0].size();
        if (n <= 2) {
            return;
        }
        // 1. 找出边界上的 'O'
        // 2. 找出与上述的 'O' 相连的 'O' (深度优先搜索)
        // 3. 将上述找到的 'O' 填充为 'A'
        for (int i = 1; i <= m - 2; i++) {
            dfs(board, i, 0, m, n);
            dfs(board, i, n - 1, m, n);
        }
        for (int j = 0; j <= n - 1; j++) {
            dfs(board, 0, j, m, n);
            dfs(board, m - 1, j, m, n);
        }
        // 4. 遍历矩阵,将 'O' 修改为 'X',将 'A' 修改为 'O'
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (board[i][j] == 'O') {
                    board[i][j]  = 'X';
                }
                if (board[i][j] == 'A') {
                    board[i][j]  = 'O';
                }

            }
        }
    }
};

(5)岛屿的数量 | leetcode200

给定一个 m × n 数字矩阵,由数字 01 组成,找到所有被 0 围绕的 1,并统计数量

// 广度优先搜索

class Solution {
public:
    const int dx[4] = {1, -1, 0, 0};
    const int dy[4] = {0, 0, 1, -1};

    void bfs(vector<vector<char>>& grid, int x, int y, int m, int n) {
        // 初始化队列
        queue<pair<int, int>> q;
        q.emplace(x, y);
        grid[x][y] = '0';
        // 依次取出队列中的节点进行处理
        // 把这些节点的相邻节点加入队列
        // 直至队列为空时结束
        while (!q.empty()) {
            // 当前元素出队
            int ax = q.front().first;
            int ay = q.front().second;
            q.pop();
            // 相邻元素入队
            for (int k = 0; k < 4; k++) {
                int bx = ax + dx[k];
                int by = ay + dy[k];
                // 判断相邻元素是否合法
                if  (
                    bx < 0 ||
                    by < 0 ||
                    bx >= m ||
                    by >= n ||
                    grid[bx][by] != '1'
                ) {
                    continue;
                }
                // 如果相邻元素合法,则入队
                q.emplace(bx, by);
                grid[bx][by] = '0';
            }
        }
    }

    int numIslands(vector<vector<char>>& grid) {
        int m = grid.size();
        int n = grid[0].size();
        // 遍历矩阵
        // 发现陆地之后,岛屿数量加一,并使用广度优先搜索将相邻的陆地标记
        int ans = 0;
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (grid[i][j] == '1') {
                    ans += 1;
                    bfs(grid, i, j, m, n);
                }
            }
        }
        return ans;
    }
};
// 深度优先搜索

class Solution {
public:
    void dfs(vector<vector<char>>& grid, int x, int y, int m, int n) {
        if  (
            x < 0 ||
            y < 0 ||
            x >= m ||
            y >= n ||
            grid[x][y] != '1'
        ) {
            return;
        }
        grid[x][y] = '0';
        dfs(grid, x + 1, y, m, n);
        dfs(grid, x - 1, y, m, n);
        dfs(grid, x, y + 1, m, n);
        dfs(grid, x, y - 1, m, n);
    }

    int numIslands(vector<vector<char>>& grid) {
        int m = grid.size();
        int n = grid[0].size();
        // 遍历矩阵
        // 发现陆地之后,岛屿数量加一,并使用深度优先搜索将相邻的陆地标记
        int ans = 0;
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (grid[i][j] == '1') {
                    ans += 1;
                    dfs(grid, i, j, m, n);
                }
            }
        }
        return ans;
    }
};

(6)打开转盘锁 | leetcode752

实际应用场景下的广度优先搜索,本质上是对字符串进行遍历

关键是找到当前节点的相邻节点,以及定义清楚元素要满足的约束条件

class Solution {
public:
    int openLock(vector<string>& deadends, string target) {
        // 初始化步数
        int step = 0;
        // 初始化队列
        queue <string> q;
        q.push("0000");
        // 初始化集合
        unordered_set <string> deadeds {deadends.begin(), deadends.end()};
        unordered_set <string> visited {"0000"};
        // 初始化辅助函数
        auto addOne = [](char x) -> char {
            return (x == '9' ? '0' : x + 1);
        };
        auto subOne = [](char x) -> char {
            return (x == '0' ? '9' : x - 1);
        };
        // 每次取出队列中的所有节点进行处理
        // 然后把这些节点的相邻节点加入队列
        // 直至队列为空时结束
        while (!q.empty()) {
            int num = q.size();
            while (num-- > 0) {
                // 当前元素出队
                string str = q.front(); q.pop();
                // 若元素不满足约束条件
                // 跳过当前元素
                if (deadeds.find(str) != deadeds.end()) {
                    continue;
                }
                // 若元素能满足结束条件
                // 返回对应结果
                if (str == target) {
                    return step;
                }
                // 相邻元素入队
                char chr;
                for (int i = 0; i < 4; i++) {
                    // 备份
                    chr = str[i];
                    // 第 i 个字符加 1 得到相邻元素,如果相邻元素合法,将其加入队列
                    str[i] = addOne(chr);
                    if (visited.find(str) == visited.end()) {
                        q.push(str);
                        visited.insert(str);
                    }
                    // 第 i 个字符减 1 得到相邻元素,如果相邻元素合法,将其加入队列
                    str[i] = subOne(chr);
                    if (visited.find(str) == visited.end()) {
                        q.push(str);
                        visited.insert(str);
                    }
                    // 还原
                    str[i] = chr;
                }
            }
            // 步数加一
            step++;
        }
        // 遍历完没找到目标
        return -1;
    }
};

(7)解滑动谜题 | leetcode773

咋一看是二维数组的广度优先搜索,实际上可以将其转化为字符串形式,本质上与例题六相似

解题的关键在于预处理,一是将二维数组转字符串,二是找到找到当前节点的相邻节点

class Solution {
public:
    int slidingPuzzle(vector<vector<int>>& board) {
        // 预处理
        string target = "123450";
        int m = board.size();
        int n = board[0].size();
        string input;
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                input += char(board[i][j] + '0');
            }
        }
        vector<vector<int>> neighbors = {{1, 3}, {0, 2, 4}, {1, 5}, {0, 4}, {1, 3, 5}, {2, 4}};
        // 初始化步数
        int step = 0;
        // 初始化队列
        queue <string> q;
        q.push(input);
        // 初始化集合
        unordered_set <string> visited;
        visited.insert(input);
        // 每次取出队列中的所有节点进行处理
        // 然后把这些节点的相邻节点加入队列
        // 直至队列为空时结束
        while (!q.empty()) {
            int num = q.size();
            while (num-- > 0) {
                // 当前元素出队
                string str = q.front(); q.pop();
                // 若元素能满足结束条件
                // 返回对应结果
                if (str == target) {
                    return step;
                }
                // 相邻元素入队
                int x = str.find('0');
                for (int y: neighbors[x]) {
                    // 交换
                    // 得到相邻元素
                    swap(str[x], str[y]);
                    // 如果相邻元素合法
                    // 将其加入队列
                    if (visited.find(str) == visited.end()) {
                        q.push(str);
                        visited.insert(str);
                    }
                    // 还原
                    swap(str[x], str[y]);
                }
            }
            // 步数加一
            step++;
        }
        // 遍历完没找到目标
        return -1;
    }
};

(8)单词接龙 | leetcode127

本质上也是字符串的遍历,与例题六相似,只不过增加了遍历的约束

class Solution {
public:
    int ladderLength(string beginWord, string endWord, vector<string>& wordList) {
        // 初始化步数
        int step = 1;
        // 初始化队列
        queue <string> q;
        q.push(beginWord);
        // 初始化集合
        unordered_set<string> words {wordList.begin(), wordList.end()};
        // 每次取出队列中的所有节点进行处理
        // 然后把这些节点的相邻节点加入队列
        // 直至队列为空时结束
        while (!q.empty()) {
            int num = q.size();
            while (num-- > 0) {
                // 当前元素出队
                string str = q.front(); q.pop();
                // 若元素能满足结束条件
                // 返回对应结果
                if (str == endWord) {
                    return step;
                }
                // 相邻元素入队
                char b;
                for (int i = 0; i < str.size(); i++) {
                    // 备份
                    b = str[i];
                    // 第 i 个字符修改为 a-z 得到相邻元素,如果相邻元素合法,将其加入队列
                    for (char c = 'a'; c <= 'z'; c++) {
                        str[i] = c;
                        if (words.find(str) != words.end()) {
                            q.push(str);
                            words.erase(str);
                        }
                    }
                    // 还原
                    str[i] = b;
                }
            }
            // 步数加一
            step++;
        }
        // 遍历完没找到目标
        return 0;
    }
};

5、优化

广度优先搜索还有一个常用的优化方法,那就是双向广度优先搜索

一般的广度优先搜索是从初始节点开始向周围扩散,直至遇到目标节点或满足结束条件时停止

而双向广度优先搜索是从初始节点和目标节点同时开始扩散,直至两者出现交集时停止


根据上面的描述,相信大家也能发现,双向广度优先搜索有其使用限制,那就是必须知道目标节点

如果一开始不知道目标节点,那么就无法使用这个优化方法


为什么双向广度优先搜索更好呢?或者说在什么场景下能够表现更好呢?

随着扩散步数的增加,所维护的节点集合也会随之增大,在这种情况下使用双向广度优先搜索更佳

双向广度优先搜索通过交替扩散初始节点和目标节点以减少维护的节点数量,大家看下图就能理解

在这里插入图片描述

实现上,双向广度优先搜索使用两个哈希集合分别维护两个从初始节点和目标节点扩散的节点集合

为什么不再使用队列了呢?这是为了方便判断两个节点集合是否存在交集

而在扩散两个节点集合时,可以选择每次扩散小的集合,而非轮流扩散,这也是一个小的优化策略


这里还是以例题六为例,给出双向广度优先搜索的代码

class Solution {
public:
    int openLock(vector<string>& deadends, string target) {
        // 初始化步数
        int step = 0;
        // 初始化队列
        // 改为用集合,方便判断是否有交集
        unordered_set <string> q1 {"0000"};
        unordered_set <string> q2 {target};
        // 初始化集合
        unordered_set <string> deadeds {deadends.begin(), deadends.end()};
        unordered_set <string> visited;
        // 初始化辅助函数
        auto addOne = [](char x) -> char {
            return (x == '9' ? '0' : x + 1);
        };
        auto subOne = [](char x) -> char {
            return (x == '0' ? '9' : x - 1);
        };
        // 每次取出队列中的所有节点进行处理
        // 然后把这些节点的相邻节点加入队列
        // 直至队列为空时结束
        while (
            !q1.empty() &&
            !q2.empty()
        ) {
            // 构造临时集合
            unordered_set <string> q0;
            // 扩散小的集合
            if (q1.size() > q2.size()) {
                unordered_set <string> q3 = q1;
                q1 = q2;
                q2 = q3;
            }
            // 取出当前元素
            for (string str: q1) {
                // 若元素不满足约束条件
                // 跳过当前元素
                if (deadeds.find(str) != deadeds.end()) {
                    continue;
                }
                // 若元素能满足结束条件
                // 返回对应结果
                if (q2.find(str) != q2.end()) {
                    return step;
                }
                // 更新集合
                visited.insert(str);
                // 相邻元素入队
                char chr;
                for (int i = 0; i < 4; i++) {
                    // 备份
                    chr = str[i];
                    // 第 i 个字符加 1 得到相邻元素,如果相邻元素合法,将其加入队列
                    str[i] = addOne(chr);
                    if (visited.find(str) == visited.end()) {
                        q0.insert(str);
                    }
                    // 第 i 个字符减 1 得到相邻元素,如果相邻元素合法,将其加入队列
                    str[i] = subOne(chr);
                    if (visited.find(str) == visited.end()) {
                        q0.insert(str);
                    }
                    // 还原
                    str[i] = chr;
                }
            }
            // 步数加一
            step++;
        }
        // 遍历完没找到目标
        return -1;
    }
};

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

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

相关文章

基于PHP+MySQL的大学生交友社交网站

近年来,大学生的数量在逐步的增加,为了能够让这些大学生有一个更好的交友环境,需要创建一个基于大学生的社交交友网站。这样可以拉近彼此大学生之间的感情,让他们可以更好的进行学习和交流。 PHP大学生交友社交网站通过PHP&#xff1a;MySQL进行开发,分为前台和后台两部分,通过…

线程的状态

Java中线程的状态是通过枚举类型Thread.State表示的 &#xff0c;通过打印这些枚举类型&#xff0c;就可以知道java中线程的状态有哪些 public class ThreadState {public static void main(String[] args) {for (Thread.State state : Thread.State.values()) {System.out.pr…

进销存管理系统是什么?有哪些功能?

对于2022年刚开始&#xff0c;但是可以的确的是禽流感在短时期内是不可能返回他们&#xff0c;作为虚拟店面批发商想勇往直前中&#xff0c;就必须要亲吻网络&#xff0c;把销售业务从实体店搬至线上去。 想突破现状&#xff0c;化解虚拟店面批发民营企业的存活问题&#xff0…

ES初使用记录——写入与查询数据

本周接到一个任务&#xff1a;定时统计订单表中的数据&#xff0c;将异常订单挑出来&#xff0c;放入ES中供统计页面从总数点击跳转过去进行列表展示。 一、配置ES 配置maven&#xff0c;注入template Resource private ElasticsearchTemplate elasticsearchTemplate; 二、…

一、OBS概述

1. 概述 obs官网git源码编译 2. 软件能力 输入&#xff1a;文本、图片、窗口、音视频及摄像头等 处理&#xff1a;视频及图片滤镜、音频混音等 输出&#xff1a;rtmp推流、本地录制(mp4)、音视频裸数据(pcm/yuv/rgb)等 3. 模块 a. core核心模块 libobs 加载、管理各个功能…

Ansys Zemax | 大功率激光系统的STOP分析2:如何进行光机械设计准备

大功率激光器广泛用于各种领域当中&#xff0c;例如激光切割、焊接、钻孔等应用中。由于镜头材料的体吸收或表面膜层带来的吸收效应&#xff0c;将导致在光学系统中由于激光能量吸收所产生的影响也显而易见&#xff0c;大功率激光器系统带来的激光能量加热会降低此类光学系统的…

性能测试面试题总结(答案全)

目录 1.什么是负载测试&#xff1f;什么是性能测试&#xff1f; 2.性能测试包含了哪些测试&#xff08;至少举出3种&#xff09; 3.简述性能测试的步骤 4.什么时候可以开始执行性能测试&#xff1f; 5.你如何在负载测试模式下执行功能测试&#xff1f; 6.响应时间和吞吐量…

时序数据库 InfluxDB

一、介绍 InfluxDB 是一个时间序列数据库&#xff0c;GO 编写的,旨在处理高写入和查询负载。InfluxDB 旨在用作涉及大量时间戳数据的任何用例的后备存储&#xff0c;包括 DevOps 监控、应用程序指标、物联网传感器数据和实时分析。 特点&#xff1a; 专门为时间序列数据编写的…

格式工厂安装与使用教程

格式工厂支持各种类型视频、音频、图片、word转pdf等多种格式的免费转换&#xff0c;是一款非常优秀的良心软件。 在电脑浏览器中打开下载地址http://www.pcgeshi.com/index.html , 单击"立即下载"按钮即可。 打开下载的文件&#xff0c;等待安装即可。&#xff08…

标记肽Bz-Pro-Phe-Arg-pNA、59188-28-2

血浆激肽释放酶&#xff0c;cruppain和胰蛋白酶的显色底物。编号: 140214 中文名称: 标记肽Bz-PFR-对硝基苯胺 英文名: Bz-Pro-Phe-Arg-pNA CAS号: 59188-28-2 单字母: Bz-PFR-pNA 三字母: Benzoyl-Pro-Phe-Arg-pNA 氨基酸个数: 3 分子式: C33H38O6N8 平均分子量: 642.7 精确分…

python 多线程编程(线程同步和守护线程)

守护线程&#xff1a; 随着主线程的终止而终止&#xff0c;不管当前主线程下有多少子线程没有执行完毕&#xff0c;都会终止。 线程同步&#xff1a; join所完成的工作就是线程同步&#xff0c;即主线程任务结束之后&#xff0c;进入阻塞状态&#xff0c;一直等待其他的子线程执…

深入了解tomcat线程池

1.概述 在正式进入Tomcat线程池之前&#xff0c;小伙伴们可以先回顾一下JDK中的线程池相关特性&#xff0c;对于JDK线程池的总结和源码的解析感兴趣的童鞋&#xff0c;也可参考博主的层层剖析线程池源码的这篇文章&#xff0c;文章主要讲述对线程池的生命周期&#xff0c;核心参…

Vue3 - 不再支持 IE11,到底为什么?

前言 咱们的 Vue2 目前仍然支持 IE11&#xff0c;但是到了 Vue3 这里&#xff0c;直接被抛弃了。 IE 浏览器可以说是早期前端开发的噩梦&#xff0c;现在还充斥的大量兼容 IE 浏览器的代码&#xff0c;你可以在网上看到很多类似的信息。 IE 浏览器下 float 布局错乱。IE 浏览器…

商务呈现之沟通管理-上

一、前言 课程目标及适用人群课程目标:商务/项目的目标达成,任务推动,良好的商务呈现 现实的困扰我们商务活动中是否有遇到以下情况: (1)需求老是变 理解不一致细节不清晰(2)CR很难谈 需求基线不清晰没有利用好"交换"(3)原地打转 事项推进缓慢几个月还在讨…

相似度系列-6:单维度方法:Evaluating Coherence in Dialogue Systems using Entailment

Evaluating Coherence in Dialogue Systems using Entailment coherence 英文中意味着连贯性、条理性。 这篇文章是面向对话应用的&#xff0c;更加关注于对话中上下位的连贯性。1. 直接转换为 NLI问题&#xff0c;premise-hypothesis问题。——2. 数据集是自己构造的。——数…

一文带你了解【抽象类和接口】

1. 抽象类概念 在面向对象的概念中&#xff0c;所有的对象都是通过类来描绘的&#xff0c;但是并不是所有类都是用来描绘对象的。如果一个类中没有包含足够的信息来描绘一个具体的对象&#xff0c;这样的类就是抽象类。 举个简单的例子 上图中&#xff0c;三角形&#xff0…

构建一个商业智能 BI 分析平台,公司CIO应该重点关注什么?

企业级商业智能 BI 分析平台的构建是一个系统型的工程&#xff0c;涉及业务分析需求的把控、各类数据资源的整合清洗、数据仓库的架构设计、可视化分析报表逻辑设计、IT 部门与业务部门的工作边界划分与配合等等居多环节。 每一个环节的重要性都不容忽视&#xff0c;第一是业务…

(算法设计与分析)第三章动态规划-第二节:动态规划之背包类型问题

文章目录一&#xff1a;01背包问题&#xff08;1&#xff09;题目描述&#xff08;2&#xff09;解题思路&#xff08;3&#xff09;完整代码二&#xff1a;分割等和子集&#xff08;01背包变形&#xff09;&#xff08;1&#xff09;题目描述&#xff08;2&#xff09;解题思路…

Java:Jar包反编译,解压和压缩

1、简述 JAR 文件就是 Java Archive &#xff08; Java 档案文件&#xff09;&#xff0c;它是 Java 的一种文档格式。 JAR 文件非常类似 ZIP 文件。准确的说&#xff0c;它就是 ZIP 文件&#xff0c;所以叫它文件包。JAR 文件与 ZIP 文件唯一的区别就是在 JAR 文件的内容中&a…

蓝桥杯必备算法分享——差分算法

AcWing—差分算法 文章目录AcWing---差分算法一、什么是差分&#xff1f;二、差分的作用三、一维差分模板四、二维差分五、二维差分构造方法图示&#xff1a;六、二维差分矩阵模板总结差分算法是前缀和算法的逆运算。两者可以对比着学习&#xff1a; 一、什么是差分&#xff1…