本章将抛砖引玉地探讨一些程序设计中常用的知识及算法,包括图论及其基本应用,动态规划等。要想对这些问题进行深一步地研究,可以系统地学习图论、组合数学、运筹学等书籍。
图是另一种有层次关系的非线性的数据结构。在日常生活中有图的许多实例,如铁路交通网,客运航空线示意图,化学结构式,比赛安排表等。下面 的如几个例子都可称为图。在实际运用中,我们可以用图来表示事物间相互的关系,从而根据需要灵活地构建数学模型。例如:图(A),可以用点来表示人,如果两个人相互认识,则在表示这两个人的点之间连一条线。这样从图中我们就可以清楚地看到,这些人之间相互认识的关系。图(B):可以用点表示城市,若两城市间有连线则表示可在这两城市架设通信线路,线旁的数字表示架设这条线路的费用。图(C):4个点表示4支足球队,它们进行循环比赛,若甲队胜乙队,则连一条由甲队指向乙队的有向线段。在上面三个例子中,(A),(B)又可称为无向图,(C)称为有向图,其中(B)是一个有权图。
图常用的存储方式有两种,一种是邻接表法,另一种是邻接矩阵法。
邻接表表示图有两个部分:一部分表示某点的信息,另一部分表示与该点相连的点。例如:图A的邻接表如下所示:
┌─┬─┐ ┌─┬─┐ ┌─┬─┐ ┌─┬─┐
│v1│─┼→│v2│─┼→│v3│─┼→│v4│^ │
└─┴─┘ └─┴─┘ └─┴─┘ └─┴─┘
┌─┬─┐ ┌─┬─┐ ┌─┬─┐
│v2│─┼→│v1│─┼→│v3│^ │
└─┴─┘ └─┴─┘ └─┴─┘
┌─┬─┐ ┌─┬─┐ ┌─┬─┐
│v3│─┼→│v1│─┼→│v2│^ │
└─┴─┘ └─┴─┘ └─┴─┘
┌─┬─┐ ┌─┬─┐
│v4│─┼→│v1│^ │
└─┴─┘ └─┴─┘
邻接矩阵是用一个二维数组表示图的有关信息,比如图A的邻接矩阵为:
┌─┬─┬─┬─┬─┐
│点│v1│v2│v3│v4│
├─┼─┼─┼─┼─┤
│v1│0 │1 │1 │1 │
├─┼─┼─┼─┼─┤
│v2│1 │0 │1 │0 │
├─┼─┼─┼─┼─┤
│v3│1 │1 │0 │0 │
├─┼─┼─┼─┼─┤
│v4│1 │0 │0 │0 │
└─┴─┴─┴─┴─┘
在邻接矩阵中,第i行第j表示图中点i和点j之间是否有连线,若有则值为1,否则为0。比如说:点v1和v2之间有连线,则矩阵的第一行第二列的值为1。而矩阵的第四行行三列的值为0说明图中点v4和v3之间没有连线。图A是一个无向图,它的邻接矩阵是一个关于主对角线对称的矩阵。而有向图的邻接矩阵则不一定是对称的。比如图C的邻接矩阵为:
┌─┬─┬─┬─┬─┐
│点│v1│v2│v3│v4│
├─┼─┼─┼─┼─┤
│v1│0 │1 │1 │1 │
├─┼─┼─┼─┼─┤
│v2│0 │0 │1 │1 │
├─┼─┼─┼─┼─┤
│v3│0 │0 │0 │0 │
├─┼─┼─┼─┼─┤
│v4│0 │0 │1 │0 │
└─┴─┴─┴─┴─┘
例一、图的遍历:请编一个程序对下图进行遍历。
分析:"遍历"是指从图的某个点出发,沿着与之相连的边访问图中的每个一次且仅一次。基本方法有两种:深度优先遍历和广度优先遍历。
深度优先和广度优先遍历,与前面所说的树的深度与广度优先遍历是类似的:比下图中,如果从点V1出发,那么:
深度优先遍历各点的顺序为:v1,v2,v4,v7,v5,v3,v6,v8。
广度优先遍历各点的顺序为:v1,v2,v3,v4,v5,v6,v7,v8。
下面是两种方法的Pascal程序:
解:程序一:深度优先遍历
可以仿照前面树的深度优先遍的得出图的深度优先遍历的递归算法,这里是用栈来实现的非递归算法,算法如下:
(1)利用一个栈S来记录访问的路径,由于从点1出发,因此初始时S[1]:=1;
(2)找一个与栈顶的点相连而又未被访问过的点,如找到,则输出并压入栈顶;如果未找到,则栈顶的点弹出。
(3)重复(2)直到所有的点均己输出。如果栈指针为0,而尚的点未输出,说明该图不是一个连通图。
Program lt8_1_a;
uses crt;
const n=8;
v:array[1..8,1..8] of 0..1=((0,1,1,0,0,0,0,0),
(1,0,0,1,1,0,0,0),
(1,0,0,0,0,1,1,0),
(0,1,0,0,0,0,0,1),
(0,1,0,0,0,0,0,1),
(0,0,1,0,0,0,1,0),
(0,0,1,0,0,1,0,0),
(0,0,0,1,1,0,0,0));
var visited:array[1..n] of boolean;
s:array[1..n] of byte;
top,p,i,total:integer;
find:boolean;
begin
clrscr;
fillchar(visited,sizeof(visited),false);
write('V1 ');s[1]:=1;visited[1]:=true;
total:=1;top:=1;p:=1;
repeat
find:=false;
for i:=1 to n do
if not visited[i] and (v[p,i]=1) then
begin
inc(top);s[top]:=i;p:=i;
visited[p]:=true;find:=true;
inc(total);write('V',p,' ');break;
end;
if not find then
begin p:=s[top];dec(top);end;
until total=n;
writeln;
end.
程序二:进行广度优先遍历(程序略)
例二、最短路径。
|
下图是一个铁路交通图的例子。图中的顶点表示车站,分别用1,2,...,6编号,两个点之间的连线则表示铁路线路,而线旁的数字则表示该条线路的长度。要求编一个程序,求出从车站1至车站6的最短路径。
解:本题是在一个有权图中求给定两点之间的最短路径,也许大家会考虑用搜索的方法试探所有可能经过的路线,再从中找出最小者,这样在理论上是成立的,但可能效率不高,这里介绍一种有效的算法,就是Dijkstra算法。
PASCAL程序:
Program lt8_2;
uses crt;
const max=100;
var map:array[1..max,1..max] of integer;
d:array[1..max,1..2] of integer;
p:array[1..max] of boolean;
n:integer;
procedure init;
var i,j:integer;
f:text;
fn:string;
begin
write('Filename:');readln(fn);
assign(f,fn);reset(f);
readln(f,n);
for i:=1 to n do
for j:=1 to n do
read(f,map[i,j]);
close(f);
fillchar(p,sizeof(p),false);
fillchar(d,sizeof(d),0);
end;
function find_min:integer;
var i,min,t:integer;
begin
min:=maxint;
for i:=1 to n do
if not p[i] and (d[i,1]>0) and (d[i,1]<min) then
begin
min:=d[i,1];t:=i;
end;
find_min:=t;
end;
procedure change(t:integer);
var i:integer;
begin
for i:=1 to n do
if not p[i] and (map[t,i]>0)
and (d[i,1]=0) or (d[i,1]>d[t,1]+map[t,i]) then
begin d[i,1]:=d[t,1]+map[t,i];d[i,2]:=t;end;
end;
procedure dijkstra;
var i,j,t:integer;
begin
p[1]:=true;t:=1;
repeat
change(t);
t:=find_min;
p[t]:=true;
until p[n];
end;
procedure print;
var i,t:integer;
r:array[1..max] of integer;
begin
t:=1;r[1]:=6;
while d[r[t],2]>0 do
begin
r[t+1]:=d[r[t],2];inc(t);
end;
writeln('From V1 to V6');
for i:=t downto 2 do
write('V',r[i],'->');
writeln('V',r[1]);
writeln('Min=',d[6,1]);
end;
begin
clrscr;
init;
dijkstra;
print;
end.
练习一、
1、拓扑排序。
有N个士兵(1<=N<=26),依次编号为A,B,...,Z。队列训练时,指挥官要把士兵从高到矮依次排成一行,但现在指挥官不能直接获得每个人的身高信息,只能获得"P1比P2高"这样的比较结果,记为"P1>P2",如"A>B"表示A比B高。现从文本文件读入比较结果的信息,每个比较结果在文本文件中占一行。编程输出一种符合条件的排队方案。若输入的数据无解,则打印"NO ANSWER!"。
例如:若输入为:
A>B
B>D
F>D
则一种正确的输出为:
ABFD
分析:
|
(1)由局部可比信息求得全局可比信息就是拓扑排序。
(2)局部信息可以用一个有向图表示。如A>B,则在加一条由点A指向点B的有向线。那么上例可以用右图表示出来。
(3)拓扑排序的方法:
(a)从图中选一个无前驱的点输出,同时删去由该点出发的所有的有向线;
|
(b)重复a,直至不能进行为止。此时若图中还有点未输出,则问题无解;否则,输出的序列即为一可行方案。
注意:在步骤(3)的b中,如果图中还有点未输出,说明图中有一个回路(即环)。这在拓扑排序中是矛盾的。比方说有如右图所示的一个环,其表示出来关于身高的信息是:A>B,B>C,C>A,这显然是矛盾的,因此无解。
解:略
2、地图四色:用不超过四种的颜色给一个地图染色,使得相邻的两个地区所着的颜色各不相同。
3、最小部分树。
有一个城市分为N个区,现要在各区之间铺设有线电视线路。任意两个区之间铺设线路的费用如下图所示,其中图的顶点表示各个区,线旁的数字表示铺设该条线路的费用。要求设计一个费用最省的铺线方案使得每一个区均能收看到有线电视
分析:(1)这个问题实际上是求图的最小部分树。因为每一个区均能收看到有线电视,所以该图是连通的;又因为要求费用最省,所以该图无圈。
(2)求图的最小部分树常用Prim算法(也称加边法)和Kurskal算法(也称破圈法)
这里介绍的是Prim算法。具体操作如下:
(a)去掉图中所有的线,只留下顶点;
(b)从图中任意选出一个点加入集合S1,余下的点划入集合S2;
(c)从所有连接集合S1与S2的点的线中选一条最短的加入图中,并将该线所连的集合S2中的点从集合S2中删去,加入集合S1中;
(d)重复步骤c,直至往图中加入N-1条边(N为顶点的个数)。
4、一笔画:编一个程序对给定的图进行一笔画。
分析:图的一笔画是指从图的某一个顶点出发,经过图中所有的边一次且仅一次。现由文本文件读入一个图的邻接矩阵,判断该图能否一笔画,若能,则给出一笔画的方法。
分析:一笔画是图论中研究得比较早的一个问题,一个图能够一笔画的条件是:
(1)该图是一个连通图;
(2)该图至多有2个度为奇数的点。
这里所说的"度"是指与一个点相连的边的数目,如果边的条数是奇数,则该点的度是奇数,否则是偶数。
在一笔画问题中,如果连通图所有的点的度均为偶数,则一笔画时可以从任意一点出发,最后又能回到起点。如果有两个点的度为奇数,则一笔画时,一定是从一个奇数度的点出发,最后到达另一个奇数度的点。此外,在连通图中,奇数度的点总是成对出现的。一笔画所经过的路线又叫欧拉路(如果是回路的话,也叫欧拉回路)。
找欧拉路(或回路)可以用Fleury算法,如下:
(1)找一个出发点P(如果图中有两个奇数度的点话,任取其一)。
(2)找一条与P相连的边,伸展欧拉路(选边的时候应注意,最后再选取断边;断边是:当去掉该边后使得图不连通的边)。
(3)将选出的边所连的另一个点取代P,去掉该边,重复(2),直至经过所有的边。
5、哈密尔顿问题。
哈密尔顿问题是指在一个图中找出这样的一条路:从一个图的顶点出发,经过图中所有顶点一次且仅一次。象这样的路称为哈密尔顿路。现由文本文件读入一个图的邻接表,判断该图是否有哈密尔顿路,若有则输出。
6、图的关节点。
如果从一个连通图中删去某点V,使得该图不连通,那么V点就称为该图的一个关节点。现从文本文件中读入一个图的邻接矩阵,试编程找出该图所有的关节点。
7、有一集团公司下有N个子公司,各子公司由公路连接,现要在N个子公司中选一个出来成立总装车间,装配各子公司送来的部件,且使得各子公司到总装车间的路程总和最小。
动态规划是近来发展较快的一种组合算法,是运筹学的一个分支,是解决多阶段决策过程最优化的一种数学方法。我们可以用它来解决最优路径问题,资源分配问题,生产调度问题,库存问题,装载问题,排序问题,设备更新问题,生产过程最优控制问题等等。
在生产和科学实验当中,有一类活动的过程,可将它分成若干个阶段,在它的每个阶段要作出决策,从而使全局达到最优。当各个阶段决策确定后,就组成一个决策序列,因而也就决定了整个过程的一条活动路线。这种把一个过程看作一个前后相关具有链状结构的多阶段过程就称为多阶段决策过程。
所谓动态是指在多阶段决策问题中,各个阶段采取的决策,一般来说是与时间有关的,决策依赖于当前的状态,又随即引起状态的转移,一个决策序列就是在变化的状态中产生,故有"动态"的含义。
下面,我们结合最短路径问题来介绍动态规划的基本思想。
求下图中点O到点U的最短距离(假设只许往上和往右走)。
|
从点O到点U,可以按经过的路径,分成七个阶段,分别为:O->AB->CDE->FGHJ->
KLMN->PQR->ST->U。
最短路径有一个重要特性:如果点O经过点H到达点U是一条最短路径,则在这条最优路径上由点H出发到达点U的子路径,是由点H出发到达点U所有可能选择的不同路径的最短路径(证明略)。根据这一特点,寻找最短路径的时候,可以从最后一段开始,用由后向前逐段递推的方法,求出个点到点U的最短路径。
如若考虑到从点O到点U的最短路径,也是该路径上个点到点的最短路径,令O点到U点的最短距离为dO,A点到U点的最短距离为dA,...,故有:
dO=min{2+dA,1+dB},
dA=min{3+dC,2+dD},
dB=min{2+dD,3+dE},
................
dQ=min{5+dT,2+dS}.
下面按照动态规划的方法,将上例从最后一段开始计算,由后向前逐步递推移至O点。计算步骤如下:
阶段7:从S点或T点到达U点,这时各自只有一种选择,故:
dS=2;dT=3;
阶段6:出发点有P,Q,R三个,其中Q点到达U点有两种选择,或是经过S点,或是经过T点,故:
dQ=min{2+dS,5+dT}=min{4,8}=4;
dP=min{1+dS}=min{3}=3;
dR=min{3+dT}=min{6}=6;
阶段5:出发点有K,L,M,N四个,同理有:
dK=min{3+dP}=min{6}=6;
dL=min{2+dP,4+dq}=min{5,8}=5;
dM=min{2+dQ,4+dR}=min{6,10}=6;
dN=min{4+dR}=min{10}=10;
阶段4:出发点有F,G,H,J四个,同理有:
dF=min{2+dK}=min{8}=8;
dG=min{1+dK,3+dL}=min{7,8}=7;
dH=min{1+dL,1+d}=min{6,7}=6;
dJ=min{3+dM,3+dN}=min{9,13}=9;
阶段3:出发点有C,D,E三个,同理有:
dC=min{2+dF,2+dG}=min{10,9}=9;
dD=min{4+dG,2+dH}=min{11,8}=8;
dE=min{1+dH,2+dJ}=min{7,11}=7;
阶段2:出发点有A,B两个,同理有:
dA=min{3+dC,2+dD}=min{12,10}=10;
dB=min{2+dD,3+dE}=min{10,10}=10;
阶段1:出发点是O,同理有:
dO=min{2+dA,1+dB}=min{12,11}=11.
由此得到全过程的最短路径是11,并且可以由以上推导过程反推得最短的路线是:O->B->D->H->L->P->S->U;或O->B->E->H->L->P->S->U。
从上可以知道,能应用动态规划解决的问题,必须满足最优性原则,动态规划的关键在于正确的写出基本的递推关系式和恰当的边界条件。它需要把问题的过程化成几个互相联系的阶段,恰当的选取状态变量和决策变量及定义最优值函数,从而把一个大问题化成一族同类型的子问题,然后逐个求解。即从边界条件开始,逐段递推寻优,在每一个子问题的求解中,都利用到前面的子问题的最优化结果,依次进行,最后一个子问题的最优解,就是整个问题的最优解。
例一、数字三角形:下图给出了一个数字三角形.请编一个程序计算从顶至底的某处的一条路径,使得该路径所经过的数字的总和最大。对于路径规定如下:
(1)每一步可沿左斜线向下走或右斜线向下走; ⑦
(2)1<三角形的行数<=100; ③ 8
(3)三角形中的数字为整数0,1,...,99; ⑧ 1 0
输入数据:由文件INPUT.TXT中首先读出的 2 ⑦ 4 4
是三角形的行数。在例子中INPUT.TXT表示如下: 4 ⑤ 2 6 5
5
7
3 8
8 1 0
2 7 4 4
4 5 2 6 5
输出最大的总和。如上例为:30(其路径如图圈的数字)。
分析:(1)假设最优路径(即总和最大)为⑦→③→⑧→⑦→⑤,则子路径⑦→③→⑧→⑦必定是从初始点⑦到中间点⑦的所有路径中最优的,否则,如果从初始点⑦到中间点⑦还有另一条的路径更优,假设为⑦→a1→a2→⑦,则新路径⑦→a1→a2→⑦→⑤则优于原来假设的最优路径⑦→③→⑧→⑦→⑤,与假设矛盾。从以上反证法可以清楚地知道:数字三角形问题满足动态规划的最优性原则,可以利用动态规划求解。
(2)采用顺推法:记第i行第j列的数字为a(i,j),从初始点到a(i,j)的最大总和记为f(i,j);则应有第一层上的数字的a(1,1)的最大总和为它本身,即:
f(1,1)=a(1,1);
以下各层按如下方法递推:
f(i,j)=a(i,j)+max{f(i-1,j-1),f(i-1,j)}
(3)用以上方法递推计算完最后一层,最后一中寻最大值即为本题的解.
解:Pascal程序:
Program lt10_2_1;
uses crt;
const max=100;
var n:integer;
a:array[0..max,0..max] of longint;
procedure work;
var f:text;
i,j:integer;
begin
assign(f,'input.txt');
reset(f);
readln(f,n);
fillchar(a,sizeof(a),0);
for i:=1 to n do
for j:=1 to i do
begin
read(f,a[i,j]);
if a[i-1,j-1]>a[i-1,j] then
a[i,j]:=a[i,j]+a[i-1,j-1]
else a[i,j]:=a[i,j]+a[i-1,j];
end;
close(f);
end;
procedure print;
var max:longint;
i:integer;
begin
max:=0;
for i:=1 to n do
if a[n,i]>max then max:=a[n,i];
writeln('Max=',max);
end;
begin
clrscr;
work;print;
end.
习题二:
1、某工业生产部门根据国家计划的安排,拟将某种高效率的五台机器,分配给所属的A,B,C三个工厂,各工厂若获得这种机器后,可以为国家盈利如下表,问:这五台机器如何分配给各工厂,才能使国家盈利最大?
单位:万元
P S |
A |
B |
C |
0 |
0 |
0 |
0 |
1 |
3 |
5 |
4 |
2 |
7 |
10 |
6 |
3 |
9 |
11 |
11 |
4 |
12 |
11 |
12 |
5 |
13 |
11 |
12 |
其中:p为盈利,s为机器台数。
2、己知:f(x)=x1^2+2x2^2+x3^2-2x1-4x2-2x3
x1+x2+x3=3 且 x1,x2,x3均为非负整数。
求f(x)的最小值。
3、N块银币中有一块不合格,不合格的银币较正常为重,现用一天平找出不合格的来,要求最坏情况不用天平的次数最少。
4、某一印刷厂有6项加工任务,对印刷车间和装订车间所需时间见下表:
任 务 |
J1 |
J2 |
J3 |
J4 |
J5 |
J6 |
印刷车间 |
3 |
12 |
5 |
2 |
9 |
11 |
装订车间 |
8 |
10 |
9 |
6 |
3 |
1 |
时间单位:天
完成每项任务都要先去印刷车间印刷,再到装订车间装订。问怎样安排这6项加工任务的加工工序,使得加工总工时最少。
5、有一个由数字1,2,...,9组成的数字串(长度不超过200),问如何M(1<=M<=20)个加号插入这个数字串中,使得所形成的算术表达式的值最小。
注意:(1)加号不能加在数字串的最前面或最末尾,也不应有两个或两个以上的加号相邻;
(2)M保证小于数字串的长度。
例如:数字串79846,若需加入两个加号,则最佳方案是79+8+46,算术表达式的值是133。
输入格式:从键盘读入输入文件名。数字串在输入文件的第一行行首(数字串中间无空格且不换行),M的值在输入文件的第二行行首。 输出格式:在屏幕上输出最小的和。