DS博客作业04——图

0.PTA得分截图

1.学习总结

图存储结构

邻接矩阵

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

邻接表

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

图遍历

深度优先搜索(DFS)

1、从顶点出发
2、访问顶点,也就是根节点
3、依次从顶点的未被访问的邻接点出发,进行深度优先遍历;直至和顶点有路径相通的顶点都被访问
4、若此时尚有顶点未被访问,则从一个未被访问的顶点出发,重新进行深度优先遍历,直到所有顶点均被访问过为止

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

时间复杂度
邻接表:O(n+e)
邻接矩阵:O(n^2)

广度优先遍历(BFS)

1、访问初始点v,接着访问v的所有未被访问过的邻接点
2、按照次序访问每一个顶点的所有未被访问过的邻接点
3、依此类推,直到图中所有顶点都被访问过
遍历非连通图

void  BFS1(AdjGraph *G)
{      int i;
        for (i=0;i<G->n;i++)     //遍历所有未访问过的顶点
             if (visited[i]==0) 
                  BFS(G,i);
}

时间复杂度:O(n+e)

图应用

判断图是否连通

DFS遍历

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;
}
BFS() 
{	int count = 0 ;
	boolean flag = false;	
	Queue Q = new Queue();				    
	visited = new int[this.vertexNum];		
	while(!Q.isEmpty()) 
	{       			
		int s = Q.front();					
		Q.remove();
		visited[s] = 1;
		count ++;							
		for(int j=0; j<this.vertexNum; j++)
 		{
			if(this.a[s][j] == 1 && visited[j] == 0 )
			{
				    visited[j] = 1;
			   	    Q.add(j);
			}											
		}	
	}		
	if(count == this.vertexNum)
	flag = true;						
 	return flag;
}

最短路径

在带权有向图中A点(源点)到达B点(终点)的多条路径中,寻找一条各边权值之和最小的路径,即最短路径

一个顶点到其余各顶点的最短路径——Dijkstra算法

引进两个集合S和U。
S的作用是记录已求出最短路径的顶点(以及相应的最短路径长度)
U则是记录还未求出最短路径的顶点(以及该顶点到起点s的距离)

过程

1、S只包含起点s;U包含除s外的其他顶点,且U中顶点的距离为”起点s到该顶点的距离”[U中顶点v的距离为(s,v)的长度,如果s和v不相邻,则v的距离为∞]
2、从U中选出”距离最短的顶点k”,并将顶点k加入到S中;同时从U中移除顶点k
3、更新U中各个顶点到起点s的距离。之所以更新U中顶点的距离,是因为上一步中确定了k是求出最短路径的顶点,从而可以利用k来更新其它顶点的距离;
例如,(s,v)的距离可能大于(s,k)+(k,v)的距离
4、重复2和3,直到遍历完所有顶点。

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);	//输出最短路径
}

时间复杂度O(n^2)

不适用带负权值的带权图求单源最短路径

不适用求最长路径长度

每一对顶点之间的最短路径——Floyd算法

创建一个二维数组Path路径数组,用于存放任意一对顶点之间的最短路径。每个单元格的内容表示从i点到j点途经的顶点

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

时间复杂度为O(n^3)

空间复杂度为O(n^2)

最小生成树

生成树:一个连通图的生成树是指一个连通子图,它含有图中全部n个顶点,但只有足以构成一棵树的n-1条边。
一颗有n个顶点的生成树有且仅有n-1条边,如果生成树中再添加一条边,则必定成环。
最小生成树:在连通网的所有生成树中,所有边的代价和最小的生成树,称为最小生成树。

Prim算法

“加点法”,每次选择权值最小的边对应的点,加入到最小生成树中。

#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\n",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;
	        }
    }
}

Kruskal算法

“加边法”,初始最小生成树边数为0,每次选择一条满足条件的最小权值边,加入到最小生成树的边集合里。

void Kruskal(AdjGraph *g)
{     int i,j,u1,v1,sn1,sn2,k;
      int vset[MAXV]; //集合辅助数组
      Edge E[MaxSize];	//存放所有边
      k=0;			//E数组的下标从0开始计
      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;
	 }
      }
      Sort(E,g.e);	//用快排对E数组按权值递增排序
      for (i=0;i<g.n;i++) 	//初始化集合
      vset[i]=i;
      k=1;		//k表示当前构造生成树的第几条边,初值为1
      j=0;		//E中边的下标,初值为0
      while (k<g.n)	//生成的顶点数小于n时循环
      { 
             u1=E[j].u;v1=E[j].v;	//取一条边的头尾顶点
             sn1=vset[u1];
             sn2=vset[v1];	//分别得到两个顶点所属的集合编号
             if (sn1!=sn2)  	//两顶点属于不同的集合
	     {
        	printf("  (%d,%d):%d\n",u1,v1,E[j].w);
		k++;		   	//生成边数增1
		for (i=0;i<g.n;i++)  	//两个集合统一编号
		      if (vset[i]==sn2) 	//集合编号为sn2的改为sn1
			vset[i]=sn1;
	     }
	     j++;			   //扫描下一条边
       }
}

拓扑排序

1、从DGA图中找到一个没有前驱的顶点输出
2、删除以这个点为起点的边。(它的指向的边删除,为了找到下个没有前驱的顶点)
3、重复上述,直到最后一个顶点被输出。如果还有顶点未被输出,则说明有环

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网中,没有入边的顶点称为源点;如顶点V0
终点:在AOE网中,没有出边的顶点称为终点;如顶点V3
AOE网的性质:
1、只有在进入某顶点的活动都已经结束,该顶点所代表的事件才发生
2、只有在某顶点所代表的事件发生后,从该顶点出发的各活动才开始
在AOE网中,所有活动都完成才能到达终点,因此完成整个工程所必须花费的时间(即最短工期)应该为源点到终点的最大路径长度
具有最大路径长度的路径称为关键路径
关键路径上的活动称为关键活动

