DS博客作业04--图

0.PTA得分截图

1.本周学习总结(0-5分)

1.1 总结图内容

图存储结构

1.邻接矩阵
什么是邻接矩阵:邻接矩阵,顾名思义,就是用矩阵来存储图结构,需要我们在结构体里定义一个二维数组,利用行数与列数来表示某个点到某个点的关系

结构体定义:

typedef struct
{
	int vexs[MaxV];/* 顶点表 */
	int arc[MaxV][MaxV];/* 邻接矩阵,可看作边表 */
	int numNodes, numEdges;/* 图中当前的顶点数和边数  */
} MGraph;

邻接矩阵的优缺点:
优点:1.能够很方便的遍历图的各个边
缺点:1.矩阵能够存储的信息有限 2.浪费空间较多,无论有没有边都需要给予内存

2.邻接表
什么是邻接表:邻接表,就是利用类似链表的结构来存储图,与普通链表不同的是,邻接表有一个头结点数组来做一条条链表的的头结点,数组里的每一个结点代表图里对应的某一个结点

结构体定义:

typedef struct ANode                 //链表结点
{
	int adjvex;                  //边所指的结点在头结点数组里对应的下标
	struct ANode* nexttarc;      //指向下一个结点
	int info;                    //可存储边的信息
}ArcNode;  

typedef struct Vnode                 //头结点
{
	 Vertex data;                //结点数据
	 ArcNode * firstarc;         //指向链的第一个结点
}VNode;

typedef struct                       //邻接表
{
	VNode adjlist[MaxV];         //头结点数组
	int n, e;                    //n为图的节点数,e为图的边数
}AdjGraph;

邻接表的优缺点:
优点:1.可以将点与边的信息都存储在邻接表里 2.有多少结点与多少边就申请多少空间,不会造成内存浪费
缺点:1.在查找边与对边的修改上较邻接矩阵麻烦,需要利用指针遍

图遍历及应用

图遍历:
1.深度优先遍历(DFS)
什么是深度优先遍历:深度优先遍历,就是在访问完一个结点后,若该结点仍有未访问过的邻接点,则往下访问该邻接点,若没有则回溯到上一个结点看有没有未访问的邻接点,重复此过程直到结束。

代码实现
邻接矩阵实现DFS:

void DFS(MGraph G, int v)
{
	int i, j;
	visit[v] = 1;     
	if (flag == 0)
	{
		flag = 1;
		cout << v;
	}
	else
	{
		cout <<" "<< v;
	}
	for (j = 1; j <= G.n; j++)
	{
		if (G.edges[v][j] ==1 && visit[j] == 0)    
		{
			DFS(G, j);
		}
	}
}

邻接表实现DFS:

void DFS(AdjGraph* G, int v)
{
	ArcNode* p;
	int i;
	p = G->adjlist[v].firstarc;    
	visit[v] = 1;                
	if (flag == 0)
	{
		flag = 1;
		cout << v;
	}
	else
	{
		cout << " " << v;
	}
	while(p!=NULL)                  
	{
		i = p->adjvex;            
		if (visit[i] != 1)
		{
			DFS(G, i);
		}
		p = p->nextarc;
	}
}

2.广度优先遍历BFS
什么是广度优先遍历:就是在访问完第一个结点后,依次访问该结点所有未访问过的结点,再依次进入上次访问过的结点重复上述过程,直到结束。

代码实现:
邻接矩阵实现BFS:

void BFS(MGraph G, int v)//广度遍历 
{
    int i;
    queue<int> Q;
    if (visit[v] == 0)
    {
	    visit[v] = 1;
	    cout << v;
	    Q.push(v);
    }
    while (!Q.empty())
    {
	    v = Q.front();
	    Q.pop();
	    for (i = 1; i <= G.n; i++)
	    {    
		    if (G.edges[v][i] && visit[i] == 0)  
		    {
			    visit[i] = 1;
			    cout << " " << i;
			    Q.push(i);
		    }
	    }
    }
}

