DS博客作业04--图

0.PTA得分截图

1.本周学习总结

1.1 总结图内容

1.1.1 图存储结构:

  • 邻接矩阵:其实就是一个二维数组。对于带权图来说,数组中的元素代表边的权值,例如g.edges[1][2]=3代表顶点1与顶点2有边,且边的权值为3,如果两个顶点不连通的话,一般用无穷大来表示,即g.edges[1][2]=∞,由于VS编译器中没有无穷的表达方式,所以一般取一个非常大的数值来表示;有向图的邻接矩阵是对称矩阵,关于主对角线对称;但是无向图的邻接矩阵不一定对称。
  • 邻接表:顾名思义,就是用链的形式来存储图,即为每一个顶点建立一条单链表,每条链表的结点元素为与该顶点连接的顶点。例如:顶点A分别于B、C、D连接,则在以A为表头的链表中有三个结点:B、C、D,结点的顺序与建表的方式有关,一般采用头插法。

1.1.2图遍历及应用:

  • DFS:也叫深度优先搜索。与树的深度遍历类似,也是从图中某个顶点v出发,找出v的第一个未被访问的邻接点,然后访问该顶点,然后再以该顶点为新的顶点,重复上述步骤,直到所有顶点都被访问一次为止。DFS可以用来判断连通图和非连通图:当遍执行完DFS函数后,如果还有顶点未被访问,说明该图为非连通图。具体代码如下:
void DFS(MGraph g, int v)//邻接矩阵深度遍历 
{
	static int n = 0;
	int j;
	if (!visited[v])
	{
		if (!n)
		{
			cout << v;
			n++;
		}
		else
		{
			cout << " " << v;
		}
		visited[v] = 1;
	}
	for (j=1; j <= g.n; j++)
	{
		if (g.edges[v][j] && !visited[j])
		{
			DFS(g, j);
		}
	}
}

void DFS(AdjGraph* G, int v)//邻接表,v节点开始深度遍历 
{
	static int n = 0;

	ArcNode* p;

	visited[v] = 1;

	if (!n)
	{
		cout << v;
		n++;
	}
	else
	{
		cout << " " << v;
		n++;
	}
	p = G->adjlist[v].firstarc;
	while (p )
	{
		if (!visited[p->adjvex])
		{
			DFS(G, p->adjvex);
		}
		p = p->nextarc;
	}
}
  • BFS:也叫广度优先搜索。图的广度优先搜索类似于树的层次遍历,也需要借助一个辅助队列来实现。例如:从顶点v0开始遍历,先将v0入队,访问v0并出队,然后将v0的所有未被访问过的邻接点入队;然后再出队一个顶点,重复以上操作,直到队空。BFS也可以用来判断图是否连通,即当队空时,如果还有顶点未被访问,则该图为非连通图,但仅限于无向图。具体代码如下:
void BFS(MGraph g, int v)//邻接矩阵广度遍历 
{
	int i, j, x, n = 0;
	queue<int >q;
	if (!visited[v])
	{
		cout << v;
		visited[v] = 1;
		q.push(v);
	}
	while (!q.empty())
	{
		x = q.front();
		q.pop();
		for (j = 1; j <= g.n; j++)
		{
			if (g.edges[x][j] && !visited[j])
			{
				cout << " " << j;
				visited[j] = 1;
				q.push(j);
			}
		}
	}
}

