ACM培训第三发 简单图论 主讲人—— 陈星毅.

Slides:



Advertisements
Similar presentations
1 、谁能说说什么是因数? 在整数范围内( 0 除外),如果甲数 能被乙数整除,我们就说甲数是乙数的 倍数,乙数是甲数的因数。 如: 12÷4=3 4 就是 12 的因数 2 、回顾一下,我们认识的自然数可以分 成几类? 3 、其实自然数还有一种新的分类方法, 你知道吗?这就是我们今天这节课的学.
Advertisements

因数与倍数 2 、 5 的倍数的特征
3 的倍数的特征 的倍数有 : 。 5 的倍数有 : 。 既是 2 的倍数又是 5 的倍数有 : 。 12 , 18 , 20 , 48 , 60 , 72 , , 25 , 60 ,
2 和 5 的倍数的特征 运动热身 怎样找一个数的倍数? 从小到大写出 2 的倍数( 10 个): 写出 5 的倍数( 6 个) 2 , 4 , 6 , 8 , 10 , 12 , 14 , 16 , 18 , 20 5 , 10 , 15 , 20 , 25 , 30.
练一练: 在数轴上画出表示下列各数的点, 并指出这些点相互间的关系: -6 , 6 , -3 , 3 , -1.5, 1.5.
信号与系统 第三章 傅里叶变换 东北大学 2017/2/27.
常用逻辑用语复习课 李娟.
小学生游戏.
一、原函数与不定积分 二、不定积分的几何意义 三、基本积分公式及积分法则 四、牛顿—莱布尼兹公式 五、小结
第三节 格林公式及其应用(2) 一、曲线积分与路径无关的定义 二、曲线积分与路径无关的条件 三、二元函数的全微分的求积 四、小结.
探索三角形相似的条件(2).
C++中的声音处理 在传统Turbo C环境中,如果想用C语言控制电脑发声,可以用Sound函数。在VC6.6环境中如果想控制电脑发声则采用Beep函数。原型为: Beep(频率,持续时间) , 单位毫秒 暂停程序执行使用Sleep函数 Sleep(持续时间), 单位毫秒 引用这两个函数时,必须包含头文件
在PHP和MYSQL中实现完美的中文显示
第七章 图 (Graph)
Ch.7 图 图是一种复杂的非线性结构 应用:AI、工程、数学、生物、计算机 结点间的逻辑关系:任两个结点都可能相关
第七章 图 7.1 图的基本概念 7.2 图的存储表示 7.3 图的遍历 7.4 图的生成树 7.5 最短路径 7.6 拓扑排序
图(一).
图的遍历.
强连通分量 无向图 1、任意两顶点连通称该图为连通图 2、否则将其中的极大连通子图称为连通分量 A D C B E 有向图
第七章 图 东南大学计算机学院 方效林 本课件借鉴了清华大学殷人昆老师 和哈尔滨工业大学张岩老师的课件.
走进编程 程序的顺序结构(二).
数据结构 芦溪中学 胡小妹.
§2 求导法则 2.1 求导数的四则运算法则 下面分三部分加以证明, 并同时给出相应的推论和例题 .
第7章 图 7.1 图的定义和术语 7.2 图的存储结构 7.3 图的遍历 7.4 图的连通性问题 7.5 有向无环图的应用
第七章 图.
本节内容 模拟线程切换 视频提供:昆山滴水信息技术有限公司 官网地址: 论坛地址: QQ交流 :
动态规划(Dynamic Programming)
使用矩阵表示 最小生成树算法.
无向树和根树.
第四章 第三节 最短路径算法.
第6章 图 本章中介绍下列主要内容: 图的定义 图的存储结构 图的遍历操作 图的几个典型问题.
专题作业.
知识点回顾 拓扑排序 关键路径 应用领域、求解步骤、算法实现过程 应用领域 求解步骤
简单介绍 用C++实现简单的模板数据结构 ArrayList(数组, 类似std::vector)
线段的有关计算.
复习 图的定义和术语 图(Graph)、有向图、无向图、子图、有向完全图 、完全图、稀疏图、稠密图、权、网、邻接点、相关边、度、路径、路径长度、回路、简单路径、简单回路、连通、连通图、连通分量、强连通图、强连通分量、生成树 图的存储方法 给定一个图,画出邻接矩阵、邻接表 图的邻接矩阵和邻接表的存储算法.
顺序表的删除.
Three stability circuits analysis with TINA-TI
4.2 证明⑶.
12.2全等三角形的判定(2) 大连市第三十九中学 赵海英.
常宝宝 北京大学计算机科学与技术系 数据结构(六) 常宝宝 北京大学计算机科学与技术系
成绩是怎么算出来的? 16级第一学期半期考试成绩 班级 姓名 语文 数学 英语 政治 历史 地理 物理 化学 生物 总分 1 张三1 115
图论初步 柏钧文.
定理21.9(可满足性定理)设A是P(Y)的协调子集,则存在P(Y)的解释域U和项解释,使得赋值函数v(A){1}。
§6.7 子空间的直和 一、直和的定义 二、直和的判定 三、多个子空间的直和.
知识点回顾 图的深度优先和广度优先遍历思想 图的深度优先遍历算法(用邻接表作为存储结构) 图的广度优先遍历算法(用邻接矩阵作为存储结构)
本节内容 结构体 视频提供:昆山爱达人信息技术有限公司 官网地址: 联系QQ: QQ交流群 : 联系电话:
1.设A和B是集合,证明:A=B当且仅当A∩B=A∪B
树和图 tree and graph 蔡亚星.
第4课时 绝对值.
多层循环 Private Sub Command1_Click() Dim i As Integer, j As Integer
第七、八次实验要求.
分数再认识三 真假带分数的练习课.
第15讲 特征值与特征向量的性质 主要内容:特征值与特征向量的性质.
最小生成树.
基于列存储的RDF数据管理 朱敏
第四节 向量的乘积 一、两向量的数量积 二、两向量的向量积.
4.2 图的存储结构(cont.) v1 v2 v3 v4 v5 vertex= v1 v2 v3 v4 v5 v
Advanced Competitive Programming
第十七讲 密码执行(1).
插入排序的正确性证明 以及各种改进方法.
位似.
使用Fragment 本讲大纲: 1、创建Fragment 2、在Activity中添加Fragment
§4.5 最大公因式的矩阵求法( Ⅱ ).
<编程达人入门课程> 本节内容 有符号数与无符号数 视频提供:昆山爱达人信息技术有限公司 官网地址: 联系QQ:
1、图的基本概念 2、图的存储结构 3、图的遍历与连通性
最小生成树 最优二叉树.
§2 自由代数 定义19.7:设X是集合,G是一个T-代数,为X到G的函数,若对每个T-代数A和X到A的函数,都存在唯一的G到A的同态映射,使得=,则称G(更严格的说是(G,))是生成集X上的自由T-代数。X中的元素称为生成元。 A变, 变 变, 也变 对给定的 和A,是唯一的.
百万行、千万行数据查询教程 老黄牛.
9.3多项式乘多项式.
Presentation transcript:

ACM培训第三发 简单图论 主讲人—— 陈星毅

上节课你们正昊学长给你们讲了一些图的概念,比如说树、有向图、无向图;如何以代码形式表示图;图的存储方式:邻接表和邻接矩阵。并且他还讲了两个搜索(据说训练题没什么人做。。 。) 正昊一脸幽怨地表示不开心。 So upset。。。

(啊不对,那是你们祝♂叶学长的表情,不是正昊的,发错了2333 (啊哈祝叶在不在这里啊,不管啦hhhhh

为了复习一下,我们从一道水题开始—— 给出n个点m条边,问有多少个连通分量。 输入:第一行两个整数n和m(n<=m<=1000),点从1到n开始编号。从第二行开始的m行,每一行为两个整数a和b,表示点a和点b之间有一条边。 输出:连通分量的数目。 连通分量是shenmegui?有没有人知道?

显然可以用深搜的方法写。 首先是存图,为了明显我们先用邻接矩阵表示。(也就是二维数组) const int N=1010; int n, m, g[N][N], vis[N], cnt; //先把要用的东西都声明,cnt表示联通分量的数目,g数组用来存图,vis数组用来标记是否访问过。

然后是递归函数 void dfs(int u){ if(vis[u]) return; else{ vis[u]=1; for(int i=1;i<=n;i++) if(g[u][i]) dfs(i); }

int main(){ while(~scanf(“%d%d”,&n,&m){ cnt=0; memset(g,0,sizeof(g)); memset(vis,0,sizeof(vis)); for(int i=0;i<m;i++){ scanf(“%d%d”,&a,&b); g[a][b]=g[b][a]=1; } solve(); return 0;

void solve(){ for(int i=1;i<=n;i++) if(!vis[i]){ cnt++; dfs(i); } printf(“%d\n”,cnt); 问题解决了。 那么如果,需要输出同一个联通分量的所有点呢? 比如说和编号为1的点同一个联通分量的点。

不难想到,属于同一个联通分量的点,除了第一个被访问到的点外,其它的点的联通分量编号都与上一个访问的点的所属联通分量编号相同。 则只需要将递归函数改一个小地方就可以了。

void dfs(int u){ if(vis[u]) return; else{ vis[u]=cnt; for(int i=1;i<=n;i++) if(g[u][i]) dfs(i); } 然后vis值相同的点即属于同一个联通分量。

然后复习下邻接表 通常用4个数组: 假定最多有N个点,M条边 int head[N], nxt[M], to[M], cost[M]; head[i]表示以 i 为源点的第一条边的编号。 nxt[j]表示编号为 j 的边的下一条边的编号。 to[j]表示编号为 j 的边指向的点。 cost[i]表示编号为 j 的边的权值或者说边长 以tot表示边的总数,初始化为0。 初始化以memset(head,-1,sizeof(head));即可

void dfs(int u){ if(vis[u]) return; else{ else vis[u]=cnt; for(int i=head[u];~i;i=nxt[i]) dfs(to[i]); } void addedge(int u,int v){ to[tot]=v; nxt[tot]=head[u]; head[u]=tot++;

除了用dfs,还有一个方法 并查集!

它的作用是模拟出一种集合,并提供合并不同集合、查询几个点是否属于相同集合的功能。 是一种树形结构的东西。 具体怎么操作呢?

其实实现的原理非常简单。 我们定义数组father[N],表示编号为 i 的结点的父亲。 每次查询到两个点之间有我们要的联系的话,将其中一个结点设为另一个结点的父亲。 一次类推,那么只要这些点的“祖先结点”是同一个,则为同一个集合。 我们定义当 father[i]==i 时,i 即为祖先结点。

以刚才的连通分量为例 首先是初始化: for(int i=0;i<=n;i++) father[i]=i; 查询一个结点的祖先结点: int Find(int x){ return x==father[x]? x : Find(father[x]); } 合并集合: void uni(int x,int y){ father[find(x)]=find(y);

然后我们只需要把所有边访问一遍,若这条边的两端 x 和 y 不属于同一个集合(即祖先结点不一样),则合并,否则跳过即可。 为了方便访问所有边,我们定义一个结构体: struct edge{ int u,v; }E[M];

然后,存图的时候—— for(int i=0;i<m;i++){ scanf(“%d%d”,&a,&b); E[i].u=a,E[i].v=b; } 然后遍历访问—— for(int i=0;i<m;i++) if(Find(E[i].u)!=Find(E[i].v)) uni(E[i].u,E[i].v); 然后有多少个联通分量呢?

set<int>all; all.clear(); for(int i=1;i<=n;i++) all.insert(Find(i)); printf(“%d\n”,all.size()); 即可。 并且,Find值相同的,即属于同一个集合,或者说,同一个联通分量。 这就是并查集。

我们重新回顾一下: 并查集的要点: 初始化:将所有的父亲结点设为自己 Find函数,递归查找祖先结点 uni函数,将不同结合合并

并查集的压缩路径优化 我们注意到一点: 如果当前有两个集合 12->1->3->4->5->6(6为祖先结点) 11->2->7->8->9->10(10为根节点) 然后要把11和12合并,那么就需要调动 12次Find函数, 然后如果来一发13->14 要把13和11合并,又要调用6次Find函数 效率太低

优化很简单 改一下Find函数就好 int Find(int x){ return x==f[x]? x : Find(f[x]); } 加一点东西—— int Find(int x){ return x==f[x]? x : f[x]=Find(f[x]);

12->1->3->4->5->6(6为祖先结点) 11->2->7->8->9->10(10为根节点) 调用Find(12),得到6,并且—— 12->6, 1->6, 3->6, 4->6, 5->6 调用Find(11),得到10,并且—— 11->10, 2->10, 7->10, 8->10, 9->10 然后合并,6->10。 这样再度合并相关点的时候,路径大大缩短

但需要注意两点: 一个是,压缩路径后,原有的“父子关系”就没了。 另一个是——举个例子 5->6->7, 1->2->3->4, 然后合并7和4,就得到 5->6->7->4, 1->2->3->4。 这个时候其实是没有压缩到路径的。 所以在看有多少个集合的时候,即使是用了压缩路径,也依然需要遍历Find(i)而不是father[i]。

并查集的用处不小,而且也不难掌握。 记住令不同元素变成同一个集合的因素是由我们决定的,而不仅仅是图里面的边。

最小生成树 我们知道树是指,所有结点连通、无环的图。 最小生成树则是,给出一些点一些边,以及边权——在总边权最小的情况下,取一些边使得这些点构成一棵树。 解决最小生成树的算法一般有prim算法、以及kruskal算法。时间关系只讲kruskal。至于前者有兴趣的同学可以自行了解。

先说kruskal算法 核心是我们刚介绍的并查集。 与求连通块个数时一样,定义当点连通(即相互可达)时,在同一个集合(即并查集中的同一个父亲)内。 然后,只需要对所有边按边权从小到大排序,每次不在同一个集合的点合并,已在同一个集合的跳过。这样到最后得出来的就是最小生成树。

正确性的证明 我百度了下好长好长…………你们有兴趣的自己去看,,,,, 易证,对于一个无向加权连通图,总是存在一棵或以上的有限课生成树,而这些生成树肯定存在至少一棵最小生成树。下面证明Kruskal算法构造的生成树是这些最小生成树中的一棵。 设T为Kruskal算法构造出的生成树,U是G的最小生成树。如果T==U那么证明结束。如果T != U,我们就需要证明T和U的构造代价相同。由于T != U,所以一定存在k > 0条边存在于T中,却不在U中。接下来,我们做k次变换,每次从T中取出一条不在U中的边放入U,然后删除U一条不在T中的边,最后使T和U的边集相同。每次变换中,把T中的一条边e加入U,同时删除U中的一条边f。e、f按如下规则选取:a). e是在T中却不在U中的边的最小的一条边;b). e加入U后,肯定构成唯一的一个环路,令f是这个环路中的一条边,但不在T中。f一定存在,因为T中没有环路。 这样的一次变换后,U仍然是一棵生成树。  我们假设e权值小于f,这样变换后U的代价一定小于变换前U的代价,而这和我们之前假设U是最小生成树矛盾,因此e权值不小于f。  再假设e权值大于f。由于f权值小于e,由Kruskal算法知,f在e之前从E中取出,但被舍弃了。一定是由于和权值小于等于f的边构成了环路。但是T中权值小于等于f(小于e)的边一定存在于U中,而f在U中却没有和它们构成环路,又推出矛盾。所以e权值不大于f。于是e权值等于f。   这样,每次变换后U的代价都不变,所以K次变换后,U和T的边集相同,且代价相同,这样就证明了T也是最小生成树。由证明过程可以知道,最小生成树可以不是唯一的。 无向图G的所有最小生成树的边权集合相同

struct edge{ int u,v,cost; bool operator < (const edge e) const{ return cost<e.cost; } }E[M]; int main(){ //读入数据存图 sort(E,E+m); for(int i=0;i<m;i++) if(Find(E[i].u)!=Find(E[i].v)) uni(E[i].u,E[i].v);

最短路算法 最短路问题可抽象为给出一些点一些边,找出从一个点(起点)到某一个点(终点)的最短路径长度。 最短路又分单源最短路和多源最短路。

单源最短路 即问的都是最短路都是从同一个起点,到其它某一个点的最短路径距离。 常用的有Dijkstra算法、bellman-ford算法以及spfa算法。

Dijkstra

怎么代码实现呢? 我猜你们的表情: 就是要大小不一逼死强迫症!

const int inf=21e8; int g[N][N], d[N], vis[N]; int dijkstra(int s, int t){ memset(vis,0,sizeof(vis)); for(int i=0;i<=n;i++) d[i]=inf; d[s]=0; for(int i=0;i<n;i++){ int u=-1; for(int j=0;j<n;j++) if(!vis[j]) if(u==-1||d[u]>d[j]) u=j; vis[u]=1; for(int j=0;j<n;j++) if(!vis[j]&&g[u][j]) d[j]=min(d[j],g[u][i]+d[u]); } return d[t];

时间复杂度分析 显然可以看出来,该dijkstra的时间复杂度为O(n^2),(假定n为点数),感觉有点不尽如人意,但大多数时候都够用了。 稀疏图下,用优先队列优化可以快很多,但只能得到由源点到终点的最短路,但稠密图下,则并没有,尤其若很多重边的时候,可能会比朴素实现还慢。 还可以用堆实现,可将复杂度优化到O((n+m)logn)。嗯。。。。这个我等你们来教我2333

bellman-ford 一条边(u,v)称为紧的,如果 dis[u] + gra[u][v] < dis[v] 松弛: if( dis[u] + gra[u][v] < dis[v] ) dis[v] = dis[u] + gra[u][v]; 算法:每次对所有边进行松弛,直到某一轮迭代没有边可以松弛。可以证明最多需要松弛n-1次,n为顶点数。

spfa算法 用队列优化的bellman-ford算法 算法: 把起点s加入队列 取出队首,松弛以队首为起点的边 当一条边被松弛,则把该边的终点加入队列 当队列为空,算法结束

int spfa(int s,int t){ memset(inq,0,sizeof(inq)); for(int i=0;i<n;i++) d[i]=inf; d[s]=0,inq[s]=1; queue<int>q; q.push(s); while(!q.empty()){ int u=q.front(); inq[u]=0; q.pop(); for(int i=head[u];~i;i=nxt[i]){ int v=to[i]; if(d[v]>d[u]+cost[i]){ d[v]=d[u]+cost[i]; if(!inq[v]){ q.push(v); inq[v]=1; } return d[t];

要注意的是: dijkstra不能用于计算负权图,而spfa可以。 虽然看起来是spfa比较6,单有时用spfa算法TLE了用dijkstra却能过——dijkstra在对各种图进行最短路计算的时候,其平均复杂度是低于spfa的。 还碰到过用queue会TLE而用stack能a,这个,,原因还没搞清。 除了手写栈或队列,spfa还有两个方法优化,分别是SLF和LLL。有兴趣的可以自行了解。

多源最短路 Floyd算法: for( int k = 1; k <= n; ++k ) for( int i = 1; i <= n; ++i ) for( int j = 1; j <= n; ++j ) d[i][j]=min(d[i][j],d[i][k]+d[k][j]); d[i][j]为任意i到任意j的最短距离。 但注意到复杂度是O(n^3)的,所以点数为100数量级的时候才好考虑使用。

今天的内容也比较多,我们回顾一下: 最开始的时候先讲了个求连通块数量的水题,用dfs一遍,用并查集一遍,并复习了下邻接矩阵和邻接表的使用。 然后是: 并查集——并查集压缩路径优化 最小生成树——kruskal算法 最短路—— 单源最短路:dijkstra和spfa 多源最短路:floyd

希望大家回去好好总结好好消化,训练题已群邮,大家好好刷哈~ 有问题的可以留下来讨论 今天的培训就到这里,谢谢大家~

下一节内容预告: 是祝叶叶的专题,详情见部门专业或者群邮咯~ 再来一发祝叶叶 的表情2333