(1)图的存储方法
- 邻接矩阵
// 因为节点标号是从1开始的,为了节点标号和下标对齐,我们申请 n + 1 * n + 1 这么大的二维数组。
vector<vector<int>> graph(n + 1, vector<int>(n + 1, 0));
while(m --){
cin >> s >> t;
graph[s][t] = 1;
}
- 邻接表
节点1 指向 节点3 和 节点5
节点2 指向 节点4、节点3、节点5
节点3 指向 节点4
节点4指向节点1
// 节点编号从1到n,所以申请 n+1 这么大的数组,数组里的元素是一个链表,表示该节点指向的所有节点
vector<list<int>> graph(n + 1);
while(m --){
cin >> s >> t;
graph[s].push_back(t);
}
(2)图的遍历算法
深度优先算法:
-
搜索方向,是认准一个方向搜,直到碰壁之后再换方向
-
换方向是撤销原路径,改为节点链接的下一个路径,回溯的过程
实现方法:递归和回溯
void dfs(参数){
if(终止条件){
存放结果;
return;
}
for(选择:本节点所连接的其他节点){
处理节点;
dfs(图,选择的节点); // 递归
回溯,撤销处理结果
}
}
广度优先算法:
广搜的搜索方式就适合于解决两个点之间的最短路径问题。
因为广搜是从起点出发,以起始点为中心一圈一圈进行搜索,一旦遇到终点,记录之前走过的节点就是一条最短路。
用栈和队列都可以
//表示四个方向(向上、向右、向左、向下)
int dir[4][2] = {0, 1, 1, 0, -1, 0, 0, -1};
// grid 是地图,也就是一个二维数组
// visited标记访问过的节点,不要重复访问
// x,y 表示开始搜索节点的下标
void bfs(vector<vector<char>>& grid, vector<vector<bool>>& visited, int x, int y){
queue<pair<int,int>> que;
que.push({x, y}); // 初始节点加入队列
visited[x][y] = true;
while(!que.empty()){
pair<int, int> cur = que.front();
que.pop(); // 取队首元素
int curx = cur.first;
int cury = cur.second;
for(int i = 0; i < 4; i ++){
int nextx = curx + dir[i][0];
int nexty = cury + dir[i][1];
if(nextx < 0 || nextx >= grid.size() || nexty < 0 || nexty >= grid[0].size()) continue; // 坐标越界了,直接跳过
if(!visited[nextx][nexty]){ // 如果节点没有被访问过
que.push({nextx, nexty});
visited[nextx, nexty] = true;
}
}
}
}
(一)所有可达路径
模板题
#include <iostream>
using namespace std;
#include <vector>
vector<vector<int>> result;
vector<int> path;
void dfs(const vector<vector<int>>& graph, int x, int n){
if(x == n){
result.push_back(path);
return;
}
for(int i = 1; i <= n; i ++){
if(graph[x][i] == 1){
path.push_back(i);
dfs(graph, i, n);
path.pop_back();
}
}
}
int main(){
int n, m;
cin >> n >> m;
vector<vector<int>> graph(n + 1, vector<int>(n + 1, 0));
int a, b;
for(int i = 0; i < m; i ++){
cin >> a >> b;
graph[a][b] = 1;
}
// 注意无论什么路径都先走第1个节点
path.push_back(1);
dfs(graph, 1, n);
if(result.size() == 0) cout << -1 << endl;
for(const vector<int>&path: result){
for(int j = 0; j < path.size() - 1; j ++){
cout << path[j] << " ";
}
cout << path[path.size() - 1] << endl;
}
return 0;
}
(二)岛屿数量
(1)深搜
本题思路,是用遇到一个没有遍历过的节点陆地,计数器就加一,然后把该节点陆地所能遍历到的陆地都标记上。
在遇到标记过的陆地节点和海洋节点的时候直接跳过。 这样计数器就是最终岛屿的数量。
#include <iostream>
using namespace std;
#include <vector>
int dir[4][2] = {1, 0, 0, 1, -1, 0, 0, -1};
void dfs(const vector<vector<int>>& grid, vector<vector<bool>>& visited, int x, int y){
for(int i = 0; i < 4; i ++){
int nextx = x + dir[i][0];
int nexty = y + dir[i][1];
if(nextx < 0 || nextx >= grid.size() || nexty < 0 || nexty >= grid[0].size()) continue;
if(!visited[nextx][nexty] && grid[nextx][nexty]){
visited[nextx][nexty] = true;
dfs(grid, visited, nextx, nexty); // 这里的回溯包含在了递归中
}
}
}
int main(){
int n, m;
cin >> n >> m;
vector<vector<int>> grid(n, vector<int>(m, 0));
for(int i = 0; i < n; i ++){
for(int j = 0; j < m; j ++){
cin >> grid[i][j];
}
}
vector<vector<bool>> visited(n, vector<bool>(m, false));
int result = 0;
for(int i = 0; i < n; i ++){
for(int j = 0; j < m; j ++){
if(!visited[i][j] && grid[i][j]){
visited[i][j] = true;
result ++; // 遇到没访问过的陆地,+1
dfs(grid, visited, i, j); // 将与其链接的陆地都标记上 true
}
}
}
cout << result;
return 0;
}
(2)广搜
注意:只要 加入队列就代表 走过,就需要标记,而不是从队列拿出来的时候再去标记走过,因为这样会导致一些节点重复加入队列
#include <iostream>
using namespace std;
#include <vector>
#include <queue>
int dir[4][2] = {1, 0, 0, 1, -1, 0, 0, -1};
void dfs(const vector<vector<int>>& grid, vector<vector<bool>>& visited, int x, int y){
queue<pair<int, int>> que;
que.push({x, y});
visited[x][y] = true;// 每次放入一个元素就将其标记为visited
while(!que.empty()){
// 对于队列取最前面的元素front
pair<int, int> cur = que.front();
que.pop();
int curx = cur.first;
int cury = cur.second;
for(int i = 0; i < 4; i ++){
int nextx = curx + dir[i][0];
int nexty = cury + dir[i][1];
if(nextx < 0 || nextx >= grid.size() || nexty < 0 || nexty >= grid[0].size()) continue;
if(!visited[nextx][nexty] && grid[nextx][nexty]){
visited[nextx][nexty] = true;
dfs(grid, visited, nextx, nexty);
}
}
}
}
int main(){
int n, m;
cin >> n >> m;
vector<vector<int>> grid(n, vector<int>(m, 0));
for(int i = 0; i < n; i ++){
for(int j = 0; j < m; j ++){
cin >> grid[i][j];
}
}
vector<vector<bool>> visited(n, vector<bool>(m, false));
int result = 0;
for(int i = 0; i < n; i ++){
for(int j = 0; j < m; j ++){
if(!visited[i][j] && grid[i][j]){
result ++;
dfs(grid, visited, i, j);
}
}
}
cout << result;
return 0;
}
(三)岛屿的最大面积
(1)深搜
#include <iostream>
using namespace std;
#include <vector>
int count;
int dir[4][2] = {1, 0, 0, 1, -1, 0, 0, -1};
void dfs(vector<vector<int>>&graph, vector<vector<bool>>&visited, int x, int y){
for(int i = 0; i < 4; i ++){
int nextx = x + dir[i][0];
int nexty = y + dir[i][1];
if(nextx < 0 || nextx >= graph.size() || nexty < 0 || nexty >= graph[0].size()) continue;
if(!visited[nextx][nexty] && graph[nextx][nexty]){
// 遇到该岛屿的下一个新节点,count加1
count ++;
visited[nextx][nexty] = true;
dfs(graph, visited, nextx, nexty);
}
}
}
int main(){
int n, m;
cin >> n >> m;
vector<vector<int>> graph(n, vector<int>(m, 0));
vector<vector<bool>> visited(n, vector<bool>(m, false));
for(int i = 0; i < n; i ++){
for(int j = 0; j < m; j ++){
cin >> graph[i][j];
}
}
int result = 0;
for(int i = 0; i < n; i ++){
for(int j = 0; j < m; j ++){
if(!visited[i][j] && graph[i][j]){
// 每次遇到一个新的岛屿都将count先置为1,为当前搜索的节点
count = 1;
visited[i][j] = true;
dfs(graph, visited, i, j);
result = max(result, count);
}
}
}
cout << result;
return 0;
}
(2)广搜
#include <iostream>
using namespace std;
#include <vector>
#include <queue>
int count;
int dir[4][2] = {1, 0, 0, 1, -1, 0, 0, -1};
void dfs(vector<vector<int>>&graph, vector<vector<bool>>&visited, int x, int y){
queue<pair<int, int>> que;
que.push({x, y});
visited[x][y] = true;
while(!que.empty()){
pair<int, int> cur = que.front();
que.pop();
for(int i = 0; i < 4; i ++){
int nextx = x + dir[i][0];
int nexty = y + dir[i][1];
if(nextx < 0 || nextx >= graph.size() || nexty < 0 || nexty >= graph[0].size()) continue;
if(!visited[nextx][nexty] && graph[nextx][nexty]){
visited[nextx][nexty] = true;
count ++;
dfs(graph, visited, nextx, nexty);
}
}
}
}
int main(){
int n, m;
cin >> n >> m;
vector<vector<int>> graph(n, vector<int>(m, 0));
vector<vector<bool>> visited(n, vector<bool>(m, false));
for(int i = 0; i < n; i ++){
for(int j = 0; j < m; j ++){
cin >> graph[i][j];
}
}
int result = 0;
for(int i = 0; i < n; i ++){
for(int j = 0; j < m; j ++){
if(!visited[i][j] && graph[i][j]){
count = 1;
visited[i][j] = true;
dfs(graph, visited, i, j);
result = max(result, count);
}
}
}
cout << result;
return 0;
}
(四)孤岛的总面积
法一:直接法
如果判断该岛屿为边缘岛屿,则不将count加入result
#include <iostream>
using namespace std;
#include <vector>
int count;
int dir[4][2] = {1, 0, 0, 1, -1, 0, 0, -1};
bool dfs(vector<vector<int>>&graph, vector<vector<bool>>&visited, int x, int y){
bool is_edge = false;
if(x == 0 || x == graph.size() - 1 || y == 0 || y == graph[0].size() - 1) is_edge = true;
for(int i = 0; i < 4; i ++){
int nextx = x + dir[i][0];
int nexty = y + dir[i][1];
if(nextx < 0 || nextx >= graph.size() || nexty < 0 || nexty >= graph[0].size()) continue;
if(!visited[nextx][nexty] && graph[nextx][nexty]){
count ++;
visited[nextx][nexty] = true;
is_edge = dfs(graph, visited, nextx, nexty) || is_edge;
}
}
return is_edge;
}
int main(){
int n, m;
cin >> n >> m;
vector<vector<int>> graph(n, vector<int>(m, 0));
vector<vector<bool>> visited(n, vector<bool>(m, false));
for(int i = 0; i < n; i ++){
for(int j = 0; j < m; j ++){
cin >> graph[i][j];
}
}
int result = 0;
for(int i = 1; i < n - 1; i ++){
for(int j = 1; j < m - 1; j ++){
if(!visited[i][j] && graph[i][j]){
count = 1;
visited[i][j] = true;
if(!dfs(graph, visited, i, j)){
result += count;
}
}
}
}
cout << result;
return 0;
}
法二:补集法,将所有靠边的岛屿全部变为0(海洋),再统计剩下的陆地数量
#include <iostream>
using namespace std;
#include <vector>
int count;
int dir[4][2] = {1, 0, 0, 1, -1, 0, 0, -1};
void dfs(vector<vector<int>>&graph, vector<vector<bool>>&visited, int x, int y){
for(int i = 0; i < 4; i ++){
int nextx = x + dir[i][0];
int nexty = y + dir[i][1];
if(nextx < 0 || nextx >= graph.size() || nexty < 0 || nexty >= graph[0].size()) continue;
if(!visited[nextx][nexty] && graph[nextx][nexty]){
count ++;
visited[nextx][nexty] = true;
dfs(graph, visited, nextx, nexty); // 深搜
}
}
}
int main(){
int n, m;
cin >> n >> m;
vector<vector<int>> graph(n, vector<int>(m, 0));
vector<vector<bool>> visited(n, vector<bool>(m, false));
int countall = 0;
int edgeNum = 0;
for(int i = 0; i < n; i ++){
for(int j = 0; j < m; j ++){
cin >> graph[i][j];
if(graph[i][j] == 1) countall ++;
}
}
for(int i = 0; i < n; i ++){
if(!visited[i][0] && graph[i][0]){
count = 1;
visited[i][0] = true;
dfs(graph, visited, i, 0);
edgeNum += count;
}
if(!visited[i][m - 1] && graph[i][m - 1]){
count = 1;
visited[i][m - 1] = true;
dfs(graph, visited, i, m - 1);
edgeNum += count;
}
}
for(int j = 1; j < m - 1; j ++){
if(!visited[0][j] && graph[0][j]){
count = 1;
visited[0][j] = true;
dfs(graph, visited, 0, j);
edgeNum += count;
}
if(!visited[n - 1][j] && graph[n - 1][j]){
count = 1;
visited[n - 1][j] = true;
dfs(graph,visited, n - 1, j);
edgeNum += count;
}
}
cout << countall - edgeNum;
return 0;
}
(五)沉没孤岛
还是先找出边缘的岛屿,但是这里不用额外定义空间,标记周边的陆地,可以直接改陆地为其他特殊值作为标记。
#include <iostream>
using namespace std;
#include <vector>
int dir[4][2] = {1, 0, 0, 1, -1, 0, 0, -1};
void dfs(vector<vector<int>>&graph, int x, int y){
// 将边界岛屿的每块陆地都标为2
graph[x][y] = 2;
for(int i = 0; i < 4; i ++){
int nextx = x + dir[i][0];
int nexty = y + dir[i][1];
if(nextx < 0 || nextx >= graph.size() || nexty < 0 || nexty >= graph[0].size()) continue;
// 为1表示是陆地,但是还未遍历过
if(graph[nextx][nexty] == 1) dfs(graph, nextx, nexty);
}
}
int main(){
int n, m;
cin >> n >> m;
vector<vector<int>> graph(n, vector<int>(m, 0));
for(int i = 0; i < n; i ++){
for(int j = 0; j < m; j ++){
cin >> graph[i][j];
}
}
for(int i = 0; i < n; i ++){
// 这里不需要visited,因为如果graph为1,就表示没有遍历过,否则为2
if(graph[i][0] == 1) dfs(graph, i, 0);
if(graph[i][m - 1] == 1) dfs(graph, i, m - 1);
}
for(int j = 1; j < m - 1; j ++){
if(graph[0][j] == 1) dfs(graph, 0, j);
if(graph[n - 1][j] == 1) dfs(graph, n - 1, j);
}
for(int i = 0; i < n; i ++){
for(int j = 0; j < m; j ++){
// 为1的是孤岛,将其置0输出
if(graph[i][j] == 1) cout << 0 << " ";
// 为2的是边缘岛屿,将其恢复为陆地1
if(graph[i][j] == 2) cout << 1 << " ";
// 剩下的为正常海洋,还是输出0
if(graph[i][j] == 0) cout << 0 << " ";
}
cout << endl;
}
return 0;
}
(六)水流问题
从结果出发,从第一组边界上的节点逆流而上,将遍历过的节点标记上,从第二组边界上的节点出发,将遍历过的节点都标记上
#include <iostream>
using namespace std;
#include <vector>
int dir[4][2] = {1, 0, 0, 1, -1, 0, 0, -1};
void dfs(vector<vector<int>>&graph, vector<vector<bool>>&visited, int x, int y){
visited[x][y] = true;
for(int i = 0; i < 4; i ++){
int nextx = x + dir[i][0];
int nexty = y + dir[i][1];
if(nextx < 0 || nextx >= graph.size() || nexty < 0 || nexty >= graph[0].size()) continue;
// 这里高于或者相等都是可以流向的
if(graph[nextx][nexty] >= graph[x][y] && !visited[nextx][nexty]){
visited[nextx][nexty] = true;
dfs(graph, visited, nextx, nexty);
}
}
}
int main(){
int n, m;
cin >> n >> m;
vector<vector<int>> graph(n, vector<int>(m, 0));
for(int i = 0; i < n; i ++){
for(int j = 0; j < m; j ++){
cin >> graph[i][j];
}
}
// 第一边界线
vector<vector<bool>> visitedFirst(n, vector<bool>(m, false));
for(int i = 0; i < n; i ++) dfs(graph, visitedFirst, i, 0);
for(int j = 0; j < m; j ++) dfs(graph, visitedFirst, 0, j);
// 第二边界线
vector<vector<bool>> visitedSecond(n, vector<bool>(m, false));
for(int i = 0; i < n; i ++) dfs(graph, visitedSecond, i, m - 1);
for(int j = 0; j < m; j ++) dfs(graph, visitedSecond, n - 1, j);
for(int i = 0; i < n; i ++){
for(int j = 0; j < m; j ++){
if(visitedFirst[i][j] && visitedSecond[i][j]){
cout << i << " " << j << endl;
}
}
}
return 0;
}
(七)建造最大岛屿
从要变的格子出发,只要知道该格子上下左右的岛屿面积,即可计算将该格子变为1后,连起来的岛屿数量。
第一步:一次遍历地图,得出各个岛屿的面积,并做编号记录。可以使用map记录,key为岛屿编号,value为岛屿面积
第二步:再遍历地图,遍历0的方格(因为要将0变成1),并统计该1(由0变成的1)周边岛屿面积,将其相邻面积相加在一起,遍历所有 0 之后,就可以得出 选一个0变成1 之后的最大面积。
#include <iostream>
using namespace std;
#include <vector>
#include <unordered_map>
#include <unordered_set>
int dir[4][2] = {1, 0, 0, 1, -1, 0, 0, -1};
int count;
void dfs(vector<vector<int>>&graph, vector<vector<bool>>&visited, int x, int y, int mark){
for(int i = 0; i < 4; i ++){
int nextx = x + dir[i][0];
int nexty = y + dir[i][1];
if(nextx < 0 || nextx >= graph.size() || nexty < 0 || nexty >= graph[0].size()) continue;
if(graph[nextx][nexty] && !visited[nextx][nexty]){
count ++;
visited[nextx][nexty] = true;
// 将地图上遍历过的位置改为其对应的小岛编号
graph[nextx][nexty] = mark;
dfs(graph, visited, nextx, nexty, mark);
}
}
}
int main(){
int n, m;
cin >> n >> m;
// 这里先做是否全为岛屿的判断,因为如果全为1,后面就走不到添加陆地的循环,这样得到的result=0,而正确的result应该为所有陆地的面积
bool isAllGrid = true;
vector<vector<int>> graph(n, vector<int>(m, 0));
for(int i = 0; i < n; i ++){
for(int j = 0; j < m; j ++){
cin >> graph[i][j];
if(graph[i][j] == 0) isAllGrid = false;
}
}
// 如果全为陆地,就没有添加陆地的机会,答案即为所有陆地面积
if(isAllGrid){
cout << n * m;
return 0;
}
// 这里小岛的mark要从2开始标起,区分graph的0、1两种状态
int mark = 2;
unordered_map<int, int> gridNum;
vector<vector<bool>> visited(n, vector<bool>(m, false));
for(int i = 0; i < n; i ++){
for(int j = 0; j < m; j ++){
if(graph[i][j] == 1 && !visited[i][j]){
count = 1;
visited[i][j] = true;
graph[i][j] = mark;
dfs(graph, visited, i, j, mark);
gridNum[mark] = count;
mark ++; // mark自增
}
}
}
int result = 0;
unordered_set<int> visitedGrid; // 标记访问过的岛屿,因为有的岛屿在多个方向包围该水面
for(int i = 0; i < n; i ++){
for(int j = 0; j < m; j ++){
// Y遇到了水面
if(graph[i][j] == 0){
// 每次将记录访问过的岛屿情况
visitedGrid.clear();
count = 1; // 记录连接后的岛屿数量
for(int k = 0; k < 4; k ++){
int nextx = i + dir[k][0];
int nexty = j + dir[k][1];
if(nextx < 0 || nextx >= graph.size() || nexty < 0 || nexty >= graph[0].size()) continue;
int nearGraph = graph[nextx][nexty];
// 如果旁边这个为岛屿并且这个岛屿没有被访问过
if( nearGraph && visitedGrid.find(nearGraph) == visitedGrid.end()){
// 标记该岛屿为已经访问过
visitedGrid.insert(nearGraph);
count += gridNum[nearGraph];
}
}
result = max(result, count);
}
}
}
cout << result;
return 0;
}
(八)字符串接龙
求起点和终点的最短路径长度,这里无向图求最短路,广搜(queue)最为合适,广搜只要搜到了终点,那么一定是最短的路径。因为广搜就是以起点中心向四周扩散的搜索。
-
本题是一个无向图,需要用标记位(visited),标记着节点是否走过,否则就会死循环!
-
使用set来检查字符串是否出现在字符串集合里更快一些
#include <iostream>
using namespace std;
#include <string>
#include <vector>
#include <queue>
#include <unordered_map>
#include <unordered_set>
int main(){
int n;
string beginStr, endStr, strTemp;
unordered_set<string> strList;
cin >> n;
cin >> beginStr >> endStr;
for(int i = 0; i < n; i ++){
cin >> strTemp;
strList.insert(strTemp);
}
// 记录走到的节点以及其对应的路径长度
queue<pair<string, int>> que;
unordered_set<string> visited;
que.push(pair<string, int>(beginStr, 1));
visited.insert(beginStr);
while(!que.empty()){
pair<string, int> temp = que.front();
que.pop();
string str = temp.first;
int pathLenth = temp.second;
for(int i = 0; i < str.size(); i ++){
for(int j = 0; j < 26; j ++){
string newWord = str; // 记录当前替换后的字符串
newWord[i] = j +'a'; // 从a到z依次替换
// 判断是否找到了终结的str,提前结束
if(newWord == endStr){
cout << pathLenth + 1;
return 0;
}
if(strList.find(newWord) != strList.end() && visited.find(newWord) == visited.end()){
que.push(pair<string, int>(newWord, pathLenth + 1));
visited.insert(newWord);
}
}
}
}
// 没有找到输出0
cout << 0;
return 0;
}
(九)有向图的完全可达性
visited数组来记录访问过的节点,该节点默认 数组里元素都是false,把元素标记为true就是处理 本节点了。
这里visited不需要回溯,因为只要将所有访问过的节点标出来即可
#include <iostream>
using namespace std;
#include <vector>
void dfs(vector<vector<int>>&graph, vector<bool>&visited, int key){
visited[key] = true;
for(int i = 1; i < graph[0].size(); i ++){
if(graph[key][i] && !visited[i]){
dfs(graph, visited, i);
}
}
}
int main(){
int n, k;
cin >> n >> k;
vector<vector<int>> graph(n + 1, vector<int>(n + 1, 0));
int x, y;
vector<bool> visited(n + 1, false);
for(int i = 0; i < k; i ++){
cin >> x >> y;
graph[x][y] = 1;
}
dfs(graph, visited, 1);
for(int i = 1; i <= n; i ++){
if(!visited[i]){
cout << -1;
return 0;
}
}
cout << 1;
return 0;
}
(十)岛屿的周长
该题不需要BFS或DFS
每个为1的格子有5总情况:
没有格子环绕:边长 + 4
一个格子环绕:边长 + 3
两个格子环绕:边长 + 2
三个格子环绕:边长 + 1
四个格子环绕:边长 + 0
#include <iostream>
#include <vector>
using namespace std;
int main(){
int n, m;
cin >> n >> m;
vector<vector<int>> graph(n + 1, vector<int>(m, 0));
for(int i = 0; i < n; i ++){
for(int j = 0; j < m; j ++){
cin >> graph[i][j];
}
}
int dir[4][2] = {1, 0, 0, 1, -1, 0, 0, -1};
int result = 0;
for(int i = 0; i < n; i ++){
for(int j = 0; j < m; j ++){
if(graph[i][j]){
int count = 0;
for(int k = 0; k < 4; k ++){
int nextx = i + dir[k][0];
int nexty = j + dir[k][1];
if(nextx < 0 || nextx >= graph.size() || nexty < 0 || nexty >= graph[0].size()) continue;
if(graph[nextx][nexty]) count ++;
}
result += (4 - count);
}
}
}
cout << result;
return 0;
}
(十一)并查集理论基础
并查集主要有两个功能:
-
将两个元素添加到一个集合中。
-
判断两个元素在不在同一个集合
我们将三个元素A,B,C (分别是数字)放在同一个集合,其实就是将三个元素连通在一起,
只需要用一个一维数组来表示,即:father[A] = B,father[B] = C 这样就表述 A 与 B 与 C连通了(有向连通图)。
// 1. 并查集初始化。初始化为根。
void init(){
for(int i = 0; i < n; i ++){
father[i] = i;
}
}
// 2. 将v、u加入同一并查集(连通关系)
void join(int v, int u){
v = find(v); // 寻找v的根
u = find(u); // 寻找u的根
if(v == u) return; // 如果发现根相同,则已经在一个并查集中,不需要操作
father[v] = u; // 将两个元素连通(注意这里是将将两个元素的根连通)
}
// 3. 并查集中寻根过程(递归)
int find(int u){
if(u == father[u]) return u; // 找到根就是当前元素为止
else return find(father[u]);
}
// 4. 判断 u 和 v 是否找到同一个根
bool isSame(int u, int v){
u = find(u);
v = find(v);
return u == v;
}
路径压缩:因为所有节点只是根据根节点判断所在集合,所以只要将所有节点挂载到根节点即可。
(非根节点的所有节点直接指向根节点)
在寻根过程中,将递归函数 find(father[u]) 的返回结果
// 3. 并查集中寻根过程(递归)
int find(int u){
if(u == father[u]) return u; // 找到根就是当前元素为止
else return father[u] = find(father[u]); // 路径压缩
}
(十二)寻找存在的路径
题目中各个点是双向图链接,那么判断 一个顶点到另一个顶点有没有有效路径其实就是看这两个顶点是否在同一个集合里。(是否属于同一个并查集)
#include <iostream>
using namespace std;
#include <vector>
vector<int> father(101, 0);
void init(){
for(int i = 0; i < father.size(); i ++){
father[i] = i;
}
}
int find(int u){
if(father[u] == u) return u;
else return father[u] = find(father[u]);
}
void join(int u, int v){
u = find(u);
v = find(v);
if(u == v) return;
father[u] = v; // 将根节点相连
}
bool isSame(int u, int v){
u = find(u);
v = find(v);
return u == v;
}
int main(){
int n, m;
cin >> n >> m;
int s, t;
init(); // 初始化father数组
for(int i = 0; i < m; i ++){
cin >> s >> t;
join(s, t);
}
int source, destination;
cin >> source >> destination;
if(isSame(source, destination)) cout << 1 << endl;
else cout << 0 << endl;
return 0;
}
(十三)冗余连接
优先加入前面的边。(删除标准输入中最后出现的那条边。)
判断该边是否可以加入树 → 加入该边是否成环 → 该边的两个点是否属于同一个集合(有同一个根节点)
并查集:a 和 b 原本即在同一个根节点上(树中的两个节点),再将这两个节点连接,则构成环
#include <iostream>
using namespace std;
#include <vector>
vector<int> father(1001, 0);
int n;
void init(){
for(int i = 1; i <= n; i ++){
father[i] = i;
}
}
int find(int u){
if(father[u] == u) return u;
else return father[u] = find(father[u]);
}
void join(int v, int u){
v = find(v);
u = find(u);
if(v == u) return;
father[u] = v;
}
bool isSame(int u, int v){
u = find(u);
v = find(v);
return u == v;
}
int main(){
int s, t;
cin >> n;
// 使用并查集一定要记得先初始化
init();
for(int i = 0; i < n; i ++){
cin >> s >> t;
if(isSame(s, t)){
cout << s << " " << t << endl;
// 这里直接返回,因为n条边只要去掉2个就可以成为树
return 0;
}
join(s, t);// 符合条件的边,加入树中(一个树就是一个并查集)
}
return 0;
}
(十四)冗余连接II
有向树:根节点入度为0,其余节点入度都为1
情况一:如果我们找到入度为2的点,那么删一条指向该节点的边
情况二:入度为2, 但只能删特定的一条边(孤立边)
情况三:如果没有入度为2的点,说明 图中有环了(注意是有向环),删掉构成环的边
isTreeAfterRemoveEdge()
判断删一个边之后是不是有向树: 将所有边的两端节点分别加入并查集,遇到要 要删除的边则跳过,如果遇到即将加入并查集的边的两端节点 本来就在并查集了,说明构成了环。如果顺利将所有边的两端节点(除了要删除的边)加入了并查集,则说明 删除该条边 还是一个有向树。
getRemoveEdge()
确定图中一定有了有向环,那么要找到需要删除的那条边: 将所有边的两端节点分别加入并查集,如果遇到即将加入并查集的边的两端节点 本来就在并查集了,说明构成了环。
#include <iostream>
using namespace std;
#include <vector>
vector<int> father(1001, 0);
int n;
void init(){
for(int i = 1; i <= n; i ++){
father[i] = i;
}
}
int find(int u){
if(father[u] == u) return u;
else return father[u] = find(father[u]);
}
void join(int v, int u){
v = find(v);
u = find(u);
if(v == u) return;
father[u] = v;
}
bool isSame(int u, int v){
u = find(u);
v = find(v);
return u == v;
}
// 在有向图里找到删除的那条边,使其变成树
void getRemoveEdge(vector<vector<int>>&edge){
init();
for(int i = 0; i < n; i ++){
if(isSame(edge[i][0], edge[i][1])){
cout << edge[i][0] << " " << edge[i][1] << endl;
return;
}
join(edge[i][0], edge[i][1]);
}
}
// 删一条边之后判断是不是树
bool isTreeAfterRemoveEdge(vector<vector<int>>&edge, int deleteEdge){
init();
for(int i = 0; i < n; i ++){
if(i == deleteEdge) continue;
if(isSame(edge[i][0], edge[i][1])){
return false;
}
join(edge[i][0], edge[i][1]);
}
return true;
}
int main(){
int s, t;
cin >> n;
vector<vector<int>> edge;
vector<int> inDegree(n + 1, 0); // 记录节点的入度
for(int i = 0; i < n; i ++){
cin >> s >> t;
edge.push_back({s,t});
inDegree[t] ++;
}
vector<int> vec; // 记录入度为2的节点对应的边(如果有的话为2条)
// 找入度为2的节点所对应的边,注意要倒序,因为优先删除最后出现的一条边
for(int i = n - 1; i >= 0; i --){
if(inDegree[edge[i][1]] == 2) vec.push_back(i);// 加入第i条边
}
// 情况一、情况二
if(vec.size()){
// 放在vec里的边已经按照倒叙放的,所以这里就优先删vec[0]这条边
// 看是否能删除该边
if(isTreeAfterRemoveEdge(edge, vec[0])){
cout << edge[vec[0]][0] << " " << edge[vec[0]][1] << endl;
}else{// 删掉另一条边(情况二)
cout << edge[vec[1]][0] << " " << edge[vec[1]][1] << endl;
}
return 0;
}
// 情况三,成环
// 明确没有入度为2的情况,那么一定有有向环,找到构成环的边返回就可以了
getRemoveEdge(edge);
return 0;
}
(十五)prim算法精讲(寻宝)
以最短的总公路距离将 所有岛屿联通起来。
prim算法:采用贪心的策略,每次寻找距离最小生成树最近的节点,并加入到最小生成树中。
-
第一步,选距离生成树最近节点
-
第二步,最近节点加入生成树
-
第三步,更新非生成树节点到生成树的距离(即更新minDist数组)
初始化:将每个节点到最小生成树的距离先初始化为最大值,这样后面我们在比较的时候,发现更近的距离,才能更新到 minDist 数组上。
最终minDist数组中:记录了最小生成树所有边的权值,如果需要求最小生成树里边的权值总和,就是把最后的 minDist 数组 累加一起。
#include <iostream>
#include <vector>
using namespace std;
#include <climits>
int main(){
int v, e;
int x, y, k;
cin >> v >> e;
vector<vector<int>> graph(v + 1, vector<int>(v + 1, 10001));
for(int i = 0; i < e; i ++){
cin >> x >> y >> k;
// 无向图,所有两个方向都要赋值
graph[x][y] = k;
graph[y][x] = k;
}
// 所有节点到最小生成树的最小距离
// 0 空出来,从1开始,与节点1-v向对应,先初始化为最大值10001
vector<int> minDist(v + 1, 10001);
// 该节点是否在最小生成树中
vector<int> isInTree(v + 1, false);
// 循环 v-1次,将v-1个节点加入树中,最后一个节点到最小生成树的距离自然也更新为最小的了
for(int i = 1; i < v; i ++){
// 1、prim三部曲,第一步:选距离生成树最近节点
int cur = -1; // 选中哪个节点 加入最小生成树
int min = INT_MAX; // 距离生成树的最小距离
for(int j = 1; j <= v; j ++){ // 1 - v,顶点编号,这里下标从1开始
// 选取最小生成树节点的条件:
// (1)不在最小生成树里
// (2)距离最小生成树最近的节点
// 这里最先加入的一定为第1个节点,因为minDist[1] = 10001, 小于INT_MAX
if(!isInTree[j] && minDist[j] < min){
cur = j;
min = minDist[j];
}
}
// 2、prim三部曲,第二步:最近节点(cur)加入生成树
isInTree[cur] = true;
// 3、prim三部曲,第三步:更新非生成树节点到生成树的距离(即更新minDist数组)
// cur节点加入之后, 最小生成树加入了新的节点,那么所有节点到 最小生成树的距离(即minDist数组)需要更新一下
// 由于cur节点是新加入到最小生成树,那么只需要关心与 cur 相连的 非生成树节点 的距离 是否比 原来 非生成树节点到生成树节点的距离更小了呢
for(int j = 1; j <= v; j ++){
// 更新的条件:
// (1)节点是非生成树里的节点
// (2)与cur相连的某节点的权值 比 该某节点距离最小生成树的距离小
if(!isInTree[j] && graph[cur][j] < minDist[j]){
minDist[j] = graph[cur][j];
}
}
}
// 统计结果
int result = 0;
// 不计第一个顶点,因为统计的是边的权值,v个节点有 v-1条边
// 这里第1个节点的值因为是第一个加入生成树的,所以没有更新权重,其实应该为0
for(int i = 2; i <= v; i ++){
result += minDist[i];
}
cout << result;
return 0;
}
补充:如何打印该最小生成树的每条边
两个节点确定一条边,parent[节点编号] = 节点编号,在求最短边的时候更新
// 记录边
for(int j = 1; j <= v; j ++){
if(!isInTree[j] && grid[cur][j] > minDist[j]){
minDist[j] = grid[cur][j];
parent[j] = cur; // 记录最小生成树的边 (注意数组指向的顺序很重要,这里是记录当前遍历的节点j的上一个节点)
}
// 打印边
for(int i = 2; i <= v; i ++){
cout << i << "->" << parent[i] << endl;
}
(十六)Kruskal算法精讲
prim 算法是维护节点的集合,而 Kruskal 是维护边的集合。
kruscal的思路:
-
边的权值排序,因为要优先选最小的边加入到生成树里
-
遍历排序后的边
-
如果边首尾的两个节点在同一个集合,说明如果连上这条边图中会出现环
-
如果边首尾的两个节点不在同一个集合,加入到最小生成树,并把两个节点加入同一个集合
-
判断两个节点是否在同一个集合:并查集
#include <iostream>
using namespace std;
#include <vector>
#include <algorithm>
struct Edge{
int l, r, val;
};
bool cmp(const Edge&a, const Edge&b){
return a.val < b.val;
};
//注意:这里定义后,main函数里面就不能再定义了,不然会记作两个变量!!!
int v;
// 并查集
vector<int> father(10001, 0);
void init(){
for(int i = 1; i <= v; i ++){
father[i] = i;
}
}
int find(int u){
if(father[u] == u) return u;
else return father[u] = find(father[u]);
}
void join(int u, int v){
u = find(u);
v = find(v);
if(v == u) return;
father[v] = u;
}
bool inSame(int u, int v){
u = find(u);
v = find(v);
return u == v;
}
int main(){
int e;
int s, t, val;
cin >> v >> e;
init(); // 并查集初始化
vector<Edge> edges;
for(int i = 0; i < e; i ++){
cin >> s >> t >> val;
edges.push_back({s, t, val});
}
int result = 0;
sort(edges.begin(), edges.end(), cmp);
// 从最短的边开始,依次检查是否能加入
for(Edge edge: edges){
if(!inSame(edge.l, edge.r)){
result += edge.val;
join(edge.l, edge.r);
}
}
cout << result << endl;
return 0;
}
(十七)拓扑排序精讲
拓扑排序:给出一个有向图,把这个有向图转成线性的排序就叫拓扑排序
实现拓扑排序的算法有两种:卡恩算法(BFS)和DFS
BFS:广度优先算法
- 找到入度为0 的节点,加入结果集
(只有入度为0,它才是出发节点)
- 将该节点从图中移除
循环以上两步,直到 所有节点都在图中被移除了。
判断有环
节点1、2、3、4 形成了环,找不到入度为0 的节点了,所以此时结果集里只有一个元素。
如果结果集元素个数不等于图中节点个数,我们就可以认定图中一定有有向环!
#include <iostream>
using namespace std;
#include <vector>
#include <queue>
int main(){
int n, m, s, t;
cin >> n >> m;
vector<vector<int>> outMap(n + 1); // 记录某个节点的后面连接的节点
vector<int> inDegree(n + 1, 0); // 记录每个结果的入度,入度为0的是开始节点
for(int i = 0; i < m; i ++){
cin >> s >> t;
outMap[s].push_back(t);
inDegree[t] ++;
}
queue<int> que;
for(int i = 0; i < n; i ++){
// 入度为0的文件,可以作为开头,先加入队列
if(inDegree[i] == 0) que.push(i);
}
vector<int> result;
// que里面存放的始终为入度0的节点,即当前的开始节点
while(!que.empty()){
int cur = que.front();
que.pop();
result.push_back(cur);// 加入结果集
vector<int> back = outMap[cur];
for(int i = 0; i < back.size(); i ++){
// 将某个节点从图中移除,即将它后面关联节点的入度减1
inDegree[back[i]] --;
// inDegree变为0的节点一定在这个back里面
// 如果inDegree变为0,则称为新的开始节点
if(inDegree[back[i]] == 0) que.push(back[i]);
}
}
// 如果没有将所有节点加入result中,则说明形成了有向依赖环
if(result.size() != n){
cout << -1 << endl;
return 0;
}
for(int i = 0; i < result.size() - 1; i ++){
cout << result[i] << " ";
}
// 注意最后一个的输出格式
cout << result[result.size() - 1];
return 0;
}
(十八)dijkstra(朴素版)
最短路径问题:给出一个有向图,一个起点,一个终点,问起点到终点的最短路径。
dijkstra算法:在有权图(权值非负数)中求从起点到其他节点的最短路径算法。
dijkstra三部曲:
-
第一步,选源点到哪个节点近且该节点未被访问过
-
第二步,该最近节点被标记访问过
-
第三步,更新非访问节点到源点的距离(即更新minDist数组)
#include <iostream>
using namespace std;
#include <vector>
#include <climits>
int main(){
int n, m;
cin >> n >> m;
int s, e, v;
vector<vector<int>> graph(n + 1, vector<int>(n + 1, INT_MAX));
for(int i = 0; i < m; i ++){
cin >> s >> e >> v;
graph[s][e] = v;
}
vector<int> minDist(n + 1, INT_MAX);
vector<bool> visited(n + 1, false);
// 为加入第一个节点后,更新其他节点的minDist作准备
minDist[1] = 0;
// 只要加入n - 1个节点,最后一个节点会被更新到
for(int i = 1; i < n; i ++){
int cur = 1;
int min = INT_MAX;
for(int j = 1; j <= n; j ++){
if(!visited[j] && minDist[j] < min){
cur = j;
min = minDist[j];
}
}
// cout << cur;
visited[cur] = true;
for(int j = 1; j <= n; j ++){
if(!visited[j] && graph[cur][j] < INT_MAX && minDist[j] > (minDist[cur] + graph[cur][j])){
minDist[j] = minDist[cur] + graph[cur][j];
}
}
}
// 无法到达
if(minDist[n] == INT_MAX){
cout << -1 << endl;
return 0;
}
// 打印第n个节点(终点站)到第1个节点(起始站)的最近路线长度
cout << minDist[n] << endl;
return 0;
}
(十九)dijkstr(堆优化版)精讲
朴素版的dijkstra,时间复杂度为 O(n^2),时间复杂度只和 n(节点数量)有关系。
当 n 很大时,可以从边的数量出发(稀疏图)
使用邻接表存储稀疏图:
优点
-
只要存储边,空间利用率高
-
遍历节点链接情况相对容易
缺点
-
检查任意两个节点间是否存在边,效率相对低,需要 O(V)时间,V表示某节点链接其他节点的数量。
-
实现相对复杂,不易理解
从边的角度出发:
直接把边(带权值)加入到小顶堆(利用堆来自动排序),那么每次我们从堆顶里取出边自然就是 距离源点最近的节点所在的边。
取出后更新和该节点相关的节点权重,将新链接到的边添加到优先队列中
#include <iostream>
#include <vector>
#include <queue>
#include <list>
#include <climits>
using namespace std;
struct Edge{
int to;
int val;
Edge(int e, int v):to(e), val(v){}// 构造函数
};
class mycomparison{
public:
bool operator()(const pair<int, int>&a, const pair<int, int>&b){
return a.second > b.second;
}
};
int main(){
int n, m;
int s, e, v;
cin >> n >> m;
vector<list<Edge>> grid(n + 1);
for(int i = 0; i < m; i ++){
cin >> s >> e >> v;
grid[s].push_back({e, v});
}
vector<int> minDist(n + 1, INT_MAX);
vector<bool> visited(n + 1, false);
priority_queue<pair<int, int>, vector<pair<int,int>>, mycomparison> pq;
// 将第1个节点加入优先队列,其对应的路径长度为0
pq.push(pair<int, int>(1, 0));
minDist[1] = 0;
while(!pq.empty()){
// 1. 第一步,选源点到哪个节点近且该节点未被访问过 (通过优先级队列来实现)
// <节点, 源点到该节点的距离>
pair<int, int> cur = pq.top();
pq.pop();
// 2. 第二步,该最近节点被标记访问过
visited[cur.first] = true;
// 3. 第三步,更新非访问节点到源点的距离(即更新minDist数组)
for(Edge edge: grid[cur.first]){
if(!visited[edge.to] && minDist[edge.to] > minDist[cur.first] + edge.val){
minDist[edge.to] = minDist[cur.first] + edge.val;
// 更新边节点
pq.push(pair<int, int>(edge.to, minDist[edge.to]));
}
}
}
if(minDist[n] == INT_MAX){
cout << -1 << endl;
return 0;
}else{
cout << minDist[n] << endl;
}
return 0;
}
(二十)Floyd算法精讲
多源最短路径问题
Floyd 算法对边的权值正负没有要求,都可以处理。
Floyd算法核心思想是动态规划。
插点法:每次选择一个点插入到 i 和 j 中,选择最短的结果
#include <iostream>
#include <vector>
using namespace std;
#include <climits>
int main(){
int n, m;
int u, v, w;
cin >> n >> m;
vector<vector<int>> graph(n + 1, vector<int>(n + 1, INT_MAX));
for(int i = 0; i < m; i ++){
cin >> u >> v >> w;
// 双向图
graph[u][v] = w;
graph[v][u] = w;
}
// 对角线距离为0
for(int i = 1; i <= n; i ++){
graph[i][i] = 0;
}
int q;
cin >> q;
int start, end;
vector<pair<int, int>> visit;
for(int i = 0; i < q; i ++){
cin >> start >> end;
visit.push_back({start, end});
}
// 依次选择 k 插点法
for(int k = 1; k <= n; k ++){
for(int i = 1; i <= n; i ++){
for(int j = 1; j <= n; j ++){
// 这里要先判断一下是否溢出,因为如果graph[i][k]的初始值已经为INT_MAX,再加一个正数会导致正溢出
if(graph[i][k] < INT_MAX - graph[k][j])
graph[i][j] = min(graph[i][j], graph[i][k] + graph[k][j]);
}
}
}
for(int i = 0; i < visit.size(); i ++){
int startIndex = visit[i].first;
int endIndex = visit[i].second;
if(graph[startIndex][endIndex] == INT_MAX) cout << -1 << endl;
else{
cout << graph[startIndex][endIndex] << endl;
}
}
return 0;
}