[Dijkstra算法]解微软暑期实习面试题

Dijkstra算法

用途: 最短路径

前提: 顶点与顶点的距离大于0

贪心策略: 当前步数最小值, 即为由起点出发到达此点的最小步数. 因为点与点间的距离都为正数, 不可能通过第三节点进一步减小最小步数.

核心步骤: 找出最小点, 访问最小点, 计算经过此最小点能否缩小其他未访问点步数

二面题:

对于给定数组vector v中下标为i的点, 在不越界的前提下, 有以下3种跳跃方式:

  1. 值相同的两个点可相互跳跃, i<->j (i≠j且num[i]=num[j])

  2. i->i+1

  3. i->i-1

求从下标为0跳跃到下标为nums.size()-1需要的最少步数

解题思路: 化为无向图, 相邻坐标间有边, 值相同的点有边. 即: 将邻接的条件dis[u][v]!=INT_MAX (其中INT_MAXu, v两点间没有边) 改为index == mini + 1 || index == mini - 1 || v[index] == v[mini]. 此时问题就转化为无向图的最短路径问题, 利用Dijkstra算法解决

int vectorGraphShortestSteps(vector<int> v)
{
	bool* visited = new bool[v.size()];// 记录是否访问过
	memset(visited, 0, sizeof(bool) * v.size());
	int* totalSteps = new int[v.size()];// 记录步数

	visited[0] = true;
	totalSteps[0] = 0;
	// 初始化起点到各点的步数
	for (int i = 1; i < v.size(); i++)
	{
		totalSteps[i] = (v[i] == v[0]) ? 1 : i;
	}

	for (int i = 1; i < v.size(); i++)
	{
		// 寻找步数最小的点
		int mini = -1;
		int min = INT_MAX;
		for (int index = 0; index < v.size(); index++)
		{
			if (!visited[index] && totalSteps[index] < min)
			{
				mini = index;
				min = totalSteps[index];
			}
		}
		if (mini == v.size() - 1) break;// 数组某尾为当前最小点,计算结束
		if (mini < 0)
		{
			cout << "没有距离小于INT_MAX的点!" << endl;
			return -1;
		}
		visited[mini] = true;

		// 经过此点步数更小的更新
		for (int index = 0; index < v.size(); index++)
		{
			if (!visited[index]
				&& (index == mini + 1 || index == mini - 1 || v[index] == v[mini])
				&& totalSteps[mini] + 1 < totalSteps[index])
			{
				totalSteps[index] = totalSteps[mini] + 1;
			}
		}
	}

	return totalSteps[v.size() - 1];
}

一面题:

一个二维数组迷宫, 一次走一步, 不能斜着走, 找出最短路径中得到金子最多的路径.

输入迷宫元素的含义如下:

# 墙

S 起点, 只有一个

E 终点, 只有一个

0-9 金子数量

解题思路: 化为无向图, 二维数组元素与紧邻最多4个非墙元素之间有边. 即: 将邻接的条件dis[u][v]!=INT_MAX (其中INT_MAXu, v两点间没有边) 改为map[r][c] != '#' && ((r == minR - 1 && c == minC) || (r == minR + 1 && c == minC) || (r == minR && c == minC - 1) || (r == minR && c == minC + 1)). 此时问题就转化为无向图的最短路径问题, 利用Dijkstra算法解决.

注意此处要求出多个最短路径, 利用pre保存前驱节点. 由前驱节点dfs找到全部最短路径. 再在全部最短路径中找到取得金子最多的路径.

即: D i j k s t r a ( p r e ) → d f s ( p a t h ) → m o s t G o l d ( p a t h [ m a x i ] ) Dijkstra( pre )\to dfs( path )\to mostGold( path[maxi]) Dijkstra(pre)dfs(path)mostGold(path[maxi])

vector<vector<int>> pre;// 前驱节点(因寻找多个最短路径,前驱节点可能不止一个,用数组存前驱节点)
vector<int> r;// 一种路径
vector<vector<int>> path;// 全部路径
// 深度优先搜索:由终点开始,寻找前驱数组中可达起点的全部路径
void dfs(int cur, int start)
{
	if (cur == start)// 终止条件
	{
		path.push_back(r);
		return;
	}

	for (auto it : pre[cur])// 遍历当前点的前驱数组
	{
		r.push_back(it);
		dfs(it, start);
		r.pop_back();
	}
}