void BFS(AdjGraph* G, int v) //邻接表,v节点开始广度遍历 
{
	queue<int> t;
	int q;
	ArcNode* p;

	cout << v;
	t.push(v);
	visited[v] = 1;
	while (!t.empty())
	{
		q = t.front();
		t.pop();
		p = G->adjlist[q].firstarc;
		while (p)
		{
			if (!visited[p->adjvex])
			{
				cout << " " << p->adjvex;
				visited[p->adjvex] = 1;
				t.push(p->adjvex);
			}
			p = p->nextarc;
		}
	}
}
  • 如何判断图是否连通:除了上面的两种图遍历方式可以用来判断图是否连通外,还可以用并查集来判断图的连通性。即通过并查集逐步合并所有顶点,看最后有几个集合。如果只有一个集合的话说明图是连通的,否则不连通。
  • 如何查找图路径:可以通过图遍历来查找图路径,从顶点出发开始进行图遍历,当遍历到目标顶点时停止。但需要额外增设一个一维数组path[],用来存放路径中的顶点的前驱,以便输出路径。
  • 如何找最短路径:有两种方法:①Dijkstra算法:定义两个集合,两个一维数组,一个集合S用来存放最短路径上的顶点,一个集合T用来存放其他顶点,初始化S为V0,初始化Dist数组为源点到其他顶点的距离,如果有边的话就赋予其边的权值,如果没边的话其值就置为无穷,初始化path数组除源点的path值为自身外,其他的值都为1;从T中选取一个距离值为最小的顶点W,加入S,然后对T中顶点的距离值进行修改,如果加入W做中间结点后,源点到其他顶点的距离值比不加W的路径要短,则修改此距离值;重复以上步骤,直到S中包含所有顶点为止。 ②Floyd算法:用邻接矩阵存储图,定义两个二维数组,一个A0用来存放各个结点之间的最短路径,一个path数组用来存放两个顶点之间的最短路径所经过的顶点;开始时初始化所有A[i][j]=g.edges[i][j],然后对所有顶点进行加入判断,例如:加入A顶点后,B到A再到C的距离是否比不加入A顶点时B到C的距离短,如果是的话,则修改A[B][C]=A[B][A]+A[A][C],path[B][C]=A;重复上诉步骤,直到所有顶点都判断完,这样得到的A数组便是各个顶点之间的最短距离。具体代码如下:
void Dijkstra(MGraph g, int v)
{
	int dist[MAXV], path[MAXV];
	int s[MAXV];
	int mindis, i, j, u;
	for (i = 0; i < g.n; i++)
	{
		dist[i] = g.edges[v][i];
		s[i] = 0;
		if (g.edges[v][i] < INF)
		{
			path[i] = v;
		}
		else
		{
			path[i] = -1;
		}
	}
	s[v] = 1;
	for (i = 0; i < g.n; i++)
	{
		mindis = INF;
		for (j = 0; j < g.n; j++)
		{
			if (s[j] == 0 && dist[j] < mindis)
			{
				u = j;
				mindis = dist[j];
			}
		}
		s[u] = 1;
		for (j = 0; j < g.n; j++)
		{
			if (s[j] == 0)
			{
				if (g.edges[u][j] < INF && dist[u] + g.edges[u][j] < dist[j])
				{
					dist[j] = dist[u] + g.edges[u][j];
					path[j] = u;
				}
			}
		}
	}
}

void Floyd(MGraph g)
{
	int A[MAXV][MAXV];
	int path[MAXV][MAXV];
	int i, j, k;
	for (i = 0; i < g.n; i++)
	{
		for (j = 0; j < g.n; j++)
		{
			A[i][j] = g.edges[i][j];
			if (i != j && g.edges[i][j] < INF)
			{
				path[i][j] = i;
			}
			else
			{
				path[i][j] = -1;
			}
		}
	}
	for (k = 0; k < g.n; k++)
	{
		for (i = 0; i < g.n; i++)
		{
			for (j = 0; j < g.n; j++)
			{
				if (A[i][j] > A[i][k] + A[k][j])
				{
					A[i][j] = A[i][k] + A[k][j];
					path[i][j] = k;
				}
			}
		}
	}
}

1.1.3 最小生成树相关算法及应用:

  • 什么是最小生成树:即将一个图结构转换为树的结构,且要求从树中任意一个顶点出发都能访问到其余任意顶点,在这基础上生成的总权值最小的树就较最小生成树。如果有n个顶点,那么最小生成树就有n-1条边。
  • 求最小生成树有两种方法:①Prim算法:有两个集合,一个U用来存放已选中的顶点,T用来存放未选中的顶点。两个辅助一维数组,closest[]用来存放最小生成树的边依附在U中顶点的编号,lowcost[]用来表示顶点到U中顶点的边权重,取最小权重的顶点k加入U,规定lowcost[k]=0表示k顶点在U中,即以被选中。初始化U={v},v到其他所有顶点的边为侯选边,然后从侯选边中挑选权值最小的边输出,设该边在T中的顶点是k,则将k加入U中;观察T中所有顶点j,修改侯选边:若(j,k)的权值小于原来和顶点k关联的侯选边,则用(k,j)取代后者作为侯选边。重复以上n-1次后,所有顶点都被选中,U中所有的顶点和选中的边构成一棵最小生成树。时间复杂度为O(n²)n为顶点总数,适用于稠密图②Kruskal算法:与Prim算法一样,两个集合U和TE,TE用来存放已选中的边,但Kruskal算法额外增加了一个结构体用来存放边的信息,初始化U的初值为包含全部顶点,TE为空集;将图中的边按权值从大到小排序依次选取,若选取的边不会使生成树形成回路(用并查集判断是否形成回路),则加入TE,否则舍弃,直到TE中包含n-1条边为止。时间复杂度为O(eloge)e为图所含边数,适用于稀疏图。具体代码如下:
void Prim(MGraph g, int v)//邻接矩阵
{
	int lowcost[MAXV], min, closest[MAXV], i, j, k;
	for (i = 0; i < g.n; i++)
	{
		lowcost[i] = g.edges[v][i];
		closest[i] = v;
	}
	for (i = 1; i < g.n; i++)
	{
		min = INF;
		for (j = 0; j < g.n; j++)//在T中找离U最近的顶点
		{
			if (lowcost[j] != 0 && lowcost[j] < min)
			{
				min = lowcost[j];
				k = j;//k记录最近顶点的编号
			}
			cout << "边(" << closest[k] << "," << k << ")权为:" << min<<endl;
			lowcost[k] = 0;//标记k已加入U
		}
		for (j = 0; j < g.n; j++)
		{
			if (lowcost[j] != 0 && g.edges[k][j] < lowcost[j])
			{
				lowcost[j] = g.edges[k][j];
				closest[j] = k;
			}
		}
	}
}

void Kruskal(AdjGraph* G, Edge E[])//邻接表
{
	int e=1,j=1;
	int totalCost = 0;
	int u1, v1,root1,root2;

	while (e < G->n)
	{
		u1 = E[j].u;
		v1 = E[j].v;
		root1 = FindRoot(u1);
		root2 = FindRoot(v1);
		if (root1 != root2)
		{
			Union(E[j].u, E[j].v);
			e++;
			totalCost += E[j].w;
			visited[E[j].u] = visited[E[j].v] = 1;
		}
		j++;
	}
	for (int i = 1; i <= G->n; i++)
	{
		if (!visited[i])
		{
			cout << "-1";
			return;
		}
	}
	cout << totalCost;
}

1.1.4 最短路径相关算法及应用,可适当拓展最短路径算法:

  • 算法:最短路径的相关算法上面已经介绍过了(Dijkstra算法和Floyd算法),这里就不做过多介绍了。
  • 应用:旅游规划求最短路线、计算机网络路由求最省方案。两个的思想都差不多,这里只介绍旅游路线规划。
  • pta7-6 旅游规划:有了一张自驾旅游路线图,你会知道城市间的高速公路长度、以及该公路要收取的过路费。现在需要你写一个程序,帮助前来咨询的游客找一条出发地和目的地之间的最短路径。如果有若干条路径都是最短的,那么需要输出最便宜的一条路径。
  • 这题算是对Floyd算法创新,因为这题要同时考虑两个变量:路程和费用,所以只需要在原算法的基础上多增加一个二维数组,在结构体定义中多增加一个二维数组用来记录最短路径:即在遇到有多条最短路径时,比较其费用,取费用小的最短路径为最终结果。主要代码如下:
void Floyd(MGraph g, int u, int v)
{
	int A[MAXV][MAXV];//最短路径的花费
	int B[MAXV][MAXV];//最短路径长度
	int i, j, k;
	for (i = 0; i < g.n; i++)
	{
		for (j = 0; j < g.n; j++)
		{
			A[i][j] = g.edges[i][j];
			B[i][j] = g.length[i][j];
			if (i != j && g.edges[i][j] < INF)
			{
				path[i][j] = i;
			}
			else
			{
				path[i][j] = -1;
			}
		}
	}
	for (k = 0; k < g.n; k++)
	{
		for (i = 0; i < g.n; i++)
		{
			for (j = 0; j < g.n; j++)
			{
				if (B[i][j] > B[i][k] + B[k][j])
				{
					A[i][j] = A[i][k] + A[k][j];
					B[i][j] = B[i][k] + B[k][j];
					path[i][j] = k;
				}
				else if (B[i][j] == B[i][k] + B[k][j])//如果有多条最短路径,比较其花费
				{
					if (A[i][j] > A[i][k] + A[k][j])
					{
						A[i][j] = A[i][k] + A[k][j];
						B[i][j] = B[i][k] + B[k][j];
						path[i][j] = k;
					}
				}
			}
		}
	}
	cout << B[u][v] << " " << A[u][v]<<endl;
}

