趣谈数据结构(十)
                      
                     福州一中 陈颖

  本讲通过几个例子,着重讲解图的两种存储结构--邻接表和邻接矩阵的使用,介绍图应用中常见的几种问题的处理方法及算法。

  例1 有如图1所示的七巧板,试编写一源程序如下,使用至多四种不同颜色对七巧板进行涂色(每块涂一种颜色),要求相邻区域的颜色互不相同,打印输出所有可能有
涂色方案。
  分析:本题实际上就是著名的"地图四色"问题。无论地图多么复杂,只需用四种颜色就可以将相邻的区域分开。
               
                   图 1
  为了解题方便,我们可以把七巧板上每一个区域看成一个顶点,若两个区域相邻,则相应的顶点间用一条边相连,这样,该问题就转化为图的问题了。
  算法步骤:
  按顺序分别对1号、2号、......、7号区域进行试探性涂色,用1、2、3、4号代表4种颜色。数据采用邻接矩阵。
  ⒈对某一区域涂上与其相邻区域不同的颜色。
  ⒉若使用4种颜色进行涂色均不能满足要求,则回溯一步,更改前一区域的颜色。
  ⒊转步骤1继续涂色,直到全部结束为止,输出。
  源程序如下:
  program four-colors(input,output,fdat);
  const
  max=10;
  type
  gradat=array[1..max,1..max] of byte;
  var
  data:gradat;
  n:byte;
  color:array[1..max] of byte;
  total:integer;
  procedure getdata;{输入数据}
  var
  name:string[12];
  fdat:text;
  i,j:byte;
  begin
  write('use which file?');
  readln(name);
  assign(fdat,name);
  reset(fdat);
  read(fdat,n);
  for i:=1 to n do
  for j:=1 to n do
  read(fdat,data[i,j]);
  for i:=1 to n do
  begin
  for j:=1 to n do write(data[i,j]:5);
  writeln;
  end;
  writeln;
  end;
  function colorsame(s:byte):boolean;{判断相邻点是否同色}
  var
  i:byte;
  begin
  colorsame:=false;
  for i:=1 to s-1 do
  if (data[i,s]=1) and (color[i]=color[s]) then colorsame:=true;
  end;
  procedure print;{输出}
  var
  i:byte;
  begin
  for i:=1 to n do write(color[i]:2);
  inc(total);
  writeln(' con:',total);
  end;
  procedure try(s:byte);{递归搜索}
  var
  i:byte;
  begin
  if s>7 then print
  else
  begin
  i:=0;
  repeat
  inc(i);
  color[s]:=i;
  if not(colorsame(s)) then try(s+1);
  until i=4
  end;
  end;
  begin {主源程序如下}
  getdata;
  total:=0;
  try(1);
  writeln('Total=',total);
  readln;
  end.
  fdat文件内容:
  7
  0 1 0 0 1 0 1
  1 0 0 1 0 1 0
  0 0 0 0 0 1 1
  0 1 0 0 0 1 1
  1 0 0 0 0 0 1
  0 1 1 1 0 0 0
  1 0 1 1 1 0 0

  例2 对图2从V1点出发,沿着边系统地访问该图中其它所有的顶点一次且仅一 次。(用两种不同的解法)
   
          图2                图 3
  分析:从图上某点出发,沿边访问图中其它所有的顶点一次且仅一次,称为图的遍历。图的遍历有两种方式:深度优先搜索遍历与广度优先搜索遍历。
  算法步骤:

  (一)深度优先搜索遍历算法
  ⒈以给定的某个顶点V0为起始点,访问该顶点;
  ⒉选取一个与顶点V0相邻接且未被访问过的顶点V1,用V1作为新的起始点,重复上述过程;
  ⒊当到达一个其所有邻接的顶点都已被访问过的顶点Vi时,就退回到新近被访问过的顶点Vi- 1,继续访问Vi-1尚未访问的邻接点,重复上述搜索过程;
  ⒋直到从任意一个已访问过的顶点出发,再也找不到未被访问过的顶点为止,遍历便告完成。
  这种搜索的次序体现了向纵深发展的趋势,所以称之为深度优先搜索。
  图13.14中从V1出发的一种深度优先搜索访问顺序:
  V1-->V2-->V4-->V8-->V5-->V6-->V3-->V7

  (二)广度优先搜索遍历算法
  ⒈从图中的某一顶点V0开始,先访问V0;
  ⒉访问所有与V0相邻接的顶点V1,V2,......,Vt;
  ⒊依次访问与V1,V2,......,Vt相邻接的所有未曾访问过的顶点;
  ⒋循此以往,直至所有的顶点都被访问过为止。
  这种搜索的次序体现沿层次向横向扩长的趋势,所以称之为广度优先搜索。
  图13.14中,从V1出发的一种广度优先搜索访问顺序为:
  V1-->V2-->V3-->V4-->V5-->V6-->V7-->V8
  源程序如下:
  源程序如下中数据采用邻接表方式表示。邻接表如图13.15所示。
  存储邻接表数据需要三个变量,一个存储结点的内容,一个存储结点指向下一个结点的指针,一个存储每个链表的链头指针。设数组变量A(i,0)存放结点,A(i,1)存放结点的指针,H存放链头指针。
  数据存放形式如表1所示。 
                  表1
   

  源程序如下:
program bianli(input,output,fdat);
const
max=10;
type
gradat=array[1..max,1..max] of integer;
way=set of 1..max;
var
data:gradat;
n,k:byte;
path:array[1..max] of byte;
already:way;
m:integer;
procedure getdata;{输入数据}
var
name:string[12];
fdat:text;
i,j:byte;
begin
write('use which file?');
readln(name);
assign(fdat,name);
reset(fdat);
read(fdat,n);
for i:=1 to n do
begin
j:=0;
repeat
inc(j);
read(fdat,data[i,j]);
until data[i,j]=0;
end;
for i:=1 to n do
begin
j:=1;
write('[',i:2,' ]');
while data[i,j]<>0 do
begin
write(data[i,j]:3);
inc(j);
end;
writeln;
end;
end;
procedure print;{输出}
var
i:byte;
begin
for i:=1 to n do write(path[i]:2);
writeln;
end;
procedure width(s:byte);{广度优先搜索遍历}
var
useful,next:way;
i,j:byte;
begin
k:=1;
path[k]:=s;
useful:=[s];
already:=[s];
next:=[];
repeat
for i:=1 to n do
if i in useful then
begin
j:=1;
repeat
if not(data[i,j] in already) then
begin
next:=next+[data[i,j]];
already:=already+[data[i,j]];
inc(k);
path[k]:=data[i,j];
end;
inc(j)
until data[i,j]=0;
end;
useful:=next;
next:=[];
until k=n;
end;
procedure deepth(s:byte);{深度优先搜索遍历}
var
i:byte;
begin
i:=1;
inc(k);
path[k]:=s;
already:=already+[s];
while data[s,i]<>0 do
begin
if not(data[s,i] in already)
then deepth(data[s,i]);
inc(i);
end;
end;
begin
getdata;
k:=0;
already:=[];
repeat
write('Which do you want to begin:');
readln(m);
until (m>0) and (m<=n);
deepth(m);
write('Deep path is: ');
print;
width(m);
write('Wide path is: ');
print;
readln;
end.
fdat文件数据:
8
2 3 0
1 4 5 0
1 6 7 0
2 8 0
2 8 0
3 8 0
3 8 0
4 5 6 7 0

  例3 如图4所示,从A到B有好几条可供选择的路线, 每条路线的长度均标示在图上,问选择什么样的路线,可使从A到B所走的路径最短?
      
               图 4
  分析:在一个有向图G=(V,E)中,给定一个始点V1和终点Vp,对每条弧(Vi,Vj) 相应地有一个权W(i,j),最短路问题就是要求从始点V1到终点Vp的一条路径,使其在所有从V1到Vp的路径中,它是总权最小的一条。
  当所有的W(i,j)>=0时,解决最短路径问题的比较好的方法是Dijkstra方法。 它是荷兰计算机科学家(E.W.Dijkstra)于1959年提出的。这个方法不仅求出了从A到顶点B的最短路径,而且求出了从A到各顶点的最短路径。
  算法步骤:
  从V1开始,给每一个顶点赋予一个标号,标号分为两种:
  (1)T标号--V1到该顶点的最短路径的上界(临时标号);
  (2)P标号--V1到该顶点的最短路径(固定标号)。
  若某一顶点已得到P标号,则表明已求出V1到该点的最路径,凡未得到P标号的顶点,一律标上T标号。
  算法的每一步把某一顶点的T标号改变为P标号,最多经过(n-1)步(n为顶点个数),即可得到从V1到每一个顶点的最短路径。
  ⒈在初始状态下,令P(V1)=0,T(Vi)=+∞(i=1,2,3,......,n), 反复执行下面两个步骤;
  ⒉设Vi是已经得到P标号的点,考虑所有满足下列条件的顶点Vi的集合B:
  B={ Vj|(Vi,Vj) E且Vj是T标号}