vector<int> mazeMostGoldOfShortestPath(vector<vector<char>> map)
{

	int C = map[0].size();// 二维迷宫一维存储,行r列c对应一维下标为r*C+c,C为一共的列数
	int R = map.size();
	pre.resize(R * C);// 不resize会越界
	vector<bool> visited(R * C, 0);
	vector<int> totalSteps(R * C, INT_MAX);// 不可到达点之间的距离是INT_MAX

	visited[0] = true;
	totalSteps[0] = 0;

	// 找到起点与终点
	int startR, startC, endR, endC;// 起点与终点行(row)列(column)坐标,r表示行,c表示列
	startC = startR = endR = endC = -1;
	for (int r = 0; r < R; r++)
	{
		for (int c = 0; c < C; c++)
		{
			if (map[r][c] == 'S')
			{
				startC = c;
				startR = r;
			}
			else if (map[r][c] == 'E')
			{
				endR = r;
				endC = c;
			}
		}
	}
	if (startR < 0 || startC < 0 || endR < 0 || endC < 0)
	{
		cout << "地图中没有起点或终点!" << endl;
		return { -1 };
	}

	// 访问起点
	totalSteps[startR * C + startC] = 0;
	visited[startR * C + startC] = true;
	// 起点可一步到达(有相邻边)的点距离置为1
	if (startC+1>=0&&startC+1<C&&map[startR][startC+1]!='#')
	{
		totalSteps[startR * C + startC + 1] = 1;
		pre[startR * C + startC + 1].push_back(0);
	}
	if (startC - 1 >= 0 && startC - 1 < C && map[startR][startC - 1] != '#')
	{
		totalSteps[startR * C + startC - 1] = 1;
		pre[startR * C + startC - 1].push_back(0);
	}
	if (startR + 1 >= 0 && startR + 1 < R && map[startR+1][startC] != '#')
	{
		totalSteps[(startR+1) * C + startC] = 1;
		pre[(startR + 1) * C + startC].push_back(0);
	}
	if (startR - 1 >= 0 && startR - 1 < R && map[startR - 1][startC] != '#')
	{
		totalSteps[(startR - 1) * C + startC] = 1;
		pre[(startR - 1) * C + startC].push_back(0);
	}

	for (int i = 1; i < R * C; i++)// 最多再访问R*C-1次(因已访问过起点,次数-1)
	{
		int minR = -1;
		int minC = -1;
		int min = INT_MAX;
		// 寻找未访问过的距离最短的点,注意不是之前点能达到的最小的点
		for (int r = 0; r < R; r++)
		{
			for (int c = 0; c < C; c++)
			{
				if (!visited[r * C + c] && totalSteps[r * C + c] < min)
				{
					minR = r;
					minC = c;
					min = totalSteps[r * C + c];
				}
			}

		}
		if (minR == endR && minC == endC) break;// 已访问起点最短路径
		if (minR < 0 || minC < 0)// 到达起点前已无路可走(没有小于INT_MAX的路径,即余下点全为INT_MAX不可到达)
		{
			cout << "迷宫无解!" << endl;
			return { -1 };
		}
		visited[minR * C + minC] = true;

		// 更新经过此最小点距离变短的点
		for (int r = minR - 1; r <= minR + 1; r++)
		{
			for (int c = minC - 1; c <= minC + 1; c++)
			{
				if (r >= 0 && r < R && c >= 0 && c < C// 不越界
					&& !visited[r * C + c]// 未访问
					&& map[r][c] != '#'// 不是墙
					&& ((r == minR - 1 && c == minC) || (r == minR + 1 && c == minC) || (r == minR && c == minC - 1) || (r == minR && c == minC + 1))// 4个紧接节点
					&& totalSteps[minR * C + minC] + 1 <= totalSteps[r * C + c])
					// 关键:距离相等的点也要加入前驱数组!
				{
					if (totalSteps[minR * C + minC] + 1 < totalSteps[r * C + c])// 更新最小点,清空前驱数组
					{
						totalSteps[r * C + c] = totalSteps[minR * C + minC] + 1;
						pre[r * C + c].clear();
					}
					pre[r * C + c].push_back(minR * C + minC);// 补充最小点
				}
			}
		}
	}
	// 若要求求出最小步数,加上此行代码
	//int minSteps = totalSteps[endR * C + endC];
	// 打印前驱节点
	int cnt = 0;
	for (auto it:pre)
	{
		cout << "[" << cnt++ << "] ";
		for (auto single:it)
		{
			cout << single << " ";
		}
		cout << endl;
	}


	// dfs找出所有可能最短路径存入path
	dfs(endR * C + endC, startR * C + startC);
	// 所有最短路径中,捡到金子最多的路径
	int maxi = -1;
	int max = 0;
	for (int i = 0; i < path.size() - 1; i++)// 从终点前驱dfs,不计起点数字
	{
		int sum = 0;
		for (auto it : path[i])
		{
			int r = it / C;
			int c = it % C;
			sum += (map[r][c] - '0');
		}
		if (sum > max)
		{
			max = sum;
			maxi = i;
		}
	}

	if (maxi < 0)
	{
		cout << "没有路线可以拾到金子!" << endl;
		return { -2 };
	}
	return path[maxi];
}

测试:

int main() {
	// 地图构建
	vector<vector<char>> map;
	map.push_back({ 'S','0','1','2','3','4' });
	map.push_back({ '0','1','2','3','5','E' });
	int C = map[0].size();
	cout << "无墙时: ";
	for (auto it : mazeMostGoldOfShortestPath(map))
	{
		cout << "(" << it / C << "," << it % C << ") ";
	}
	cout << endl;
	cout << "有墙时: ";
	map[0][3] = '#';
	for (auto it : mazeMostGoldOfShortestPath(map))
	{
		cout << "(" << it / C << "," << it % C << ") ";
	}
	cout << endl;
	cout << "无法到达终点时:";
	map[1][3] = '#';
	for (auto it : mazeMostGoldOfShortestPath(map))
	{
		cout << "(" << it / C << "," << it % C << ") ";
	}
	return 0;
}

控制台输出效果:

image-20200507231229748

其实还有更简便的算法, 之前没有想到, 我果然还是太菜…

解题思路: bfs, 同一步数的点存在一轮栈中. 比如第1步为开始点, 将第一点推入栈作初始化. 记录此时栈的大小, 这次只弹出上层栈大小数量的节点, 寻找它周边的可到达的点, 压入数组, 此时数组中所有的点都是步数为1的点, 记录所有有终点的点的路径到shortestPath. 如果这些点中有终点, 就停止计算. 计算shortestPath中取得金子最多的点.

struct point {
	int x;
	int y;
};
// 栈中的点
struct node {
	int x;
	int y;
	vector <point> path;
};

vector <point> bfs(vector<vector<char>> map)
{
	queue<node> que;
	int startx=0, starty=0, endx=1, endy=5;
	int next[4][2] = { {0,1},{0,-1},{-1,0},{1,0} };
	bool flag = false;
	vector<vector<bool>> book(map.size(), vector<bool>(map[0].size(),0));
	node tmp;
	tmp.x = startx;
	tmp.y = starty;
	tmp.path.push_back({ startx,starty });
	que.push(tmp);

	vector<vector <point>> shortestPath;
	while (que.size())
	{
		int tx, ty;
		int n = que.size();

		for (size_t i = 0; i < n; i++)
		{
			
			for (size_t k = 0; k < 4; k++)
			{
				tx = que.front().x + next[k][0];
				ty = que.front().y + next[k][1];
				if (tx<0||tx>= map.size() ||ty<0||ty>=map[0].size())
					continue;
				if (map[tx][ty]=='E')
				{
					flag = true;
					shortestPath.push_back(que.front().path);
				}
				if (map[tx][ty]!='#'&& !book[tx][ty])
				{
					book[tx][ty] = true;
					tmp.x = tx;
					tmp.y = ty;
					tmp.path = que.front().path;
					tmp.path.push_back({ tx,ty });
					que.push(tmp);
				}
			}
			que.pop();
		}
		if (flag) break;
	}
	if (shortestPath.empty())
	{
		cout << " 无路可走!";
		return {};
	}
	// 遍历寻找最短路径中金子最少的点
	int maxGold = 0;
	int maxi = 0;
	for (size_t i = 0; i < shortestPath.size(); i++)
	{
		int sum = 0;
		for (size_t k = 0; k < shortestPath[i].size(); k++)
		{
			sum += map[shortestPath[i][k].x][shortestPath[i][k].y] - '0';
		}
		if (sum>maxGold)
		{
			maxi = i;
			maxGold = sum;
		}
	}
    if(maxGold == 0)
    {
        cout << "没有最短路径可以拾到金子!" <<endl;
        return {};
    }
	return shortestPath[maxi];
}

附参考此博客学习的Dijkstra算法实现程序:

Graph_Di.h ( Di在此处指Dijkstra )

#pragma once
#include <iostream>
#include <vector>
using namespace std;
struct Discript
{
	vector<int> path;// 路径
	int totalWeight;// 路径权值
	bool visited;// 是否访问过
	Discript()
	{
		totalWeight = 0;
		visited = false;
	}
};

class Graph_Di
{
private:
	int** adjMatrix;// 邻接矩阵
	int vexNum;// 顶点数
	int edgeNum;// 边数
	Discript* dis;// 每个点的路径及路径权值及是否访问信息

	bool isValidEdge(int start, int end, int weight);
public:
	Graph_Di(int vexNum, int edgeNum);
	~Graph_Di();

	void createGraph();
	void printAdjacentMatrix();
	void Dijkstra(int start);
	void printPath();
};

Graph_Di.cpp

#include "Graph_Di.h"
string line(100, '-');

bool Graph_Di::isValidEdge(int start, int end, int weight)
{
	return start > 0 && start <= vexNum
		&& end > 0 && end <= vexNum
		&& weight > 0;
}

