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