cahiers d`informatique Estructuras de datos y de la información II

Anuncio
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
cahiers d’informatique
cuadernosdeinformáticacomputingnotebooksquadernid’informatica
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p p p p p p
p p p p p p
p p p p p p
p p p p p p
ppp pp ppp pp ppp pp ppp pp ppp pp pp
pp pp pp pp pp pp pp pp pp pp p
pp pp pp pp pp pp pp pp pp pp p
pp pp pp pp pp pp pp pp pp pp p
pppppppppp
ppp ppp ppp ppp ppp ppp ppp ppp ppp ppp pp
pp pp pp pp pp pp pp pp pp pp p
pp pp pp pp pp pp pp pp pp pp p
pp pp pp pp pp pp pp pp pp pp p
ppp ppp ppp ppp ppp ppp ppp ppp ppp ppp p
pp pp pp pp pp pp pp pp pp pp p
pp pp pp pp pp pp pp pp pp pp p
ppp ppp ppp ppp ppp ppp ppp ppp ppp ppp pp
pp pp pp pp pp pp pp pp pp pp p
pp pp pp pp pp pp pp pp pp pp p
ppp ppp ppp ppp ppp ppp ppp ppp ppp ppp p
ppp ppp ppp ppp ppp ppp ppp ppp ppp ppp pp
pp pp pp pp pp pp pp pp pp pp p
pp pp pp pp pp pp pp pp pp pp p
ppp ppp ppp ppp ppp ppp ppp ppp ppp ppp p
pp pp pp pp pp pp pp pp pp pp p
pp pp pp pp pp pp pp pp pp pp p
ppppppppppp
p p p p p p
p p p p p p
p p p p p p
p p p p p p
p p p p p p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
Estructuras de datos y de la
información II
Heap y Grafos
Simone Santini
Escuela Politécnica Superior
Universidad Autónoma de Madrid
2/2009
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p p p p p p p p p p p p p p p p p p p p p
p p p p p p p p p p p p p p p p p p p p p
p p p p p p p p p p p p p p p p p p p p p
p p p p p p p p p p p p p p p p p p p p p
ppp pp ppp pp ppp pp ppp pp ppp pp ppp pp ppp pp ppp pp ppp pp ppp pp ppp pp ppp pp ppp pp ppp pp ppp pp ppp pp ppp pp ppp pp ppp pp ppp pp pp
pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp p
pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp p
pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp p
pppppppppppppppppppppppppppppppppppppppp
ppp ppp ppp ppp ppp ppp ppp ppp ppp ppp pp p pp p pp p pp p pp p pp p pp p pp p pp p pp p pp p pp p pp p pp p pp p pp
pp pp pp pp pp pp pp pp pp pp p p p p p p p p p p p p p p p p
pp pp pp pp pp pp pp pp pp pp p p p p p p p p p p p p p p p p
pp pp pp pp pp pp pp pp pp pp p p p p p p p p p p p p p p p p
ppp ppp ppp ppp ppp ppp ppp ppp ppp ppp p p p p p p p p p p p p p p p p
pp pp pp pp pp pp pp pp pp pp p p p p p p p p p p p p p p p p
pp pp pp pp pp pp pp pp pp pp p p p p p p p p p p p p p p p p
ppp ppp ppp ppp ppp ppp ppp ppp ppp ppp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp
pp pp pp pp pp pp pp pp pp pp p p p p p p p p p p p p p p p p
pp pp pp pp pp pp pp pp pp pp p p p p p p p p p p p p p p p p
ppp ppp ppp ppp ppp ppp ppp ppp ppp ppp p p p p p p p p p p p p p p p p
ppp ppp ppp ppp ppp ppp ppp ppp ppp ppp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp
pp pp pp pp pp pp pp pp pp pp p p p p p p p p p p p p p p p p
pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp p
ppp ppp ppp ppp ppp ppp ppp ppp ppp ppp ppp ppp ppp ppp ppp ppp ppp ppp ppp ppp ppp ppp ppp ppp ppp ppp ppp ppp ppp ppp ppp ppp ppp ppp ppp ppp ppp ppp ppp ppp p
pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp p
pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp pp p
ppppppppppppppppppppppppppppppppppppppppp
p p p p p p p p p p p p p p p p p p p p p
p p p p p p p p p p p p p p p p p p p p p
p p p p p p p p p p p p p p p p p p p p p
p p p p p p p p p p p p p p p p p p p p p
p p p p p p p p p p p p p p p p p p p p p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
cahiers d’informatique
2
The title of this series of technical reports constitute my
homage to the legendary magazine Cahiers du cinéma, and to
the role that it has played for many years in the creation
of a widespread critical sense regarding film.
(C) Simone Santini, 2009
grafos
3
I. HEAP Y COLAS DE PRIORIDAD.
1.1
Colas de prioridad
Ya hemos encontrado en los capı́tulos precedentes, un tipo de cola: la cola FIFO (first
in, first out). Una cola FIFO es una estructura datos en la cual se insertan elementos
que después se sacan en el mismo orden en que se han insertado. Formalmente, una cola
FIFO proporciona dos operaciones:
insert(S,x);
extract(S);
inserta el elemento x en la cola S;
extrae un elemento de la cola S y lo devuelve.
La figura 1.1 contiene un simple programa compuesto de una secuencia de operaciones en
una cola FIFO. La ejecución del programa produce la secuencia 4, 5, 6, 7. En una cola
FIFO, el único parámetro que determina cuando un elemento será extraido es su tiempo de
inserción, es decir, el orden de extración de los elementos de la cola FIFO depende sólo
del orden de inserción de los mismos elementos. En el ejemplo precedente, el 5 es el
secundo elemento que se saca porque se ha insertado en la cola después del 4 y antes del
6, 7 y 8.
Una pila, recordamos, funciona de forma diferente: en este caso los último elementos que
se han insertado son los primeros que se sacan. Si se ejecutara el programa precedente
con una pila, en vez de una cola FIFO, con la misma secuencia de inserciones y
extraciones (en este caso las inserciones se hacen por medio de la función push y las
extracciones por medio de la función pop ), el resultado será diferente. La figura 1.1
muestra el estado de la ejecución del programa en el caso de una pila. El resultado
será, en este caso, 4, 5, 8, 7, 6. A pesar de la diferencia de resultado y del estado de
cahiers d’informatique
4
insert(S, 3)
-
insert(S, 4)
-
print extract(S)
-
insert(S, 5)
-
print extract(S)
-
insert(S, 6)
-
insert(S, 7)
-
insert(S, 8)
print extract(S)
- 8
-
print extract(S)
-
print extract(S)
-
Figure 1.1:
3
-
3
-
4
-
4
-
5
-
6
5
-
7
6
5
-
7
6
5
-
8
7
6
-
8
7
-
8
-
4
5
Ejecución de un simple programa en una cola FIFO.
la estructura, aún en el caso de una pila se cumple la propiedad que el orden de
extración de los elementos depende sólo del orden de inserción.
Hay casos en que necesitamos un mayor control sobre la secuencia de extración de los
elementos. Un caso muy comune es el siguiente: cada vez que se inserta un elemento en
la cola se le asigna un número, llamado la prioridad del elemento; cada vez que se
ejecuta una extración queremo extraer el elemento con prioridad más alta entre los que
están en la cola.
Consideramos otra vez la secuencia de inserciones y extracciones de los ejemplos
precedentes, pero esta vez asociamos a cada elemento un entero que represente la
prioridad de ese elemento. Indiquemos con nm el elemento n insertado con prioridad m.
Una posible secuencia de inserciones y extraciones es representada en figura 1.1. En
este caso el programa produce 3, 4, 8, 7, 6. La diferencia entre esta estructura y las
de antes es que en este caso el momento en que se extrae un elemento de la estructura no
depende simplemente del momento en que se ha insertado, sino también por su prioridad.
Por ejemplo, el tercer elemento que se inserta (el 5) nunca se saca de la cola, debido a
su baja prioridad. El momento de la inserción también tiene su importancia. Por
ejemplo, el elemento 3, con prioridad 4, se saca inmediatamente de la cola, mientras que
el 6, que también tiene prioridad 4, se saca después de los elementos 7 y 8 que, si bien
se han insertado después del 6, tienen prioridad más alta.
Desde un punto de vista formal, una cola de prioridad es una estructura de datos que
proporciona las operaciones siguientes (es decir, es una estructura de datos que presenta
la siguiente interfaz):
insert(S,x); inserta el elemento x (con su prioridad) en la cola S;
max(S); devuelve el elemento con prioridad mas alta entre los que se encuentran en la
cola S;
extract(S); elimina el elemento de prioridad maxima de la cola S y lo devuelve como
resultado.
grafos
5
push(S, 3)
push(S, 4)
print pop(S)
push(S, 5)
print pop(S)
push(S, 6)
push(S, 7)
push(S, 8)
print pop(S)
print pop(S)
print pop(S)
Figure 1.2:
3
3
4
-
3
3
5
-
3
3
6
6
7
7
3
3
3
3
3
-
6
6
6
-
7
8
-
Ejecución de la misma secuencia de inserciones y extraciones en una pila.
Hay muchas aplicaciones en que se necesita una cola de prioridad. Consideremos, por
ejemplo, un sistema operativo multitarea. En cualquier momento hay varios procesos
esperando que la CPU pueda ejecutarlos. En general, el sistema operativo determina un
cuanto de tiempo (time slot) durante la cual la CPU ejecuta un proceso. Al final del
time slot, la CPU suspende la ejecusión del proceso, guarda su estado, y pasa a ejecutar,
para el time slot siguiente, otro proceso. Ası́, en secuencia, la CPU dedica tiempo a
todo proceso. Por otro lado, hay procesos más criticos que otros. Muchos procesos del
sistema operativo se ocupan de funciones criticas del sistema, y deben ejecutarse en
seguida cuando la CPU esté disponible.
Al fin de gestionar estas situaciones, cada proceso que se ejecuta tiene una prioridad, y
todos los procesos que están esperando la CPU se ponen en una cola de prioridad. Cada
vez que la CPU se encuentra disponible, el sistema operativo extrae el proceso a
prioridad más alta entre los en espera y la CPU ejecuta durante el cuanto de tiempo
siguiente. Nuevos procesos pueden ser insertados en la cola mediante la función insert.
Otra aplicación tı́pica se encuentra en sistemas de simulación basados en eventos. Los
elementos en la cola son los eventos que se tienen que simular, y la prioridad es el
instante de tiempo en que ocurren. Los eventos deben ser simulados en el orden en que
llegan y por esto se puede usar una cola de prioridad. Notamos que en este caso, ya que
los evento se simulan desde el más antiguo hasta el más reciente, la prioridad más alta
corresponde al tiempo mı́nimo, o sea, la prioridad de un evento que llega en el instante t
es −t.
Una forma sencilla de implementar una cola de prioridad es alumbrada el la figura 1.1:
se pueden guardar los elementos ordenados, desde la prioridad más ata hasta la más baja y
cada vez devolver el primer elemento de la cola. Si se implementa la cola como una lista
enlazada, le extración del maximo se hace de forma inmediada, pero la inserción supone un
coste medio O(n) para mantener la lsta ordenada. El coste medio de las operaciones de la
interfaz es el siguiente:
insert(S,x);
max(S);
O(n)
Θ(1)
cahiers d’informatique
6
insert(S, 34 )
34
insert(S, 42 )
34 42
print extract(S)
42
insert(S, 51 )
42 51
print extract(S)
51
insert(S, 64 )
64 51
insert(S, 75 )
75 64 51
insert(S, 86 )
86 75 64 51
print extract(S)
75 64 51
print extract(S)
64 51
print extract(S)
51
Figure 1.3:
max extract(S);
Ejecución de un simple programa en una cola de prioridad
Θ(1)
El problema de esta solución es el alto coste de la función insert. Por esto, las colas
de prioridad se implementan tı́picamente usando un heap, que es la estructura de datos de
que trataremos en seguida. El uso de heap proporciona una implementación eficiente de
las operaciones de cola de prioridad, con las siguiente eficiencias asimptóticas:
insert(S,x);
max(S);
max extract(S);
1.2
O(log n)
Θ(1)
O(log n)
Heap
Un heap es un árbol binario que cumple las dos propriedades siguientes:
i) cada nivel del árbol, con la posible excepción del nivel más bajo, es completo; el
nivel más bajo se completa ordenadamente desde la izquierda (propiedad de
complitud) ;
ii) cada nodo que no sea la raı́z contiene un valor no superior a él de su antecesor
(propiedad heap).
grafos
7
Según estas propiedades, el siguiente árbol es un heap :
@ABC
GFED
21 PP
PPP
ppp
p
PP
p
p
p
@ABC
GFED
@ABC
GFED
20
15
>>
} AAA
ppp
p
}
>
p
}
pp
@ABC
GFED
@ABC
GFED
@ABC
GFED
89:;
?>=<
13
11
10 >
9
>>
?>=<
89:;
?>=<
89:;
?>=<
4
6 89:;
1
(1.1)
mientras los dos siguientes no son heap : el primero no cumple la propiedad heap (el
vértice 14 y el 7 contienen un valor mayor que 4, el valor de su antecesor), y en el
segundo el nivel más bajo no se ha completado de izquierda a derecha (hay un hueco entre
el 4 y el 1):
@ABC
GFED
q 21 PPPP
q
q
PPP
qqq
?>=<
89:;
@ABC
GFED
6
20 A
oo :::
AA
}
o
o
}
o
}
o
o
@ABC
GFED
?>=<
89:;
@ABC
GFED
@ABC
GFED
15 A
9
13
11
AA
?>=<
89:;
@ABC
GFED
89:;
4
10 ?>=<
1
@ABC
GFED
21 PP
PPP
pp
p
p
PP
p
p
p
@ABC
GFED
@ABC
GFED
15
20 A
>>
pp
AA
}
p
p
}
>
p
}
p
p
@ABC
GFED
89:;
?>=<
@ABC
GFED
GFED
@ABC
10 >
9
13
11
}
>>
}
}}
89:;
?>=<
89:;
?>=<
89:;
?>=<
4
6
1
(1.2)
La propiedad de complitud nos permite representar el árbol muy fácilmente en una cadena
(array). Dado un array A y un heap 1 , se almacena el heap en el array como sigue:
i) la raı́z del árbol se pone en A[1];
ii) los hijos izquierdo y derecho del elemento almacenado en A[i] se almacenan en A[2i] y
A[2i + 1] respectivamente.
Con este macanismo, el antecesor, y los hijos de derecha y de izquierda del vértice
almacenado en la posición i se calculan como
parent(i) = bi/2c;
left(i) = 2i;
right(i) = 2i + 1;
Recordamos que en cualquier ordenador moderno, el calculo de 2i y 2i + 1 no requiere
multiplicaciones, sino se puede obtener con un shift y un or. Se trata, por eso, de
operaciones muy veloces.
La representación del heap (1.1) como array es la seguiente:
16
14
10
8
7
9
3
2
4
1
La propiedad heap se traduce en el requisito que para cada i tal que A[i] no es la raı́z,
se cumpla A[parent(i)] ≥ A[i], es decir:
∀i > 1
A[bi/2c] ≥ A[i]
(1.3)
1 La propiedad heap no es esencial para esta representación: cualquier árbol que cumpla la propiedad de
cumplitud se puede representar en esta forma. Sin embargo, aquı́ la representación solo se usará para heap.
cahiers d’informatique
8
Como veremos, la propiedad heap s la clave para una implementación eficiente de las
funciones de cola de prioridad y del algoritmo heapsort. Las operaciones insert y
extract funcionan insertando (o, respectivamente, removiendo) un elemento de un heap de
una forma que "destruye" la propiedad heap y luego llamando la función heapify para
"reconstruir" la propiedad. La función heapify es llamada con un array A y un ı́ndice i.
Se asume que los sub-árboles left(i) y right(i) cumplen la propiedad heap, pero que A[i]
puede ser menor que sus hijos, ası́ que el árbol con raı́z en i puede no cumplir la
propiedad. Un tal caso se da en el árbol de izquierda de (1.2). Si se considera como
nodo i el vértice con valor 4, es fácil darse cuenta que los dos árboles que cuelgan de
eso (es decir, los árboles con raı́z en los vértices con valor 14 y 7), si considerados
individualmente, cumplen la propiedad heap, pero que el árbol con raı́z en 4 no la cumple.
En este caso, el vértice con valor 4 se encuentra el la segunda posición del array y este
heap se puede corregir con la llamada heapify(A, 2). El algoritmo heapify es el
siguiente:
heapify(A, i)
1
l ← left(i);
2
r ← right(i);
3
if l ≤ heap size(A) and A[l] > A[i] then
4
largest ← l;
5
else
6
largest ← i;
7
fi
8
if r ≤ heap size(A) and A[r] > A[largest] then
9
largest ← r;
10
fi
11
if largest 6= i then
12
exchange A[i], A[largest]
13
heapify(A, largest);
14
fi
Las lı́neas 3-10 determinan el más grande de los tres elementos A[i], A[left(i)] y
A[right(i)], poniendo el indice correspondiente en la variable largest. Si A[i] es el más
grande, entonces el sub-árbol con raı́z i ya es un heap, y el algoritmo termina. En caso
contrario, a la lı́nea 12, A[i] se pone en lugar del más grande de los elementos A[left(i)]
y A[right(i)]. A este punto, el vértice i es más grande que sus hijos y cumple la
propiedad heap. Por otro lado, el vértice en el que se ha puesto el valor A[i] ha
decrementado su valor, y puede no cumplir la propiedad heap. Para obviar al problema, en
la lı́nea 13 se procede a llamar heapify recursivamente en este sub-árbol.
Consideremos el árbol a la izquierda de (1.2). El vértice i contiene 4, y el valor más
grande entre el de i el los de sus hijos es 14. Entonces, se procede a intercambiar el 4
con el 14, obteniendo el árbol:
@ABC
GFED
21 PP
PPP
ppp
p
PP
p
p
p
@ABC
GFED
@ABC
GFED
15
20
>>
} AAA
ooo
o
}
>
o
}
oo
?>=<
89:;
89:;
?>=<
@ABC
GFED
@ABC
GFED
6>
9
13
11
>
>
?>=<
89:;
@ABC
GFED
89:;
4
10 ?>=<
1
(1.4)
grafos
9
El vértice i y sus hijos ahora cumplen la propiedad heap (el antecesor contiene 14, el
valor más grande), pero el árbol en que se ha cambiado el 14 por un valor más peque~
no no
la cumple. Por esto, se llama heapify con el nodo en que se ha puesto el 4. Ahora el
valor más grande entre 4 y sus hijos es el 8. El 4 se intercambia con el 8, obteniendo
el árbol siguiente, que es un heap :
@ABC
GFED
21 PP
PPP
pp
p
p
PP
p
p
p
@ABC
GFED
@ABC
GFED
20
15
>>
} AAA
ppp
p
}
>
p
}
p
p
@ABC
GFED
@ABC
GFED
@ABC
GFED
89:;
?>=<
13
11
10 >
9
>>
?>=<
89:;
?>=<
89:;
?>=<
4
6 89:;
1
(1.5)
Al fin de determinar el tiempo de ejecución de heapify, notamos que todas las operaciones
son Θ(1), con la excepción de la llamada recursiva en la lı́nea 13. ¿Cuantas veces se
llama recursivamente la función? Si se está analizando un nodo v, la función de llama a
la lı́nea 13 usando como parametro uno de los hijos de v. Es decir, por cada llamada
recursiva bajamos de un nivel en el árbol. Si la altura del árbol es h, la función se
llama, en el caso peor, h veces, resultando en un tiempo de ejecución O(h). La propiedad
de cumplitud nos asegura que el árbol no es degenerado, y que h = dlog2 ne, proporcionando
un tiempo de ejecución O(log n).
Para demostrar la corrección del algoritmo, sea heap(A, i) el predicado que sostiene que el
sub-árbol de A con raı́z en i cumple la propiedad heap.
Theorem 1.2.1.
heap(A, left(i)) ∧ heap(A, right(i)) ∧ A → heapify(A, i)
⇒
heap(A, i)
(1.6)
Demostración. Si los sub-árboles de un árbol cumplen la propiedad heap, y el valor de la
raı́z es mayor que el valor de las raı́ces de los sub-árboles, entonces el árbol completo
cumple la propiedad heap. Es decir, en fórmulas,
heap(A, left(i)) ∧ heap(A, right(i)) ∧ A[i] > A[left(i)] ∧ A[i] > A[right(i)]
Sea h(A, i) la altura del sub-árbol de A con raı́z en el vértice i.
inducción en h(A, i).
⇒
heap(A, i) (1.7)
La demostración es por
Si h(A, i) = 0, la propiedad es evidente. Supongamos que el teorema sea valido para
h(A, i) = k, y consideramos un vértice j tal que h(A, j) = k + 1.
i) si A[j] > A[left(j)], A[right(j)], entonces h(A, j) y, ya que el el algoritmo no cambia
nada, h(A, j) sigue valido después de su ejecución;
ii) supongamos ahora que A[left(j)] > A[j], A[right(j)]; después de heapify, se ha
heap(B, right(j)), porque este sub-árbol no cambia; después del paso 12, se ha
A[j] > A[left(j)], A[right(j)]. El subárbol A[left(j)] tiene h(A, left(j)) = k y heapify
funciona en este árbol por la hipótesis de inducción, ası́ que se da heap(A, left(j))
y, consecuentemente, heap(A, j) por la (1.7);
iii) el caso A[right(j)] > A[j], A[left(j)] es claramente simétrico al precedente.
cahiers d’informatique
10
1.2.1
Construcción de un heap
La función heapify se puede usar de una forma bottom-up (o sea, empezando por las hojas
de una árbol y subiendo hasta la raı́z) para convertir un array A[1, . . . , n] en un heap.
Como en la sección precedente, consideramos A como la representación de un árbol binario
que cumple la propiedad de complitud. Los elementos A[bn/2c, . . . , n] son las hojas del
árbol y por eso ya son heap de tama~
no 1. A partir del elemento A[bn/2c] hasta la raı́z, la
procedura siguiente recorre los nodos del árbol llamando para cada uno heapify. Después
de la llamada heapify (A, i) el árbol con raı́z en A[i] es un heap.
build heap(A)
1
heap size(A) ← length(A);
2
for i ← blength(a)/2c downto 1 do
3
heapify(A, i);
4
od
Es posible derivar una simple estima del tiempo de ejecución de build heap considerando
que la función heapify tiene un tiempo de ejecución O(log n) y que en build heap se llama
n/2 veces. Luego, el tiempo de ejecución es a lo peor O(n log n). Este lı́mite superior
es correcto, pero no es una buena estima, en el sentido que, en realdad, la función
build heap se ejecuta en un tiempo menor. Es decir, la estima no es asintóticamente
precisa.
Una estima mejor se puede conseguir con las consideraciones siguientes. Observamos que
el tiempo de ejecución de heapify varia según la altura del nodo con que es llamado: el
tiempo de ejecución es O(h), donde h es la altura del nodo. Se puede demostrar que en un
heap hay, en el caso peor, dn/sh+1 e nodos de altura h ası́ que el tiempo de ejecución de
build heap se puede expresar como


blog nc
blog nc
X
X
h
n
(1.8)
d h+1 eO(h) = O n
2
2h
h=0
h=0
Para determinar el valor de esta suma, consideremos la igualdad
∞
X
k=0
kxk =
x
(1 − x)2
(1.9)
que, para x = 1/2 nos da
∞
X
h
1/2
=
=2
h
2
(1 − 1/2)2
(1.10)
k=0
Luego, el tiempo de ejecución de la procedura de construcción es:


!
blog nc
∞
X h
X
h


O n
=O n
= O(n)
2h
2h
h=0
(1.11)
h=0
O sea, es posible construir un heap partendo de un array desordenado en un tiempo lineal.
La figura 1.2.1 muestra los varios pasos de la construcción de un heap.
grafos
11
89:;
?>=<
1 NN
NNN
nnn
n
n
NN&
nw nn
89:;
?>=<
89:;
?>=<
2
3=
p @@@
p
==
p
p
@
p
px p
@ABC
GFED
89:;
?>=<
89:;
?>=<
89:;
?>=<
10
6
7
4=
~~
==
~
~
~
z ~
89:;
?>=<
89:;
?>=<
?>=<
8
9 89:;
5
?>=<
89:;
oo 1 NNNNN
o
o
o
NN&
ow oo
?>=<
89:;
89:;
?>=<
2
3=
oo @@@
o
==
o
o
@
o
ow o
?>=<
89:;
?>=<
89:;
89:;
?>=<
?>=<
89:;
5
6
7
4=
~
==
~~~~
?>=<
89:;
?>=<
89:;
GFED
8
9 @ABC
10
1 2 3 4 10 6 7 8 9 5
heapify(5)
cambios:
1 2 3 4 5 6 7 8 9 10
?>=<
89:;
1 NN
NNN
nnn
n
n
NN&
nw nn
?>=<
89:;
89:;
?>=<
2
3=
p @@@
p
==
p
p
@
p
px p
?>=<
89:;
@ABC
GFED
?>=<
89:;
89:;
?>=<
9 ==
10
6
7
~~
====
~
~
~
|
"
z ~
?>=<
89:;
?>=<
89:;
89:;
8
4 ?>=<
5
1 2 3 9 10 6 7 8 4 5
heapify(4)
cambios:
89:;
?>=<
1 NN
NNN
nnn
n
n
NN&
nw nn
89:;
?>=<
89:;
?>=<
2
7 ==
p @@@
p
====
p
p
@
p
| "
px p
89:;
?>=<
@ABC
GFED
89:;
?>=<
89:;
?>=<
9 ==
10
6
3
~~
====
~
~
~
|
"
z ~
89:;
?>=<
89:;
?>=<
?>=<
8
4 89:;
5
1
?>=<
89:;
1 NN
NNN
ooo
o
o
NN&
o
o
w
89:;
?>=<
@ABC
GFED
7
10
@@@@@
======
ooo
o
@
o
| "
$
s{ oo
?>=<
89:;
?>=<
89:;
?>=<
89:;
89:;
?>=<
9 ==
5
6
3
~~
~
====
~
~
|
"
z ~
?>=<
89:;
?>=<
89:;
89:;
8
4 ?>=<
2
1 10 7 9 5 6 3 8 4 2
heapify(2)
cambios:
1
1 2 7 9 10 6 3 8 4 5
heapify(3)
cambios:
1
@ABC
GFED
10 OO
OOO
ooo
o
o
OO #+
oo
{
s
89:;
?>=<
89:;
?>=<
9
7
p ====
======
ppp
=="
p
p
| "
t| p
89:;
?>=<
89:;
?>=<
89:;
?>=<
89:;
?>=<
8 ==
5
6
3
====
|
"
|
89:;
?>=<
89:;
?>=<
?>=<
1
4 89:;
2
2
10 9 7 8 5 6 3 1 4 2
heapify(1)
cambios:
Figure 1.4: Los pasos necesarios para la construcción de un heap. Para cada paso,
de izquierda a derecha se muestran la llamada a heapify, el árbol en la apresentación
estandar y representado como array, y el número de operaciones de intercambio hechas
por heapify. Las aristas en lı́nea doble marcan las partes del árbol que ya cumplen la
propiedad heap.
3
cahiers d’informatique
12
1.3
Ordenación con heap :
heapsort
El algoritmo heapsort utiliza una propiedad importante de un heap : el elemento más
grande se encuentra siempre en la raı́z del heap, o sea en el primer elemento del array
donde se almacena el heap. (Se vea el problema 8.) El algoritmo ese el siguiente:
heapsort(A)
1.
Build heap(A);
2.
for i ← length[A] downto 2 do
3.
swap(A[1], A[i]);
4.
heapsize[A] ← heapsize[A] - 1;
5.
Heapify(A, 1);
6.
od;
El algoritmo empieza con la construcción del heap. El elemento máximo se encuentra en la
primera posición del array (la raı́z del heap ), y se pone en su posición final (al último
elemento del array) con el cambio a la lı́nea 3. El nodo se "quita" del heap
decrementando su tama~
no (lı́nea 4), ası́ que as operaciones siguienten no afecten el nodo
que hemos puesto en su lugar definitivo. A este punto los dos sub-árboles de la raı́z
cumplen la propiedad heap (ya que no los hemos alterados), pero la raı́z puede ser menor
que uno de sus hijos. La situación se arregla con la llamada a heapify en la lı́nea 5.
Esto nos deja un heap en el array A[1, . . . , n − 1]. El algoritmo repite el procedimiento en
este array, y en todos los array de tama~
no decrecente hasta el tama~
no 2.
La figura 1.3 muestra el funcionamiento del algoritmo después que se ha construido el
heap. En cada paso se muestra la situación después de la lı́nea 4 y después de la llamada
a heapify. Las lı́neas doubles marcan los cambios que se hacen en cada llamada a heapify.
El heap inicial es el final de la figura 1.2.1.
La llamada a Build heap, como hemos visto, supone un tiempo O(n); cada llamada a Heapify
toma un tiempo O(log n), y se llama n − 1 veces. Luego, el tiempo de ejecución de Heapsort
es O(n log n).
1.4
Uso de heap como colas de prioridad
El algoritmo heapsort tiene una eficiencia asimptotica optimal pero, en práctica, hay
algoritmos más eficientes como, por ejemplo, el algoritmo quicksort que, si bien tienen
la misma eficiencia asimptotica, son más rapidos. Consecuentemente, los heap se usan
raramente para la ordenacción. Por otro lado, los heap proporcionan una forma muy eficaz
y sencilla de implementar colas de prioridad, o sea, de implementar las operaciones
insert, extract, y max que hemos introducido en la sección 1.
La función max es implementada en una manera muy sencilla: dado un heap implementado
como array, la función max sólo tiene que devolver el primer elemento del array : gracias
a la propiedad heap, este elemento es el máximo (se vea el problema 8).
La función extract también devuelve el primer elemento del array pero, como el elemento
tiene que eliminarse del heap, será necesario reparar el heap que se ha da~
nado con su
eliminación.
El algoritmo es el siguiente:
extract(A)
grafos
13
Tras la lı́nea 4
?>=<
89:;
2
pp NNNNN
p
p
NN&
p
p
px
?>=<
89:;
89:;
?>=<
9=
7
p
p
==
===
p
p
p
p
px
?>=<
89:;
?>=<
89:;
?>=<
89:;
89:;
?>=<
8=
5
6
3
==
?>=<
89:;
?>=<
89:;
()*+
1
4 /.-,
2 9 7 8 5 6 3 1 4 10
?>=<
89:;
p 2 NNNN
p
p
NNN
p
px pp
&
?>=<
89:;
89:;
?>=<
8
7
=
r
==
===
rr
r
r
ry
?>=<
89:;
?>=<
89:;
89:;
?>=<
?>=<
89:;
5
6
3
4
?>=<
89:;
()*+
/.-,
1
final del heap
?
2 8 7 4 5 6 3 1 9 10
()*+
/.-,
?>=<
89:;
1
pp NNNNN
p
p
NN&
p
p
px
?>=<
89:;
?>=<
89:;
5
7
=
==
===
vv
v
v
z
v
?>=<
89:;
?>=<
89:;
89:;
?>=<
89:;
?>=<
4
2
6
3
final del heap
?
1 5 7 4 2 6 3 8 9 10
?>=<
89:;
3
pp NNNNN
p
p
NN&
p
p
px
?>=<
89:;
?>=<
89:;
5
6
=
==
vv
v
zvv
?>=<
89:;
?>=<
89:;
?>=<
89:;
()*+
/.-,
4
2
1
final del heap
?
3 5 6 4 2 1 7 8 9 10
Tras Heapify
89:;
?>=<
9 NN
NNN
ppp
p
p
NN&
p
p
|
t
89:;
?>=<
89:;
?>=<
8
7
p
=
pp
==
===
p
p
p
p
t|
89:;
?>=<
89:;
?>=<
89:;
?>=<
89:;
?>=<
4 ==
5
6
3
====
"
89:;
?>=<
89:;
?>=<
1
2
final del heap
?
9 8 7 4 5 6 3 1 2 10
89:;
?>=<
pp 8 NNNN
p
p
NNN
p
&
t| pp
89:;
?>=<
89:;
?>=<
5
7
=
=
v
=
===
===
vv
v
"
zv
89:;
?>=<
89:;
?>=<
89:;
?>=<
89:;
?>=<
4
2
6
3
89:;
?>=<
1
final del heap
?
8 5 7 4 2 6 3 1 9 10
89:;
?>=<
7 NNN
ppp
NNNN
p
p
px p
"*
89:;
?>=<
89:;
?>=<
6=
v 5 ==
==
v
v
=
| zvv
89:;
?>=<
89:;
?>=<
89:;
?>=<
89:;
?>=<
4
2
1
3
final del heap
?
7 5 6 4 2 1 3 8 9 10
89:;
?>=<
6
pp NNNNN
p
p
NN "*
p
p
px
89:;
?>=<
89:;
?>=<
5
3
vv ===
v
zvv
89:;
?>=<
89:;
?>=<
89:;
?>=<
4
2
1
final del heap
?
6 5 3 4 2 1 7 8 9 10
cahiers d’informatique
14
Tras la lı́nea 4
Tras Heapify
?>=<
89:;
1
pp LLLL
p
p
LL
p
%
px p
?>=<
89:;
?>=<
89:;
3
5
=
v
==
vv
v
zv
?>=<
89:;
?>=<
89:;
()*+
/.-,
4
2
89:;
?>=<
5
pp HHHH
p
p
p
H$
px p
89:;
?>=<
89:;
?>=<
3
4
vv ===
v
v
vz
89:;
?>=<
89:;
?>=<
1
2
final del heap
?
1 5 3 4 2 6 7 8 9 10
final del heap
?
5 4 3 1 2 6 7 8 9 10
?>=<
89:;
2
rr HHHH
r
r
H$
ry r
?>=<
89:;
?>=<
89:;
3
4
v
v
v
v
zv
?>=<
89:;
()*+
/.-,
1
89:;
?>=<
4
vvvv HHHH
v
v
v
v
H$
v~ vv
89:;
?>=<
89:;
?>=<
2
3
v
v
v
v
zv
89:;
?>=<
1
final del heap
?
2 4 3 1 5 6 7 8 9 10
/.-,
()*+z
?>=<
89:;
1 HH
HH
vv
v
H$
v
zv
?>=<
89:;
?>=<
89:;
2
3
final del heap
?
1 2 3 4 5 6 7 8 9 10
?>=<
89:;
1
vv
v
v
z
v
?>=<
89:;
2
#
/.-,
()*+
final del heap
?
1 2 3 4 5 6 7 8 9 10
()*+z
/.-,
?>=<
89:;
1
final del heap
?
1 2 3 4 5 6 7 8 9 10
final del heap
?
4 2 3 1 5 6 7 8 9 10
89:;
?>=<
3 H
vv HHHHHHHHH
v
zvv
(
89:;
?>=<
89:;
?>=<
2
1
final del heap
?
3 2 1 4 5 6 7 8 9 10
89:;
?>=<
2
vvvv
v
v
v
vvv
~
v
89:;
?>=<
1
final del heap
?
2 1 3 4 5 6 7 8 9 10
89:;
?>=<
1
final del heap
?
1 2 3 4 5 6 7 8 9 10
Figure 1.5: Ejecución del algoritmo heapsort. En cada paso se muestra la situación
después de la lı́nea 4 y después de la llamada a heapify. Las lı́neas doubles marcan los
cambios que se hacen en cada llamada a heapify.
grafos
1
2
3
4
5
6
15
if heap size(A) < 1 then abort "heap underflow"; fi
max ← A[1];
A[1] ← A[heap size(A)];
heap size(A) ← heap size(A) - 1;
heapify(A, 1);
return max;
La lı́nea 2 copia el valor de la raı́z del heap (el máximo, por la propiedad heap ) para
devolverlo. En las lı́neas 3 y 4 se pone el último elemento del heap en la raı́z y se
reduce el tama~
no del heap. Esta operación deja un árbol que no cumple la propiedad heap :
los sub-árboles que cuelgan de la raı́z son heap, pero la raı́z puede tener un valor menor
que sus hijos. La llamada a heapify en la lı́nea 5 repara el heap.
Todas las operaciones de este algoritmo se ejecutan en un tiempo O(1), con la excepción
de heapify, cuyo tiempo de ejecución domina él del algoritmo, por lo cual
T (extract) = O(log n), donde n es el numero de elementos en la cola.
La inserción se hace de una manera que garantiza el mantenimiento de la propiedad heap.
La idea es crear una nueva hoja vacı́a al final del árbol y bajar elementos de un nivel al
siguiente, empezando con el antepasado de la hoja vacı́a, hasta encontrar un elemento más
grande que el elemento que se va a insertar. El nuevo elemento se puede insertar como
hijo de este, garantizando que la estructura resultante es un heap. En resumen:
i) se incrementa el tama~
no del heap ;
ii) empezando por el último elemento, y saltando desde un elemento a su antecesor, se
bajan elementos, causando un "hueco" que sube hasta la cima del árbol;
iii) cuando se encuentre un valor más grande que el valor a insertar, se introduce éste
último en el hueco abierto.
Supongamos que se inserte el valor 16 en el heap (1.1). El algoritmo empieza creando un
"hueco" en la ultima posición del array. En este caso, el array contiene 10 elementos, y
el nuevo elemento que se crea será el numero 11, correspondiente al hijo derecho del
elemento 9, o sea, se creará el árbol siguiente:
@ABC
GFED
21 PP
PPP
pp
p
p
PP
pp
p
@ABC
GFED
@ABC
GFED
20
15 >
p
} AAA
>>
pp
p
}
p
}
pp
@ABC
GFED
89:;
?>=<
@ABC
GFED
@ABC
GFED
10 ;
9;
13
11
;;
;;;
;
?>=<
89:;
?>=<
89:;
?>=<
89:;
?>=<
4
6 89:;
1
(1.12)
El nuevo elemento (16) es más grande que el antecesor del hueco (9), entonces se mueve el
antecesor en el hueco, de hecho "subiendo" el hueco hacia la raı́z:
@ABC
GFED
r 21 PPPP
r
r
PPP
rrr
@ABC
GFED
@ABC
GFED
20 >
q 15 88
>>
q
q
q
>
8
q
qq
@ABC
GFED
@ABC
GFED
@ABC
GFED
89:;
?>=<
13
11
10 ;
88
;
8
;
8
;
?>=<
89:;
89:;
?>=<
?>=<
89:;
?>=<
4
6 89:;
1
9
(1.13)
cahiers d’informatique
16
Todavı́a el nuevo elemento (16) es más grande que el antecesor del hueco (15), ası́ que el
procedimiento tiene que repitirse:
@ABC
GFED
p 21 OOOO
p
p
OOO
p
O
ppp
@ABC
GFED
?>=<
89:;p
20 >
;
r
r
;
>>
r
;
r
;
>
r
rr
@ABC
GFED
@ABC
GFED
@ABC
GFED
@ABC
GFED
15 A
13
11
10 >
AAA
>>
?>=<
89:;
?>=<
89:;
89:;
89:;
?>=<
4
6 ?>=<
1
9
(1.14)
Esta vez el antecesor del hueco (21) es más grande que el nuevo elemento, y el
procedimiento termina insertando el nuevo elemento en el hueco:
@ABC
GFED
21 PP
PPP
nn
n
n
PP
n
n
n
@ABC
GFED
@ABC
GFED
16
20 A
AA
pp
AA
}
p
p
A
}
p
}
p
p
@ABC
GFED
@ABC
GFED
@ABC
GFED
@ABC
GFED
10 >
15 A
13
11
AAA
}
>>
}
}}
?>=<
89:;
?>=<
89:;
89:;
89:;
?>=<
4
6 ?>=<
1
9
(1.15)
El algoritmo es el siguiente:
insert(A, x)
if heap size(A) > max array size then abort "heap overflow" fi
heap size(A) ← heap size(A) + 1;
i ← heap size(A);
while i > 1 and A[parent(i)] < x do
A[i] ← A[parent(i)];
i ← parent(i);
od;
A[i] ← x;
El tiempo de ejecución del algoritmo es determinado por el numero de veces que se ejecuta
el ciclo "while". Éste se ejecuta, en el caso peor, un número de veces igual al numero
de niveles del árbol, o sea T (insert) = O(log n).
grafos
1.5
17
Ejercicios
1. ¿Cuales de estos array representan heap ?
array.
Dibujar el árbol correspondiente a cada
a) [9, 7, 5, 6, 4, 3]
b) [15, 8, 10, 9, 7, 4, 1]
c) [5, 5, 5, 5, 5]
d) [7, 2, 6, 1, 2, 3, 4]
e) [13, 8, 5, 7, 7, 7]
2. ¿Cuales de estos árboles son heap ?
array.
Para cada heap se escriba la representación como
?>=<
89:;
5
===
?>=<
89:;
?>=<
89:;
3
2
(a)
?>=<
89:;
5
===
?>=<
89:;
?>=<
89:;
5
5
(d)
89:;
?>=<
5=
==
89:;
?>=<
2
(b)
89:;
?>=<
9
pp NNNNN
p
p
NN&
p
p
px
89:;
?>=<
89:;
?>=<
7
9=
==
89:;
?>=<
89:;
?>=<
89:;
?>=<
8
9
7
(e)
89:;
?>=<
5
(c)
89:;
?>=<
9
pp
p
p
p
p
px
89:;
?>=<
9=
==
89:;
?>=<
89:;
?>=<
8
7
(f)
3. Dado un array A[1, . . . , n] que representa un heap, ¿en que posición se puede encontrar
el elemento más peque~
no?
4. ¿Cual es el mı́nimo y el maximo número de elementos que puede tener un heap de altura
h?
5. ¿Un sub-árbol de un heap es un heap ?
6. Mostrar que el tiempo de ejecución de Heapify es Ω(log n), o sea que existe un heap
tal que heapify necesita un tiempo log n para ejecutarse. (Sugerencia: se construya
un heap con n nodos y valores tales que heapify se llama recursivamente para cada
nodo desde la raı́z hasta una hoja.)
7. Dado un heap en que ningún elemento se repite, demonstrar que el elemento más grande
se encuentra en la raı́z del árbol.
8. Dado un heap cualquiera, demonstrar que uno de los elementos más grandes se
encuentra en la raı́z del árbol.
9. Dado un heap de 127 elementos contenido en un array A, ¿cual es el efecto de la
llamada heapify(A, 65)?
10. Se construya un heap insertando los elementos 1, 2, 3, 4, 5, 6, 7, 8 en este orden.
Se muestren los heap intermedios que se construyen.
18
cahiers d’informatique
11. ¿Como se puede implementar una cola FIFO usando una cola de prioridad?
¿Y una pila?
12. En la representación de un árbol en un array, el hijo izquierdo del nodo almacenado
en la posición i se almacena en la posición 2i, y el hijo derecho en la posición
2i + 1. ¿Que pasarı́a si se usara la convención opuesta, o sea, si se almacenara el
hijo izquierdo el la posición 2i + 1 y el hijo derecho en la posición 2i?
13. Con la convención del ejercicio precedente, ¿cual serı́a el array correspondiente a
los heap del ejercicio 2? ¿Cual serı́a la representación del heap (1.1)?
grafos
19
II. INTRODUCCIÓN A LOS GRAFOS
2.6
Definiciones
El grafo es una de las estructuras de datos más importantes y de uso más común en
informática. Esta popularidad es debida principalmente a la gran expresividad y
flexibilidad de los grafos como modelo computational de una gran variedad de problemas y
situacciones prácticas. Cada vez que nos enfrentamos a un problema cuyos elementos se
relacionan de forma binaria (es decir, se organizan en pares), podemos utilizar un grafo
como modelo. En el lenguaje de los grafos, los elementos se llamarán vértices o nodos, y
las conexiones entre ellos aristas o arcos. Hemos dicho que las relaciones son binarias,
es decir, cada arista del grafo conecta exactamente dos vértices. La relación, en
general, no será simetrica: el hecho de que u sea relacionado con v o sea, en el
lenguaje de los grafo, de que haya una arista de u a v, no implica que v sea relacionado
con u, o sea, no implica que haya una arista de v a u. Los siguientes son unos ejemplos
de problemas que se pueden modelar con grafos:
i) La red de una compa~
nı́a aerea. En este caso el modelo será un grafo cuyos vértices
representarán a los aeropuertos servidos por la compa~
nı́a, y habrá una arista del
vértice u al vértice v si hay un vuelo directo desde el aeropuerto representado por
u al aeropuerto representado por v. Una red de tren o de metro se puede modelar de
manera analoga.
ii) Las prioridades de una carrera universitaria. En este caso los vértices del grafo
representan asignaturas, y hay una arista del vértice u al vértice v si es necesario
aprobar la asignature u antes de poder cursar v.
iii) Relaciones de parentesco. Los vértices del grafo representan personas, y hay una
arista de u a v si u es el padre o la madre de v.
iv) Relaciones de nacionalidad. Los vértices del grafo representan personas, y hay una
arista de u a v si v tiene la misma nacionalidad que u.
cahiers d’informatique
20
v) Red bibliográfica. Los vértices del grafo representan libros, y hay una arista de u a
v si el libro u cita el libro v.
vi) El web. Los vértices representan páginas web, y hay una arista de u a v si la página
u contiene un enlace a la página v.
Como siempre, la vida no nos regala nada, y esta flexibilidad en modelación se paga en
términos de algoritmos: los algoritmos que operan en grafos son más complejos que los
que operan en las estructuras que hemos visto hasta ahora, y su ejecución es
asintóticamente más lenta. Sin embargo, a pesar de su complejidad, los algoritmos para
grafos tienen una gran importancia en muchos problemas de interés teórico y práctico, y
se han publicado literalmente millares de artı́culos y estudios sobre la teorı́a de los
grafos y sobre algoritmos que los utilizan como estructura de datos. Un estudio, por
superficial que sea, de esta multitud de algoritmos serı́a claramente imposible en las
circunstancias presentes. En este capı́tulo y en los siguientes nos limitaremos a
estudiar las propiedades de los grafos de mayor interés en informática, y a analizar los
algoritmos clásicos más importantes.
Empecemos con la definición general de grafo:
Definition 2.6.1. Un grafo G es un par G = (V, E), donde:
i) V es un conjunto de elementos llamados vértices o nodos del grafo;
ii) E ⊆ V × V es una relación binaria en V , es decir, un conjunto de pares de vértices
llamados arcos o aristas del grafo.
Un grafo se dice finito si V es finito (es fácil darse cuenta en este caso que E también
es finito). Los grafos infinitos tienen un cierto interés en matemáticas pero, por
razones bastante obvias, no se usan en informática, por tanto asumiremos siempre que V
sea finito. Dado un grafo G se usará la notación V [G] para indicar el conjunto de sus
vértices y E[G] para el conjunto de sus arcos.
Definition 2.6.2. Un grafo G0 es un sub-grafo de G si V [G0 ] ⊆ V [G] y E[G] ⊆ E[G].
Definition 2.6.3. Dado un grafo G y el conjunto V 0 ⊆ V [G], el sub-grafo inducido por V 0
es el grafo G0 = (V 0 , E 0 ) donde
E 0 = {(u, v) ∈ E : u ∈ V 0 , v ∈ V 0 }
(2.16)
Los grafos se representan en ordenadores utilizando los métodos que veremos en la próxima
sección. Cuando, por contras, utilizamos papel y lápiz, los grafos se representan a
menudo utilizando diagramas. Cada vértice vi se representa mediante un cı́rculo, y se
pone una flecha del cı́rculo vi al cı́rculo vj si (vi , vj ) ∈ E[G]. Por ejemplo, el grafo
G = ({1, 2, 3, 4}, {(1, 2), (1, 3), (2, 4), (3, 4), (4, 1)})
se representará como
89:;
?>=<
4
rr8 A
r
r
r
r
r ?>=<
89:;
89:;
/ ?>=<
1 MM
2 MMM
MM& 89:;
?>=<
3
(2.17)
(2.18)
grafos
21
El grafo siguiente es un sub-grafo de (2.18)
89:;
?>=<
1
?>=<
/ 89:;
2
?>=<
/ 89:;
4
(2.19)
pero no es el sub-grafo inducido por {1, 2, 4}, ya que en (2.18) hay un arco entre 4 y 1.
El sub-grafo inducido es
89:;
?>=<
?>=<
(2.20)
1 gNNN / 89:;
2
NNN >>>
>
NNN >
NNN >>
NNN>>
N
89:;
?>=<
4
Por otro lado, el grafo
?>=<
89:;
?>=<
/ 89:;
2
1>
>>
>>
>>
>> 89:;
?>=<
3
(2.21)
no es un sub-grafo de (2.18), ya que contiene la arista (2, 3) que no se encuentra en
(2.18).
Un vértice v se dice adyacente a un vértice u si existe un arco de u a v, es decir, si
(u, v) ∈ E[G]. El conjunto de todos los vértices adyacentes a un vértice u se llama el
entorno (neighborhood) de u:
NG (u) = {v|(u, v) ∈ E[G]}
(2.22)
Un camino (path) en un grafo es una lista de vértices que en el grafo son enlazados por
aristas:
Definition 2.6.4. Dado un grafo G, un camino en G es una lista [v1 , . . . , vn ] tal que
1 ≤ i ≤ n ⇒ vi ∈ V [G], y 1 ≤ i < n ⇒ (vi , vi+1 ) ∈ E[G].
Por ejemplo, en el grafo (2.18), los vértices [1, 3, 4] forman un camino, representado
gráficamente como
89:;
?>=<
?>=<
?>=<
/ 89:;
/ 89:;
(2.23)
1
3
4
La longitud de un camino p, indicada con |p|, es el número de vértices en la lista: el
camino p = [v1 , . . . , vn ] tiene longitud |p| = n. Si hay un camino p entre los vertices u y v
(o sea, si p = [u, v2 , . . . , vn−1 , v]), se dirá que v es alcanzable desde u a través del camino
p
p, y se escribirá u → v o, según el caso, simplemente u → v. Un camino es simple si
todos los vértices que lo componen son distintos. En el grafo
?>=<
89:;
?>=<
/ 89:;
1 >^
@2
>>
>>
>>
>> 89:;
?>=<
89:;
?>=<
o
3
4
(2.24)
los caminos [1, 3] y [1, 2, 4, 3] de 1 a 3 son simples, mientras los caminos [1, 2, 4, 1, 2, 4, 3],
[1, 3, 2, 4, 3] o [1, 2, 4, 3, 2, 4, 3] no lo son.
Un sub-camino de p = [v1 , . . . , vn ] es una sub-lista de sus vértices, o sea una lista
pij = [vi , . . . , vj ], 1 ≤ i ≤ j ≤ n.
cahiers d’informatique
22
El grado en entrada (input degree) de un vértice es el número de arcos que llegan en él:
in(u) = |{v|v ∈ V ∧ (v, u) ∈ E}|
(2.25)
y el grado en salida (output degree) es el número de arcos que salen del vértice:
out(u) = |{v|v ∈ V ∧ (u, v) ∈ E}|
(2.26)
la distancia entre dos vértices de un grafo es el número de arcos en el camino de menor
longitud que los une:
p
minp:u→v
|p| si u 6= v
d(u, v) =
(2.27)
0
si u = v
Nótese que, a pesar del nombre, d no es una función distancia en el sentido matemático.
Por ejemplo, en general no es simétrica. En el grafo
()*+
/.-,
~ _???
~
??
~
??
~~
~
??
~ ~
?>=<
?>=<
89:;
/ 89:;
v
u
(2.28)
hay d(u, v) = 1 y d(v, u) = 2. En un grafo no dirigido, por otro lado, d es una distancia.
El diametro de un grafo es el valor máximo de distancia entre vértices del grafo:
D[G]
=
max d(u, v)
(2.29)
u,v∈V [G]
=
max
min |p|
(2.30)
p
u,v∈V [G] p:u→v
Un ciclo (cycle) es un camino que empieza y termina en el mismo vértice. Por ejemplo, en
(2.18), son ciclos los caminos [2, , 3, 2], [1, 2, 4, 1], [1, 2, 4, 1, 2, 4, 1], etc. Un ciclo es simple
si ningún vértice en él se repite, excepto el primero y el últmo. El ciclo [1, 2, 4, 1, 2, 4, 1]
no es simple.
Definition 2.6.5. Un ciclo de un grafo G es un camino [v1 , . . . , vn ] de G tal que v1 = vn .
Nótese que el siguiente no es un ciclo:
89:;
?>=<
1
?>=<
/ 89:;
@2
(2.31)
89:;
?>=<
3
La definición de camino no nos permite recorrer arcos "al revés":
1 a 3 y de 3 a 2, pero no hay arco para volver de 2 a 1.
se puede ir, p.ej., de
Dado un grafo G, su transpuesta GT es el grafo que se deriva de G cambiando la
dirección de todas las aristas. Es decir, formalmente:
Definition 2.6.6. Dado un grafo G = (V, E), la transpuesta de G es el grafo GT = (V, E T )
con
E T = {(v, u)|(u, v) ∈ E}
(2.32)
grafos
23
El máximo número de arcos que se puede encontrar en un grafo se da cuando cualquier
vértice está directamente conectado a todos los demás y a sı́ mismo. Es decir
|E[G]| ≤ |V [G]|2 .
*
*
*
La definición que se ha dado es la de una familia muy general de grafos2 . Sin embargo, a
veces es útil considerar familias más restringidas, que se obtienen imponiendo vı́nculos
adicionales a la definición básica. Estas familias son importantes en informática porque
hay algoritmos que se aplican sólo a ciertas familias y no a otras, o porque ciertos
algoritmos son más eficientes si se aplican a grafos con vı́nculos adicionales.
2.6.1
Grafos dirigidos y no dirigidos
Los grafos de la definición general se llaman también grafos dirigidos. Un grafo no
dirigido es un grafo G en que la relación E es simétrica, es decir, para cualquier par
de vértices u, v, (u, v) ∈ E[G] ⇔ (v, u) ∈ E[G]. En la representación gráfica, esta propiedad
implica que si hay un arco que va de u a v, hay otro de v a u. En este caso se puede
considerar el arco (u, v) como un arco sin dirección; un arco que nos permite de ir en
ambos sentidos. Equivalentemente, se puede considerar la relación E como constituida
por pares no ordenados. El grafo del ejemplo ii) es dirigido: dadas dos asignaturas u y
v, so (u, v) ∈ E, entonces u es un prerequisito para cursar v. En este caso, claramente,
(v, u) 6∈ E, porque si no v también serı́a un prerequisito para cursar u, y no serı́a posible
cursar ninguna de las dos asignaturas. Por contra, el grafo del ejemplo iv) es no
dirigido: si u tiene la misma nacionalidad que v ((u, v) ∈ E), claramente v tiene la misma
nacionalidad que u ((v, u) ∈ E).
Gráficamente, un grafo no dirigido se representa con arcos sin flechas.
grafo no dirigido que corresponde al grafo dirigido (2.18) es
GF
ED
89:;
?>=<
89:;
?>=<
2 MM
1;
MMM
;;
MM
;;
;;
89:;
?>=<
4
;;
rr
r
r
;
r
r
r
89:;
?>=<
3
Por ejemplo, el
(2.33)
Las definiciones de camino y ciclo se dan para un grafo no dirigido exactamente como para
un grafo dirigido, con la diferencia que en un grafo no dirigido no se admiten arcos que
empiezan y terminan en el mismo vértice: cada arco debe ser entre dos vértices
distintos.
En el caso de grafos no dirigidos, no se puede distinguir entre arcos que entran en un
vértice y arcos que salen de un vértice. Consecuentemente no se distingue entre el grado
en entrada y el grado en salida de un vértice, definiéndose el grado de un vértice u como
el número de arcos que tienen un vértice en u:
deg(u) = |{v : (u, v) ∈ E}| = |{v : (v, u) ∈ E}|
(2.34)
2 La definición que se da aquı́ se refiere a un grafo simple, en el cual sólo se admite un arco entre dos
vértices dados. Una definición un más general es la de multigrafo, en el cual se pueden encontrar varios
arcos entre dos vértices dados. En un multi-grafo, E[G] no es un conjunto, sino un multi-conjunto (bag).
cahiers d’informatique
24
Nota:
En el caso de grafos no dirigidos en general se consideran los dos arcos (u, v) y (v, u)
como un solo arco, que se pone en conjunto E como un par no ordenado; en este caso, en
la cuenta de los arcos, (u, v) y (v, u) cuentan como uno. Ésto significa, por ejemplo, que
en el caso (2.33) es |E| = 6.
2.6.2
Grafos conexos y no conexos
Un grafo (dirigido o no) se llama conexo si cualquier par de vértices está conectado por
un camino; un grafo dirigido y conexo se llama fuertemente conexo.
Definition 2.6.7. Un grafo dirigido G = (V, E) es (fuertemente) conexo si para cada par
p
u, v ∈ V existe un camino p tal que u → v.
El grafo
89:;
?>=<
1
O
?>=<
/ 89:;
2
89:;
?>=<
3 o
89:;
?>=<
4
(2.35)
es fuertemente conexo: se puede ir de cada vértice a cada vértice simplemente dando una
vuelta al grafo. El grafo siguiente, por otro lado, no es fuertemente conexo:
89:;
?>=<
1
O
?>=<
/ 89:;
2
O
89:;
?>=<
3 o
89:;
?>=<
4
(2.36)
ya que desde ningún vértice se puede alcanzar el vértice 4.
El grafo no dirigido del ejemplo iv) no es conexo, por lo menos si no consideramos
personas con doble nacionalidad: desde, por ejemplo, un espa~
nol se podrá llegar a los
demás espa~
noles pero no se podrá llegar a ningún italiano. (Aún si se considera la doble
nacionalidad, el grafo sigue no conexo, ya que hay paises que no admiten doble
nacionalidad.)
Según esta definición, el siguiente grafo no es conexo
OGF
ED
?>=<
89:;
?>=<
/ 89:;
1 ;]
2 MM
O MMM
;;
MM&
;;
;;
89:;
?>=<
8 4
;;
rrr
r
;
r
r
r
89:;
?>=<
3
(2.37)
porque, por ejemplo, no hay camino que conduzca desde vértice 2 hasta el vértice 3. Al
contrario, si se ignora el sentido de las flechas, (2.37) se reduce a (2.33), que es
conexo.
Si un grafo no es conexo (resp. fuertemente conexo), es posible dividirlo en partes
(particionarlo) de manera que cada parte sea conexa (resp. fuertemente conexa); estas
partes se llaman las componentes conexas (resp. componentes fuertemente conexas ) del
grafos
25
grafo. El grafo (2.37), por ejemplo, tiene dos componentes fuertemente conexas: una
compuesta por los vértices 1, 2 y 4 y por los arcos que los unen, la otra sólo por el
vértice 3.
En el grafo del ejemplo iv) (sin doble nacionalidad), cada nacionalidad es una componente
conexa. Cada componente, de hecho, representa un tipo particular de grafo: un grafo
completamente conexo. En cada nacionalidad, cada persona está conectada directamente con
todas las demás personas.
Theorem 2.6.1. Si G = (V, E) es conexo y no dirigido, entonces |E| ≥ |V | − 1.
Demostración. La demostración es por inducción sobre el número de vértices.
Si n = 1 o n = 2 el grafo contiene 0 o 1 arcos, es decir, |V | − 1 arcos, y el teorema es
cierto.
Supongamos que la propiedad se cumpla para todo grafo con n − 1 vértices, y consideremos
un grafo G = (V, E) con |V | = n. Eligimos un vértice u y el grafo inducido por
V 0 = V − {u}. Este grafo es un sub-grafo de G formado por n − 1 vértices y, por la
hipótesis inductiva, es |E 0 | ≥ |V 0 | − 1. Siendo G conexo, existe por lo menos un arco entre
u y un vertice v ∈ V 0 . Claramente (u, v) 6∈ E 0 , porque u 6∈ V 0 , ası́ que E 00 = E 0 ∪ {(u, v)}
contiene |E| + 1 arcos, y
|E 00 | = |E 0 | + 1 ≥ |V 0 | − 1 + 1 = |V | − 1
(2.38)
El grafo (V, E 00 ) es un sub-grafo de G, por lo cual |E| ≥ |E 00 | ≥ |V | − 1.
Si G es dirigido y conexo, G contiene por lo menos tantos arcos como su correspondiente
no dirigido, por lo cual el teorema se cumple también en el caso de grafos dirigidos
fuertemente conexos.
2.6.3
Grafos con ciclos o acı́clicos
Un grafo es acı́clico si no contiene ningún camino entre un vértice y si mismo. Solo cabe
recordar que una clase muy importante en aplicaciones es la clase de los grafos dirigidos
acı́clico, o DAG (Directed Acyclic Graph).
En el caso de grafos no dirigidos, no se tienen en cuenta ciclos formados por dos
vértices. En el caso siguiente, por ejemplo
89:;
?>=<
1
89:;
?>=<
2
(2.39)
es posible ir del vértice 1 a sı́ mismo con el recorrido [1, 2, 1]. Si se consideraran estos
ciclos, es evidente que todo grafo no dirigido tendrı́a ciclos, y la definición carecerı́a
de interés. Por tanto, en el caso de grafos no dirigidos, estipulamos que un ciclo es un
camino que empieza y termina con el mismo vértice y contiene por lo menos tres vértices
distintos. Un grafo no dirigido es acı́clico si no contiene ningún camino con estas
caraterı́sticas. Con esta definición, el grafo (2.39) y el grafo a la izquierda de (2.40)
son acı́clicos, mientras el grafo a la derecha de (2.40) es cı́clico.
?>=<
89:;
1
?>=<
89:;
3
89:;
?>=<
2
89:;
?>=<
1
89:;
?>=<
3
89:;
?>=<
2
(2.40)
cahiers d’informatique
26
La distinción ente grafos dirigidos y no dirigidos es a veces delicada.
grafo G = (V, E), con
V
E
Consideremos el
= {a, b}
= {(a, b), (b, a)}
Se puede considerar G como grafo dirigido ((2.41) a la izquierda) o no dirigido ((2.41)
a la derecha).
%
?>=<
89:;
89:;
?>=<
89:;
?>=<
89:;
?>=<
a f
a
(2.41)
b
b
En el primer caso el grafo tiene un ciclo, en el segundo no. Es necesario siempre
clarificar de que tipo de grafo se está hablando, ya que la estructura puede ser ambigua.
El grafo del ejemplo ii) es acı́clico: si hubiera un ciclo, nos encontrarı́amos frente a
un conjunto de asignaturas mutuamente prioritarias, ası́ que no se podrı́a cursar ninguna
de ellas. El grafo no dirigido del ejemplo 4 contiene ciclos (una nación tiene más de 2
ciudadanos).
2.6.4
Grafos con pesos o sin pesos
En algunas aplicaciones, es necesario asignar un valor numérico, llamado peso o coste, a
cada arco. Esto permitirá afrontar varios problemas como problemas de optimización. En
la mayorı́a de los casos que se encontrarán los pesos sólo pueden tomar valores positivos;
pero habrá también en las aplicaciones grafos con pesos negativos. Veremos que hay
varios problemas que tienen solución en el primer caso y no el el segundo.
El peso se representa como una función w : E → R.
En un grafo como el del ejemplo i) es útil en muchas aplicaciones a~
nadir una función
peso: el peso de una arista es igual a la distancia entre los aeropuertos
correspondientes a los vértices conexos. Un ejemplo (con distancias en Km) es
JFK
vv
3932 vv
v
vv
vv
LAX SSS
SSSS
SSSS
SS
10181 SSSS
SS
2.6.5
5766
(2.42)
MAD H
HH
HH959
HH
1263
HH
LHR 780 CDG
831
FCO
856
BCN
Árboles
Un árbol libre es un grafo conexo, acı́clico, y no dirigido.
()*+
/.-,
()*+
/.-,
()*+
/.-,
()*+
/.-,
()*+
/.-,
()*+
/.-,
()*+
/.-,
Por ejemplo:
()*+
/.-,
()*+
/.-,
()*+
/.-,
()*+
/.-,
()*+
/.-,
()*+
/.-,
()*+
/.-,
(2.43)
grafos
27
Theorem 2.6.2. Dado un grafo G = (V, E), las seis propiedades siguientes son equivalentes:
i) G es un árbol libre;
ii) cada par de vértices de G es conexa por uno y un solo camino;
iii) G es conexo pero si cualquier arco es eliminado de E, el grafo resultante ya no es
conexo;
iv) G es conexo y |E| = |V | − 1;
v) G es acı́clico y |E| = |V | − 1;
vi) G es acı́clico, pero si se a~
nade un arco al conjunto E, el grafo resultante contiene
un ciclo.
Demostración. Para demostrar las equivalencias es suficiente demostrar las implicaciones
i) ⇒ ii) ⇒ iii) ⇒ iv) ⇒ v) ⇒ vi) ⇒ i)
i) ⇒ ii)
Siendo un árbol conexo, cada par de vértices en V está conectado por un camino simple.
Hay que demostrar que este camino es único. Supongamos que no lo es, es decir, que haya
al menos un par de vértices u y v unidos por dos caminos simples p1 y p2 . La situacción
en este caso es la siguiente:
89:;
?>=<
o x k+ )i g' $d
o
o
a!
ooo
89:;
?>=<
?>=<
?>=<
89:;
?>=<
z o/ o/ /o 89:;
v
u o/ o/ o/ 89:;
w NN
NNN
;8x { >~
NN
89:;
?>=<
y 3s u5
(2.44)
Sea w el vértice donde p1 y p2 se separan por primera vez: w se encuentra en p1 y en p2 ,
pero en p1 el sucesor de w es x, y en p2 es y, con x 6= y. Sea z el primer vértice en que
los caminos vuelven a unirse, es decir el primer vértice que es un sucesor de w ya sea en
p1 que en p2 :
p1
p2
=
=
[u, . . . , w, x, . . . , z, . . . , v]
[u, . . . , w, y, . . . , z, . . . , v]
(2.45)
(2.46)
Consideremos los sub-caminos
p0
p00
= [w, x, . . . , z]
= [w, y, . . . , z]
(2.47)
(2.48)
Estos caminos no contienen ningún vértice en común excepto los extremos, por lo cual el
camino que se consigue recorriendo p0 seguido por p00 al revés es un ciclo:
[w, x, . . . , z, . . . , y, w]
pero si G es un árbol no puede contener ciclos:
entre u y v nos lleva a una contradición.
(2.49)
la hipótesis que haya más de un camino
cahiers d’informatique
28
ii) ⇒ iii)
Siendo cada par de vértices conexo por uno y un solo camino simple (hipótesis ii), G es
conexo. Consideremos un arco (u, v). Este es un camino simple entre u y v y, por la
hipótesis, es único. Si removemos (u, v) no hay camino entre u y v, luego el grafo sin
(u, v) no es conexo.
iii) ⇒ iv)
La hipótesis es que G sea conexo y que eliminando una arista cualquiera el grafo
resultante no es conexo. Siendo el grafo conexo, por el teorema 2.6.1 es |E| ≥ |V | − 1.
Demostraremos por inducción que |E| ≤ |V | − 1, lo que implica que |E| = |V | − 1.
Es fácil averiguar que un grafo conexo con n = 1 o n = 2 vértices tiene exactamente n − 1
arcos.
Supongamos que G tenga n ≥ 3 vértice y que cada grafo con k < n vértices que cumple iii)
también cumpla la desigualdad |E| ≤ |V | − 1. G también cumple iii), entonces removiendo un
arco el grafo se separa G en dos grafos G1 , G2 con menos de n vértices. Por la
hipótesis inductiva, si los sub-grafos tienen k1 y k2 vértices, hay |E[G1 ]| ≤ k1 − 1 y
E[G2 ] ≤ k2 − 1 y por tanto
|E[G1 ]| + |E[G2 ]| ≤ k1 + k2 − 2 = n − 2
(2.50)
A~
nadiendo el arco que habı́amos removido obtenemos
|E[G]| = |E[G1 ]| + |E[G2 ]| + 1 ≤ n − 1 = |V | − 1
(2.51)
iv) ⇒ v)
Supongamos que G sea conexo y que |E| = |V | − 1. Tenemos que demostrar que G es acı́clico.
Por contradición, asumimos que G tenga un ciclo con k vértices v1 , . . . , vk . Sea
Gk = (Vk , Ek ) el sub-grafo de G que consiste en el ciclo. Notamos que |Ek | = |Vk | = k. Si
k = n, el grafo contiene por lo menos n arcos, contradiciendo |E| = |V | − 1. Si k < n, ya
que G es conexo, hay un vértice vk+1 ∈ V − Vk adyacente a un vértice vi de Vk . Definimos
Gk+1 = (Vk+1 , Ek+1 ) con Vk+1 = Vk ∪ {vk+1 } y Ek+1 = Ek ∪ {(vi , vk+1 )}. Notese que
|Ek+1 | = |Vk+1 | = k + 1. Si k + 1 < n podemos definir de forma analoga Gk+2 , Gk+3 , hasta
llegar a un grafo Gn con |En | = |Vn | = n. Gn es un sub-grafo de G, lo que supone En ⊆ E,
o |En | ≤ |E|, o sea |E| ≥ n = |V |, en contradición con la hipótesis que |E| = |V | − 1. Luego,
G es acı́clico.
v) ⇒ vi)
Supongamos que G es acı́clico y que |E| = |V | − 1. Sea k el número de componentes conexas
de G. Cada una es un árbol, ya que es conexa y acı́clica, y la implicación i) ⇒ v) nos
dice que el total de arcos en todas las componentes es |V | − k o sea, por la hipótesis,
k = 1: el grafo es conexo y consecuentemente es un árbol. Ya que i) ⇒ ii), cada par de
vértices de G está conectado por un solo camino simple, ası́ que a~
nadiendo un arco a G se
crea un ciclo.
vi) ⇒ i)
Supongamos que G es acı́clico, y que a~
nadiendo un arco se crea un ciclo. Para demostrar
i) tenemos que demostrar que G es conexo. Sean u y v dos vértices arbitrarios de G. Si
u y v son adyacentes por supuesto hay un camino entre ellos: el arco (u, v). Si no lo
son, a~
nadimos el arco (u, v), lo que por hipótesis crea un ciclo. Todo arco de este ciclo,
excepto (u, v), está en E, ası́ que en E hay un camino entre u y v, es decir, G es
conexo.
grafos
29
Un árbol con raı́z es un árbol donde se ha identificado un vértice (llamado la raı́z del
árbol). Formalmente, un árbol con raı́z es un par (G, r), donde G es un árbol, y r ∈ V [G]
es la raı́z.
Considérese un vértice x en un árbol con raı́z: cualquier vértice y en el camino entre la
raı́z y x se llama un antepasado de x y x es un descendiente de y. Si y es un antepasado
de x, y (y, x) ∈ E, x es un hijo de y e y es el antecesor de x.
Un árbol ordenado es un árbol con raı́z en que se asume un orden entre los hijos de
cualquier vértice.
2.6.6
Ejemplos
?>=<
/ 89:;
l
A2
, 89:;
?>=<
r8 3
r
r
rrrr
?>=<
89:;
?>=<
/ 89:;
5
4
I
89:;
?>=<
1
(2.52)
/4
/3
Este grafo es dirigido, con ciclos (p.ej. 2
conexo (no hay ningún camino que termine en el vértice 1). El
componentes conexas: una representada por los vértices 2, 3 y
por el vértice 1 y otra representada sólo por el vértice 5.
/2, o 4
/ 4 ), y no
grafo tiene tres
4, otra representada sólo
89:;
?>=<
1
89:;
?>=<
2 MM
MMM
MM
89:;
?>=<
3
rr
r
r
r
r
r
89:;
?>=<
89:;
?>=<
5
4
Este grafo es no dirigido, con ciclos (p.
ej.
1
(2.53)
2
89:;
?>=<
1
89:;
?>=<
2
89:;
?>=<
3
89:;
?>=<
4
89:;
?>=<
5
89:;
?>=<
6
4
5
1 ), y conexo.
(2.54)
Este grafo es no dirigido, acı́clico y no conexo. El grafo tiene dos componentes conexas:
una representada por los vértices 1, 2, 4 y 5 y una representada por los vértices 3 y 6.
Este grafo es un árbol.
89:;
?>=<
1
89:;
?>=<
2
89:;
?>=<
3
89:;
?>=<
4
89:;
?>=<
5
89:;
?>=<
6
(2.55)
cahiers d’informatique
30
2.7
Representación de grafos
En esta sección se supondrá siempre que los vértices de un grafo sean identificados con
los enteros 1, . . . , n. Hay varias formas de representar grafos en la memoria de un
ordenador; muchas de ellas sirven para la solución de problemas especı́ficos, y se
consiguen modificando oportunamente una de las dos formas de base que se presentarán en
esta sección: las listas de adyacencias y la matriz de adyacencia.
2.7.1
Listas de adyacencia
En la representación por listas de adyacencias, los vértices del grafo se almacenan en
una cadena (array ) A[1, . . . , n], donde n = |V |. Cada elemento A[i] de la cadena contiene un
vértice, y es una estructura con dos elementos: A[i].v contiene el valor del vértice
número i, y A[i].a es una lista que contiene los ı́ndices de todos los vértices del entorno
de A[i]. El elemento A[a].a se llama la lista de adyacencia (adjacency list) del vértice y
se implementa como una lista; sin embargo cabe recordar que el orden en que los vecinos
de u aparencen en la lista es irrelevante y la lista se usa para representar lo que,
formalmente, es un conjunto. (Se utilizan listas sobre todo porque las listas son
fáciles de manejar, pero serı́a posible utilizar cualquiera de las representaciones para
conjuntos.) En pseudo-código, las estructuras que se usan para representar un grafo son:
type graph is array[1..n] of vertex;
type vertex is
begin
val: nodeval;
ady: nodelist;
end
type nodelist is
begin
idx: integer;
next: →nodelist;
end
El tipo "nodeval" es cualquier tipo necesario para representar el contenido de un
vértice. En los ejemplos, "nodeval" será casi siempre un número entero o una letra; en
práctica, "nodeval" será casi siempre un puntero a una estructura apropiada que contendrá
los datos que se almacenan en el grafo.
La representación por lista de adyacencia del grafo (2.18) es ilustrada esquemáticamente
en la figura 2.7.1
La representación por listas de adyacencias se puede extender fácilmente a grafos con
pesos. En este caso, el peso del arco (u, v), w(u, v), se almacena junto a la ocurrencia
del vértice v en la lista de adyacencias de u, y el tipo "nodelist" cambia en
type nodelist is
begin
idx: integer;
peso: real;
grafos
31
3 idx = 2
1 v = 1 next =
a = idx = 3
- next =
3 idx = 3
2 v = 2 next =
a = idx = 4
- next =
3 idx = 4
3 v = 3 next =
a = 3 idx = 1
4 v = 4 next =
a = Figure 2.6:
next:
cmdend
Representación esquematica de listas de adyacencia.
→nodelist;
La cantidad de memoria necesaria para representar un grafo G = (V, E) usando listas de
adyacencia es O(V + E). La suma del tama~
no de todas las listas de adyacencia es |E| si
el grafo es dirigido (dado que cada arco (u, v) aparece una vez el la lista de adyacencia
de u) y 2|E| si el grafo es no dirigido (dado que cada arco (u, v) aparece dos veces: una
en la lista de adyacencia de u y una en la de v).
Dado un grafo con un determinado numero de vértices |V |, esta representación es tanto más
económica cuanto más el grafo es disperso. La verificación de la existencia de un arco
entre los vértices u y v es con esta representación una operación potencialmente costosa,
en cuanto implica la búsqueda del vértice v en toda la lista de adyacencia de u. Si el
grafo es denso, el coste de esta operación es O(V ). Si se necesita un acceso rápido a
las arista de un grafo denso, es oportuno utilizar la representación por matrices de
adyacencia, que ahora vamos a estudiar.
2.7.2
Matriz de adyacencia
La matriz de adyacencia de un grafo G = (V, E) es una matriz A = (aij ) de tama~
no |V | × |V |
tal que
1 si (i, j) ∈ E
aij =
(2.56)
0 si no
La matriz de adyacencia de un grafo ocupa un espacio de memoria O(V 2 ), ya sea que el
grafo sea disperso o bien que sea denso. En un grafo no dirigido un arco entre los
vértices i y j existe si, y sólo si, un arco existe entre los vértices j y i, por lo cual,
para un grafo no dirigido, aij = aji o sea, la matriz A es simétrica. Esta propiedad
puede ser útil para almacenar la matriz en el ordenador ya que, en el caso de una matriz
simétrica, es suficiente almacenar la diagonal principal y uno de los dos "triangulos",
superior o inferior, por un total de n(n + 1)/2 elementos en lugar de n2 . Por ejemplo, la
cahiers d’informatique
32
matriz de adyacencia del grafo (2.18) es

0
 0

 0
1
Mientras para el grafo (2.33), la

0
 1

 1
1
1
0
0
0
1
1
0
0

0
1 

1 
0
(2.57)
matriz de adyacencia y su representación reducida son



0 1 1 1
1 1 1

0 1 1 
0 1 1 



(2.58)

0 1 
1 0 1 
0
1 1 0
La representación por matriz de adyacencia también se puede extender a grafos con pesos
poniendo aij = w(i, j). Nótese que en este caso no se puede poner aij = 0 para indicar la
ausencia de la arista (i, j), ya que ésto se confundirı́a con la presencia de una arista de
peso 0. En el caso de grafos ponderados, entonces, será necesario definir un sı́mbolo
especial, diferente de todas las representaciones de posible pesos, para indicar la
ausencia de arco. Este sı́mbolo se indica tradicionalmente con ∞, ya que en muchos
algoritmos la ausencia de la arista (i, j) es equivalente a la presencia de una arista (i, j)
de peso infinito. Por ejemplo, la representación por matriz de adyacencia del grafo
89:;
?>=<
1
?>=<
/ 89:;
1
vv
v
v
0
zvvv
0.5
89:;
?>=<
89:;
?>=<
o
4
3
3
1
(2.59)
2
es dada por la matriz

∞ 1∞ 0
 ∞ ∞ 0.5

 ∞ ∞ ∞
∞ ∞ ∞

2 

3 
∞
(2.60)
Con esta representación, averiguar la existencia de una arista entre los vértices i y j
es O(1), es decir, toma un tiempo constante, independiente del tama~
no del grafo. El
precio que se paga para esta velocidad es la ocupación de memoria: la representación
ocupa O(V 2 ), independientemente del número de arcos. Este compromiso entre velocidad y
memoria es muy común en informática, y volveremos a encontrarlo muchas veces.
En general, la representación por matriz de adyacencia es tanto más aprovechable cuanto
más denso es el grafo (en un grafo denso el acceso a una arista con listas de adyacencia
es O(V ), y la ocupación de memoria O(V + E) es decir, si el grafo es denso y E ≈ V 2 ,
comparable con la ocupación de la matriz), mientras en el caso de grafos poco densos se
desperdicia mucha memoria sin ventajas apreciables sobre la representación por listas.
*
*
*
Si un grafo se representa con una matriz de adyacencia A, su transpuesta GT se
representa con la transpuesta de A, AT 3 . Si G se representa con listas de adyacencia,
su transpuesta se calcula con el algoritmo siguiente, que se ejecuta en un tiempo
O(V + E):
3 De
hecho, la denominación de transpuesta de un grafo deriva de esta propiedad.
grafos
transpose(V)
V1: array[1,..|V|] of vertex;
for i=1 to |V| do
V1[i] ← V[i];
V1[i].ady ← [];
od
foreach u in V do
foreach v in u.ady do
insert(u, V1[v].ady);
od
od
return V1;
33
cahiers d’informatique
34
2.8
Ejercicios
1. (mat.) ¿Cuántos grafos diferentes se pueden construir con n vértices?
grafos no dirigidos?
2. (mat.)
¿Cuántos
Demostrar que un grafo no dirigido no tiene ningún ciclo de longitud n < 3.
3. Representar los grafos siguientes con listas de adyacencia y con matriz de
adyacencia
89:;
?>=<
89:;
/ ?>=<
2 h
6 1
O
89:;
?>=<
89:;
3
4=
?>=<
==
== ?>=<
89:;
5
89:;
?>=<
?>=<
89:;
2
1 JJ
JJJttt
t
t JJ
ttt J89:;
89:;
?>=<
?>=<
3
4
89:;
?>=<
1
89:;
?>=<
2
89:;
?>=<
1
89:;
?>=<
6 1T j
* 89:;
?>=<
2
?>=<
89:;
89:;
/ ?>=<
1=
@2
==
== ?>=<
89:;
@ 3 ==
==
= ?>=<
89:;
>=<
/ ?89:;
4
5
89:;
?>=<
3
89:;
?>=<
4
y
89:;
?>=<
3
I
4. (mat.) En un grafo no dirigido, en grado de un nodo u, deg(u) es el número de arcos
que tienen como vértice u. Demuestren que la suma de los grados de todos los
vértices de un grafo es igual a dos veces el número de arcos:
X
deg(u) = 2|E[G]|
(2.61)
u∈V [G]
5. La propiedad precedente implica que la suma de los grados de los vértices de un
grafo no dirigido es siempre par. Usen esta propiedad para demostrar que no es
posible que en un grupo de 9 personas cada uno conozca exactamente a tres personas.
6. Dada la representación de un grafo dirigido G en forma de listas de adyacencia,
escriban un algoritmo que derive la representación del grafo no dirigido
correspondiente. El grafo no dirigido deberá:
a) contener un arco entre u y v si el arco (u, v) o el arco (v, u) se encuentra en el
grafo G;
b) si G contiene ciclos de longitud 1, estos de deberán eliminar del grafo no
dirigido.
Asuman que las operaciones sobre listas (inserción de un elemento, eliminación,
búsqueda) son disponibles.
7. Dada la representación de un grafo por listas de adyacencia, escriban un algoritmo
para determinar el grado en ingreso y el grado en salida de cualquer vértice.
8. Repitan el ejercicio precedente con el grafo dado en representación con matriz de
adyacencia.
grafos
9. Un pozo es un vértice con grado de ingreso |V | − 1 y grado de salida 0, o sea, un
vértice en que entran arcos desde todos los demás vertices y no sale ningún arco.
Dado un grafo representado con listas de adyacencias, escriban un algoritmo para
determinar si el grafo contiene un pozo.
10. Repitan el ejercicio precedente con el grafo dado en representación con matriz de
adyacencia.
11. Escriban la representación con listas de adyacencias y con matriz de adyacencia de
un árbol binario completo con 7 vértices.
12. Demuestren que si un grafo G es no dirigido se cumple GT = G.
13. Demuestren que un grafo G es acı́clico si y sólo si GT es acı́clico.
14. Calculen el tiempo de ejecución (asintótico) del algoritmo transpose.
35
36
cahiers d’informatique
III. ALGORITMOS DE BÚSQUEDA
Una componiente fundamental de muchos algoritmo de grafos es un algoritmo para "visitar"
de forma sistemática todos los vértices que se puedan alcanzar desde un vértice fuente
dado. Los algoritmos de búsqueda permiten esta visita. Como en el caso de los árboles,
hay dos tipos de búsqueda que recorren los vértices de manera distinta: la búsqueda en
anchura (breadth-first search, o BFS ), y la búsqueda en profundidad (depth-first search,
o DFS ).
A pesar de su nombre, los algoritmos de búsqueda no se utilizan principalmente para
buscar vértices. Si nos interesan, por ejemplo, todos los vértices que cumplen una
condición dada, la manera más rápida y sencilla de encontrarlos es mediante una búsqueda
en el campo val de la representación por listas de adyacencia, o en la cadena vertex de
la representación por matriz de adyacencia. Tal búsqueda es independiente de la
estructura del grafo (es decir, de sus aristas). Pero, además de recorrer todos los
vértices, los algoritmos BFS y DFS construyen una estructura (el árbol BFS y el árbol
DFS, respectivamente) que proporciona información sobre la estructura del grafo y sobre
la relación entre vértices. Como veremos, varios algoritmos usan los árboles construidos
por los algoritmos de búsqueda para resolver varios problemas, entre ellos:
i) la determinación de la existencia de ciclos;
ii) la búsqueda de componentes conexas;
iii) la determinación del diámetro de un grafo;
iv) la búsqueda de caminos mı́nimos;
v) la búsqueda de caminos que pasan por todos los vértices;
vi) la búsqueda de caminos compuestos por el mı́nimo número de arcos.
Hamilton).
(caminos de
Los algoritmos BFS y DFS para grafos tienen una estructura bastante parecida a sus
homónimos para árboles. Sin embargo, hay diferencias estructurales importantes entre
grafos
37
árboles y grafos que hacen que los sencillos algoritmos de búsqueda en árboles no
funcionen en grafos.
Los algoritmos BFS y DFS en árboles funcionan gracias a una importante propiedad de los
árboles: dado cualquier vértice v, existe uno y un solo camino que une la raı́z del árbol
a v. Esta propiedad no se extiende a otros tipos de grafos: dado un grafo, y
identificado un nodo "fuente", s, generalmente hay varios caminos entre s y un vértice
dado v. A causa de esta multiplicidad, si nos pusiéramos a explorar un grafo con el
algoritmo BFS para árboles, correrı́amos el riesgo de visitar el mismo vértice muchas
veces. Si el grafo contiene ciclos, podrı́amos pasar por el mismo vértice un número
infinito de veces. Aplicando el algoritmo BFS-árboles a un grafo, es posible que la
ejecución del algoritmo no termine. El caso del algoritmo DFS es análogo.
Como veremos, para garantizar que cada vértice se visite una sola vez, los algoritmos BFS
y DFS para grafos tendrán que "marcar" los vértices a medida de que se visitan, a~
nadiendo
a todo vértice un estado.
3.9
Búsqueda en anchura
Dado un grafo G, y un vértice fuente s, el algoritmo BFS explora los arcos de G llegando
a cualquier vértice alcanzable desde s; en el curso de esta operación, BFS calcula la
distancia (en numero de arcos cruzados) de s a cada vértice alcanzable, y determina un
árbol (el árbol BF ) que contiene todos los vértices visitados y tiene s como raı́z. Para
cada vértice v alcanzable desde s el camino entre s y v en el árbol corresponde al camino
más corto en el grafo entre los mismos vértices. El algoritmo funciona en grafos
dirigidos y no dirigidos; el nombre de búsqueda en anchura le deriva del hecho de que el
algoritmo visita todo vértice a distancia k de la fuente antes de empezar la visita de
los vértices a distancia k + 1. El algoritmo extiende uniformemente la frontera entre la
parte del grafo analizada y la parte todavı́a desconocida.
Para su funcionamiento, el algoritmo BFS asocia a cada vértice del grafo tres atributos:
un estado, una distancia, y un antecesor, que definimos a continuación:
Estado: un vértice se puede encontrar
principio, todos los vértices se
encuentra en estado V; cuando un
exploración, su estado cambia de
en estado visitado (V) o no visitado (N). Al
encuentran en estado N, excepto la fuente, que se
vértice se encuentra por primera vez en la
N a V.
Distancia: el atributo d[u] del vértice u contiene la distancia (expresada en número de
arcos cruzados) entre u y s. Al principio, se pone d[u] = ∞ para u 6= s, y d[s] = 0.
Antecesor: el
desde s.
juega el
atributo
algoritmo construye un árbol que contiene todos los vértices alcanzables
Este árbol es identificado guardando, por cada vértice u, el vértice que
papel de su antepasado en el árbol; tal antepasado se almacena en el
π[u]. Al principio se pone π[u] = nil para cada vértice u.
La representación por listas de adyacencia de este grafo "aumentado" supone la siguiente
variación de la representación de la sección precedente:
type vertex is
begin
val: nodeval;
cahiers d’informatique
38
ady: nodelist;
d: integer;
estado : {V, N};
π: integer;
end
Los otros tipos
En esta sección
representacción
con matrices de
no cambian. La representación por matriz se cambia de manera análoga.
y en las siguientes, los algoritmos se presenterán con referencia a la
por listas de adyacencia. Las modificaciones necesarias para utilizarlos
adyacencia son mı́nimas.
El algoritmo usa una cola fifo (first in, first out), para determinar el orden de
procesamiento de los vértices:
BFS(G, s)
1.
Q: queue of vertex;
2.
foreach u in V[G] do
3.
estado[u] ← N;
4.
d[u] ← ∞;
5.
π[u] ← nil;
6.
od
7.
estado[s] ← V;
8.
d[s] ← 0;
9.
Q.insert(s);
10. while ¬empty(Q) do
11.
u ← Q.extract;
12.
foreach v in u.ady do
13.
if estado[v] = N then
14.
estado[v] ← V;
15.
d[v] ← d[u] + 1;
16.
π[v] ← u;
17.
Q.insert(v);
18.
fi;
19.
od
20. od
A continuación, se explicará el funcionamiento del algoritmo con referencia al ejemplo de
la figura 3.7. El estado de cada vértice se representará mediante su forma: los
vértices que todavı́a no se han visitado se representarán con un cı́rculo sencillo y los
que se han visitado con un cı́rculo doble. Dentro del cı́rculo que representa un vértice u
se pondrá el valor d[u] como resulta en cada paso del algoritmo; los antepasados se
representan con la dirección de las flechas en doble lı́nea (la dirección de la flecha es
desde el antepasado al hijo (es decir π[u] ⇒ u).
Las lı́neas 1--9 del algoritmo nos dejan en la situación en (a): todo vértice v 6= s se
inicializa con d[v] = ∞, se pone d[s] = 0, y nadie tiene antepasado. Cada una de las
figuras (b)--(i) representa el resultado de una ejecución del bucle while. En (b), por
ejemplo, se ha sacado s de la cola, se ha cambiado el estado, la distancia, y el
antepasado de cada uno de sus hijos (los vértices t y w), y se han puesto estos últimos
vértices en la cola. En (c) se ha sacado t de la cola, se ha puesto la distancia de su
hijo x a d[x] = d[t] + 1 = 2, se ha puesto el antepasado de x a π[x] = t, y se ha cambiado el
estado de x. Además, se ha puesto x en la cola.
grafos
39
s
?>=<
89:;
()*+
/.-,
0
(a)
?>=<
89:;
∞
?>=<
89:;
∞
w
?>=<
89:;
()*+
/.-,
0
w
?>=<
89:;
()*+
/.-,
0
(e)
w
s
?>=<
89:;
()*+
/.-,
0
(g)
w
x
?>=<
89:;
89:;
?>=<
∞
∞
:::
::
::
::
?>=<
89:;
()*+
/.-,
?>=<
89:;
89:;
?>=<
∞
∞
2
s
89:;
?>=<
()*+
/.-,
0
t
y
z
w
x
u
v
s
t
?>=<
89:;
()*+
/.-,
1
s
?>=<
89:;
()*+
/.-,
0
?>=<
89:;
()*+
/.-,
1
w
Figure 3.7:
u
v
?>=<
89:;
()*+
/.-,
0
89:;
?>=<
()*+
/.-,
1
89:;
?>=<
()*+
/.-,
0
?>=<
()*+
/.-,
+3 89:;
1
?>=<
()*+
/.-,
+3 89:;
1
?>=<
()*+
/.-,
+3 89:;
1
z
w
x
u
v
s
t
89:;
?>=<
()*+
/.-,
0
89:;
?>=<
()*+
/.-,
1
y
z
w
89:;
()*+
/.-,
+3 ?>=<
1
u
v
x
∅
y
z
x
[y, v]
t
y
[t, w]
u
y
[x, z]
u
?>=<
()*+
/.-,
+3 89:;
1
y
[u, y]
u
v
y
[v]
(d)
z
v
(f )
z
v
89:;
?>=<
()*+
/.-,
?/.-,
()*+
>=<
+3 89:;
4
?G 3 66
6
66
66
6
89:;
?>=<
()*+
/.-,
89:;
?
()*+
/.-,
>=<
89:;
?>=<
()*+
/.-,
3
+
2
3
4< 2
x
(b)
z
89:;
?>=<
()*+
/.-,
89:;
?>=<
∞
?G 3 88
8
88
88
8
89:;
?>=<
()*+
/.-,
?/.-,
()*+
>=<
89:;
?>=<
()*+
/.-,
+3 89:;
2
3
3; 2
y
?/.-,
()*+
>=<
?>=<
89:;
()*+
/.-,
+3 89:;
4
?G 3 66
6
66
66
6
?>=<
89:;
()*+
/.-,
?
89:;
()*+
/.-,
>=<
89:;
?>=<
()*+
/.-,
3
+
2
3
4< 2
v
89:;
?>=<
89:;
?>=<
∞
∞
:::
::
::
::
89:;
?>=<
()*+
/.-,
89:;
?>=<
?>=<
()*+
/.-,
∞
2
2
3; 89:;
89:;
?>=<
()*+
/.-,
1
89:;
()*+
/.-,
+3 ?>=<
1
u
89:;
?>=<
89:;
?>=<
∞:
∞
:
:
::
::
::
89:;
?>=<
89:;
?>=<
89:;
?>=<
∞
∞
∞
?>=<
89:;
()*+
/.-,
89:;
?>=<
∞
?G 3 88
8
88
88
8
?>=<
89:;
()*+
/.-,
()*+
/.-,
>=<
89:;
?>=<
()*+
/.-,
+3 ?89:;
2
3
3; 2
x
[z, u, y]
t
w
(i)
z
89:;
()*+
/.-,
+3 ?>=<
1
?>=<
89:;
()*+
/.-,
1
t
y
x
[w, x]
t
s
s
89:;
?>=<
()*+
/.-,
1
89:;
()*+
/.-,
+3 ?>=<
1
?>=<
89:;
()*+
/.-,
1
v
?>=<
89:;
89:;
?>=<
∞;
∞
;
;
;
;;
;;
;
?>=<
89:;
?>=<
89:;
89:;
?>=<
∞
∞
∞
x
[s]
t
s
(c)
u
t
(h)
z
?>=<
89:;
()*+
/.-,
?/.-,
()*+
>=<
+3 89:;
4
?G 3 66
6
66
66
6
?>=<
89:;
()*+
/.-,
()*+
/.-,
>=<
89:;
?>=<
()*+
/.-,
+3 ?89:;
2
3
4< 2
Las fases sucesivas del funcionamiento del algoritmo BFS
cahiers d’informatique
40
Nótese que al paso (g) el grafo ya está en su estado final: todos los vértices han sido
alcanzados y han cambiado de estado. Sin embargo, el algoritmo no termina, ya que
todavı́a hay dos vértices en la cola. Los dos vértices son analizados en los pasos (h) y
(i) pero, como ningún vecino de ellos se encuentra en estado N, el test de la linea 13
del algoritmo no se cumple nunca, ası́ que estas iteraciones no alteran el estado del
grafo.
3.9.1
Análisis del tiempo de ejecución
El algoritmo contiene dos bucles anidados: el bucle while de la lı́nea 10 y, dentro de
éste, el bucle que empieza por el foreach de la lı́nea 12. El bucle while se ejecuta un
número de veces igual al número de vértices del grafo, por tanto la lı́nea 11 se ejecuta
un número de veces O(V ). El bucle foreach de la lı́nea 12 también se ejecuta O(V ) veces.
Cada bucle foreach, por otro lado, necesita un tiempo igual al tama~
no de de la lista de
adyacencia del vértice correspondiente. Es decir, el número de veces que se ejecuta cada
ciclo foreach depende de la naturaleza del grafo. Pero este ciclo se ejecuta una vez
para cada vértice, y sabemos que el tama~
no total de las listas de adyacencias es igual al
número de arcos. Es decir, aún si no sabemos cuantas veces las lineas 13--18 se
ejecutarán para cada ciclo, sabemos que en total se ejecutarán un número de veces O(E).
Formalmente, la ejecución del bucle foreach para el vértice u toma un tiempo O(|ady(U )|)
y, siendo
X
|ady(U )| = |E|
(3.62)
u∈V
el tiempo necesario para ejecutar el bucle en todos los vértices es O(E).
el tiempo total de ejecución del algoritmo BFS es O(V + E).
3.9.2
Por lo tanto,
Corrección del algoritmo
El algoritmo BFS visita todos los vértices alcanzables desde la fuente s, determina el
camino más corto entre la fuente y cada vértice y construye, con estos caminos, un árbol
que contiene todos los vértices visitados. La distancia de camino más corto (shortest
path distance) entre la fuente s y un vértice v se indicará con δ(s, v) y se define como el
número mı́nimo de arcos en todos los caminos entre s y v, o ∞ si no existe camino entre s
y v (se vea la definición (2.27), a la página 22. Un camino entre s y v consistente en
δ(s, v) arcos es un camino más corto (shortest path).
Antes de demostrar el teorema fundamental de esta sección, que demuestra que el algoritmo
BFS calcula el camino más corto entre s y cualquier vértice, será necesario demostrar
unos cuantos lemas preliminares. En estos lemas siempre se hará referencia a un grafo
G = (V, E), dirigido o no dirigido (el teorema vale en ambos casos).
Lemma 3.9.1. Sea s un vértice de G.
Para todo arco (u, v) ∈ E se cumple
δ(s, v) ≤ δ(s, u) + 1
(3.63)
Demostración. Si u es alcanzable, v también es alcanzable, y δ(s, v) + 1 es la distancia
entre s y u que se consigue recorriendo el camino más corto de s a v y luego cruzando el
arco (u, v). El camino más corto de s a v no puede ser (por definición) más largo que este
recorrido. Si u no es alcanzable desde s, δ(s, u) = ∞, y el lema se cumple
independientemente del valor δ(s, v).
grafos
41
Lemma 3.9.2. Supongamos que se ejecute BFS en G con fuente s. Al final del algoritmo,
para todo vértice v se cumple d[v] ≥ δ(s, v), donde d[v] es el valor de la distancia
calculado por el algoritmo BFS.
Demostración. La demostración se realiza por inducción sobre el número de veces que se ha
introducido un vértice en la cola Q, es decir, sobre el número de vértices que se
introducen en la cola a lo largo del algoritmo. Sea q este número.
i) Sea q = 0; esta es la situación en que el algoritmo se encuentra tras ejecutar la lı́nea
9. En este caso d[s] = 0 = δ(s, s) y, para cada vértice v 6= s, d[v] = ∞ ≥ δ(s, v).
ii) Supongamos que el lema se cumple para los primeros q vértices que se han introducido
en la cola, y que v es el vértice que se encuentra en este punto durante la búsqueda
en el entorno de un vértice u. Para llegar a v, el vértice u se ha extraı́do de la
cola en el paso 11, es decir, se ha insertado en la cola como uno de los primeros q
vértices. La hipótesis inductiva nos dice que el lema se cumple para u, o sea que
d[u] ≥ δ(s, u). Entonces:
d[v]
= d[u] + 1;
≥ δ(s, u) + 1
≥ δ(s, v)
(asignado en el paso 15)
(hipótesis de inducción)
(lema 3.9.1)
(3.64)
Una vez que el vértice v se ha introducido en la cola, no se va a introducir nunca
más, porque en el paso 14 se le ha cambiado su estado, y el if del paso 13 sólo
permite la introdución de vértices cuyo estado es N, por tanto el valor d[v] no se
vuelve a cambiar y al final del algoritmo sigue cumpliéndose d[v] ≥ δ(s, v).
Lemma 3.9.3. Supongamos que, en un determinado momento de la ejecución de BFS, sea
Q = [v1 , . . . , vr ], siendo v1 la cabecera de la cola fifo y vr el último elemento que se ha
insertado. Entonces,
i) d[vr ] ≤ d[v1 ] + 1 y
ii) para i = 1, . . . , r − 1 se cumple d[vi ] ≤ d[vi+1 ].
Demostración. La demostración es por inducción sobre el número de operaciones que se
efectúan en la cola. Inicialmente, la cola sólo contiene s, y el lema se cumple
banalmente.
Supongamos ahora que el lema se cumple tras q operaciones. Debemos demostrar que el lema
sigue válido tras la operación q + 1, la cual puede ser la inserción o la extracción de un
vértice.
Consideremos primero el caso en que la operación número q + 1 es la extracción de un
vértice de la cola. Si después de esta extracción la cola queda vacı́a, el lema se cumple
banalmente. Si no, v2 es la nueva cabecera. Para el punto i), se ha
d[vr ] ≤ d[v1 ] + 1 ≤ d[v2 ] + 1 (la segunda relación deriva de la hipótesis de inducción); la
propiedad ii) no cambia a causa de esta extracción, siendo válida tras la operación q,
sigue válida tras la q + 1.
Consideremos ahora el caso en que se está introduciendo un vértice. En este caso,
tenemos que considerar el historial del estado de la cola. Cuando se inserta un vértice
cahiers d’informatique
42
v en la cola (en el paso 17), se está analizando el entorno de un vértice u y,
claramente, u es el último vértice que se ha extraı́do de la cola. La secuencia de
operaciones en la cola, en este punto, es la siguiente:
a)
b)
u
[u, v1 , . . . , vp ]
[v1 , . . . , vp ]
.
.
.
situación antes de extraer u
justo después de extraer u
c)
d)
u
u
[v1 , . . . , vp , . . . , vr ]
[v1 , . . . , vr , vr+1 = v]
se insertan unos vértices del entorno de u
ésta es la situación en el paso q
éste es el paso q + 1
(En la última lı́nea se ha introducido el vértice v en la cola llamándolo vr+1 por
coherencia con la notación previa, y para conseguir una notación más clara en lo
siguiente.) Por la hipótesis de inducción, hasta la operación número q las propiedades
i) y ii) se cumplen. Es decir, i) y ii) se cumplen para las colas en a), b), y c). En
particular, la cola c) garantiza que la propiedad ii) se cumpla para los vértices
v1 , . . . , vr . La inserción de vr+1 en d) no cambia los valores de distancia de estos vértices
ası́ que en la cola d) sigue cumpliéndose
d[v1 ] ≤ d[v2 ] ≤ · · · ≤ d[vr ]
(3.65)
Para demostrar el teorema, tenemos que demostrar que i) y ii) se cumplen en la cola d).
Para demostrar i) tenemos que demostrar que
d[vr+1 ] ≤ d[v1 ] + 1;
(3.66)
para demostrar ii), gracias a la validez de (3.65), será suficiente demostrar que
d[vr ] ≤ d[vr+1 ]
(3.67)
Observamos que vr+1 se analiza como parte del entorno de u y por tanto, en el paso 15, se
le asigna un valor
d[vr+1 ] = d[u] + 1.
(3.68)
Consideremos ahora la situación en a). Por la hipótesis de inducción, vale d[u] ≤ d[v1 ] y
por tanto
d[vr+1 ] = d[u] + 1 ≤ d[v1 ] + 1
(3.69)
Esto demuestra que la propiedad i) se cumple para la cola d).
Para la demostración del punto ii), tenemos que distinguir dos casos. Supongamos primero
que vr+1 es el primer vértice del entorno de u que se inserta en la cola. Entonces las
colas a) y c) coinciden, y la nueva cola es [v1 , . . . , vp , vr+1 ]. Ya que la propiedad ii) se
cumple en la cola a), se da
d[vr ] ≤ d[u] + 1 = d[vr+1 ]
(3.70)
y el punto ii) es demostrado. Supongamos ahora que vr+1 no es el primer vértice del
entorno de u que se ha introducido en la cola, sino que antes se han introducido
vp+1 , . . . , vr . Como todos estos vértices se encuentran en el entorno de u, en el paso 15
todos han recibido el mismo valor de d, o sea
d[vp+1 ] = d[vp+2 ] = · · · = d[vr ] = d[vr+1 ] = d[u] + 1
la igualdad d[vr ] = d[vr+1 ] demuestra el punto ii) en este último caso.
(3.71)
grafos
43
Este lema establece que, en cada momento, sólo hay dos valores distintos de d en la cola.
Si hubiera, digamos, 3, entonces a causa de la condición ii) serı́a d[vr ] ≥ d[v1 ] + 2, y la
condición i) no se cumplirı́a.
Este lema es importante para entender el funcionamiento del algoritmo porqué nos
garantiza que el algoritmo no "profundiza" demasiado. En particular, nos garantiza que,
antes de insertar un vértice a distancia d + 2 de la fuente, se habrán extraido todos los
vértices a distancia d. Es decir, el algoritmo funciona en "capas": se extraen los
vértices a distancia d y se van insertando sus vecinos a distancia d + 1. Sólo cuando
todos los vértices a distancia d se hayan extraido, se empezarán a extraer los vértices a
distancia d + 1, insertando contemporaneamente en la cola sus vecinos a distancia d + 2, y
ası́ sucesivamente.
Lemma 3.9.4. Ejecútese el algoritmo BFS en el grafo G a partir de una fuente s ∈ V [G], y
sea v ∈ V [G] un vértice no alcanzable desde s. Entonces, a lo largo de su ejecución el
algoritmo BFS no recorre v, o sea, no ejecuta las operaciones 14--17 sobre v.
Demostración. Supongamos por contradicción que el algoritmo recorra v. Siendo v no
alcanzable desde s, es δ(s, v) = ∞ y, por el lema 3.9.2, d[v] = ∞. Si el algoritmo
recorriera v entonces, al paso 15 le asignarı́a un valor d[v] = d[u] + 1 = ∞, lo que supone
que también u, el antepasado de v en el árbol BF, tendrı́a d[u] = ∞.
Si v se ha visitado y se ha puesto π[v] = u en el paso 16, esto se ha hecho como parte del
análisis del entorno de u después que u se ha extraı́do de la cola, por lo tanto, u
también se ha visitado y se ha puesto d[u] = ∞. Siguiendo el mismo razonamiento, el
antepasado de u también se ha visitado y tiene distancia ∞, y ası́ sucesivamente.
Sea x el primer vértice al que se le asigna d[x] = ∞. Siendo d[s] = 0, tiene que ser
x 6= s. Entonces, el valor d[x] fue asignado en el paso 15 y, razonando como en el caso de
v, para que esto suceda, el antepasado de x, y, tiene que tener d[y] = ∞ y este valor
tiene que haberse asignado al paso 15. Esto contradice la hipótesis de que x sea el
primer elemento al que se asigna d[x] = ∞. Luego, la hipótesis que el algoritmo recorre
v es falsa.
Lemma 3.9.5. Ejecútese el algoritmo BFS en el grafo G a partir de una fuente s ∈ V [G].
Para cada v ∈ V [G], las operaciones siguiente se ejecutan como máximo una vez a lo largo
de la ejecución del algoritmo:
i) se pone estado[v] ← V ;
ii) se pone d[v] = δ(s, v);
iii) si v 6= s se pone π[v] = u, con u ∈ V [G] y δ(s, u) = δ(s, v) − 1;
iv) se inserta v en la cola.
(es decir, los pasos 14--17 del algoritmo se ejecutan como máximo una vez para cada
vértice).
Demostración. Si v no es alcanzable desde s entonces, por el lema 3.9.4, no se recorre, y
las operaciones i)--iv) (pasos 14--17) no se ejecutan nunca.
Si v es alcanzable desde s y las operaciones se ejecutan una vez, podemos razonar como en
la demostración del lema 3.9.2: antes de poner v en la cola, su estado se pone igual a
V y, desde entonces, el if de la lı́nea 13 impidirá que se vuelvan a ejecutar las lı́neas
14--17 para v.
44
cahiers d’informatique
Una vez establecidos estos lemas, podemos proceder a demostrar el teorema principal de
esta sección, o sea la correción del algoritmo BFS.
Theorem 3.9.1. Supóngase que el algoritmo BFS se ejecute en el grafo G a partir de una
fuente s. Entonces, a lo largo de su ejecución, BFS recorre todos (y sólo) los vértices
accesibles desde s. Al final, para cada v ∈ V , se cumple d[v] = δ(s, v). Para cualquier v
accesible desde s con v 6= s, uno de los caminos más corto4 desde s a v es el camino más
corto desde s a π[v] seguido por el arco (π[v], v).
Demostración. Si el vértice v no es alcanzable desde s no se recorre por el lema 3.9.4 y
el lema 3.9.2 garantiza que, al final, d[v] = δ(s, v) = ∞.
Consideramos entonces sólo los vértices alcanzables desde s. Sea Vk = {v ∈ V [G]|δ(s, v) = k}
el conjunto de vértices a distancia k de s. Queremos demostrar que para cada v ∈ Vk las
cuatro operaciones del lema 3.9.5
i) se pone estado[v] ← V ;
ii) se pone d[v] = δ(s, v);
iii) si v 6= s se pone π[v] = u, con u ∈ V [G] y δ(s, u) = δ(s, v) − 1;
iv) se inserta v en la cola.
se ejecutan una y sólo una vez. El lema 3.9.5 demuestra que estas operaciones se
ejecutan como máximo una vez. Ahora tenemos que demostrar que se ejecutan al menos una
vez para todo v ∈ Vk . La demostración se hace por inducción sobre k.
Para k = 0, el único vértice a distancia 0 de la fuente es la misma fuente, por tanto
V0 = {s}. La lı́nea 7 pone el estado de s a V , la lı́nea 8 pone d[s] = 0, y la lı́nea 9
inserta s en la cola (la operación iii) no nos interesa en este caso, ya que se ejecuta
sólo si v 6= s): el teorema queda demostrado para k = 0.
Supongamos ahora que las propiedades i)--iv) se cumplen para todo u ∈ Vk−1 .
Nótese que la cola no se encuentra nunca vacı́a hasta el final del algoritmo y que, por el
lema 3.9.5, una vez que un vértice se ha insertado en la cola, sus atributos d[u] y π[u] no
cambian. Entonces, por el lema 3.9.3, si los vértices se insertan en la cola en el orden
u1 , . . . , un , se cumple
d[u1 ] ≤ d[u2 ] ≤ · · · ≤ d[un ]
(3.72)
es decir, los vértices se insertan en la cola en orden no decreciente de distancia.
Consideremos v ∈ Vk arbitrario. Por el lema 2, al final del algoritmo se cumple d[v] ≥ k
y, ya que el valor d[v] no se asigna más de una vez, d[v] ≥ k se cumple a lo largo de la
ejecución. Para la hipótesis de inducción, para todo u ∈ Vk−1 es d[u] = k − 1 entonces,
para la (3.72), si v se visita---y consecuentemente se inserta en la cola---se visita
sólo tras haber visitado todo vértice u ∈ Vk−1 .
Siendo δ(s, v) = k hay un camino de k arcos entre s y v y este camino, justo antes de
llegar a v, tiene que pasar por un vértice u que se encuentra a distancia k − 1 de s, es
decir, por un vértice u ∈ Vk−1 tal que (u, v) ∈ E. En general, existirán varios vértices
que cumplen con estos requisitos; asumamos que u es el primero de estos vértices que se
visita y se pone en la cola.
4 En un grafo pueden existir varios caminos de distancia mı́nima entre dos vértices:
encontrará uno de ellos.
el algoritmo BSF
grafos
45
El algoritmo termina sólo cuando la cola se encuentre vacı́a, por lo cual antes del final
del algoritmo el vértice u tendrá que extraerse de la cola, su entorno se analizará, y se
descubrirá el vértice v. ¿Cuál es el estado de v en este momento? Para que el estado de
v fuera V , v tendrı́a que estar en el entorno de un vértice que ya se ha extraı́do de la
cola, pero:
i) v no está en el entorno de ningún w ∈ Vj con j < n − 1. Si ası́ fuera, v se encontrarı́a
en Vj+1 , contra la hipótesis que v se encuentre en Vk ;
ii) v no se encuentra en el entorno de ningún w ∈ Vk−1 que ya se ha extraı́do de la cola
porque hemos asumido que u es el primer vértice de Vk−1 en cuyo entorno se encuentra
v que se extrae de la cola.
Entonces, cuando se extrae u de la cola, se cumple estado[V ] = N . En este caso, la
condición del if de la lı́nea 13 se cumple, y las lı́neas 14--17 se ejecutan, cumpliendo
las operaciones i)--iv).
Ya que v es arbitrario, la propiedad se cumple para todo v ∈ Vk . Esto termina la
demostración por inducción. Esta y el lema 3.9.5 garantizan que las operaciones i)--iv)
se ejecutan una y sólo una vez para cada vértice v alcanzable desde s.
La operación ii) demuestra que, al final del algoritmo, es d[v] = δ(s, v). Para terminar la
demostración observamos que, siendo π[v] ∈ Vk−1 , se puede encontrar el camino más corto
hasta v tomando el camino más corto desde s hasta π[v] (con k − 1 arcos) y luego cruzando
el arco (π[v], v), por un total de k arcos.
3.10
Árboles BF
Tras la ejecución del algoritmo BFS, los vértices alcanzables desde s y los arcos (π[v], v),
forman un sub-grafo de G con propiedades importantes. Las propiedades que nos interesan
se resumen en la definición siguiente:
Definition 3.10.1. Dado un grafo G = (V, E) y un vértice de salida s, un sub-grafo de G
G0 = (V 0 , E 0 ) es un árbol BF si G0 es un árbol con raı́z s, V 0 está formado por todos los
vértices accesibles desde s y para todo v ∈ V 0 el camino (único) desde s a v en G0 es
además un camino de distancia mı́nima entre s y v en G.
El teorema siguiente guarantiza que la estructura construida por el algoritmo BFS cumple
estas propiedades.
Theorem 3.10.1. Dado un grafo G = (V, E), y un vértice s ∈ V , sea el grafo Gπ = (Vπ , Eπ )
definido por
Vπ
Eπ
= {v ∈ V : π[v] 6= nil} ∪ {s}
= {(π[v], v) ∈ E : v ∈ Vπ − {s}}
donde el atributo π[v] se ha asignado durante la ejecución de BFS(G, s).
un árbol BFS.
(3.73)
Entonces, Gπ es
Demostración. G es un árbol, porque es conexo (se puede derivar de la demostración de la
correción del algoritmo BFS) y |Eπ | = |Vπ | − 1 (propiedad iv del teorema 2.6.2 a la página
27). La linea 16 del algoritmo BFS pone π[v] = u si y sólo si (u, v) ∈ E y δ(s, u) < ∞, o
cahiers d’informatique
46
sea, si y sólo si v es accesible desde s, ası́ que Vπ está formado por todos los vértices
accesibles desde s.
Siendo Gπ un árbol, hay un solo camino desde s a cada v ∈ Vπ , y aplicando inductivamente
la parte final de la demostración del teorema precedente, se demuestra que el camino es
mı́nimo.
La función print path recibe un grafo al que se ha aplicado el algoritmo BFS, e imprime
el camino más corto entre la fuente s y un vértice dado v.
print path(G, s, v)
1.
if v = s then print s
2.
else
3.
if π[v] = NIL then
4.
print "no path";
5.
else
6.
print path(G, s, π[v]);
7.
print v;
8.
fi
el tiempo de ejecución del algoritmo es lineal en el número de vértices en el camino más
corto entre s y v, ya que cada llamada es para un camino más corto de un vértice. En el
peor de los casos, el tiempo de ejecución es O(V ).
3.10.1
Variantes
Definición implı́cita del estado
En la práctica, es posible simplificar un poco la estructura de datos que se utiliza para
la ejecución del algoritmo BFS. La introducción del estado de un vértice facilita la
demostración de las propiedades del algoritmo, pero podemos observar que es, de alguna
forma, redundante. Al principio del algoritmo se pone para cada vértice d[v] = ∞ y
estado[v] = N . El teorema precedente nos dice que el estado de v cambia una sola vez a lo
largo de la ejecución y cuando se cambia el estado de N a V también se pone d[v] = δ(s, v).
Es decir, las dos proposiciones
estado[v] = N
estado[v] = V
⇔ d[v] = ∞
⇔ d[v] < ∞
(3.74)
(3.75)
se cumplen a lo largo de la ejecución. Entonces, es posible usar el valor d[v] como
indicador de estado: si d[v] = ∞ todavı́a no se ha visitado el vértice v (su estado es
N ), mientras que si d[v] < ∞ el vértice ya se ha visitado (su estado es V ). El
algoritmo se transforma como sigue:
BFS-NoEst(G, s)
1.
Q: queue of vertex;
2.
foreach u in V[G] do
3.
d[u] ← ∞;
4.
π[u] ← nil;
5.
od
6.
d[s] ← 0;
grafos
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
47
Q.insert(s);
while ¬empty(Q) do
u ← Q.extract;
foreach v in u.ady do
if d[v] < ∞ then
d[v] ← d[u] + 1;
π[v] ← u;
Q.insert(v);
fi;
od
od
Determinación de todos los caminos
Ya hemos observado precedentemente que en general el camino mı́nimo de la fuente s a un
vértice v no es único. Por ejemplo, en el grafo
89:;
?>=<
t
? ???

??


??

??



?>=<
89:;
89:;
?>=<
s@
?~ v
@@
@@
~~
@@
~~
~
@@ ~~
~
89:;
?>=<
u
(3.76)
?>=<
/ 89:;
w
hay un camino mı́nimo (de longitud 1) de s a t y de s a u, pero hay dos caminos mı́nimos,
de longitud 2, de s a v---el camino [s, t, v] y el camino [s, u, v]---y dos, de longitud 3, de s
a w---los caminos [s, t, v, w] y [s, u, v, w]. En este caso el algoritmo BFS retorna uno de los
caminos. El camino elegido depende del orden en que se analizan los vecinos de un
vértice en el foreach de la lı́nea 12 (la lı́nea 10 en el algoritmo modificado BFS-NoEst).
Una modificación relativamente sencilla de BFS permite determinar todos los caminos
mı́nimos. La idea es guardar, para cada vértice, en vez de un solo antepasado, una lista
de antepasados, uno para cada camino mı́nimo que converge en el vértice. En el grafo
(3.76), por ejemplo, se pondrı́a
π[s] = []
π[t] = [s]
π[u] = [s]
π[v] = [t, u]
π[w] = [v]
(3.77)
(3.78)
Supongamos que se ha extraı́do de la cola un vértice u a distancia d[u] de la fuente y que
se está analizando su entorno. Durante este análisis se encuentra el vértice v en estado
V . Esto quiere decir que ya se ha encontrado un camino corto hasta v y por tanto, en el
algoritmo estándar, no se vuelve a visitar v y no se cambia el valor de π[v]. Por contra,
en este caso, tenemos que fijarnos en el valor d[v]. Si d[v] = d[u] + 1, entonces u nos
proporciona otro camino mı́nimo de s a v (la longitud del camino de s a v que pasa por u
es igual a la longitud del camino hasta u más el arco (u, v), o sea d[u] + 1 que, siendo
48
cahiers d’informatique
igual al valor d[v] es también igual a δ(s, v).), por tanto, se a~
nade u a la lista π[u] de
antepasados de v. Si el estado de v es N en el momento en que se descubre, se procede
como en el algoritmo estándar.
BFS-All(G, s)
1.
Q: queue of vertex;
2.
foreach u in V[G] do
3.
estado[v] ← N;
4.
d[u] ← ∞;
5.
π[u] ← [];
6.
od
7.
estado[s] ← V;
8.
d[s] ← 0;
9.
Q.insert(s);
10. while ¬empty(Q) do
11.
u ← Q.extract;
12.
foreach v in u.ady do
13.
if estado[v] = N then
14.
estado[v] ← V;
15.
d[v] ← d[u] + 1;
16.
π[v] ← u;
17.
Q.insert(v);
18.
elseif d[v] = d[u] + 1 then
19.
π[v] ← append(π[v], [u]);
20.
fi
21.
od
22. od
Las operaciones que se han a~
nadido, lı́neas 18 y 19, se ejecutan, el el peor de los casos,
un número de veces igual a la suma de los tama~
nos de todas las listas de adyacencia, o
sea, un número de veces O(E). Por tanto esta modificación no cambia la complejidad del
algoritmo, que sigue siendo O(V + E).
A continuación se demuestran las propiedades del algoritmo, y su corrección.
Lemma 3.10.1. En el momento en que se ejecuta la lı́nea 19 para un vértice v 6= s, el
vértice v se encuentra en la cola.
Demostración. El elseif de la lı́nea 18 implica que la lı́nea 19 se puede ejecutar sólo si
el estado de v es V ya que, si el estado es N se ejecutará la primera parte del if. El
único lugar donde se pone estado[v] ← V es la lı́nea 14, por tanto la lı́nea 19 se puede
ejecutar para v sólo después que se hayan ejecutado las lı́neas 14--17, o sea, después que
v se haya insertado en la cola.
Si v ya se ha sacado de la cola, razonando como en la demostración del teorema 3.9.1
notamos que para cada vértice u que se extrae de la cola después de v, se cumple
d[u] ≥ d[v]. La condición de la lı́nea 18 se cumple si d[v] = d[u] + 1, que supone d[u] < d[v] y
que, como consecuencia de la observación precedente, no se verificará si u se ha extraido
de la cola después de v.
Theorem 3.10.2. Sea G un grafo en que se ejecuta el algoritmo BFS-All con fuente
p
/u
/ v si, y sólo si,
s ∈ V [G]. Para cada v ∈ V [G] existe un camino mı́nimo s
u ∈ π[v].
grafos
49
Demostración. Las lı́neas que se han a~
nadido, 18 y 19, no afectan d[v] ni el primer
elemento de π[v]: para estos elementos sigue valido el teorema 3.9.1. En particular, si
v ∈ Vk donde Vk es definido como en la demostración del teorema 3.9.1, se cumple d[v] = k y
head(π[v]) ∈ Vk−1 .
Tenemos que demostrar entonces que los demás elementos de π[v] también definen caminos
mı́nimos y que cada camino mı́nimo hacia v pasa por ellos.
Si v no es alcanzable desde s entonces, por el teorema 3.9.1, las lı́neas 12--14 no se
ejecutan, v no se inserta en la cola y, para el lema 3.10.1, la lı́nea 19 nunca se ejecuta
para v, ası́ que al final π[v] = [].
Sea ahora v ∈ Vk .
El teorema se demuestra por inducción sobre k.
Para k = 0, V0 = {s} y, ya que d[s] = 0 y para todo u 6= s es d[u] > 0, la condicción de la
lı́nea 18 nunca se cumple para s y la lı́nea 19 nunca se ejecuta. El teorema se cumple
banalmente.
Sea ahora el teorema cierto para todo v ∈ Vj , j ≤ k − 1, y consideremos v ∈ Vk arbitrario.
p
/ v , con u = head(π[v])
/u
El teorema 3.9.1 garantiza que d[v] = k y que el camino s
es mı́nimo. Consideremos los dos casos del "si" y "sólo si".
/ v . Nos
/u
a) Sea u ∈ π[v]: tenemos que demostrar que existe un camino mı́nimo s
interesa sólo el caso u 6= head(π[v]). En este caso, u se ha introducido en la lı́nea
19 y, por la lı́nea 18, d[u] = k − 1. Por el teorema 3.9.1 esto implica δ(s, u) = k − 1,
por lo cual u ∈ Vk−1 , la hipótesis inductiva se cumple para u y el algoritmo
p
/ u mı́nimos, con |p| = k − 1. Entonces, si p0 es el camino
encuentra todo camino s
p + [(u, v)] se cumple |p| = k: todo camino hasta v que pasa por u es mı́nimo. Ya que v
es arbitrario, y u es cualquier elemento de π[v], concluimos que todo camino que el
algoritmo encuentra hasta un vértice de Vk es mı́nimo.
p
p0
/ v sea un camino mı́nimo hasta v ∈ Vk , con |p0 | = k, y sea u el
p
/ v . Tenemos que demostrar que u ∈ π[v]. Si
/u
predecesor de v en p0 : s
|p0 | = k entonces |p| = k − 1 y, siendo un sub-camino de un camino mı́nimo también
mı́nimo, es u ∈ Vk−1 . Por el teorema 3.9.1, u se ha sacado de la cola antes que v y,
al explorar el entorno de u se ha cruzado el arco (u, v) y encontrado v.
b) Supongamos que s
Si en este momento es estado[v] = N se ejecutan las lı́neas 14--17, y se pone
u = head(π[v]).
Si no, a este punto d[v] ya se ha asignado y, siendo p0 mı́nimo, se ha puesto d[v] = k.
Cuando se saca u de la cola, siendo u ∈ Vk−1 también se ha puesto d[u] = k − 1 (d[u] se
asigna cuando u se inttroduce en la cola), por lo cual la condición de la lı́nea 18
se cumple, y se inserta u en π[v]. La
La arbitrariedad de v garantiza que el teorema es cierto para todo v ∈ Vk .
cahiers d’informatique
50
3.10.2
Ejercicios
1. Ejecutar el algoritmo BFS en los grafos siguiente, tomando como fuente el vértice
marcado doble. Se asume que las listas de adyacencias están ordenadas
alfabeticamente por nombre de vértices.
89:;
?>=<
a G
GG
ww
w
GG
ww
GG
w
w
G
w
89:;
?>=<
89:;
?>=<
c3
b3
33
3
33
33
3
89:;
?>=<
89:;
?>=<
89:;
?>=<
7654
0123
89:;
?>=<
g
e
f
d
?>=<
89:;
89:;
a = ?>=<
b
==
==
==
?>=<
89:;
?>=<
89:;
0123
7654
c
d
89:;
?>=<
b
89:;
?>=<
c
89:;
?>=<
d
89:;
?>=<
e
89:;
?>=<
f
?>=<
/89:;
b II
II
I$
89:;
?>=<
: e
u
u
u
u
u
89:;
?>=<
?>=<
/ 89:;
c
d
?>=<
()*+
/.-,
/89:;
b II
II
I$
89:;
?>=<
: e
u
u
u
u
u
89:;
?>=<
?>=<
/ 89:;
c
d
89:;
?>=<
()*+
/.-,
a
)89:;
?>=<
?>=<
89:;
?>=<
/89:;
c
b i
d
O
J V
89:;
?>=<
?>=<
89:;
?>=<
89:;
?>=<
/ 89:;
g
e
f
@h
89:;
?>=<
89:;
?
>=<
89:;
?
>=<
89:;
?>=<
/
/
j
i
k
l
w
89:;
?>=<
a
O
89:;
?>=<
a
89:;
/?>=<
b II
II
I$
?>=<
89:;
()*+
e
: /.-,
u
u
u
u
u
?>=<
89:;
>=<
/ ?89:;
c
d
?>=<
89:;
a
?>=<
89:;
a
)?>=<
89:;
89:;
()*+
/.-,
?>=<
89:;
/?>=<
c
b i
d
O
J V
?>=<
89:;
89:;
?>=<
89:;
?>=<
89:;
/ ?>=<
g
e
f
@h
?>=<
89:;
?
89:;
>=<
?
89:;
>=<
?>=<
89:;
/
/
j
i
k
l
89:;
?>=<
()*+
/.-,
a
w
?>=<
89:;
()*+
/.-,
a i
O
?>=<
89:;
a
)?>=<
89:;
>=<
>=<
/?89:;
/ ?89:;
d
@ bO =^ = @ cO
@
=
=
==
= ?>=<
89:;
?>=<
89:;
?>=<
89:;
?>=<
89:;
g o
e o
f o
h
?>=<
89:;
a o
89:;
?>=<
a
?>=<
89:;
89:;
/?>=<
c
b 5Z
O 5 D O dHHHH
5
H
55
?>=<
89:;
g
55
C
55 89:;
?>=<
89:;
?>=<
89:;
()*+
/.-,
/ ?>=<
e
f
d o
?>=<
?>=<
/89:;
/89:;
c
b
O
O
89:;
?>=<
?/.-,
()*+
>=<
?>=<
/ 89:;
/ 89:;
e
f
d g
( 89:;
?>=<
()*+
/.-,
?>=<
?>=<
/89:;
c
b = 89:;
d
O
O
=
=
=
=
=
= 89:;
?>=<
89:;
?>=<
89:;
?>=<
89:;
?>=<
g
e
f
7 h
89:;
?>=<
a o
89:;
?>=<
?>=<
/89:;
c
b 5Z
O 5 D O dHHHH
5
H
55
89:;
?>=<
()*+
/.-,
g
55
C
55 89:;
?>=<
89:;
?>=<
?>=<
/ 89:;
e
f
d o
89:;
?>=<
89:;
?>=<
a8
b
88ssss
s
8
s 8 89:;
89:;
?>=<
7654
0123
88 ?>=<
c
d
t8t8
t
t
t
t
89:;
?>=<
?>=<
e JJ 89:;
f
JtJtt
J
t
J
tt 89:;
89:;
?>=<
?>=<
g
h
89:;
?>=<
a o
89:;
?>=<
/.-,
()*+
?>=<
/89:;
c
b 5Z
O 5 D O dHHHH
5
H
55
89:;
?>=<
g
55
C
55 89:;
?>=<
89:;
?>=<
?>=<
/ 89:;
e
f
d o
2. Sea G un grafo no dirigido; llamen Gπ (s) el árbol BFS que se construye ejecutando el
algoritmo BFS en G con fuente s. ¿Que propiedad expresa la fórmula lógica
siguiente?
∀u, v, ∈ V [G] u ∈ V [Gπ (v)] ⇔ v ∈ V [Gπ (u)]
(3.79)
¿Es cierta la propiedad?
Demostrar que si o dar un contra-ejemplo.
grafos
51
3. ¿Como cambia la respuesta a la pregunta precedente si G es dirigido?
la propiedad se cumple o dar un contra-ejemplo.
Demostrar si
4. El árbol marcado en el grafo siguiente se compone de caminos mı́nimos. Demostrar
que, a pesar de esto, el árbol no es un árbol BFS, o sea que el algoritmo BFS con
fuente a no encontrará este árbol, independientemente del órden en que los vértices
aparecen en las listas de adyacencia.
?>=<
89:;
?>=<
+389:;
c
uu 6> b 66 D
u
u
u
66 uuuuu
89:;
?>=<
()*+
/.-,
6
a III
66
IIIIII
I ( 6
89:;
?>=<
?>=<
+3 89:;
e
d
(3.80)
5. Un grafo G es bipartito si se puede dividir V [G] en dos sub-conjuntos separados:
V [G] = V1 ∪ V2 , y V1 ∩ V2 = ∅ tal que todo arco del grafo une un vértice de V1 con uno
de V2 , mientras dos vértices de V1 o dos de V2 no están conectado. Por ejemplo, el
grafo siguiente es bipartido:
89:;
?>=<
89:;
?>=<
a8
(3.81)
b
88ssss
ss88 89:;
89:;
?>=<
88 ?>=<
c
td
tt88
t
tt 89:;
89:;
?>=<
?>=<
e
f
Usando el algoritmo BFS, se escriba un algoritmo para determinar si un grafo es
bipartido.
(Sugerencia: costruir el árbol BFS, y fijarse donde están los vértices v con d[v]
par y donde están los con d[v] impar. ¿Que caracterı́sticas tienen los valores de d
de los vértices unidos por los arcos que hacen el grafo bipartido?)
cahiers d’informatique
52
3.11
Búsqueda en profundidad
El algoritmo de búsqueda en anchura (BFS) visita los vértices de un grafo en orden de
distancia desde la fuente: todo vértice a distancia k de la fuente se visita antes de
visitar cualquier vértice a distancia k + 1. Cuando se analiza un vértice u se ponen sus
vecinos---los que todavı́a no se han visitado---en la cola, y todos estos vecinos se
analizarán antes de empezar a analizar vértices más distantes. Como se ha demostrado en
la sección precedente, la clave para el funcionamiento del algoritmo BFS se encuentra en
las operaciones de la cola FIFO. La estrategia de la búsqueda en profundidad es de alguna
forma dual: en vez de analizar entornos ordenadamente, el algoritmo va de vértice en
vértice sin explorar los entornos exaustivamente, "profundizando"; visitando cada vez un
hijo del último vértice visitado hasta llegar a un "callejón sin salida": un vértice
cuyos vecinos han sido todos visitados. Cuando esto occurre, el algoritmo re-visita al
revés los vértices ya visitados hasta encontrar uno que todavı́a tiene vecinos no
visitados (backtracking). Ya como en la búsqueda en anchura, el proceso sigue hasta que
se hayan visitado todos los vértices alcanzables desde la fuente. Si en este punto
quedarán en el grafo vértices por visitar, se eligirá una nueva fuente y se repitirá el
algoritmo: el resultado de la búsqueda en profundidad es por tanto un bosque, mientras
que el de la búsqueda en anchura era un árbol5 .
Debido a las diferencias de estrategia, los árboles encontrados por el algoritmo de
búsqueda en profundidad (árboles DF) son en general diferentes de los encontrados por el
algoritmo BFS (se vean los ejemplos (a), (b), y (c) de la figura 3.11) aúnque a veces los
dos pueden coincidir (como en el ejemplo (d) de la figura 3.11). Consecuentemente, en
general, los árboles encontrados por el algoritmo DFS no contienen caminos mı́nimos de la
fuente a los demás vértices del grafo. Por eso, el algoritmo DFS no se utiliza en
problemas de optimización, sino que se utilizan las propiedades estructurales de los
árboles DF en problemas como la ordenación topológica, la detección de ciclos o la
determinación de componentes conexas.
Tal como el algoritmo BFS, el DFS debe evitar que vértices ya visitados se visiten otra
vez y, por ésto, asocia a cada vértice un indicador de estado. En DFS, el estado de un
vértice puede tomar tres valores: N para un vértice que todavı́a no se ha descubierto, E
para un vértice ya encontrado, y P para un vértice completamente procesado (es decir, un
vértice cuya lista de adyacencia se ha explorado completamente). El algoritmo también
asocia a cada vértice una marca de tiempo. El tiempo se mide en número de vértices
procesados: se marca una unidad de tiempo cada vez que se visita un vértice y cada vez
que se termina de procesar un vértice. Para cada vértice v, se asigna a d[v] el tiempo en
que el vértice es visitado, y a f [u] el tiempo en que el nodo se termina de procesar.
Dado que el contador de tiempo se incrementa dos veces para cada vértice (una vez cuando
se descubre el vértice, y otra cuando se termina su procesamiento), se cumple
1 ≤ d[u] < f [u] ≤ 2|V |,
(3.82)
El vértice u se encuentra en estado N antes del instante d[u], en estado V entre d[u] y
f [u], y en estado P después del instante f [u]. Además, el algoritmo mantiene en π[u] el
antepasado de u en el árbol DFS que se va construyendo. La forma canónica del algoritmo
utiliza un bucle sobre todos los vértices que, cada vez que se encuentra un vértice
5 ¿Por qué en el caso de la búsqueda en profundidad se repite el algoritmo y en el caso de la búsqueda en
anchura no? Las razones son eminentemente practicas: la búsqueda en anchura produce un árbol abarcador
mı́nimo, y se usa en problemas en que nos sirve un árbol con esta propiedad. La búsqueda en profundidad (cuyo
árbol no es mı́nimo) se usa en problemas donde necesitamos visitar todo el grafo, aúnque esto se tenga que
hacer con varios árboles.
grafos
53
Árboles DFS
Árboles BFS
?>=<
89:;
?>=<
+389:;
c
b
xx
yy8@ EEE
x
x
y
x
y
x
EE xxx
y
yyyyy
" xx
?>=<
89:;
()*+
/.-,
?>=<
89:;
aD
d E
DD
z
EE
zzz
DD
z
EE z
D" x zzzz
"
?>=<
?>=<
89:;
+3 89:;
e
f
?>=<
89:;
()*+
/.-,
a
?>=<
89:;
c
89:;
+3?>=<
b GGG
GGGG
GGG
'
89:;
?>=<
e
w
ww
w wwwwwww
>=<
/ ?89:;
d
()*+
+3/.-,o
?


 ()*+ks
/.-,
()*+
/.-,
()*+
/.-,
?>=<
89:;
()*+
/.-,
a
89:;
+3?>=<
b
?>=<
89:;
d
?>=<
89:;
e
()*+
/.-,
89:;
?>=<
?>=<
+389:;
c
b
x
yy8@ EEEEEEEE
x
y
x
y
y
EE& x| xx
yyyyy
89:;
?>=<
89:;
?>=<
()*+
/.-,
a DD
d
z EEE
DDDDD
z
DDD zzz
EE "
& |z
89:;
?>=<
?>=<
+3 89:;
e
f
(a)
89:;
?>=<
c
(b)
/.-,
()*+
89:;
+3?>=<
c
89:;
?>=<
()*+
/.-,
a
(c)
>=<
/ ?89:;
f
()*+
/.-,
?>=<
+389:;
b GGG
GGGG
GGG
'
89:;
?>=<
e
w
w{ www
?>=<
+3 89:;
d
()*+
/.-,
+3 o
?


 ()*+o
/.-,
()*+
/.-,
?>=<
89:;
()*+
/.-,
a
?>=<
+389:;
b
89:;
?>=<
d
89:;
?>=<
e
()*+
/.-,
/.-,
()*+
?>=<
+389:;
c
?>=<
/ 89:;
f
(d)
Figure 3.8: Ejemplos de árboles DFS y BFS. La fuente es marcada con un doble cı́rculo; se
asume que la lista de adyacencia de cada vértice está ordenada alfabéticamente por nombre
de vértices.
cahiers d’informatique
54
todavı́a no visitado, llama al "verdadero" algoritmo de búsqueda, DFS-Visita, que
construye un árbol DF con raı́z en ese vértice. En la versión más común del algoritmo, la
que se presenta aquı́, DFS-Visita es una función recursiva (en la sección 3.11.3 se
presentará una versión no-recursiva):
DFS(G)
1.
foreach u in V[G] do
2.
estado[u] ← N;
3.
π[u] ← NIL;
4.
od
5.
t ← 0;
6.
foreach u in V[G] do
7.
if estado[u] = N then
8.
DFS-Visita(G, u);
9.
fi
10. od
DFS-Visita(G, u)
1.
estado[u] ← E;
2.
t ← t + 1;
3.
d[u] ← t;
4.
for v in u.ady do
5.
if estado[v] = N then
6.
π[v] ← u;
7.
DFS-Visita(v);
8.
fi
9.
od
10. estado[u] ← P;
11. t ← t+1;
12. f[u] ← t;
La figura 3.9
representa su
N (todavı́a no
encuentran en
encuentran en
ilustra el funcionamiento del algoritmo DFS. La forma de un vértice
estado: los vértices representados por un cı́rculo se encuentran en estado
se han visitado); los vértices representados por un doble cı́rculo se
estado E (encontrados) y los vértices representados por un rectángulo se
estado P (toda la lista de adyacencia ha sido visitada).
El algoritmo empieza por el vértice a, que se pone en estado E, asignandole un tiempo
d[u] = 1. Luego (t = 2) se pasa a un vértice de su lista de adyacencia (el vértice d), a
uno de la suya, y ası́ sucesivamente hasta que se encuentre un vértice (el vértice c, a
t = 5) cuya lista de adyacencia ha sido completamente visitada. Este vértice se marca P y
se le pone un tiempo de fin de procesamiento (f [c] = 5). Luego se vuelve al vértice
precedente y, como éste también tiene su lista de adyacencia completamente visitada, se
marca P y se vuelve al nodo precedente (d). Éste tiene todavı́a vértices marcados N en su
lista (el vértice e), por lo cual se procede, a t = 7, a visitar este vértice. Se
continúa ası́ hasta que, a t = 12, todos los vértices estén marcados como procesados.
grafos
55
a
b
c
e
f
a
()*+
/.-,
/.-,
@ABC
GFED
?>=<
89:;
/()*+
1/
>> ? O
B O
>>
>> ()*+
/.-,
()*+
/.-,
()*+
//.-,
/
d
z
d
a
t=1
b
x
@ABC
GFED
?>=<
89:;
@ABC
GFED
?>=<
89:;
1/
3/
>> ;C O
>>
>>
>>
@ABC
GFED
?>=<
89:;
()*+
/.-,
/
2/
d
a
c
a
a
()*+
//.-,
e
t=4
b
f
e
t=7
b
f
d
t=5
a
e
t = 10
e
t=2
b
d
f
a
c
e
t=8
b
f
Figure 3.9:
e
t = 11
d
e
f
f
c
x
@ABC
GFED
89:;
?>=<
+3 4/5
1/
3/6
@@ :B O
= O
@@~~~
|
|
~
|
@
~
~
|
~~~~~~ @@@ |||
@ABC
GFED
89:;
?>=<
()*+
/.-,
()*+
/ |
//.-,
2/
a
c
b
x
@ABC
GFED
89:;
?>=<
+3 4/5
1/
3/6
@@ :B O
@@~~~
|= O
~@@
||
~
|
~
~
|
~~~~~ @ ||
@ABC
GFED
89:;
?>=<
GFED
89:;
?>=<
+3 @ABC
+3 8/9
2/
7/
d
c
w
@ABC
GFED
89:;
?>=<
+3 4/5
1/ E
3/6
EE 8@ O
= O
z
y
EEyyy
z
z
y
E
z
y
zz
yyyy E"
3
+
+3 8/9
2/11
7/10
d
f
a
e
t=3
b
d
f
c
@ABC
GFED
89:;
?>=<
@ABC
GFED
89:;
?>=<
// .-,
()*+
1/
3/
>> ;C O
A O
>>
>>
>> ()*+
@ABC
GFED
89:;
?>=<
()*+
/.-,
/ //.-,
2/
f
e
t=6
b
b
y
c
x
@ABC
GFED
89:;
?>=<
+3 4/5
1/
3/6
@@ :B O
@@~~~
|= O
~@@
||
~
|
~
~
|
~~~~~ @ ||
GFED
89:;
?>=<
@ABC
GFED
89:;
?>=<
GFED
89:;
?>=<
+3 @ABC
+3 @ABC
8/
2/
7/
c
x
@ABC
GFED
?>=<
89:;
+3 4/5
1/ B
3/6
BB 9A O
= O
z
BB||||
z
z
|
B
z
|
zz
|||||| B!
@ABC
GFED
?>=<
89:;
3
+
+3 8/9
2/
7/10
d
a
c
x
@ABC
GFED
89:;
?>=<
GFED
@ABC
89:;
?>=<
+3 4/5
3/
1/
>> ;C O
> O
>>
~
~
>>
~
>> ~~~
@ABC
GFED
89:;
?>=<
()*+
/.-,
()*+
/.-,
/ ~
/
2/
@ABC
?>=<
89:;
3+ GFED
4/
? O
c
x
@ABC
GFED
?>=<
89:;
+3 4/5
1/
3/6
@@ :B O
@@~~~
|= O
~@@
||
~
|
~
~
|
~~~~~ @ ||
()*+
/.-,
@ABC
GFED
?>=<
89:;
GFED
89:;
?>=<
+3 @ABC
/
2/
7/
d
b
()*+
/.-,
// .-,
()*+
GFED
@ABC
89:;
?>=<
1/
:: A O
C O
::
::: /.-,
@ABC
GFED
89:;
?>=<
()*+
()*+
//.-,
/
2/
z
a
v
e
t=9
b
f
c
+3 4/5
3/6
EE y 8@ O
= O
z
EyEyyy
z
z
E
z
y
yyyy E"
zz
3
+
+3 8/9
2/11
7/10
1/12
d
Funcionamiento del algoritmo DFS
e
t = 12
f
cahiers d’informatique
56
3.11.1
Análisis del tiempo de ejecución
Para el análisis del tiempo de ejecución, notamos que los bucles a las lı́neas 1--4 y 6--9
toman un tiempo Θ(V ), excepto por el tiempo necesario para ejecutar la funcion
DFS-Visita. La función DFS-Visita se llama una (y sólo una) vez para cada vértice v ∈ V ,
ya que DFS-Visita se llama sólo si el vértice está en estado N, y en la función el estado
de v se cambia en seguida a E. Durante una llamada a DFS-Visita, el bucle 4--9 se ejecuta
|ady[v]| veces. Como
X
|ady[v]| = Θ(E)
(3.83)
v∈V
el costo total de la ejecución de este ciclo es Θ(E).
O(V + E).
3.11.2
El tiempo de ejecución de DFS es
Propiedades de la búsqueda en profundidad
Tras la ejecución del algoritmo DFS es posible definir el grafo Gπ = (Vπ , Eπ ) formado por
todos los vértices del grafo (recordamos que el algoritmo DFS se ejecuta hasta que se
hayan visitado todos los vértices), y por los arcos (π[u], u) con π[u] 6= N IL. En el caso
del algoritmo BFS este grafo era un árbol: el árbol BF del grafo. En este caso, como ya
se ha observado, el grafo Gπ es un bosque con un árbol construido por cada llamada a
DFS-Visita. Cada árbol de Gπ es un árbol DF del grafo. Observamos que π[v] = u si y
sólo si DFS-Visita(v) se ha llamado durante la exploración de la lista de adyacencia de u
(lı́neas 6 y 7 de DFS-Visita).
Una propiedad importante del algoritmo de búsqueda en profundidad es la propiedad del
paréntesis : si se representa la primera visita a un vértice con un paréntesis abierto y
la finalización del vértice con un paréntesis cerrado, toda ejecución de DFS resulta en
una estructura de paréntesis bien anidados.
Por ejemplo, en la ejecución de figura 3.9, se empieza visitando el primer vértice
(parentésis abierto), el segundo, el tercero y el cuarto (tres paréntesis abiertos),
luego se cierra el cuarto y el tercero, y ası́ siguiendo. El resultado es la estructura
siguiente, donde encima de cada paréntesis se pone el nombre del vértice seguido por + si
se trata de la primera visita, por - si se trata de la finalización:
a+ d+ b+ c+ c− b− e+ f + f − e− d− a−
(
(
(
(
)
)
(
(
)
)
)
)
(3.84)
Esta propiedad, expresada aquı́ de forma intuitiva, se enuncia formalmente en el teorema
siguiente.
Theorem 3.11.1. (del paréntesis) Sean u y v dos vértices cualesquiera de un grafo al que
se ha aplicado el algoritmo DFS, y sean definidos los intervalos Iu = [d[u], f [u]] y
Iv = [d[v], f [v]]. Entonces una (y sólo una) de las siguientes proposiciones se cumple:
i) Iu y Iv son disjuntos;
ii) Iu ⊂ Iv y u es un descendente de v en el árbol DF;
iii) Iv ⊂ Iu y v es un descendente de u en el árbol DF.
Demostración. Supongamos d[u] < d[v]. Hay que considerar dos casos: d[v] > f [u] o d[v] < f [u]
(el algoritmo asigna a cada paso un solo valor de tiempo, ya sea a un "d" o a un "f":
por eso no se puede dar el caso d[v] = f [u], ni ninguna otra igualdad de tiempos).
grafos
57
En el primer caso, por la (3.82), se cumple d[u] < f [u] < d[v] < f [v], y consecuentemente los
intervalos Iu y Iv son disjuntos.
En el segundo caso, si d[u] < d[v] < f [u], v se ha visitado mientras que u estaba todavı́a en
estado E. Entonces, cuando se llama DFS-Visita(v), ya se ha llamado DFS-Visita(u) (en
cuanto d[v] > d[u]) pero la DFS-Visita(u) todavı́a no ha retornado (ya que d[v] < f [u]). Por
tanto, DFS-Visita(v) está anidado en DFS-Visita(u). Esto implica que entre la llamada
DFS-Visita(u) y la llamada DFS-Visita(v) hay una serie de llamadas a DFS-Visita con
vértices que en el árbol DF forman una cadena de u a v. Entonces v es un descendiente de
u. Además, como DFS-Visita(v) está anidado en DFS-Visita(u), retorna antes que
DFS-Visita(u), o sea f [v] < f [v], se modo que Iv ⊂ Iu .
En el caso en que d[v] < d[u] se sigue el mismo razonamiento, invirtiendo los papeles de u
y v.
Corollary 3.11.1. (anidamiento de los intervalos de los descendientes) El vértice v es un
descendiente de u en el árbol DF de un grafo si y sólo si d[u] < d[v] < f [v] < f [u].
Demostración. Si d[u] < d[v] < f [v] < f [u], la propiedad sigue directamente del teorema del
paréntesis.
Supongamos que v sea un descendiente de u en el árbol DF, y sea u = w0 → w1 → · · · → wn = v
la rama del árbol de u a v. Demostraremos por inducción sobre k que
d[u] < d[wk ] < f [wk ] < f [u].
Si k = 1, w1 es un vértice en la lista de adyacencia de u, por tanto, DFS-Visita(w1 ) se
llama desde DFS-Visita(u) (cuando se visita u, w1 no se ha visitado, si no no podrı́a ser
un hijo de u en el árbol), por tanto d[u] < d[w1 ]. Como las llamadas se encuentran
anidadas, DFS-Visita(w1 ) retorna antes que DFS-Visita(u), por tanto f [w1 ] < f [u].
Supongamos ahora que
d[u] < d[wk ] < f [wk ] < f [u],
(3.85)
y demostremos la propiedad en el caso de wk+1 . Aplicando el razonamiento precedente al
camino desde wk a v se ve que d[wk ] < d[wk+1 ] < f [wk+1 ] < f [wk ] y considerando la (3.85) se
obtiene d[u] < d[wk+1 ] < f [wk+1 ] < f [u].
El corolario segue del caso k = n.
Ejemplo:
Para el grafo de figura 3.10.a, y el árbol DF de figura 3.10.b, se da la situación
representada en figura 3.10.c; la estructura de paréntesis correspondiente se representa
en figura 3.10.d
Si en al tiempo d[v] (cuando se visita v por primera vez) el estado de un vértice u es V ,
entonces v descenderá de u en el árbol DF. La razón es que si al tiempo d[v] el estado de
u es V , entonces u ya se ha visitado, es decir d[u] < d[v] < f [v]. Por el teorema del
paréntesis, se cumple d[u] < d[v] < f [u] < f [v] y, por el corolario, u es un antepasado de v
en el árbol.
La condiciones bajo las cuales un vértice v es descendiente de otro vértice u en el árbol
DF no son inmediatamente evidentes. La presencia de un arco (u, v) no garantiza que sea
π[v] = u. El la figura 3.10, hay un arco (g, h), pero h no es hijo (ni descendiente) de g.
La razón es que h se ha descubierto analizando otro camino antes de llegar a g, por lo
tanto en el momento en que se analiza g el vértice h ya se encuentra en estado P. Si v es
adyacente a u, y el estado de v es N en el momento en que se discubre u, entonces v será
cahiers d’informatique
58
c
b
d
()*+
/.-,
()*+> /.-,
/.-,
()*+
/()*+
/ OO //.-,
? O
O >>
OOO
O
>>
OOO
>
O'
/.-,
()*+o
()*+
/.-,
()*+ O/.-,
()*+
//.-,
/
a
a
e
g
f
c
b
d
+3 2/9 S
+3 3/6
/ 4/5
1/16
SSS S
<y O
O HHHHH
S
SSSS
HHHHH
y
SSySyySy
HH '
SS
%/ 7/8
+3 13/14
11/12 ks
10/15
h
e
g
f
(a)
h
(b)
1
4
8
12
16
t
a
b
f
c
e
g
d
(c)
a+ b+ c+ d+ d− c− h+ h− b− f + e+ e− g+ g− f − a−
(
(
(
(
)
)
(
) )
(d)
(
(
)
(
)
)
)
Figure 3.10: Ejemplo del teorema del paréntesis: Para el grafo (a), con el árbol
DF (b), la estructura de intervales se muestra en (c), y la estructura de paréntesis
correspondiente en (d).
un descendiente de u en el árbol DF en cuanto o se discubre v a través de un camino que
sale de u---en cuyo caso v será descendiente de u---o bien se descubre cruzando el arco
(u, v) mientras que se analiza la lista de adyacencia de u---en cuyo caso v será un hijo
de u en el árbol DF.
Por otro lado, si v es accesible desde u (pero no adyacente a u), y el estado de v es N
cuando se descubre u, v no será necesariamente un descendiente de u en el árbol DF. Por
ejemplo, en el grafo
u(3/4) ck
(3.86)
'
6>
uuuu
u
u
u
u
uuuu
y(2/7)
+3 v(5/6)
x(1/8)
el vértice v es accesible desde u pero no es su descendiente en el árbol DF. Si el
análisis del grafo empieza por x, u y v serán hijos de y, pero v no será un descendiente
de u. El teorema siguiente caracteriza este caso con más precisión: proporciona las
grafos
59
condiciones necesarias y suficientes para que un vértice v accesible desde un vértice u
en el grafo sea su descendiente en el árbol DF.
Theorem 3.11.2. (del camino blanco) En un árbol DF de un grafo G = (V, E) (dirigido o no
dirigido), el vértice v es un descendiente de un vértice u si, y sólo si, en el momento
d[u] en que u es visitado por primera vez, v es accesible desde u a través de un camino
formado enteramente por vértices de estado N .
Demostración. Supongamos que v sea un descendiente de u en
u → · · · → w → · · · → v el camino entre u y v en el árbol DF,
del camino. Por el teorema del paréntesis, d[u] < d[w], por
no se habı́a todavı́a visitado, o sea, estaba en estado N .
del camino, se concluye que todo nodo del camino estaba en
el árbol, y sea
donde w es un nodo cualquiera
lo cual en el instante d[u], w
Como w es un nodo cualquiera
estado N en el instante d[u].
Supongamos ahora que al tiempo d[u] exista un camino de u a v formado por vértices en
estado N . (En particular, al instante d[u], v se encuentra en estado N .) Demostramos
que, en este caso, Iv ⊂ Iu . Por contradicción, se suponga que Iv 6⊂ Iu y consideramos, en
el camino entre u y v, el primer vértice y tal que Iy 6⊂ Iu . Sea x el predecesor de y en
dicho camino. Como y es el primer vértice para el cual la inclusión no vale, será
Ix ⊂ Iu , o sea d[u] ≤ d[x] < f [x] ≤ f [u] (la igualdad vale en el caso x = u). Ya que y es
parte del camino blanco, al instante d[u] y se encuentra en estado N y por tanto debe ser
d[y] > d[u]. Pero y es parte del entorno de x y por tanto será seguramente descubierto
antes que se termine la análisis de x, o sea d[u] < d[y] < f [x] ≤ f [u]. Pero, para el teorema
del paréntesis, esto quiere decir que Iy ⊂ Ix , que es una contradición del hipótesis.
Entonces, v es un descendente de u en el árbol.
3.11.3
Versión no recursiva de DFS
El algoritmo DFS se ha presentado en una versión recursiva, ya que esta versión es más
sencilla y facilita la demostración de las propiedades del algoritmo. Existe una versión
no recursiva que pone en evidencia como el DFS es un algoritmo de alguna forma dual al
BFS. Hemos visto que la clave para el funcionamiento del BFS es la cola FIFO en que se
insertan los vértices en orden de distancia de la fuente, y de donde se van sacando en el
mismo orden en que se han insertado: de los más cercanos hacia los más distantes. Ahora
bien, si en el BFS se remplaza la cola FIFO con una pila (LIFO: last in, first out) se
consigue un algoritmo que visita los vértices en el mismo orden que el DFS. Con una pila
no se procede en orden de distancia, sino que, a cada paso, se sigue yendo "en
profundidad" explorando el último vecino insertado en la pila.
El algoritmo que se consigue no es exactamente igual al DFS porque el DFS supone dos
modificaciones adicionales: la definición del estado P para vértices que ya se han
explorado completamente, y de los contadores de tiempo. Estos cambios no modifican el
orden de visita de los vértices ni los árboles DF que se construyen, pero nos
proporcionan información adicional que, como veremos, se utilizan en las aplicaciones.
Sin embargo, estas modificaciones a~
naden ciertas complicaciones al algoritmo ya que, tras
exporar los vecinos de un vértice, hay que volver a visitarlo para poner su estado a P y
fijar su tiempo de finalizaciı́on. Esta visita adicional es algo que en BFS no se hacı́a,
ya que una vez insertado un vértice en la cola no se volvı́a a cambiar. Con el fin de
hacer la presentación más clara, se presentará el algoritmo "DFS-Visita-no-rec" en dos
tiempos. Por el momento, ignoraremos el estado P y el tiempo de finalización, y
escriberemos un algoritmo que determina un árbol DF sin poner los vértices en estado P y
sin asignar un tiempo de finalización. Este algoritmo es el dual de BFS:
60
cahiers d’informatique
DFS-Visita-no-rec-1(G, s)
1.
Q: stack of vertex;
2.
t d[s]
← 0;←3.t;
4.
t ← t+1;
5.
Q.push(s);
6.
while ¬empty(Q) do
7.
u ← Q.pop;
8.
estado[u] ← E;
9.
d[u] ← t;
10.
t ← t+1;
11.
foreach v in u.ady do
12.
if estado[v] = N then
13.
π[v] ← u;
14.
Q.push(v);
15.
fi;
16.
od
17. od
Para la versión completa, observamos que, cuando visitamos por primera vez el vértice u
(es decir, cuando lo descubrimos y lo encontramos en estado N) tendremos que volver a
visitarlo, tras haber visitado sus vecinos, para finalizarlo. La idea entonces es que
cuando se saca u de la pila y se encuentra en estado N, se pone su estado a E y se vuelve
a poner en la pila, antes de insertar sus vecinos. Cuando se hayan visitado todos los
vecinos, se volverá a sacar u de la pila y, encontrando su estado en E, nos daremos
cuentas que está listo para finalizar. En ste caso, en vez de visitar sus vecinos, se
pondrá su estado a P, se asignará su tiempo de finalización, y no se volverá a poner en
la cola.
DFS-Visita-no-rec(G, s)
1.
Q: stack of vertex;
2.
t ← 0;
3.
d[s] ← t;
4.
t ← t+1;
5.
Q.push(s);
6.
while ¬empty(Q) do
7.
u ← Q.pop;
8.
if estado[u] = N then
9.
estado[u] ← E;
10.
d[u] ← t;
11.
t ← t+1;
12.
foreach v in u.ady do
13.
if estado[v] = N then
14.
π[v] ← u;
15.
Q.push(v);
16.
fi;
17.
od
18.
elseif estado[u] = E then
19.
estado[u] ← P;
20.
f[u] ← t;
21.
t ← t+1;
22.
fi
grafos
23.
61
od
La figura 3.11 ilustra el mismo ejemplo de la figura 3.9 ense~
nando, a cada paso, el
estado de la pila (los vértices que contiene y el estado de dichos vértices). Nótese que
a
b
c
e
f
a
()*+
/.-,
@ABC
GFED
?>=<
89:;
// .-,
()*+
1/
>> ? O
B O
>>
>> ()*+
/.-,
()*+
/.-,
()*+
/.-,
/
/
b
c
a
b
c
()*+
/.-,
@ABC
GFED
89:;
?>=<
// .-,
()*+
1/
:: A O
C O
::
::: //.-,
@ABC
GFED
89:;
?>=<
()*+
()*+
//.-,
2/
@ABC
GFED
89:;
?>=<
@ABC
GFED
89:;
?>=<
()*+
//.-,
1/
3/
>> ;C O
A O
>>
>>
>> ()*+
/.-,
@ABC
GFED
89:;
?>=<
()*+
//.-,
/
2/
[a(E), e(N ), d(N )]
a
c
b
x
@ABC
GFED
?>=<
89:;
@ABC
GFED
?>=<
89:;
G?>=<
89:;
FED
+3 @ABC
1/
3/
4/
>> ;C O
? O
>>
>>
>>
()*+
/.-,
@ABC
GFED
?>=<
89:;
()*+
/
//.-,
2/
e
f
d
[a(E), e(N ), d(E), e(N ), b(N )]
a
c
b
x
@ABC
GFED
89:;
?>=<
@ABC
GFED
89:;
?>=<
+3 4/5
3/
1/
>> ;C O
> O
>>
~~
>>
~
>> ~~~
~
()*+
@ABC
GFED
89:;
?>=<
()*+
/.-,
//.-,
/
2/
e
f
d
[a(E), e(N ), d(E), e(N ), b(E), c(N )]
a
c
b
x
@ABC
GFED
89:;
?>=<
+3 4/5
1/
3/6
@@ :B O
@@~~~
|= O
~@@
||
~
~
|
~
~~~~~ @@ |||
@ABC
GFED
89:;
?>=<
()*+
/.-,
()*+
/.-,
/ |
/
2/
e
f
d
[a(E), e(N ), d(E), e(N ), b(E), c(E)]
a
c
b
x
@ABC
GFED
?>=<
89:;
+3 4/5
1/
3/6
@@ :B O
@@~~~
|= O
||
~~~@@
|
~
|
~
~~~~ @ ||
()*+
@ABC
GFED
?>=<
89:;
GFED
?>=<
89:;
+3 @ABC
//.-,
2/
7/
e
f
d
[a(E), e(N ), d(E), e(N ), b(E)]
a
c
b
x
@ABC
GFED
89:;
?>=<
+3 4/5
3/6
1/
@@ :B O
@@~~~
|= O
||
~~~@@
|
~
|
~
~~~~ @ ||
GFED
89:;
?>=<
GFED
89:;
?>=<
@ABC
GFED
89:;
?>=<
+3 @ABC
+3 @ABC
8/
7/
2/
e
f
d
[a(E), e(N ), d(E), e(N )]
a
c
b
x
@ABC
GFED
89:;
?>=<
+3 4/5
1/
3/6
@@ :B O
@@~~~
|= O
||
~~~@@
|
~
|
~
~~~~ @ ||
GFED
89:;
?>=<
@ABC
GFED
89:;
?>=<
+3 8/9
+3 @ABC
7/
2/
e
f
d
[a(E), e(E), d(E), e(E), f (N )]
a
c
b
x
@ABC
GFED
?>=<
89:;
+3 4/5
3/6
1/ B
BB 9A O
= O
z
|
BB|||
z
z
|
B
z
|
zz
|||||| B!
@ABC
GFED
?>=<
89:;
+3 8/9
3
+
7/10
2/
e
f
d
[a(E), e(E), d(E), e(E), f (E)]
a
c
b
w
@ABC
GFED
89:;
?>=<
+3 4/5
3/6
1/ E
EE 8@ O
= O
z
y
EEyyy
z
z
y
E
z
y
zz
yyyy E"
+3 8/9
3
+
7/10
2/11
e
f
d
[a(E), e(E), d(E), e(E)]
a
c
b
v
+3 4/5
3/6
1/12
= O
EE y 8@ O
z
EyEyyy
z
z
E
z
y
zz
yyyy E"
+3 8/9
3
+
7/10
2/11
d
d
z
e
f
[a(E), e(E), d(E)]
Figure 3.11:
d
z
e
f
[a(E), e(P )], [a(E)]
y
d
e
[]
f
Funcionamiento del algoritmo DFS en versión no recursiva.
el vértice e se inserta dos veces en la pila: una vez en cuanto vecino de a y una en
cuanto vecino de d. El vértice se procesa como vecino de d cuando se extrae su versión
más reciente al instante 6, y se finaliza al instante 10. En este momento, el vértice
sigue en la pila (en segunda posición) pero, cuando se extrae otra vez de la cola, su
estado ya es P, ası́ que el algoritmo simplemente lo ignora (se vea como al tiempo 11 hay
dos extracciones de la pila sin que cambie el marcador de tiempo).
cahiers d’informatique
62
En fin, cabe notar que la versión recursiva del algoritmo también se basa en una pila,
aún si esta no aparece explicitamente en el algoritmo: se trata de la pila de las
llamadas a DFS-Visita y de sus parámetros, que es definida y gestionada por el compilador
y que tiene la misma estructura de la pila que se ha definido aquı́. Por esta razón, no
es fácil dar una versión recursiva de BFS: los compiladores guardan las llamadas siempre
en una pila, y nunca en una cola FIFO.
3.11.4
Clasificación de los arcos
El algoritmo de búsqueda en profundidad nos proporciona una clasificación de las aristas
del grafo que, a la vez, nos proporciona información sobre la estructura del grafo. Se
pueden definir cuatro tipos de arcos, en relación con el bosque Gπ resultante de la
aplicación del algoritmo DFS:
arcos del árbol: son los arcos incluidos en los árboles DF de Gπ ; el arco (u, v) es un
arco del árbol si v se ha descubierto por primera vez durante la exploración del
entorno de u;
arcos hacia atrás: son arcos (u, v) tal que v es un antecesor de u en un árbol DF; árcos
entre un vértice y si mismo se consideran arcos hacia atrás;
arcos hacia adelante: son arcos (u, v) que conectan un vértice v a un descendente v en un
árbol DF (excepto en el caso que el arco sea un arco del árbol);
arcos de cruce: los demás; los arcos (u, v) donde u y v no son uno el antecesor del otro.
Los arcos se pueden clasificar (parcialmente) durante la ejecución del algoritmo DFS,
notando el estado del vértice v que se accede cuando el arco se explora por primera vez.
Si se está analizando el entorno de u, y se llega al vértice v, entonces
i) si estado[v] = N el arco (u, v) es un arco del árbol;
ii) si estado[v] = E el arco (u, v) es un arco hacia atrás;
iii) si estado[v] = P, el arco (u, v) es un arco hacia adelante o un arco de cruce.
El primer caso es obvio. En el segundo, los vértices marcados E corresponden a una pila
de llamadas a DFS-Visita, y forman una rama de un árbol DF, ası́ que encontrar un vértice
en estado E quiere decir encontrar un antecesor en la rama del árbol.
Esta clasificación no permite distinguir entre arcos hacia adelante y arcos de cruce.
Para esto, será necesario analizar el grafo después la aplicación del algoritmo: se
puede demostrar que un arco (u, v) es un arco hacia adelante si d[u] < d[v], y de cruce si
d[v] < d[u].
La clasificación de un arco depende de las modalidades de ejecución del algoritmo DFS.
Consideremos el grafo siguiente, en que los vértices se visitan en orden alfabético:
?>=<
89:;
c
cruce
?>=<
/ 89:;
a
arbol
?>=<
+389:;
b
(3.87)
Se empieza por el vértice a, y el arco (a, b) será parte del árbol DF con raı́z en a. En el
momento en que se visita c, a ya se encuentra en estado P y, siendo d[c] > d[a], el arco
(c, a) se clasificará como arco de cruce. Por otro lado, si los vértices se visitan en
grafos
63
orden alfabético inverso, empezando por c, los dos arcos serán arcos del árbol DF con
raı́z en c:
arbol +3 89:;
arbol +389:;
?>=<
?>=<
?>=<
89:;
a
c
(3.88)
b
Sin embargo, hay ciertas propiedades de la clasificaión que dependen sólo de la
estructura del grafo, como demuestra el teorema siguiente:
Theorem 3.11.3. Un grafo dirigido G es acı́clico si y sólo si el algoritmo DFS no produce
ningún arco hacia atrás.
Demostración. Supongamos que el grafo sea acı́clico. Si se da un arco hacia atrás (u, v),
entonces v es un antecesor de u en el árbol. Sea v → w1 → · · · → wn → u el camino de u a
v en el árbol: dicho camino más el arco (u, v) es un ciclo, y se contradice la hipótesis
que el grafo sea acı́clico.
Supongamos ahora que G contenga un ciclo. Sea v el primer vértice del ciclo que se
visita, y sea u el vértice que lo precede en el ciclo. Cuando v es visitado, los demás
vértices del ciclo están en estado N (¡v es el primer vértice que se visita!), por tanto
hay un camino de vértices en estado N hasta u. Por el teorema del camino blanco, u
desciende de v en el árbol, y (u, v) es un arco hacia atrás.
El teorema no garantiza que siempre se encontrará el mismo arco hacia atrás. Por
ejemplo, en el grafo siguiente, si se visitan los vértices en orden alfabético, el arco
(c, b) es un arco hacia atrás
89:;
?>=<
?>=<
?>=<
/89:;
/ 89:;
a
(3.89)
b >^
d
>>
>>

89:;
?>=<
c
Por otro lado, si se visitan los vértices
hacia atrás. Pero, en todo caso, como el
garantiza que siempre se encontrará uno y
encuentra sólo uno, veanse los ejercicios
*
en orden alfabético inverso, el arco (b, d) es
grafo contiene un ciclo, el teorema nos
un sólo arco hacia atrás. (Sobre el porqué se
al final de esta sección.)
*
*
En un grafo no dirigido, esta clasificación es ambigüa, ya que los arcos (u, v) y (v, u) son
en realidad el mismo arco. En este caso, el arco es clasificado según la dirección en
que se cruce. La clasificación de un grafo no dirigido no contiene arcos hacia adelante
ni arcos de cruce.
Theorem 3.11.4. En la búsqueda en profundidad de un grafo no dirigido G, cada arco es un
arco del árbol o un arco hacia atrás.
Demostración. Sea (u, v) un arco de G. Ya que el arco se puede escribir como (u, v) o
(v, u), se asumirá que está escrito en el orden tal que d[u] < d[v]. Entonces, v se descubre
antes de que se acabe la exploración de u, ya que se encuentra en su lista de adyacencia
(por el teorema del camino blanco). Si el arco (u, v) se recorre primero en dirección de
u a v, entonces (u, v) es un arco del árbol. Si se recorre primero en dirección de v a u,
entonces ed un arco hacia atrás, ya que en ese momento u se encuentra en estado E.
cahiers d’informatique
64
3.11.5
Ejercicios
1. Ejecute el algoritmo BFS en los grafos siguientes, tomando como fuente el vértice
marcado doble. Asuma que las listas de adyacencias están ordenadas alfabéticamente
por nombre de vértices.
?>=<
89:;
89:;
a = ?>=<
b
==
==
==
?>=<
89:;
?>=<
89:;
0123
7654
c
d
89:;
/?>=<
b II
II
I$
?>=<
89:;
()*+
/.-,
e
u:
u
u
u
u
?>=<
89:;
>=<
/ ?89:;
c
d
?>=<
89:;
a
89:;
?>=<
a
w GGG
w
w
GG
w
w
GG
ww
G
w
89:;
?>=<
89:;
?>=<
c3
b3
33
3
33
33
3
89:;
?>=<
89:;
?>=<
89:;
?>=<
7654
0123
89:;
?>=<
g
e
f
d
89:;
?>=<
d
89:;
?>=<
e
89:;
?>=<
f
89:;
?>=<
()*+
/.-,
a
)89:;
?>=<
?>=<
89:;
?>=<
/89:;
c
d
b i
O
J V
89:;
?>=<
?>=<
89:;
?>=<
89:;
?>=<
/ 89:;
g
e
f
h
@
89:;
?>=<
89:;
?>=<
89:;
?
>=<
89:;
?/ >=<
/
j
i
l
k
w
89:;
?>=<
a
O
89:;
?>=<
()*+
/.-,
a
w
?>=<
89:;
()*+
/.-,
a i
O
?>=<
89:;
a
89:;
?>=<
89:;
/?>=<
c
b 5Z
O 5 D O dHHHH
5
H
55
?>=<
89:;
g
5
55
C
5
89:;
?>=<
89:;
?>=<
89:;
()*+
/.-,
/ ?>=<
e
f
d o
89:;
?>=<
c
?>=<
()*+
/.-,
/89:;
b II
II
I$
89:;
?>=<
e
u:
u
u
u
u
89:;
?>=<
?>=<
/ 89:;
c
d
)?>=<
?>=<
89:;
89:;
()*+
/.-,
89:;
/?>=<
c
b i
d
O
J V
?>=<
89:;
89:;
?>=<
89:;
?>=<
89:;
/ ?>=<
g
e
f
h
@
?>=<
89:;
?
89:;
>=<
?89:;
?>=<
89:;
/
/ >=<
j
i
k
l
?>=<
89:;
a o
89:;
?>=<
b
?>=<
/89:;
b II
II
I$
89:;
?>=<
e
u:
u
u
u
u
89:;
?>=<
?>=<
/ 89:;
c
d
89:;
?>=<
a
?>=<
89:;
a
)?>=<
89:;
>=<
>=<
/?89:;
/ ?89:;
@ bO =^ = @ cO
@d
==
=== ?>=<
89:;
?>=<
89:;
?>=<
89:;
?>=<
89:;
g o
e o
f o
h
89:;
?>=<
a
?>=<
?>=<
/89:;
/89:;
c
b
O
O
89:;
?>=<
89:;
?
()*+
/.-,
>=<
?>=<
/
/ 89:;
e
f
d g
( 89:;
?>=<
()*+
/.-,
?>=<
?>=<
/89:;
c
b = 89:;
d
O
=
==
O
=
== 89:;
?>=<
89:;
?>=<
89:;
?>=<
89:;
?>=<
g
e
f
7 h
89:;
?>=<
a o
89:;
?>=<
89:;
?>=<
a8
b
88ssss
ss88 89:;
89:;
?>=<
7654
0123
88 ?>=<
c
td
8
t
tt 8
tt ?>=<
89:;
?>=<
e JJ 89:;
f
JtJtt
J
t
tt J89:;
?>=<
89:;
?>=<
g
h
89:;
?>=<
a o
?>=<
89:;
?>=<
/89:;
c
b 5Z
O 5 D O dHHHH
5
H
55
89:;
?>=<
()*+
/.-,
g
5
55
C
5
89:;
?>=<
89:;
?>=<
?>=<
/ 89:;
e
f
d o
?>=<
89:;
?>=<
/.-,
()*+
/89:;
c
b 5Z
O 5 D O dHHHH
5
H
55
89:;
?>=<
g
5
55
C
5
?>=<
89:;
?>=<
89:;
?>=<
/ 89:;
e
f
d o
2. Escriba la estructura de paréntesis para cada uno de los árboles (o bosques) DF del
ejercicio 1.
3. De un ejemplo de grafo y un bosque DF en tal grafo, con un vértice u con la
siguiente propiedad: existe un árbol DF que contiene sólo u, a pesar del hecho que
existe un arco que sale de u y un arco que entra en u. Dibuje el grafo dando
etiquetas a sus vértices. Asuma que el bucle principal del algoritmo DFS analiza
grafos
65
los vértices en orden alfabetico, y que todas las listas de adyacencias son
ordenadas alfabeticamente.
4. Determine un grafo conexo en que las etiquetas de los vértices son tales que cada
vértice constituye un árbol DF separado. Asuma que el bucle principal del algoritmo
DFS analiza los vértices en orden alfabetico, y que todas las listas de adyacencias
son ordenadas alfabeticamente.
5. Suponga que se hace una clasificación de los arcos de un grafo dirigido, similar a
la que se ha hecho en esta sección, tras ejecutar el algoritmo BFS. Demuestre que en
ningún caso hay arcos hacia adelante.
6. En el mismo caso del ejercicio precedente, demuestre que, si el grafo es no
dirigido, no hay ni arcos hacia atrás ni arcos hacia adelante.
7. En el ejemplo (3.89) se muestra que dependiendo del orden de la visita, el arco
hacia atrás que se encuentra puede cambiar. Demuestre que, dado un grafo con un
sólo ciclo, el algoritmo DFS siempre encontrará un sólo arco hacia atrás.
(Sugerencia: se puede utilizar el hecho de que si un grafo contiene un solo ciclo y
se elimina un arco de dicho ciclo, el grafo resultante es acı́clico.)
8. Generalice el teorema precedente al caso de n ciclos.
cahiers d’informatique
66
A
B
C
D
E
F
G
H
I
J
K
L
a~
nadir un vaso de vino blanco;
abrir una lata de tomate (¡San Marzano, de Napoli!);
poner la cebolla en la sartén y freir;
a~
nadir trozos de hojas de albahaca;
picar la cebolla;
quitar el ajo del fuego;
poner los tomates en la sartén;
poner los dientes de ajo en la sartén;
a~
nadir sal, al gusto;
pelar dos dientes de ajo;
dejar cocer durante cinco minutos y servir;
untar una sartén con aceite y ponerla a fuego moderado;
Table 4.1:
Operaciones necesarias para preparar una salsa de tomate.
IV. APLICACIONES DE LA BÚSQUEDA
4.12
Ordenación topológica
Una ordenación topológica de un grafo dirigido acı́clico (Directed Acyclic Graph, DAG)
G = (V, E) es una ordenación de los vértices de V tal que para cada arista (u, v) ∈ E, u
precede v en la ordenación. La ordenación topológica se define sólo para grafos
acı́clicos en cuanto si el grafo contiene un ciclo, no existe ninguna permutación con la
propiedad requerida. En forma gráfica, una ordenación topológica se puede representar
poniendo todos los vértice del grafo uno al lado del otro de forma tal que todos los
arcos van de izquierda a derecha.
Para introducir el concepto de ordenación topológica con un ejemplo, consideremos el
problema de preparar una salsa de tomate. De manera un poco esquemática, podemos
considerar que las operaciones necesarias para preparar la salsa son las que se
representan (en orden casual) en la tabla 4.12. Claramente, para conseguir una buena
grafos
67
salsa y no un desastre culinario, estas operaciones no se pueden efectuar en un order
cualquiera. Por ejemplo, es claramente necesario picar la cebolla (operación E) antes de
ponerla en la sartén (operación C). En este caso, se dirá que la operación E tiene
prioridad, o que es un prerequisito para poder efectuar la operación C. Se pueden
representar las operaciones como los vértices de un grafo, poniendo una arista desde un
prerequisito a las operaciones que dependen de él. En el caso de las operaciones E y C,
se pondrá
?>=<
89:;
?>=<
/ 89:;
(4.90)
C
E
Otras operaciones no tienen este tipo de dependencia. Por ejemplo, se puede picar la
cebolla (operación E) antes o después de pelar el ajo (operación J). Todas las
dependencias entre operaciones se pueden representar en un grafo en que, como en el caso
de las operaciones E y C arriba, los vértices representan operaciones y las aristas
relaciones de prioridad/dependencia. En el caso de la salsa de tomate, este grafo de
prioridad se presenta como:
?>=<
89:;
J>
>>
>
?>=<
89:;
?>=<
/ 89:;
F>
@ H)
>>
))
>
)
)) 89:;
?>=<
?>=<
89:;
?>=<
89:;
?>=<
B / 89:;
L
G>
@K
))
>>
>
))
89:;
?>=<
))
@A
))
?>=<
89:;
?>=<
?>=<
/ 89:;
E
C / 89:;
I
(4.91)
El grafo nos da toda la información necesaria para hacer una salsa de tomate, pero lo que
de verdad queremos es una receta: una secuencia determinada de operaciones que,
ejecutadas en el orden especificado, nos consigan una buena salsa. Esta lista ordenada
debe respetar las prioridades entre las operaciones: si, como es el caso, E tiene
prioridad sobre C, C debe aparecer en la lista después de E, y ası́ para todas las demás
prioridades. Esto supone encontrar una ordenación de los vértices tal que cada vez que
haya una arista (u, v) ∈ E, u preceda v en la ordenación. Es decir, la receta tiene que
ser una ordenación topológica del grafo de prioridades.
La definición formal de ordenación topológica es la siguiente:
Definition 4.12.1. Dado un grafo G = (V, E), con V = {v1 , . . . , vn }, una ordenación topológica
de G es una permutación Vπ = [vπ1 , . . . , vπn ] tal que si (vi , vj ) ∈ E, entonces πi < πj .
No todos los grafos se pueden ordenar topológicamente. El lema siguiente caracteriza una
clase de grafos en que no se puede definir una ordenación topológica.
Lemma 4.12.1. Si G = (V, E) contiene ciclos, entonces no existe ninguna ordenación de sus
vértices que cumpla la propiedad de ordenación topológica.
Demostración. Supongamos que G posee un ciclo [v1 , v2 , . . . , vn , v1 ] de n vértices distintos y
una ordenación topológica. Escribimos u v si el vértice u precede a v en dicha
ordenación. Notamos que (v1 , v2 ) ∈ E ası́ que, por la propiedad de ordenación topológica
será v1 v2 . Siguiendo ası́ vemos que para cada i, vi vi+1 o sea, finalmente,
v1 v2 · · · vn v1
(4.92)
cahiers d’informatique
68
Pero v1 vn v1 ⇒ v1 = vn , en contradicción con la hipótesis que todos los vértices
fuesen distintos.
Un grafo no dirigido tampoco tiene ordenación topológica. En un grafo dirigido, para
cada arista (u, v) ∈ E hay otra (v, u) ∈ E, por tanto en una ordenación topológica, u
tendrı́a que aparecer a la vez antes de v (ya que (u, v) ∈ E) y después (ya que (v, u) ∈ E).
Pero, siendo la ordenación una permutación de los vértices, u sólo puede aparecer una
vez, y por tanto la ordenación no existe.
Por otro lado, el algoritmo que analizaremos en esta sección proporciona una ordenación
topológica para todo grafo dirigido acı́clico, por tanto el algoritmo, junto al
lema 4.12.1 contituye una demostracción del teorema siguiente.
Theorem 4.12.1. Un grafo G posee una ordenación topológica si y sólo si G es un grafo
dirigido acı́clico.
Consideremos otra vez el grafo (4.91), relativo a la salsa de tomate.
ordenación de vértices es una ordenación topológica del grafo:
B
G
J
E
L
H
F
C
I
A
La siguiente
K
Para convencerse, será suficiente dibujar las aristas y averiguar que, con los vértices
en este orden, todas van hacia la derecha, es decir, desde un vértice a uno que lo sigue
en la ordenación:
?>=<
89:;
B
>=<
/ ?89:;
G
?>=<
89:;
J
?>=<
89:;
E
?>=<
89:;
L
?>=<
/( 89:;
H
?>=<
/ 89:;
F
?>=<
C
45 89:;
?>=<
/ 89:;
I
?>=<
4/ 89:;
A
?>=<
/* 89:;
K
(4.93)
La ordenación topológica de un grafo no es única. Por ejemplo, la siguiente es otra
ordenación topológica del grafo de la salsa de tomate:
?>=<
89:;
J
?>=<
89:;
B
>=<
/ ?89:;
G
?>=<
89:;
L
*/ ?89:;
>=<
H
?>=<
/ 89:;
F
89:;
?>=<
E
?>=<
/4 89:;
C
?>=<
/ 89:;
I
?>=<
/4 89:;
A
?>=<
/+ 89:;
K
(4.94)
La ordenación topológica tiene muchas aplicaciones de gran relevancia práctica (además,
por supuesto, de conseguir recetas para la salsa de tomate). Dos aplicaciones muy
comunes son las siguientes:
i) Sistemas de gestión de proyectos. Un proyecto es un conjunto de actividades. En un
proyecto hay actividades que se pueden ejecutar en cualquier momento y actividades
que sólo se pueden ejecutar tras haber completado otras. En la construcción de una
casa, las ventanas se pueden instalar independentiemente de la tuberı́a, pero es
necesario esperar que se haya instalado la tuberı́a para instalar el suelo. Los
sistemas de gestión de proyectos usan la ordenación topologica para determinar el
orden en que se ejecutarán las actividades de un proyecto.
ii) Sistemas de procesamiento a flujo de datos. Estos sistemas se representan como
grafos en que los vértices son procesos que consumen y producen datos y los arcos
con canales a través de los cuales los datos producidos por un proceso se comunican
a otros procesos. Para ejecutar un sistema de flujo de datos es necesario ordenar
los procesos de forma tal que un proceso se ejecute sólo después que se han
ejecutado los procesos que producen sus ingresos. La ordenación topológica
proporciona un orden de ejecución que satisface estos vı́nculos.
grafos
69
Para derivar un algoritmo de ordenación topológica, empezamos con una observación.
grafo (4.95) muestra el resultado de una ejecución de dfs en el grafo (4.95).
WVUT
PQRS
17/18
HH
HH
H$
HIJK
ONML
2/13
6>
....
v
v
v
v
v
....
vvvvv
.... PQRS
HIJK
ONML
WVUT
1/14
.... 19/22
....
....
....
....
WVUT
PQRS
HIJK
/ ONML
15/16
3/10
El
(4.95)
WVUT
+3 PQRS
11/12
GG
GG
G#
HIJK
ONML
WVUT
+3 PQRS
20/21
7? 6/7
JJ
w
w
JJ
w
ww
JJ
wwwww
$
HIJK
ONML
5/8
tt 6>
t
t
t
t
tttttt
HIJK
+3 ONML
4/9
Concentrémonos simplemente en los tiempos de finalización f [u] de los vértices. Si
ordenamos los vértices por valores decrecientes de f [u], empezando con el vértice J, que
tiene f = 18, y terminando con el vértice K con f = 7, conseguimos la ordenación (4.94).
Por otro lado, se puede verificar que la ordenación (4.93) deriva de otra ejecución de
DFS, una que empieza con el vértice B. Esta observación nos sugiere el siguiente
algoritmo para la ordenación topológica de un grafo sin ciclos.
i) se ejecute DFS con la modificación siguiente:
i.a) al final de la visita de un vértice (es decir, justo después de poner el estado
del vértice igual a P ), insertar el vértice al principio de una lista enlazada;
i.b) al final del algoritmo, devolver la lista; el orden en que los vértices
aparecen en la lista constituye una ordenación topológica del grafo.
Es decir, el algoritmo completo de ordenación topológica es el siguiente:
L = Topologica(G)
1.
L: List; L = [];
2.
foreach u in V[G] do
3.
estado[u] ← N;
4.
π[u] ← NIL;
5.
od
6.
t ← 0;
7.
foreach u in V[G] do
8.
if estado[u] = N then
9.
Topo-rec(G, u, L);
10.
fi
11. od
12. return L;
Topo-rec(G, u, L)
1.
estado[u] ← E;
2.
t ← t + 1;
3.
d[u] ← t;
4.
foreach v in Ady[u] do
cahiers d’informatique
70
5.
6.
7.
8.
9.
10.
11.
12.
13.
if estado[v] = N then
π[v] ← u;
Topo-rec(v);
fi
od
estado[u] ← P;
t ← t+1;
f[u] ← t;
insert-at-head(L, u);
El vértice u se pone en la lista al instante f [u] (al paso 13, tras fijar el valor f [u] al
paso 12) y, ya que u se inserta al principio de la lista, cuando el algoritmo termina la
lista se encuentra ordenada por valores decrecentes de f [u].
El tiempo de ejecución de la ordenación topológica es O(V + E), ya que el algoritmo DFS
se ejecuta en un tiempo O(V + E), y la inserción de un elemento al principio de una lista
enlazada se ejecuta en un tiempo O(1) (o sea, es necesario un tiempo O(V ) para insertar
todos los vértices del grafo en la lista). La clave para demostrar la correción del
algoritmo es el teorema de la sección precedente según el cual un grafo G es acı́clico si
y sólo si el bosque DF no contiene ningún arco hacia atrás.
Theorem 4.12.2. Sea G un grafo dirigido acı́clico. Tras la ejecución de Topologica(G) la
lista L contiene una ordenación topológica del grafo.
Demostración. Para demostrar el teorema es suficiente mostrar que, si (u, v) ∈ E, entonces
f [v] < f [u]: esto garantiza que v será insertado en la lista antes que u y, como todos los
vértices se insertan al principio de la lista, que u estará más cerca que v al principio
de la lista L que se retorna, o sea, que u aparecerá antes que v en la ordenación.
Consideremos el momento en que el algoritmo DFS (o la función Topologica, que se basa en
DFS) explora el arco (u, v). En este momento, el estado de u es E. El vértice v, en este
momento, no puede estar en estado E, si no v serı́a un antecesor de u, (u, v) serı́a un arco
hacia atrás, y por tanto el grafo tendrı́a un ciclo (por el teorema 3.11.3 de página 63).
Entonces v sólo se puede encontrar en estado N o P . Si v está en estado N , por el
teorema del camino blanco será un descendente de u en el árbol DF, por tanto
d[u] < d[v] < f [v] < f [u]. Si v está en estado P , entonces al instante en que se visita u,
ya se ha terminado de visitar v, o sea f [v] < d[u] < f [u].
Las varias posibilidades de las posiciones reciprocas de Iu y Iv se muestran en la figura
4.12, donde se evidencian las combinaciones declaradas imposibles en la demostración del
teorema (recordamos que Iv y Iu tienen que ser disjuntos o uno incluido en el otro, por
las propiedades del algoritmo DFS).
4.13
Componentes fuertemente conexas
En esta sección consideraremos otra aplicación muy importante del algoritmo DFS: la
determinación de las componentes fuertemente conexas de un grafo. Esta determinación es
interesante en si misma, para la solución de algunos problemas de relevancia práctica y
como algoritmo para la construción del grafo de componentes de un grafo dado, una
estructura que proporciona información importante sobre la estructura del grafo inicial.
La determinación de las componentes fuertemente conexas se usa también como paso inicial
grafos
71
Iu
Iv
Iv
v → ··· → u → v
¡hay ciclo!
No:
-
-
(u, v) ∈ E
No:
Iv
-
Iv
f [v] < f [u]
-
Figure 4.12:
Las posibilidades de posiciones reciprocas de Iu y Iv
antes la aplicación de otros algoritmos, ya que en algunos problemas la forma más
eficiente de operar sobre un grafo consiste en determinar sus componente fuertemente
conexas, resolver el problema separadamente en cada componente, y luego usar el grafo de
componentes para integrar las soluciones encontradas. Empecemos con dar la definición
formal de componentes fuertemente conexas:
Definition 4.13.1. Una componente fuertemente conexa de un grafo G es un conjunto
U ⊆ V [G] que goza de las propiedades siguientes:
p0
p
i) para cada pareja de vértices u, v ∈ U existen en el grafo los caminos u → v y v → u;
ii) no existe nungún conjunto U 0 ∈ V [G] tal que U 0 cumple i) y U ⊂ U 0 .
La condición i) es la condición de conexión: implica que desde cualquier vértice de una
componente fuertemente conexa se puede llegar a cualquier otro vértice. La condición ii)
es la condición de maximalidad: la componente fuertemente conexa que contiene un vértice
u es el conjunto más grande entre los que contienen u y cumplen la condición i). Las
condiciones i) y ii) juntas implican que un camino entre dos vértices de una componente
fuertemente conexa U no sale de U :
Lemma 4.13.1. Sea G un grafo, U ⊆ V [G] una componente fuertemente conexa, y u, v ∈ U .
p = [u, y1 , . . . , yn , v] un camino entre u y v. Entonces para todo 1 ≤ i ≤ n, es yi ∈ U .
Demostración. La demostración es por contradición.
Sea
p
Sea w un vértice del camino u → v tal
0
p
00
p
que w 6∈ U . El camino p se puede dividir en dos partes p0 y p00 tales que u → w → v.
q
Siendo u y v en la misma componente fuertemente conexa, existe un camino v → u, por tanto
q
p0
que v → u → w es un camino de v a w. Desde cualquier vértice de U se puede llegar hasta
v y desde aquı́, a través del camino q · p0 , hasta w. Similarmente, desde w se puede llegar
hasta u y, desde aquı́ a cualquier vértice de U .
Entonces el conjunto U 0 = U ∪ {w} cumple la propiedad i) de la definición y, siendo
w 6∈ U , es U ⊂ U 0 . Es decir U no cumple la propiedad ii), contradiciento la hipótesis
que U fuese una componente fuertemente conexa.
cahiers d’informatique
72
Ejemplo
Ejemplo:
El grafo de la figura 4.13 contiene tres componentes fuertemente conexas: una formada
por los vértices a, b, c, e ; una formada por d, g, h; y una formada sólo por i.
a
?>=<
89:;
1 j
b
c
g
h
i
* 89:;
?>=<
?>=<
/ 89:;
1 Fc
1
O FF
FF
FF
F ?>=<
89:;
?>=<
/ 89:;
e
d u: 2
< 1O
x
u
x
u
x
u
xx
uu
xxx
uu
u
?>=<
?>=<
89:;
89:;
?>=<
/ 89:;
3
2 o
2
Figure 4.13:
Componentes fuertemente conexas de un grafo.
Definition 4.13.2. El grafo de componentes de un grafo G = (V, E) es el grafo Ĝ = (V̂ , Ê)
donde V̂ contiene un vértice para cada componente conexa de G, y (u, v) ∈ Ê si hay un arco
entre cualquier vértice de la componente conexa u y cualquier vértice de la componente
conexa v.
El grafo de componentes del grafo de figura 4.13 se ilustra en figura 4.14.
()*+
/.-,
8 Id
qqq IIII
q
q
q
II
qq
I
// .-,
()*+
()*+qq
/.-,
a, b, c, e
d, g, h
Figure 4.14:
i
El grafo de componentes del grafo precedente.
Theorem 4.13.1. El grafo de componente de un cualquer grafo G es acı́clico.
La demostración de este teorema se deja al lector (véase el ejercicio 9 de esta sección).
El algoritmo siguiente usa dos llamadas a DFS para determinar las componentes fuertemente
conexas de un grafo en tiempo O(V + E):
i) invocar DFS(G) para calcular los tiempos finales f[u] para cada vértice u;
ii) calcular GT ;
iii) invocar DFS(GT ), pero en el bucle principal (el bucle donde se llama DFS-Visit)
recorrer los vértices en orden decreciente de los valores f[u] calculados al paso 1;
iv) mostrar los vértice de cada árbol del bosque obtenido en 3 como componentes conexas.
grafos
73
a
b
(0 ONML
HIJK
HIJK
/ ONML
3/6
2/7 Le
LL
O
LL
LL
LL L
PQRS
HIJK
/ ONML
10/15
4/5
d WVUT
4<
r9 O
q
r
q
r
qq
rrr
qqq
rrrr
qqq
WVUT
PQRS
PQRS
HIJK
ONML
+3 WVUT
11/14
12/13
9/16 o
HIJK
ONML
1/8 hp
g
h
a
c
i
r
ONML
HIJK
9/16
e
b
c
WVUT
PQRS
WVUT
10/15 Lo
12/13
6. PQRS
LLLLL
KS
LLLLLL
LLLL
L
L !)
HIJK
ONML
PQRS
WVUT
o
3/4
11/14
d
q KS
r
q
r
q
rr
qq
rrr
qqq
r
q
r
q
ry
qx
HIJK
HIJK
ONML
HIJK
ONML
+3 ONML
7/8
2/5 o
1/6
g
h
e
i
Figure 4.15: En (a), el resultado de la aplicación del primer paso del algoritmo de
determinación de las componentes fuertemente conexas al grafo de figura 4.13, y en (b) el
resultado de la aplicacióndel tercer paso a la traspuesta del grafo. Los árboles DFS de
(b) corresponden a las componentes fuertemente conexas del grafo.
Ejemplo:
Consideremos el grafo de la figura 4.13. La figura 4.13.a muestra el resultado de una
ejecución de DFS en este grafo. En cada vértice u se han marcado los instantes d[u] y
f [u] y, por claridad, se han marcado los arcos del árbol DFS que se ha construido. (Estos
arcos y los instantes d[u] son irrelevantes para la determinación de las componentes
fuertemente conexas, ya que la primera ejecución de DFS sirve sólo para determinar los
valores f [u] y, a través de ellos, el orden en que los vértices se recorrerán en la
segunda ejecución.)
La figura 4.13.b representa la transpuesta del grafo de figura 4.13 en que se ejecuta el
segundo DFS. El primer nodo que se examina es el nodo g, que tiene el valor f [g] = 16.
Partiendo de este nodo se determina un árbol DF compuesto por los vértices g, h, d.
Después de la determinación de este árbol, los vértices con f = 15 y f = 14 (o sea h y d)
ya están en estado P , ası́ que el próximo árbol se empieza por el vértice i, que tiene
f [i] = 13; este árbol se compone sólo del vértice i. El valor de f más alto es ahora
f [a] = 8, por lo cual empezamos un árbol DF del vértice a. Este árbol incluye los
vértices a, b, c, e. Ahora se ha analizado todo vértice, por lo cual el DFS termina.
Cada uno de los tres árboles que resultan de la última ejecución de DFS contiene los
vértices de una de las componentes fuertemente conexas del grafo de figura 4.13.
4.13.1
Desarrollo del algoritmo
El algoritmo tiene una estructura muy sencilla, pero merece la pena invertir un poco de
tiempo en desarrollarlo más concretamente, ya que tal desarrollo constituye un útil
ejercicio de adaptación de los algoritmos elementales a situaciones más complejas.
Empezaremos con un cambio en la estructura que almacena los grafos. En la sección 2.7,
la representación por listas de adyacencia constaba de una cadena de estructuras de tipo
vertex, y la estructura arista contenı́a el ı́ndice del vértice destino. Para este
algoritmo, es conveniente representar el grafo como una lista de vértices. Con esta
representación los vértices ya no tienen ı́ndices, por tanto será necesario cambiar la
estructura donde se almacenan las aristas de forma tal que en lugar del ı́ndice del
cahiers d’informatique
74
vértice destino contengan un puntero a dicho vértice.
type graph is list of vertex;
type vertex is
begin
data: nodedata;
state: { N, E, P }
ady: list of edge;
next: →vertex;
end
type edge is
begin
dest:
next:
end
→vertex;
→edge;
También asumiremos que, si V es una lista, el bucle
foreach v in V do
.
.
.
od
recorra la lista ordenadamente desde el primer elemento hasta el último.
Para implementar el algoritmo DFS necesitamos una función DFS-Visita per, en este caso,
no necesitamos que el DFS-Visita recoga todos los datos de la versión estándar. La
primera llamada, de la cual nos estamos ocupando ahora, no necesita proporcionar ninguna
información sobre el tiempo de descubrimiento d[u] ni sobre los punteros π[u]: por el
momento necesitamos sólo conocer el orden de finalización de los vértices. Para esto,
tampoco necesitamos conocer el tiempo de finalización f [u] ya que, como se ha visto en el
algoritmo de ordenación topológica, es suficiente insertar los vértices en la cabecera de
una lista mano a mano que se finalizan para conseguir el órden deseado6 . Un algoritmo
DFS-Visita "minimalista" es el siguiente. Nótese que el resultado de este algoritmo es
una lista de vértices, es decir, un grafo.
DFS-V-CC(V : graph, u : vertex)
1.
out, o1: graph;
2.
state[u] ]lef tarrow E;
3.
out ← [];
4.
foreach v in u.ady do
5.
o1 ← DFS-V-CC(V, v);
6.
out ← append(o1, out);
7.
od;
8.
state[u] ← P;
6 Por
supuesto, esto no quiere decir que en esta fase del algoritmo se consigue una ordenación topológica
del grafo: los grafos que nos ocupan en esta sección pueden tener ciclos, por tanto la ordenación topológica
puede no existir.
grafos
9.
10.
75
out ← insert-at-head(u, out);
return out;
En la primera llamada a DFS, todas las listas creadas por llamadas a DFS-V-CC se juntan
en una que, al final de la ejecucción, contendrá todos los vértices del grafo, ordenados
por valores decrecientes del tiempo de finalización.
DFS-CC-1(V: graph)
1.
foreach u in V do
2.
state[u] ← N;
3.
od
4.
g, l1: graph;
5.
g ← [];
6.
foreach u in V do
7.
if state[u] = N then
8.
l1 ← DFS-V-CC(V, u);
9.
g ← append(l1, g);
10.
fi
11. od
12. return g;
Al algoritmo para calcular la transpuesta de un grafo en esta representación es muy
sencillo, y se deja como ejercicio para el lector (véase el ejercicio 10 al final de esta
sección). La única advertencia es que hay que tener cuidado para construir, en el grafo
transpuesta, una lista de vértices ordenada como en el grafo original, ya que este orden
nos sirve para la llamada al segundo DFS.
La segunda llamada a DFS es un poco distinta de la primera. Los vértices se deben
recorrer en órden de finalización, pero esto no es un problema, ya que la primera llamada
a DFS ha dejado la lista en el orden que se necesita. El cambio más importante tiene que
ver con la preparación del resultado. Esta llamada a DFS debe retornar las componentes
conexas del grafo. El número de dichas componentes es desconocido en el momento de la
llamada, por tanto la solución más racional es almacenarlas en una lista, una componente
en cada elemento de la lista. Cada componente conexa se produce con una llamada a
DFS-V-CC, que retorna la lista de vértices de un árbol DF (correspondientes, como se
verá, a una componente conexa). Esto supone que cada elemento de la lista de componentes
será una lista de vértices. Es decir, el resultado de esta llamada a DFS será una lista
de listas.
DFS-CC-2(V: graph)
1.
res: list of list of vertex;
2.
l: list of vertex;
3.
l ← [];
1.
foreach u in V do
2.
state[u] ← N;
3.
od
6.
foreach u in V do
7.
if state[u] = N then
8.
l1 ← DFS-V-CC(V, u);
9.
g ← append(l1, g);
10.
fi
cahiers d’informatique
76
11.
12.
od
return g;
Nótese que las primera dos funciones retornan una lista de vértices, es decir, un grafo.
La función DFS-CC-2 retorna una lista de listas. En este caso, las aristas no nos
interesan: sólo nos interesan los grupos de vértices que se forman en cada sub-lista, ya
que estas son las componentes conexas del grafo. El algoritmo final es muy sencillo:
CC(V: graph)
1.
V1, V2: graph;
2.
C: list of list of vertex;
3.
V1 ← DFS-CC-1(V);
4.
V2 ← transpose(V1);
5.
C ← DFS-CC-2(V2);
6.
return C;
El algoritmo DFS y el algorimo transpose tienen un tiempo de ejecución O(V + E), que es
también el tiempo de ejecución de CC.
4.13.2
Corrección del algoritmo
El algoritmo CC es una aplicación sencilla de la búsqueda en profundidad que, a primera
vista, parece no tener ninguna relación con las componentes conexas de un grafo. Todo lo
que hace es establecer un orden de los vértices (en la primer llamada a DFS), y utilizar
este orden para conseguir árboles DF en la transpuesta del grafo. En esta sección
demostraremos que, efectivamente, estas operaciones determinan las componentes
fuertemente conexas del grafo.
El teorema siguiente proporciona una primera conexión, todavı́a bastante débil, entre
árboles DF y componentes conexas.
Theorem 4.13.2. En cualquer ejecución de DFS, los vértices de la misma componente
fuertemente conexa formarán parte del mismo árbol DF.
Demostración. Sea U una componente fuertemente conexa del grafo, y sea r ∈ U el primer
vértice de U que se visita durante la ejecución de DFS. Sea u ∈ U otro vértice arbitrario
p
de U . Por definición de componente fuertemente conexa hay un camino p : r → u y, por el
lema 4.13.1, p se compone enteramente de vértices de U . Siendo r el primer vértice de U
que se descubre, al tiempo d[r] todos los vértices de p se encuentran en estado N. por
tanto, p es un camino blanco de r a u y, por el teorema del camino blanco, u será un
descendiente de r en el árbol DF.
El árbol DF contiene toda la componente fuertemente conexa de su raı́z, pero puede
contener también otras componentes. En el grafo
?>=<
89:;
s
@ ===
==
==
==
?>=<
89:;
89:;
?>=<
a
f o
?>=<
?>=<
/89:;
/89:;
c
b =^
==
==
==
== 89:;
?>=<
d
(4.96)
grafos
77
la componente conexa de s se compone de los vértices {s, a, f }, pero el árbol DF con raı́z
en s abarca todo el grafo, e incluye la componente de s y la de b ({b, c, d}). Nótese que
este resultado depende de la elección de la fuente: tomando b come primera fuente se
consiguen dos árboles DF, cada uno correspondiente a una componente fuertemente conexa
del grafo. El teorema 4.13.2 demuestra que si el árbol DF contiene un vértice de una
componente fuertemente conexa, entonces contiene toda la componente. Por tanto un
corolario del teorema es:
Corollary 4.13.1.
Sea T el conjunto de vértices que constituye un árbol DF del grafo.
S
Entonces T = k Uk , donde cada Uk es una componente fuertemente conexa del grafo.
La idea del algoritmo, expresada en manera muy informal, es la siguiente. Un árbol DF
puede contener varias componentes fuertemente conexas porque hay "puentes" entre las
componentes que el algoritmo puede cruzar. En el ejemplo precedente, el arco (s, b) es un
puente entre las dos componentes fuertemente conexas del grafo. Estos puentes se pueden
cruzar sólo en una dirección (si no, las dos componentes serı́an la misma), mientras que
entre vértices de una misma componente hay conexiones en las dos direccipnes. Por tanto,
si tomamos la transpuesta del grafo, los caminos internos a la misma componente quedan
abiertos, mientras que los puentes se cierran:
89:;
?>=<
89:;
?>=<
c
b =o
@
==
==
==
== 89:;
?>=<
d
?>=<
89:;
s o
^===
==
==
==
?>=<
89:;
?>=<
/ 89:;
a
f
(4.97)
Esta es la razón por la cual, en el paso final del algoritmo, se buscan árboles DF en la
traspuesta. Claramente, es necesario elegir oportunamente las raices del árbol. En esta
traspuesta, si empezáramos con el vértice b, conseguirı́amos un árbol que abarca todo el
grafo. Por otro lado, empezando con el vértice s conseguimos las componentes fuertemente
conexas. El orden correcto de visita de las raices de los árboles DF se consigue con el
primer paso del algoritmo, como se demostrará a continuación.
*
*
*
En el resto de esta sección, las cantidades d[u] y f [u] indicarán el tiempo de
descubrimento y el tiempo de finalización del vértice u en la primera ejecución de DFS, y
p
la notación u → v indicará que p es un camino de u a v en el grafo G, y no en la
traspuesta GT .
Dado un vértice u, su progenitor (forefather) es el vértice alcanzable desde u que
finaliza por último en la primera ejecución de DFS. Es decir, el vértice tal que
u → φ(u)
y
f [φ(u)] = max f [w]
w:u→w
(4.98)
Es posible que φ(u) sea u mismo, ya que u es alcanzable desde si mismo. Esta observación
y la segunda de las condiciones (4.98) implican que, para todo u ∈ V [G],
f [u] ≤ f [φ(u)]
Lemma 4.13.2. Si u → v, entonces f [φ(v)] ≤ f [φ(u)].
(4.99)
cahiers d’informatique
78
Demostración: Cada vértice alcanzable desde v es también alcanzable desde u.
poniendo
U = {w : u → w}
W = {w : v → w}
se cumple V ⊆ U .
Por tanto,
(4.100)
Por la definición de progenitor, entonces
f [φ(v)] = max f [φ(v)]
w∈V
(4.101)
y
f [φ(u)]
= max f [φ(v)]
w∈U
= max max f [w], max f [w]
w∈V
w∈U −V
= max f [φ(v)], max f [w]
w∈U −V
≥ f [φ(v)]
(4.102)
Lemma 4.13.3. Para cada u ∈ V [G], se cumple φ(φ(u)) = φ(u).
Demostración: Por definición de φ(u), u → φ(u) is por tanto, por el lema 4.13.2,
f [φ(φ(u))] ≤ f [φ(u)]. Por la propiedad (4.99), también es f [φ(u)] ≤ f [φ(φ(u))], por tanto
f [φ(φ(u))] = f [φ(u)]. Ya que vértices distintos no pueden tener el mismo tiempo de
finalización, es φ(φ(u)) = φ(u)
Para cada componente fuertemente conexa U hay un progenitor de todos los vértices de U ,
y que consideraremos como el "representante" de la componente. En el primer DFS, este
vértice es el primer vértice de U que se descubre, y consecuentemente el último que se
finaliza. En el segundo DFS, él que se ejecuta en GT , este vértice es la raı́z del árbol
DF que coincide con U .
Theorem 4.13.3. En un grafo G = (V, E), se ejecute el algoritmo DFS. Al final de la
ejecución, para cada vértice u, φ(u) es un antepasado de u en un árbol DF.
Demostración: Si φ(u) = u el teorema se cumple banalmente. Consideremos entonces
φ(u) 6= u, y analicemos el estado de φ(u) en el momento d[u] en que se descubre u.
i) Si estado[φ(u)] = P, entonces f [φ(u)] < d[u] < f [u], contradiciendo la inegualdad (4.99).
ii) si estado[φ(u)] = N, hay que distinguir dos casos.
p
ii.a) Hay un camino blanco u → φ(u) (Recordemos que al tiempo d[u] es estado[u] = N y
que, por definición, hay un camino u → φ(u).) En este caso, por el teorema del
camino blanco, φ(u) es un descendiente de u en el árbol DF. Por tanto
f [φ(u)] < f [u], contradiciendo la inegualdad (4.99).
p
ii.b) En el camino u → φ(u) hay por lo menos un vértice cuyo estado no es N. Sea w
el último de estos vértices, es decir, sea el camino de u a φ(u)
r
q
u → w → φ(u)
(4.103)
grafos
79
donde q es blanco. El estado de w en este momento no puede ser P porque, si no,
durante su análisis se habrı́a cruzado el camino q y el estado de φ(u) no podrı́a
ser N, como estamos hipotizando. Si estado[w] = E, hay un camino blanco de w a
φ(u), y φ(u) será un decendiente de w en el árbol DF, lo que supone
f [w] > f [φ(u)]. Pero hay un camino de u a w y por el lema 4.13.2,
f [φ(w)] ≤ f [φ(u)], es decir
f [w] > f [φ(u)] ≥ f [φ(w)]
(4.104)
contradiciendo la igualdad (4.99) para el vértice w.
Por tanto, al tiempo d[u] tiene que ser estado[φ(u)]=E y, por el teorema del paréntesis,
φ(u) será un antepasado de u en el árbol DF.
Un corolario de este teorema nos proporciona otra relación entre árboles DF y componentes
fuertemente conexas:
Corollary 4.13.2. En cada ejecución de DFS en un grafo G, para cada vértice u, u y φ(u)
pertenecen a la misma componente fuertemente conexa.
Demostración: Hay un camino u → φ(u) por la definición de progenitor, y un camino φ(u) → u
porque, por el teorema precedente, φ(u) es un antepasado de u en el árbol DF.
El teorema siguiente nos proporciona una condición más fuerte.
Theorem 4.13.4. En un grafo G, los vértices u y v se encuentran en la misma componente
fuertemente conexa si y sólo si en cualquier ejecución de DFS los dos tienen el mismo
progenitor.
Demostración: Supongamos primero que los dos tengan el mismo progenitor φ. Por el
corolario 4.13.2, u y v están en la misma componente fuertemente conexa de φ.
Supongamos ahora que u y v estén en la misma componente fuertemente conexa del grafo.
Entonces existen caminos u → v y y → u. Aplicando el lema 4.13.2 a los dos conseguimos
f [φ(v)] ≤ f [φ(u)]
f [φ(u)] ≤ f [φ(v)]
(4.105)
Por tanto f [φ(v)] = f [φ(u)] y, ya que vértices distintos no pueden finalizar al mismo
tiempo, φ(u) = φ(v).
Sea Tr en árbol DF con raı́z en r encontrado durante la segunda ejecución de DFS. Nótese
que, a pesar de que ahora consideremos árboles encontrados durante la segunda ejecución
de DFS en GT , las cantidades d[u], f [u], y el progenitor φ(u) siguen siendo referidas a la
primera ejecución de DFS, la ejecución en el grafo G. Sea
C(r) = {v ∈ V : φ(v) = r}
(4.106)
el conjunto de vértices que tienen r como progernitor. Por el teorema 4.13.4, cada C(r)
es una componente fuertemente conexa del grafo. Para demostrar la corrección del
algoritmo, nos queda demostrar que cada C(r) coincide con Tr .
Lemma 4.13.4. Si C(r) 6= ∅, entonces r ∈ C(r).
cahiers d’informatique
80
Demostración: Si C(r) 6= ∅, hay por lo menos un vértice v tal que v ∈ C(r), es decir, un
vértice v tal que φ(v) = r. Por tanto
≡
≡
≡
≡
r
= φ(v)
φ(r) = φ(φ(v)) (aplicación de φ)
φ(r) = φ(v)
(lema 4.13.3)
φ(r) = r
(definición del φ(v))
r
∈ C(r)
(definición de C(r))
(4.107)
Lemma 4.13.5. Durante cualquier ejecución de DFS, todo vértice u ∈ C(r) se introduce en
Tr .
Demostración: Por el teorema 4.13.2, todo vértice en la misma componente fuertemente
conexa se pone en el mismo árbol DF, es decir, por el teorema 4.13.4, todos los vértices
de C(r) se introducen en el mismo árbol DF. Ya que, por el lema ??, r ∈ C(r), y r es la
raı́z de Tr , este árbol es Tr .
El lema 4.13.5 proporciona una parte de la demostración de corrección: el algoritmo
introduce toda la componente conexa C(r) en el árbol Tr . Queda por demostrar que sólo la
componente C(r) se introduce en Tr , es decir, que si w ∈ V se introduce en Tr , entonces
φ(w) = r. Esto se demostrará basándose en dos lemas, que establecen las condiciones para
f [φ(w)].
Lemma 4.13.6. Si un vértice w se introduce en Tr , entonces f [φ(w)] ≥ f [r].
Demostración: Si w se introduce en Tr , entonces hay en GT un camino r → w y por tanto
hay en G un camino w → r. Por tanto
f [φ(w)] ≥ f [φ(r)] (lema 4.13.2)
= f [r]
(demostración del lema 4.13.4)
(4.108)
Lemma 4.13.7. Si un vértice w se introduce en Tr , entonces f [φ(w)] ≤ f [r].
Demostración: La demostración es por inducción sobre el número de árboles DF que se crean
a lo largo del algoritmo.
Base: Sea Tr el primer árbol que se crea. Por la ordenación de los vértices en el bucle
principal de la segunda ejecución de DFS, f [r] es el valor máximo de todos los tiempos de
finalización, por tanto f [φ(w)] ≤ f [r].
Inducción: suponga que el lema se cumple por los primeros n árboles, Tr1 , . . . , Trn , y que
se inserta w en Trn+1 . Asumamos por contradicción que f [φ(w)] ≥ f [rn+1 ]. Entonces existe
r0 tal que f [φ(w)] = f [r0 ] y por tanto w ∈ C(r0 ). Por la ordenación del bucle principal de
DFS, si f [r0 ] > f [rn+1 ], entonces en el momento en que se están introduciendo vértices en el
árbol Trn+1 , el árbol Tr0 ya se ha analizado, es decir, Tr0 es uno de los árboles
Tr1 , . . . , Trn . Ya que w ∈ C(r0 ) y el árbol Tr0 ya se ha construido, por el lema 4.13.5 w ya
se ha introducido en Tr0 , y por tanto no se puede introducir en Trn+1 .
Con estos lemas, la demostración de la corrección del algoritmo es inmediata.
grafos
81
Theorem 4.13.5. Tras ejecutar el algoritmo CC en un grafo G = (V, E), para cada vértice
u ∈ V se cumple que u ∈ Tr si y sólo si u ∈ C(r), y cada C(r) es una componente
fuertemente conexa del grafo.
Demostración: C(r) es una componente fuertemente conexa del grafo por el teorema 4.13.4.
Si u ∈ C(r), entonces u se introduce en Tr por el lema 4.13.5. Por otro lado, si u se
introduce en Tr , los lemas 4.13.6 y 4.13.7 demuestran que f [φ(u)] = f [r] y, es decir, por
la unicidad de los tiempos de ejecución, que φ(u) = r. Por tanto, que u ∈ C(r).
cahiers d’informatique
82
4.13.3
Ejercicios
1. Ordenen topológicamente los grafos siguientes. Asuman que las listas de adyacencias
estén ordenadas alfabéticamente y, en el bucle principal del algoritmo DFS, que se
visiten los vértices en orden alfabético (o sea, el primer árbol DFS siempre tendrá
como raı́z el vértice a).
()*+
/.-,
a
b
()*+
//.-,
/.-,
()*+
/.-, o
()*+
()*+
8/.-,
d
e
f
a
b
()*+
/.-,
/.-,H
()*+
/
z
z HH
HH
zz
zz
zz
H# z} zz
z
}
()*+
()*+D
g /.-,
c /.-,
DD
vv
DD
v
D! v{ vv
()*+?
/.-,
()*+
/.-,
??
?
d
f ???
? ()*+
/.-,
()*+
//.-,
O
c
()*+
/.-,
b
/.-,
()*+
()*+
/.-,
/.-,>
()*+
>>
>>
> 
()*+
/.-,
()*+
//.-,
()*+
//.-,
a
c
e
()*+
/.-,
a
b
// .-,
()*+
d
/.-,
/()*+
/.-,
/()*+
c
a
b
()*+
/.-,
()*+o
/.-,
o
z= O Hc HH
z= O
HH
zz
zz
z
z
z
H zz
()*+
()*+Dza
g /.-,
c /.-,
; O
v
DD
v
DD
v
v
D vv
()*+
/.-,
()*+_?
/.-,
A
??
??
f
??
d
? ()*+
/.-,
d
()*+
/.-,
b
/.-,
/()*+
()*+
//.-,
c
()*+
//.-,
a
a
d
b
()*+
/.-,
e
c
/.-,
/()*+
B
f
e
2. Repitan el ejercicio precedente ejecutando el bucle principal de DFS en orden
alfabético inverso (o sea, de forma tal que el primer árbol BFS tenga como raı́z el
último vértice en orden alfabético).
3. La ordenación topológica de un grafo no es única. Se pueden encontrar varias
ordenaciones de un mismo grafo. Entre todos los grafos con n vértices, ¿cuál es el
grafo que permite el mayor número de ordenaciones topológicas diferentes? ¿Cuál es
el grafo que permite el mı́nimo número? ¿Cuántas ordenaciones diferentes permiten
los grafos citados? Para el caso n = 5 dibujen estos dos grafos.
4. Demuestren que un grafo acı́clico con n vértices siempre contiene n componentes
fuertemente conexas.
5. Demuestren que un grafo es acı́clico si, y sólo si, es posible enumerar sus vértices
de forma tal que su matrı́z de adyacencia es triangular con ceros en la diagonal.
grafos
83
6. El grafo siguiente no es acı́clico. ¿Qué pasa si se ejecuta el algoritmo de
ordenación topológica en el grafo? ¿El algoritmo termina o produce un resultado?
Si el algoritmo produce un resultato, muéstrelo.
t
()*+
/.-,
/.-,
()*+_>OO /.-,
/.-,
()*+
/
/()*+
>>OOO oooo7 ???  O
>>ooOoOO
?

oo> OOO'   ??
()*+
/.-,
()*+
()*+oo /.-,
/.-,
()*+
/.-,
\
\
p
z
u
v
s
q
w
(4.109)
7. Determinen las componentes fuertemente conexas del grafo del ejercicio precedente.
8. Determinen las componentes fuertemente conexas de los grafos siguientes y dibujen el
grafo de componentes correspondiente.
a
/.-,
()*+
O
b
// .-,
()*+
/.-,
/()*+
/.-,o
()*+
/.-,o
()*+
/.-,
()*+
/.-,
()*+
d
e
f
b
a
()*+
/.-,
a
/.-,
()*+
_
a
/.-,
()*+
_
c
c
b
/.-,
()*+
b
()*+
/.-,
O
/.-,
()*+
d
a
/.-,>
()*+
>>
>>
>
// .-,
()*+
c
b
/.-,> /.-,
()*+
()*+
/
>>
>>
> ()*+
/.-,
()*+
//.-,
d
a
()*+
/.-,
[
/.-,
()*+
d
e
b
/.-,
()*+
C [
c
b
()*+> /.-,
/.-,
()*+
()*+o
//.-,
O >> O >>> ?
>>
>>
>
> ()*+
/.-,
()*+
/.-,
// .-,
()*+
e
&()*+
//.-,
/.-,
()*+
/()*+
/ .-,
/.-,o
()*+
/.-,
()*+
e
a
()*+
//.-,
g
f
c
f
c
()*+
/.-,
C
d
()*+
/.-,
?
a
b
.-,
//()*+
?
/.-,
()*+
h
/.-,
()*+
/
O
c
()*+
//.-,
c
b
()*+
/.-,
()*+
/.-,
()*+
/.-,
/
yy
yy O
y
y
y
y
yy
yy
/.-,y|
// .-,
()*+
()*+y|
/.-,
/()*+
a
d
e∗f
9. Demuestren que el grafo de componentes de cualquier grafo es acı́clico.
10. Escriban el algoritmo transpose para la representación del grafo con lista de
vértices, de forma tal que con la llamada V1 ← transpose(V), los vértices en los
grafos V y V1 tengan el mismo orden.
cahiers d’informatique
84
V. PROBLEMAS DE MÍNIMO EN GRAFOS
PONDERADOS
5.14
Caminos de coste mı́nimo
En esta sección trataremos un problema muy común y con importantes aplicaciones: la
determinación del camino de coste mı́nimo (o, simplemente, del camino mı́nimo ) entre dos, o
más, vértices de un grafo. En las secciones precedentes ya hemos encontrado una versión
de este problema: el algoritmo BFS, como hemos visto, encuentra el camino con el menor
número de arcos de la fuente s a cualquier vértice alcanzable desde ella. Es decir,
encuentra el camino de coste mı́nimo si el coste de un camino se define como el número de
arcos que lo compone. En esta sección consideraremos una generalización importante de
este problema, que consiste en asignar a cada arco del grafo un peso o coste. Al fin de
darse cuenta de la importancia del peso en las aplicaciones, consideremos el ejemplo
(2.42) de la red de una compa~
nı́a aerea:
JFK
vv
3932 vv
v
vv
vv
LAX SSS
SSSS
SSSS
SS
10181 SSSS
SS
5766
MAD H
HH
HH959
HH
1263
HH
LHR 780 CDG
(5.110)
831
FCO
856
BCN
Si consideramos los caminos más cortos simplemente en términos de número de arcos, ası́
como se hacı́a con el algoritmo BFS, resulta que da lo mismo ir de Madrid (MAD) a Roma
(FCO) pasando por Parı́s (CDG) y Barcelona (BCN) que pasando por Nueva York (JFK) y Los
Angeles (LAX): ambos recorridos se componen de tres arcos. Al contrario, si tomamos en
cuenta los pesos de las aristas del grafo y los interpretamos como el coste de recorrer
la arista, vemos que pasar por Parı́s nos "cuesta" 2.646Km, mientras pasar por Los Angeles
nos "cuesta" 19.879Km. Los pesos pueden suponer una diferencia importante en problemas
de mı́nimo.
grafos
5.15
85
Caminos en grafos ponderados
Los grafos ponderados, permiten modelizar algunas situaciones que no se pueden modelizar
adecuadamente con un grafo simple como, por ejemplo, el grafo de aeropuertos del ejemplo
precedente. La formalización del concepto de grafo ponderado se encuentra en la
siguiente definición.
Definition 5.15.1. Un grafo ponderado es un par (G, w), donde G es un grafo y w : E[G] → R
es una función que asocia a cada arco (u, v) ∈ E un peso w(u, v).
Esta definición asocia un peso w a un arco (u, v) pero, para los problemas de esta
sección, nos interesa más bien asociar pesos a caminos, o sea, a conjuntos de arcos
contı́guos. Para este fin, necesitamos una convención que nos diga como se componen los
pesos de conjuntos de arcos. Esta operación de composición debe ser conmutativa (el
coste es igual, independientemente del orden en que se recorren los arcos) y asociativa
(recorrer e1 seguido por e2 y e3 es igual a recorrer e1 y e2 seguido por e3 ). La manera
más sencilla de componer pesos de forma conmutativa y asociativa es sumando: el peso del
camino p = [v1 , . . . , vn ] se define como
w(p) =
n−1
X
w(vi , vi+1 )
(5.111)
i=1
Esta forma de composición es útil en muchos problemas prácticos. Funciona bien, por
ejemplo, en el caso de la lı́nea aerea: la longitud de una ruta compuesta por dos tramos
es igual a la suma de las longitudes de los dos tramos. En otros problemas se necesitan
otras formas de composición. Por ejemplo, si los pesos representan probabilidades, la
manera más natural de componerlos es por multiplicación:
w(p) =
n−1
Y
w(vi , vi+1 )
(5.112)
i=1
Los algoritmos de esta sección asumirán siempre que los pesos se componen por suma. De
toda manera, si el problema impone encontrar un camino mı́nimo con composición de coste
por multiplicación, es muy fácil modificar la función de coste para utilizar los
algoritmos que aquı́ presentamos. Dados los pesos w(vi , vj ) consideremos los pesos
modificados ŵ(vi , vj ) = log w(vi , vj ). Por la nota propiedad de los logaritmos,
logab = loga + logb, hay
logw(p)
=
log
n−1
Y
w(vi , vi+1 )
(5.113)
i=1
=
n−1
X
logw(vi , vi+1 )
i=1
=
n−1
X
ŵ(vi , vi+1 )
i=1
Por tanto, remplazando los pesos dados por sus logaritmos, los algoritmos de esta sección
(con composición por suma) determinarán el camino que minimiza logw(p). Por la monotonı́a
de la función logaritmo, el mismo camino minimiza w(p).
cahiers d’informatique
86
Análogamente, si el problema nos pide encontrar el camino de coste máximo entre dos
vértices (como es el caso, por ejemplo, cuando el peso de un arco representa una
ganancia), será suficiente remplazar cada w(vi , vj ) con −w(vi , vj )7 .
El coste del camino mı́nimo de u a v se define como
p
min{w(p) : u −→ v} si existe un camino entre u y v
δ(u, v) =
∞
en otro caso
(5.114)
Un camino mı́nimo entre u y v es cualquier camino p de u a v con w(p) = δ(u, v).
El problema del camino mı́nimo se presenta de varias formas. En esta sección nos
ocuparemos sobre todo del problema del camino más corto con única salida : dado un grafo
G y un vértice de salida (o fuente ) s, se quiere encontrar el camino mı́nimo desde s
hasta cualquier vértice v ∈ V . Otras variantes del camino mı́nimo se pueden resolver
usando los algoritmos que estudiaremos aquı́:
único destino: dado un grafo G y un vértice destino u, determinar el camino mı́nimo desde
cualquier vértice v de G a u; el problema es equivalente a resolver el problema de
la única salida en GT ;
único par: dados dos vértices, encontrar el camino mı́nimo entre estos vértices; el
problema se resuelve de forma óptima con el algoritmo de única salida;
todos los pares: encontrar el camino mı́nimo entre todo par de vértices; el problema se
puede resolver aplicando el algoritmo de única salida a todos los vértices, aún si
hay formas más eficientes de hacerlo.
5.15.1
Ciclos de coste negativo
Hay que hacer una distinción muy importante en los problemas de camino mı́nimo según si la
función de peso w es siempre no negativa o si el grafo puede contener arcos de coste
negativo. Si los arcos de coste negativo no dan lugar a ningún ciclo de coste total
negativo, entonces el problema del camino mı́nimo queda bien definido y, para cada vértice
u del grafo, es posible definir el coste del camino mı́nimo δ(s, u) y darle un valor finito.
Consideremos, por ejemplo, el grafo de figura 5.16. El grafo contiene dos arcos de coste
negativo (w(e, b) = −2 y w(c, d) = −5), pero el coste del único ciclo es positivo
(w[b, c, e, b] = 2). Existen una infinidad de caminos entre, por ejemplo, a y d: [a, b, c, d],
[a, b, c, e, b, c, d], [a, b, c, e, b, c, e, b, c, d], etc., pero cada vez que se recorre el ciclo el coste
del camino aumenta de 2, ası́ que el camino mı́nimo es el que nunca recorre el ciclo:
[a, b, c, d], con coste w([a, b, c, d]) = −2.
Por otro lado, si el grafo contiene un ciclo de coste negativo, tal como el grafo de
figura 5.17, el camino mı́nimo entre dos vértices puede no existir. Para cada camino
7 A primera vista, parece que esta sustitución nos pueda causar problema porque puede introducir ciclos
de coste negativo y por tanto, como veremos en la sección siguiente, transformar en indefinido un problema
que, originariamente, era definito. En realidad no es ası́. Es fácil darse cuenta que un problema de camino
máximo en un grafo con ciclos de coste positivos es indeterminado exactamente como un problema de mı́nimo en
un grafo con coste negativo: siempre se puede aumentar la ganancia recorriendo una vez más el ciclo. Ası́
que si, después de la sustitución, nos aparece un ciclo de coste negativo en el grafo, el problema inicial
era mal puesto. Por otro lado, veremos que el algoritmo de Dijkstra acepta sólo grafos con función de coste
no negativa y por tanto con la sustitución propuesta no serı́a posible su aplicación hasta en problemas bien
puestos. En este caso se pueden utilizar otra funciones: cada sustitución ŵ(vi , vj )
=
f (w(vi , vj )) con f
decreciente transformará un problema de máximo en uno de mı́nimo. Se puede poner, por ejemplo, ŵ(vi , vj )
=
1/w(vi , vj ).
grafos
87
a
89:;
?>=<
a
b
1
c
2 /89:;
?>=<
?>=<
/89:;
c
b <^
<<
<< 2
−2 << 89:;
?>=<
d
d
−5
?>=<
/ 89:;
e
e
Figure 5.16:
negativo.
Un grafo con pesos de coste negativo pero sin ciclos de coste total
89:;
?>=<
a
Figure 5.17:
1
1 / 89:;
2 /89:;
?>=<
?>=<
?>=<
/89:;
c
e
b =^
==
== −1
−2 = 89:;
?>=<
d
Un grafo con un ciclo de coste negativo.
entre s y u que roce o contenga el ciclo, es posible encontrar un camino de coste menor
dando una vuelta más al ciclo. En el caso del grafo de figura 5.17, indiquemos con
[a, b, c, (e, b, c)n , d] el camino entre a y d que se consigue dando n vueltas al ciclo. El coste
de este camino es
w([a, b, c, (e, b, c)n , d]) = 4 − n
(5.115)
En este caso es δ(a, d) = −∞, y el camino no es definido.
El algoritmo de Dijkstra, que se analizará en la próxima sección, asume que todos los
pesos del grafo son positivos, ası́ que el problema de los ciclos negativos no se
presenta. El algoritmo de Bellman-Ford acepta grafos con pesos negativos, y está en
grado de detectar la presencia de ciclos negativos.
5.15.2
Árboles de caminos mı́nimos
En muchos problemas nos interesa una representación del camino más que el mero coste del
camino mı́nimo entre la fuente y un vértice cualquera. Puede ser interesante saber que en
una red de metro se puede ir de la estación A a la estación B recorriendo, en el caso
mejor, 15Km, pero mucho más nos interesa saber como hacerlo, o sea, por que estaciones
hay que pasar para recorrer este camino mı́nimo. Los caminos mı́nimos entre la fuente y
los demás vértices del grafo G = (V, E) se guardan usando la misma técnica usada en el
algoritmo BFS: a cada vértice v ∈ V se le asocia un predecesor π[v]. Los caminos mı́nimos
entre la fuente8
y los demás vértices del grafo constituyen el (sub-)grafo de caminos Gπ = (Vπ , Eπ ), donde
Vπ = {s} ∪ {v ∈ V |π[v] 6= Nil}
(5.116)
Eπ = {(π[v], v)|v ∈ (Vπ − {s}) }
(5.117)
y
8 Recordamos
que nos estamos ocupando del problema de encontrar caminos mı́nimos con única salida.
cahiers d’informatique
88
?>=<
89:;
;C a
2 ~~
~
~
~~~~
?>=<
89:;
s>
3
>>
>> 5
89:;
?>=<
b
89:;
?>=<
;C a
2 ~~
~
~
~~~~
89:;
?>=<
s >>
3
>>>>
>># 5
89:;
?>=<
b
Figure 5.18: Un grafo puede contener varios árboles de caminos mı́nimos:
mismo grafo, los dos árboles (a) y (b) son de caminos mı́nimos.
aquı́ para el
En el caso del algoritmo BFS se ha demostrado que el grafo Gπ era un árbol y que el
camino desde la fuente s hasta cualquier vértice v ∈ Vπ correspondı́a al camino más corto,
en términos de número de arcos, entre s y v en el grafo. Los algoritmos de esta sección
construyen un grafo Gπ que es también un árbol (el árbol de caminos mı́nimos ) que
contiene, pero, los caminos de coste mı́nimo entre s y cualquier vértice v ∈ Vπ (es decir,
hasta cualquier vértice alcanzable desde s). Los caminos de coste mı́nimo no son
necesariamente únicos y, consecuentemente, un grafo puede contener varios árboles de
caminos mı́nimos. Un ejemplo se ilustra en la figura 5.15.2
El algoritmo BFS es un algoritmo de camino mı́nimos con única salida para grafos en que w
es una constante (todos los arcos tienen el mismo peso).
5.15.3
Estructura de los caminos mı́nimos
Los algoritmos de búsqueda del camino mı́nimo utilizan una propiedad importante: los
trozos de un camino mı́nimo son ellos mismos caminos mı́nimos entre los vértices que unen.
Esta propiedad se formaliza en el lema siguiente:
Lemma 5.15.1. Sea G = (V, E) un grafo con función peso w : E → R, sea p = [v1 , . . . , vk ] el
camino mı́nimo entre v1 y vk y, para cada i, j, con 1 ≤ i ≤ j ≤ k, sea pij = [vj , . . . , vj ] el
sub-camino de p entre los vértices vi y vj . Entonces pij es un camino mı́nimo entre vi y
vj .
p1i
pij
pjk
Demostración: El camino p se puede descomponer como v1 −→ vi −→ vi −→ vk , con coste
w(p) = w(p1i ) + w(pij ) + w(pjk ). Supongamos que existe un camino p0 entre vi y vj con coste
p1i
p0
pjk
w(p0 ) < w(pij ). Entonces v1 −→ vi −→ vi −→ vk es un camino entre v1 y vk con coste
w0 = w(p1i ) + w(p0 ) + w(pjk ) < w(p), contradiciendo la hipótesis que p era un camino mı́nimo
entre v1 y vk .
Los dos lemas siguientes demuestran propiedades de la función δ de distancia mı́nima entre
los vértices de un grafo y una fuente dada. Se trata de generalizaciones a grafos
ponderados de propiedades similares que se han demostrados durante el análisis de la
busqueda en profundidad.
Lemma 5.15.2. Sea G = (V, E) un grafo con función peso w : E → R.
p0
Supongamos que el
camino mı́nimo p entre s y v se puede descomponer en s −→ u → v, con u ∈ V y (u, v) ∈ E.
Entonces el peso del camino más corto entre s y v es δ(s, v) = δ(s, u) + w(u, v).
grafos
89
Demostración: Por el lema precedente, p0 (que es un sub-camino de p), es el camino mı́nimo
entre s y u, entonces
δ(s, v)
= w(p)
= w(p0 ) + w(u, v)
= δ(s, u) + w(u, v)
(5.118)
Lemma 5.15.3. Sea G = (V, E) un grafo ponderado dirigido con función peso w : E → R y
fuente s. Entonces, para cada arco (u, v) ∈ E se cumple δ(s, v) ≤ δ(s, u) + w(u, v).
Demostración: El camino mı́nimo entre s y v tiene un coste δ(s, v), y este es menor o igual
al coste de cualquier otro camino entre s y v. En particular, es menor o igual al coste
p0
del camino s −→ u → v, es decir δ(s, u) + w(u, v).
5.16
Relajación
Los algoritmos de determinación de caminos mı́nimos usan una técnica llamada relajación, y
mantienen, para cada vértice v, dos atributos: el predecesor π[v] de v en el camino que
se está encontrando, y una estimación d[v] de la distancia del camino mı́nimo entre v y la
fuente. Como en el caso de la busqueda BFS (que, recordémolos, es un algoritmo de
caminos mı́nimos para grafos ponderados con función de peso constante), a lo largo de la
ejecución de un algoritmo, d siempre será una estimación por exceso de la distancia
mı́nima, o sea, se cumplirá siempre d[v] ≥ δ(s, v). En el algoritmo BFS, el valor d[v] se
inicializaba a +∞ para cada vértice v y durante la ejecucción del algoritmo, se cambiaba
sólo una vez, justo antes de insertar el vértice v en la cola, asignándole el valor final
δ(s, v). En los algoritmos de esta sección las cosas son más complicadas: el valor d[v] se
cambiará (relajará) varias veces durante la ejecución, decrementándolo cada vez, y
demostraremos que, al final de la ejecución de cada algoritmo de esta sección, se
cumplirá d[v] = δ(s, v).
Las estimaciones del camino más corto se inicializan como en el caso del BFS:
Inicializa(G, s)
1.
foreach v ’cmdin V[G] do
2.
d[v] ← ∞;
3.
π[v] ← NIL;
4.
od;
5.
d[s] ← 0
El proceso de relajación de un arco (u, v) consiste en verificar si se puede mejorar la
estimación actual de d[v] pasando por u y, consecuentemente, cambiando d[v] y π[v]. El
proceso de relajación puede reducir el valor d[v] si pasando por u se obtiene un camino de
coste menor que la estimación actual, pero no puede en ningún caso aumentar el valor d[v],
una propiedad que se usará muchas veces en los teoremas de esta sección. El código
siguiente relaja el arco (u, v) de un grafo con función de peso w:
Relaja(u, v, w)
1.
if d[v] > d[u] + w(u, v) then
cahiers d’informatique
90
2.
3.
4.
d[v] ← d[u] + w(u, v);
π[v] ← u;
fi
La figura 5.19 ilustra dos ejemplos de aplicación de la función de relajación: en el
primer caso el camino por u mejora la estimación actual d[v], mientras que en el segundo
no; consecuentemente en el segundo caso la estimación d[v] no cambia.
d[u]=5 ul
d[u]=5 ul
Figure 5.19:
segundo no.
2
@
@
2
- vld[v]=9
d[u]=5 ul
- vld[v]=7
d[u]=5 ul
Dos ejemplos de relajación:
2
@
@
2
- vld[v]=6
- vld[v]=6
en el primero se cambia el valor d[v], en el
La corrección de los algoritmos de búsqueda del camino más corto depende de las
propiedades de la función de relajación, propiedades que se establecerán en los lemas
siguientes. Observamos que se trata de propiedades que se cumplen para cualquier tipo de
ejecución de Inicializa seguido por una o más ejecuciones de Relaja, independientemente
del hecho que estas llamada sean parte o menos de una búsqueda de camino más corto.
Tanto el algoritmo de Dijkstra como el de Bellman-Ford se componen de una llamada a
Inicializa seguida por varias llamadas a Relaja. La diferencia entre los dos consiste en
la secuencia de arcos que se relajan y en el criterio de terminación utilizado.
Lemma 5.16.1. Sea G = (V, E) un grafo dirigido ponderado con función peso w : E → R, y sea
(u, v) ∈ E. Entonces, después de relajar (u, v) se cumple d[v] ≤ d[u] + w(u, v).
Demostración: Si antes de llamar Relaja(u, v, w) ya es d[v] ≤ d[u] + w(u, v), la función no
cambia d[v], por tanto la propiedad sigue cumpliendose después de la llamada. Si
d[v] > d[u] + w(u, v), entonces la función pone d[v] = d[u] + w(u, v).
Lemma 5.16.2. Sea G = (V, E) un grafo dirigido ponderado con función peso w : E → R, s ∈ V
el vértice de salida y sea el grafo inicializado por Inicializa(G, s). Entonces, después
de un numero arbitrario de llamadas a Relaja, con cualesquiera arcos de E, para cada
v ∈ V se cumple d[v] ≥ δ(s, v). Si después de alguna llamada se cumple d[v] = δ(s, v), entonces
d[v] no variará en ninguna de las llamadas siguientes.
Demostración: Después de la inicialización la desigualdad se cumple banalmente:
d[s] = δ(s, s) ≥ 0 (δ(s, s) = −∞ si s se encuentra en un ciclo negativo, y 0 en otro caso), y
d[v] = ∞ ≥ δ(s, v) para cualquier vértice v 6= s.
La demostración de la validez general del lema es por contradicción. Supongamos que el
lema no se cumpla, y sea v el primer vértice para el cual una relajación del arco (u, v)
causa d[v] < δ(s, v). Entonces, después de la relajación, se cumple
d[u] + w(u, v)
= d[v]
< δ(s, v)
≤ δ(s, u) + w(u, v)
(porque en este paso se ha relajado d[v])
(por la hipótesis)
(por el lema 5.15.3)
(5.119)
grafos
91
o sea, d[u] < δ(s, u). Pero, ya que relajar (u, v) no cambia en ningún caso d[u], esta
relación se cumplı́a antes de relajar el arco (u, v), contradiciendo la hipótesis que v
fuese el primer vértice para el cual el lema no valı́a. Entonces debe ser, d[v] ≥ δ(s, v)
para cada vértice v ∈ V a lo largo de cualquier número de llamadas a Relaja.
Si en algún momento se cumple d[v] = δ(s, v), el valor de d[v] no puede decrementar, en
cuanto acabamos de demostrar que d[v] ≥ δ(s, v). Por otro lado, la función Relaja solo
puede decrementar el valor de d[v], entonces d[v] ni siquiera puede aumentar.
Este lema es importante en cuanto nos garantiza que no es posible "estropear" una
solución llamando demasiadas veces a Relaja. Relaja es, en este sentido, una función
segura : si ya hemos encontrado el camino mı́nimo para llegar a un vértice u y seguimos
relajando los arcos que entran en u, el resultado no cambia.
El lema siguiente se refiere al uso de Inicializa y Relaja en un algoritmo de búsqueda
del camino mı́nimo, y es de importancia fundamental para demostrar la corrección de los
algoritmos de esta sección.
Lemma 5.16.3. Sea G = (V, E) un grafo dirigido ponderado con función peso w : E → R, sea
s ∈ V el vértice de salida y sea el grafo inicializado por Inicializa(G, s). Supongamos
p
que s −→ u → v sea el camino mı́nimo para dos vértices u, v ∈ V , y que se ejecute una serie
de pasos de relajaciń que incluya Relaja(u, v, w). Si d[u] = δ(s, u) antes de la llamada a
Relaja(u, v, w), entonces d[v] = δ(s, v) en cualquier momento después de la llamada.
Demostración: Por el lema 5.16.2, si d[u] = δ(s, u) en cualquier momento de la serie de la
relajación de (u, v), entonces la igualdad se cumple en cualquier momento sucesivo. En
particular, después de relajar (u, v), se cumple
d[v] ≤ d[u] + w(u, v)
= δ(s, u) + w(u, v)
= δ(s, v)
(por el lema 5.16.2)
(por la hipótesis de este lema)
(por el corolario 5.15.2
y porque el camino es un camino mı́nimo)
(5.120)
Pero por el lema 5.16.2, se cumple d[v] ≥ δ(s, v), ası́ que debe ser d[v] = δ(s, v).
Al final de todo algoritmo de esta sección, los valores d[u] serán iguales a la distancia
del camino mı́nimo entre la fuente s y el vértice u. Además, los punteros π[u] definirán
el grafo Gπ = (Vπ , Eπ ) definido en 5.116 y 5.117.
Los lemas siguientes caracterizan este grafo.
Lemma 5.16.4. Sea G = (V, E) un grafo ponderado con función peso w : E → R sin ciclos de
coste negativo y s ∈ V . Tras llamar a la función Inicializa(G,s) y un número arbitrario
n ≥ 0 de veces la función Relaja(u, v, w) con cualesquiera vértices u y v del grafo, para
cada vértice v ∈ Vπ , el grafo de caminos Gπ contiene un camino de s a v.
(n)
(n)
(n)
Demostración: Sea Gπ = (Vπ , Eπ ) el grafo que se consigue tras n llamadas a Relaja.
(n)
Demostraremos por inducción sobre n que el lema se cumple por cada Gπ .
(0)
Si n = 0, Gπ contiene sólo la fuente s, y el lema se cumple banalmente. Sea el lema
(n)
cierto para Gπ , y supongamos que la próxima llamada a Relaja relaje el arco (u, v). Hay
que distinguir dos casos:
i) antes de la ejecucción se cumplı́a d[v] ≤ d[u] + w(u, v); en este caso, la llamada a Relaja
(n+1)
(n)
no cambia nada, Gπ
= Gπ , y el lema sigue cumlpiéndose.
cahiers d’informatique
92
ii) antes de la ejecución se cumplı́a d[v] > d[u] + w(u, v); el único cambio concerne v, por
(n+1)
tanto será suficiente demostrar que en Gπ
hay un camino de s a v.
Si d[v] > d[u] = w(u, v), entonces antes de la llamada era d[u] < ∞ y, en el momento en
que se habı́a asignado el valor d[u], también se habı́a puesto d[u] 6= Nil (a menos que
no fuese u = s). Supongamos que esto haya occurrido en la llamada número k a Relaja
(k)
(k ≤ n). El vértice se ha insertado en Vπ en esta llamada y, como π[u] no puede
(n)
(n)
volver nunca a ser Nil, es también u ∈ Vπ . (Si era u = s, claramente u ∈ Vπ , ya
(n)
que s está en todo Vπ por definición.) Por la hipótesis inductiva, hay un camino
p
(n)
(n)
s → u que, encontrándose en Gπ , está formado por vértices de Vπ . Siendo
(n+1)
(n)
(n+1)
(n+1)
Vπ
=π ∪{v}, p se encuentra también en Gπ
y, siendo π[v] = u, (u, v) ∈ Eπ
.
p
v
(n+1)
Por tanto s → u → es un camino de s a v en Gπ
.
Theorem 5.16.1. Sea G = (V, E) un grafo ponderado con función peso w : E → R sin ciclos de
coste negativo y s ∈ V . Tras llamar a la función Inicializa(G,s) y un número arbitrario
n ≥ 0 de veces la función Relaja(u, v, w) con cualesquiera vértices u y v del grafo, el
grafo Gπ es un árbol con raı́z s.
Demostración: Por el lema 5.16.4 hay un camino de s a cualquier vértice de Gπ , por tanto,
si se ignora el sentido de las aristas de Gπ , se consigue un grafo no dirigido conexo
(para ir de un vértice u a cualquier vértice v se puede ir de u a s en sentido contrario
al sentido de las aristas y luego de s a v).
Por definición del grafo Gπ , es |Eπ | = |Vπ | − 1.
27), Gπ es un árbol.
Por tanto, por el teorema 2.6.2 (página
Theorem 5.16.2. Sea G = (V, E) un grafo ponderado con función peso w : E → R sin ciclos de
coste negativo y s ∈ V . Tras llamar a la función Inicializa(G,s) y se llame la función
Relaja(u, v, w) un número de veces tal que, al final, para cada vértice del grafo sea
d[u] = δ(s, u). Entonces Vπ contiene todos los vértices alcanzables desde s y, para cada
u ∈ Vπ el camino (único) de s a u en Gπ es un camino de coste mı́nimo de s a u en el grafo
G.
Demostración: Demostremos primero que Vπ es el conjunto de vértices alcanzables desde s.
La distancia δ(s, v) es finita si y sólo si v es alcanzable y, por tanto, d[v] es finito si
y sólo si v es alcanzable. Pero para cada v 6= s se asigna a d[v] un valor finito en la
misma llamada a Relaja en que se pone π[v] 6= Nil, es decir, cuando se pone v en Vπ . Por
tanto, v ∈ Vπ si y sólo si v es alcanzable desde s.
p
Dado ahora v ∈ Vπ , demostraremos que el (único) camino s → v en Gπ es un camino mı́nimo en
el grafo. Sea p = [v0 , . . . , vk ], con v0 = s y vk = v. Para cada i con 1 ≤ i ≤ k se cumple
d[vi ] = δ(s, vi ) (hipótesis del teorema). También, para cada vi es π[vi ] = vi−1 . Entonces, la
última vez que se cambió vi , se puso
π[vi ] ← vi−1
d[vi ] ← d[vi−1 ] + w(vi−1 , vi )
(5.121)
Desde entonces, vi no se ha vuelto a cambiar y, si se ha cambiado d[vi ], su valor sólo se
puede haber reducido (una llamada a Relaja nunca incrementa el valor d), ası́ que será:
d[vi ] ≥ d[vi−1 ] + w(vi−1 , vi ),
(5.122)
grafos
93
es decir
w(vi−1 , vi ) ≤ δ(s, vi ) − δ(s, vi−1 )
(5.123)
Sumando para todos los vértices del camino conseguimos
w(p)
Pk
=
w(vi−1 , vi )
Pi=1
k
≤
(δ(s,
vi ) − δ(s, vi−1 ))(por la (5.123))
i=1
= δ(s, vk ) − δ(s, v0 )
(suma telescópica)
= δ(s, vk )
(porque v0 = s)
(5.124)
Es decir, w(p) ≤ δ(s, vk ). Pero siendo δ(s, vk ) la distancia de camino mı́nimo de s a vk es
también w(p) ≥ δ(s, vk ) y por tanto w(p) = δ(s, vk ), o sea, w(p) es un camino mı́nimo.
La definición del grafo Gπ asume implı́citamente que π[s] = Nil. Si no fuera ası́, no
tendrı́amos ninguna justificación para eliminar s del conjunto de vértices que se utilizan
en la definición de Eπ , y ya no se podrı́a deducir que |Eπ | = |Vπ | − 1, una propiedad que
necesitamos para demostrar que Gπ es un árbol. El teorema siguiente justifica esta
decisión demostrando que π[s] 6= Nil sólo cuando el grafo contiene ciclos de coste
negativo.
Theorem 5.16.3. Sea G = (V, E) un grafo ponderado con función peso w : E → R sin ciclos de
coste negativo y s ∈ V . Tras llamar a la función Inicializa(G,s) y un número arbitrario
n ≥ 0 de veces la función Relaja(u, v, w) con cualesquiera vértices u y v del grafo, Si
en el curso de estas llamadas se pone en algun momento π[s] 6= Nil, entonces hay en el
grafo un ciclo de coste total negativo.
Demostración: Empecemos demostrando que el grafo tiene un ciclo y que, en el momento en
que se pone π[s] 6= Nil, este ciclo es un ciclo del grafo Gπ . Sea (vk , s) la arista
relajando la cual se pone por primera vez π[s] 6= Nil. En este momento, todavı́a no se ha
cambiado el vértice s que, por tanto, sigue teniendo los valores que le habı́a dado
Inicializa. Durante esta llamada, la condición del if de la linea 1 de Relaja se cumple.
Por tanto es
0 = d[s] > d[vk ] + w(vk , s)
(5.125)
condición que implica d[vk ] < ∞. Por el lema 5.16.2 es siempre d[vk ] ≥ δ(s, vk ) y, por
p
tanto, δ(s, vk ) < ∞. Es decir, vk es alcanzable desde s a través de un camino s → vk .
A~
nadiendo a este camino la arista (vk , s) se consigue un ciclo en el grafo.
En el momento en que se puso d[vk ] < ∞, también se puso π[vk ] 6= Nil y desde este momento,
p
es vk ∈ Vπ . Por el lema 5.16.4 hay un camino s → vk en Gπ . Después de relajar el arco
p
s
(vk , s) se pone π[s] = vk y por tanto (vk , s) ∈ Eπ . EL ciclo s → vk → es entonces un ciclo en
Gπ .
Queda por demostrar que este ciclo tiene un coste negativo. Por tal fin, se escribe el
ciclo como p = [v0 , . . . , vk , vk+1 ], con v0 = vk+1 = s. En el momento en que se relaja el arco
(vk , s) se cumple la (5.125), que podemos escribir como
d[vk+1 ] > d[vk ] + w(vk , vk+1 )
(5.126)
Para los demás vi , se cumple π[vi ] = vi−1 y por tanto la última vez que se cambió
π[vi ] ← vi−1
d[vi ] ← d[vi−1 ] + w(vi−1 , vi )
(5.127)
cahiers d’informatique
94
Desde entonces, vi no se ha vuelto a cambiar y, si se ha cambiado d[vi ], su valor sólo se
puede haber resucido (una llamada a Relaja nunca incrementa el valor d), ası́ que será:
d[vi ] ≥ d[vi−1 ] + w(vi−1 , vi ),
(5.128)
Sumando la (5.126) y todas las (5.128) se consigue
k+1
X
i=1
d[vi ] >
k+1
X
d[vi−1 ] +
i=1
k+1
X
w(vi−1 , vi )
(5.129)
i=1
pero, siendo v0 = vk+1 , es
k+1
X
d[vi ] =
i=1
k+1
X
d[vi−1 ]
(5.130)
i=1
y por tanto
0>
k+1
X
w(vi−1 , vi ).
(5.131)
i=1
Es decir, el camino p es un ciclo de coste total negativo.
5.17
El algoritmo de Dijkstra
El algoritmo de Dijkstra determina el camino mı́nimo entre un vértice de salida y los
demás vértices de un grafo dirigido ponderado con función de peso no negativa. Gracias a
esta limitación a priori, para este algoritmo no tendremos que preocuparnos de la
presencia de ciclos con coste negativo.
El algoritmo mantiene un conjunto S de vértices de que ya se ha determinado el camino
mı́nimo; es decir, tal que para todo v ∈ S, se cumpla d[v] = δ(s, v). En cada iteración, el
algoritmo selecciona el vértice v ∈ V − S con el valor mı́nimo de d[v], relaja todos los
arcos que salen de v, e introduce v en S. En la implementación del algoritmo se usa una
cola de prioridad Q para guardar los vértices de V − S.
Dijkstra(G, w, s)
1.
Inicializa(G, s);
2.
S ← ∅;
3.
Q ← V[G];
4.
while not empty(Q) do
5.
u ← Q.extract min;
6.
S ← S ∪ {u};
7.
foreach v in u.ady do
8.
Relax(u, v, w);
9.
od;
10. od;
El algoritmo relaja los arcos como ilustrado en figura 5.20. La lı́nea 1 inicializa el
valor de distancia de todo vértice, excepto la fuente, a +∞, y las lı́neas 2 y 3
inicializan el conjunto se vértices analizados (que en este momento está vacı́o), y la
introducen todos los vértices en la cola de prioridad Q (que, en todo momento, contiene
el conjunto V − S de vértices no analizados). A cada iteración del bucle, el vértice de
V − S con la estimación de distancia menor se extrae de Q (en la primera iteracción el
grafos
95
vértice extraı́do será, por supuesto, s, dado que todos los demás vértices tienen una
estimación igual a ∞), y lo inserta en S. Las lı́neas 7 y 8 relajan todos los vértices
que están en el entorno del vértice extracto de Q.
s
t
u
8
89:;
89:;
/) ?>=<
/ ?>=<
∞
:v ∞
v: U
v
v
1 vvv
3 vvv
1
2
5
vv
vv 2
vvv 4 + vvv
9
?>=<
89:;
?>=<
89:;
89:;
/5 ?>=<
∞ k 1
∞
∞
?>=<
89:;
0
w
s
12
4
x 15
[s, t, u, w, x, y]
t 12
y
u
)+3 @ABC
8
89:;
GFED
/ ?>=<
3
@
8
; 11T
x
y
y
x
y
y
x
y
3 x
1 yy
1
2
5
xx 2
yyyy
yyyyyyy4
xxxx
.
&
9
@ABC
GFED
89:;
?>=<
89:;
6
2 j 1 ?>=<
19/ 17
?>=<
89:;
0
w
4
15
x
[x, u, y]
y
s
t
u
8
?>=<
GFED
/%- @ABC
+3 89:;
12
; 4
x
v; T
x
v
x
v
3 vv
1 xx
2
5
1
x
v 2
vv
xxxx 4
v
v 9
+
?>=<
89:;
?>=<
89:;
?>=<
/5 89:;
∞
∞
1
2 j
?>=<
89:;
0
12
4
15
w
x
[w, t, u, x, y]
t 12
s
w
u
s
4
15
w
x
[u, y]
t 12
s
y
t
u
%-/ GFED
8
?>=<
@ABC
/ 89:;
3
@
8
; 12T
x
y
y
x
y
y
x
y
3 x
1 yy
2
5
1
xx 2
yyyy
yyyyyyy4
xxxx
.
&
9
89:;
?>=<
?>=<
G@ABC
FED
6
2 j 1 89:;
19/ 17
89:;
?>=<
0
y
(/ 89:;
8
?>=<
?>=<
/ 89:;
3
7? 9 T
@
8
x
y
x
y
x
y
x
y
x
y
1 yy
3 xx
1
2
5
xxxx 2
yyyy
yyyyyyy4
xxxxxxx
.
&
9
@ABC
GFED
?>=<
89:;
?>=<
2 j 1 89:;
6
5+3 15
89:;
?>=<
0
s
12
4
15
x
[t, x, u, y]
t 12
y
u
(/ 89:;
8
?>=<
?>=<
/ 89:;
3
7? 9 T
@
8
x
y
x
y
x
y
x
y
x
y
3 xx
1 yy
1
2
5
xxxx 2
yyyy
yyyyyyy4
xxxxxxx
.
&
9
G@ABC
FED
?>=<
89:;
?>=<
6
2 j 1 89:;
5/ 11
89:;
?>=<
0
w
4
15
x
[y]
y
u
(/ 89:;
8
?>=<
?>=<
/ 89:;
7? 9 T
8@ 3
x
y
x
y
x
y
x
y
x
y
1 yy
3 xx
2
5
1
xxxx 2
yyyy
yyyyyyy4
xxxxxxx
.
&
9
?>=<
@ABC
GFED
89:;
?>=<
2 j 1 89:;
6
5/ 11
89:;
?>=<
0
w
4
x
[]
15
y
Figure 5.20: Ejecución del algoritmo de Dijkstra. Los vértices indicados con un cı́rculo
no se han todavı́a analizados, y se encuentran en la cola Q; los vértices indicados con
un doble cı́rculo ya se han analizados, y se encuentran en el conjunto S; el vértice
cuadrado es el vértice que se elegirá en el próximo ciclo a la lı́nea 4. Los valores
en los cı́rculos son la estimación de la distancia del camino más corto.
Tiempo de ejecución
En la lı́nea 3, todos los vértices se insertan en la cola; en cada iteración del bucle
4--10 se extrae un vértice, y nunca se vuelve a insertar ningún vértice, Por tanto, el
bucle "while" se ejecuta |V | veces. La inicialización toma un tiempo O(V ). Si la cola
de prioridad de implementa con un heap, la función extract min se ejecuta en un tiempo
O(log V ), por tanto el tiempo de ejecución del bucle, excepto por la llamada a Relaja, es
O(V log V ).
La función Relaja se llama una vez para cada arco. Si en la función se cambia el valor
de d[v], es necesario ejecutar la función heapify para reconstituir el heap (se vea el
cahiers d’informatique
96
capı́tulo II), operación que toma un tiempo O(log V ). En el caso peor, heapify se ejecuta
cada vez que se ejecuta Relaja, o sea una vez para cada arco del grafo, por un tiempo
total de O(E log V ).
El tiempo total de ejecución del algoritmo es entonces O((V + E) log V ). Es posible
mejorar este tiempo usando una estructura de heap de Fibonacci para implementar la cola
de prioridad. En esta estructura, reajustar el heap después de la relajación toma un
tiempo medio O(1), ası́ que el tiempo de ejecución del algoritmo es O(V log V + E).
Corrección
En cada iteración el algoritmo de Dijkstra elige el vértice de Q con el valor mı́nimo de
d, lo saca de la cola, lo analiza, y lo inserta en el conjunto S: una vez que un vértice
se ha elegido, la decisión no se cambia, y el vértice queda en S. Una estrategia de este
tipo se llama estrategia avara (greedy strategy). Estas estrategias llegan muy
rápidamente a una solución en los problemas de optimización en que se pueden utilizar,
porque sólo tienen que tomar una decisión para cada elemento de la solución, sin
necesidad de volver a analizar y quizás a cambiar decisiones ya tomadas.
Desafortunatamente, no todos los problemas de mı́nimo admiten ser solucionados con una
estrategia avara (de hecho, muy pocos lo admiten); por otro lado, el problema de camino
mı́nimo sin pesos negativo si lo hace, como podremos corroborar demostrando la corrección
del algoritmo de Dijkstra. La clave para demostrar que el algoritmo encuentra una
solución óptima es mostrar que, cada vez que se inserta un vértice u en al conjunto S, se
cumple d[u] = δ(s, u).
Theorem 5.17.1. Tras ejecutar el algoritmo de Dijkstra en un grafo G = (V, E) con funcion
peso w : E → R+ ∪ {0} y salida s ∈ V , se cumple d[v] = δ(s, v) para todo v ∈ V .
Demostración: La demostración se efectuará por inducción sobre el número de vértices que
se salen de la cola de prioridad Q. Después de la inicialización, se cumple d[s] = 0 y
v 6= s ⇒ d[v] = ∞. Entonces la primera vez que se ejecuta la instrucción 5, el vértice que
sale de la cola es s y, para este vértice, d[s] = 0 = δ(s, s).
Supongamos ahora que el teorema se cumple para los primeros k vértices que se extraen de
la cola, y sea v el vértice número k + 1 que se extrae. Si v no es accesible, se cumple
d[v] ≥ δ(s, v) = ∞ (por el lema 5.16.2), o sea d[v] = ∞ = δ(s, v). Si v es accesible,
consideremos un camino de coste mı́nimo de s a v. Sea y el primer vértice de este camino
que todavı́a se encuentra en la cola, y x el vértice que lo precede (se vea la figura
5.21).
p
p0
Escribamos el caminos cómo s −→ x → y −→ v.
Demostremos que d[y] = δ(s, y). Ya que x ha salido de la cola antes de v, para la hipótesis
p
de inducción se cumple d[x] = δ(s, x). Por el lema 5.15.1, s −→ x → y es un camino de coste
mı́nimo de s a y y, en el ciclo en que se sacó x de la cola, en paso 8, se llamó Relaja(x,
y, w) luego, por el lema 5.16.2 se cumple d[y] = δ(s, y).
Entonces tenemos las desigualdades siguientes:
d[y]
= δ(s, y)
≤ δ(s, v)
≤ d[v]
(como se ha demostrado arriba)
(porque los pesos no son negativos y el camino hasta v es más largo)
(por el lema 5.16.1)
(5.132)
grafos
97
Por el otro lado, v sale de la cola antes de y, ya que v es el próximo elemento que sale,
entonces d[v] ≤ d[y]. Las dos desigualdades proporcionan d[v] = d[y] = δ(s, v).
Después que d[v] ha alcanzado el valor δ(s, v) no cambia por el lema 5.16.2.
- A
s
AA
Q
- x
- y
v
6
A
AA
Figure 5.21:
Ilustración de la dimostración de la corrección del algoritmo de Dijkstra.
Corollary 5.17.1. El sub-grafo Gπ = (Vπ , Eπ ), donde
Vπ = {s} ∪ {v ∈ V |π[v] 6= Nil},
(5.133)
Eπ = {(π[v], v)|v ∈ Vπ − {s}}
(5.134)
y π[v] es el valor asignado por el algoritmo de Dijkstra define un árbol formado por
caminos de coste mı́nimo.
Demostración: La demostración se efectua por inducción sobre el número de vértices que
constituyen un camino. Si este número es cero, el camino es de s a s, y el teorema se
cumple banalmente.
Supongamos que el teorema se cumpla para caminos de k arcos, y sea v un vértice cuyo
camino contiene k + 1 arcos. El valor π[v] se asigna en el momento en que se asigna d[v], y
d[v] tiene el valor d[v] = δ(s, v) = d[π[v]] + w(π[v], v) = δ(s, π[v]) + w(π[v], v), donde
d[π[v]] = δ(π[v], v) por la hipótesis de inducción, ya que el camino hasta π[v] contiene k
arcos. Entonces el camino hasta π[v] es una camino de coste mı́nimo, luego el camino hasta
v también es de coste mı́nimo.
5.18
El algoritmo de Bellman-Ford
A diferencia del algoritmo de Dijkstra, el algoritmo de Bellman-Ford se aplica al caso
general en que un grafo ponderado contenga pesos positivos y negativos, o sea en que el
dominio de la función w : E → R no es restringido a números no negativos. Esta mayor
generalidad, claramente, se paga: el tiempo de ejecución del algoritmo de Bellman-Ford
es mayor (asintóticamente) que él del algoritmo de Dijkstra. Como ya el algoritmo de
Dijkstra, el algoritmo de Bellman-Ford usa la técnica de la relajación, disminuyendo la
estimación de camino más corto entre la fuente y cada vértice v ∈ V hasta estabilizarse
cuando todas las estimaciones han llegado al valor δ(s, v). El algoritmo retorna FALSE si
el grafo contiene ciclos negativos accesible desde el vértice de salida.
Bellman-Ford(G, w, s)
1.
Inicializa(G, s);
cahiers d’informatique
98
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
for i=1 to |V|-1 do
foreach (u,v) in E do
Relaja(u, v, w);
od
od
foreach (u,v) in E do
if d[v] > d[u] + w(u, v) then
return FALSE;
fi;
od;
return TRUE;
(hay un ciclo negativo)
El algoritmo ejecuta |V | − 1 veces un bucle (lı́nea 2) en que se relaja una vez cada arco
del grafo (lı́neas 3-5). Un ejemplo de ejecución del algoritmo se encuentra en figura
5.22. Observamos que no hay un orden definido en que se relajan los arcos (ası́ como
pasaba en el algoritmo de Dijkstra): el algoritmo se basa en la propiedad que relajando
cada arco |V | − 1 veces siempre se consigue encontrar el camino más corto para cada
vértice, a menos que haya ciclos con coste negativo, en cual caso el ciclo se detecta con
el test de lı́nea 8.
w
*
89:;
?>=<
89:;
?>=<
89:;
/ ?>=<
∞ Aj −2
∞
∞A
U AA
AA
}> O
}
AA
AA }}
AA9
AA}}
AA 8
}A
1
7
−3 }} AA −4
−1
AA
AA
}
A
}
A
A
}}
* ?>=<
?>=<
89:;
89:;
89:;
/ ?>=<
7
∞ j
∞
∞
z
v
5
6
9
−3
u
z
y
x
v
w
) 89:;
6
89:;
?>=<
89:;
/ ?>=<
5
3 ==em −2 ?>=<
0=
U ==
====
<D O
==
==== ==9
====
== 8
==
1
7
−1
== −3 =====−4
=
== =="
%- ?>=<
?>=<
89:;
89:;
>=<
/ ?89:;
7
8
2
1 i
9
5
u
−3
x
y
z
v
w
*
6
89:;
?>=<
?>=<
89:;
?>=<
+3 89:;
0 >>
6 @i
−2
? ∞O
U >>>>
@@
~
~
>
@@ ~~
>>>
@@~~
>>>>9
~@
>>> 8
7
1
>
−3
−1
~~ @@@−4
>>>>
~
@
~
>># ~~
@
* 89:;
89:;
?>=<
89:;
?>=<
?>=<
/
7
∞
∞
1 i
5
9
u
z
−3
y
w
x
v
) 89:;
6
?>=<
89:;
?>=<
?>=<
/ 89:;
3 ??em
0=
−2
;C 5O
U ==
????



==
???? 
==9
????
== 8
1
??????−4 7
−3 
−1
==

????
== 
#

%
89:;
?>=<
GFED
?>=<
89:;
/ @ABC
7
−1
8
1 i
9
5
−3
u
x
y
z
v
w
)
6
?>=<
89:;
?>=<
?>=<
+3 89:;
0=
6 ==i −2 89:;
<D 5O
U ==
====
==
==== ==9
====
== 8
==
7
1
−1
== −3 =====−4
=
=
=="
= %- 89:;
?>=<
89:;
?>=<
?>=<
/ 89:;
7
8
2
1 i
9
5
u
z
−3
y
w
x
v
) 89:;
6
?>=<
?>=<
89:;
?>=<
/ 89:;
3 ??em
0=
−2
;C 5O
U ==
????



==
???? 
==9
????
== 8
1
??????−4 7
−3 
−1
==

????
== 
#

%
89:;
?>=<
@ABC
89:;
?>=<
/ GFED
7
−1
8
1 i
9
5
−3
u
x
Figure 5.22: Una ejecución del algoritmo Bellman-Ford. El vértice de salida es z; los
valores de d son reportados dentro de los cı́rculos que representan los vértices. Se
asume que, a cada paso, los arcos se relajan en orden lexicográfico. La primera figura
muestra la situación después de la inicialización, las siguentes muestran la situación
después de cada ejecución del bucle. En las últimas dos la situación no ha cambiado: el
algoritmo se ha estabilizado y desde este momento ninguna llamada a Relaja cambiará la
situación.
y
grafos
99
Tiempo de ejecución
La inicialización del algoritmo toma un tiempo O(V ). Cada una de las |V | − 1 ejecuciones
del bucle 2--6 toma un tiempo O(E) (cada una consta en |E| ejecuciones de Relaja), y el
bucle de las lı́neas 7--12 toma un tiempo O(E). Por tanto, el tiempo de ejecución del
algoritmo es O(V E). Estas observaciones subrayan que la posibilidad de funcionar con
grafos de coste negativo se paga en tiempo de ejecución: el algoritmo Bellman-Ford es
más lento que el algoritmo de Dijkstra que, recordamos, tiene un tiempo de ejecución
asintótico de O((V + E) log V ).
El tiempo asintótico de ejecución del algoritmo no se puede mejorar, pero se puede
mejorar el tiempo efectivo de ejecución, por lo menos en la mayorı́a de los casos. El
algoritmo standard de Bellman-Ford relaja |V | veces todos los arcos, incluso los arcos
que ya se han estabilizado, o sea, los arcos por los que ya se ha encontrado un camino
mı́nimo. Se puede mejorar la velocidad del algoritmo si se visitan en cada iteración sólo
los vértices por los cuales en la iteracción precedente se ha cambiado la estimación de
distancia. Esta versión del algoritmo usa una cola para guardar los vértices que se van
a visitar.
Bellman-Ford-Fast(G, w, s)
1.
Inicializa(G, s);
2.
enqueue(s, Q);
3.
enquqeued[s] ← true;
4.
while Q 6= ∅ do
5.
u ← extract(Q);
6.
enqueued[u] ← false;
7.
foreach v in u.ady do
8.
if d[v] > d[u] + w(u, v); then
9.
d[v] ← d[u] + w(u, v);
10.
π[v] ← u;
11.
if not enqueued[v] then
12.
enqueue(v, Q);
13.
enqueued[v] ← true;
14.
fi
15.
fi
16.
od
17. od
Este algoritmo no funciona con ciclos negativos: si hay un ciclo negativo, la estimación
del coste de los vértices que lo componen seguirá bajando y los vértices se seguirán
insertando en la cola. Por tanto, el algoritmo no termina. Se puede modificar el
algoritmo, marcando el número de veces que un vértice se introduce en la cola, de manera
tal que se detecten ciclos de coste negativo. Esta modificación, y la propiedad en que
se basa, son el objeto de los ejercicios 9 y 10 de esta sección.
Corrección
Para demostrar la corrección del algoritmo de Bellman-Ford (no optimizado), empezamos
demostrando que, si no hay ciclos negativos, el algoritmo determina el camino más corto
para todo vértice accesible desde el vértice de salida.
Lemma 5.18.1. Sea G = (V, E) un grafo dirigido ponderado con vértice de salida s y función
cahiers d’informatique
100
de peso w : E → R, sin ciclos de coste negativo accesibles desde s. Entonces, al final
de la ejecución de Bellman-Ford, se cumple d[v] = δ(s, v) para todo vértice v accesible
desde s.
Demostración: Sea v un vértice accesible desde s, y sea p = [v0 , . . . , vk ] un camino más corto
de s a v, con v0 = s, vk = v. Ya que no hay ciclos negativos, el camino p es simple (no
contiene ciclos), y por tanto k ≤ |V | − 1. Demostramos por inducción que, para i = 0, . . . , k,
tras la iteración número i del algoritmo se cumple d[vi ] = δ(s, vi ) y que esta igualdad se
mantiene durante todas las iteraciones siguientes. Recordamos que cada iteración
consiste sólo en llamadas a la función Relaja, por tanto los lemas 5.15.3--5.16.2 se
cumplen.
Para i = 0, después de la inicialización, se cumple d[s] = 0 = δ(s, s) y, para el lema 5.16.1,
esta igualdad no cambia después. Supongamos ahora que tras i − 1 iteraciones, se cumpla
d[vi−1 ] = δ(s, vi−1 ). En la iteración número i se relaja el arco (vi−1 , vi ) y por tanto, por el
lema 5.16.2, se cumple d[vi ] = δ(s, vi ), y por el lema 5.15.3 la igualdad se mantiene en las
iteraciones siguientes. Como k < |V | − 1, el algoritmo ejecutará por lo menos k
iteraciones, ası́ que al final se cumplirá d[v] = δ(s, v).
Este lema contiene la idea fundamental del algoritmo. El lema 5.16.2 nos garantiza que,
si p = [v0 , . . . , vk ] es un camino más corto y, en un momento dado de la ejecución, ya hemos
estimado correctamente p hasta el vértice vi , una llamada a Relaja(vi , vi+1 , w) nos
proporcionará una estimación correcta hasta vi+1 . Entonces, llamando k veces la función
Relaja con los arcos correctos, al final tendremos la estimación correcta del camino. Si
no hay ciclos negativos, ningún camino contiene más de |V | − 1 arcos, por tanto relajando
|V | − 1 veces todos los arcos del grafo se encontrarán todos los caminos.
Para terminar la demostración de la corrección de Bellman-Ford, solo queda demostrar que
si no hay ciclos negativos el algoritmo retorna TRUE y si los hay el algoritmo retorna
FALSE. Estos resultados, ası́ como el resultado precedente, forman el teorema siguiente:
Theorem 5.18.1. Sea G = (V, E) un grafo ponderado con salida s ∈ V y peso w : E → R; se
ejecute Bellman-Ford en este grafo. Entonces, si el grafo no contiene ciclos negativos,
el algoritmo retorna TRUE y al final de cumple d[v] = δ(s, v) para todo vértice v. Si el
grafo contiene un ciclo negativo accesible desde s, el algoritmo retorna FALSE.
Demostración: Supongamos que el grafo no contenga ciclos negativos. Demostramos que
d[v] = δ(s, v) para todo v. Si v es accesible desde s, entonces el lema 5.18.1 demuestra el
resultado. Si v no es accesible desde s, el lema 5.16.2 garantiza que d[v] ≥ δ(s, v) = ∞, o
sea d[v] = ∞ = δ(s, v). Todavı́a debemos demostrar que el algoritmo retorna TRUE. Al final
del bucle a las lı́neas 2--6 se cumple, para cada arco (u, v) ∈ E
d[v]
= δ(s, v)
≤ δ(s, u) + w(u, v)
= d[u] + w(u, v)
(por el lema 5.16.1)
(5.135)
por tanto ningún arco cumple la propiedad del if de lı́nea 8, y la instruccción return
FALSE no se ejecuta. Se ejecuta entonces la lı́nea 12, y el algoritmo retorna TRUE.
Supongamos ahora que el grafo contenga un ciclo de coste negativo c = [v0 , . . . , vk ] con v0 = vk
accesible desde s. Entonces
k
X
w(vi−1 , vi ) < 0
(5.136)
i=1
grafos
101
Si Bellman-Ford retornara TRUE, el if de la linea 8 no se cumplirı́a para ningún arco o
sea, para los arcos del ciclo, se cumplirı́a d[vi ] ≤ d[vi−1 ] + w(vi−1 , vi ), i = 1, . . . , k. Sumando
estas desigualdades a lo largo del ciclo c se obtiene
k
X
i=1
d[vi ] ≤
k
X
d[vi−1 ] +
i=1
k
X
w(vi−1 , vi ).
(5.137)
i=1
Cada vértice de c aparece exactamente una vez en cada una de las primeras dos sumas
(recordamos que v0 = vk ), ası́ que
k
X
d[vi ] =
i=1
entonces
0≤
k
X
d[vi−1 ],
(5.138)
i=1
k
X
w(vi−1 , vi ),
(5.139)
i=1
contradiciendo la hipótesis que el ciclo fuese de coste negativo.
retorna FALSE.
Por tanto el algoritmo
cahiers d’informatique
102
5.19
Ejercicios
1. Ejecuten el algoritmo de Dijkstra en los grafos siguientes, empezando por el vértice
marcado con una s.
/.-,o 7 /.-,
()*+
()*+
{ O `AAA 7 }} O
{
AA }}
{{
A}
1 {{
2
{
}} AAA4
2
{
}
A
}
{ 2
()*+{}
/.-,
()*+
()*+
//.-,~} 1 /.-,
/
u
?>=<
89:;
s
U
3
6
1
/.-,`A 7 ()*+
()*+
// .-,o
O AA
}>
}
AA }}
A}
2
}} AAA 5
}
A
}
()*+}o 7 3 /.-,
/.-,
()*+
1
()*+
/.-,
89:;
?>=<
/
sC
{
O CC
{
CC {{2
CC
4 4 {{
{
C 2
{{ 8 CC! {
}
()*+
()*+
/.-,
/
//5 .-,
2
6
5
2
()*+Mf
/.-,; 2 //.-,
()*+
MMM2
O ;
;; MMM
2 ; 2
M
;
()*+
/.-,
;;
2
2
8D
q
q
;
2
q
q
;
q
; qq
()*+o 2 /.-,
/.-,
()*+q
5
()*+
/.-,
3
//.-,
k()*+
kkk
k
k
k
kkk
kkk 5
k
k
k
()*+ku k
/.-,
// .-,
()*+
// .-,
()*+
1
7
>=<
/ ?89:;
s
9
#
3
()*+
/.-,
/.-,8o
()*+
()*+o
/.-,
I 8 2 3 _??? 1
88 ??
88
89:;
s
3 88
9 ?>=<
2
88
 ????1


8
?

 6
()*+o
/.-,
()*+
/.-,
()*+
//.-,
17
)/.-,
/.-,Bi
()*+
()*+Bo
()*+
/.-,
O BB 3
BB13
|= U
|
BB ||
BB
B|B|
BB 7
1 9
B
BB 2 ||| BBBB
7
5
B! ||
B! ()*+
/.-,
/.-,
()*+o
89:;
?>=<
s 5
O
|O
15
| O Z
|
|
|
2
7
|
|
|| 4 3
||
|
|
|| 3
||
||1
}|||
}
|
()*+
/.-,
()*+
()*+
//.-,
//.-,
5
8
8
2
1
5
4
2
1
1
4
3
()*+
/.-,
/.-,O
()*+
()*+
//.-,
/ 5Z
D
33 OOO2
5
O
5
33 OO 55
OOO
33
525
OO
1 3
33 OOOO 55 7
OO' 1
()*+
/.-,
?>=<
/ 89:;
s
4
*
()*+
/.-,
/.-,Hj
()*+
3
O
O HH
u
u
HH
uu
HH
u
H# zuuu 2
1
()*+j
/.-,
1
2
x
8
5 xxx
7
x
xx
* 89:;
()*+x| o
/.-,
?>=<
s
2
1
5
2 //.-,
()*+
89:;
?>=<
s?
??
??4 1
3
??
? ()*+
/.-,
()*+
//.-,
5
9
2. Dibujen un grafo y una fuente s con la
existe un árbol de caminos mı́nimos con
caminos mı́nimos con fuente s que no lo
este tipo que contiene tres vértices.
siguiente propiedad: para cada arco (u, v) ∈ E
fuente s que contiene (u, v) y otro árbol de
contiene. (Sugerencia: existe un grafo de
El grafo contiene arcos de coste negativo.)
3. ¿Dónde falla la demostración de la corrección del algoritmo de Dijkstra si el grafo
contiene arcos de coste negativo?
4. Den un ejemplo de grafo con arcos de coste negativo por el cual el algoritmo de
Dijkstra da una respuesta equivocada.
5. Escriban el algoritmo de Dijkstra para un grafo representado como matrı́z de
adyacencia. ¿Cuál es el tiempo de ejecución de esta versión?
6. Ejecuten el algoritmo de Bellman-Ford en los grafos del ejercicio 1.
7. Ejecuten el algoritmo de Bellman-Ford en los grafos siguiente comprobando, al final
del algoritmo, si el grafo contiene ciclos de coste negativo
grafos
103
4
4
()*+
/.-,
/.-,aBo
()*+
()*+
/.-,
/
BB 5
|D = aBBB
|
|
|
BB || 1
BB ||
B
B
| 1
|
|B
|B
3
4
|| BBB 3 ||| BBB
|
B }| | 1 B || 3
()*+
/.-,
/.-,
()*+
/.-,
/
/()*+
2
()*+
()*+ −4 //.-,
/.-,aBo −4 /.-,
()*+
BB−3 ||D = aBBB
|
|
BB ||
BB ||
B|B| 6 5
B||B −1
4
B
|
2
| BB
|| BBB
|
|
B }| | 1 B | 2
()*+|
/.-,
()*+
()*+
//.-,
//.-,
−2
1
7
$/.-,
3
()*+
()*+
/.-,
/.-,d
()*+
/
00 −2 g
00
4
00
1
3
00 −2 −1
&/ ?89:;
()*+
/.-,
>=<
s
3
tt
/.-,t
()*+
O
()*+Ke
/.-,
()*+
/.-,
KK
t
t
KK
tt
2 K ytt −5
()*+
/.-,
−7
−1
/.-,
()*+
9 KK 5
1 ttt
KK
KK
t
t
t
%
1
()*+
/.-,
/.-,
()*+fMo
O X11 MMM
11 MMM1
MMM
−1
−1
11
MMM
11−2
M /.-,
()*+Ke
()*+
/.-,
1
KK 11
tt
KK 1
t
t
−1 K ytt −1
()*+
/.-,
/.-,
()*+
9 KK 3
KK
KK
%
()*+
/.-,
9 ttt
/.-,
()*+
O d
1
&
$/.-,
89:;
?>=<
()*+>o 1 /.-,
()*+?d
s
3
1
?
>
O

8
?
>

?
>

>
?

>
?
>>−3 1 ?? 8 3
1
5
>>
 ?
>  −2 ??$ −2
()*+o
/.-,
()*+d
/.-,
()*+
/.-,
()*+o
/.-,
0
3
()*+
()*+ −4 //.-,
/.-,aBo −4 /.-,
()*+
BB−3 ||D = aBBB
|
|
BB ||
BB ||
B|B| 3 5
B|B| −1
4
B
|
2
| BB
|| BBB
|
|
| −1 B }| | −2 B ()*+|
/.-,
()*+
()*+
//.-,
//.-,
−2
2
/.-,
()*+
9 KK 5
KK
KK
t
t
%
1
()*+
/.-,
()*+tfMo
/.-,
XO 11 MMM
11 MMM1
MMM
−1
−1
11
MMM
111
M ()*+Ke
/.-,
()*+
/.-,
1
KK 11
t
KK 1
tt
t
−1 K ytt −1
()*+
/.-,
1 ttt
1
4
4
8. El diámetro de un grafo es la máxima distancia, en número de arcos, que se puede
encontrar entre cualquier par de vértices. Por ejemplo, el grafo siguiente tiene un
diámetro igual a 2.
()*+?
/.-,
()*+
/.-,
??
??
??
()*+
/.-,
()*+
/.-,
Dado un grafo G con un diametro D < V , ¿que simple modificación se puede hacer al
algoritmo Bellman-Ford (versión no optimizada) para que ejecute más rapidamente?
¿Cuál es el tiempo de ejecución de la versión modificada?
9. En el algoritmo de Bellman-Ford optimizado, demuestren que si un vértice se
introduce más de |V | veces en la cola, el grafo contiene un ciclo de coste negativo.
10. Utilizando la propiedad del ejercicio precedente, modificar el algoritmo
Bellman-Ford optimizado de manera tal que detecte la presencia de ciclos de coste
negativo.
cahiers d’informatique
104
5.20
Árboles abarcadores mı́nimos
En muchos problemas prácticos se necesita conectar una serie de elementos con el coste de
conexión más bajo posible. Consideremos por ejemplo un circuito eléctrico con n puntos
que deben ser conectados usando la menor cantidad posible de alambre. El problema se
puede modelar como un grafo no dirigido ponderado, con pesos positivos G = (V, E), donde
V es el conjunto de puntos que se deben conectar, E es el conjunto de conexiones
posibles, y el coste de un arco w(u, v) ≥ 0 representa la cantidad de alambre necesaria
para realizar una conexión directa entre u y v. El problema que nos ponemos es conectar
todos los puntos con el menor coste posible, o sea, determinar un sub-grafo conexo
G0 = (V, T ), con T ⊆ E que contenga todos los vértices de V y tal que el coste total de
los arcos de T sea mı́nimo. Es bastante fácil darse cuenta que en ningún caso el grafo G0
contendrá ciclos. Si el grafo contuviera un ciclo, serı́a posible eliminar un arco del
ciclo, consiguiendo un grafo G00 que seguirı́a conteniendo todos los vértices de V y que,
conteniendo un arco menos y siendo los pesos positivos, tendrı́a un coste menor. Por
ejemplo, en el grafo a la izquierda se puede eliminar un arco, consiguiendo otro grafo
abarcador de coste menor:
1
()*+
/.-,
/.-,?
()*+
??


??1
1 
??

? 
()*+
/.-,
coste = 3
1
/.-,?
()*+
()*+
/.-,
??


??1
1 
??

? 
()*+
/.-,
coste = 2
Ya que G es no dirigido, G0 tampoco será dirigido, por tanto G0 es un árbol, llamado el
árbol abarcador mı́nimo (o de coste mı́nimo) de G. Observamos que aquı́ "mı́nimo" se
refiere al coste, y no al número de arcos, ya que cada árbol abarcador contiene |V | − 1
arcos (si contuviera menos, el sub-grafo G0 no podrı́a ser conexo).
Formalmente, el problema se pone de la forma siguiente:
Definition 5.20.1. Dado un grafo G = (V, E), ponderado, conexo, y no dirigido, con función
peso w : E → R no negativa, un árbol abarcador mı́nimo de G es un sub-grafo acı́clico
conexo G0 = (V, T ), T ⊆ E que minimiza
X
w(T ) =
w(u, v)
(5.140)
(u,v)∈T
Como ya hemos visto, el sub-grafo abarcador de coste mı́nimo es siempre acı́clico, o sea,
es un árbol. La figura 5.20 es un ejemplo de grafo donde se ha marcado un árbol
abarcador mı́nimo. Nótese que el árbol abarcador mı́nimo no es único: en el árbol de
figura 5.20 se puede eliminar el arco (c, g) y a~
nadir el arco (f, g) consiguiendo un árbol
abarcador con el mismo coste. El algoritmo que veremos en esta sección para la búsqueda
del árbol abarcador mı́nimo pertenece a una clase de algoritmos conocidos como algoritmos
avaros (greedy algorithms), de los cuales ya hemos visto un ejemplo. Estos algoritmos
eligen, en cualquier momento de su ejecución, el elemento (el arco, en este caso) que da
la mejor ventaja inmediada, sin tener en cuenta una visión global del algoritmo. Una tal
estrategia, en general, no encuentra la solución óptima de un problema: a veces una
solución que no parece tan buena en un momento resulta mejor a largo plazo. Pero, en el
caso del árbol abarcador mı́nimo demostraremos que la estrategia avara consigue la
solución óptima.
grafos
105
a
c
b
9
1
()*+F
/.-,
()*+
/.-,
()*+
/.-,
66
FFFFFFF 4
66
FFFFF d
6 FFFF
8 6 F
66 2
8 4
3
()*+
/.-,
r
666
r
7 rr
6
2
r
6 rr
()*+
/.-,
()*+
/.-,
()*+r
/.-,
4
e
4
f
g
Figure 5.23: Un ejemplo de árbol abarcador mı́nimo para un grafo conexo no dirigido.
El árbol no es único: se puede quitar el arco (c, g) y remplazarlo con el arco (f, g) sin
cambiar el coste.
El algoritmo de Dijkstra para caminos mı́nimos, que hemos analizado en la sección 5.17, se
basa en una estrategia similar: en cualquier momento elige el vértice de camino estimado
mı́nimo entre los que todavı́a no se han elegido. El algoritmo de Dijkstra presenta muchas
similitudes con el algoritmo de Primm, que será presentado en esta sección y, de hecho,
determina un tipo de árbol abarcador mı́nimo: si el grafo G es dirigido, y se busca un
árbol abarcador mı́nimo con raı́z en un vértice dado (el vértice de salida), el algoritmo
de Dijkstra encuentra el árbol abarcador mı́nimo.
5.20.1
Acrecimiento de un árbol
El algoritmo de esta sección funciona por acrecimiento: el árbol abarcador se construye
un arco a la vez, insertando cada arco en un conjunto A que el algoritmo mantiene y
seleccionando cada vez el arco más conveniente entre los que cumplen un determinado
invariante, (una propiedad que se cumple a lo largo de la ejecución del algoritmo). En
el caso presente, el invariante es:
Dado el conjunto de arcos A (el conjunto de arcos seleccionados en un momento
dado de la ejecución), existe un árbol abarcador mı́nimo T tal que A ⊆ T .
En cada paso, el arco (u, v) que se a~
nade al conjunto A se elige entre los que mantienen
el invariante, o sea, entre los arcos tales que A ∪ {(u, v)} es también un sub-conjunto de
un árbol abarcador mı́nimo. Un arco que mantiene el invariante se llama seguro.
Claramente, si seguimos a~
nadiendo arcos seguros hasta llegar a un conjunto de |V | − 1
arcos, obtendremos un árbol abarcador mı́nimo. El algoritmo general para árboles
abarcadores mı́nimos es entonces el siguiente:
AAM generico(G)
1.
A ← ∅;
2.
while |A| < |V| − 1 do
3.
busca un arco (u, v) seguro para A;
4.
A ← A ∪ {(u, v)};
5.
od
6.
return (V, A);
El problema es encontrar, en cada iteración del bucle de las lı́neas 2--5, un arco seguro
para A. Un arco seguro existe siempre: antes de la ejecución de la lı́nea 3, el
cahiers d’informatique
106
C
C
C1
2
a
b
c
C
C
@
6
C 2 3
4
1
4 5 @
C@
? C @
?
R
C
d
e
f
3 C 2 CC
Figure 5.24:
Ejemplo de cruce
invariante se cumple, es decir, existe un árbol abarcador T tal que A ⊆ T ; si |T | = |A|,
entonces A es un árbol abarcador mı́nimo, si no existe por lo menos un arco en T − A, y
este arco es seguro para A.
El problema entonces es: ¿cómo encontrar arcos seguros?
necesitamos empezar con unas cuantas definiciones.
Antes de resolver este problema
Dado un grafo G = (V, E), un corte de G es una partición de V en dos conjuntos (S, V − S).
Un arco (u, v) cruza el corte si uno de sus vértices se encuentra en S y el otro en V − S.
Dado un conjunto de arcos A, un corte (S, V − S) respeta A si ningún arco de A cruza el
corte. Un arco ligero de un corte es un arco de coste mı́nimo entre los que cruzan el
corte. En general, decimos que (u, v) es un arco ligero para una dada propiedad si es un
arco de peso mı́nimo entre los que cumplen la propiedad. Un arco ligero, generalmente, no
es único, ya que pueden existir varios arcos con el mismo peso.
Por ejemplo, la lı́nea espesa de figura 5.20.1 representa un cruce con S = {a, d, f }
V − S = {b, c, e} (y también representa el cruce complementar con S = {b, c, e} y
V − S = {a, d, f }: el dibujo es ambuguo). El cruce respeta conjuntos de arcos como
A = {(b, e), (e, c)}
A = {(a, d)}
A = {}
También respeta el conjunto
A = {(a, d), (b, e), (b, c), (e, c)}
que es el conjunto máximo respetado por el corte:
si el corte respeta B, entonces B ⊆ A.
(5.141)
para cada otro conjunto de arcos B,
La clave para encontrar arcos seguros la proporciona el teorema siguiente.
Theorem 5.20.1. Sea G = (V, E) un grafo dirigido conexo con función peso w : E → R y A un
sub-conjunto de E, tal que existe un árbol abarcador mı́nimo (V, T ) tal que A ⊂ T . Sea
(S, V − S) un corte que respeta A, y (u, v) un arco ligero que cruza (S, V − S). Entonces el
arco (u, v) es seguro para A.
Demostración: Si (u, v) ∈ T , entonces claramente (u, v) es seguro para A. Supongamos que
(u, v) 6∈ T y vamos a construir un árbol abarcador mı́nimo T 0 tal que A ∪ {(u, v)} ⊆ T 0 . Si
dicho árbol existe entonces (u, v) es claramente seguro para A. Siendo T un árbol
abarcador, y (u, v) 6∈ T , hay en T un camino de u a v que no incluye (u, v). Entonces, como
se ve en figura 5.20.1, el arco (u, v) forma con el camino p un ciclo. Los vértices u y v
están en lados opuestos del corte, por tanto hay en p por lo menos un arco (x, y) que cruza
grafos
107
S
V-S
m
xm
ym
m
m
m
p
m
?
um
vm
Figure 5.25: Construcción del árbol T 0 . Los arcos que se muestran son los del árbol
T , y los arcos marcados doble pertenecen a A (claramente, aquı́ no se muestran todos los
arcos del grafo). El arco (u, v) es un arco ligero que cruza el corte, y el arco (x, y) es
un arco en el camino (único) entre u y v en T . Un árbol abarcador mı́nimo que contenga
(u, v) se construye sacando (x, y) de T y a~
nadiendo (u, v).
el corte, y (x, y) 6∈ A, ya que, por hipótesis, el corte respeta A. El arco (x, y) se
encuentra en el único camino en T entre u y v, entonces, sacando (x, y) divide T en dos
partes, y a~
nadiendo el arco (u, v) lo recompone en un nuevo árbol abarcador
T 0 = T − {(x, y)} ∪ {(u, v)}.
T 0 es entonces un árbol abarcador, y A ∪ {(u, v)} ⊆ T 0 , ya que A ⊆ T y (x, y) 6∈ A. Hay que
demostrar que T 0 es mı́nimo. Como (u, v) es un arco ligero que cruza el corte, y (x, y)
también cruza el corte, se cumple w(u, v) ≤ w(x, y), entonces
w(T 0 )
= w(T ) − w(x, y) + w(u, v)
≤ w(T ).
(5.142)
Pero T era un árbol abarcador mı́nimo, ası́ que w(T ) ≤ w(T 0 ); por tanto w(T ) = w(T 0 ), y T 0
es también un árbol abarcador mı́nimo.
5.21
El algoritmo de Primm
El algoritmo de Primm se basa en el algoritmo genérico de acrecimiento del árbol
abarcador de un grafo G y construye un árbol abarcador mı́nimo dada una raı́z arbitraria
r ∈ V [G] un arco a la vez. En cada paso el algoritmo mantiene un conjunto S de vértices
ya a~
nadidos y encuentra un arco ligero que cruza el corte (S, V − S). Por construcción el
corte respeta el conjunto de vértices ya encontrados y, por el teorema precedente, si
(u, v) es el arco escogido y v 6∈ S, entonces S ∪ {v} es un sub-árbol de un árbol abarcador
mı́nimo. Si seguimos a~
nadiendo arcos hasta que sea S = V [G] conseguimos el árbol que
buscamos. La figura 5.21 ilustra el funcionamiento del algoritmo.
El algoritmo de Primm sigue una estrategia avara; en cada iteración el árbol se aumenta
usando el arco de coste mı́nimo disponible en momento, sin ocuparnos del coste de los
demás arcos. La clave para la implementación eficaz del algoritmo es una búsqueda rápida
del arco de coste mı́nimo entre los que cruzan el corte. El algoritmo guarda los vértices
que todavı́a no se han a~
nadido al árbol en una cola de prioridad Q ordenada por un valor
key[v]. Para cada v ∈ V [G], key[v] es el peso mı́nimo de cualquier arco que une v al árbol
cahiers d’informatique
108
1
()*+J
/.-,
/.-,; 9 ?>=<
()*+
89:;
()*+
/.-,
r
;;
JJJ4
JJ
;; J
;;
4
()*+
/.-,
3
2
r
8
2 ;;8
r
r
;; r
rr
()*+
/.-,
()*+r 7
/.-,
()*+
/.-,
1
/.-,; 9 89:;
()*+
()*+
/.-,
J
?>=<
()*+
/.-,
r
;;
JJJ4
JJ
;; J
;;
4
()*+
/.-,
3
2
r
8
2 ;;8
r
r
;; r
rr
()*+
/.-,
()*+r 7
/.-,
()*+
/.-,
1
/.-,; 9 89:;
()*+
()*+
/.-,
J
?>=<
()*+
/.-,
r
;;
JJJ4
JJ
;; J
;;
4
()*+
/.-,
3
2
r
8
2 ;;8
r
r
;; r
rr
()*+
/.-,
()*+r 7
/.-,
()*+
/.-,
1
()*+
/.-,
J
/.-,; 9 ?>=<
()*+
89:;
()*+
/.-,
r
JJ 4
;;
JJ
;; JJ
;
()*+
/.-,
;
4
3
2 8
2 ;;8
rr
r
;; r
rr
()*+
/.-,
()*+
/.-,
()*+
/.-,
r 7
1
()*+
/.-,
J
/.-,; 9 89:;
()*+
?>=<
()*+
/.-,
r
JJ 4
;;
JJ
;; JJ
;
()*+
/.-,
;
4
3
2 8
2 ;;8
rr
r
;; r
rr
()*+
/.-,
()*+
/.-,
()*+
/.-,
r 7
1
()*+
/.-,
J
/.-,
()*+
; 9 89:;
?>=<
()*+
/.-,
r
JJ 4
;;
JJ
;; JJ
;
()*+
/.-,
;
4
3
2 8
2 ;;8
rr
r
;; r
rr
()*+
/.-,
()*+
/.-,
()*+
/.-,
r 7
4
4
(1)
4
4
(4)
4
4
(2)
4
4
1
/.-,
()*+
; 9 ?>=<
89:;
()*+
/.-,
()*+
/.-,
JJ
r
JJJJJ4
;;
JJJJ
;; JJJ
;;
()*+
/.-,
3
4
2
8
2 ;;8
rr
r
;; r
rr
()*+
/.-,
()*+
/.-,
()*+
/.-,
r 7
(5)
4
4
4
(3)
4
4
(6)
4
(7)
Figure 5.26: Ejemplo de aplicación del algoritmo de Primm. Notese que el arco (c, d) era
una opción posible en el paso 4 (una opción que, en este ejemplo, no se elige), pero no
en el paso 5, ya que a este punto el arco mı́nimo que cruza el corte es el arco (e, a), the
coste 3.
grafos
109
que se está construyendo, o key[v] = ∞ si no hay arco.
de v en el árbol.
El campo π[v] contiene el "padre"
El conjunto A que aparece en el algoritmo genérico---el conjunto de arcos que ya se han
a~
nadido al árbol---no se almacena de forma explı́cita en el algoritmo. El conjunto es
A = {(v, π[v])|v ∈ V − {r} − Q}
(5.143)
y, por una ejecución dada del algoritmo con una raı́z dada, depende, en cualquier momento
de la ejecución, sólo del contenido de la cola Q y de los punteros π[v] que ya se han
asignado ası́ que almacenando estos elementos también se almacena (implı́citamente) A. Al
final del algoritmo la cola Q se encuentra vacı́a, ası́ que el árbol abarcador mı́nimo
queda definido por los punteros π[v] como
T = {(v, π[v])|v ∈ V − {r}}
(5.144)
El algoritmo es el siguiente:
Primm(G, w, r)
1.
Q ← V[G];
2.
foreach u in Q do
3.
key[u] ← ∞;
4.
od;
5.
key[r] ← 0;
6.
π[r] ← Nil;
7.
while Q 6= ∅ do
8.
u ← extract min(Q);
9.
foreach v in u.ady do
10.
if v in Q and w(u, v) < key[v] then
11.
π[v] ← u;
12.
key[v] ← w(u, v);
13.
fi
14.
od
15. od
Para determinar el tiempo de ejecución del algoritmo, condideremos que cola Q se
implemente con un Heap. En este caso, la inicialización de la cola supone un build heap,
con un tiempo O(V ). La cola se inicializa con todos los vértices, y en cada iteración
del bucle 7--15 se saca un vértice, ası́ que el bucle se ejecutará O(V ) veces. La
función extract heap tiene un tiempo de ejecución O(log V ), por tanto el tiempo de
ejecución del bucle 7--15, excepto la ejecución del bucle 9--14, es O(V log V ). Como ya
hemos visto en otras ocasiones, el bucle 9--14 se ejecuta un total de O(E) veces (la
suma de los tama~
nos de las listas de adyacencias de todos los vértices). Averiguar si
v ∈ Q se puede hacer en un tiempo O(1), guardando un flag asociado a v que se inicializa
a true y se pone false en el momento en que v se saca de la cola. La operación al paso
12 supone la ejecución de un decrease key en el heap, ya que ahora la posición de v puede
haber cambiado. Esta operación se puede copletar en un tiempo O(log V ) y por tanto el
tiempo total de ejecución del bucle 9--14 es O(E log V ).
Entonces, el tiempo total de ejecución del algoritmo es O((V + E) log V ).
El tiempo se puede mejorar implementando la cola con una estructura de heap de Fibonacci.
En este caso el tiempo de ejecución medio de decrease key es O(1), y el tiempo de
ejecución del algoritmo es O(E + V log V ).
cahiers d’informatique
110
5.22
Ejercicios
1. Ejecuten el algoritmo de Prim en los grafos siguientes
()*+
()*+? 5 /.-,
/.-,

 ??? 8 

??
3 
4
4
 4
?



 ??7?


()*+ 1 /.-,
/.-,
()*+ 2 /.-,
()*+
()*+
/.-,
1
()*+
/.-,
()*+
/.-,
3
1
3
()*+
/.-,
()*+
/.-,
3
5
()*+
/.-,
()*+?
/.-,
 ??3? 

()*+ 1 1
/.-,
()*+? 4
/.-,
9
?


?? 

()*+ 5 13 /.-,
/.-,
()*+ 1
()*+
/.-,
()*+
/.-,
7
()*+
/.-,
()*+OO
/.-,
()*+
/.-,
OOO 


OOO 5


 O 3 
2 
3
7 OOOOO





 OO4OO
()*+
/.-,
()*+
/.-,
()*+
/.-,
()*+
/.-,
()*+
/.-,
1
3
4
()*+
/.-,
()*+
/.-,
/.-,?
()*+
??  ???4 
?
?? 1
1 4 ??2
 ?  ??

()*+
/.-,
()*+ 2 /.-,
/.-,
()*+
3
3
/.-,?
()*+
()*+?
/.-,
()*+
/.-,
??2
??1
?
?
()*+?
()*+ 3 /.-,
/.-,
1
2
??
 ???1

?
?

2
()*+ 3
/.-,
()*+
/.-,
()*+
/.-,
1
4
()*+
/.-,
2
()*+
/.-,
()*+
/.-,
()*+
/.-,
yy
y
y
()*+yE 4
/.-,
9
EE13
y
y
EE
yy5
y
()*+
/.-,
()*+
/.-,
()*+
/.-,
7
()*+
/.-,
/.-,O 3
()*+
()*+
/.-,
OOO 5  ??5 o3oo
OOO 6 ?o?oo
()*+
()*+? OoOooOoOoO /.-,
/.-,
2
OOO???4 2
4  oo?o?

 oo ?  OOO?
()*+oo
/.-,
()*+ 7
()*+
/.-,
7 /.-,
1
1
2. Escriban una versión del algoritmo de Prim que funciona con grafo representados por
matriz de adyacencia.
3.
grafos
111
VI. APENDIX. EL PRINCIPIO DE INDUCCIÓN.
El principio de inducción matemática es una propiedad del campo de los números naturales
que nos proporciona una técnica muy útil para demostrar teoremas. El principio fue
enunciado por primera vez en su totalidad por el matemático Giuseppe Peano como parte de
su sistema de axiomatización de las matemáticas. La importancia del principio reside en
ser a la base del procedimiento de demostración por inducción, que se usa mucho en varios
campos de las matemáticas y de la informática cada vez que es posible expresar una
familia de problemas de una manera que dependa de un parámetro entero.
6.23
Propiedades hereditarias
El principio establece las condiciones bajo las cuales una cierta propiedad es válida en
el campo de los números naturales. Una propiedad, en general, se expresa como un
predicado lógico P (n). Por ejemplo:
i) P1 (n) = n es par;
ii) P2 (n) = n es mayor que 5;
iii) P3 (n) = n es un número primo;
iv) P4 (n) = n es un múltiplo de dos o el sucesor de un múltiplo de dos; y
v) P5 (n) = n es un múltiplo de tres, el sucesor de un múltiplo de tres, o el sucesor del
sucesor de un múltiplo de tres.
Estas propiedades son proposiciones lógicas y, como tales, se pueden expresar usando el
lenguaje de la lógica simbólica:
i) P1 (n) = ∃m ∈ N : n = 2m;
ii) P2 (n) = n > 5;
cahiers d’informatique
112
iii) P3 (n) = ∀m, p ∈ N : n = mp ⇒ (m = 1 ∨ m = n);
iv) P4 (n) = ∃m ∈ N : n = 2m ∨ n = 2m + 1;
v) P5 (n) = ∃m ∈ N : n = 3m ∨ n = 3m + 1 ∨ n = 3m + 2;
Algunas de estas proposiciones gozan de la propiedad siguiente:
Definition 6.23.1. Sea P (n) una proposición que expresa una propiedad de los números
naturales. La proposición P es hereditaria si, cada vez que un numero n goza de P , el
numero n + 1 también goza de P . En fomulas:
∀n
P (n) ⇒ P (n + 1)
(6.145)
De las propiedades P1 , . . . , P5 es fácil de mostrar que las P2 , P4 , P5 son
hereditarias, mientras que las P1 y la P3 no lo son:
i) Supongamos P1 (n), o sea que hay un m de tal modo que n = 2m. Tomamos ahora el número
n + 1. Para este número vale n + 1 = 2m + 1 y, claramente, no existe ningún m0 para
el que 2m + 1 = 2m0 . (Esto corresponde a la propiedad en la que si n es par n + 1 es
impar.)
ii) Supongamos n > 5, entonces n + 1 > n > 5 ası́ que la propiedad se cumple para n + 1 y por
lo tanto es hereditaria.
iii) En este caso utilizaremos un contra-ejemplo. Supongamos n = 7; sabemos que 7 es un
número primo, entonces P3 (7) es cierto. Por supuesto, n + 1 = 7 + 1 = 8 y 8 es un
múltiplo de 2 y como tal no es primo. Existe entonces un caso en que P3 (n) es
cierto, pero P (n + 1) no lo es. Ya que la definción de propiedad hereditaria
requiere que (6.145) se cumpla para cada n, se concluye que P3 no es hereditaria.
iv) Supongamos P4 (n), o sea, que existe un m tal que n = 2m o n = 2m + 1. Aquı́ hay que
analizar dos casos. Si n = 2m, entonces n + 1 = 2m + 1, y P4 (n + 1) es cierto. Si
n = 2m + 1, entonces n + 1 = 2m + 2 = 2(m + 1) = 2m0 , donde m0 = 2m + 1. La propiedad
P4 (n + 1) se cumple en este caso también, por lo cual se concluye que P4 es
hereditaria.
v) P5 también es hereditaria, como se puede mostrar de manera análoga al caso de P4 .
Una vez definida esta propiedad, la formulación del principio de inducción matemática
(también llamado principio de inducción completa ) es muy simple:
Principio de inducción matemática
matemática:
Supongamos P una propiedad de los números naturales.
hereditaria, entonces cada entero goza de P .
Si 0 goza de P y P es
En fórmulas:
(P (0) ∧ (∀m ∈ N : P (m) ⇒ P (n + 1)))
⇒
∀n ∈ N : P (n)
(6.146)
grafos
6.24
113
Aplicación a teoremas matemáticos
El principio de inducción matemática es un instrumento matemático muy útil porque permite
evitar una demostración completa de que una propiedad es cierta para cada entero: será
suficiente demostrar que la propiedad vale para 0 y que es hereditaria. Para demostrar
que la propiedad es hereditaria se asume que n la cumple y, basándose en esto, se deriva
que n + 1 también la cumple. Pongamos, por ejemplo, que se quiera demostrar el teorema
siguiente:
Theorem 6.24.1. Cada entero es par o impar; o sea, para cada entero n existe un entero m
tal que n se puede expresar como n = 2m (en tal caso n es par) o como n = 2m + 1 (en tal
caso n es impar).
Demostrar este teorema con el principio de inducción matemática es muy fácil:
que demostrar los dos hechos siguientes:
sólo habrá
i) 0 cumple con la propiedad. Ésto es muy fácil de demostrar, en cuanto es posible
escribir 0 como 0 = 2 · 0 (lo cual nos dice que 0 es par).
ii) La propiedad es hereditaria.
Ésto ya se ha demostrado en el punto iv) arriba.
¿Qué pasa si P (0) no se cumple? Hay propiedades, como la P2 de arriba, que son
hereditarias, pero que empiezan a ser cumplidas a partir de un número que no es 0: en el
caso de la P2 , por ejemplo, la propiedad se cumple a partir de n = 6. En esto caso
también se puede aplicar el principio de inducción de la siguiente forma modificada:
Principio de inducción matemática (modificado)
(modificado):9
Sea P una propiedad de los números naturales. Si un número k ∈ N goza de P y
P es hereditaria, entonces cada entero mayor o igual a k goza de P .
En formulas:
(P (k) ∧ (∀m ∈ N : P (m) ⇒ P (n + 1)))
⇒
∀n ≥ k : P (n)
(6.147)
Gracias a este principio, es muy fácil demostrar que P2 se cumple para cada número n ≥ 6.
Es importante notar que el principio de inducción matemática nos permite
afirmar que la propiedad P es válida para cada número finito. Esta observación es
particularmente importante en el caso de las aplicaciones informáticas, en las cuales el
principio no se aplica directamente a los números, sino a alguna estructura parametrizada
por números. Consideremos, por ejemplo, el teorema siguiente:
Theorem 6.24.2. Cada conjunto finito de números posee un máximo.
Demostración. Este teorema se puede demostrar de varias maneras pero, en este caso, se
usará el principio de inducción. La inducción se hará sobre el número de elementos del
conjunto. Es decir, la propiedad que se considererá es la siguiente:
P (n) = cada conjunto de n números posee un máximo.
En el caso n = 1, la propiedad es inmediata:
es el máximo.
el único número que aparece en el conjunto
9 En realidad esta formulación del principio es un teorema, en cuanto se puede derivar del principio
(6.146). Aquı́, sin embargo, se seguirá el uso común, y se llamará principio.
cahiers d’informatique
114
Sea ahora P (n) válida; queremos demostrar P (n + 1). Un conjunto de n + 1
números se puede escribir como {u1 , . . . , un+1 }. Quitamos el número un+1 del conjunto. Nos
queda el conjunto {u1 , . . . , un } que, estando compuesto de n elementos, cumple la propiedad y
posee un máximo, siendo µ(n) este máximo. Ahora es posible definir
un+1 si un+1 > µ(n)
µ(n + 1) =
(6.148)
µ(n) si no
Claramente µ(n + 1) es el máximo del conjunto {u1 , . . . , un+1 }, y el teorema queda
demostrado.
Como ya se ha mencionado, el teorema permite demostrar, en este caso,
que cada conjunto finito posee un máximo. En el caso de conjuntos infinitos la propiedad
no vale: por ejemplo, el conjunto N de todos los enteros no posee máximo.
En informática, el teorema se aplica mucho para demostrar la correción
de algoritmos. En muchos casos el número n sobre el que se hace inducción es el tama~
no
de la estructura a la cual se aplica el algoritmo: se demuestra que el algoritmo es
correcto si se aplica a una estructura de tama~
no 1 (lo que, en muchos casos, es muy fácil
de demostrar) y luego se demuestra que, si el algoritmo funciona correctamente con todas
las estructura de tama~
no n, entonces funciona también con las de tama~
no n + 1.
6.25
Otras variantes
A veces la aplicación del principio de inducción, ası́ como se ha enunciado en la sección
precedente es imposible porque no se puede pasar del caso n al caso n + 1, aún si se ha
demostrado para n. Consideremos, por ejemplo, el algoritmo merge sort :
merge-sort(A, p, r)
if p < r then
q ← b(p + r)/2c;
merge-sort(A, p, q);
merge-sort(A, q + 1, r);
merge(A, p, q, r);
fi
Se desea determinar el tiempo asintótico de ejecución del algoritmo. Sea T (n) el tiempo
de ejecución de merge sort con un array de tama~
no n. Se sabe que el tiempo de ejecución
de la función merge es n, pero el tiempo de ejecución total depende también del tiempo de
ejecución de merge sort mismo, que se llama recursivamente dos veces con array de tama~
no
n/2. Se puede entonces escribir:
T (n) = 2T (n/2) + n
(6.149)
Se quiere demonstrar que, dada esta ecuación, el tiempo de ejecución es T (n) = O(n log n) o
sea, que existe un valor c tal que, para cada n, T (n) ≤ cn log n, con una demostración que
utilice el método de inducción. Para n = 1 la propiedad no vale (c · 1 · log 1 = 0 para cada
c), pero esto no es tan importante: siendo una propiedad asintótica, nos interesan
valores grandes de n. Empezamos con n = 2, para el cual se necesita T (2) < c · 2 · log 2. Es
posible escoger c de tamańo tal para que la propiedad se cumpla. Supongamos entonces que
la propiedad se cumple para n − 1, y demostramos que
T (n) = 2T (n/2) + n ≤ cn log n
(6.150)
grafos
115
Aqui hay un problema:
aparece n/2
hemos supuesto que la propiedad vale para n − 1 pero en la ecuación
La intuición nos dice que la cosa deberı́a funcionar también: si se
asume que la propiedad se cumple para n − 1, quiere decir que, de alguna manera, se asume
demonstrada para todos los n más peque~
nos. De hecho, esta constatación se encuentra en
una nueva formulación del principio de inducción10 :
Definition 6.25.1. Una propiedad P es hereditaria en sentido general si el hecho de que
P valga para todos los números 1, . . . , n − 1 implica que P vale para n. Es decir:
∀n : P (0) ∧ P (1) ∧ · · · ∧ P (n − 1)
⇒
P (n)
(6.151)
Se pueden hacer aquı́ las mismas constataciones de antes si la propiedad
empieza a verificarse para un número que no es cero. Esta formulación del principio
resuelve el problema del tiempo de ejecución de merge-sort. Supongamos que T (k) ≤ ck log k
se cumple para k = 2, . . . , n − 1. Ya que n/2 ≤ n − 1, la propiedad se cumple para n/2, ası́
que (6.150) se puede escribir como
T (n)
6.26
=
≤
=
≤
≤
2T (n/2) + n ≤ cn log n
2c n2 log n2 + n
cn log n − cn log 2 + n
cn log n − cn + n
cn log n
(definición de T (n))
(hipótesis inductiva)
(propiedad del logaritmo)
(porque log 2 ≤ 1)
(se asume c > 1)
(6.152)
Peligros
Algo que siempre es necesario recordar cuando se haga la demostración de que una
propiedad es hereditaria: en la demostracción que, si P (n) se cumple, entonces P (n + 1)
también se cumple, el número n tiene que ser absolutamente cualquiera. Es decir, no se
puede usar ninguna propiedad que algunos n pueden cumplir y otros no.
Un ejemplo de los riesgos a que uno se expone sin la debida atención se
da en el teorema de los espa~
noles (Santini, 2006):
Theorem 6.26.1. En un conjunto de personas, si una es espa~
nola, entonces todas son
espa~
nolas.
Demostración. El teorema se demuestra con el método de inducción. Para n = 1 el teorema
es cierto: en un grupo de una persona, si esta persona es espa~
nola, todas lo son.
Se suponga, entonces, que el teorema sea verificado para conjuntos de n
personas, y vamos a demostrarlo para conjuntos de n + 1 personas. Consideramos el
conjunto {p1 , p2 , . . . , pn+1 }. Para demostrar el teorema, es suficiente suponer que una
persona es espa~
nola y derivar, de este hecho, que todas lo son. Supongamos, entonces,
que en este conjunto haya un espa~
nol y, sin pérdida de generalidad, tomamos un espa~
nol
que no sea p1 .
Ahora quitamos a p1 del grupo, obteniendo el conjunto {p2 , . . . , pn+1 }.
Este conjunto contiene n personas, entonces, para este conjunto, el teorema es cierto:
como una persona es espa~
nola (y se hizo la hipótesis de que uno de los espa~
noles no era
p1 ), todas lo son. Ahora, quitamos a pn+1 del grupo y ponemos otra vez a p1 , obteniendo
10 Esta
forma del principio también es en realidad un teorema que se puede derivar de la forma original
116
cahiers d’informatique
{p1 , p2 , . . . , pn }. Este grupo también contiene n personas y ya hemos demostrado que
{p2 , . . . , pn } son espa~
nolas, entonces hay en el grupo {p1 , p2 , . . . , pn } por lo menos un espa~
ol
y, siendo el grupo de n personas, se aplica la hipótesis de inducción, y se concluye que
todos son espa~
noles. Se ha acabado de demostrar que, asumiendo que una persona del grupo
es espa~
nola, todas las {p1 , p2 , . . . , pn+1 ) lo son. El teorema es válido.
¿Dónde está el problema? Supongamos que n + 1 = 2. Se asume que una
persona en el grupo {p1 , p2 } es espa~
nola. Quitamos p1 del grupo, y mos quedamos con {p2 }.
Si p2 es nuestro espa~
nol, entonces todos en el grupo lo son, porque el teorema es válido
para n = 1. Ahora ponemos p1 en el grupo, y sacamos p2 . El grupo ahora es {p1 }, y...
¡la persona que se habı́a supuesto espa~
ola ha desaparecido del grupo ! Es decir, cuando se
supone que haya un espa~
nol en el grupo, es necesario que esta persona se quede en el
grupo ya sea cuando se quita a p1 o cuando se quita a pn+1 . Es decir, es necesario que
el grupo {p2 , . . . , pn } que aparece en el teorema no esté vacı́o. Pero esto solo se puede dar
si n + 1 > 2: sin pensarlo hemos hecho la hipótesis de que n > 1. Nuestra inducción no
puede demostrar que el teorema es valido para n = 2 y, no pudiendo pasar el "obstaculo
2", la inducción se rompe. Solo hemos demostrado el teorema para n = 1, o sea, hemos
demostrado que si en un grupo de una persona esta persona es espa~
nola, todos lo son; una
propiedad que, aún si se ha demostrado con la certitud absoluta de las matemáticas,
carece bastante de interés.
Descargar