算法竞赛入门经典授课教案第11章图论模型与算法.docx

上传人:b****5 文档编号:12066788 上传时间:2023-04-16 格式:DOCX 页数:64 大小:165.44KB
下载 相关 举报
算法竞赛入门经典授课教案第11章图论模型与算法.docx_第1页
第1页 / 共64页
算法竞赛入门经典授课教案第11章图论模型与算法.docx_第2页
第2页 / 共64页
算法竞赛入门经典授课教案第11章图论模型与算法.docx_第3页
第3页 / 共64页
算法竞赛入门经典授课教案第11章图论模型与算法.docx_第4页
第4页 / 共64页
算法竞赛入门经典授课教案第11章图论模型与算法.docx_第5页
第5页 / 共64页
点击查看更多>>
下载资源
资源描述

算法竞赛入门经典授课教案第11章图论模型与算法.docx

《算法竞赛入门经典授课教案第11章图论模型与算法.docx》由会员分享,可在线阅读,更多相关《算法竞赛入门经典授课教案第11章图论模型与算法.docx(64页珍藏版)》请在冰豆网上搜索。

算法竞赛入门经典授课教案第11章图论模型与算法.docx

算法竞赛入门经典授课教案第11章图论模型与算法

第11章图论模型与算法

【教学内容相关章节】

11.1再谈树11.2最短路问题

11.3网络流初步11.4进一步学习的参考

【教学目标】

(1)掌握无根树的常用存储法和转化为有根树的方法;

(2)掌握由表达式构造表达式树的算法;

(3)掌握Kruskal算法及其正确性证明,并用并查集实现;

(4)掌握基于优先队列的Dijkstra算法实现;

(5)掌握基于FIFO队列的Bellman-Ford算法实现;

(6)掌握Floyd算法和传递闭包的求法;

(7)理解最大流问题的概念、流量的3个条件、残流网络的概念和求法;

(8)理解增广路定理与最小割最大流定理的证明方法,会实现Edmonds-Karp算法;

(9)理解最小费用最大流问题的概念,以及平行边和反向弧可能造成的问题;

(10)会实现基于Bellman-Ford的最小费用路算法。

【教学要求】

掌握Kruskal算法及其正确性证明,并用并查集实现;掌握基于优先队列的Dijkstra算法实现;掌握基于FIFO队列的Bellman-Ford算法实现;掌握Floyd算法和传递闭包的求法;理解最大流问题的概念、流量的3个条件、残流网络的概念和求法;理解增广路定理与最小割最大流定理的证明方法,会实现Edmonds-Karp算法;理解最小费用最大流问题的概念,以及平行边和反向弧可能造成的问题;会实现基于Bellman-Ford的最小费用路算法。

【教学内容提要】

本章主要介绍了一些常见的图论模型和算法,包括最小生成树、单源最短路、每对结点的最短路、最大流、最小费用最大流等。

对于树型结构介绍了无根树转有根树、表达式树、最小生成树和并查集。

对图状结构,主要介绍了求单源最短路的Dijkstra算法、每对结点的最短路的Floyd算法。

对于网络流主要介绍了最大流的增广路算法、最小割最大流定理、最小费用最大流问题的Bellman-Ford算法。

【教学重点、难点】

教学重点:

(1)掌握Kruskal算法及其正确性证明,并用并查集实现;

(2)掌握基于优先队列的Dijkstra算法实现和基于FIFO队列的Bellman-Ford算法实现;

(3)掌握Floyd算法和传递闭包的求法;

(4)理解增广路定理与最小割最大流定理的证明方法,会实现Edmonds-Karp算法;

(5)会实现基于Bellman-Ford的最小费用路算法。

教学难点:

(1)掌握Kruskal算法及其正确性证明,并用并查集实现;

(2)掌握基于优先队列的Dijkstra算法实现和基于FIFO队列的Bellman-Ford算法实现;

(3)掌握Floyd算法和传递闭包的求法;

(4)理解增广路定理与最小割最大流定理的证明方法,会实现Edmonds-Karp算法;

【课时安排】

11.1再谈树11.2最短路问题

11.3网络流初步11.4进一步学习的参考

 