Graph_Di::Graph_Di(int vexNum, int edgeNum) :vexNum(vexNum), edgeNum(edgeNum)
{
	if (vexNum < 0 || edgeNum < 0 || vexNum * (vexNum - 1) / 2 < edgeNum)// 两两组合,第一个有vexNum个选择,第二个有vexNum-1个选择,二者顺序无关/2,则两两组合最多vexNum*(vexNum-1)/2个
	{
		cout << "顶点数及边数必须大于零,且边数不得超过vexNum * (vexNum - 1) / 2!" << endl;
		return;
	}

	adjMatrix = new int* [vexNum];
	dis = new Discript[vexNum];
	for (size_t i = 0; i < vexNum; i++)
	{
		adjMatrix[i] = new int[vexNum];
		for (size_t j = 0; j < vexNum; j++)
		{
			adjMatrix[i][j] = INT_MAX;// 距离初始化为INT_MAX,意为不可到达
		}
	}
}

Graph_Di::~Graph_Di()
{
	if (adjMatrix)
	{
		for (size_t i = 0; i < vexNum; i++)
		{
			delete[] adjMatrix[i];
		}
		delete[] adjMatrix;
		adjMatrix = nullptr;
	}
	if (dis)
	{
		delete[] dis;
		dis = nullptr;
	}
}

void Graph_Di::createGraph()
{
	cout << "请依次输入起点,终点,以及它们之间的距离:" << endl;
	int start, end, weight;
	for (size_t i = 0; i < edgeNum; i++)
	{
		cin >> start >> end >> weight;
		while (!isValidEdge(start, end, weight))
		{
			cout << "输入点不合法,请重新输入:" << endl;
			cin >> start >> end >> weight;
		}
		// start,end从1开始,其对应下标-1
		adjMatrix[start - 1][end - 1] = weight;
		// 无向图添加此行代码
		//adjMatrix[end - 1][start - 1] = weight;
	}
}

void Graph_Di::printAdjacentMatrix()
{
	cout << line << endl;
	cout << "邻接矩阵:" << endl;
	for (size_t i = 0; i < vexNum; i++)
	{
		for (size_t j = 0; j < vexNum; j++)
		{
			if (adjMatrix[i][j] == INT_MAX)
			{
				cout << "∞\t";
			}
			else
			{
				cout << adjMatrix[i][j] << "\t";
			}
		}
		cout << endl;
	}
	cout << line << endl;
}

void Graph_Di::Dijkstra(int start)
{
	// 初始化开始点
	for (size_t i = 0; i < vexNum; i++)
	{
		dis[i].totalWeight = adjMatrix[start - 1][i];
		dis[i].path.push_back(start);
	}
	dis[start - 1].totalWeight = 0;
	dis[start - 1].visited = true;

	for (size_t i = 1; i < vexNum; i++)
	{
		int mini = -1;
		int min = INT_MAX;// 最小值初始化为INT_MAX
		for (size_t vex = 0; vex < vexNum; vex++)
		{
			if (!dis[vex].visited && dis[vex].totalWeight < min)
			{
				mini = vex;
				min = dis[vex].totalWeight;
			}
		}
		if (mini < 0)//有无法到达的点则终止计算
		{
			cout << "有无法到达的点!" << endl;
			return;
		}
		dis[mini].path.push_back(mini + 1);
		dis[mini].visited = true;
		// 经过这点去往其他未访问点的距离会不会更小
		for (size_t vex = 0; vex < vexNum; vex++)
		{
			if (!dis[vex].visited && adjMatrix[mini][vex] != INT_MAX //此点未访问,可通过出度点到达此点
				&& dis[vex].totalWeight > dis[mini].totalWeight + adjMatrix[mini][vex])
			{
				dis[vex].path.clear();
				dis[vex].path = dis[mini].path;
				dis[vex].totalWeight = dis[mini].totalWeight + adjMatrix[mini][vex];
			}
		}
	}
}

void Graph_Di::printPath()
{
	cout << line << endl;
	cout << "最短路径:" << endl;
	for (size_t i = 0; i < vexNum; i++)
	{
		cout << "[V" << i + 1 << "] ";
		if (dis[i].totalWeight == INT_MAX)
		{
			cout << "∞: ";
		}
		else
		{
			cout << dis[i].totalWeight << ": ";
		}
		for (auto it : dis[i].path)
		{
			cout << it << " ";
		}
		cout << endl;
	}
	cout << line << endl;
}

main.cpp (测试文件)

#include "Graph_Di.h"

int main()
{
	Graph_Di graph(6, 8);
	graph.createGraph();
	graph.printAdjacentMatrix();
	graph.Dijkstra(1);
	graph.printPath();
}

控制台输入测试内容:

1 3 10
1 5 30
1 6 100
2 3 5
3 4 50
4 6 10
5 6 60
5 4 20

控制台输出效果:

image-20200507173604898
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值