1.1.5 拓扑排序、关键路径:

  • 什么是拓扑排序:首先我们要只要顶点的入度这一概念,可以简单理解为在一个有向图中一个顶点被几条边指着,那么该顶点的入度就是那几条边的数量。拓扑排序可以理解成有一些事件要做,但是有部分事件需要完成其他事件后才可以做。例如:每天起床后,需要做的事情有:换衣服、刷牙、洗脸、吃早饭、上学。那么上学必须在换衣服之后才可以进行,总不能穿着睡衣去上学吧。那么对于这些事件就有了先后顺序,哪些事件是必须先做,将所有事件的执行顺序列举出来就是拓扑排序。

    对于图中事件:必须要执行完换衣服、刷牙、洗脸后才可以进行吃饭;必须执行完吃饭后才可以进行上学。即当一个顶点的入度为0时,才可以移除该顶点。

  • 拓扑排序特点:拓扑排序要求每个顶点出现且只出现依次;若存在一条从顶点A到顶点B的路径,那么在序列中顶点A出现在顶点B的前面,即A的完成顺序在B之前;对于一个有向无环图,如果图中存在回路,则无法完成拓扑排序。因此,拓扑排序可以用来检测图中是否有回路。主要代码如下:

  • 如何计算顶点的入度:可以通过遍历邻接表,每遍历一条链,将该链中除表头结点外的其他所有结点的入度+1,直到遍历完邻接表。

//结构体定义
typedef struct ANode
{  int adjvex;			//该边的终点编号
   struct ANode *nextarc;	//指向下一条边的指针
   int info;	                //该边的相关信息,如权重
} ArcNode;                      //边表节点类型
typedef int Vertex;
typedef struct Vnode
{  
   Vertex data;			//顶点信息
   int count;                   //入度
   ArcNode *firstarc;		//指向第一条边
} VNode;			//邻接表头节点类型
typedef VNode AdjList[MAXV];
typedef struct 
{  AdjList adjlist;		//邻接表
   int n,e;		        //图中顶点数n和边数e
} AdjGraph;
void TopSort(AdjGraph* G)//邻接表拓扑排序。注:需要在该函数开始计算并初始化每个节点的入度,然后再进行拓扑排序
{
	int i, j,k=0;
	int St[MAXV], top = -1;
	int result[MAXV];
	ArcNode* p;
	int num = 0;

	for (i = 0; i < G->n; i++)
	{
		G->adjlist[i].count = 0;
	}
	for (i = 0; i < G->n; i++)
	{
		p = G->adjlist[i].firstarc;
		while (p != NULL)
		{
			G->adjlist[p->adjvex].count++;
			p = p->nextarc;
		}
	}
	for (i = 0; i < G->n; i++)
	{
		if (G->adjlist[i].count == 0)
		{
			top++;
			St[top] = i;
		}
	}
	
	while (top > -1)
	{
		i = St[top];
		top--;
		result[k++] = i;
		p = G->adjlist[i].firstarc;
		while (p != NULL)
		{
			j = p->adjvex;
			G->adjlist[j].count--;
			if (G->adjlist[j].count == 0)
			{
				top++;
				St[top] = j;
			}
			p = p->nextarc;
		}
	}
	if (G->n != k )
	{
		cout << "error!";
		exit(0);
	}
	else
	{
		cout << result[0];
		for (i = 1; i < k; i++)
		{
			cout << " " << result[i];
		}
	}
}
  • 什么是关键路径:对于一个带权有向拓扑图,从起点到终点的最长路径就叫做关键路径。关键路径上的边称作关键活动。

    图中的绿色路线即为关键路线,即起点为起床,目的为上学,则从起床到上学所需要的最长时间为6分钟,这条路径上所包含的关键活动,也就是上学之前必须做的事情有:起床、换衣服、吃饭、上学。

  • 相关题目:pta7-7 剿灭魔教,主要代码如下:

void TopSort(AdjGraph* G)//邻接表拓扑排序。注:需要在该函数开始计算并初始化每个节点的入度,然后再进行拓扑排序
{
	int i, j,k=0,t,temp;
	int St[MAXV], top = -1;
	int result[MAXV];
	ArcNode* p;
	int num = 0;

	for (i = 1; i <= G->n; i++)
	{
		G->adjlist[i].count = 0;
	}
	for (i = 1; i <= G->n; i++)
	{
		p = G->adjlist[i].firstarc;
		while (p != NULL)
		{
			G->adjlist[p->adjvex].count++;
			p = p->nextarc;
		}
	}
	for (i = 1; i <= G->n; i++)
	{
		if (G->adjlist[i].count == 0)
		{
			top++;
			St[top] = i;
		}
	}
	
	while (top > -1)
	{

		sort(St, St + top+1,greater<int>());
		i = St[top];
		top--;
		result[k++] = i;
		p = G->adjlist[i].firstarc;
		while (p != NULL)
		{
			j = p->adjvex;
			G->adjlist[j].count--;
			if (G->adjlist[j].count == 0)
			{
				top++;
				St[top] = j;
			}
			p = p->nextarc;
		}
	}
	if (G->n != k )
	{
		cout << "-1 ";
		return;
	}
	else
	{
		for (i = 0; i < k; i++)
		{
			cout << result[i]<<" ";
		}
	}
}

1.2.谈谈你对图的认识及学习体会。

  • 对图的认识:一个图就是一些顶点的集合,这些顶点通过一些列边连接在一起;一个不带权值的图可以表示一个社交网络,每一个人就是一个顶点,两个人之间有边的话,就说明两个人是朋友。对于社交网络图,我们可以了解到自己的朋友有哪些朋友,朋友的朋友又有哪些朋友,可以通过朋友来认识朋友的朋友,结交更多的朋友(pta 7-2 六度空间);对于带权值的图可以表示成交通路线图,两个城市之间如果通路的话,就有边,边的权值代表从一个城市到另一个城市所需要的费用,可以是机票价格,也可以是高速公路收费。对于这样的一张交通图,我们可以计算从一个城市到另一个城市的最短路径为多少,最低费用为多少,从而规划一条旅游路线;图还可以用来进行拓扑排序等等……总而言之,图的实际用途十分广泛,是一种非常实用的结构
  • 学习体会:同二叉树一样,图的实际应用十分广泛,但是操作起来却容易出错。主要原因出在图的遍历上,图的遍历与二叉树的遍历十分相似,需要用到递归,一碰到递归就得注意递归出口,否则很容易陷入死循环;对于图的存储方式,需要根据不同的情况选择不同的存储方式(邻接矩阵或邻接表):对于稀疏图可以选用邻接表,对于稠密图可以选用邻接矩阵,这样比较不会造成内存上的浪费;还有就是要养成动态申请内存后要释放内存的习惯,如果程序需要长时间运行,而动态申请的内存又没有及时得到释放,容易造成电脑卡顿甚至死机

2.阅读代码

2.1 题目及解题代码

class Solution {
public:
    bool dfs(const vector<vector<int>>& graph, vector<int>& cols, int i, int col) {
        cols[i] = col;
        for (auto j : graph[i]) {
            if (cols[j] == cols[i]) return false;
            if (cols[j] == 0 && !dfs(graph, cols, j, -col)) return false;
        }
        return true;
    }
    bool isBipartite(vector<vector<int>>& graph) {
        int N = graph.size();
        vector<int> cols(N, 0);
        for (int i = 0; i < N; ++i) {
            if (cols[i] == 0 && !dfs(graph, cols, i, 1)) {
                return false;
            }
        }
        return true;
    }
};

作者:da-li-wang
链接:https://leetcode-cn.com/problems/is-graph-bipartite/solution/c-dfsran-se-by-da-li-wang/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