11.1再谈树

在第6章介绍了二叉树,后面的章节中介绍了解答树、BFS树等其它树状结构。

有n个顶点的树具有以下3个特点:

连通、不含圈、恰好包含n-1条边。

有意思的是,具备上述3个特点中的任意两个,就可以推导出第3个。

11.1.1无根树转有根树

输入一个n结点的无根树的各条边,并指定一个根结点,要求把该树转化为有根树,输出各个结点的父亲编号。

n≤106,如图11-1所示。

图11-1无根树转有限树

【分析】

树是一种特殊的图,可用邻接矩阵来存储,但是邻接矩阵要用n2个元素的空间,空间开销很大。

由于n个结点树只有n-1条边,所以可以用C++的STL(StandardTemplateLibrary,标准模板库存)中的顺序容器vector(向量)来存储。

vectorG[maxn];

voidread_tree(){

intu,v,n;

scanf("%d",&n);//输入结点数n

for(inti=0;i<=n-1;i++){

scanf("%d%d",&u,&v);//输入边对应的结点u和v

G[u].push_back(v);//存入u结点的邻接点为v

G[v].push_back(v);//存入v结点的邻接点为u

}

}

vector是STL中的可变长数组,可理解成STL中的可变长数组,因此G是一个“包含maxn行,但每行长度可以不同”的“二维数组”。

结点u的所有相邻点都放在G[u]中,用G[u].size()获取u的相邻点个数,G[u][i]表示其中第i个相邻点。

由于vector是变长的,这个“二维数组”占用的空间是O(n),而非O(n2)。

下面是转化过程:

voiddfs(intu,intfa){//递归转化以u为根的子树,u的父亲为fa

intd=G[u].size();

for(inti=0;i

intv=G[u][i];//结点u的第i个相邻点v

if(v!

=fa)

dfs(v,p[v]=u);//把v的父亲设为u,然后递归转化为以v为根的子树

}

}

无根树转有根树的完整程序如下:

#include

#include

usingnamespacestd;

constintmaxn=10;//表示最大的结点数为10

vectorG[maxn];//G为超级数组,用作邻接表

intp[maxn-1];//保存每一个节点的父亲

voidread_tree(){

intu,v;

intn;//n个结点

cout<<"输入n个点:

"<

cin>>n;//输入结点数n

for(inti=0;i

cin>>u>>v;//输入边对应的结点u和v

G[u].push_back(v);//存入u结点的邻接点为v

G[v].push_back(u);//存入v结点的邻接点为u

}

}

voiddfs(intu,intfa){//递归转化以u为根的子树,u的父亲为fa

intd=G[u].size();

for(inti=0;i

intv=G[u][i];//结点u的第i个相邻点v

if(v!

=fa){

dfs(v,p[v]=u);//把v的父亲设为u,然后递归转化为以v为根的子树

cout<

}

}

}

intmain(){

memset(p,-1,sizeof(p));//将数组p中元素初始化为-1

read_tree();//读入边对应的结点

dfs(1,-1);//递归调用dfs,初次调用时1表示根结点,-1表示结点1的无父结点

return0;

}

在主程序中设置p[root]=-1(表示根结点的父亲不存在),然后调用dfs(root,-1)即可,如果没有判断结点v是否和其父亲相等,将引起无限递归。

本程序的思想仍然是DFS的思想,搜索每一个节点的邻接点,然后保存父亲,之后递归。

运行结果:

输入数据:

8

01

02

03

14

15

56

57

输出结果:

2的父结点为0

3的父结点为0

0的父结点为1

4的父结点为1

6的父结点为5

7的父结点为5

5的父结点为1

说明:

(1)vector向量容器简介

vector向量容器不但能像数组一样对元素进行随机访问,还能在尾部插入元素,是一个种简单、高效的容器,完全可以代替数组。

值得注意的是,vector具有内存自动管理的功能,对于元素的插入和删除,可动态调整所占用的内存空间。

使用vector向量容器,需要头文件包含“#inlcude”。

vector文件在C:

\Prog

ramFiles\MicrosoftVisualStudio\VC98\Include文件夹中可以找到。

