• 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染色来将结点分为两类,从而判断是否是二分图
    难点:难点在于如何在递归时对下一节点的染色,判断染何色,并需要一边染色的同时一边检查判断其邻接点是否合法

  • 相关阅读:
    Foundations of Machine Learning: The PAC Learning Framework(2)
    Foundations of Machine Learning: The PAC Learning Framework(1)
    图形渲染流水线
    如何用python的装饰器定义一个像C++一样的强类型函数
    Python 装饰器学习心得
    PAT 1087 All Roads Lead to Rome
    PAT 1086 Tree Traversals Again
    PAT 1085 Perfect Sequence
    PAT 1084 Broken Keyboard
    LeetCode: Sort Colors
  • 原文地址:https://www.cnblogs.com/Guangyang/p/12832531.html
Copyright © 2020-2023  润新知