2.1.1 该题的设计思路

  • 设计思路:这题可以转换为着色问题:将所有的点初始为未染色的状态。随机选择一个点,将其染成白色。再以它为起点,将所有相邻的点染成黑色。再以这些黑色的点为起点,将所有与其相邻未染色的点染成白色。不断重复直到整个图都染色完成。对于一个已染色的点,如果存在一个与它相邻的已染色的顶点与它的颜色相同,那么就说明该图不是二分图。

  • 时间复杂度:O(边数+顶点数)

  • 空间复杂度:O(顶点数+边数)

2.1.2 该题的伪代码

输入顶点个数n,边数m;
根据n、m创建无向图邻接表;
调用dfs深度遍历邻接表并对顶点着色;
bool dfs(const vector<vector<int>>& graph, vector<int>& cols, int i, int col)
{
将起始点i染色成col;
for(遍历i的所有邻接点)
  if(存在一个i的邻接点颜色与i 相同)  不是二分图,return false;
  if(存在i的邻接点未被染色) 以该邻接点作为新的起始点调用dfs进行染色,染成-col;
end for
return true;//能运行到这里说明不存在两个连通顶点颜色相同的情况,即该图为二分图
}

2.1.3 运行结果

2.1.4分析该题目解题优势及难点。

  • 解题优势:对于图的存储结构可以选择邻接表,既方便访问邻接点,也方便比较初始点与其邻接点的颜色;对于顶点的染色,可以选DFS也可以选用BFS,只要在遍历、染色图顶点时,判断初始点与邻接点的颜色,在遍历完后判断所有顶点是否已被染色即可。
  • 难点:虽然这题和pta上的着色问题很像,但pta上是提前给出所有顶点的颜色再判断相邻顶点颜色是否相同。而这题是要在遍历图的过程中对顶点着色,然后再进行判断,且着色过程中要判断邻接点是否已被着色,如已被着色还要判断颜色是否与初始点相同,判断语句偏多容易遗漏

2.2 题目及解题代码

class Solution {
    vector<bool> viewed;
    vector<vector<int>> adList;
public:
    bool findWhetherExistsPath(int n, vector<vector<int>>& graph, int start, int target) {
        viewed = vector<bool>(n,0);
        adList = vector(n,vector<int>(1,-1));
        for (int i=0;i<graph.size();i++){
            adList[graph[i][0]].push_back(graph[i][1]);
        }
        return search(start,target);
    }

    bool search(int start,int target){
        viewed[start] = 1;
        bool result = 0;
        for(int i=1;i<adList[start].size();i++){
            if (viewed[adList[start][i]]==0){
                if(adList[start][i]==target){
                    viewed[adList[start][i]] =1 ;
                    return 1;
                }
                result = search(adList[start][i],target);
                if(result==1)
                    break;
            }
        }
        return result;
    }
};

作者:fireshaman
链接:https://leetcode-cn.com/problems/route-between-nodes-lcci/solution/cdfs-by-lfgxboangw/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

2.2.1 该题的设计思路

  • 设计思路:用邻接表来存储图,从以出发点为表头的链表开始对出发点能到达的顶点依次遍历(BFS与DFS均可),如果遍历到了目标顶点,则直接返回true;如果遍历结束还没有遍历到目标顶点的话说明出发点与目标顶点之间没有路径。

  • 时间复杂度:O(顶点数+边数)

  • 空间复杂度:O(顶点数+边数)

2.2.2 该题的伪代码

输入顶点数和边数;
根据顶点数和边数创建邻接表;
输入出发点和目标点;
bool result=0;
从以出发点为表头的链表开始深度遍历邻接表;
{
for(遍历当前表头的所有邻接点)
  if(当前表头的第一个子结点==目标结点)  找到目标顶点,标记result=1,return true;  
  if(当前表头的第一个子结点不空)  递归遍历以该子结点为表头的链表;
end for
}
根据result的值输出结果;

2.2.3 运行结果

2.2.4分析该题目解题优势及难点。

  • 解题优势:对于图的存储结构可以选择邻接表,一来节省内存空间,二来方便进行图的遍历;如果需要输出出发点到目标点的路径的话只需要额外增加一个一维数组path[]来记录路径上顶点的前驱即可。
  • 难点:对于图的递归遍历一定要小心递归出口,否则很容易陷入死循环;如果使用递归函数对图进行遍历的话还需要注意找到目标顶点的状态,即从出发点开始遍历完图后是否有找到目标顶点。