vector容器的下标是从0开始计数的,也就是说,如果vector容器的大小是n,那么,元素的下标是0~n-1。

对于vector容器的容量定义,可以事先定义一个固定大小,事后,可以随时调整其大小;也可以事先不定义,随时使用push_back()方法从尾部扩张元素,也可以使用insert()在某个元素位置前插入新元素。

vector容器有两个重要的方法:

begin()和end()。

begin()返回的是首元素位置的迭代器;end()返回的是最后一个元素的下一元素位置的迭代器。

(2)创建vector对象

创建vector对象常用的有三种方式:

①不指定容器的元素个数,如定义一个用来存储整型的容器:

vectorv;

②创建时,指定容器的大小,如定义一个用来存储10个double类型元素的向量容器:

vectorv(10);

注意,元素的下标为0~9;另外,每个元素的值初始化为0.0。

③创建一个具有n个元素的向量容器对象,每个元素具有指定的初始值:

vectorv(10,8.6);

上述语句定义了v向量容器,共有10个元素,每个元素的值是8.6。

(3)尾部元素扩张

通常使用push_back()对vector容器在尾部追加新元素。

尾部追加元素,vector容器会自动分配新内存空间。

可对空的vector对象扩张,也可对已有元素的vector对象扩张。

但在vector中无push_front(),因为对vector进行push_front()会造成所有元素的迁移,不符合vector设计的初衷。

例如,将2、7、9三个元素从尾部添加到v容器中,这样,v容器中就有三个元素,其值依次2、7、9。

部分代码如下:

vectorv;//一个存放int元素的向量,一开始里面没有元素

v.push_back

(2);

v.push_back(7);

v.push_back(9);

(4)下标方式访问vector元素

访问或遍历vector对象是常要做的事情。

对于vector对象,可以采用下标方式随意访问它的某个元素,当然,也可以以下标方式对某元素重新赋值,这点类似于数组的访问方式。

下面的代码就是采用下标方式对数组赋值,再输出元素的值2、7、9。

部分代码如下:

vectorv(3);

v[0]=2;

v[1]=7;

v[2]=9;

cout<

(5)用迭代器(iterator)访问vector元素

常使用迭代器配合循环语句来对vector对象进行遍历访问,迭代器的类型一要与它要遍历的vector对象的元素类型一致。

部分代码如下:

vectorv(3);

v[0]=2;

v[1]=7;

v[2]=9;

//定义迭代器

vector:

:

iteratorit;

for(it=v.begin();it!

=v.end();it++){

//输出迭代器上的元素值

cout<<*it<<"";

}

STL提供三种类型的组件:

容器、迭代器和算法,它们都支持泛型程序设计标准。

STL设计的精髓在于,把容器(Containers)和算法(Algorithms)分开,而迭代器(iterator)是连接容器和算法的纽带,可见迭代器在STL中的重要程度。

迭代器的作用其实相当于一个智能指针,它指向顺序容器或关联容器中的任意元素,还能遍历整个容器。

可以通过operator*操作符来解指针获得数据的值,也可以通过operator->操作符来获取数据的指针,还能够重载++,--等运算符来移动指针。

不同的容器可能需要不同的迭代器,实际上,在STL中,为每种容器都typedef了一个迭代器,名为iterator。

例如,vector的迭代器类型为vector:

:

iterator(是一种随机访问迭代器)、list的迭代器类型为list:

:

iterator(是一种双向迭代器)。

例如,定义一个容器类的迭代器的方法可以是:

容器类名:

:

iterator变量名;

所以定义一个vector向量容器类的迭代器的方法如下:

vector:

:

iterator变量名;

如vector:

:

iteratorit;。

++it表示向前移动迭代器,使其指向容器的下一个元素。

而*it返回iterator指向元素的值。

每种容器类型提供一个begin()和一个end()成员函数。

begin()返回一个iterator,它指向容器的第一个元素。

end()返回一个iterator,它指向容器的末元素的下一个位置。

(6)元素的插入

insert()方法可以在vector对象的任意位置前插入一个新的元素,同时,vector自动扩张一个元素空间,插入位置后的所有元素依次向后挪动一个位置。

