第53天,图论04,加强广搜和深搜的理解练习💪(ง •_•)ง,编程语言:C++
目录
110.字符串接龙
105.有向图的完全可达性
106.岛屿的周长
总结
110.字符串接龙
文档讲解:手撕字符串接龙
题目:110. 字符串接龙 (kamacoder.com)
学习:从示例中可以看出,实际上abc到def的路径不止一条,但是输出的是最短的那条路径。
因此本题主要需要解决的是两个问题:1.图中的线是如何使连在一起(图是如何构成的)2.找到最短的路径。
1.图中的线是如何连在一起:针对一个问题,首先题目中给的输入是没有包含点与点之间的连线的,图中的连线需要我们自己去连,而相连的条件也很直观,即点与点之间字符只差一个。因此我们只需要判断点与点之间的关系,如果只差一个字符,就说明是有连线的。
2.找到最短路径:这里无向图求最短路,广搜最为合适,广搜只要搜到了终点,那么一定是最短的路径。因为广搜就是以起点中心向四周扩散的搜索。本题如果用深搜,会比较麻烦,要在到达终点的不同路径中选则一条最短路。 而广搜只要达到终点,一定是最短路。
注意:本题是一个无向图,需要用标记位,标记着节点是否走过,否则就会死循环!可以使用set来检查字符串是否出现在字符串集合里更快一些。
代码:
#include <iostream>
#include <vector>
#include <string>
#include <unordered_set>
#include <unordered_map>
#include <queue>
using namespace std;
int main() {
int n;
cin >> n;
string beginStr, endStr;
cin >> beginStr >> endStr;
unordered_set<string> strSet; //保存字典中的点(使用哈希表,便于查找,查找时间复杂度为O(1))
string str;
for (int i = 0; i < n; i++) {
cin >> str;
strSet.insert(str);
}
unordered_map<string, int>visitMap; //记录字符串和路径长度
queue<string> que; //初始化队列
que.push(beginStr);
visitMap.insert(pair<string, int>(beginStr, 1)); //初始化visitMap
while(!que.empty()) {
string word = que.front();
que.pop();
int path = visitMap[word]; //这个字符串在路径中的长度
// 开始在这个str中,挨个字符去替换
for (int i = 0; i < word.size(); i++) {
string newWord = word; //用一个新字符串替换str,因为每次要置换一个字符
for (int j = 0 ; j < 26; j++) { //遍历26个字母
newWord[i] = j + 'a';
if (newWord == endStr) { //处理特殊情况,如果发现替换字母后,字符串与终点字符相同
cout << path + 1 << endl; // 找到了路径
return 0;
}
//字符串集合里出现了newWord,并且newWord没有被访问过
if (strSet.find(newWord) != strSet.end() //如果发现改完后的字符在字典中
&& visitMap.find(newWord) == visitMap.end()) {
//添加访问信息,并将新字符串放到队列中
visitMap.insert(pair<string, int>(newWord, path + 1));
que.push(newWord);
}
}
}
}
// 没找到输出0
cout << 0 << endl;
return 0;
}
105.有向图的完全可达性
文档讲解:手撕有向图的完全可达性
题目:105. 有向图的完全可达性 (kamacoder.com)
学习:本题定义的是一个有向图,本质上是一个有向图搜搜全路径的过程。可以采用深搜dfs 或者广搜 bfs来进行搜索。
1.采用深搜dfs:我们可以从深搜三部曲出发进行分析。
确定返回值和参数列表:首先我们需要传入图用以遍历,接着我们还需要知道当前的节点key,最后我们还需要一个visited数组,来确定哪些点已经被遍历了,防止走回头路。
void dfs(const vector<list<int>>& graph, int key, vector<bool>& visited) {
确定终止条件:我们再遍历的过程中,从一个节点到另一个节点,是依据当前节点的所有出度进行的。而为了防止出现走回头路或者陷入死循环的情况(同时本题也规定了是从节点1出发到所有节点)我们用visited数组来记录访问过的节点,该节点默认数组里的元素都是false,一旦遍历到就标记为true。因此当前访问的节点如果是true,说明该节点已经访问过了,那就终止本层递归,如果不是,我们就进入递归。该终止条件可以写在进入递归后,也可以写在进入递归前。
// 写法一:处理当前访问的节点
void dfs(const vector<list<int>>& graph, int key, vector<bool>& visited) {
if (visited[key]) {
return;
}
visited[key] = true;
list<int> keys = graph[key];
for (int key : keys) {
// 深度优先搜索遍历
dfs(graph, key, visited);
}
}
// 写法二:处理下一个要访问的节点
void dfs(const vector<list<int>>& graph, int key, vector<bool>& visited) {
list<int> keys = graph[key];
for (int key : keys) {
if (visited[key] == false) { // 确认下一个是没访问过的节点
visited[key] = true;
dfs(graph, key, visited);
}
}
处理目前搜索节点出发的路径:本题的处理逻辑实际上很简单,就是遍历到节点就把其标记为true,没有则进入新的循环,直到把所有的true都标记完。本题也没有回溯操作,因为我们不需要保存路径只需要标记即可。
代码:最后我们可以得到代码(dfs)
#include <iostream>
#include <vector>
#include <list>
using namespace std;
//1.确定返回值和参数列表
void dfs(const vector<list<int>>& graph, int key, vector<bool>& visited) { //什么时候能加const取决于,graph是否需要改变
list<int> keys = graph[key]; //邻接表方式
for (int key : keys) {
//2.确定终止条件:将终止条件写在进入递归前,如果不符合直接不进入递归
if (visited[key] == false) { //找到下一个没有访问过的点
visited[key] = true;
dfs(graph, key, visited);
}
}
}
int main() {
int n, m;
cin >> n >> m;
int s,t;
vector<list<int>> graph(n + 1); //节点编号从1到n,所以申请 n+1 这么大的数组(采用邻接表的方式)
while (m--) {
cin >> s >> t;
graph[s].push_back(t);
}
vector<bool> visited(n + 1, false);
visited[1] = true; // 节点1 预先处理
dfs(graph, 1, visited);
//检查是否都访问到了
for (int i = 1; i <= n; i++) {
if (visited[i] == false) { //如果存在没有访问过的点,则说明有向图不具备完全可达性
cout << -1 << endl;
return 0;
}
}
cout << 1 << endl;
}
代码:采用bfs的方法,我们也可以通过一个count进行计数,假如count = n则说明每个点都遍历到了。
#include <iostream>
#include <vector>
#include <list>
#include <queue>
using namespace std;
int main() {
int n, m;
cin >> n >> m;
int s, t;
vector<list<int>> graph(n + 1); //保存地图,同时节点编号是从1开始的,因此开拓n+1的空间
while (m--) {
cin >> s >> t;
graph[s].push_back(t);
}
vector<bool> visited(n + 1, false); //访问数组
visited[1] = true; //初始化visited数组
queue<int> que; //设置队列准备开始遍历
que.push(1); //初始化队列
// BFS
int count = 1;
while (!que.empty()) {
int key = que.front();
que.pop();
list<int> keys = graph[key]; //邻接表的方式
for (int key : keys) {
if (!visited[key]) { //如果这个点没有被访问
que.push(key);
visited[key] = true;
count++;
}
}
}
if(count == n) {
cout << 1 << endl;
}
else {
cout << -1 << endl;
}
return 0;
}
106.岛屿的周长
文档讲解:手撕岛屿的周长
题目:106. 岛屿的周长 (kamacoder.com)
学习:本题是求岛屿的周长,实际上本题不需要进行dfs或者bfs搜索方式,因为本题求周长更多的是一个数学问题。本题的解法有两种
第一种:根据题意,我们可以发现如果一块陆地:边是靠水的,或者是边界,则说明该边是外边,即属于周长计算里面;而边如果是靠陆地的,则说明该边是内边,不包含周长里面的。
因此我们可以通过遍历地图,找到一个陆地之后,就判断其上下左右的状态,如果是外边,则周长+1,如果是内边则周长不变,最后我们就可以求得岛屿的周长。
#include <iostream>
#include <vector>
using namespace std;
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];
}
}
int dir[4][2] = {0, 1, 1, 0, -1, 0, 0, -1}; //四个方向
int result = 0;
for(int i = 0; i < n; i++) {
for(int j = 0; j < m; j++) {
if(grid[i][j] == 1) { //找到了一个土地
for(int k = 0; k < 4; k++) {
int nextx = i + dir[k][0];
int nexty = j + dir[k][1];
//两种情况,在四条边界处或者是海洋
if(nextx < 0 || nexty < 0 || nextx >=n || nexty >= m || grid[nextx][nexty] == 0) {
result++;
}
}
}
}
}
cout << result << endl;
return 0;
}
第二种:我们可以发现,岛屿的中边长,为岛屿的陆地数量*4,而每有一个陆地和另一个陆地是相邻的,边的数量-2,因此最后岛屿的周长就为:岛屿的陆地数量*4 - cover*2(cover表示相邻的边的数量)
#include <iostream>
#include <vector>
using namespace std;
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];
}
}
int sum = 0;
int cover = 0;
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
if (grid[i][j] == 1) {
sum++; // 统计总的陆地数量
//为了避免重复计算,我们只统计每一块陆地上边和左边的相邻情况,这样就能够覆盖所有的情况
// 统计上边相邻陆地
if(i - 1 >= 0 && grid[i - 1][j] == 1) cover++;
// 统计左边相邻陆地
if(j - 1 >= 0 && grid[i][j - 1] == 1) cover++;
}
}
}
cout << sum*4 - cover*2 << endl;
return 0;
}
总结
今天的三道题是三种完全不同的解法和思路。
第一道题,告诉了我们图的边没有给我们的时候,是如何进行边的构造的。同时如何采用bfs方式找到最短的路径。
第二道题,是有向图搜索全路径的问题,重点在于dfs和bfs的基础考察,属于图的遍历问题。
第三道题,是让我们不要盲目在图论中使用dfs和bfs,还是要读懂题意,理解题意,才能找到解题的关键。