2.3 题目及解题代码

class Solution {
public:
    int findTheCity(int n, vector <vector<int>> &edges, int distanceThreshold) {
        // 定义二维D向量,并初始化各个城市间距离为INT_MAX(无穷)
        vector <vector<int>> D(n, vector<int>(n, INT_MAX));
        // 根据edges[][]初始化D[][]
        for (auto &e : edges) {
            // 无向图两个城市间的两个方向距离相同
            D[e[0]][e[1]] = e[2];
            D[e[1]][e[0]] = e[2];
        }
        // Floyd算法
        for (int k = 0; k < n; k++) {
            // n个顶点依次作为插入点
            for (int i = 0; i < n; i++) {
                for (int j = 0; j < n; j++) {
                    if (i == j || D[i][k] == INT_MAX || D[k][j] == INT_MAX) {
                        // 这些情况都不符合下一行的if条件,
                        // 单独拿出来只是为了防止两个INT_MAX相加导致溢出
                        continue;
                    }
                    D[i][j] = min(D[i][k] + D[k][j], D[i][j]);
                }
            }
        }
        // 选择出能到达其它城市最少的城市ret
        int ret;
        int minNum = INT_MAX;
        for (int i = 0; i < n; i++) {
            int cnt = 0;
            for (int j = 0; j < n; j++) {
                if (i != j && D[i][j] <= distanceThreshold) {
                    cnt++;
                }
            }
            if (cnt <= minNum) {
                minNum = cnt;
                ret = i;
            }
        }
        return ret;
    }
};

作者:huwt
链接:https://leetcode-cn.com/problems/find-the-city-with-the-smallest-number-of-neighbors-at-a-threshold-distance/solution/yu-zhi-ju-chi-nei-lin-ju-zui-shao-de-cheng-shi-flo/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

2.3.1 该题的设计思路

  • 设计思路:通过Floyd算法求出各个城市间的距离,保存在二维数组A[n][n]中;遍历二维数组A[n][n],统计各个城市在距离不超过阈值的情况下能到达的城市数量;输出能到达其他城市数量最少的城市。

  • 时间复杂度:O(顶点数³)

  • 空间复杂度:O(顶点数²)

2.3.2 该题的伪代码

输入顶点数N和边数M;
根据顶点数和边数创建邻接矩阵g.edge[][];
用Floyd算法计算各个城市之间距离不超过阈值的城市之和;
{
定义二维数组A[n][n]用来存放各个城市之间的最短距离;
for(i=0 to N-1)
  for(j=0 to N-1)
    初始化A[i][j]=g.edge[i][j];
  end for
end for

for(k=0 to N-1)
  for(i=0 to N-1)
    for(j=0 to N-1)
      if(经过城市k后,i到j的距离比不经过k小)
        修改i到j的最短距离为i到k的距离+k到j的距离:A[i][j]=A[i][k]+A[k][j];
    end for
  end for
end for

定义minNum=∞用来存储一个城市到其他城市的距离不超过阈值的数量;
定义ret用来记录城市编号;
for(i=0 to N-1)
  定义距离小于阈值的城市数量cnt=0;
  for(j=0 to N-1)
    if(i!=j&&i到j的最短距离小于等于阈值) cnt++;
  end for
  if(cnt<=minNum) minNum=cnt,ret=i;
end for
输出ret;
}

2.3.3 运行结果

2.3.4分析该题目解题优势及难点。

  • 解题优势:可以在Floyd算法的基础上进行运算,事半功倍,正所谓前人种树后人乘凉;由于这题是无向图,所以采用邻接矩阵来存储图结构比较不会造成内存上的浪费,且邻接矩阵比较容易判断两个城市间是否连通。
  • 难点:使用邻接矩阵存储图结构的话一定要注意先初始化邻接矩阵后再对输入的边进行存储;对于用来存储各个城市间最短距离的二维数组A[][]也要进行初始化,否则程序可能无法正常运行;要注意题目要求如果有多个城市的阈值距离内邻居最少的城市数量是相同的话,要取编号最大的那个;由于Floyd算法使用了三层循环,所以在循环过程中要注意不要搞混了三个循环变量之间的关系
posted @ 2020-05-05 21:03  Kevin。  阅读(358)  评论(1编辑  收藏  举报