要注意的是,insert()方法要求插入的位置,是元素的迭代器位置,而不是元素的下标。

下面的部分代码输出的结果是8,2,1,7,9,3:

vectorv(3);

v[0]=2;

v[1]=7;

v[2]=9;

//在最前面插入新元素,元素值为8

v.insert(v.begin(),8);

//在第2个元素前插入新元素1

v.insert(v.begin()+2,1);

//在向量末尾追加新元素3

v.insert(v.end(),3);

//定义迭代器变量

vector:

:

iteratorit;

for(it=v.begin();it!

=v.end();it++){

//输出迭代器上的元素值

cout<<*it<<"";

}

(7)元素的删除

erase()方法可以删除vector中迭代器所指的一个元素或一段区间中的所有元素。

clear()方法则一次性删除vector中的所有元素。

下面的代码演示了vector元素的删除方法:

//删除第2个元素(即删除单个元素),从0开始计数

v.erase(v.begin()+2);

//删除迭代器第1到第5区间的所有元素

//即删除一对iterator标记的一段范围内的元素

v.erase(v.begin()+1,v.begin()+5);

//清空向量

v.clear();

清空完向量后,向量中一个元素都没有,即v.size()的值为0。

(8)使用reverse反向排列算法

reverse反向排列算法,需要定义头文件“#inlcude”。

algorithm文件位于C:

\ProgramFiles\MicrosoftVisualStudio\VC98\Include文件夹中。

reverse算法可将向量中某段迭代器区间元素反向排列,部分代码如下:

//反向排列向量的从首到尾间的元素

reverse(v.begin(),v.end());

(9)使用sort算法对向量元素排序

使用sort算法,需要声明头文件“#inlcude”。

sort算法要求使用随机访问迭代器进行排序,在默认的情况下,对向量元素进行升序排列,部分如下:

//排序,升序排列

sort(v.begin(),v.end());

还可以自己设计排序比较函数,然后,把这个函数指定给sort算法,那么,sort就根据这个比较函数指定的排序规则进行排序。

下面的部分代码自己设计了一个排序比较函数Comp,要求对元素的值由大到小排序:

//自己设计排序比较函数:

对元素的值进行降序排列

boolComp(constint&a,contint&b){

if(a!

=b)returna>b;

elsereturna>b;

}

在main()函数就可以用sort算法调用Comp()函数:

//按Comp函数比较规则排序

sort(v.begin(),v.end(),Comp);

(10)向量的大小

使用size()方法可以返回向量的大小,即元素的个数。

使用empty()方法返回向量是否为空。

下面的部分代码演示了size()方法和empty()方法的用法:

//输出向量的大小,即包含了多少个元素

cout<

//输出向量是否为空,如果非空,则返回逻辑假,即0,否则返回逻辑真,即1

cout<

11.1.2表达式树

二叉树是表达式处理的常用工具。

例如,a+b*(c-d)-e/f可以表示成如图11-2所示的二叉树。

图11-2表达式树

其中,每个非叶结点表示一个运算符,左子树是第1个运算符对应的表达式,而右子树则是第2个运算数对应的表达式。

给表达式建立表达式树的方法很多,其中的一种方法是:

找到“最后计算”的运算符(它是整棵表达式树的根),然后递归处理。

下面的函数buildTree是根据算术表达式来建立算术表达式树。

部分代码如下:

constintMAXN=1000;

intlch[MAXN],rch[MAXN];//每个结点的左右儿子

charop[MAXN];//存放每个结点的字符

charinput[MAXN];//input数组存放输入的表达式字符串

intnc=0;//nc存放表达式树的结点,开始时结点数为0

