1 算法思想
图
1.1含义
图的含义: 图G是由顶点集V和边集E组成。V(G)表示图的顶点集,E(G)表示图的边集。
图的阶:图G中顶点的个数
有向图:若E是有向边的有限集合,则图为有向图
无向图:若E是无向边的有限集合,则图为无向图
连通:若从顶点v到顶点w有路径存在,则v和w是连通的。
连通图:图G中任意两个顶点都是连通的,则图G为连通图。
连通分量:无向图中的极大连通子图称为连通分量。
强连通:顶点v到顶点w,顶点w到顶点v之间都有路径,则称为这两个顶点是强联通的。
强连通图:图中任何一对顶点都是强连通的
生成树:包含图中全部顶点的极小连通子图,n个顶点有n-1条边。
顶点的度:顶点为一个端点的边的数目
无向图的全部顶点的度之和等于边数的两倍
有向图的顶点入度:以顶点v为终点的有向边的数目
有向图的顶点出度:顶点v为起点的有向边的数目。
图的存储方式
邻接矩阵法: 用一个一维数组存储图中顶点的信息,用一个二维数组存储图中边的信息,这个二维数组即为邻接矩阵
邻接表法: 对图中每个顶点Vi建立一个单链表,第i个单链表中的节点表示依附于顶点Vi的边,这个单链表就称为顶点Vi的边表。
边表的头指针和顶点数据信息采用顺序存储(称为顶点表)
顶点表:由顶点域和指向第一条邻接边的指针构成。
图的遍历方式
图的遍历:从图中某一顶点出发,按照某种搜索方法沿着图中的边对图中所有顶点访问一次且仅一次。
广度优先搜索:类似二叉树的层序遍历算法。首先访问起始顶点v,接着由v出发,依次访问v的各个未访问过的邻接顶点w1,w2,…,wi,然后再依次
访问w1,w2,…,wi的所有未被访问过的邻接顶点,…
深度优先搜索:类似树的先序遍历,尽可能深地搜索一个图
1.2特点
1.3应用
1)最小生成树
含义:所有生成树中边的权值之和最小的那颗生成树。生成树满足n个顶点n-1条边
最小生成树算法:
Prim算法和Kruskal算法,基于贪心算法的策略。
Prim算法:
从所有已经选择的顶点和所有没有选择过的顶点中各选择一个顶点,使这两个顶点连接的边的权值最小,直到所有顶点都已经被选择过。
时间复杂度O(V^2)
Kruskal算法:
按照权值递增次序选择边(该边加入后不构成回路)来构造最小生成树。时间复杂度为O(ELogE)。
具体步骤如下:
A初始所有结点为孤立集合
B按照边权值递增顺序遍历所有边,若遍历到的边的两个顶点属于不同集合,则该边为最小生成树上的一条边,
并将这两个顶点所属的集合合并。
C遍历所有边后,原图上所有结点属于同一个集合则被选取的边和结点构成最小生成树。
由于Kruskal算法求解最小生成树中涉及大量集合操作,可以用并查集实现。需要对所有结点是否属于同一个集合做判断。
2)最短路径
性质:两点之间的最短路径也包含了路径上其他顶点间的最短路径
单源最短路径: 求图中某一顶点到其他各个顶点之间的最短路径,可通过Dijkstra算法求解。
每一对顶点间的最短路径:通过Floyd-Warshall算法求解。
Dijkstra算法:
设置一个集合S,记录已经求得的最短路径的顶点,初始时把源点v0加入S中。
dist[]:记录从原点v0到其他各个顶点当前的最短路径长度
path[]:path[i]表示从源点到顶点i之间的最短路径的前驱结点。
算法思想:
从顶点集合V-S中选出Vj,满足dist[j]=Min{dist[i],Vi属于V-S},其中S是已经加入的顶点集合,令S=S并上{j}
修改从V0出发到集合V-S上任一顶点Vk可达的最短路径长度:
如果dist[j] + arcs[j][k] < disk[k],则令dist[k] = dist[j] + arcs[j][k]
Floyd算法:
求各顶点之间的最短路径
思想:递推产生一个n阶方阵序列A^(-1), A^(0),…,A^(n-1),其中A^(k)[i][j]表示从顶点Vi到顶点Vj的路径长度,k表示第k个顶点作为中间顶点的运算步骤。
逐步在路径中加入顶点k(k=0,1,…,n-1)作为中间顶点,若加入中间顶点后,得到路径比原路径长度减少,则用新路径代替原路径
3)拓扑排序
有向无环图:DAG图,图中不存在环。
拓扑排序:满足
每个顶点出现仅出现一次,若顶点A在序列中排在顶点B的前面,则在图中不存在顶点B到顶点A的路径。
排序方法步骤:
选择一个没有前驱的顶点并输出
从图中删除该顶点和所有以它为起点的有向边。
重复上述步骤直到DAG图为空。
4)并查集
是一种集合表示。
应用: 确定某个集合包含哪些元素,判断两个元素是否在同一个集合中,求集合中元素的数量,求集合的数量等。
实现:用数组实现,数组中每个下标对应的元素是其父元素的下标。即数组单元i中保存节点i的双亲节点编号,若该节点已经是根节点则其双亲节点信息为-1。
思想:通过不断求双亲节点来找到该节点所在树的根节点,若两元素所在树的根节点相同,则它们在同一颗树上,同属于一个集合。
合并就是让一个树变成另一颗树根节点的子树。
优化:查找某个特定结点的根节点的同时将其与根节点之间的所有结点都指向根节点,这个过程被称为路径压缩,来降低树高。
3种操作:
Union(S, Root1, Root2):把集合S中的子集合Root2并入子集合Root1中
Find(S,x):查找集合S中单元素x所在的子集合,返回该子集合的名字。
Initial(S):将集合S中每一个元素都初始化为只有一个单元素的子集合
为了得到两个子集合的并,只要将其中一个子集合根节点的双亲指针指向另一个集合的根节点即可。
并查集结构定义:
#define SIZE 100
int UFSets[SIZE];
初始化操作
void Initial(int S[]){
for(int i=0; i <size; i++)
S[i] = -1;
}
int Find(int S[], int x){
while(S[x] >= 0) // 根的S[]小于0
x = S[x]
return x;
}
另外带有路径压缩的方法
int Find(int S[], int x){
if(S[x] == -1) return x;
else{
int tmp = Find(S[x]);
S[x] = tmp;//将当前节点的双亲节点设置为查找返回的根节点编号
return tmp;
}
}
void Union(int S[], int Root1, int Root2){
S[Root2] = Root1; //将根Root2连接到另一根Root1下面,本质就是设置父节点下标
}
1.4通用解法
图算法: 1 图的题型大多分布在最小生成树,最短路径,并查集,图的遍历方式,DAG图等,理解图的概念再去做题。 |
1.5经典例题讲解
Dijkstra:单源最短路径。第m+1近的结点与结点1的最短路径上的中间节点一定属于集合K,任意最短路径中间有一个不属于集合K的节点,则它的最短路径距离一定<第
m+1近的节点的最短路径长度,与距离与第m+1近的节点的最短路径已经确定、这样的结点属于集合K相矛盾。
最短路径 = 从结点1出发经最短路径集合中K中的节点P2 + <p2,Vn>
算法流程:
1)初始化,在集合K中加入结点1,结点1到结点1的最短距离为0,到其他结点距离为无穷大
2)遍历与集合K中结点直接相邻的边(U,V,C),其中U属于集合K,V不属于集合K,计算从结点1出发,按照已经得到的最短路径到达U,再由U经过该边到达V的路径长度。比较所有与集合K中直接相邻的非集合K中结点的
路径长度,路径长度最小的节点为下一个确定节点,将该结点加入集合K
3)若集合K中含有所有节点,结束
先更新距离,后挑选路径最短的节点加入,对于已经加入的节点需要直接跳过,通过vector来构建单链表
代码如下:
typedef struct Edge { int _iNext;//下一个节点编号 int _iDistance;//从当前节点到下一个节点之间的距离 }Edge;
bool Mark[MAX];//标记是否已经是在已知节点集合 int Dist[MAX];//距离向量,为true表示最短记录。否则,表示经过中间节点的最短距离
int main(int argc) { int n,m,i,j,iVi,iVj,iDis; vector<Edge> vecEdge[MAX];//用了100个数组,每个数组用来存放单链表信息 while(EOF!=scanf("%d %d",&n,&m)) { //易错,n和m都为0的时候要退出 if(0==n && 0==m) { break; }
//易错,要初始化邻接链表 for(i = 1 ; i <=n ; i++) { vecEdge[i].clear(); }
//获取每条边的信息 for(i = 0 ; i < m ; i++) { scanf("%d %d %d",&iVi,&iVj,&iDis); //要构造一个结构体 Edge edge,edge2; edge._iNext = iVj; edge._iDistance = iDis; edge2._iNext = iVi; edge2._iDistance = iDis; //由于是无向图,所以需要添加2条边 vecEdge[iVi].push_back(edge); vecEdge[iVj].push_back(edge2); }
//初始化 for(int i = 1 ; i < MAX; i++) { Mark[i] = false; Dist[i] = -1; } Dist[1] = 0;//设置节点1的长度为0 Mark[1] = true;//设置节点1为已经加入集合 //易错,需要设置一个新节点 int iNewP = 1;
//遍历n-1趟,先更新距离,再挑选最短距离 for(i = 1 ; i < n ; i++) { //遍历vector必须从0开始遍历 for(j = 0 ; j < vecEdge[iNewP].size(); j++) { //取出边,判断与其相连的节点是否已经被标记 int iNext = vecEdge[iNewP][j]._iNext; int iWeight = vecEdge[iNewP][j]._iDistance; //如果已经属于集合K,则跳过 //if(iNext==true) if(Mark[iNext]==true) { continue; } //如果距离不可达 或者走新加入节点的路径距离更短时,更新距离 if(Dist[iNext]==-1 || Dist[iNewP] + iWeight < Dist[iNext]) { Dist[iNext] = Dist[iNewP] + iWeight; } }
//选择距离最短的结点,遍历所有节点 int min = 123123123; for(j = 1 ; j <= n;j++) { //若其属于集合K,节点不可达,则跳过 if(Mark[j]==true || -1==Dist[j]) { continue; } if(Dist[j] < min) { min = Dist[j]; iNewP = j;//更新加入的结点 } } //设置新加入的节点标记 Mark[iNewP] = true; } printf("%d\n",Dist[n]);
}
|
2 图系列
类别-编号 |
题目 |
遁去的一 |
1 |
畅通工程 某省调查城镇交通状况,得到现有城镇道路统计列表,表中列出了每条道路直接连通的城镇。畅通工程的目标是使得全省任何两个城镇之间都可以实现交通(但不一定有直连的道路相连,只要互相间接通过道路到达即可)。问最少还需要建设多少条道路? 输入:城镇数目N(<1000)和道路数目M,城镇从1到N开始编号,N=0时输入结束
输入:城镇数目N(<1000)和道路数目M,城镇从1到N开始编号,N=0时输入结束 输入: 4 2
1 3 4 3
3 3 1 2 1 3 2 3
5 2 3 5 999 0
0 输出: 1 0 2 998 |
计算机考研—机试指南 https://blog.csdn.net/qingyuanluofeng/article/details/47161661 关键: 1 输入m要单独输入,不能写成同时要求输入2个数,这样你输入一个数就不起作用了 2 在路径压缩中 寻找的是x的父节点的根节点 int iTemp = findRoot(Tree[x]); 3 问题被抽象为在一个图上查找连通分量(彼此连通的结点集合)的个数。在ans个集合之间再修建ans-1条道路即可使所有结点连通。 4 同一条边的两个结点需要进行合并,即设置一个结点的父结点为这条边上的另一个结点。 以此来计算连通分量。 代码: int Tree[1024]; //路径压缩:在查找某个特定节点的根节点的同时将其与根节点之间的所有的节点都直接指向根节点 int findRoot(int x) { if(-1==Tree[x]) { return x; } //如果不是根节点 else { //int iTemp = findRoot(x);//temp最终拿到的是根节点的值,易错,这里查找的应该是x的父节点的根节点 int iTemp = findRoot(Tree[x]); Tree[x] = iTemp;//设置当前节点的双亲节点为查找返回的根节点编号 return iTemp; } }
int main(int argc,char* argv) { //n表示城镇数目,m表示道路数目 int n,m; int iResArr[100]; int iSize = 0; int i; while(EOF!=scanf("%d",&n) && 0!=n) { scanf("%d",&m);//输入m要单独输入 //初始,要设置城镇中每个父节点为根节点 for(i = 1 ; i <= n ; i++) { Tree[i] = -1; } int iRoadName1,iRoadName2; //对M组输入进行归并 //for(i = 1 ; i <= n ; i++) for(i = 1 ; i <= m; i++) { scanf("%d %d",&iRoadName1,&iRoadName2); int iRoot1 = findRoot(iRoadName1); int iRoot2 = findRoot(iRoadName2); if(iRoot1!=iRoot2) { Tree[iRoot1] = iRoot2; } } //计算独立的并查集 int iTotalRoad = 0; for(int j = 1 ; j <= n ; j++) { if(Tree[j]==-1) { iTotalRoad++; } } //printf("%d",iTotalRoad-1); iResArr[iSize++] = iTotalRoad - 1; } for(int k = 0 ; k < iSize ; k++) { printf("%d\n",iResArr[k]); } system("pause"); getchar(); return 0; } |
2 |
More is better 1000 0000个朋友,之中有n对朋友,朋友关系具有传递性。要求找出一个最大(人数最多)的集合,该集合中任意两人都是朋友或者只有1个人,输出最大人数
输入: 4 1 2 3 4 5 6 1 6 4 1 2 3 4 5 6 7 8 输出: 4 2 |
计算机考研—机试指南 https://blog.csdn.net/qingyuanluofeng/article/details/47161689 本质:比较每个并查集中元素个数,可以在根结点处设置一个数组用来保存每个根结点对应元素个数,在合并时累加被合并集合中的元素个数 #define N 10000001 int Tree[N]; int Num[N];//Num[i]表示以节点i为根结点的总节点个数,其中保存Tree[i]为-1,为该节点为树的根结点时有效 int findRoot(int x) { //如果父节点是-1,表明当前节点为根结点 if(-1==Tree[x]) { return x; } else { //寻找父节点的根结点 int iRoot = findRoot(Tree[x]); //设置当前节点的父节点为根结点 Tree[x] = iRoot; //返回根节点 return iRoot; } }
int main(int argc,char* argv[]) { int n,i; while(EOF!=scanf("%d",&n)) { //初始化节点信息 for(i = 1 ; i < N ; i++) { Tree[i] = -1; //同时要加入根结点的树的总节点个数 Num[i] = 1; }
//获取输入的n对数据信息 int a,b; for(i = 0 ; i < n;i++) { scanf("%d %d",&a,&b); int iRoot1 = findRoot(a); int iRoot2 = findRoot(b); if(a!=b) { Tree[a] = b; //这里不能用2,因为后面Sum[a]可能不是2 //Sum[b] = 2; //Num[b] += Num[a];//这里应该用查找到的根结点的值进行累加,而不是用子节点的值进行累加 Num[iRoot2] += Num[iRoot1]; } } //统计答案,至少为1 int iResult = 1; //for(i = 0 ; i < N ; i++) for(i = 1; i < N;i++) { if(Tree[i]==-1 && Num[i] > iResult) { iResult = Num[i]; } } printf("%d\n",iResult); } system("pause"); getchar(); return 0; } |
3 |
还是畅通工程 有任意两个村庄的距离,使全省任意两个村庄之间都可以畅通(不要求直接相连,间接联通也可以),要求铺设公路最短和次最短。请计算最小公路总长度 输入:第一行给出村庄数N(<100),随后的N(N-1)/2给出村庄之间的距离,每行给出一对正整数,分别是两个村庄的编号,以及两个村庄之间的距离。村庄从1到N编号,N=0时结束 输出:求出最短公路总长度
3 1 2 1 1 3 2 2 3 4 4 1 2 1 1 3 4 1 4 1 2 3 3 2 4 2 3 4 5 0 输出: 3 5 |
计算机考研—机试指南 https://blog.csdn.net/qingyuanluofeng/article/details/47177895 思路:利用并查集,这里要设置一条边来进行比较。本质是一个最小生成树的问题,使所有城市直接或间接联通且使道路的总长度最短。kruskal算法可以通过并查集实现。 关键:快速排序edge[low] <=iPos <= edge[high],而不是edge[low] < iPos,需要重载<=,而不是<
代码: #define N 1000 int Tree[N];
//设定边结构体 typedef struct Edge { int _iV1,_iV2;//设定边的起始节点 int _iWeight;//设定权重 //快速排序必须重载<=而不是< bool operator <= (const Edge& edge) { return _iWeight <= edge._iWeight; } }Edge;
//并查集寻找根结点的路径压缩函数 int findRoot(int x) { if(-1==Tree[x]) { return x; } else { //寻找当前节点父节点的根结点 int iRoot = findRoot(Tree[x]); //设定当前节点的父节点为根结点 Tree[x] = iRoot; //返回父节点 return iRoot; } }
//这个快速排序有问题,小于等于需要动
int partition(Edge* edge,int low,int hig |