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("\n");
        }
        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\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;
	   } }}

(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\n",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),相比暴力法,空间和时间利用率都大大提高。
难点:想到运用栈来解题,正常想到的都是题主写的第一种暴力破解法。

posted @ 2020-05-05 20:47  王柏鸿  阅读(272)  评论(0编辑  收藏  举报