intbuildTree(char*s,intx,inty){//根据表达式建立表达式树

//c1、c2分别记录“最右”出现的加减号和乘除号

//标志p=0避免记录括号内的+、-、*、/

inti,c1=-1,c2=-1,p=0;

intu;

if(y-x==1){//仅一个字符,建立单独结点

u=nc++;

lch[u]=rch[u]=0;//单独结点无左右子树,所以左右子树的编号为0

op[u]=s[x];//此时把s[x]值赋给数组op第u个元素

returnu;

}

for(i=x;i

switch(s[i])

{

case'(':

p++;

break;

case')':

p--;

break;

case'+':

case'-':

if(!

p)c1=i;

break;

case'*':

case'/':

if(!

p)c2=i;

break;

}

}

if(c1<0)c1=c2;//找不到括号外的加减号,就用乘除号

if(c1<0)returnbuildTree(s,x+1,y-1);//整个表达式被一对括号括起来

u=nc++;

//对结点的左子树区间[x,c1],对表达式进行处理

lch[u]=buildTree(s,x,c1);

//对结点的右子树区间[c1+1,y],对表达式进行处理

rch[u]=buildTree(s,c1+1,y);

op[u]=s[c1];//

returnu;

}

代码围绕一个核心思想:

对于一个算式,找到最后一个被使用的运算符作为划分,以此运算符为界,递归计算左边的值,递归计算右边的值,然后以此运算符进行运算,即可得到结果。

主要任务是找最后一个被使用的运算符。

例如:

2+3*(4-1)-5/1,肯定先算括号里的,然后算*、/法,最后才考虑+、-法。

所以,先考虑+、-,再考虑*、/,括号外的+、-、*、/可能很多,所以确定一个原则,找整个算术表达式中最右边+、-、*、/。

由于要找的是括号外的+、-、*、/,所以得想办法避免记录括号内的+、-、*、/,所以设置了一个标志p,初始0。

一旦遇到一个左括号,p+1,这时候说明目前在括号内,不应该记录+-、*、/,当遇到右括号,p-1,p恢复为0,这时候说明目前已经走出括号,可以记录+、-、*、/。

扫描完整个算术表达式的时候,c1记录了最右边的括号外的+、-号,c2记录了最右边的括号外的*、/号。

如果c1<0,说明没扫描到括号外的+、-号,那么只能考虑*、/号作为最后一个运算的运算符了。

把c1=c2,然后判断c1<0,如果还<0,说明括号外也没有*、/号,说明整个算式被括号包围起来了。

所以可以递归运算时忽略这对括号,即递归(x+1,y-1)的算式,返回它的子树根。

这样,就找到了最后计算的运算符s[c1],它的左子树是区间[x,c1],右区间[c1,y]。

提示11-1:

建立表达式树的一种方法是每次找到最后计算的运算符,然后递归建树。

“最后计算”的运算符是在括号外的、优先级最低的运算符。

如果有多个,根据结合性来选择:

左结合的(如加、减、乘、除)选最右边;右结合的(如乘方)选最左边。

根据规定,优先级相同的运算符的结合性总是相同。

建立表达式树的完整程序如下:

#include

usingnamespacestd;

constintMAXN=1000;

intlch[MAXN],rch[MAXN];//每个结点的左右儿子

charop[MAXN];//存放每个结点的字符

charinput[MAXN];//input数组存放输入的表达式字符串

intnc=0;//nc存放表达式树的结点,开始时结点数为0

/*****************************************************

递归构造表达式树,每次找括号外的+、-、*、/

优先+、-划分,其次*、/划分,原则是找最后运算的符号

如果所有运算符均在括号内,忽略这个括号,继续递归内层

*****************************************************/

intbuildTree(char*s,intx,inty){//根据表达式建立表达式树

//c1、c2分别记录“最右”出现的加减号和乘除号

//标志p=0避免记录括号内的+、-、*、/

inti,c1=-1,c2=-1,p=0;

intu;

if(y-x==1){//仅一个字符,建立单独结点

u=nc++;

lch[u]=rch[u]=0;//单独结点无左右子树,所以左右子树的编号为0

op[u]=s[x];//此时把s[x]值赋给数组op[u]

returnu;

}

for(i=x;i

switch(s[i])

{

case'(':

p++;

break;

case')':

p--;

break;

case'+':

case'-':

if(!

p)c1=i;

break;

case'*':

case'/':

if(!

p)c

展开阅读全文
相关资源
猜你喜欢
相关搜索

当前位置:首页 > 高中教育 > 小学教育

copyright@ 2008-2022 冰豆网网站版权所有

经营许可证编号:鄂ICP备2022015515号-1