邻接表实现BFS:

void BFS(AdjGraph* G, int v) //v节点开始广度遍历   
{
	queue<VNode> Q;
	ArcNode* p; 
	visit[v]=1;
	Q.push(G->adjlist[v]);

	while (!Q.empty())
	{
		cout << Q.front.data ;
		p = Q.front.firstarc;
		Q.pop();
		while (p!=NULL)
		{
			if (visit[p->adjvex] == 0)
			{
				Q.push(G->adjlist[p->adjvex]);
				visited[p->adjvex] = 1;
			}
			p = p->nextarc;
		}
	}
	
}

判断图是否连通:
可以利用上述的两个遍历方法对图进行遍历,再检查visit数组是否全置为1,如果全置为1,说明全图都被遍历到了,即图为连通,否则为不连通。

查找图路径:
可以利用上述的两个遍历方法对图进行遍历,并利用一个数组path来存储结点的前驱,直到访问到目标结点停止。

找最短路径:
1.Dijkstra算法求最短路径:
定义两个数组,Dist数组为起始点到其他结点的距离,初始化时如果与起始点有边的话就修改为其边的值,如果没边置为无穷,初始化path数组除起始点的path值为自身外,其他的值都为1;从未访问的结点中选取一个对应的dist数组里值为最小的顶点,先修改其path数组里对应的值为上一个结点,然后对该结点对应的dist的距离值进行修改,如果加入该结点做中间结点后,起始点到其他顶点的距离值比原来路径要短,则修改此距离值;重复以上步骤,直到所有结点都被访问为止。

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;
				}
			}
		}
	}
}

2.Floyd算法求最短路径:
采用矩阵来进行操作,A[],path[][],每次取一个顶点 k 为中间点,若 i 到 j 的距离大于 i 到 k 的距离加上 k 到 j 的距离,则修正A[i][j]=A[i][k]+A[k][j],path[i][j]=k。直到顶点遍历完成

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.prim算法
采用两个数组closest[]存未入选顶点距离最近的已选顶点,lowcost[]存closest[k]到k之间的距离。每次从lowcost数组中挑选最短边节点k输出(将lowcost[k]=0,表示已选),若新加入的顶点使得未加入顶点最短边有变化则修正closest跟lowcost数组。循环直到所有节点入选。

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++)    
                    if (lowcost[j]!=0 && lowcost[j]<min)  
                        {	
                            min=lowcost[j];  
                            k=j;	
                        }
                cout>>k;
                lowcost[k]=0;		
                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;
	               }
}
}

2.Kruskal算法:
算法将所有边权值进行排序。依次从小选取边,若选取的边没有使图形成环路,则入选该边,直到选满n-1一条边为止。

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结点指向2结点,则在序列里1必须在2之前。所以能够进行拓扑排序的图必须是有向无环图。
伪代码:

遍历图,将所以顶点的入度存入count[]数组;
遍历顶点
    将入度为0的顶点入栈
while (栈不空)
{
    出栈节点a;
    遍历a的所有邻接点
        {
            入度--;
            若入度为0,入栈
        }
}

2.关键路径
在AOE网中,从起始点到目标点所有路径中最长路径长度的路径称为关键路径。

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

图结构能将一些几何结构或者平面结构的图形数据化存储,从而解决许多诸如路径问题、图着色问题的平面图形问题,我也相信在以后通过与其他新学习的数据结构结合,能够解决更多复杂的问题。
对于图的学习,主要是通过对各个算法的掌握,如prim算法、Kruskal算法等等,本章学习内容比较紧凑,可能自己掌握地不是非常熟练,希望自己通过更多的练习来提高自己。

2.阅读代码(0--5分)

2.1 题目及解题代码

题目:

解题代码:

int largestRectangleArea(vector<int>& heights)
{
    int ans = 0;
    vector<int> st;
    heights.insert(heights.begin(), 0);
    heights.push_back(0);
    for (int i = 0; i < heights.size(); i++)
    {
        while (!st.empty() && heights[st.back()] > heights[i])
        {
            int cur = st.back();
            st.pop_back();
            int left = st.back() + 1;
            int right = i - 1;
            ans = max(ans, (right - left + 1) * heights[cur]);
        }
        st.push_back(i);
    }
    return ans;
}

2.1.1 该题的设计思路

该题利用单调栈(单调栈分为单调递增栈和单调递减栈,单调递增栈即栈内元素保持单调递增的栈,同理单调递减栈即栈内元素保持单调递减的栈)里的单调递增栈来解决,在新元素小于栈顶元素时,出栈顶元素,计算新栈顶元素与新元素的距离乘以旧栈顶元素的高,即为以旧栈顶元素为高并包含栈顶元素的最大矩阵。
时间复杂度:O(n)
空间复杂度:O(n)

2.1.2 该题的伪代码

int largestRectangleArea(vector<int>& heights)
{
    将0进栈
    while (遍历所有高度)
    {
        while (栈不空 && 栈顶元素大于新元素)
        {
            cur = 栈顶
            left = 新栈顶下标 + 1;
            right = 新元素下标 - 1;
            最大矩阵 = max(最大矩阵, (right - left + 1) * cur高度);
        }
        入栈新元素
    }
}

2.1.3 运行结果

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

解题优势:运用单调栈的特性解题,快速找到每个高度对应的宽度。
难点:每个高度对应的宽度需要找到左右界限求得宽,而高度是不确定的,所以不好寻找。

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;
    }
};


2.2.1 该题的设计思路

从start开始深度优先或广度优先搜索,如果使用邻接矩阵存储图的话,有些用例会超出内存限制,所以使用邻接表存储图。
利用DFS,从出发点开始遍历,如果遍历到目标点返回true,若访问完所有结点仍未找到目标点,则说明无路径,返回false。
时间复杂度O(n+e)
空间复杂度O(n+e)

2.2.2 该题的伪代码

search(当前结点,目标结点)
{
      输入顶点数和边数
      创建邻接表
      输入出发点和目标点
      bool result=0//用于记录结果有无路径
      for(遍历邻接表的头结点)
      {
            for(遍历头结点的所有邻接点)
              if(第一个邻接点==目标结点)  
              {
                  result=1;
                  return true;  
              }
              result = search(当前结点,目标结点)//递归
      }
      根据result的值输出结果;
}

2.2.3 运行结果

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

解题优势:运用递归,使算法更加简短清晰。使用邻接表来存储数据,不会浪费空间
难点:在使用递归的方法时,如果没有很好的设置递归口,很容易进入死循环。

2.3 题目及解题代码

题目:

解题代码:

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;
    }
};

2.3.1 该题的设计思路

利用DFS染色法解决问题,在进行DFS遍历时,先将图所有结点的颜色初始化为0,然后在遍历时对颜色为0的结点以1与-1交替赋值,对已赋值的点判断两邻接点的颜色是否相同,若相同,说明图无法二分。
时间复杂度:O(n+e)
空间复杂度:O(n+e)

2.3.2 该题的伪代码

bool dfs(const vector<vector<int>>& graph, vector<int>& cols, int i, int col)
{
将起始点染色成col;
for(遍历i的邻接点)
  if(邻接点颜色与i 相同)  return false;
  if(邻接点未被染色) 调用dfs(graph, cols, i, -col);
end for
return true;
}

2.3.3 运行结果

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

解题优势:运用DFS染色来将结点分为两类,从而判断是否是二分图
难点:难点在于如何在递归时对下一节点的染色,判断染何色,并需要一边染色的同时一边检查判断其邻接点是否合法

posted @ 2020-05-05 20:24  ZGGYY  阅读(168)  评论(0编辑  收藏  举报