2024年10月28日刷题完成
- 图论理论基础
- 深度优先搜索理论基础
- 广度优先搜索的基础:看了一下广搜的代码,还没开始自己写,思路很好,我想到本科时候比赛的时候看到队友在尝试一些新的算法,其中就有广搜和深搜,这下我也要会了,爽,太爽了。
所有可达路径
很多比较好的写法我都了解不够,确实要一直学习,用更简单的写法。stl掌握不够。
邻接矩阵法代码:
#include<iostream>
#include<vector>
#include<list>
using namespace std;
//这道题是一个有向图
//记录所有路径,记录单一路径
vector<vector<int>> result;
vector<int> path;
//x代表起始点,n代表终点
void dfs(const vector<vector<int>>& graph, int x, int n) {
//终止条件
if (x == n) {
result.push_back(path);
return;
}
//顺序执行,处理当前节点的可走路径
for (int i = 1; i < graph.size(); i++) {
//如果路径通着
if (graph[x][i] == 1) {
//深搜
path.push_back(i);
dfs(graph, i, n);
//回溯
path.pop_back();
}
}
return;
}
int main() {
int N, M, s, t;
cin >> N >> M;
//邻接矩阵表达法
vector<vector<int>> graph(N + 1, vector<int>(N + 1, 0));
while (M--) {
cin >> s >> t;
graph[s][t] = 1;
}
path.push_back(1);
dfs(graph, 1, N);
if (result.size() == 0) cout << -1;
for (int i = 0; i < result.size(); i++) {
for (int j = 0; j < result[i].size() - 1; j++) {
cout << result[i][j] << " ";
}
cout << result[i][result[i].size() - 1] << endl;
}
return 0;
}
邻接表法的代码:
#include<iostream>
#include<vector>
#include<list>
using namespace std;
//这道题是一个有向图
//记录所有路径,记录单一路径
vector<vector<int>> result;
vector<int> path;
//x代表起始点,n代表终点
void dfs(const vector<list<int>>& graph, int x, int n) {
//终止条件
if (x == n) {
result.push_back(path);
return;
}
//顺序执行,处理当前节点的可走路径
for (list<int>::const_iterator it = graph[x].begin(); it != graph[x].end(); it++) {
//深搜
path.push_back(*it);
dfs(graph, *it, n);
//回溯
path.pop_back();
}
return;
}
int main() {
int N, M, s, t;
cin >> N >> M;
//邻接表表达法
vector<list<int>> graph(N + 1);
while (M--) {
cin >> s >> t;
graph[s].push_back(t); //这句代码涉及list模板内部知识,之后可以找时间深入一下
}
path.push_back(1);
dfs(graph, 1, N);
if (result.size() == 0) cout << -1;
for (int i = 0; i < result.size(); i++) {
for (int j = 0; j < result[i].size() - 1; j++) {
cout << result[i][j] << " ";
}
cout << result[i][result[i].size() - 1] << endl;
}
return 0;
}
比较好的遍历graph的写法:
for (int i : graph[x]) { // 找到 x指向的节点
path.push_back(i); // 遍历到的节点加入到路径中来
dfs(graph, i, n); // 进入下一层递归
path.pop_back(); // 回溯,撤销本节点
}
for (const vector<int> &pa : result) {
for (int i = 0; i < pa.size() - 1; i++) {
cout << pa[i] << " ";
}
cout << pa[pa.size() - 1] << endl;
}
10月29日刷题
岛屿数量:广搜版+深搜版
很巧妙,二刷再加深理解。
#include<iostream>
#include<vector>
#include<queue>
using namespace std;
int dir[4][2] = { 0, 1, 1, 0, 0, -1, -1, 0 };
//输入地图框架,是否被访问,此时访问的位置坐标
void bfs(const vector<vector<int>>& gird, vector<vector<bool>>& isvisited, int loc_x, int loc_y) {
queue<pair<int, int>> que;
que.push({ loc_x, loc_y });
isvisited[loc_x][loc_y] = true;
while (!que.empty()) {
pair<int, int> temp = que.front(); que.pop();
int curX = temp.first;
int curY = temp.second;
for (int i = 0; i < 4; i++) {
int nextX = curX + dir[i][0];
int nextY = curY + dir[i][1];
//假如到了边界点,直接跳过找下一个点
if (nextX < 0 || nextX > gird.size() - 1 || nextY < 0 || nextY > gird[0].size() - 1) continue;
//如果gird未被访问过且此处有岛屿,那就继续沿着岛屿搜索,一个方向没有岛屿gird[nextX][nextY] == 0的话就不用去搜索了
if (!isvisited[nextX][nextY] && gird[nextX][nextY] == 1) { //找到一个未探测的陆地块
//存入该路地块
que.push({ nextX,nextY });
//标记该陆地块已探测
isvisited[nextX][nextY] = true;
}
}
}
}
int main() {
int N, M;
cin >> N >> M;
vector<vector<int>> gird(N, vector<int>(M, 0));
for (int i = 0; i < N; i++) {
for (int j = 0; j < M; j++) {
//C++不支持直接输入bool型数值,只能先输入int
cin >> gird[i][j];
}
}
vector<vector<bool>> isvisited(N, vector<bool>(M, false));
int result = 0;
for (int i = 0; i < N; i++) {
for (int j = 0; j < M; j++) {
if (!isvisited[i][j] && gird[i][j] == 1) {//发现一个未探索岛屿的陆地块,就开始确认整个岛屿的区域
//岛屿数量加1
result++;
//搜索整个岛屿的区域,然后在isvisited区域进行标注
dfs(gird, isvisited, i, j);
}
}
}
cout << result << endl;
return 0;
}
深搜版
#include<iostream>
#include<vector>
#include<queue>
using namespace std;
//岛屿数量(深搜版)
//确定搜索方向:
int dir[4][2] = { 1, 0, 0, 1, -1, 0, 0, -1 };
//这个深搜函数没有终止条件
/*void dfs(const vector<vector<int>>& gird, vector<vector<bool>>& isvisited, int loc_x, int loc_y) {
//深搜是用递归的方法
//终止条件应该是位置超出边界
for (int i = 0; i < 4; i++) { //对应的是四个方向
//先探一个方向:
int nextX = loc_x + dir[i][0];
int nextY = loc_y + dir[i][1];
if (nextX < 0 || nextX > gird.size() - 1 || nextY < 0 || nextY > gird[0].size() - 1) continue;//首先确保这个地方没有进入外围水域
//接着要看这个地方有没有探过
if (!isvisited[nextX][nextY] && gird[nextX][nextY] == 1) {
//这个地方是块陆地而且没有被探过
isvisited[nextX][nextY] = true;
dfs(gird, isvisited, nextX, nextY);//进去探
}
}
}*/
//换个标准的有截至条件的深搜
void dfs(const vector<vector<int>>& gird, vector<vector<bool>>& isvisited, int loc_x, int loc_y) {
if (loc_x < 0 || loc_x > gird.size() - 1 || loc_y < 0 || loc_y > gird[0].size() - 1) return;
if (gird[loc_x][loc_y] == 0 || isvisited[loc_x][loc_y]) return;
//顺序执行
isvisited[loc_x][loc_y] = true;
for (int i = 0; i < 4; i++) {
int nextX = loc_x + dir[i][0];
int nextY = loc_y + dir[i][1];
dfs(gird, isvisited, nextX, nextY);
}
}
int main() {
int N, M;
cin >> N >> M;
vector<vector<int>> gird(N, vector<int>(M, 0));
//搭建岛屿陆地矩阵
for (int i = 0; i < N; i++) {
for (int j = 0; j < M; j++) {
cin >> gird[i][j];
}
}
vector<vector<bool>> isvisited(N, vector<bool>(M, false));
int result = 0;
for (int i = 0; i < N; i++) {
for (int j = 0; j < M; j++) {
if (!isvisited[i][j] && gird[i][j] == 1) {
result++;
dfs(gird, isvisited, i, j);
}
}
}
cout << result << endl;
return 0;
}
10.30号刷题
岛屿的最大面积
用广搜的方法写,自己独立编写,重复手撕才能理解更深入
#include<iostream>
#include<vector>
#include<queue>
using namespace std;
//岛屿的最大面积:用广搜的方法
int dir[4][2] = { 1, 0, 0, 1, -1, 0, 0, -1 };
int maxArea = 0;
int areaCount = 0;
void bfs(const vector<vector<int>>& gird, vector<vector<bool>>& isvisited, int loc_x, int loc_y) {
queue<pair<int, int>> tempqueue;
tempqueue.push({ loc_x, loc_y });
isvisited[loc_x][loc_y] = true;
areaCount++;
//想一想,广搜能算吗?
while (!tempqueue.empty()) {//队列元素不为空时
pair<int, int> cur = tempqueue.front(); tempqueue.pop();
int currentX = cur.first;
int currentY = cur.second;
for (int i = 0; i < 4; i++) {
int nextX = currentX + dir[i][0];
int nextY = currentY + dir[i][1];
if (nextX < 0 || nextX > gird.size() - 1 || nextY < 0 || nextY > gird[0].size() - 1) continue;
if (gird[nextX][nextY] == 1 && !isvisited[nextX][nextY]) {
tempqueue.push({ nextX,nextY });
isvisited[nextX][nextY] = true;
areaCount++;
}
}
}
}
int main() {
int N, M;
cin >> N >> M;
vector<vector<int>> gird(N, vector<int>(M, 0));
for (int i = 0; i < N; i++) {
for (int j = 0; j < M; j++) {
cin >> gird[i][j];
}
}
vector<vector<bool>> isvisited(N, vector<bool>(M, false));
for (int i = 0; i < N; i++) {
for (int j = 0; j < M; j++) {
if (!isvisited[i][j] && gird[i][j] == 1) {
areaCount = 0;
bfs(gird, isvisited, i, j);
maxArea = max(maxArea, areaCount);
}
}
}
cout << maxArea;
}
孤岛的总面积
依然广搜(广搜用的少,练一下广搜)
孤岛是那些位于矩阵内部、所有单元格都不接触边缘的岛屿。注意读题:不接触边缘是最重要的。
比较简单,只需要在边缘条件判断处添加一个标志位就好了。只有当前位置在边缘,广搜的探索区域(nextX,nextY才能超出边缘)
#include<iostream>
#include<vector>
#include<queue>
using namespace std;
//岛屿的最大面积:用广搜的方法
int dir[4][2] = { 1, 0, 0, 1, -1, 0, 0, -1 };
int sumArea = 0;
int areaCount = 0;
bool edgeflag = false;
void bfs(const vector<vector<int>>& gird, vector<vector<bool>>& isvisited, int loc_x, int loc_y) {
queue<pair<int, int>> tempqueue;
tempqueue.push({ loc_x, loc_y });
isvisited[loc_x][loc_y] = true;
areaCount++;
//想一想,广搜能算吗?
while (!tempqueue.empty()) {//队列元素不为空时
pair<int, int> cur = tempqueue.front(); tempqueue.pop();
int currentX = cur.first;
int currentY = cur.second;
for (int i = 0; i < 4; i++) {
int nextX = currentX + dir[i][0];
int nextY = currentY + dir[i][1];
if (nextX < 0 || nextX > gird.size() - 1 || nextY < 0 || nextY > gird[0].size() - 1) {
edgeflag = true; continue;
}
if (gird[nextX][nextY] == 1 && !isvisited[nextX][nextY]) {
tempqueue.push({ nextX,nextY });
isvisited[nextX][nextY] = true;
areaCount++;
}
}
}
}
int main() {
int N, M;
cin >> N >> M;
vector<vector<int>> gird(N, vector<int>(M, 0));
for (int i = 0; i < N; i++) {
for (int j = 0; j < M; j++) {
cin >> gird[i][j];
}
}
vector<vector<bool>> isvisited(N, vector<bool>(M, false));
for (int i = 0; i < N; i++) {
for (int j = 0; j < M; j++) {
if (!isvisited[i][j] && gird[i][j] == 1) {
areaCount = 0;
edgeflag = false;
bfs(gird, isvisited, i, j);
if (edgeflag == false) {
sumArea += areaCount;
}
}
}
}
cout << sumArea<<endl;
}
沉没孤岛
比较简单:先判断孤岛,再补充个沉没孤岛函数就ok了。
先掌握一个方法就ok,二刷的时候可以多想几个方法,核心在于掌握图论的方法,而不是玩脑筋急转弯。
#include<iostream>
#include<vector>
#include<queue>
using namespace std;
//接下是沉没孤岛问题:问题分两步,先识别孤岛再沉没孤岛
int dir[4][2] = { 1, 0, 0, 1, -1, 0, 0, -1 };
bool edgeflag = false;
void bfs(const vector<vector<int>>& gird, vector<vector<bool>>& isvisited, int loc_x, int loc_y) {
queue<pair<int, int>> tempqueue;
tempqueue.push({ loc_x, loc_y });
isvisited[loc_x][loc_y] = true;
//想一想,广搜能算吗?
while (!tempqueue.empty()) {//队列元素不为空时
pair<int, int> cur = tempqueue.front(); tempqueue.pop();
int currentX = cur.first;
int currentY = cur.second;
for (int i = 0; i < 4; i++) {
int nextX = currentX + dir[i][0];
int nextY = currentY + dir[i][1];
if (nextX < 0 || nextX > gird.size() - 1 || nextY < 0 || nextY > gird[0].size() - 1) {
edgeflag = true; continue;
}
if (gird[nextX][nextY] == 1 && !isvisited[nextX][nextY]) {
tempqueue.push({ nextX,nextY });
isvisited[nextX][nextY] = true;
}
}
}
}
void sink(vector<vector<int>>& gird, vector<vector<bool>>& isvisited, int loc_x, int loc_y) {
queue<pair<int, int>> tempqueue;
tempqueue.push({ loc_x, loc_y });
isvisited[loc_x][loc_y] = false;
gird[loc_x][loc_y] = 0;
//想一想,广搜能算吗?
while (!tempqueue.empty()) {//队列元素不为空时
pair<int, int> cur = tempqueue.front(); tempqueue.pop();
int currentX = cur.first;
int currentY = cur.second;
for (int i = 0; i < 4; i++) {
int nextX = currentX + dir[i][0];
int nextY = currentY + dir[i][1];
if (nextX < 0 || nextX > gird.size() - 1 || nextY < 0 || nextY > gird[0].size() - 1) continue;
if (gird[nextX][nextY] == 1 && isvisited[nextX][nextY]) {
tempqueue.push({ nextX,nextY });
isvisited[nextX][nextY] = false;
gird[nextX][nextY] = 0;
}
}
}
}
int main() {
int N, M;
cin >> N >> M;
vector<vector<int>> gird(N, vector<int>(M, 0));
for (int i = 0; i < N; i++) {
for (int j = 0; j < M; j++) {
cin >> gird[i][j];
}
}
vector<vector<bool>> isvisited(N, vector<bool>(M, false));
for (int i = 0; i < N; i++) {
for (int j = 0; j < M; j++) {
if (!isvisited[i][j] && gird[i][j] == 1) {
edgeflag = false;
bfs(gird, isvisited, i, j);
if (edgeflag == false) {
//目前已经判定是孤岛,那就开始沉没孤岛
sink(gird, isvisited, i, j);
}
}
}
}
//开始输出沉没后的岛屿矩阵
for (int i = 0; i < N; i++) {
for (int j = 0; j < M - 1; j++) {
cout << gird[i][j] << " ";
}
cout << gird[i][M - 1] << endl;
}
}
水流问题
按照代码随想录的思路来:分别从第一组边界,第二组边界溯流而上,将溯流过的位置都标记上,最后看两个溯流过的数组的重合位置就是要求的坐标。
广搜的代码逻辑理清楚了,只要把问题和广搜的框架结合到一起,这题目就解决了。
#include<iostream>
#include<vector>
#include<queue>
using namespace std;
//标记四个方向
int dir[4][2] = { 1, 0, 0, 1, -1, 0, 0, -1 };
//代码随想录的思路是:分别从第一组边界,第二组边界溯流而上,将溯流过的位置都标记上,最后看两个溯流过的数组的重合位置就是要求的坐标
void bfs(const vector<vector<int>>& gird, vector<vector<bool>>& edgeReachable, int loc_x, int loc_y) {
queue<pair<int, int>> tempqueue;
tempqueue.push({ loc_x, loc_y });
edgeReachable[loc_x][loc_y] = true;
while (!tempqueue.empty()) {//队列元素不为空时
pair<int, int> cur = tempqueue.front(); tempqueue.pop();
int currentX = cur.first;
int currentY = cur.second;
for (int i = 0; i < 4; i++) {
int nextX = currentX + dir[i][0];
int nextY = currentY + dir[i][1];
if (nextX < 0 || nextX > gird.size() - 1 || nextY < 0 || nextY > gird[0].size() - 1) continue; //超出边界了此路不通
//在合理矩阵范围内
if (gird[nextX][nextY] >= gird[currentX][currentY] && !edgeReachable[nextX][nextY]) {//可以逆流并且未被标记,必须未被标记,不然的话会重复访问。
tempqueue.push({ nextX, nextY });
edgeReachable[nextX][nextY] = true;
}
}
}
}
int main() {
int N, M;
cin >> N >> M;
vector<vector<int>> gird(N, vector<int>(M, 0));
for (int i = 0; i < N; i++) {
for (int j = 0; j < M; j++) {
cin >> gird[i][j];
}
}
vector<vector<bool>> firstEdgeReachable(N, vector<bool>(M, false));
vector<vector<bool>> secondEdgeReachable(N, vector<bool>(M, false));
//先从第一边界进行回溯,用广搜//第一边界:x = 0,y = 0
//先从第一边界回溯
//x=0
for (int i = 0; i < M; i++) {
bfs(gird, firstEdgeReachable, 0, i);
}
for (int i = 1; i < N; i++) {
bfs(gird, firstEdgeReachable, i, 0);
}
//从第二边界进行回溯
for (int i = 0; i < M; i++) {
bfs(gird, secondEdgeReachable, N - 1, i);
}
for (int i = 0; i < N; i++) {
bfs(gird, secondEdgeReachable, i, M - 1);
}
//结果记录:
for (int i = 0; i < N; i++) {
for (int j = 0; j < M; j++) {
if (firstEdgeReachable[i][j] == secondEdgeReachable[i][j] && firstEdgeReachable[i][j]) {
cout << i << " " << j << endl;
}
}
}
}
10.31日刷题
建造最大孤岛
1.考虑全是陆地的情况(特殊情况是首先应该考虑的,而我最近一直忽视这一点)
2.最后阶段:gird[i][j]搜索四周的时候,四周并不全是互不相同的岛屿,不注意这一点,就会把一些岛屿的面积多加一次。
#include<iostream>
#include<vector>
#include<queue>
#include<unordered_map>
#include<unordered_set>
using namespace std;
//建造最大岛屿,最多将矩阵中的一格水变成陆地
//我没有什么思路,跟着代码随想录的思路来
//先做一次广搜,把所有岛屿面积记录下来,并对岛屿进行编号,用map把每座岛的面积记录下来,然后对于每个水域
//标记四个方向
int dir[4][2] = { 1, 0, 0, 1, -1, 0, 0, -1 };
int singleArea = 0;
int index = 1;//记录索引
void bfs(vector<vector<int>>& gird, vector<vector<bool>>& isvisited, int loc_x, int loc_y, int index) { //index代表岛屿位置记录
queue<pair<int, int>> tempqueue;
tempqueue.push({ loc_x, loc_y });
gird[loc_x][loc_y] = index;
singleArea++;
while (!tempqueue.empty()) {//队列元素不为空时
pair<int, int> cur = tempqueue.front(); tempqueue.pop();
int currentX = cur.first;
int currentY = cur.second;
for (int i = 0; i < 4; i++) {
int nextX = currentX + dir[i][0];
int nextY = currentY + dir[i][1];
if (nextX < 0 || nextX > gird.size() - 1 || nextY < 0 || nextY > gird[0].size() - 1) continue; //超出边界了此路不通
//在合理矩阵范围内
if (gird[nextX][nextY] == 1 && !isvisited[loc_x][loc_y]) {//可以逆流并且未被标记,必须未被标记,不然的话会重复访问。
tempqueue.push({ nextX, nextY });
gird[nextX][nextY] = index;
singleArea++;
}
}
}
}
int main() {
int N, M;
cin >> N >> M;
vector<vector<int>> gird(N, vector<int>(M, 0));
for (int i = 0; i < N; i++) {
for (int j = 0; j < M; j++) {
cin >> gird[i][j];
}
}
int maxArea = 0;
int sumArea = 1;
unordered_map<int, int> tempmap;//记录每个岛屿面积
vector<vector<bool>> isvisited(N, vector<bool>(M, false));
bool isAllGrid = true;//记录是否全陆地
//结果记录:
for (int i = 0; i < N; i++) {
for (int j = 0; j < M; j++) {
if (gird[i][j] == 0) isAllGrid = false;
if (!isvisited[i][j] && gird[i][j] == 1) { //有岛且未被访问过
//面积记录清0
singleArea = 0;
index++;
bfs(gird, isvisited, i, j, index);
tempmap[index] = singleArea;
}
}
}
if (isAllGrid == true) {
cout << N * M << endl;
return 0;
}
unordered_set<int> tempset;//用来去除重复计算岛屿的问题
for (int i = 0; i < N; i++) {
for (int j = 0; j < M; j++) {
if (gird[i][j] == 0) { //有岛且未被访问过
tempset.clear();
sumArea = 1;
for (int k = 0; k < 4; k++) {
int nextX = i + dir[k][0];
int nextY = j + dir[k][1];
if (nextX < 0 || nextX > gird.size() - 1 || nextY < 0 || nextY > gird[0].size() - 1) continue; //超出边界了此路不通
if (tempset.count(gird[nextX][nextY]) == 0 && gird[nextX][nextY] != 0) {
sumArea += tempmap[gird[nextX][nextY]];
tempset.insert(gird[nextX][nextY]);
}
}
maxArea = max(maxArea, sumArea);
}
}
}
cout << maxArea << endl;
}
字符串接龙
跟着代码随想录的代码思路一步一步跟下来的,这个图模型用广搜还是比较抽象的,二刷可以加深理解一下并拓宽一下思路。
//刷题:字符串接龙(ACM模式),比较特殊的广搜
//预计耗时45分钟,拓宽自己的思路
#include<iostream>
#include<string>
#include<unordered_set>
#include<queue>
#include<unordered_map>
using namespace std;
int main() {
int N;
cin >> N;
string beginStr, endStr, tempStr;
cin >> beginStr >> endStr;
unordered_set<string> strList;
for (int i = 0; i < N; i++) {
cin >> tempStr;
strList.insert(tempStr);
}
//数据输入已完毕
//开始广搜
unordered_map<string, int> visitMap;
queue<string> tempqueue;
tempqueue.push(beginStr);
visitMap.insert(pair<string, int>(beginStr, 1));
while (!tempqueue.empty()) {//队列不空就一直搜
string cur = tempqueue.front(); tempqueue.pop();
int path_length = visitMap[cur];//记录当前走了几步距离
//广搜:
for (int i = 0; i < cur.size(); i++) {
string newWord = cur;
for (int j = 0; j < 26; j++) {
newWord[i] = 'a' + j;
if (newWord == endStr) {//走到了终点
cout << path_length + 1 << endl;
return 0;
}
if (strList.find(newWord) != strList.end() && visitMap.find(newWord) == visitMap.end()) { //如果新字符串可以在strList找到,并且这个路径之前没走过,走过就变成环路了
tempqueue.push(newWord);
visitMap.insert(pair<string, int>(newWord, path_length + 1));
}
}
}
}
cout << 0 << endl;
return 0;
}
11.1日刷题
有向图的完全可达性
10.31日晚简单思路:用set记录是否都搜索到了,搜索齐了则立刻返回。
深搜,广搜都可以,先只试一下广搜,深搜后面的题目有机会再尝试多刷。
先是广搜,广搜不需要递归,需要记录访问过的路径isvisited.我这个广搜麻烦了,二刷的时候跟着代码随想录的思路再来一遍。
//有向图的完全可达性
#include<iostream>
#include<vector>
#include<unordered_set>
#include<queue>
using namespace std;
//先用广搜的思路来一下子
int main() {
int N, K, startNode, endNode;
cin >> N >> K;
vector<vector<bool>> graph(N + 1, vector<bool>(N + 1, false));
for (int i = 0; i < K; i++) {
cin >> startNode >> endNode;
graph[startNode][endNode] = true;
}
//邻接矩阵填好了
//开始广搜
//出发点为1,使用一个set记录到达过的节点
queue<int> tempqueue;
unordered_set<int> visitMemory;
vector<vector<bool>> isvisited(N + 1, vector<bool>(N + 1, false)); //记录是否有重复走过的路径
tempqueue.push(1);
visitMemory.insert(1);
while (!tempqueue.empty()) {
int cur = tempqueue.front(); tempqueue.pop();
for (int i = 1; i <= N; i++) {
if (graph[cur][i] && !isvisited[cur][i]) { //这条路径可走并且之前没有走过
tempqueue.push(i);
isvisited[cur][i] = true;
if (visitMemory.find(i) == visitMemory.end()) {
visitMemory.insert(i);
}
}
}
if (visitMemory.size() == N) {
cout << 1 << endl; return 0;
}
}
cout << -1 << endl;
return 0;
}
再试深搜:深搜要递归
岛屿的周长
代码随想录的第一个思路挺ok的:直接看一块陆地有几个方向接触边界与水域即可,广搜即可
//岛屿的周长
//看了代码随想录的一第一个想法:直接看一块陆地有几个方向接触边界与水域即可,广搜深搜都没什么大用
#include<iostream>
#include<vector>
#include<unordered_set>
#include<queue>
int dir[4][2] = { 1, 0, 0, 1, -1, 0, 0, -1 };
using namespace std;
int main() {
int N, M;
int sidelength = 0;
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++) {
for (int j = 0; j < M; j++) {
if (graph[i][j] == 1) {
for (int k = 0; k < 4; k++) {
int nearX = i + dir[k][0];
int nearY = j + dir[k][1];
if (nearX < 0 || nearX > N - 1 || nearY < 0 || nearY > M - 1 || graph[nearX][nearY] == 0) {//要么边界,要么水域
sidelength++;
}
}
}
}
}
cout << sidelength << endl;
return 0;
}
并查集理论基础
有路径压缩和按秩合并两种方法,路径压缩的方法我理解了,按秩和并的方法理解了一半,后边一边做题一边体会。
复杂度分析的话我还搞定不了。二刷针对复杂度分析要下一下功夫。
寻找存在的路径
标准的并查集题目,我在写代码的时候犯了巨大错误就是在主函数中没有进行init初始化,找问题找了15分钟。细节细节。
//寻找存在的路径
//并查集基础题目
//先写并查集的基本框架
//题目限制,数据范围:1 <= M, N <= 100
#include<iostream>
#include<vector>
using namespace std;
int len = 105;
vector<int> father(len, 0);
void init() {
for (int i = 0; i < len; i++) {
father[i] = i;
}
}
int find(int u) {
return u == father[u] ? u : father[u] = find(father[u]);
}
bool isSame(int u, int v) {
u = find(u);
v = find(v);
return u == v;
}
void join(int u, int v) {
u = find(u);
v = find(v);
if (u == v) return;
father[v] = u;
}
int main() {
//解决输入描述
int N, M;
init();
cin >> N >> M;
int sideStart, sideEnd;
for (int i = 0; i < M; i++) {
cin >> sideStart >> sideEnd;
join(sideStart, sideEnd);
}
int source, destination;
cin >> source >> destination;
if (isSame(source, destination)) {
cout << 1 << endl;
}
else cout << 0 << endl;
return 0;
}
11月2日刷题
冗余连接
比较简单,自上向下开始连接。发现一条边的起始点和终点已经在一个并查集了,自然就冗余了。由于题目说只加了一条边就冗余了,那么从上向下往并查集添加,找到的第一个冗余边就是要求的冗余边。后边不会再有其他冗余边了,因为后面不管再怎么加,也不可能把前面的那条冗余边搞掉。冗余边出现了,那就是它了。
//冗余连接
#include<iostream>
#include<vector>
using namespace std;
int len = 1005;
vector<int> father(len, 0);
void init() {
for (int i = 0; i < len; i++) {
father[i] = i;
}
}
int find(int u) {
return u == father[u] ? u : father[u] = find(father[u]);
}
bool isSame(int u, int v) {
u = find(u);
v = find(v);
return u == v;
}
void join(int u, int v) {
u = find(u);
v = find(v);
if (u == v) return;
father[v] = u;
}
int main() {
//解决输入描述
int N;
init();
cin >> N;
int sideStart, sideEnd;
bool findFlag = false;
vector<int> result(2, 0);
for (int i = 0; i < N; i++) {
cin >> sideStart >> sideEnd;
if (isSame(sideStart, sideEnd) && !findFlag) {
findFlag = true;
result[0] = sideStart;
result[1] = sideEnd;
}
if (!findFlag) join(sideStart, sideEnd);
}
cout << result[0] <<" "<< result[1] << endl;
return 0;
}
冗余连接II
这道题并不典型,但是有助于深入对并查集的特点进一步理解。
这道问题分析起来比较麻烦。是把有向图和并查集的性能结合起来。
并查集可以直接扫描无向图。
根据描述,有向树是必然无环的,这个是直接可以用并查集扫描一遍确定的。
第一步:确定要删除的边。(根据有向图的特点确定,直接用并查集只能删掉构成环的边(在无向图的范畴内))
第二步:确定删除边之后是否是有向树。(用并查集解决)
#include<iostream>
#include<vector>
using namespace std;
int len = 1005;
vector<int> father(len, 0);
void init() {
for (int i = 0; i < len; i++) {
father[i] = i;
}
}
int find(int u) {
return u == father[u] ? u : father[u] = find(father[u]);
}
bool isSame(int u, int v) {
u = find(u);
v = find(v);
return u == v;
}
void join(int u, int v) {
u = find(u);
v = find(v);
if (u == v) return;
father[v] = u;
}
bool isTreeAfterRemoveEdge(const vector<pair<int, int>>& sideMemory, const pair<int, int>& waitDelSide) {
init();
for (int i = 0; i < sideMemory.size(); i++) {
if (sideMemory[i] == waitDelSide) continue;
if (isSame(sideMemory[i].first, sideMemory[i].second)) return false;
else join(sideMemory[i].first, sideMemory[i].second);
}
return true;
}
pair<int, int> getRemoveEdge(const vector<pair<int, int>>& sideMemory) {
init();
for (int i = 0; i < sideMemory.size(); i++) {
if (isSame(sideMemory[i].first, sideMemory[i].second)) return sideMemory[i];
else join(sideMemory[i].first, sideMemory[i].second);
}
}
int main() {
int N;
init();
cin >> N;
int sideStart, sideEnd;
//冗余连接II这道问题比较复杂,需要考虑多种情况
//输入描述中,每行输入两个整数 s 和 t,代表这是 s 节点连接并指向 t 节点的单向边 ,我的理解s是父节点,t是子节点
//题目中的冗余连接是指:现在有一个有向图,有向图是在有向树中的两个没有直接链接的节点中间添加一条有向边。s->t,s是父节点,t是子节点,相对与原本的有向树来说,t这个子节点有了两个父节点
//那么代码随想录思路中可以概述为找到有两个父节点的子节点q,删除指向q的任意一条边(指向q本身有两条边)
//但指向q本身两条边又分为三种情况,这个代码随心想录中已经把图片画出来了
//首先确定每个节点都有几条边同时指向它,即找每个节点父节点的数量,数量为2就要做删除了
vector<pair<int, int>> sideMemory;
vector<int> nodeDadcout(N + 1, 0);
int waitDelSideEndnode = 0;//等于0表示没有找到节点有两个父节点的情况
for (int i = 0; i < N; i++) {
cin >> sideStart >> sideEnd;
sideMemory.push_back(pair<int, int>(sideStart, sideEnd));
nodeDadcout[sideEnd]++;
if (nodeDadcout[sideEnd] == 2) waitDelSideEndnode = sideEnd; //把要剔除的边的末端节点(节点有两个父节点)记录下来
//sideStart还没做记录,得先存储一下,存储在sideMemory中
}
//这里消灭的是前两种情况
//删除两条边,具体删除哪一条,自然是看删除之后是否还是有向树
//需要实现还是判断是否是有向树?
vector<pair<int, int>> waitDelSide;
if (waitDelSideEndnode > 0) { //说明存在有两个父节点的节点
for (int i = N; i >= 1; i--) {
if (sideMemory[i - 1].second == waitDelSideEndnode) {
waitDelSide.push_back(sideMemory[i - 1]);//优先删除最后出现的一条边
}
}
if (isTreeAfterRemoveEdge(sideMemory, waitDelSide[0])) {
//如果删除边后满足有向树,这问题就解决了
cout << waitDelSide[0].first << " " << waitDelSide[0].second;
}
else {
cout << waitDelSide[1].first << " " << waitDelSide[1].second;
}
}
else {
//还要消灭第三种情况,有有向环,消除构成环的边
pair<int, int> result = getRemoveEdge(sideMemory);
cout << result.first << " " << result.second << endl;
}
return 0;
}
最小生成树prim
我先试着按思路自行编写了一下,还行,算是一遍过,算法算是理解了。
代码随想录里的写法:我记录最小生成树用的是unordered_set,这个查找的复杂度就相对高了,代码随想录用的是一个bool类型的数组,复杂度就低很多了。
这一段是看了代码随想录的思路自行编写
//最小生成树prim
//按照自己的理解去编写,因为思路你已经在代码随想录看过一遍了,只有没有自己上手编写
//对,按着自己的思路编写,算是一个代码复现练习吧
#include<iostream>
#include<vector>
#include<stack>
#include<unordered_set>
using namespace std;
int main() {
int V, E;
cin >> V >> E;
//初始化mindistance数组
vector<int> minDistance(V + 1, 10001); //因为val的范围最大为10000
//需要存储每条边的数据,因为后面要进行遍历,用个大的图矩阵数组吧graph,优化的问题后边再说,先实现功能
vector<vector<int>> graph(V + 1, vector<int>(V + 1, INT32_MAX));
int v1, v2, val;
for (int i = 0; i < E; i++) {
cin >> v1 >> v2 >> val;
graph[v1][v2] = val;
}
//开始更新minDistance数组
//用个栈来存储那条边加进去了
int minSumDistance = 0;
unordered_set<int> minTree;
minTree.insert(1);
int tail = 1;
for (int i = 0; i < V - 1; i++) {//V个节点,最多V-1条边就足够了
for (int i = 2; i <= V; i++) {
if (minTree.find(i) == minTree.end() && min(graph[tail][i],graph[i][tail]) < minDistance[i]) minDistance[i] = min(graph[tail][i], graph[i][tail]);
}//mindistance数组更新完毕
//选择最小的距离加入最小生成树
int minDis = INT32_MAX;
for (int i = 2; i <= V; i++) {
if (minTree.find(i) == minTree.end() && minDistance[i] < minDis) {
minDis = minDistance[i];
tail = i;
}
}
minTree.insert(tail);//加入生成树
minSumDistance += minDis;
}
cout << minSumDistance << endl;
return 0;
}
最小生成树之kruskal
比较简单,二刷再加固印象。
#include<iostream>
#include<vector>
#include<stack>
#include<unordered_set>
#include<algorithm>
using namespace std;
int len = 10001;
vector<int> father(len, 0);
struct Edge {
int l, r, val;
};
void init() {
for (int i = 0; i < len; i++) {
father[i] = i;
}
}
int find(int u) {
return u == father[u] ? u : father[u] = find(father[u]);
}
bool isSame(int u, int v) {
u = find(u);
v = find(v);
return u == v;
}
void join(int u, int v) {
u = find(u);
v = find(v);
if (u == v) return;
father[v] = u;
}
const bool compare(const Edge& a, const Edge& b) {
return a.val < b.val;
}
int main() {
int V, E;
cin >> V >> E;
int v1, v2, val;
int resultSumDistance = 0;
vector<Edge> edges;
for (int i = 0; i < E; i++) {
cin >> v1 >> v2 >> val;
edges.push_back(Edge{ v1,v2,val });
}
//对边的长度进行排序
sort(edges.begin(), edges.end(), compare);
//开始遍历
init();
for (int i = 0; i < edges.size(); i++) {
if (isSame(edges[i].l, edges[i].r)) continue;
else {
resultSumDistance += edges[i].val;
join(edges[i].l, edges[i].r);
}
}
cout << resultSumDistance << endl;
return 0;
}
11月3日
身体感冒,头比较晕,状态差,休息一天。
11月4日
拓扑排序
跟着代码随想录的思路来,ok的
//拓扑排序:不考虑循环依赖,循环依赖的情况输出-1
#include<iostream>
#include<vector>
#include<queue>
#include<unordered_map>
using namespace std;
int main() {
int N, M;
cin >> N >> M; //N个文件之间有M条依赖关系
//考虑如何存储这M条依赖关系,参考代码随想录用unordered_map
unordered_map<int, vector<int>> umap;
//记录入度信息
vector<int> inDegree(N, 0);
int S, T;
for (int i = 0; i < M; i++) {
cin >> S >> T;
inDegree[T]++;
umap[S].push_back(T);//T依赖于S,umap中的key值为被依赖的文件,value为依赖key的文件
}
//开始寻找入度为0的点,把这些点给加入到queue中
queue<int> inDegreeZero;
vector<int> result;//结果集
for (int i = 0; i < N; i++) {
if (inDegree[i] == 0) inDegreeZero.push(i);
}
while (!inDegreeZero.empty()) {
int cur = inDegreeZero.front(); inDegreeZero.pop();
//cur为要从图中删除的文件,表示这个文件已经被处理了,删除cur之后,还要告诉依赖cur的文件inDegree可以减去1了
result.push_back(cur);
for (int i = 0; i < umap[cur].size(); i++) {
inDegree[umap[cur][i]]--;
if (inDegree[umap[cur][i]] == 0) inDegreeZero.push(umap[cur][i]);
}
}
if (result.size() == N) {
for (int i = 0; i < result.size() - 1; i++) {
cout << result[i] << " ";
}
cout << result[result.size() - 1] << endl;
}
else cout << -1 << endl;
}
dijkstra朴素版
自己写的时候逻辑没有任何问题,但又出现了3个问题:
1.没有考虑特殊情况:即不能到达的情况。
2.多写了一行代码:graph[E][S] = V;这道题是两地之间的耗时,不是距离,耗时的话双向时间是不一样的。
3.minDist[currentNode] + graph[currentNode][j],当graph[currentNode][j]为INT32_MAX,这两个数相加会出问题的,这个没有解决好,卡了一会时间,其他都ok.
4.visualstudio有一些编译器优化,使得有些变量debug时控制台看不到,必须自己打印,这一点代码随想录里也有讲。
for (int i = 0; i < M; i++) {
cin >> S >> E >> V;
graph[S][E] = V;
graph[E][S] = V;
}
//dijkstra(朴素版)精讲
//最短路径问题
#include<iostream>
#include<vector>
using namespace std;
int main() {
//先处理输入问题
int N, M; //N个车站,M条公路
cin >> N >> M;
//接下来要记录给出的通车路径和距离,用什么数据结构记录更好呢?先拿二维数组上
const int maxV = INT32_MAX;//假设最大距离为1000
vector<vector<int>> graph(N + 1, vector<int>(N + 1, maxV));
int S, E, V;
for (int i = 0; i < M; i++) {
cin >> S >> E >> V;
graph[S][E] = V;
}
//记录最小距离的数组
vector<int> minDist(N + 1, INT32_MAX);
minDist[1] = 0;
//记录节点是否访问过
vector<bool> isvisited(N + 1, false);
//开始更新最小距离数组
int currentNode = 1;//当前节点为1
int minValue = INT32_MAX;
for (int i = 2; i <= N; i++) { //minDist[1]已经更新过了,所以只需要更新2-N
minValue = INT32_MAX;
for (int j = 1; j <= N; j++) {
//第一个循环是用来确定要更新N轮,一直要更新到最后一轮数值
if (isvisited[j] == false && minDist[j] < minValue) { //没有被访问过的节点中到源点距离最小的点
minValue = minDist[j];
currentNode = j;
}
//循环一轮后就可以知道该选择访问哪个节点了
}
isvisited[currentNode] = true;
//更新未被访问的节点到原点的距离
for (int j = 1; j <= N; j++) {
if (isvisited[j] == false && graph[currentNode][j] != INT32_MAX) {
minDist[j] = min(minDist[currentNode] + graph[currentNode][j], minDist[j]);
}
}
}
if (minDist[N] == INT32_MAX) cout << -1 << endl;
else cout << minDist[N] << endl;
return 0;
}
11月5日
对自己的要求,要学会通过打印数组来debug,不要尝试用编译器的debug,因为一些变量会被优化掉。
dijkstra(堆优化版)精讲
还是太着急了,debug信息没写没管,二刷的时候这个题目还得加深印象
难点:
1.stl中的优先级队列我之前没有使用过。
2.操作符重载的东西忘了太多了,后面得补充。
3.结构体的初始化函数的定义也忘了。
4.优先级队列比较函数返回true,则第一个入口参数优先级靠后,出队是优先级靠前的元素先出队。
//dijkstra(堆优化)精讲
//最短路径问题
//这个堆优化版的想法是为了解决节点多但边少的情况
#include<iostream>
#include<vector>
#include<list>
#include<queue>
using namespace std;
class mycomparison {
public:
bool operator()(const pair<int, int>& lhs, const pair<int, int>& rhs) { //这算是操作符重定义
return lhs.second > rhs.second; //这里的first指的是交通终点,second指的是这段通路的耗时
}
};
struct trafficInfo {
int destination;
int spendtime;
trafficInfo(int destination, int spendtime) :destination{ destination }, spendtime{ spendtime } {};
//结构体初始化,这里涉及C++的一些内容,有些遗忘了,算法刷完之后一定要对C++的整个语法进行深一步的认识,或者说专门做一个针对C++的项目
};
int main() {
//先处理输入问题
int N, M; //N个车站,M条公路
cin >> N >> M;
//接下来要记录给出的通车路径和距离,用什么数据结构记录更好呢?堆优化版用的是邻接表
vector<list<trafficInfo>> graphInfo(N + 1);//只定义了数量,并没有定义list<trafficInfo>依然为空
int S, E, V;
for (int i = 0; i < M; i++) {
cin >> S >> E >> V;
graphInfo[S].push_back(trafficInfo{ E, V });
}
//记录最小距离的数组
vector<int> minDist(N + 1, INT32_MAX);
minDist[1] = 0;
//记录节点是否访问过
vector<bool> isvisited(N + 1, false);
//堆优化版本
//代码随想录用的是优先级队列,C++中的数据结构自己掌握的还是非常稀烂的
priority_queue<pair<int, int>, vector<pair<int, int>>, mycomparison> pq; //优先级队列,顾名思义,队列的中队列的排列顺序是有规律的,这个规律由mycomparison来体现
//优先级队列用于排列S为出发点,可以到达的终点的最短耗时所对应的终点
int startNode = 1;
int endNode = N;
pq.push(pair<int, int>(startNode, 0));
while(!pq.empty()) {
//选择距离源点最近的点
pair<int, int> cur = pq.top(); pq.pop();
//标记已经访问过
isvisited[cur.first] = true;//标记
int currentNode = cur.first;
//更新minDist数组
for (trafficInfo desttime : graphInfo[currentNode]) {
if (!isvisited[desttime.destination]) {
minDist[desttime.destination] = min(minDist[currentNode] + desttime.spendtime, minDist[desttime.destination]);
pq.push(pair<int, int>(desttime.destination, minDist[desttime.destination]));
}
}//minDist数组更新完毕
}
if (minDist[N] == INT32_MAX) cout << -1 << endl;
else cout << minDist[N] << endl;
return 0;
}
11月6日
Bellman_ford算法
浅谈一下对于Bellman_ford算法的理解:
1.引导:算法在做松弛的时候,是按顺序对每一条边进行处理,更新minDist数组。
两种顺序:
第一种:
6 7
5 6 -2
1 2 1
5 3 1
2 5 2
2 4 -3
4 6 4
1 3 5
第二种:
6 7
5 3 1
5 6 -2
4 6 4
2 5 2
2 4 -3
1 3 5
1 2 1
如果用算法对第二种顺序进行处理,那么第一轮只会对minDist[2]和minDist[3]进行处理;第二轮才会对minDist[4]和minDist[5]进行处理。
宏观理解:
dijkstra算法不能处理有负数的情况,因为这个算法本身就是默认所有路径都是正值,只有满足这个假设我才有理由选取minDist未被访问的最小值对应的节点作为下一节点。
Bellman_ford算法是可以处理带负数的情况的,这是因为在每一次松弛的过程中我走了所有的路径,并不是像dijkstra算法那样只走当前最短的那条路径。
针对第二种顺序:
6 7
5 3 1
5 6 -2
4 6 4
2 5 2
2 4 -3
1 3 5
1 2 1
第一轮先更新节点2(1->2)和节点3(1->3);
第二轮则更新节点4(1->2->4)和节点5(1->2->5);
第三轮则更新节点6,包括了(1->2->4->6和1->2->5->6),此时肯定求最小值,还更新了节点3,即(1->2->5->3),之前更新过(1->3),这两者也要取最小值。
理解:把这个东西当作给定算法,只要在直观感觉上有认识会使用就行。n个节点,更新n-1次,minDist[i]的值记为1->i节点的最低成本。二刷的时候得加深印象,目前属于感觉上理解了,但是描述不出来。
dijkstra只能处理非负数的情况。
Bellman-ford 可以处理带负数的情况,但是不能处理有负权回路的情况
按照代码随想录的思路自行编写代码:然而没理解
//Bellman_ford算法:有向图,带负数
/*
* 对自己的要求
* 1.考虑特殊情况处理
* 2.添加debug信息
*/
//代码随想录说的松弛我没看懂,先按着代码随想录的步骤复现代码
#include<iostream>
#include<vector>
#include<list>
using namespace std;
struct roadWeightInfo {
int startNode;
int endNode;
int weightValue;
roadWeightInfo(int startNode, int endNode, int weightValue) :startNode{startNode},endNode { endNode }, weightValue{ weightValue } {};
};
int main() {
int n, m;
cin >> n >> m;
//用什么数据结构来存储运输成本呢?,因为要按顺序对每条边做松弛
vector<roadWeightInfo> graph;
int s, t, v;
for (int i = 0; i < m; i++) {
cin >> s >> t >> v;
graph.push_back(roadWeightInfo{ s,t,v });
}
vector<int> minDist(n + 1, INT32_MAX);//minDist[i]含义为源节点到i号节点的最低成本
minDist[1] = 0;//1为初始节点
//更新minDist
for (int k = 0; k < n - 1; k++) {
for (int i = 0; i < graph.size(); i++) {
if (minDist[graph[i].startNode] != INT32_MAX && minDist[graph[i].startNode] + graph[i].weightValue < minDist[graph[i].endNode]) {
minDist[graph[i].endNode] = minDist[graph[i].startNode] + graph[i].weightValue;
}
}
//更新完一轮打印minDist信息
cout << "第" << k + 1 << "轮更新:" << " ";
for (int i = 1; i < minDist.size() - 1; i++) {
cout << minDist[i] << " ";
}
cout << minDist[minDist.size() - 1] << endl;
}
if (minDist[minDist.size() - 1] == INT32_MAX) cout << "unconnected" << endl;
else cout << minDist[minDist.size() - 1] << endl;
return 0;
}
11月7日
SPFA算法
做的比较好的点:
1.打印debug信息
2.考虑终止条件
//SPFA算法:有向图,带负数(相对于对Bellman_ford算法进行队列优化)
//和我在Bellman_ford算法中想到的更新流程符合一致
/*
* 对自己的要求
* 1.考虑特殊情况处理
* 2.添加debug信息
*/
#include<iostream>
#include<vector>
#include<queue>
using namespace std;
struct endNodeInfo {
int endNode;
int weight;
endNodeInfo(int endNode, int weight) :endNode{ endNode }, weight{ weight } {};
};
int main() {
int n, m;
cin >> n >> m;
//存储道路信息
vector<vector<endNodeInfo>> graph(n + 1);
int s, t, v;
for (int i = 0; i < m; i++) {
cin >> s >> t >> v;
graph[s].push_back(endNodeInfo{ t,v });
}
vector<int> minDist(n + 1, INT32_MAX);
minDist[1] = 0;
queue<int> tempqueue;
vector<bool> isinqueue(n + 1, false);//记录谁在队列里
tempqueue.push(1);//初始元素先入队
isinqueue[1] = true;
while (!tempqueue.empty()) { //需要判空吗?当然,只要有元素入队,说明路径还可以继续更新
int cur = tempqueue.front(); tempqueue.pop(); isinqueue[cur] = false;
for (int i = 0; i < graph[cur].size(); i++) {
//只要入队的节点,一定是更新过后的,即minDist[i]不是INT32_MAX
if (minDist[cur] + graph[cur][i].weight < minDist[graph[cur][i].endNode]) {
minDist[graph[cur][i].endNode] = minDist[cur] + graph[cur][i].weight;
//对这个点进行更新,那么这个点可以到达的点也需要更新,所以这个点入队
if (!isinqueue[graph[cur][i].endNode]) {
tempqueue.push(graph[cur][i].endNode);
isinqueue[graph[cur][i].endNode] = true;
}
}
}
//打印下来看一下
for (int i = 0; i < minDist.size() - 1; i++) {
cout << minDist[i] << " ";
}
cout << minDist[minDist.size() - 1] << endl;
}
if (minDist[minDist.size() - 1] == INT32_MAX) cout << "unconnected" << endl;
else cout << minDist[minDist.size() - 1] << endl;
return 0;
}
bellman_ford之判断负权回路
思路:
1.Bellman_ford算法最多更新n-1次,之后再更新就不会有什么变化,如果更新第n次,minDist数组还会变化,就可以判断有负权回路。
2.针对SPFA算法,每个节点最多会被更新n-1次,即每个节点入队次数超过n-1次,可以判定有环。
二刷时加深理解,bellman_ford/SPFA算法是要更新完所有路径的,这是相对本质的东西,bellman_ford那一题我有做思路笔记,二刷时加深理解。
11月8日
bellman_ford之单源有限最短路
核心思路:需要记录上一次更新的minDist值。
原因:如果依据当前minDist按顺序不断更新,更新了一次可能已经就把最短路径跑出来了,此时如果存在负权回路,最终结果一定是错的。
依据上一次更新的minDist值有什么作用呢?确保每一次更新只会比上一次更新多走一条边,这样可以记录中间途径了几个城市。
//Bellman_ford算法:有向图,带负数
/*
* 对自己的要求
* 1.考虑特殊情况处理
* 2.添加debug信息
*/
//代码随想录说的松弛我没看懂,先按着代码随想录的步骤复现代码
#include<iostream>
#include<vector>
#include<list>
using namespace std;
struct roadWeightInfo {
int startNode;
int endNode;
int weightValue;
roadWeightInfo(int startNode, int endNode, int weightValue) :startNode{ startNode }, endNode{ endNode }, weightValue{ weightValue } {};
};
int main() {
int n, m;
cin >> n >> m;
//用什么数据结构来存储运输成本呢?,因为要按顺序对每条边做松弛
vector<roadWeightInfo> graph;
int s, t, v;
for (int i = 0; i < m; i++) {
cin >> s >> t >> v;
graph.push_back(roadWeightInfo{ s,t,v });
}
int src, dst, k;
cin >> src >> dst >> k;
vector<int> minDist(n + 1, INT32_MAX);//minDist[i]含义为源节点到i号节点的最低成
vector<int> minDistCopy(n + 1, INT32_MAX);//保留上次的minDist数组
minDist[src] = 0;//1为初始节点
minDistCopy[src] = 0;
//更新minDist
for (int m = 0; m < k + 1; m++) {
for (int i = 0; i < graph.size(); i++) {
if (minDistCopy[graph[i].startNode] != INT32_MAX && minDistCopy[graph[i].startNode] + graph[i].weightValue < minDist[graph[i].endNode]) {
minDist[graph[i].endNode] = minDistCopy[graph[i].startNode] + graph[i].weightValue;
}
}
//做copy
for (int i = 1; i <= n; i++) {
minDistCopy[i] = minDist[i];
}
//更新完一轮打印minDist信息
cout << "第" << m + 1 << "轮更新:" << " ";
for (int i = 1; i < minDist.size() - 1; i++) {
cout << minDist[i] << " ";
}
cout << minDist[minDist.size() - 1] << endl;
}
if (minDist[dst] == INT32_MAX) cout << "unreachable" << endl;
else cout << minDist[dst] << endl;
return 0;
}
前面几道题已经把在图中求最短路径的算法基本讲了个遍,整体放在一块,对于最短路径问题就能理解很深,二刷还得再加深理解。
Floyd 算法精讲
这是个多源最短路径算法(数组更新完成后要得到任意两个节点之间的最短路径),前面都是单源最短路径算法(初始化起点,然后找终点)
比较难理解,完全按照代码随想录的思路一步一步写下来,边写边做理解。二刷再加深理解。
难点在于对grid[i][j][k]的理解:
k=1时,即在更新i->j经过节点[0,1]的最短路径距离
min(grid[i][1][0] + grid[1][j][0], grid[i][j][0]);即为i->j只经过节点1或者不经过节点1,压根不考虑经过节点2到n.
k=2时,即在更新i->j经过节点[0,2]的最短路径距离
min(grid[i][2][1] + grid[2][j][1], grid[i][j][1]);即为i->j经过节点2([0,1]且包含节点2)或者不经过节点2(经过[0,1]),压根不考虑经过节点3-n.
k=n时,即在更新i->j经过节点[0,n]的最短路径距离.
二刷时在加深理解,从k=1一直递归到k=n就包含了i->j经过中间各种路径的组合都考虑到了。比较抽象,二刷多思考多理解,或者背记。
// Floyd算法精讲:有点难度,跟着代码随想录的代码敲,一边敲代码一边想
三维版本:
#include<iostream>
#include<vector>
using namespace std;
int main() {
int N, M;//N个景点(编号为1到N)M条双向道路
cin >> N >> M;
//说的是用动态规划的方法:
//确定dp数组及其下标含义:grid[i][j][k]从i景点到j景点,路过集合k中的节点的最短路径长度
//递推公式:
//情况一:i->j经过景点k grid[i][j][k] = grid[i][k][k-1]+grid[k][j][k-1] [k-1]代表的是除k节点以外的节点集合
//情况二:i->j不经过景点k grid[i][j][k] = grid[i][j][k-1]
//初始化:(三维数组)
vector<vector<vector<int>>> grid(N + 1, vector<vector<int>>(N + 1,vector<int>(N + 1, 10005)));//因为两地之间最长道路为10000
int u, v, w;
for (int i = 0; i < M; i++) {
cin >> u >> v >> w;
grid[u][v][0] = w;//这句话的含义是u景点到v景点,0代表无中途景点,即u->v直达距离
grid[v][u][0] = w;
}
int Q;//观景计划数量
cin >> Q;
//保存观景计划列表
vector<pair<int, int>> scheme;
int start, end;
for (int i = 0; i < Q; i++) {
cin >> start >> end;
scheme.push_back({ start,end });
}
//开始遍历更新三维数组grid[i][j][k],递推公式中,k要求k-1
//初始化已经把k = 0更新过了,并且k=0时,也就是直达路径,最大值设为10005,超过题目给定最大值,也就是两节点之间有路则记录为实际路径长度,莫有路就记录为最大值,ok
for (int k = 1; k <= N; k++) {
for (int i = 1; i <= N; i++) {
for (int j = 1; j <= N; j++) {
grid[i][j][k] = min(grid[i][k][k - 1] + grid[k][j][k - 1], grid[i][j][k - 1]);
}
}
}
for (int i = 0; i < Q; i++) {
if (grid[scheme[i].first][scheme[i].second][N] != 10005) cout << grid[scheme[i].first][scheme[i].second][N] << endl;
else cout << -1 << endl;
}
return 0;
}
二维降维版本:
// Floyd算法精讲:有点难度,跟着代码随想录的代码敲,一边敲代码一边想
#include<iostream>
#include<vector>
using namespace std;
int main() {
int N, M;//N个景点(编号为1到N)M条双向道路
cin >> N >> M;
//说的是用动态规划的方法:
//确定dp数组及其下标含义:grid[i][j][k]从i景点到j景点,路过集合k中的节点的最短路径长度
//递推公式:
//情况一:i->j经过景点k grid[i][j][k] = grid[i][k][k-1]+grid[k][j][k-1] [k-1]代表的是除k节点以外的节点集合
//情况二:i->j不经过景点k grid[i][j][k] = grid[i][j][k-1]
//初始化:(三维数组)
vector<vector<int>> grid(N + 1, vector<int>(N + 1,10005));//因为两地之间最长道路为10000
int u, v, w;
for (int i = 0; i < M; i++) {
cin >> u >> v >> w;
grid[u][v] = w;//这句话的含义是u景点到v景点,0代表无中途景点,即u->v直达距离
grid[v][u] = w;
}
int Q;//观景计划数量
cin >> Q;
//保存观景计划列表
vector<pair<int, int>> scheme;
int start, end;
for (int i = 0; i < Q; i++) {
cin >> start >> end;
scheme.push_back({ start,end });
}
//开始遍历更新三维数组grid[i][j][k],递推公式中,k要求k-1
//初始化已经把k = 0更新过了,并且k=0时,也就是直达路径,最大值设为10005,超过题目给定最大值,也就是两节点之间有路则记录为实际路径长度,莫有路就记录为最大值,ok
for (int k = 1; k <= N; k++) {
for (int i = 1; i <= N; i++) {
for (int j = 1; j <= N; j++) {
grid[i][j] = min(grid[i][k] + grid[k][j], grid[i][j]);
}
}
}
for (int i = 0; i < Q; i++) {
if (grid[scheme[i].first][scheme[i].second] != 10005) cout << grid[scheme[i].first][scheme[i].second] << endl;
else cout << -1 << endl;
}
return 0;
}
11月9日
A*算法
多源最短路径问题,边的表达方式不一样
A*算法二刷还得再理解。
出现问题的点:
1.对于优先级队列的了解不足,其实就是对于stl库的掌握目前比较糟糕,这道题刷完就剩下单调栈了,接下来的一段时间把STL整体攻坚一下,攻坚下来之后再刷算法题。
首先是超时方案:
比较特殊的一点就是:如何在广搜方法下记录步骤数,代码随想录给出了movecount数组的解
决方案。
初始点movecount[startX][startY] = 0;
移动点:ovecount[nextX][nextY] = movecount[curX][curY] + 1;
同时!movecount[nextX][nextY]还充当了之前isvisited数组的角色,非常妙,
二刷再加深理解。
#include<iostream>
#include<vector>
#include<queue>
using namespace std;
int dir[8][2] = { 2,1,1,2,-1,2,-2,1,-2,-1,-1,-2,1,-2,2,-1 };//一次可以走八个位置
int movecount[1001][1001];
void bfs(int startX, int startY, int endX, int endY) {
queue<pair<int, int>> searchQueue;
searchQueue.push({ startX,startY });
while (!searchQueue.empty()) {
pair<int, int> cur = searchQueue.front(); searchQueue.pop();
int curX = cur.first;
int curY = cur.second;
if (curX == endX && curY == endY) break;
for (int j = 0; j < 8; j++) {
int nextX = cur.first + dir[j][0];
int nextY = cur.second + dir[j][1];
if (nextX < 1 || nextX > 1000 || nextY < 1 || nextY > 1000) continue; //出界了
//没出界的话就看有没有加入到队列中去
if (!movecount[nextX][nextY]) {
searchQueue.push({ nextX,nextY });
movecount[nextX][nextY] = movecount[curX][curY] + 1;
}
}
}
return;
}
int main() {
int n;
cin >> n;
int a1, a2, b1, b2;
vector<int> result;
for (int i = 0; i < n; i++) {
cin >> a1 >> a2 >> b1 >> b2;
memset(movecount, 0, sizeof(movecount));
bfs(a1, a2, b1, b2);
result.push_back(movecount[b1][b2]);
}
for (int i = 0; i < n; i++) {
cout << result[i] << endl;
}
return 0;
}
接下来是省时方案
#include<iostream>
#include<vector>
#include<queue>
#include<cstring>
using namespace std;
int dir[8][2] = { 2,1,1,2,-1,2,-2,1,-2,-1,-1,-2,1,-2,2,-1 };//一次可以走八个位置
int movecount[1001][1001];
int b1, b2;
struct Knight {
int x, y;//当前位置
int g, h, f;//g为起点到达目前遍历节点的距离,h为目前遍历节点到达终点的距离,就是起点到达终点的距离
bool operator < (const Knight& k) const { // 重载运算符, 从小到大排序
return k.f < f;
}
};
priority_queue<Knight> que;
int Heuristic(const Knight& k) { // 计算欧拉距离(b1,b2是终点)
return (k.x - b1) * (k.x - b1) + (k.y - b2) * (k.y - b2); // 统一不开根号,这样可以提高精度
}
void astar(const Knight& k) {
Knight cur, next;
que.push(k);
while (!que.empty()) {
cur = que.top(); que.pop();
if (cur.x == b1 && cur.y == b2) break;
for (int i = 0; i < 8; i++) {
next.x = cur.x + dir[i][0];
next.y = cur.y + dir[i][1];
if (next.x < 1 || next.x > 1000 || next.y < 1 || next.y > 1000) continue;
if (!movecount[next.x][next.y]) {
movecount[next.x][next.y] = movecount[cur.x][cur.y] + 1;
next.g = cur.g + 5;// 统一不开根号,这样可以提高精度,马走日,1 * 1 + 2 * 2 = 5
next.h = Heuristic(next);
next.f = next.g + next.h;
que.push(next);
}
}
}
}
int main() {
int n;
cin >> n;
int a1, a2;
vector<int> result;
for (int i = 0; i < n; i++) {
cin >> a1 >> a2 >> b1 >> b2;
memset(movecount, 0, sizeof(movecount));//清理movecount为新的一轮搜索做准备。
Knight start;
start.x = a1;
start.y = a2;
start.g = 0;//从起点到该节点的路径消耗
start.h = Heuristic(start);//该节点到终点的预估消耗
start.f = start.g + start.h;
astar(start);
while (!que.empty()) que.pop();//循环情况que,为下一轮搜索做准备
result.push_back(movecount[b1][b2]);
}
for (int i = 0; i < n; i++) {
cout << result[i] << endl;
}
return 0;
}