对图的认识及学习体会

图对我而言非常的难,我对于其中的知识大部分都不太理解,导致了学习进度慢。
图这种结构已经非常接近现实了,多对多的关系令它能够做更多的事。

2.阅读代码

2.1 最大正方形

    int maximalSquare(vector<vector<char>>& matrix) 
    {
        if (matrix.size() == 0 || matrix[0].size() == 0) 
        {
            return 0;
        }
        int maxSide = 0;
        int rows = matrix.size(), columns = matrix[0].size();
        for (int i = 0; i < rows; i++) 
        {
            for (int j = 0; j < columns; j++)        
            {
                if (matrix[i][j] == '1') 
                {
                    // 遇到一个 1 作为正方形的左上角
                    maxSide = max(maxSide, 1);
                    // 计算可能的最大正方形边长
                    int currentMaxSide = min(rows - i, columns - j);
                    for (int k = 1; k < currentMaxSide; k++) 
                    {
                        // 判断新增的一行一列是否均为 1
                        bool flag = true;
                        if (matrix[i + k][j + k] == '0')  break;
                        for (int m = 0; m < k; m++) 
                        {
                            if (matrix[i + k][j + m] == '0' || matrix[i + m][j + k] == '0') 
                            {
                                flag = false;
                                break;
                            }
                        }
                        if (flag) maxSide = max(maxSide, k + 1);
                        else      break;
                    }
                }
            }
        }
        int maxSquare = maxSide * maxSide;
        return maxSquare;
    }

2.1.1 设计思路

由于正方形的面积等于边长的平方,因此要找到最大正方形的面积,首先需要找到最大正方形的边长,然后计算最大边长的平方即可。
1、遍历矩阵中的每个元素,每次遇到1,则将该元素作为正方形的左上角;
2、确定正方形的左上角后,根据左上角所在的行和列计算可能的最大正方形的边长(正方形的范围不能超出矩阵的行数和列数),在该边长范围内寻找只包含 1 的最大正方形;
3、每次在下方新增一行以及在右方新增一列,判断新增的行和列是否满足所有元素都是1。

时间复杂度:O(mn*min(m,n)^2),其中 m 和 n 是矩阵的行数和列数。
空间复杂度:O(1)

2.1.2 伪代码

int maximalSquare(vector<vector<char>>& matrix) 
    {
        if (空矩阵)
             return 0;
        定义整型变量maxside记录最长边的大小
        记录矩阵行列大小
        for (int i = 0; i < rows; i++) 
        {
            for (int j = 0; j < columns; j++)        
            {
                if (遇到一个 1 作为正方形的左上角) 
                {
                    计算可能的最大正方形边长
                    int currentMaxSide = min(rows - i, columns - j);
                    for (int k = 1; k < currentMaxSide; k++) 
                    {
                        判断新增的一行一列是否均为 1
                    }
                }
            }
        }
        int maxSquare = maxSide * maxSide;
        return maxSquare;
    }

2.1.3 运行结果

2.1.4 解题优势及难点

此题使用暴力法,难点在于如何降低时间复杂度。
解题优势在于对面积较小的正方形的遍历不会重复遍历邻接矩阵

2.2 找到最终的安全状态

class Solution {
public:
    vector<int> eventualSafeNodes(vector<vector<int>>& graph) {
        int n = graph.size();
        vector<int> outDegree(n, 0); // 维护出度
        vector<vector<int>> revGraph(n, vector<int>{});
        vector<int> ans;
        for (int i =0; i < n; i++){
            outDegree[i] = graph[i].size();
            for (auto &end : graph[i]){
                revGraph[end].push_back(i);
            }
        }
        queue<int> q;
        for (int i =0; i< n ; i++){
            if (outDegree[i] == 0) q.push(i);
        }
        while (!q.empty()){
            int f = q.front();
            ans.push_back(f);
            q.pop();
            for (auto start: revGraph[f]){
                outDegree[start]--;
                if (outDegree[start] == 0) q.push(start);
            }
        }
        sort(ans.begin(), ans.end());
        return ans;
    }
};

2.2.1 设计思路

定义安全的点:路径终点,也就是出度为0的点
定义最终安全的点:从起始节点开始,可以沿某个路径到达终点,那么起始节点就是最终安全的点。

1、找到出度为0的顶点,这些点是安全的点
2、逆向删除以出度为0的顶点为弧头的边,弧尾的出度减一
3、重复上面两步,直到不存在出度为0的顶点

时间复杂度:O(n^2)
空间复杂度:O(n^2)

2.2.2 伪代码

    遍历顶点
	if(顶点v出度度为0)
	    v入队列
    while(队列非空)
    {
	top出队
	遍历逆向邻接表
	{
            top所有的邻接点出度减一
	    if (邻接点出度为0)
		    邻接点入队
	}
}

2.2.3 运行结果

2.2.4 解题优势及难点

优势:使用拓扑排序,而不是BFS或DFS
难点:逆向边的操作

2.3 不邻接植花

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.3.1 设计思路

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

2.3.2 伪代码

        建立邻接表
        初始化全部花园未种花answer(N,0);
        for i=0 to N-1 do
            每次循环都要初始化color
            for j=0 to G[i].size()-1 do
                把已染过色的去除
            end for
            对当前顶点种花answer[i]=*(color.begin());
        end for
        return answer;
    }
};

2.3.3 运行结果

2.3.4 解题优势及难点

优势:使用邻接表存储数据
难点:color的应用及操作

posted @ 2020-05-05 22:12  极仙  阅读(255)  评论(0编辑  收藏  举报