将集合B中的每一个顶点Vj的T标号修改为:
min{T(Vj),P(Vi)+W(i,j)}
  ⒊若G中已没有T标号的顶点,则结束,否则,将集合B中最小者的T标号改变为P 标号,然后转到2。
  下面,以求V1到V7的最短路径为例,说明Dijkstra方法:
开始,令P(V1)=0,T(Vj)=+∞(j=1,2,3,...,7)

  第一步:考虑所有与V1相联且标有T标号的点的集合:
   B={V2,V3,V4};修改集合B中元素的T标号
   T(V2)=min{T(V2),P(V1)+W(1,2)}=min{+∞,2}=2
   T(V3)=min{T(V3),P(V1)+W(1,3)}=min{+∞,5}=5
   T(V4)=min{T(V4),P(V1)+W(1,4)}=min{+∞,3}=3
  将其中最小者改为标号:P(2)=2

  第二步:考虑所有与V1,V2相联且标有T标号的点集合:
   B={V3,V4,V6};修改集合B中元素的T标号
   T(V3)=min{T(V3),P(2)+W(2,3)}=min{5,4}=4
   T(V4)=3 (同前面求法)
   T(V6)=min{T(V6),P(2)+W(2,6)}=min{+∞,9}=9
  将其中最小者改变为P标号,P(V4)=3

  第三步:考虑所有与V1,V2,V4相联且标有T标号的点的集合:
   B={V3,V5,V6};修改集合B中元素的T标号
   T(V3)=4
   T(V5)=min{T(V5),P(V4)+W(4,5)}=min{+∞,8}=8
   T(V6)=9
  将其中最小者改变为P标号:P(V3)=4

  第四步:考虑所有与V1,V2,V3,V4相联且标有T标号的点的集合:
   B={V5,V6};修改集合B中的元素的T标号
   T(V5)=min{T(V5),P(V3)+W(3,5)}=min{8,7}=7
   T(V6)=min{T(V6),P(V3)+W(3,6)}=min{9,9}=9
  将其中最小者改变为P标号:P(V5)=7

  第五步:考虑所有与V1,V2,V3,V4,V5相联且标有T标号的点的集合:
   B={V6,V7};修改集合B中的元素的T标号
   T(V6)=min{T(V6),P(V5)+W(5,6)}=min{9,8}=8
   T(V7)=min{T(V7),P(V5)+W(5,7)}=min{+∞,14}=14
  将其中最小者改变为T标号:P(V6)=8

  第六步:考虑所有与V1,V2,V3,V4,V5,V6相联且标有T标号的点的集合:
   B={V7};修改集合中B元素的T标号
   T(V7)=min{T(V7),P(V6)+W(6,7)}=min{14,13}=13
  将其中最小者改变为P标号:P(V7)=13
  至此,所有顶点的T标号均已改变为P标号了,说明V1到各顶点的最短路径已全部找到。

  源程序如下:
program least way;
const
max=10;
type
gradat=array[1..max,1..max] of real;
way=set of 1..max;
var
a,b:byte;
n:byte;
data:gradat;
path,path1:array[1..max] of byte;
least:real;
k:byte;
procedure getdata;{输入数据}
var
name:string[12];
fdat:text;
i,j:byte;
begin
write('use which file?');
readln(name);
assign(fdat,name);
reset(fdat);
read(fdat,n);
for i:=1 to n do
for j:=1 to n do
read(fdat,data[i,j]);
for i:=1 to n do
begin
for j:=1 to n do write(data[i,j]:5);
writeln;
end;
writeln;
end;
procedure print;{输出}
var
i:byte;
begin
i:=1;
write('Path is:',a:2);
while path[i]<>0 do
begin
write(path[i]:2);
inc(i);
end;
writeln;
writeln('Longth=',least);
end;
procedure lookfor(d:integer;s:real;k:integer;already:way);{求最短路径}
var
j:byte;
begin;
for j:=1 to n do
if (data[d,j]<>0) and not(j in already) then
begin
path1[k]:=j;
if (j=b) and (s+data[d,j]<least) then
begin
path:=path1;
least:=s+data[d,j];
path[k+1]:=0;
end
else lookfor(j,s+data[d,j],k+1,already+[j]);
end;
end;
begin
getdata;
write('From which>');
readln(a);
write(' To which>');
readln(b);
least:=1e+36;
lookfor(a,0,1,[a]);
print;
readln;
end.
fdat文件数据:
7
0 2 5 3 0 0 0
0 0 2 0 0 7 0
0 0 0 1 3 5 0
0 0 0 0 5 0 0
0 0 0 0 0 1 7
0 0 0 0 0 0 5
0 0 0 0 0 0 0

  例4 图5所示的网络代表6个城市间的交通网,边上权是公路的造价,现在要用公路把6个城市联系起来,这要修筑5条公路,如何设计使得这5 条公路总的造价最小。
        
             图 5
  分析:这是一个最小生成树问题,所谓最小生成树就是从某个顶点出发遍历图中的各点,且使各边权的总和为最小,一个网络的最小生成树不一定是唯一的。图6是图5的两个最小生成树,它们权的总和均为5+16+11+6+18=56。
       
                  图 6
  算法步骤:
  构成最小生成树的典型算法有Prime算法与Kruskal算法等。本题解答用Kruskal算法:
  ⒈先把平面上各顶点之间的边统统擦掉,只剩下一些孤立的点;
  ⒉把各边的长度按从小到大的次序排列好;
  ⒊按从小到大的次序把每边加到图中去,每加进去一条边,就判断一下是否产生回路?没有回路就加进去下一条边;若产生回路就放弃这条边,而考虑下一条边,碰到两条等长的边,可任取一条;
  ⒋重复步骤3,直至加满(n-1)条边,就把n个顶点连接起来了,且边长总和为最小。
  源程序如下:
program least cost;
const
max=10;
type
way=set of 1..max;
gradat=array[1..max,1..max] of real;
var
data:gradat;
n:byte;
cost:real;
already:way;
k:byte;
path:array[1..2,1..max] of byte;
procedure getdata;{输入数据}
var
name:string[12];
fdat:text;
i,j:byte;
begin
write('use which file?');
readln(name);
assign(fdat,name);
reset(fdat);
read(fdat,n);
for i:=1 to n do
for j:=1 to n do
read(fdat,data[i,j]);
for i:=1 to n do
begin
for j:=1 to n do write(data[i,j]:5);
writeln;
end;
writeln;
end;
procedure print;{输出}
var
i:byte;
begin
for i:=1 to n-1 do
writeln('Road side:',path[1,i]:3,path[2,i]:3);
writeln('Total=',cost);
end;
procedure lookfor(var n1,n2:byte);{查找这一轮中的最小边}
var
i,j:byte;
m:real;
begin
m:=1e+36;
for i:=1 to n do
if i in already then
for j:=1 to n do
if (not(j in already)) and (data[i,j]<>0) and (data[i,j]<m) then
begin
m:=data[i,j];
n1:=i;
n2:=j;
end;
end;
procedure add;{记录最小生成树及总造价}
var
i,j:byte;
begin
repeat
lookfor(i,j);
already:=already+[j];
cost:=cost+data[i,j];
inc(k);
path[1,k]:=i;
path[2,k]:=j;
until already=[1..n];
end;
begin
getdata;
already:=[1];
k:=0;
cost:=0;
add;
print;
readln;
end.
  fdat文件数据:
6
0 16 0 0 19 21
16 0 5 6 0 11
0 5 0 6 0 0
0 6 6 0 18 14
19 0 0 18 0 33
21 11 0 14 33 0

   

 
网站导航 | 关于曙光 | 联系我们 | 请提意见
Copyright © FuJian Sunshine Educational Info. Co.,Ltd.
福建曙光教育资讯有限公司 版权所有