图(中)
在上一节的时候曾说过了图的两种遍历方式,在这一节将使用他们做更深层的应用,研究从一个点到另一个点的最短距离。
最短路径问题
单源无权图的最短路径
基本思想是,按照非递减的顺序,找出各个点的最短路。
很容易想到按照非递减的顺序,也就是优先从原点开始,不断的计算与他相距最近的点的距离,整个的过程就是一个BFS。
在bfs的过程中,我们之前是用一个布尔类型的数组来保存一个节点是否被访问。现在我们可以将其改成为int类型的二维数组,同时该数组还需要实现两个功能,对于第I个节点Vertex i,p[ i ][ 0 ]用于保存它到原点的距离,而p[ i ][ 1 ]用于保存最短路径中他被哪个节点所指。在输出最短路径时,我们只需要沿着它的路径倒着走直到原点,反一下就是正确的路径了。
//无全权图最短路径
int p[MAXSIZE][2];
void UnweightShortestPath(Graph G, Vertex x) {
for (int i = 0; i < MAXSIZE; i++)
{
memset(p[i], -1, sizeof(p[i]));
}
//自己到自己的距离是0
p[x][0] = 0;
Queue Q = makeempty();
QueueAdd(x, Q);
while (!isEmpty(Q))
{
Vertex v = QueueDelete(Q);
for (int i = 0; i < G->Nvertex; i++)
{
if (G->graph[v][i] != 0 && p[i][0]==-1) {
p[i][0] = p[v][0] + 1;
p[i][1] = v;
QueueAdd(i, Q);
}
}
}
int end;
scanf_s("%d", &end);
stack<int> v;
}
单源有权图的最短路径(Dijkstra算法)
Dijkstra算法的基本算法思想是:创建一个集合S,不断的向该集合中添加顶点,每次移出时确定了该顶点的最短的。随着不断的添加,移除使得所有顶点的最短路径都被确定为最小。(贪心算法的思想)
具体实现是首先从原点开始,将其标记为已读(代表该点在整个图中最短路径确定 ),将它的所有连接点加入集合中,并将这些点到原点的最短距离设置为权值,并标记他们指向原点(在确定了最短路径打印时需要用)。
随后从集合中移除距离最小的一个,将该节点标记为已读(该点最短路径一定已经确定,因为他是原点的连接点中距离最短的那个,如果从原点经过其他的节点到他,距离必定会更大)。
将该节点的邻接点加入集合,并将这些点到原点的最短距离设置为移除点的最短距离加上自身到移除点的距离和自己原来最短距离中的最小值。
即\(dp[ i ] = Min\){\(dp[ i ] ,dp[ v ]+weight[ v ][ i ]\)}(\(dp[ i ]\)表示i点到远点的最短距离\(weight[ v ][ i ]\)v到i的距离)。
递归的则运行直到所有的点都被集合收入又移出结束。
注意到:在上面的算法中我们是不断的距离从近到远,在图中如果有负值出现的话,我们将会在这个负值圈内打转。该算法再存在负值的权值是无效的。
#define INFINITY 10000000
//不同的编辑器该值并不相同,可能会报导致值为0
//find MinDis
Vertex findMin(Graph G,int dis[],bool collection[]) {
Vertex minV = 0;
int minDis = INFINITY;
for (Vertex i = 0; i < G->Nvertex; i++)
{
if (collection[i] == false && dis[i] < minDis) {
minDis = dis[i];
minV = i;
}
}
//如果最短距离被改变,说明有找到
if (minDis < INFINITY) {
return minV;
}
return notFound;
}
//Dijkstra
//注意,在该有权值图中,应是无边连接的两点为正无穷大
bool Dijkstra(Graph G, Vertex x) {
//save information
int shortDis[MAXSIZE];
bool collection[MAXSIZE];
int path[MAXSIZE];
memset(shortDis, INFINITY, sizeof(shortDis));
memset(collection, false, sizeof(collection));
memset(path, -1, sizeof(path));
//先将起始点的连接点距离初始化
for (Vertex i = 0; i <= G->Nvertex; i++)
{
shortDis[i] = G->graph[x][i];
if (shortDis[i] < INFINITY) {
path[i] = x;
}
}
collection[x] = true;
shortDis[x] = 0;
while (true)
{
Vertex v = findMin(G, shortDis, collection);
if (v == notFound) {
break;
}
collection[v] = true;
for (Vertex i = 0; i <= G->Nvertex; i++)
{
if (G->graph[v][i] < INFINITY && collection[i] == false) {
//存在负值圈算法无法正常进行
if (G->graph[v][i] < 0) {
return false;
}
/*
* 移除点的最短距离加上自身到移除点的距离小于自己原来最短距离。
*/
if (shortDis[i] > shortDis[v] + G->graph[v][i]) {
shortDis[i] = shortDis[v] + G->graph[v][i];
path[i] = v;
}
}
}
}
return true;
}
多源有权图的最短路径(Floyd算法)
在上面的方法中为了求某一点到与固定点的最短距离,使用Dijkstra算法,现在我们要求任意两点间的最短距离,我们当然可以将所有的点都用一次Dijkstra算法来得到,以后更简单更快的方法是使用Floyd算法。
Floyd算法的算法思路是:
用一个二维数组dp[ ][ ]来保存第I个点到第J这个点的最短距离。
每次循环尝试在I到J的路径中,加入第K个节点(k = 1.....N)试试会不会让路径变短,如果路径会变短即(\(dp[ i ][ j ]<dp[ i ][ k ]+dp[ k ][ j ]\)),就更新\(dp[ i ][ j ]=dp[ i ][ k ]+dp[ k ][ j ]\),否则不变。经过K轮循环之后,我们将会得到所有的任意两点间的最短路径同样的 。
该算法如何进行:
初始的时候我们令数组保存的是原来图中两点的距离,并且将自己指向自己的距离设为1。在插入第1个节点时(在程序中下标为0),我们注意到在起始点或结束点有该第一个节点的话,是不会改变的,即\(dp[ i ][ 0 ]==dp[ i ][ 0 ]+dp[ 0 ][ 0 ]\)或\(dp[ 0 ][ j ]==dp[ 0 ][ 0 ]+dp[ 0 ][ j ]\),注意到\(dp[ i ][ j ] = Min(dp[ i ][ 0 ]+dp[ 0 ][ j ],dp[ i ][ j ])\)。循环着往下走我们发现,每次\(dp[ i ][ j ]\)的值改变,一定是由第K行和第K列决定,而该列在此轮外循环中,值绝对不会发生改变。
同上面的算法一样该算法在存在负值圈的时候,也会出问题。
bool Floyd(Graph G) {
int dp[MAXSIZE][MAXSIZE];
int path[MAXSIZE][MAXSIZE];
for (int i = 0; i <= G->Nvertex; i++)
{
for (int j = 0; j <= G->Nvertex; j++)
{
if (i == j) {
dp[i][j] = 0;
}
else {
dp[i][j] = G->graph[i][j];
}
}
}
for (int k = 0; k < G->Nvertex; k++)
{
for (int i = 0; i < G->Nvertex; i++)
{
for (int j = 0; j < G->Nvertex; j++)
{
if (dp[i][j] > dp[i][k] + dp[k][j]) {
dp[i][j] = dp[i][k] + dp[k][j];
if (i == j && dp[i][j] < 0) {
return false;
}
path[i][j] = k;
}
}
}
}
return true;
}
课后练习题(三个小题)
07-图4 哈利·波特的考试 (25point(s))
哈利·波特要考试了,他需要你的帮助。这门课学的是用魔咒将一种动物变成另一种动物的本事。例如将猫变成老鼠的魔咒是haha,将老鼠变成鱼的魔咒是hehe等等。反方向变化的魔咒就是简单地将原来的魔咒倒过来念,例如ahah可以将老鼠变成猫。另外,如果想把猫变成鱼,可以通过念一个直接魔咒lalala,也可以将猫变老鼠、老鼠变鱼的魔咒连起来念:hahahehe。
现在哈利·波特的手里有一本教材,里面列出了所有的变形魔咒和能变的动物。老师允许他自己带一只动物去考场,要考察他把这只动物变成任意一只指定动物的本事。于是他来问你:带什么动物去可以让最难变的那种动物(即该动物变为哈利·波特自己带去的动物所需要的魔咒最长)需要的魔咒最短?例如:如果只有猫、鼠、鱼,则显然哈利·波特应该带鼠去,因为鼠变成另外两种动物都只需要念4个字符;而如果带猫去,则至少需要念6个字符才能把猫变成鱼;同理,带鱼去也不是最好的选择。
输入格式:
输入说明:输入第1行给出两个正整数N (≤100)和M,其中N是考试涉及的动物总数,M是用于直接变形的魔咒条数。为简单起见,我们将动物按1~N编号。随后M行,每行给出了3个正整数,分别是两种动物的编号、以及它们之间变形需要的魔咒的长度(≤100),数字之间用空格分隔。
输出格式:
输出哈利·波特应该带去考场的动物的编号、以及最长的变形魔咒的长度,中间以空格分隔。如果只带1只动物是不可能完成所有变形要求的,则输出0。如果有若干只动物都可以备选,则输出编号最小的那只。
输入样例:
输出样例:
题解:
建立一张图后,直接弗洛伊德,因为对该题,要从一只动物变成其他动物,所以是一个无向连通图。弗洛伊德后如果发现从一节点到另一节点无法达到(值为无穷大 )说明只带一条动物必定无法完成任务,直接返回0。可以带抑制动物完成任务的话,依次扫描每只动物变成其他动物的最大咒语长度,取最小值输出。
#include <cstdio>
#include<algorithm>
#include <stdlib.h>
#include <string.h>
#define WeightType int
#define MAXSIZE 100
#define Vertex int
#define notFound -1
#define INFINITY 10000000
using namespace std;
//Use the adjacency matrix to represent the graph
typedef struct GNode* Graph;
struct GNode
{
int Nvertex;
int Nedge;
WeightType graph[MAXSIZE][MAXSIZE];
};
typedef struct ENode* Edge;
struct ENode
{
Vertex begin;
Vertex end;
WeightType weight;
};
//build edge
Edge BuildEdge(Vertex begin, Vertex end, WeightType weight) {
Edge e = (Edge)malloc(sizeof(struct ENode));
e->begin = begin;
e->end = end;
e->weight = weight;
return e;
}
//creat empty graph
Graph CreateGraph(int VertexNum) {
Graph G = (Graph)malloc(sizeof(struct GNode));
G->Nvertex = VertexNum;
G->Nedge = 0;
for (int i = 0; i < G->Nvertex; i++)
{
for (int j = 0; j < G->Nvertex; j++)
{
G->graph[i][j] = INFINITY;
}
}
return G;
}
//insert edge
void InsertEdge(Graph G, Edge e) {
G->graph[e->begin][e->end] = e->weight;
//If it is an undirected graph, you need to add the following
G->graph[e->end][e->begin] = e->weight;
G->Nedge++;
}
//build graph
Graph BuildGraph() {
int Nvertex, Nedge;
scanf("%d %d", &Nvertex, &Nedge);
Graph G = CreateGraph(Nvertex);
for (int i = 0; i < Nedge; i++)
{
Vertex begin, end;
WeightType weight;
scanf("%d %d %d", &begin, &end, &weight);
InsertEdge(G, BuildEdge(begin-1, end-1, weight));
}
return G;
}
bool Floyd(Graph G,int dp[][MAXSIZE]) {
for (int i = 0; i < G->Nvertex; i++)
{
for (int j = 0; j < G->Nvertex; j++)
{
if (i == j) {
dp[i][j] = 0;
}
else {
dp[i][j] = G->graph[i][j];
}
}
}
for (int k = 0; k < G->Nvertex; k++)
{
for (int i = 0; i < G->Nvertex; i++)
{
for (int j = 0; j < G->Nvertex; j++)
{
if (dp[i][j] > dp[i][k] + dp[k][j]) {
dp[i][j] = dp[i][k] + dp[k][j];
}
}
}
}
return true;
}
void findMin(Graph G,int dp[][MAXSIZE]) {
int mindis = INFINITY;
int minpat;
for (int i = 0; i < G->Nvertex; i++)
{
int linemindis = 0;
for (int j = 0; j < G->Nvertex; j++)
{
if (dp[i][j] == INFINITY) {
printf("0\n");
return ;
}
else if (dp[i][j]>linemindis) {
linemindis = dp[i][j];
}
else {
}
}
if (linemindis<mindis) {
mindis = linemindis;
minpat = i + 1;
}
}
printf("%d %d\n",minpat,mindis);
}
int main() {
Graph G = BuildGraph();
int dp[MAXSIZE][MAXSIZE];
Floyd(G,dp);
findMin(G, dp);
return 0;
}
07-图5 Saving James Bond - Hard Version (30point(s))
This time let us consider the situation in the movie "Live and Let Die" in which James Bond, the world's most famous spy, was captured by a group of drug dealers. He was sent to a small piece of land at the center of a lake filled with crocodiles. There he performed the most daring action to escape -- he jumped onto the head of the nearest crocodile! Before the animal realized what was happening, James jumped again onto the next big head... Finally he reached the bank before the last crocodile could bite him (actually the stunt man was caught by the big mouth and barely escaped with his extra thick boot).
Assume that the lake is a 100 by 100 square one. Assume that the center of the lake is at (0,0) and the northeast corner at (50,50). The central island is a disk centered at (0,0) with the diameter of 15. A number of crocodiles are in the lake at various positions. Given the coordinates of each crocodile and the distance that James could jump, you must tell him a shortest path to reach one of the banks. The length of a path is the number of jumps that James has to make.
Input Specification:
Each input file contains one test case. Each case starts with a line containing two positive integers N (≤100), the number of crocodiles, and D, the maximum distance that James could jump. Then N lines follow, each containing the (x,y) location of a crocodile. Note that no two crocodiles are staying at the same position.
Output Specification:
For each test case, if James can escape, output in one line the minimum number of jumps he must make. Then starting from the next line, output the position (x,y) of each crocodile on the path, each pair in one line, from the island to the bank. If it is impossible for James to escape that way, simply give him 0 as the number of jumps. If there are many shortest paths, just output the one with the minimum first jump, which is guaranteed to be unique.
Sample Input 1:
Sample Output 1:
Sample Input 2:
Sample Output 2:
题解:
我的思路是通过,先将所有点读入,并将它们记作0.....N。再用这些点之间是否连通(能不能从一个点跳到另一个点)去创建图。随后按照无权图的单元最短路径算法,计算出所有通路,选择那些可以跳到岸边的点,检查他们是否可以连接到原点(用上一个点是否指向-1判断),同时选择最短的那一条,如果出现距离相同的点,路径倒回到第一跳的时候,寻找最短的那个。
注意在第一跳直接可以跳到岸上的时候,另外做一个处理,因为原点的指向也是-1 。
08-图7 公路村村通 (30point(s))
现有村落间道路的统计数据表中,列出了有可能建设成标准公路的若干条道路的成本,求使每个村落都有公路连通所需要的最低成本。
输入格式:
输入数据包括城镇数目正整数N(≤1000)和候选道路数目M(≤3N);随后的M行对应M条道路,每行给出3个正整数,分别是该条道路直接连通的两个城镇的编号以及该道路改建的预算成本。为简单起见,城镇从1到N编号。
输出格式:
输出村村通需要的最低成本。如果输入数据不足以保证畅通,则输出−1,表示需要建设更多公路。
输入样例:
输出样例:
题解:
这是一道双条件的比较,使用Dijkstra算法,对开始点做最短路径,于基本Dijkstra算法不同的是,原算法中只有一个权重,该题包含主要权重和次级权重,相比原来附加条件为当距离相同时,优先选择钱花费最少。
#include <cstdio>
#include<algorithm>
#include <stdlib.h>
#include <string.h>
#include <stack>
#define WeightType int
#define MAXSIZE 500
#define Vertex int
#define notFound -1
#define INFINITY 10000000
using namespace std;
int shortDis[MAXSIZE];
int minMoney[MAXSIZE];
bool collection[MAXSIZE];
int beginp,endp;
//Use the adjacency matrix to represent the graph
typedef struct GNode* Graph;
struct GNode
{
int Nvertex;
int Nedge;
WeightType graph[MAXSIZE][MAXSIZE][2];
};
typedef struct ENode* Edge;
struct ENode
{
Vertex begin;
Vertex end;
WeightType weight1;
WeightType weight2;
};
//build edge
Edge BuildEdge(Vertex begin, Vertex end, WeightType weight1, WeightType weight2) {
Edge e = (Edge)malloc(sizeof(struct ENode));
e->begin = begin;
e->end = end;
e->weight1 = weight1;
e->weight2 = weight2;
return e;
}
//creat empty graph
Graph CreateGraph(int VertexNum) {
Graph G = (Graph)malloc(sizeof(struct GNode));
G->Nvertex = VertexNum;
G->Nedge = 0;
for (int i = 0; i < G->Nvertex; i++)
{
for (int j = 0; j < G->Nvertex; j++)
{
G->graph[i][j][0] = INFINITY;
G->graph[i][j][1] = INFINITY;
}
}
return G;
}
//insert edge
void InsertEdge(Graph G, Edge e) {
G->graph[e->begin][e->end][0] = e->weight1;
G->graph[e->begin][e->end][1] = e->weight2;
//If it is an undirected graph, you need to add the following
G->graph[e->end][e->begin][0] = e->weight1;
G->graph[e->end][e->begin][1] = e->weight2;
G->Nedge++;
}
//build graph
Graph BuildGraph() {
int Nvertex, Nedge ,begin,end;
scanf("%d %d %d %d", &Nvertex, &Nedge,&begin,&end);
beginp = begin;
endp = end;
Graph G = CreateGraph(Nvertex);
for (int i = 0; i < Nedge; i++)
{
Vertex begin, end;
WeightType weight1;
WeightType weight2;
scanf("%d %d %d %d", &begin, &end, &weight1,&weight2);
InsertEdge(G, BuildEdge(begin, end, weight1,weight2));
}
return G;
}
//find MinDis
Vertex findMin(Graph G, int dis[],int money[], bool collection[]) {
Vertex minV = 0;
int minDis = INFINITY;
int minmoney = INFINITY;
for (Vertex i = 0; i < G->Nvertex; i++)
{
if (collection[i] == false) {
if (dis[i] < minDis) {
minDis = dis[i];
minmoney = money[i];
minV = i;
}
else if (dis[i] != INFINITY&&dis[i] == minDis&& minmoney > money[i]) {
minDis = dis[i];
minmoney = money[i];
minV = i;
}
}
}
//如果最短距离被改变,说明有找到
if (minDis < INFINITY) {
return minV;
}
return notFound;
}
//Dijkstra
//注意,在该有权值图中,应是无边连接的两点为正无穷大
bool Dijkstra(Graph G, Vertex x) {
//save information
memset(shortDis, INFINITY, sizeof(shortDis));
memset(minMoney, INFINITY, sizeof(minMoney));
memset(collection, false, sizeof(collection));
//先将起始点的连接点距离初始化
for (Vertex i = 0; i < G->Nvertex; i++)
{
shortDis[i] = G->graph[x][i][0];
minMoney[i] = G->graph[x][i][1];
}
collection[x] = true;
shortDis[x] = 0;
while (true)
{
Vertex v = findMin(G, shortDis, minMoney,collection);
if (v == notFound) {
break;
}
collection[v] = true;
for (Vertex i = 0; i < G->Nvertex; i++)
{
if (G->graph[v][i][0] < INFINITY && collection[i] == false) {
/*
* 移除点的最短距离加上自身到移除点的距离小于自己原来最短距离。
*/
if (shortDis[i] > shortDis[v] + G->graph[v][i][0]) {
shortDis[i] = shortDis[v] + G->graph[v][i][0];
minMoney[i] = G->graph[v][i][1] + minMoney[v];
}
else if(shortDis[i] == shortDis[v] + G->graph[v][i][0])
{
if (minMoney[i] > G->graph[v][i][1] + minMoney[v]) {
shortDis[i] = shortDis[v] + G->graph[v][i][0];
minMoney[i] = G->graph[v][i][1] + minMoney[v];
}
}
}
}
}
return true;
}
int main() {
Graph G = BuildGraph();
Dijkstra(G, beginp);
printf("%d %d\n", shortDis[endp], minMoney[endp]);
}