zoukankan      html  css  js  c++  java
  • DS博客作业04--图

    0.PTA得分截图

    1.本周学习总结

    1.1总结图内容

    图的存储结构

    (1)邻接矩阵

    基本思想:
    1.用一维数组存储顶点 – 描述顶点相关的数据;
    2.用二维数组存储边 – 描述顶点间的边。
    设G=(V,E)是具有n个顶点的图,则G的邻接矩阵是具有如下性质的n阶方阵:

    若G是网络,则邻接矩阵可定义为:

    例如下图的无向图G5和有向图G6

    图的邻接矩阵存储类型定义如下:

    #define  MAXV  <最大顶点个数>	
    typedef struct
    {
    	int no;			//顶点编号
    	InfoType info;		//顶点其他信息
    } VertexType;
    typedef struct  			//图的定义
    {
    	int edges[MAXV][MAXV]; 	//邻接矩阵
    	int n,e;  			//顶点数,边数
    	VertexType vexs[MAXV];	//存放顶点信息
    }  MatGraph;
    MatGraph g;//声明邻接矩阵存储的图
    

    创建邻接矩阵:

    void CreateMGraph(MGraph & g, int n, int e)
    {
    	int i, j;
    	g.n = n;
    	g.e = e;
    	 for (i = 1; i < MAXV; i++)
    	 {
    		   for (j = 1; j < MAXV; j++)
    		 {
    			 g.edges[i][j] = 0;
    		 }
    	}
    	  int a, b;
    	   for (i = 0; i < e; i++)
    		     {
    	             cin >> a >> b;
    		         g.edges[a][b] = 1;
    		         g.edges[b][a] = 1;
    		     }
    	 }
    
    

    邻接矩阵的主要特点:
    一个图的邻接矩阵表示是唯一的。
    特别适合于稠密图的存储。

    (2)邻接表

    图的邻接表存储方法是一种顺序分配与链式分配相结合的存储方法。 
    基本思想:
    1.对图中每个顶点i建立一个单链表,将顶点i的所有邻接点链起来。
    2.每个单链表上添加一个表头结点(表示顶点信息)。并将所有表头结点构成一个数组,下标为i的元素表示顶点i的表头结点。

    图的邻接表存储类型定义如下:

    typedef struct Vnode
    {
    	Vertex data;			//顶点信息
    	ArcNode* firstarc;		//指向第一条边
    }  VNode;
    
    typedef struct ANode
    {
    	int adjvex;			//该边的终点编号
    	struct ANode* nextarc;	//指向下一条边的指针
    	InfoType info;		//该边的权值等信息
    }  ArcNode;
    
    typedef struct
    {
    	VNode adjlist[MAXV];	//邻接表
    	int n,e;			//图中顶点数n和边数e
    } AdjGraph;
    AdjGraph* G;//声明一个邻接表存储的图G
    

    创建邻接表:

    void CreateAdj(AdjGraph*& G, int n, int e)
    {
    	int i;
    	G = new AdjGraph;
    	G->e = e;
    	G->n = n;
    	for (i = 1; i <= n; i++) {
    		G->adjlist[i].firstarc = NULL;
    	}
    	for (i = 1; i <= e; i++)
    	{
    		int a, b;
    		cin >> a >> b;
    		ArcNode* p, * q;
    		p = new ArcNode;
    		q = new ArcNode;
    		p->adjvex = b;
    		q->adjvex = a;
    		p->nextarc = G->adjlist[a].firstarc;//头插法
    		G->adjlist[a].firstarc = p;
    		q->nextarc = G->adjlist[b].firstarc;
    		G->adjlist[b].firstarc = q;
    	}
    }
    

    邻接表的特点如下:
    1.邻接表不唯一
    2.适用于稀疏图存储

    图遍历及应用

    深度优先遍历(DFS)

    基本思想:
    从当前节点开始,先标记当前节点,再寻找与当前节点相邻,且未标记过的节点:
    1): 当前节点不存在下一个节点,则返回前一个节点进行DFS
    2): 当前节点存在下一个节点,则从下一个节点进行DFS
    具体代码如下:

    void DFS(ALGraph* G, int v)
    {
    	ArcNode* p;
    	visited[v] = 1;                   //置已访问标记
    	printf("%d  ", v);
    	p = G->adjlist[v].firstarc;
    	while (p != NULL)
    	{
    		if (visited[p->adjvex] == 0)  DFS(G, p->adjvex);
    		p = p->nextarc;
    	}
    }
    

    图的深度优先遍历类似于二叉树的前序遍历。

    广度优先遍历(BFS)

    基本思想:
    (1)顶点v入队列。
    (2)当队列非空时则继续执行,否则算法结束。
    (3)出队列取得队头顶点v;访问顶点v并标记顶点v已被访问。
    (4)查找顶点v的第一个邻接顶点col。
    (5)若v的邻接顶点col未被访问过的,则col入队列。
    (6)继续查找顶点v的另一个新的邻接顶点col,转到步骤(5)。
    (7)直到顶点v的所有未被访问过的邻接点处理完。转到步骤(2)

    伪代码

           (1)初始化队列Q;visited[n]=0;
           (2)访问顶点v;visited[v]=1;顶点v入队列Q;
           (3) while(队列Q非空)   
                     v=队列Q的对头元素出队;
                     w=顶点v的第一个邻接点;
                     while(w存在) 
                         如果w未访问,则访问顶点w;
                         visited[w]=1;
                         顶点w入队列Q;
                         w=顶点v的下一个邻接点。
    

    判断图是否连通

    基本思想:
    采用某种遍历方式来判断无向图G是否连通。这里用深度优先遍历方法,先给visited[]数组(为全局变量)置初值0,然后从0顶点开始遍历该图。
    在一次遍历之后,若所有顶点i的visited[i]均为1,则该图是连通的;否则不连通。
    代码如下:

    int  visited[MAXV];
    bool Connect(AdjGraph *G) 	//判断无向图G的连通性
    {     int i;
          bool flag=true;
          for (i=0;i<G->n;i++)		 //visited数组置初值
    	visited[i]=0;
          DFS(G,0); 	//调用前面的中DSF算法,从顶点0开始深度优先遍历
          for (i=0;i<G->n;i++)
                if (visited[i]==0)
               {     flag=false;
    	   break;
               }
          return flag;
    }
    

    最短路径

    void ShortPath(AdjGraph *G,int u,int v)
    {   //输出从顶点u到顶点v的最短逆路径
           qu[rear].data=u;//第一个顶点u进队
            while (front!=rear)//队不空循环
            {      front++;		//出队顶点w
                   w=qu[front].data;
                  if (w==v)   根据parent关系输出路径break; 
                  while(遍历邻接表)   
                    {         rear++;//将w的未访问过的邻接点进队
    		 qu[rear].data=p->adjvex;
    		 qu[rear].parent=front;
    	  }
             }	      
    }
    

    查找图路径

    void FindAllPath(AGraph *G,int u,int v,int path[],int d)
    { //d表示path中的路径长度,初始为-1
      int w,i;  ArcNode *p;
      d++; path[d]=u;		//路径长度d增1,顶点u加入到路径中
      visited[u]=1;		//置已访问标记
      if (u==v && d>=1)		//找到一条路径则输出
            {	for (i=0;i<=d;i++)
    	    printf("%2d",path[i]);
    	printf("
    ");
            }
            p=G->adjlist[u].firstarc;	//p指向顶点u的第一个相邻点
            while (p!=NULL)
            {	 w=p->adjvex;		//w为顶点u的相邻顶点
    	 if (visited[w]==0)	//若w顶点未访问,递归访问它
    	     FindAllPath(G,w,v,path,d);
    	 p=p->nextarc;		//p指向顶点u的下一个相邻点
            }
           visited[u]=0;
    }
    

    最小生成树

    (1)普里姆(Prim)算法

    基本思想:
    (1)初始化U={v}。v到其他顶点的所有边为候选边;
    (2)重复以下步骤n-1次,使得其他n-1个顶点被加入到U中:
    从候选边中挑选权值最小的边输出,设该边在V-U中的顶点是k,将k加入U中;
    考察当前V-U中的所有顶点j,修改候选边:若(j,k)的权值小于原来和顶点k关联的候选边,则用(k,j)取代后者作为候选边。

    伪代码:

    初始化lowcost,closest数组
    for(v=1;v<=n;v++)
        遍历lowcost数组     //选最小边
               若lowcost[i]!=0,找最小边
               找最小边对应邻接点k
        最小边lowcost[k]=0;
        输出边(closest[k],k);
        遍历lowcost数组     //修正lowcost
            若lowcost[i]!=0 && edges[i][k]<lowcost[k]
                    修正lowcost[k]=edges[i][k]
                     修正closest[j]=k;
    end
    

    具体代码:

    #define INF 32767		//INF表示∞
    void Prim(MGraph g,int v)
    {  int lowcost[MAXV],min,closest[MAXV],i,j,k;
       for (i=0;i<g.n;i++)	//给lowcost[]和closest[]置初值
       {  lowcost[i]=g.edges[v][i];closest[i]=v;}
        for (i=1;i<g.n;i++)	  //找出(n-1)个顶点
       {	min=INF;
    	for (j=0;j<g.n;j++) //     在(V-U)中找出离U最近的顶点k
    	   if (lowcost[j]!=0 && lowcost[j]<min)
    	   {	min=lowcost[j];  k=j;	/k记录最近顶点的编号}
    	   printf(" 边(%d,%d)权为:%d
    ",closest[k],k,min);
    	   lowcost[k]=0;		//标记k已经加入U
       for (j=0;j<g.n;j++)	//修改数组lowcost和closest
    	   if (lowcost[j]!=0 && g.edges[k][j]<lowcost[j])
    	   {	lowcost[j]=g.edges[k][j];
    		closest[j]=k;
    	   } }}
    

    (2)克鲁斯卡尔(Kruskal)

    基本思想:
    (1)置U的初值等于V(即包含有G中的全部顶点),TE的初值为空集(即图T中每一个顶点都构成一个连通分量)。
    (2)将图G中的边按权值从小到大的顺序依次选取:
    若选取的边未使生成树T形成回路,则加入TE;
    否则舍弃,直到TE中包含(n-1)条边为止。

    代码如下:

    void Kruskal(AdjGraph *g)
    { int i,j,k,u1,v1,sn1,sn2;
      UFSTree t[MAXSize];//并查集,树结构
       ArcNode  *p;
       Edge E[MAXSize];
       k=1;			//e数组的下标从1开始计
       for (i=0;i<g.n;i++)	//由g产生的边集E
    	{   p=g->adjlist[i].firstarc;
            while(p!=NULL)    
    	  {	E[k].u=i;E[k].v=p->adjvex;
                E[k].w=p->weight;
    		k++; p=p->nextarc;
    	  }
       HeapSort(E,g.e);	//采用堆排序对E数组按权值递增排序
      MAKE_SET(t,g.n);	//初始化并查集树t
      k=1;       	//k表示当前构造生成树的第几条边,初值为1
      j=1;       		//E中边的下标,初值为1
      while (k<g.n)     	//生成的边数为n-1
      {  u1=E[j].u;
         v1=E[j].v;		//取一条边的头尾顶点编号u1和v2
         sn1=FIND_SET(t,u1);
         sn2=FIND_SET(t,v1); //分别得到两个顶点所属的集合编号
         if (sn1!=sn2) //两顶点属不同集合
         {  printf("  (%d,%d):%d
    ",u1,v1,E[j].w);
    	  k++;		//生成边数增1
    	  UNION(t,u1,v1);//将u1和v1两个顶点合并
         }
         j++;   		//扫描下一条边
      }
    } 
    

    最短路径

    (1)单源最短路径—Dijkstra(迪杰斯特拉)算法

    基本思想:
    (1)初始化:先找处从源点V0到各终点Vk的直达路径(V0,Vk),即通过一条弧到达的路径。
    (2)选择:从这些路径中找出一条长度最短的路径(V0,u)。
    (3)更新:然后对其余各条路径进行适当的调整:
    若在图中存在弧(u,Vk),且(u,Vk)+(V0,u)<(V0,Vk),则以路径(V0,u,Vk)代替(V0,Vk)。
    (4)在调整后的各条路径中,再找长度最短的路径,以此类推。

    代码如下:

    void Dijkstra(MatGraph 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;			//s[]置空
    		if (g.edges[v][i] < INF)	//路径初始化
    			path[i] = v;		//顶点v到i有边时
    		else
    			path[i] = -1;		//顶点v到i没边时
    	}
    	s[v] = 1;
    	for (i = 0; i < g.n; i++)	 	//循环n-1次
    	{
    		mindis = INF;
    		for (j = 0; j < g.n; j++)
    			if (s[j] == 0 && dist[j] < mindis)
    			{
    				u = j;
    				mindis = dist[j];
    			}
    		s[u] = 1;			//顶点u加入S中
    		for (j = 0; j < g.n; j++)	//修改不在s中的顶点的距离
    			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;
    				}
    	}
    	Dispath(dist, path, s, g.n, v);	//输出最短路径
    }
    

    所有顶点间的最短路径—Floyd(弗洛伊德)算法

    基本思想:
    弗洛伊德算法定义了两个二维矩阵:
    矩阵D记录顶点间的最小路径
    例如D[0][3]= 10,说明顶点0 到 3 的最短路径为10;
    矩阵P记录顶点间最小路径中的中转点
    例如P[0][3]= 1 说明,0 到 3的最短路径轨迹为:0 -> 1 -> 3。
    它通过3重循环,k为中转点,v为起点,w为终点,循环比较D[v][w] 和 D[v][k] + D[k][w] 最小值,如果D[v][k] + D[k][w] 为更小值,则把D[v][k] + D[k][w] 覆盖保存在D[v][w]中。

    代码如下:

    void Floyd(MatGraph g)		//求每对顶点之间的最短路径
    {
    	int A[MAXVEX][MAXVEX];	//建立A数组
    	int path[MAXVEX][MAXVEX];	//建立path数组
    	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; 	//i和j顶点之间有一条边时
    			else			 //i和j顶点之间没有一条边时
    				path[i][j] = -1;
    		}
            for (k = 0; k < g.n; k++)		//求Ak[i][j]
    {
    	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; 	//修改经过顶点k
    			}
    }
    }
    

    拓扑排序

    一、定义:是将一个有向无环图G的所有的顶点排成一个线性序列,使得有向图中的任意的顶点u 和 v 构成的弧<u, v>属于该图的边

    集,并且使得 u 始终是出现在 v 的前面。通常这样的序列称为是拓扑序列。

    二、基本思想
    1.找到有向无环图中没有前驱的节点(或者说是入度为0的节点)输入;

    2.然后从图中将此节点删除并且删除以该节点为尾的弧;

    三、代码实现

    void TopSort(AdjGraph *G)	//拓扑排序算法
    {      int i,j;
            int St[MAXV],top=-1;	//栈St的指针为top
            ArcNode *p;
            for (i=0;i<G->n;i++)		//入度置初值0
    	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++)		//将入度为0的顶点进栈
    	 if (G->adjlist[i].count==0)
    	 {	top++;
    		St[top]=i;
    	 }
             while (top>-1)			//栈不空循环
             {	  i=St[top];top--;			//出栈一个顶点i
    	  printf("%d ",i);		//输出该顶点
    	  p=G->adjlist[i].firstarc;		//找第一个邻接点
    	  while (p!=NULL)		//将顶点i的出边邻接点的入度减1
    	  {      j=p->adjvex;
    	         G->adjlist[j].count--;
    	         if (G->adjlist[j].count==0)	//将入度为0的邻接点进栈
    	         {      top++;
    		  St[top]=j;
    	         }
    	         p=p->nextarc;		//找下一个邻接点
    	}
           }
    }
    
    

    关键路径

    AOE网:在一个表示工程的带权有向图中,用顶点表示事件(如V0),用有向边表示活动(如<v0,v1> = a1),边上的权值表示活动的持续时间,称这样的有向图为边表示的活动的网,简称AOE网(activity on edge network)
    关于AOE网的相关名词
    AOE网——带权的有向无环图
    顶点--事件或状态
    弧(有向边)--活动及发生的先后关系
    权--活动持续的时间
    起点--入度为0的顶点(只有一个)
    终点--出度为0的顶点(只有一个)

    基本思想:
    1.对有向图拓扑排序
    2.根据拓扑序列计算事件(顶点)的ve,vl数组
    ve(j) = Max{ve(i) + dut(<i,j>)}
    vl(i) = Min{vl(j) - dut(<i,j>)}
    3.计算关键活动的e[],l[]。即边的最早、最迟时间
    e(i) = ve(j)
    l(i) = vl(k) - dut(<j, k>
    4.找e=l边即为关键活动
    5.关键活动连接起来就是关键路径

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

    图的学习,我们先学了图的两种存储结构:邻接表和邻接矩阵,然后是图的遍历:DFS和BFS,接着是四种算法:普里姆算法和克鲁斯卡尔算法用来解决最小生成树问题,迪杰斯特拉算法和弗洛伊德算法用来解决最短路径问题,还学习了拓扑排序和关键路径这两个重要知识点。图感觉比之前的难度都要大,比如邻接表的一大串变量,结构体就搞得我晕头转向,得多做题目才能熟悉并掌握。

    2.阅读代码

    2.1 克隆图


    class Solution {
    public:
        Node* used[101];         
        Node* cloneGraph(Node* node) {
            if(!node)return node;  
            if(used[node->val])return used[node->val]; 
            Node* p=new Node(node->val);   
            used[node->val]=p;    
            vector<Node*> tp=node->neighbors;
            for(int i=0;i<tp.size();i++) 
            p->neighbors.push_back(cloneGraph(tp[i]));
            return p;           
        }
    };
    

    2.1.1 该题的设计思路

    主要思想是通过递归,创建和更新新的图节点。通过创建一个节点(指针)数组used来记录每个拷贝过的节点,递归遍历每一个原有节点,然后将拷贝后的指针放入used数组中,然后递归实现每个节点的更新。

    2.1.2 该题的伪代码

    创建一个节点(指针)数组记录每个拷贝过的节点ued[101]
    if(空指针)return 空;
    if(该节点已经拷贝)return 改节点的指针;
    创建拷贝节点
    递归遍历每一个原有节点,然后将拷贝后的指针放入used
    for(将该节点的邻接节点放入拷贝节点邻接数组)
    递归实现每一个节点的更新
    return 拷贝后的节点;
    

    2.1.3 运行结果

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

    优势: 相比较我看到的其他解法都要简洁的多,代码量极少。
    难点:我觉得难就难在运用递归,不容易做出来。

    2.2 不邻接植花

    class Solution {
    public:
        //static const int MAXV=10000;
        //int G[MAXV][MAXV]={0};
        vector<int> gardenNoAdj(int N, vector<vector<int>>& paths) {
            vector<int> G[N];
            for (int i=0; i<paths.size(); i++){//建立邻接表
                G[paths[i][0]-1].push_back(paths[i][1]-1);
                G[paths[i][1]-1].push_back(paths[i][0]-1);
            }
            vector<int> answer(N,0);//初始化全部未染色
            for(int i=0; i<N; i++){
                set<int> color{1,2,3,4};
                for (int j=0; j<G[i].size(); j++){
                    color.erase(answer[G[i][j]]);//把已染过色的去除
                }
                answer[i]=*(color.begin());//染色
            }
            return answer;
        }
    };
    

    2.2.1该题的设计思路

    1、根据paths建立邻接表;
    2、默认所有的花园先不染色,即染0;
    3、从第一个花园开始走,把与它邻接的花园的颜色从color{1,2,3,4}这个颜色集中删除;
    4、删完了所有与它相邻的颜色,就可以把集合中剩下的颜色随机选一个给它了,为了简单,将集合中的第一个颜色赋给当前花园;
    5、循环3和4到最后一个花园。

    2.2.2该题的伪代码

    根据paths建立邻接表;
    for (int i=0; i<paths.size(); i++)建立邻接表
    vector<int> answer(N,0);//初始化全部未染色
    for (int j=0; j<G[i].size(); j++)
     把已染过色的去除
     染色
     return answer;
    

    2.2.3运行结果

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

    优势:选用邻接表做存储结构,邻接矩阵的话虽然更加易懂但是会堆栈溢出
    难点:难在先全部置为未染色,之后再遍历染色,并把已染过色的去除。

    2.3 接雨水

    int trap(vector<int>& height)
    {
        int ans = 0, current = 0;
        stack<int> st;
        while (current < height.size()) {
            while (!st.empty() && height[current] > height[st.top()]) {
                int top = st.top();
                st.pop();
                if (st.empty())
                    break;
                int distance = current - st.top() - 1;
                int bounded_height = min(height[current], height[st.top()]) - height[top];
                ans += distance * bounded_height;
            }
            st.push(current++);
        }
        return ans;
    }
    

    2.3.1该题的设计思路

    在遍历数组时维护一个栈。如果当前的条形块小于或等于栈顶的条形块,将条形块的索引入栈,即当前的条形块被栈中的前一个条形块界定。如果发现一个条形块长于栈顶,就可以确定栈顶的条形块被当前条形块和栈的前一个条形块界定,因此可以弹出栈顶元素并且累加答案到 ans 。

    2.3.2该题的伪代码

    使用栈来存储条形块的索引下标。
    遍历数组:
    当栈非空且 {height}[current]>{height}[st.top()]
    意味着栈中元素可以被弹出。弹出栈顶元素top。
    计算当前元素和栈顶元素的距离,准备进行填充操作
    distance=current-st.top()-1
    找出界定高度
    bounded_height=min(height[current],height[st.top()])−height[top]
    往答案中累加积水量ans+=distance*bounded_height
    将当前索引下标入栈
    将current 移动到下个位置
    

    2.3.3运行结果

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

    优势:时间复杂度和空间复杂度都达到了O(n),相比暴力法,空间和时间利用率都大大提高。
    难点:想到运用栈来解题,正常想到的都是题主写的第一种暴力破解法。

  • 相关阅读:
    ActiveX控件开发总结(续)
    Guru of the Week 条款04: 类的构造技巧
    tk
    C++中一个空类的大小为什么是1?
    虚继承
    计算机单位
    Guru of the week:#18 迭代指针.
    kingofark关于学习C++和编程的50个观点
    Guru of the Week 条款06:正确使用const
    Guru of the Week 条款07:编译期的依赖性
  • 原文地址:https://www.cnblogs.com/yeanxxx/p/12830937.html
Copyright © 2011-2022 走看看