0% found this document useful (0 votes)
159 views501 pages

Algorithms in C Part 5 3rd Edition 2001 8

The document is the preface of 'Algorithms in C, Third Edition, Part 5: Graph Algorithms' by Robert Sedgewick, which focuses on graph algorithms and their applications in computing. It aims to make these methods accessible to a wide audience, including students and professionals, and covers fundamental graph properties, search methods, and algorithm implementations. The book is designed for use in computer science curricula and for self-study, providing practical algorithms and performance characteristics relevant to modern programming environments.

Uploaded by

Dev Chan
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
159 views501 pages

Algorithms in C Part 5 3rd Edition 2001 8

The document is the preface of 'Algorithms in C, Third Edition, Part 5: Graph Algorithms' by Robert Sedgewick, which focuses on graph algorithms and their applications in computing. It aims to make these methods accessible to a wide audience, including students and professionals, and covers fundamental graph properties, search methods, and algorithm implementations. The book is designed for use in computer science curricula and for self-study, providing practical algorithms and performance characteristics relevant to modern programming environments.

Uploaded by

Dev Chan
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 501

Algorithms THIRD EDITION

in C
PART 5
GRAPH ALGORITHMS

Robert Sedgewick
Princeton University

Addison-Wesley
Boston • San Francisco • New York • Toronto • Montreal
London • Munich • Paris • Madrid
Capetown • Sydney • Tokyo • Singapore • Mexico City
Many of the designations used by manufacturers and sellers to distin-
guish their products are claimed as trademarks. Where those designa-
tions appear in this book and we were aware of a trademark claim, the
designations have been printed in initial capital letters or all capitals.
The author and publisher have taken care in the preparation of this
book, but make no expressed or implied warranty of any kind and as-
sume no responsibility for errors or omissions. No liability is assumed
for incidental or consequential damages in connection with or arising
out of the use of the information or programs contained herein.
Copyright 
c 2002 by Addison-Wesley
All rights reserved. No part of this publication may be reproduced,
stored in a retrieval system, or transmitted, in any form or by any
means, electronic, mechanical, photocopying, recording, or otherwise,
without the prior written permission of the publisher. Printed in the
United States of America. Published simultaneously in Canada.
The publisher offers discounts on this book when ordered in quantity
for special sales. For more information, please contact: Pearson Edu-
cation Corporate Sales Division, One Lake Street, Upper Saddle River,
NJ 07458, (800) 382-3419, [email protected].
Visit us on the Web at www.awl.com/cseng/ .
Library of Congress Cataloging-in-Publication Data
Sedgewick, Robert, 1946 –
Algorithms in C / Robert Sedgewick. — 3d ed.
500 p. 24 cm.
Includes bibliographical references and index.
Contents: v. 2, pt. 5. Graph algorithms
1. C (Computer program language) 2. Computer algorithms.
I. Title.
QA76.73.C15S43 2002
005.13’3—dc21 97-23418
CIP
ISBN 0201316633
Text printed on recycled and acid-free paper.
6 7 8 9 1011 DOC 09 08 07
6th Printing July 2007
Preface

G RAPHS AND GRAPH algorithms are pervasive in modern com-


puting applications. This book describes the most important
known methods for solving the graph-processing problems that arise
in practice. Its primary aim is to make these methods and the basic
principles behind them accessible to the growing number of people in
need of knowing them. The material is developed from first principles,
starting with basic information and working through classical methods
up through modern techniques that are still under development. Care-
fully chosen examples, detailed figures, and complete implementations
supplement thorough descriptions of algorithms and applications.

Algorithms
This book is the second of three volumes that are intended to survey
the most important computer algorithms in use today. The first volume
(Parts 1–4) covers fundamental concepts (Part 1), data structures (Part
2), sorting algorithms (Part 3), and searching algorithms (Part 4);
this volume (Part 5) covers graphs and graph algorithms; and the
(yet to be published) third volume (Parts 6–8) covers strings (Part
6), computational geometry (Part 7), and advanced algorithms and
applications (Part 8).
The books are useful as texts early in the computer science cur-
riculum, after students have acquired basic programming skills and
familiarity with computer systems, but before they have taken spe-
cialized courses in advanced areas of computer science or computer
applications. The books also are useful for self-study or as a refer-
ence for people engaged in the development of computer systems or
applications programs because they contain implementations of useful
algorithms and detailed information on these algorithms’ performance
characteristics. The broad perspective taken makes the series an ap-
propriate introduction to the field.
PREFACE

Together the three volumes comprise the Third Edition of a book


that has been widely used by students and programmers around the
world for many years. I have completely rewritten the text for this
edition, and I have added thousands of new exercises, hundreds of
new figures, dozens of new programs, and detailed commentary on all
the figures and programs. This new material provides both coverage of
new topics and fuller explanations of many of the classic algorithms. A
new emphasis on abstract data types throughout the books makes the
programs more broadly useful and relevant in modern object-oriented
programming environments. People who have read previous editions
will find a wealth of new information throughout; all readers will
find a wealth of pedagogical material that provides effective access to
essential concepts.
These books are not just for programmers and computer-science
students. Nearly everyone who uses a computer wants it to run faster
or to solve larger problems. The algorithms that we consider repre-
sent a body of knowledge developed during the last 50 years that has
become indispensable in the efficient use of the computer for a broad
variety of applications. From N -body simulation problems in physics
to genetic-sequencing problems in molecular biology, the basic meth-
ods described here have become essential in scientific research; and
from database systems to Internet search engines, they have become
essential parts of modern software systems. As the scope of computer
applications becomes more widespread, so grows the impact of basic
algorithms, particularly the fundamental graph algorithms covered in
this volume. The goal of this book is to serve as a resource so that
students and professionals can know and make intelligent use of graph
algorithms as the need arises in whatever computer application they
might undertake.

Scope
This book, Algorithms in C, Third Edition, Part 5: Graph Algorithms,
contains six chapters that cover graph properties and types, graph
search, directed graphs, minimal spanning trees, shortest paths, and
networks. The descriptions here are intended to give readers an un-
derstanding of the basic properties of as broad a range of fundamental
graph algorithms as possible.

iv
You will most appreciate the material here if you have had a
course covering basic principles of algorithm design and analysis and
programming experience in a high-level language such as C, Java, or
C++. Algorithms in C, Third Edition, Parts 1–4 is certainly ade-
quate preparation. This volume assumes basic knowledge about ar-
rays, linked lists, and ADT design, and makes uses of priority-queue,
symbol-table, and union-find ADTs—all of which are described in de-
tail in Parts 1–4 (and in many other introductory texts on algorithms
and data structures).
Basic properties of graphs and graph algorithms are developed
from first principles, but full understanding of the properties of the
algorithms can lead to deep and difficult mathematics. Although the
discussion of advanced mathematical concepts is brief, general, and
descriptive, you certainly need a higher level of mathematical maturity
to appreciate graph algorithms than you do for the topics in Parts 1–4.
Still, readers at various levels of mathematical maturity will be able to
profit from this book. The topic dictates this approach: some elemen-
tary graph algorithms that should be understood and used by everyone
differ only slightly from some advanced algorithms that are not un-
derstood by anyone. The primary intent here is to place important
algorithms in context with other methods throughout the book, not
to teach all of the mathematical material. But the rigorous treatment
demanded by good mathematics often leads us to good programs, so I
have tried to provide a balance between the formal treatment favored
by theoreticians and the coverage needed by practitioners, without
sacrificing rigor.

Use in the Curriculum


There is a great deal of flexibility in how the material here can be
taught, depending on the taste of the instructor and the preparation
of the students. The algorithms described have found widespread
use for years, and represent an essential body of knowledge for both
the practicing programmer and the computer science student. There
is sufficient coverage of basic material for the book to be used in a
course on data structures and algorithms, and there is sufficient detail
and coverage of advanced material for the book to be used for a
course on graph algorithms. Some instructors may wish to emphasize

v
PREFACE

implementations and practical concerns; others may wish to emphasize


analysis and theoretical concepts.
For a more comprehensive course, this book is also available in
a special bundle with Parts 1–4; thereby instructors can cover funda-
mentals, data structures, sorting, searching, and graph algorithms in
one consistent style. A complete set of slide masters for use in lectures,
sample programming assignments, interactive exercises for students,
and other course materials may be found by accessing the book’s home
page.
The exercises—nearly all of which are new to this edition—fall
into several types. Some are intended to test understanding of material
in the text, and simply ask readers to work through an example or
to apply concepts described in the text. Others involve implementing
and putting together the algorithms, or running empirical studies to
compare variants of the algorithms and to learn their properties. Still
other exercises are a repository for important information at a level of
detail that is not appropriate for the text. Reading and thinking about
the exercises will pay dividends for every reader.

Algorithms of Practical Use


Anyone wanting to use a computer more effectively can use this book
for reference or for self-study. People with programming experience
can find information on specific topics throughout the book. To a large
extent, you can read the individual chapters in the book independently
of the others, although, in some cases, algorithms in one chapter make
use of methods from a previous chapter.
The orientation of the book is to study algorithms likely to be of
practical use. The book provides information about the tools of the
trade to the point that readers can confidently implement, debug, and
put to work algorithms to solve a problem or to provide functionality
in an application. Full implementations of the methods discussed are
included, as are descriptions of the operations of these programs on
a consistent set of examples. Because we work with real code, rather
than write pseudo-code, the programs can be put to practical use
quickly. Program listings are available from the book’s home page.
Indeed, one practical application of the algorithms has been to
produce the hundreds of figures throughout the book. Many algo-

vi
rithms are brought to light on an intuitive level through the visual
dimension provided by these figures.
Characteristics of the algorithms and of the situations in which
they might be useful are discussed in detail. Although not emphasized,
connections to the analysis of algorithms and theoretical computer
science are developed in context. When appropriate, empirical and
analytic results are presented to illustrate why certain algorithms are
preferred. When interesting, the relationship of the practical algo-
rithms being discussed to purely theoretical results is described. Spe-
cific information on performance characteristics of algorithms and im-
plementations is synthesized, encapsulated, and discussed throughout
the book.

Programming Language
The programming language used for all of the implementations is C
(versions of the book in C++ and Java are under development). Any
particular language has advantages and disadvantages; we use C in this
book because it is widely available and provides the features needed
for the implementations here. The programs can be translated easily
to other modern programming languages because relatively few con-
structs are unique to C. We use standard C idioms when appropriate,
but this book is not intended to be a reference work on C program-
ming.
We strive for elegant, compact, and portable implementations,
but we take the point of view that efficiency matters, so we try to
be aware of the code’s performance characteristics at all stages of
development. There are many new programs in this edition, and
many of the old ones have been reworked, primarily to make them
more readily useful as abstract-data-type implementations. Extensive
comparative empirical tests on the programs are discussed throughout
the book.
A goal of this book is to present the algorithms in as simple and
direct a form as possible. The style is consistent whenever possible
so that similar programs look similar. For many of the algorithms,
the similarities remain regardless of which language is used: Dijkstra’s
algorithm (to pick one prominent example) is Dijkstra’s algorithm,
whether expressed in Algol-60, Basic, Fortran, Smalltalk, Ada, Pascal,

vii
PREFACE

C, C++, Modula-3, PostScript, Java, or any of the countless other


programming languages and environments in which it has proved to
be an effective graph-processing method.

Acknowledgments
Many people gave me helpful feedback on earlier versions of this book.
In particular, hundreds of students at Princeton and Brown have suf-
fered through preliminary drafts over the years. Special thanks are due
to Trina Avery and Tom Freeman for their help in producing the first
edition; to Janet Incerpi for her creativity and ingenuity in persuading
our early and primitive digital computerized typesetting hardware and
software to produce the first edition; to Marc Brown for his part in the
algorithm visualization research that was the genesis of so many of the
figures in the book; to Dave Hanson for his willingness to answer all of
my questions about C; and to Kevin Wayne, for patiently answering my
basic questions about networks. I would also like to thank the many
readers who have provided me with detailed comments about various
editions, including Guy Almes, Jon Bentley, Marc Brown, Jay Gischer,
Allan Heydon, Kennedy Lemke, Udi Manber, Dana Richards, John
Reif, M. Rosenfeld, Stephen Seidman, Michael Quinn, and William
Ward.
To produce this new edition, I have had the pleasure of working
with Peter Gordon and Helen Goldstein at Addison-Wesley, who have
patiently shepherded this project as it has evolved from a standard
update to a massive rewrite. It has also been my pleasure to work with
several other members of the professional staff at Addison-Wesley. The
nature of this project made the book a somewhat unusual challenge
for many of them, and I much appreciate their forbearance.
I have gained two new mentors in writing this book, and partic-
ularly want to express my appreciation to them. First, Steve Summit
carefully checked early versions of the manuscript on a technical level,
and provided me with literally thousands of detailed comments, partic-
ularly on the programs. Steve clearly understood my goal of providing
elegant, efficient, and effective implementations, and his comments not
only helped me to provide a measure of consistency across the imple-
mentations, but also helped me to improve many of them substantially.
Second, Lyn Dupre also provided me with thousands of detailed com-

viii
ments on the manuscript, which were invaluable in helping me not only
to correct and avoid grammatical errors, but also—more important—
to find a consistent and coherent writing style that helps bind together
the daunting mass of technical material here. I am extremely grateful
for the opportunity to learn from Steve and Lyn—their input was vital
in the development of this book.
Much of what I have written here I have learned from the teaching
and writings of Don Knuth, my advisor at Stanford. Although Don had
no direct influence on this work, his presence may be felt in the book,
for it was he who put the study of algorithms on the scientific footing
that makes a work such as this possible. My friend and colleague
Philippe Flajolet, who has been a major force in the development of
the analysis of algorithms as a mature research area, has had a similar
influence on this work.
I am deeply thankful for the support of Princeton University,
Brown University, and the Institut National de Recherce en Informa-
tique et Automatique (INRIA), where I did most of the work on the
books; and of the Institute for Defense Analyses and the Xerox Palo
Alto Research Center, where I did some work on the books while
visiting. Many parts of these books are dependent on research that
has been generously supported by the National Science Foundation
and the Office of Naval Research. Finally, I thank Bill Bowen, Aaron
Lemonick, and Neil Rudenstine for their support in building an aca-
demic environment at Princeton in which I was able to prepare this
book, despite my numerous other responsibilities.

Robert Sedgewick
Marly-le-Roi, France, February, 1983
Princeton, New Jersey, January, 1990
Jamestown, Rhode Island, May, 2001

ix
This page intentionally left blank
To Adam, Andrew, Brett, Robbie,
and especially Linda

xi
Notes on Exercises
Classifying exercises is an activity fraught with peril, because readers
of a book such as this come to the material with various levels of
knowledge and experience. Nonetheless, guidance is appropriate, so
many of the exercises carry one of four annotations, to help you decide
how to approach them.
Exercises that test your understanding of the material are marked
with an open triangle, as follows:
 17.2 Consider the graph
3-7 1-4 7-8 0-5 5-2 3-8 2-9 0-6 4-9 2-6 6-4.
Draw the its DFS tree and use the tree to find the graph’s bridges
and edge-connected components.
Most often, such exercises relate directly to examples in the text. They
should present no special difficulty, but working them might teach you
a fact or concept that may have eluded you when you read the text.
Exercises that add new and thought-provoking information to the
material are marked with an open circle, as follows:

◦ 18.2 Write a program that counts the number of different pos-


sible results of topologically sorting a given DAG.
Such exercises encourage you to think about an important concept
that is related to the material in the text, or to answer a question that
may have occurred to you when you read the text. You may find it
worthwhile to read these exercises, even if you do not have the time to
work them through.
Exercises that are intended to challenge you are marked with a black
dot, as follows:
• 19.2 Describe how you would find the MST of a graph so large
that only V edges can fit into main memory at once.
Such exercises may require a substantial amount of time to complete,
depending upon your experience. Generally, the most productive ap-
proach is to work on them in a few different sittings.
A few exercises that are extremely difficult (by comparison with
most others) are marked with two black dots, as follows:
•• 20.2 Develop a reasonable generator for random graphs with
V vertices and E edges such that the running time of the PFS
implementation of Dijkstra’s algorithm is nonlinear.
These exercises are similar to questions that might be addressed in the
research literature, but the material in the book may prepare you to
enjoy trying to solve them (and perhaps succeeding).
The annotations are intended to be neutral with respect to your
programming and mathematical ability. Those exercises that require
expertise in programming or in mathematical analysis are self-evident.
All readers are encouraged to test their understanding of the algorithms
by implementing them. Still, an exercise such as this one is straight-
forward for a practicing programmer or a student in a programming
course, but may require substantial work for someone who has not
recently programmed:
• 17.2 Write a program that generates V random points in the
plane, then builds a network with edges (in both directions) con-
necting all pairs of points within a given distance d of one another
(see Program 3.20), setting each edge’s weight to the distance be-
tween the two points that it connects. Determine how to set d so
that the expected number of edges is E.
In a similar vein, all readers are encouraged to strive to appreciate
the analytic underpinnings of our knowledge about properties of al-
gorithms. Still, an exercise such as this one is straightforward for a
scientist or a student in a discrete mathematics course, but may require
substantial work for someone who has not recently done mathematical
analysis:

◦ 18.2 How many digraphs correspond to each undirected graph


with V vertices and E edges?
There are far too many exercises for you to read and assimilate
them all; my hope is that there are enough exercises here to stimulate
you to strive to come to a broader understanding on the topics that
interest you than you can glean by simply reading the text.

xiii
This page intentionally left blank
Contents

Graph Algorithms

Chapter 17. Graph Properties and Types 3


17.1 Glossary · 7
17.2 Graph ADT · 16
17.3 Adjacency-Matrix Representation · 21
17.4 Adjacency-Lists Representation · 27
17.5 Variations, Extensions, and Costs · 31
17.6 Graph Generators · 40
17.7 Simple, Euler, and Hamilton Paths · 50
17.8 Graph-Processing Problems · 64

Chapter 18. Graph Search 75


18.1 Exploring a Maze · 76
18.2 Depth-First Search · 81
18.3 Graph-Search ADT Functions · 86
TABLE OF CONTENTS

18.4 Properties of DFS Forests · 91


18.5 DFS Algorithms · 99
18.6 Separability and Biconnectivity · 106
18.7 Breadth-First Search · 114
18.8 Generalized Graph Search · 124
18.9 Analysis of Graph Algorithms · 133

Chapter 19. Digraphs and DAGs 141


19.1 Glossary and Rules of the Game · 144
19.2 Anatomy of DFS in Digraphs · 152
19.3 Reachability and Transitive Closure · 161
19.4 Equivalence Relations and Partial Orders · 174
19.5 DAGs · 178
19.6 Topological Sorting · 183
19.7 Reachability in DAGs · 193
19.8 Strong Components in Digraphs · 196
19.9 Transitive Closure Revisited · 208
19.10 Perspective · 212

Chapter 20. Minimum Spanning Trees 219


20.1 Representations · 222
20.2 Underlying Principles of MST Algorithms · 228
20.3 Prim’s Algorithm and Priority-First Search · 235
20.4 Kruskal’s Algorithm · 246
20.5 Boruvka’s Algorithm · 252
20.6 Comparisons and Improvements · 255
20.7 Euclidean MST · 261

xvi
Chapter 21. Shortest Paths 265
21.1 Underlying Principles · 273
21.2 Dijkstra’s algorithm · 280
21.3 All-Pairs Shortest Paths · 290
21.4 Shortest Paths in Acyclic Networks · 300
21.5 Euclidean Networks · 308
21.6 Reduction · 314
21.7 Negative Weights · 331
21.8 Perspective · 350

Chapter 22. Network Flows 353


22.1 Flow Networks · 359
22.2 Augmenting-Path Maxflow Algorithms · 370
22.3 Preflow-Push Maxflow Algorithms · 396
22.4 Maxflow Reductions · 411
22.5 Mincost Flows · 429
22.6 Network Simplex Algorithm · 439
22.7 Mincost-Flow Reductions · 457
22.8 Perspective · 467

References for Part Five 473

Index 475

xvii
This page intentionally left blank
P A R T
F I V E

Graph Algorithms
This page intentionally left blank
CHAPTER SEVENTEEN

Graph Properties and Types

M ANY COMPUTATIONAL APPLICATIONS naturally involve


not just a set of items, but also a set of connections between
pairs of those items. The relationships implied by these connections
lead immediately to a host of natural questions: Is there a way to get
from one item to another by following the connections? How many
other items can be reached from a given item? What is the best way to
get from this item to this other item?
To model such situations, we use abstract objects called graphs.
In this chapter, we examine basic properties of graphs in detail, setting
the stage for us to study a variety of algorithms that are useful for
answering questions of the type just posed. These algorithms make
effective use of many of the computational tools that we considered in
Parts 1 through 4. They also serve as the basis for attacking problems in
important applications whose solution we could not even contemplate
without good algorithmic technology.
Graph theory, a major branch of combinatorial mathematics,
has been studied intensively for hundreds of years. Many important
and useful properties of graphs have been proved, yet many difficult
problems remain unresolved. In this book, while recognizing that there
is much still to be learned, we draw from this vast body of knowledge
about graphs what we need to understand and use a broad variety of
useful and fundamental algorithms.
Like so many of the other problem domains that we have studied,
the algorithmic investigation of graphs is relatively recent. Although
a few of the fundamental algorithms are old, the majority of the in-
teresting ones have been discovered within the last few decades. Even

3
4 CHAPTER SEVENTEEN

the simplest graph algorithms lead to useful computer programs, and


the nontrivial algorithms that we examine are among the most elegant
and interesting algorithms known.
To illustrate the diversity of applications that involve graph pro-
cessing, we begin our exploration of algorithms in this fertile area by
considering several examples.
Maps A person who is planning a trip may need to answer
questions such as, “What is the least expensive way to get from Prince-
ton to San Jose?” A person more interested in time than in money may
need to know the answer to the question "What is the fastest way
to get from Princeton to San Jose?" To answer such questions, we
process information about connections (travel routes) between items
(towns and cities).
Hypertexts When we browse the Web, we encounter docu-
ments that contain references (links) to other documents, and we move
from document to document by clicking on the links. The entire web
is a graph, where the items are documents and the connections are
links. Graph-processing algorithms are essential components of the
search engines that help us locate information on the web.
Circuits An electric circuit comprises elements such as transis-
tors, resistors, and capacitors that are intricately wired together. We
use computers to control machines that make circuits, and to check
that the circuits perform desired functions. We need to answer simple
questions such as, “Is a short-circuit present?” as well as complicated
questions such as, “Can we lay out this circuit on a chip without mak-
ing any wires cross?” In this case, the answer to the first question
depends on only the properties of the connections (wires), whereas the
answer to the second question requires detailed information about the
wires, the items that those wires connect, and the physical constraints
of the chip.
Schedules A manufacturing process requires a variety of tasks
to be performed, under a set of constraints that specifies that certain
tasks cannot be started until certain other tasks have been completed.
We represent the constraints as connections between the tasks (items),
and we are faced with a classical scheduling problem: How do we
schedule the tasks such that we both respect the given constraints and
complete the whole process in the least amount of time?
GRAPH PROPERTIES AND TYPES 5

Transactions A telephone company maintains a database of


telephone-call traffic. Here the connections represent telephone calls.
We are interested in knowing about the nature of the interconnection
structure because we want to lay wires and build switches that can
handle the traffic efficiently. As another example, a financial institution
tracks buy/sell orders in a market. A connection in this situation
represents the transfer of cash between two customers. Knowledge of
the nature of the connection structure in this instance may enhance
our understanding of the nature of the market.
Matching Students apply for positions in selective institutions
such as social clubs, universities, or medical schools. Items correspond
to the students and the institutions; connections correspond to the
applications. We want to discover methods for matching interested
students with available positions.
Networks A computer network consists of interconnected sites
that send, forward, and receive messages of various types. We are
interested not just in knowing that it is possible to get a message from
every site to every other site, but also in maintaining this connectivity
for all pairs of sites as the network changes. For example, we might
wish to check a given network to be sure that no small set of sites or
connections is so critical that losing it would disconnect any remaining
pair of sites.
Program structure A compiler builds graphs to represent the
call structure of a large software system. The items are the various
functions or modules that comprise the system; connections are asso-
ciated either with the possibility that one function might call another
(static analysis) or with actual calls while the system is in operation
(dynamic analysis). We need to analyze the graph to determine how
best to allocate resources to the program most efficiently .
These examples indicate the range of applications for which
graphs are the appropriate abstraction, and also the range of com-
putational problems that we might encounter when we work with
graphs. Such problems will be our focus in this book. In many of
these applications as they are encountered in practice, the volume of
data involved is truly huge, and efficient algorithms make the difference
between whether or not a solution is at all feasible.
We have already encountered graphs, briefly, in Part 1. Indeed,
the first algorithms that we considered in detail, the union-find algo-
6 CHAPTER SEVENTEEN

rithms in Chapter 1, are prime examples of graph algorithms. We


also used graphs in Chapter 3 as an illustration of applications of two-
dimensional arrays and linked lists, and in Chapter 5 to illustrate the
relationship between recursive programs and fundamental data struc-
tures. Any linked data structure is a representation of a graph, and
some familiar algorithms for processing trees and other linked struc-
tures are special cases of graph algorithms. The purpose of this chapter
is to provide a context for developing an understanding of graph al-
gorithms ranging from the simple ones in Part 1 to the sophisticated
ones in Chapters 18 through 22.
As always, we are interested in knowing which are the most
efficient algorithms that solve a particular problem. The study of the
performance characteristics of graph algorithms is challenging because
• The cost of an algorithm depends not just on properties of the
set of items, but also on numerous properties of the set of con-
nections (and global properties of the graph that are implied by
the connections).
• Accurate models of the types of graphs that we might face are
difficult to develop.
We often work with worst-case performance bounds on graph algo-
rithms, even though they may represent pessimistic estimates on actual
performance in many instances. Fortunately, as we shall see, a number
of algorithms are optimal and involve little unnecessary work. Other
algorithms consume the same resources on all graphs of a given size.
We can predict accurately how such algorithms will perform in specific
situations. When we cannot make such accurate predictions, we need
to pay particular attention to properties of the various types of graphs
that we might expect in practical applications and must assess how
these properties might affect the performance of our algorithms.
We begin by working through the basic definitions of graphs
and the properties of graphs, examining the standard nomenclature
that is used to describe them. Following that, we define the basic
ADT (abstract data type) interfaces that we use to study graph algo-
rithms and the two most important data structures for representing
graphs—the adjacency-matrix representation and the adjacency-lists
representation, and various approaches to implementing basic ADT
functions. Then, we consider client programs that can generate ran-
dom graphs, which we can use to test our algorithms and to learn
GRAPH PROPERTIES AND TYPES §17.1 7

properties of graphs. All this material provides a basis for us to intro-


duce graph-processing algorithms that solve three classical problems
related to finding paths in graphs, which illustrate that the difficulty
of graph problems can differ dramatically even when they might seem
similar. We conclude the chapter with a review of the most important
graph-processing problems that we consider in this book, placing them
in context according to the difficulty of solving them.

17.1 Glossary
A substantial amount of nomenclature is associated with graphs. Most
of the terms have straightforward definitions, and, for reference, it is
convenient to consider them in one place: here. We have already used
some of these concepts when considering basic algorithms in Part 1;
others of them will not become relevant until we address associated
advanced algorithms in Chapters 18 through 22.
Definition 17.1 A graph is a set of vertices plus a set of edges that
connect pairs of distinct vertices (with at most one edge connecting
any pair of vertices).
We use the names 0 through V-1 for the vertices in a V -vertex graph.
The main reason that we choose this system is that we can access
quickly information corresponding to each vertex, using array index-
ing. In Section 17.6, we consider a program that uses a symbol table
to establish a 1–1 mapping to associate V arbitrary vertex names with
the V integers between 0 and V − 1. With that program in hand, we
can use indices as vertex names (for notational convenience) without
loss of generality. We sometimes assume that the set of vertices is
defined implicitly, by taking the set of edges to define the graph and
considering only those vertices that are included in at least one edge.
To avoid cumbersome usage such as “the ten-vertex graph with the
following set of edges,” we do not explicitly mention the number of
vertices when that number is clear from the context. By convention,
we always denote the number of vertices in a given graph by V , and
denote the number of edges by E.
We adopt as standard this definition of a graph (which we first
encountered in Chapter 5), but note that it embodies two technical
simplifications. First, it disallows duplicate edges (mathematicians
8 §17.1 CHAPTER SEVENTEEN

sometimes refer to such edges as parallel edges, and a graph that can
contain them as a multigraph). Second, it disallows edges that connect
vertices to themselves; such edges are called self-loops. Graphs that
have no parallel edges or self-loops are sometimes referred to as simple
graphs.
We use simple graphs in our formal definitions because it is easier
to express their basic properties and because parallel edges and self-
loops are not needed in many applications. For example, we can
bound the number of edges in a simple graph with a given number of
vertices.
Property 17.1 A graph with V vertices has at most V (V −1)/2 edges.
Proof : The total of V 2 possible pairs of vertices includes V self-loops
and accounts twice for each edge between distinct vertices, so the
number of edges is at most (V 2 − V )/2 = V (V − 1)/2.
No such bound holds if we allow parallel edges: a graph that is not
simple might consist of two vertices and billions of edges connecting
them (or even a single vertex and billions of self-loops).
For some applications, we might consider the elimination of par-
allel edges and self-loops to be a data-processing problem that our
implementations must address. For other applications, ensuring that
a given set of edges represents a simple graph may not be worth the
trouble. Throughout the book, whenever it is more convenient to ad-
dress an application or to develop an algorithm by using an extended
definition that includes parallel edges or self-loops, we shall do so.
For example, self-loops play a critical role in a classical algorithm that
we will examine in Section 17.4; and parallel edges are common in
the applications that we address in Chapter 22. Generally, it is clear
from the context whether we intend the term “graph” to mean “simple
graph” or “multigraph” or “multigraph with self-loops.”
Mathematicians use the words vertex and node interchangeably,
but we generally use vertex when discussing graphs and node when
discussing representations—for example, in C data structures. We
normally assume that a vertex can have a name and can carry other
associated information. Similarly, the words arc, edge, and link are all
widely used by mathematicians to describe the abstraction embodying
a connection between two vertices, but we consistently use edge when
discussing graphs and link when discussing C data structures.
GRAPH PROPERTIES AND TYPES §17.1 9

When there is an edge connecting two vertices, we say that the


vertices are adjacent to one another and that the edge is incident on
both vertices. The degree of a vertex is the number of edges incident
on it. We use the notation v-w to represent an edge that connects v
and w; the notation w-v is an alternative way to represent the same
edge.
A subgraph is a subset of a graph’s edges (and associated vertices)
that constitutes a graph. Many computational tasks involve identifying
subgraphs of various types. If we identify a subset of a graph’s vertices, 0
we call that subset, together with all edges that connect two of its 6 7 8
members, the induced subgraph associated with those vertices. 1 2
We can draw a graph by marking points for the vertices and draw-
3 9 10
ing lines connecting them for the edges. A drawing gives us intuition 4
about the structure of the graph; but this intuition can be misleading, 5 11 12
because the graph is defined independently of the representation. For
example, the two drawings in Figure 17.1 and the list of edges repre-
sent the same graph, because the graph is only its (unordered) set of 7
3
vertices and its (unordered) set of edges (pairs of vertices)—nothing 5 4
more. Although it suffices to consider a graph simply as a set of edges, 11
we examine other representations that are particularly suitable as the 8 9 10
basis for graph data structures in Section 17.4. 1 12
Placing the vertices of a given graph on the plane and drawing 2 0 6
them and the edges that connect them is known as graph drawing.
The possible vertex placements, edge-drawing styles, and aesthetic 0-5 5-4 7-8
constraints on the drawing are limitless. Graph-drawing algorithms 4-3 0-2 9-11
that respect various natural constraints have been studied heavily and 0-1 11-12 5-3
9-12 9-10
have many successful applications (see reference section). For example, 6-4 0-6
one of the simplest constraints is to insist that edges do not intersect. A
planar graph is one that can be drawn in the plane without any edges
Figure 17.1
crossing. Determining whether or not a graph is planar is a fascinating
Three different representa-
algorithmic problem that we discuss briefly in Section 17.8. Being tions of the same graph
able to produce a helpful visual representation is a useful goal, and A graph is defined by its vertices
graph drawing is a fascinating field of study, but successful drawings and its edges, not by the way that
are often difficult to realize. Many graphs that have huge numbers of we choose to draw it. These two
vertices and edges are abstract objects for which no suitable drawing drawings depict the same graph,
as does the list of edges (bottom),
is feasible. given the additional information
For some applications, such as graphs that represent maps or that the graph has 13 vertices la-
circuits, a graph drawing can carry considerable information because beled 0 through 12.
10 §17.1 CHAPTER SEVENTEEN

the vertices correspond to points in the plane and the distances between
them are relevant. We refer to such graphs as Euclidean graphs. For
many other applications, such as graphs that represent relationships
0 or schedules, the graphs simply embody connectivity information, and
6 7 8 no particular geometric placement of vertices is ever implied. We
1 2 consider examples of algorithms that exploit the geometric information
in Euclidean graphs in Chapters 20 and 21, but we primarily work with
3 9 10
algorithms that make no use of any geometric information, and stress
4
5 11 12 that graphs are generally independent of any particular representation
in a drawing or in a computer.
Focusing solely on the connections themselves, we might wish to
view the vertex labels as merely a notational convenience, and to regard
10
6 1 8 two graphs as being the same if they differ in only the vertex labels.
7 2 Two graphs are isomorphic if we can change the vertex labels on one
to make its set of edges identical to the other. Determining whether
3 9 0
12 or not two graphs are isomorphic is a difficult computational problem
5 11 4 (see Figure 17.2 and Exercise 17.5). It is challenging because there are
V ! possible ways to label the vertices—far too many for us to try all
the possibilities. Therefore, despite the potential appeal of reducing
0 the number of different graph structures that we have to consider by
6 7 8 treating isomorphic graphs as identical structures, we rarely do so.
1 2
As we saw with trees in Chapter 5, we are often interested in
3 9 10 basic structural properties that we can deduce by considering specific
4 sequences of edges in a graph.
5 11 12
Definition 17.2 A path in a graph is a sequence of vertices in which
each successive vertex (after the first) is adjacent to its predecessor in
Figure 17.2 the path. In a simple path, the vertices and edges are distinct. A cycle
Graph isomorphism examples
is a path that is simple except that the first and final vertices are the
The top two graphs are isomorphic
same.
because we can relabel the ver-
tices to make the two sets of edges We sometimes use the term cyclic path to refer to a path whose first
identical (to make the middle
graph the same as the top graph, and final vertices are the same (and is otherwise not necessarily simple);
change 10 to 4, 7 to 3, 2 to 5, 3 to and we use the term tour to refer to a cyclic path that includes every
1, 12 to 0, 5 to 2, 9 to 11, 0 to 12, vertex. An equivalent way to define a path is as the sequence of
11 to 9, 1 to 7, and 4 to 10). The edges that connect the successive vertices. We emphasize this in our
bottom graph is not isomorphic to
the others because there is no way notation by connecting vertex names in a path in the same way as we
to relabel the vertices to make its connect them in an edge. For example, the simple paths in Figure 17.1
set of edges identical to either. include 3-4-6-0-2, and 9-12-11, and the cycles in the graph include
GRAPH PROPERTIES AND TYPES §17.1 11

vertex
path Figure 17.3
spanning tree Graph terminology
This graph has 55 vertices, 70
edges, and 3 connected compo-
cycle nents. One of the connected com-
ponents is a tree (right). The graph
has many cycles, one of which is
highlighted in the large connected
component (left). The diagram also
tree
depicts a spanning tree in the small
edge
connected component (center).
The graph as a whole does not
clique
have a spanning tree, because it
is not connected.

0-6-4-3-5-0 and 5-4-3-5. We define the length of a path or a cycle


to be its number of edges.
We adopt the convention that each single vertex is a path of
length 0 (a path from the vertex to itself with no edges on it, which
is different from a self-loop). Apart from this convention, in a graph
with no parallel edges and no self-loops, a pair of vertices uniquely
determines an edge, paths must have on them at least two distinct
vertices, and cycles must have on them at least three distinct edges and
three distinct vertices.
We say that two simple paths are disjoint if they have no vertices
in common other than, possibly, their endpoints. Placing this condition
is slightly weaker than insisting that the paths have no vertices at all in
common, and is useful because we can combine simple disjoint paths
from s to t and t to u to get a simple disjoint path from s to u if s and
u are different (and to get a cycle if s and u are the same). The term
vertex disjoint is sometimes used to distinguish this condition from the
stronger condition of edge disjoint, where we require that the paths
have no edge in common.

Definition 17.3 A graph is a connected graph if there is a path


from every vertex to every other vertex in the graph. A graph that is
not connected consists of a set of connected components, which are
maximal connected subgraphs.

The term maximal connected subgraph means that there is no path


from a subgraph vertex to any vertex in the graph that is not in the
subgraph. Intuitively, if the vertices were physical objects, such as
12 §17.1 CHAPTER SEVENTEEN

knots or beads, and the edges were physical connections, such as strings
2
3 or wires, a connected graph would stay in one piece if picked up by
1
any vertex, and a graph that is not connected comprises two or more
4
such pieces.
0
Definition 17.4 An acyclic connected graph is called a tree (see Chap-
5
ter 4). A set of trees is called a forest. A spanning tree of a connected
8
6
graph is a subgraph that contains all of that graph’s vertices and is a
7 single tree. A spanning forest of a graph is a subgraph that contains
2
all of that graph’s vertices and is a forest.
1 3
For example, the graph illustrated in Figure 17.1 has three con-
nected components, and is spanned by the forest 7-8 9-10 9-11 9-12
0 4
0-1 0-2 0-5 5-3 5-4 4-6 (there are many other spanning forests).
7 5 Figure 17.3 highlights these and other features in a larger graph.
6 We explore further details about trees in Chapter 4, and look at
various equivalent definitions. For example, a graph G with V vertices
2
1 is a tree if and only if it satisfies any of the following four conditions:
3
• G has V − 1 edges and no cycles.
0
• G has V − 1 edges and is connected.
4
6 • Exactly one simple path connects each pair of vertices in G.
5
• G is connected, but removing any edge disconnects it.
1 2 Any one of these conditions is necessary and sufficient to prove the
other three, and we can develop other combinations of facts about
0 3
trees from them (see Exercise 17.1). Formally, we should choose one
5 4 condition to serve as a definition; informally, we let them collectively
serve as the definition, and freely engage in usage such as the “acyclic
1
2 connected graph” choice in Definition 17.4.
0 Graphs with all edges present are called complete graphs (see
3 Figure 17.4). We define the complement of a graph G by starting with
4
a complete graph that has the same set of vertices as the original graph,
Figure 17.4 and removing the edges of G. The union of two graphs is the graph
Complete graphs induced by the union of their sets of edges. The union of a graph and
These complete graphs, with ev- its complement is a complete graph. All graphs that have V vertices are
ery vertex connected to every other subgraphs of the complete graph that has V vertices. The total number
vertex, have 10, 15, 21, 28, and of different graphs that have V vertices is 2V (V −1)/2 (the number of
36 edges (bottom to top). Every different ways to choose a subset from the V (V − 1)/2 possible edges).
graph with between 5 and 9 ver-
tices (there are more than 68 bil- A complete subgraph is called a clique.
lion such graphs) is a subgraph of Most graphs that we encounter in practice have relatively few
one of these graphs. of the possible edges present. To quantify this concept, we define the
GRAPH PROPERTIES AND TYPES §17.1 13

density of a graph to be the average vertex degree, or 2E/V . A dense


graph is a graph whose average vertex degree is proportional to V ; a
sparse graph is a graph whose complement is dense. In other words,
we consider a graph to be dense if E is proportional to V 2 and sparse
otherwise. This asymptotic definition is not necessarily meaningful for
a particular graph, but the distinction is generally clear: A graph that
has millions of vertices and tens of millions of edges is certainly sparse,
and a graph that has thousands of vertices and millions of edges is
certainly dense. We might contemplate processing a sparse graph with
billions of vertices, but a dense graph with billions of vertices would
have an overwhelming number of edges.
Knowing whether a graph is sparse or dense is generally a key
factor in selecting an efficient algorithm to process the graph. For
example, for a given problem, we might develop one algorithm that
takes about V 2 steps and another that takes about E lg E steps. These
formulas tell us that the second algorithm would be better for sparse
graphs, whereas the first would be preferred for dense graphs. For
example, a dense graph with millions of edges might have only thou-
sands of vertices: in this case V 2 and E would be comparable in value,
and the V 2 algorithm would be 20 times faster than the E lg E algo- 0
rithm. On the other hand, a sparse graph with millions of edges also 6 7 8
has millions of vertices, so the E lg E algorithm could be millions of 1 2
times faster than the V 2 algorithm. We could make specific tradeoffs
3 9 10
on the basis of analyzing these formulas in more detail, but it generally
4
suffices in practice to use the terms sparse and dense informally to help 5 11 12
us understand fundamental performance characteristics.
When analyzing graph algorithms, we assume that V /E is 0 2 4 6 8 10 12
bounded by above a small constant, so that we can abbreviate ex-
pressions such as V (V + E) to V E. This assumption comes into play
only when the number of edges is tiny in comparison to the number of
vertices—a rare situation. Typically, the number of edges far exceeds 1 3 5 7 9 11
the number of vertices (V /E is much less than 1).
A bipartite graph is a graph whose vertices we can divide into Figure 17.5
A bipartite graph
two sets such that all edges connect a vertex in one set with a vertex
in the other set. Figure 17.5 gives an example of a bipartite graph. All edges in this graph connect
odd-numbered vertices with even-
Bipartite graphs arise in a natural way in many situations, such as the numbered ones, so it is bipartite.
matching problems described at the beginning of this chapter. Any The bottom diagram makes the
subgraph of a bipartite graph is bipartite. property obvious.
14 §17.1 CHAPTER SEVENTEEN

Graphs as defined to this point are called undirected graphs. In


directed graphs, also known as digraphs, edges are one-way: we con-
sider the pair of vertices that defines each edge to be an ordered pair
that specifies a one-way adjacency where we think about having the
ability to get from the first vertex to the second but not from the second
vertex to the first. Many applications (for example, graphs that rep-
resent the web, scheduling constraints or telephone-call transactions)
are naturally expressed in terms of digraphs.
We refer to edges in digraphs as directed edges, though that
0
6 7 8
distinction is generally obvious in context (some authors reserve the
1 2 term arc for directed edges). The first vertex in a directed edge is called
the source; the second vertex is called the destination. (Some authors
3 9 10 use the terms head and tail, respectively, to distinguish the vertices in
4 directed edges, but we avoid this usage because of overlap with our
5 11 12
use of the same terms in data-structure implementations.) We draw
directed edges as arrows pointing from source to destination, and often
say that the edge points to the destination. When we use the notation
0
v-w in a digraph, we mean it to represent an edge that points from v to
6 7 8
1 2 w; it is different from w-v, which represents an edge that points from w
to v. We speak of the indegree and outdegree of a vertex (the number
3 9 10 of edges where it is the destination and the number of edges where it
4 is the source, respectively).
5 11 12
Sometimes, we are justified in thinking of an undirected graph
as a digraph that has two directed edges (one in each direction); other
Figure 17.6 times, it is useful to think of undirected graphs simply in terms of
Two digraphs connections. Normally, as discussed in detail in Section 17.4, we
The drawing at the top is a rep- use the same representation for directed and undirected graphs (see
resentation of the example graph
Figure 17.6). That is, we generally maintain two representations of
in Figure 17.1 interpreted as a di-
rected graph, where we take the each edge for undirected graphs, one pointing in each direction, so
edges to be ordered pairs and rep- that we can immediately answer questions such as, “Which vertices
resent them by drawing an arrow are connected to vertex v?”
from the first vertex to the sec-
Chapter 19 is devoted to exploring the structural properties of
ond. It is also a DAG. The drawing
at the bottom is a representation digraphs; they are generally more complicated than the corresponding
of the undirected graph from Fig- properties for undirected graphs. A directed cycle in a digraph is a
ure 17.1 that indicates the way that cycle in which all adjacent vertex pairs appear in the order indicated by
we usually represent undirected
(directed) graph edges. A directed acyclic graph (DAG), is a digraph
graphs: as digraphs with two edges
corresponding to each connection that has no directed cycles. A DAG (an acyclic digraph) is not the
(one in each direction). same as a tree (an acyclic undirected graph). Occasionally, we refer to
GRAPH PROPERTIES AND TYPES §17.1 15

the underlying undirected graph of a digraph, meaning the undirected


graph defined by the same set of edges, but where these edges are not
interpreted as directed.
Chapters 20 through 22 are generally concerned with algorithms
for solving various computational problems associated with graphs
in which other information is associated with the vertices and edges.
In weighted graphs, we associate numbers (weights) with each edge,
which generally represents a distance or cost. We also might associate
a weight with each vertex, or multiple weights with each vertex and
edge. In Chapter 20 we work with weighted undirected graphs; in
Chapters 21 and 22 we study weighted digraphs, which we also refer
to as networks. The algorithms in Chapter 22 solve classic problems
that arise from a particular interpretation of networks known as flow
networks.
As was evident even in Chapter 1, the combinatorial structure
of graphs is extensive. This extent of this structure is all the more
remarkable because it springs forth from a simple mathematical ab-
straction. This underlying simplicity will be reflected in much of the
code that we develop for basic graph processing. However, this sim-
plicity sometimes masks complicated dynamic properties that require
deep understanding of the combinatorial properties of graphs them-
selves. It is often far more difficult to convince ourselves that a graph
algorithm works as intended than the compact nature of the code
might suggest.
Exercises
17.1 Prove that any acyclic connected graph that has V vertices has V − 1
edges.
 17.2 Give all the connected subgraphs of the graph
0-1 0-2 0-3 1-3 2-3.

 17.3 Write down a list of the nonisomorphic cycles of the graph in Fig-
ure 17.1. For example, if your list contains 3-4-5-3, it should not contain
3-5-4-3, 4-5-3-4, 4-3-5-4, 5-3-4-5, or 5-4-3-5.
17.4 Consider the graph
3-7 1-4 7-8 0-5 5-2 3-8 2-9 0-6 4-9 2-6 6-4.
Determine the number of connected components, give a spanning forest, list
all the simple paths with at least three vertices, and list all the nonisomorphic
cycles (see Exercise 17.3).
16 §17.2 CHAPTER SEVENTEEN

◦ 17.5 Consider the graphs defined by the following four sets of edges:
0-1 0-2 0-3 1-3 1-4 2-5 2-9 3-6 4-7 4-8 5-8 5-9 6-7 6-9 7-8
0-1 0-2 0-3 0-3 1-4 2-5 2-9 3-6 4-7 4-8 5-8 5-9 6-7 6-9 7-8
0-1 1-2 1-3 0-3 0-4 2-5 2-9 3-6 4-7 4-8 5-8 5-9 6-7 6-9 7-8
4-1 7-9 6-2 7-3 5-0 0-2 0-8 1-6 3-9 6-3 2-8 1-5 9-8 4-5 4-7
Which of these graphs are isomorphic to one another? Which of them are
planar?
17.6 Consider the more than 68 billion graphs referred to in the caption to
Figure 17.4. What percentage of them has fewer than nine vertices?
 17.7 How many different subgraphs are there in a given graph with V ver-
tices and E edges?
• 17.8 Give tight upper and lower bounds on the number of connected com-
ponents in graphs that have V vertices and E edges.
◦ 17.9 How many different undirected graphs are there that have V vertices
and E edges?
••• 17.10 If we consider two graphs to be different only if they are not isomorphic,
how many different graphs are there that have V vertices and E edges?
17.11 How many V -vertex graphs are bipartite?

17.2 Graph ADT


We develop our graph-processing algorithms within the context of
an ADT that defines the tasks of interest, using the standard mech-
anisms considered in Chapter 4. Program 17.1 is the nucleus of the
ADT interface that we use for this purpose. Basic graph representa-
tions and implementations for this ADT are the topic of Sections 17.3
through 17.5. Later in the book, whenever we consider a new graph-
processing problem, we consider the algorithms that solve it and their
implementations in the context of new ADT functions for this inter-
face. This scheme allows us to address graph-processing tasks ranging
from elementary maintenance functions to sophisticated solutions of
difficult problems.
The interface is based on our standard mechanism that hides
representations and implementations from client programs (see Sec-
tion 4.8). It also includes a simple structure type definition that allows
our programs to manipulate edges in a uniform way. The interface pro-
vides the basic mechanisms that allows clients to build graphs (by ini-
tializing the graph and then adding the edges), to maintain the graphs
GRAPH PROPERTIES AND TYPES §17.2 17

Program 17.1 Graph ADT interface


This interface is a starting point for implementing and testing graph
algorithms. Throughout this and the next several chapters, we shall add
functions to this interface for solving various graph-processing prob-
lems. Various assumptions that simplify the code and other issues sur-
rounding the design of a general-purpose graph-processing interface are
discussed in the text.
The interface defines two data types: a simple Edge data type,
including a constructor function EDGE that makes an Edge from two
vertices; and a Graph data type, which is defined with the standard
representation-independent construction from Chapter 4. The basic
operations that we use to process graphs are ADT functions to create,
copy, and destroy them; to add and delete edges; and to extract an edge
list.
typedef struct { int v; int w; } Edge;
Edge EDGE(int, int);

typedef struct graph *Graph;


Graph GRAPHinit(int);
void GRAPHinsertE(Graph, Edge);
void GRAPHremoveE(Graph, Edge);
int GRAPHedges(Edge [], Graph G);
Graph GRAPHcopy(Graph);
void GRAPHdestroy(Graph);

(by removing some edges and adding others), and to retrieve the graphs
(in the form of an array of edges).
The ADT in Program 17.1 is primarily a vehicle to allow us to
develop and test algorithms; it is not a general-purpose interface. As
usual, we work with the simplest interface that supports the basic
graph-processing operations that we wish to consider. Defining such
an interface for use in practical applications involves making numerous
tradeoffs among simplicity, efficiency, and generality. We consider a
few of these tradeoffs next; we address many others in the context of
implementations and applications throughout this book.
We assume for simplicity that graph representations include inte-
gers V and E that contain the number of vertices and edges, respectively,
so that we can refer directly to those values by name in ADT implemen-
tations. When convenient, we make other, similar, assumptions about
18 §17.2 CHAPTER SEVENTEEN

variables in graph representations, primarily to keep implementations


compact. For convenience, we also provide the maximum possible
number of vertices in the graph as an argument to the GRAPHinit
ADT function so that implementations can allocate memory accord-
ingly. We adopt these conventions solely to make the code compact
and readable.
A slightly more general interface might provide the capability to
add and remove vertices as well as edges (and might include functions
that return the number of vertices and edges), making no assumptions
about implementations. This design would allow for ADT implemen-
tations that grow and shrink their data structures as the graph grows
and shrinks. We might also choose to work at an intermediate level of
abstraction, and consider the design of interfaces that support higher-
level abstract operations on graphs that we can use in implementations.
We revisit this idea briefly in Section 17.5, after we consider several
concrete representations and implementations.
A general graph ADT needs to take into account parallel edges
and self-loops, because nothing prevents a client program from call-
ing GRAPHinsertE with an edge that is already present in the graph
(parallel edge) or with an edge whose two vertex indices are the same
(self-loop). It might be necessary to disallow such edges in some appli-
cations, desirable to include them in other applications, and possible
to ignore them in still other applications. Self-loops are trivial to
handle, but parallel edges can be costly to handle, depending on the
graph representation. In certain situations, adding a remove parallel
edges ADT function might be appropriate; then, implementations can
let parallel edges collect, and clients can remove or otherwise process
parallel edges when warranted.
Program 17.1 includes a function for implementations to return
a graph’s set of edges to a client, in an array. A graph is nothing more
nor less than its set of edges, and we often need a way to retrieve a
graph in this form, regardless of its internal representation. We might
even consider an array of edges representation as the basis for an ADT
implementation (see Exercise 17.15). That representation, however,
does not provide the flexibility that we need to perform efficiently the
basic graph-processing operations that we shall be studying.
In this book, we generally work with static graphs, which have
a fixed number of vertices V and edges E. Generally, we build the
GRAPH PROPERTIES AND TYPES §17.2 19

graphs by executing E calls to GRAPHinsertE, then process them by


calling some ADT function that takes a graph as argument and returns
some information about that graph. Dynamic problems, where we
intermix graph processing with edge and vertex insertion and removal,
take us into the realm of online algorithms (also known as dynamic
algorithms), which present a different set of challenges. For example,
the union-find problem that we considered in Chapter 1 is an example
of an online algorithm, because we can get information about the
connectivity of a graph as we insert edges. The ADT in Program 17.1
supports insert edge and remove edge operations, so clients are free to
use them to make changes in graphs, but there may be performance
penalties for certain sequences of operations. For example, union-find
algorithms are effective for only those clients that do not use remove
edge.
The ADT might also include a function that takes an array of
edges as an argument for use in initializing the graph. We could easily
implement this function by calling GRAPHinsert for each of the edges
(see Exercise 17.13) or, depending on the graph representation, we
might be able to craft a more efficient implementation.
We might also provide graph-traversal functions that call client-
supplied functions for each edge or each vertex in the graph. For
some simple problems, using the array returned by GRAPHedges might
suffice. Most of our implementations, however, do more complicated
traversals that reveal information about the graph’s structure, while
implementing functions that provide a higher level of abstraction to
clients.
In Sections 17.3 through 17.5, we examine the primary classical
graph representations and implementations of the ADT functions in
Program 17.1. These implementations provide a basis for us to expand
the interface to include the graph-processing tasks that are our focus
for the next several chapters.
When we consider a new graph-processing problem, we extend
the ADT as appropriate to encompass functions that implement algo-
rithms of interest for solving the problem. Generally these tasks fall
into one of two broad categories:
• Compute the value of some measure of the graph.
• Compute some subset of the edges of the graph.
20 §17.2 CHAPTER SEVENTEEN

Program 17.2 Example of a graph-processing client


This program takes V and E from standard input, generates a random
graph with V vertices and E edges, prints the graph if it is small, and
computes (and prints) the number of connected components. It uses the
ADT functions GRAPHrand (see Program 17.8), GRAPHshow (see Exer-
cise 17.16 and Program 17.5), and GRAPHcc (see Program 18.4)).

#include <stdio.h>
#include "GRAPH.h"
main(int argc, char *argv[])
{ int V = atoi(argv[1]), E = atoi(argv[2]);
Graph G = GRAPHrand(V, E);
if (V < 20)
GRAPHshow(G);
else printf("%d vertices, %d edges, ", V, E);
printf("%d component(s)\n", GRAPHcc(G));
}

Examples of the former are the number of connected components and


the length of the shortest path between two given vertices in the graph;
examples of the latter are a spanning tree and the longest cycle contain-
ing a given vertex. Indeed, the terms that we defined in Section 17.1
immediately bring to mind a host of computational problems.
Program 17.2 is an example of a graph-processing client pro-
gram. It uses the basic ADT of Program 17.1, augmented by a gen-
erate random graph ADT function that returns a random graph that
contains a given number of vertices and edges (see Section 17.6), and a
connected components ADT function that returns the number of con-
nected components in a given graph (see Section 18.4). We use similar
but more sophisticated clients to generate other types of graphs, to test
algorithms, and to learn properties of graphs. The basic interface is
amenable for use in any graph-processing application.
The first decision that we face in developing an ADT implementa-
tion is which graph representation to use. We have three basic require-
ments. First, we must be able to accommodate the types of graphs
that we are likely to encounter in applications (and we also would
prefer not to waste space). Second, we should be able to construct
the requisite data structures efficiently. Third, we want to develop
GRAPH PROPERTIES AND TYPES §17.3 21

efficient algorithms to solve our graph-processing problems without


being unduly hampered by any restrictions imposed by the represen-
tation. Such requirements are standard ones for any domain that we
consider—we emphasize them again them here because, as we shall
see, different representations give rise to huge performance differences
for even the simplest of problems.
Most graph-processing applications can be handled reasonably
with one of two straightforward classical representations that are only
0 1 2 3 4 5 6 7 8 9 10 11 12
slightly more complicated than the array-of-edges representation: the 0 0 1 1 0 0 1 1 0 0 0 0 0 0
adjacency-matrix or the adjacency-lists representation. These repre- 1 1 0 0 0 0 0 0 0 0 0 0 0 0
2 1 0 0 0 0 0 0 0 0 0 0 0 0
sentations, which we consider in detail in Sections 17.3 and 17.4, are 3 0 0 0 0 1 1 0 0 0 0 0 0 0
based on elementary data structures (indeed, we discussed them both 4 0 0 0 1 0 1 1 0 0 0 0 0 0
5 1 0 0 1 1 0 0 0 0 0 0 0 0
in Chapters 3 and 5 as example applications of sequential and linked
6 1 0 0 0 1 0 0 0 0 0 0 0 0
allocation). The choice between the two depends primarily on whether 7 0 0 0 0 0 0 0 0 1 0 0 0 0
the graph is dense or sparse, although, as usual, the nature of the op- 8 0 0 0 0 0 0 0 1 0 0 0 0 0
9 0 0 0 0 0 0 0 0 0 0 1 1 1
erations to be performed also plays an important role in the decision 10 0 0 0 0 0 0 0 0 0 1 0 0 0
on which to use. 11 0 0 0 0 0 0 0 0 0 1 0 0 1
12 0 0 0 0 0 0 0 0 0 1 0 1 0
Exercises
 17.12 Write a program that builds a graph by reading edges (pairs of integers Figure 17.7
between 0 and V − 1) from standard input. Adjacency-matrix graph rep-
resentation
17.13 Write a representation-independent graph-initialization ADT function This matrix is another represen-
that, given an array of edges, returns a graph. tation of the graph depicted in
17.14 Write a representation-independent graph ADT function that uses Figure 17.1. It has a 1 in row v
GRAPHedges to print out all the edges in the graph, in the format used in and column w whenever there is
this text (vertex numbers separated by a hyphen). an edge connecting vertex v and
vertex w. The array is symmetric
17.15 Provide an implementation of the ADT functions in Program 17.1 that about the diagonal. For example,
uses an array of edges to represent the graph. Modify GRAPHinit to take the sixth row (and the sixth col-
the maximum number of edges allowed as its second argument, for use in umn) says that vertex 6 is con-
allocating the edge array. Use a brute-force implementation of GRAPHremoveE nected to vertices 0 and 4. For
that removes an edge v-w by scanning the array to find v-w or w-v, and then some applications, we will adopt
exchanges the edge found with the final one in the array. Disallow parallel the convention that each vertex is
edges by doing a similar scan in GRAPHinsertE. connected to itself, and assign 1s
on the main diagonal. The large
blocks of 0s in the upper right and
lower left corners are artifacts of
17.3 Adjacency-Matrix Representation the way we assigned vertex num-
bers for this example, not charac-
An adjacency-matrix representation of a graph is a V -by-V array of teristic of the graph (except that
Boolean values, with the entry in row v and column w defined to be 1 if they do indicate the graph to be
there is an edge connecting vertex v and vertex w in the graph, and to sparse).
22 §17.3 CHAPTER SEVENTEEN

Program 17.3 Graph ADT implementation (adjacency matrix)


This implementation of the interface in Program 17.1 uses a two-
dimensional array. An implementation of the function MATRIXint,
which allocates memory for the array and initializes it, is given in Pro-
gram 17.4. The rest of the code is straightforward: An edge i-j is
present in the graph if and only if a[i][j] and a[j][i] are both 1.
Edges are inserted and removed in constant time, and duplicate edges
are silently ignored. Initialization and extracting all edges each take
time proportional to V 2 .

#include <stdlib.h>
#include "GRAPH.h"
struct graph { int V; int E; int **adj; };
Graph GRAPHinit(int V)
{ Graph G = malloc(sizeof *G);
G->V = V; G->E = 0;
G->adj = MATRIXint(V, V, 0);
return G;
}
void GRAPHinsertE(Graph G, Edge e)
{ int v = e.v, w = e.w;
if (G->adj[v][w] == 0) G->E++;
G->adj[v][w] = 1;
G->adj[w][v] = 1;
}
void GRAPHremoveE(Graph G, Edge e)
{ int v = e.v, w = e.w;
if (G->adj[v][w] == 1) G->E--;
G->adj[v][w] = 0;
G->adj[w][v] = 0;
}
int GRAPHedges(Edge a[], Graph G)
{ int v, w, E = 0;
for (v = 0; v < G->V; v++)
for (w = v+1; w < G->V; w++)
if (G->adj[v][w] == 1)
a[E++] = EDGE(v, w);
return E;
}
GRAPH PROPERTIES AND TYPES §17.3 23

Program 17.4 Adjacency-matrix allocation and initialization


This program uses the standard C array-of-arrays representation for the
two-dimensional adjacency matrix (see Section 3.7). It allocates r rows
with c integers each, then initializes all entries to the value val. The call
MATRIXint(V, V, 0) in Program 17.3 takes time proportional to V 2
to create a matrix that represents a V -vertex graph with no edges. For
small V , the cost of V calls to malloc might predominate.

int **MATRIXint(int r, int c, int val)


{ int i, j;
int **t = malloc(r * sizeof(int *));
for (i = 0; i < r; i++)
t[i] = malloc(c * sizeof(int));
for (i = 0; i < r; i++)
for (j = 0; j < c; j++)
t[i][j] = val;
return t;
}

0
01 1 00 110 00 000
be 0 otherwise. Program 17.3 is an implementation of the graph ADT 1
10 0 00 000 00 000
that uses a direct representation of this matrix. The implementation 2
10 0 00 000 00 000
maintains a two-dimensional array of integers with the entry a[v][w] 3
00 0 01 100 00 000
set to 1 if there is an edge connecting v and w in the graph, and set to 0 4
00 0 10 110 00 000
otherwise. In an undirected graph, each edge is actually represented by 5
10 0 11 000 00 000
two entries: the edge v-w is represented by 1 values in both a[v][w] 6
10 0 01 000 00 000
7
and a[w][v], as is the edge w-v. 00 0 00 000 10 000
8
As mentioned in Section 17.2, we generally assume that the num- 00 0 00 001 00 000
9
ber of vertices is known to the client when the graph is initialized. For 00 0 00 000 00 111
10
many applications, we might set the number of vertices as a compile- 00 0 00 000 01 000
11
time constant and use statically allocated arrays, but Program 17.3 00 0 00 000 01 001
12
takes the slightly more general approach of allocating dynamically the 00 0 00 000 01 010

space for the adjacency matrix. Program 17.4 is an implementation


of the standard method of dynamically allocating a two-dimensional Figure 17.8
Adjacency matrix data struc-
array in C, as an array of pointers, as depicted in Figure 17.8. Pro- ture
gram 17.4 also includes code that initializes the graph by setting the This figure depicts the C represen-
array entries all to a given value. This operation takes time propor- tation of the graph in Figure 17.1,
tional to V 2 . Error checks for insufficient memory are not included in as an array of arrays.
24 §17.3 CHAPTER SEVENTEEN

Program 17.4 for brevity—it is prudent programming practice to add


them before using this code (see Exercise 17.22).
To add an edge, we set the two indicated array entries to 1. We
do not allow parallel edges in this representation: If an edge is to
be inserted for which the array entries are already 1, the code has no
effect. In some ADT designs, it might be preferable to inform the client
of the attempt to insert a parallel edge, perhaps using a return code
from GRAPHinsertE. We do allow self-loops in this representation: An
edge v-v is represented by a nonzero entry in a[v][v].
To remove an edge, we set the two indicated array entries to 0.
If a nonexistent edge (one for which the array entries are already 0) is
to be removed, the code has no effect. Again, in some ADT designs,
we might wish to arrange to inform the client of such conditions.
If we are processing huge graphs or huge numbers of small
graphs, or space is otherwise tight, there are several ways to save space.
For example, adjacency matrices that represent undirected graphs are
symmetric: a[v][w] is always equal to a[w][v]. In C, it is easy to
save space by storing only one-half of this symmetric matrix (see Exer-
cise 17.20). At the extreme, we might consider using an array of bits
(in this way, for instance, we could represent graphs of up to about
64,000 vertices in about 64 million 64-bit words) (see Exercise 17.21).
These implementations have the slight complication that we need to
add an ADT operation to test for the existence of an edge (see Exer-
cise 17.19). (We do not use such an operation in our implementations
because the code is slightly easier to understand when we test for the
existence on an edge v-w by simply testing a[v][w].) Such space-
saving techniques are effective, but come at the cost of extra overhead
that may fall in the inner loop in time-critical applications.
Many applications involve associating other information with
each edge—in such cases, we can generalize the adjacency matrix to be
an array that holds any information whatever. We reserve at least one
value in the data type that we use for the array elements, to indicate
that the indicated edge is absent. In Chapters 20 and 21, we explore
the representation of such graphs.
Use of adjacency matrices depends on associating vertex names
with integers between 0 and V − 1. This assignment might be done
in one of many ways—for example, we consider a program that does
so in Section 17.6). Therefore, the specific matrix of 0-1 values that
GRAPH PROPERTIES AND TYPES §17.3 25

Program 17.5 Graph ADT output (adjacency-lists format)


Printing out the full adjacency matrix is unwieldy for sparse graphs, so
we might choose to simply print out, for each vertex, the vertices that
are connected to that vertex by an edge.

void GRAPHshow(Graph G)
{ int i, j;
printf("%d vertices, %d edges\n", G->V, G->E);
for (i = 0; i < G->V; i++)
{
printf("%2d:", i);
for (j = 0; j < G->V; j++)
if (G->adj[i][j] == 1) printf(" %2d", j);
printf("\n");
}
}

we represent with a two-dimensional array in C is but one possible 0: 1 2 5 6


1: 0
representation of any given graph as an adjacency matrix, because 2: 0
another program might give a different assignment of vertex names 3: 4 5
to the indices we use to specify rows and columns. Two arrays that 4: 3 5 6
appear to be markedly different could represent the same graph (see 5: 0 3 4
6: 0 4
Exercise 17.17). This observation is a restatement of the graph iso- 7: 8
morphism problem: Although we might like to determine whether or 8: 7
not two different arrays represent the same graph, no one has devised 9: 10 11 12
an algorithm that can always do so efficiently. This difficulty is funda- 10: 9
11: 9 12
mental. For example, our ability to find an efficient solution to various 12: 9 11
important graph-processing problems depends completely on the way
in which the vertices are numbered (see, for example, Exercise 17.25).
Figure 17.9
Developing an ADT function that prints out the adjacency-matrix
Adjacency lists format
representation of a graph is a simple exercise (see Exercise 17.16). Pro-
This table illustrates yet another
gram 17.5 illustrates a different implementation that may be preferred
way to represent the graph in Fig-
for sparse graphs: It just prints out the vertices adjacent to each vertex, ure 17.1: we associate each ver-
as illustrated in Figure 17.9. These programs (and, specifically, their tex with its set of adjacent vertices
output) clearly illustrate a basic performance tradeoff. To print out (those connected to it by a single
edge). Each edge affects two sets:
the array, we need space for all V 2 entries; to print out the lists, we
for every edge u-v in the graph, u
need room for just V + E numbers. For sparse graphs, when V 2 is appears in v’s set and v appears in
huge compared to V + E, we prefer the lists; for dense graphs, when E u’s set.
26 §17.3 CHAPTER SEVENTEEN

and V 2 are comparable, we prefer the array. As we shall soon see, we


make the same basic tradeoff when we compare the adjacency-matrix
representation with its primary alternative: an explicit representation
of the lists.
The adjacency-matrix representation is not satisfactory for huge
sparse graphs: The array requires V 2 bits of storage and V 2 steps
just to initialize. In a dense graph, when the number of edges (the
number of 1 bits in the matrix) is proportional to V 2 , this cost may
be acceptable, because time proportional to V 2 is required to process
the edges no matter what representation we use. In a sparse graph,
however, just initializing the array could be the dominant factor in
the running time of an algorithm. Moreover, we may not even have
enough space for the matrix. For example, we may be faced with
graphs with millions of vertices and tens of millions of edges, but we
may not want—or be able—to pay the price of reserving space for
trillions of 0 entries in the adjacency matrix.
On the other hand, when we do need to process a huge dense
graph, then the 0-entries that represent absent edges increase our space
needs by only a constant factor and provide us with the ability to
determine whether any particular edge is present with a single array
access. For example, disallowing parallel edges is automatic in an
adjacency matrix but is costly in some other representations. If we do
have space available to hold an adjacency matrix, and either V 2 is so
small as to represent a negligible amount of time or we will be running
a complex algorithm that requires more than V 2 steps to complete,
the adjacency-matrix representation may be the method of choice, no
matter how dense the graph.
Exercises
 17.16 Give an implementation of GRAPHshow for inclusion in the adjacency-
lists graph ADT implementation (Program 17.3) that prints out a two-
dimensional array of 0s and 1s like the one illustrated in Figure 17.7.
 17.17 Give the adjacency-matrix representations of the three graphs depicted
in Figure 17.2.
17.18 Given a graph, consider another graph that is identical to the first,
except that the names of (integers corresponding to) two vertices are inter-
changed. How do the adjacency matrices of these two graphs differ?
 17.19 Add a function GRAPHedge to the graph ADT that allows clients to
test whether there is an edge connecting two given vertices, and provide an
implementation for the adjacency-matrix representation.
GRAPH PROPERTIES AND TYPES §17.4 27

 17.20 Modify Program 17.3, augmented as described in Exercise 17.19, to


cut its space requirements about in half by not including array entries a[v][w]
for w greater than v.
17.21 Modify Program 17.3, augmented as described in Exercise 17.19, to
use an array of bits, rather than of integers. That is, if your computer has B
bits per word, your implementation should be able to represent a graph with V
vertices in about V 2 /B words (as opposed to V 2 ). Do empirical tests to assess
the effect of using a bit array on the time required for the ADT operations.
17.22 Modify Program 17.4 to check malloc return codes and return 0 if
there is insufficient memory available to represent the matrix.
17.23 Write a version of Program 17.4 that uses a single call to malloc.
0
17.24 Add implementations of GRAPHcopy and GRAPHdestroy to Program 17.3. 6 5 1 2
1
0
◦ 17.25 Suppose that all k vertices in a group have consecutive indices. How can 2
0
you determine from the adjacency matrix whether or not that group of vertices
3
constitutes a clique? Add a function to the adjacency-matrix implementation 5 4
of the graph ADT (Program 17.3) that finds, in time proportional to V 2 , the 4
6 5 3
largest group of vertices with consecutive indices that constitutes a clique. 5
3 0 4
6
0 4
7
8
17.4 Adjacency-Lists Representation 8
7
9
The standard representation that is preferred for graphs that are not 12 11 10
10
dense is called the adjacency-lists representation, where we keep track 9
11
of all the vertices connected to each vertex on a linked list that is 12 9
12
associated with that vertex. We maintain an array of lists so that, 9 11

given a vertex, we can immediately access its list; we use linked lists so
that we can add new edges in constant time. Figure 17.10
Adjacency-lists data structure
Program 17.6 is an implementation of the ADT interface in Pro-
This figure depicts a representa-
gram 17.1 that is based on this approach, and Figure 17.10 depicts an
tion of the graph in Figure 17.1 as
example. To add an edge connecting v and w to this representation an array of linked lists. The space
of the graph, we add w to v’s adjacency list and v to w’s adjacency used is proportional to the number
list. In this way, we still can add new edges in constant time, but the of nodes plus the number of edges.
To find the indices of the vertices
total amount of space that we use is proportional to the number of
connected to a given vertex v , we
vertices plus the number of edges (as opposed to the number of vertices look at the v th position in an ar-
squared, for the adjacency-matrix representation). We again represent ray, which contains a pointer to a
each edge in two different places: an edge connecting v and w is rep- linked list containing one node for
each vertex connected to v . The
resented as nodes on both adjacency lists. It is important to include
order in which the nodes appear
both; otherwise, we could not answer efficiently simple questions such on the lists depends on the method
as, “Which vertices are connected directly to vertex v?” that we use to construct the lists.
28 §17.4 CHAPTER SEVENTEEN

Program 17.6 Graph ADT implementation (adjacency lists)


This implementation of the interface in Program 17.1 uses an array of
lists, one corresponding to each vertex. An edge v-w is represented by
a node for w on list v and a node for v on list w. As in Program 17.3,
GRAPHedges puts just one of the two representations of each edge into
the output array. Implementations of GRAPHcopy, GRAPHdestroy, and
GRAPHremoveE are omitted. The GRAPHinsertE code keeps insertion
time constant by not checking for duplicate edges.

#include <stdlib.h>
#include "GRAPH.h"
typedef struct node *link;
struct node { int v; link next; };
struct graph { int V; int E; link *adj; };
link NEW(int v, link next)
{ link x = malloc(sizeof *x);
x->v = v; x->next = next;
return x;
}
Graph GRAPHinit(int V)
{ int v;
Graph G = malloc(sizeof *G);
G->V = V; G->E = 0;
G->adj = malloc(V*sizeof(link));
for (v = 0; v < V; v++) G->adj[v] = NULL;
return G;
}
void GRAPHinsertE(Graph G, Edge e)
{ int v = e.v, w = e.w;
G->adj[v] = NEW(w, G->adj[v]);
G->adj[w] = NEW(v, G->adj[w]);
G->E++;
}
int GRAPHedges(Edge a[], Graph G)
{ int v, E = 0; link t;
for (v = 0; v < G->V; v++)
for (t = G->adj[v]; t != NULL; t = t->next)
if (v < t->v) a[E++] = EDGE(v, t->v);
return E;
}
GRAPH PROPERTIES AND TYPES §17.4 29

By contrast to Program 17.3, Program 17.6 builds multigraphs,


because it does not remove parallel edges. Checking for duplicate edges
in the adjacency-lists structure would necessitate searching through the
lists and could take time proportional to V . Similarly, Program 17.6
does not include an implementation of the remove edge operation.
Again, adding such an implementation is an easy exercise (see Exer-
cise 17.28), but each deletion might take time proportional to V , to
search through the two lists for the nodes to remove. These costs
make the basic adjacency-lists representation unsuitable for applica-
tions involving huge graphs where parallel edges cannot be tolerated,
or applications involving heavy use of the remove edge operation.
In Section 17.5, we discuss the use of elementary data-structure tech-
niques to augment adjacency lists such that they support constant-time
remove edge and parallel-edge detection operations.
If a graph’s vertex names are not integers, then (as with adjacency
matrices) two different programs might associate vertex names with the
integers from 0 to V − 1 in two different ways, leading to two different
adjacency-list structures (see, for example, Program 17.10). We cannot
expect to be able to tell whether two different structures represent
the same graph because of the difficulty of the graph isomorphism
problem.
Moreover, with adjacency lists, there are numerous representa-
tions of a given graph even for a given vertex numbering. No matter in
what order the edges appear on the adjacency lists, the adjacency-list
structure represents the same graph (see Exercise 17.31). This char-
acteristic of adjacency lists is important to know because the order in
which edges appear on the adjacency lists affects, in turn, the order in
which edges are processed by algorithms. That is, the adjacency-list
structure determines how our various algorithms see the graph. Al-
though an algorithm should produce a correct answer no matter how
the edges are ordered on the adjacency lists, it might get to that answer
by different sequences of computations for different orderings. If an
algorithm does not need to examine all the graph’s edges, this effect
might affect the time that it takes. And, if there is more than one cor-
rect answer, different input orderings might lead to different output
results.
The primary advantage of the adjacency-lists representation over
the adjacency-matrix representation is that it always uses space pro-
30 §17.4 CHAPTER SEVENTEEN

portional to E + V , as opposed to V 2 in the adjacency matrix. The


primary disadvantage is that testing for the existence of specific edges
can take time proportional to V , as opposed to constant time in the
adjacency matrix. These differences trace, essentially, to the difference
between using linked lists and arrays to represent the set of vertices
incident on each vertex.
Thus, we see again that an understanding of the basic properties
of linked data structures and arrays is critical if we are to develop effi-
cient graph ADT implementations. Our interest in these performance
differences is that we want to avoid implementations that are inappro-
priately inefficient under unexpected circumstances when a wide range
of operations is to be included in the ADT. In Section 17.5, we discuss
the application of basic symbol-table algorithmic technology to real-
ize many of the theoretical benefits of both structures. Nonetheless,
because Program 17.6 is a simple implementation with the essential
characteristics that we need to learn efficient algorithms for processing
sparse graphs, we use it as the basis for many implementations in this
book.
Exercises
 17.26 Show, in the style of Figure 17.10, the adjacency-lists structure pro-
duced when you insert the edges in the graph
3-7 1-4 7-8 0-5 5-2 3-8 2-9 0-6 4-9 2-6 6-4
(in that order) into an initially empty graph, using Program 17.6.
17.27 Give implementations of GRAPHshow that have the same functionality as
Exercise 17.16 and Program 17.5, for inclusion in the adjacency-lists graph
ADT implementation (Program 17.6).
17.28 Provide an implementation of the remove edge function GRAPHremoveE
for the adjacency-lists graph ADT implementation (Program 17.6). Note:
Remember the possibility of duplicates.
17.29 Add implementations of GRAPHcopy and GRAPHdestroy to the adjacency-
lists graph ADT implementation (Program 17.6).
◦ 17.30 Give a simple example of an adjacency-lists graph representation that
could not have been built by repeated addition of edges by Program 17.6.
17.31 How many different adjacency-lists representations represent the same
graph as the one depicted in Figure 17.10?
17.32 Write a version of Program 17.6 that keeps the adjacency lists in sorted
order of vertex index. Describe a situation where this approach would be
useful.
GRAPH PROPERTIES AND TYPES §17.5 31

◦ 17.33 Add a function declaration to the graph ADT (Program 17.1) that
removes self-loops and parallel edges. Provide the trivial implementation
of this function for the adjacency-matrix–based ADT implementation (Pro-
gram 17.3), and provide an implementation of the function for the adjacency- 0 1 2 3 4 5 6 7 8 9 10 11 12
list–based ADT implementation (Program 17.6) that uses time proportional 0 1 1 1 0 0 1 1 0 0 0 0 0 0
to E and extra space proportional to V . 1 0 1 0 0 0 0 0 0 0 0 0 0 0
2 0 0 1 0 0 0 0 0 0 0 0 0 0
17.34 Extend your solution to Exercise 17.33 to also remove degree-0 (iso- 3 0 0 0 1 0 0 0 0 0 0 0 0 0
lated) vertices. Note: To remove vertices, you need to rename the other vertices 4 0 0 0 1 1 0 0 0 0 0 0 0 0
and rebuild the data structures—you should do so just once. 5 0 0 0 1 1 1 0 0 0 0 0 0 0
6 0 0 0 0 1 0 1 0 0 0 0 0 0
• 17.35 Write an ADT function for the adjacency-lists representation (Pro- 7 0 0 0 0 0 0 0 1 1 0 0 0 0
gram 17.6) that collapses paths that consist solely of degree-2 vertices. Specif- 8 0 0 0 0 0 0 0 0 1 0 0 0 0
ically, every degree-2 vertex in a graph with no parallel edges appears on some 9 0 0 0 0 0 0 0 0 0 1 1 1 1
10 0 0 0 0 0 0 0 0 0 0 1 0 0
path u-...-w where u and w are either equal or not of degree 2. Replace
11 0 0 0 0 0 0 0 0 0 0 0 1 1
any such path with u-w and then remove all unused degree-2 vertices as in 12 0 0 0 0 0 0 0 0 0 0 0 0 1
Exercise 17.34. Note: This operation may introduce self-loops and parallel
edges, but it preserves the degrees of vertices that are not removed.
 17.36 Give a (multi)graph that could result from applying the transformation 0
described in Exercise 17.35 on the sample graph in Figure 17.1. 5 2 1 6
1
2

17.5 Variations, Extensions, and Costs 3


4
3
In this section, we describe a number of options for improving the 5
4 3
graph representations discussed in Sections 17.3 and 17.5. The mod- 6
4
ifications fall into one of two categories. First, the basic adjacency- 7
8
matrix and adjacency-lists mechanisms extend readily to allow us to 8
represent other types of graphs. In the relevant chapters, we consider 9
11 10 12
these extensions in detail, and give examples; here, we look at them 10
briefly. Second, we often need to modify or augment the basic data 11
12
structures to make certain operations more efficient. We do so as a 12
matter of course in the chapters that follow; in this section, we discuss
the application of data-structure design techniques to enable efficient
Figure 17.11
implementation of several basic functions. Digraph representations
For digraphs, we represent each edge just once, as illustrated in
The adjacency-array and adjacency-
Figure 17.11. An edge v-w in a digraph is represented by a 1 in the lists representations of a digraph
entry in row v and column w in the adjacency array or by the appear- have only one representation of
ance of w on v’s adjacency list in the adjacency-lists representation. each edge, as illustrated in the
These representations are simpler than the corresponding representa- adjacency array (top) and adja-
cency lists (bottom) representation
tions that we have been considering for undirected graphs, but the of the set of edges in Figure 17.1
asymmetry makes digraphs more complicated combinatorial objects interpreted as a digraph (see Fig-
than undirected graphs, as we see in Chapter 19. For example, the ure 17.6, top).
32 §17.5 CHAPTER SEVENTEEN

standard adjacency-lists representation gives no direct way to find all


edges coming into a vertex in a digraph, so we must make appropriate
modifications if that operation needs to be supported.
For weighted graphs and networks, we fill the adjacency matrix
with weights instead of Boolean values (using some nonexistent weight
to represent the absence of an edge); in the adjacency-lists representa-
tion, we include a vertex weight in the adjacency structure or an edge
weight in adjacency-list elements.
It is often necessary to associate still more information with the
vertices or edges of a graph, to allow that graph to model more com-
plicated objects. We can associate extra information with each edge
by extending the Edge type in Program 17.1 as appropriate, then using
instances of that type in the adjacency matrix, or in the list nodes in
the adjacency lists. Or, since vertex names are integers between 0 and
V − 1, we can use vertex-indexed arrays to associate extra information
for vertices, perhaps using an appropriate ADT. Alternatively, we can
simply use a separate symbol table ADT to associate extra information
with each vertex and edge (see Exercise 17.46 and Program 17.10).
To handle various specialized graph-processing problems, we of-
ten need to add specialized auxiliary data structures to the graph ADT.
The most common such data structure is a vertex-indexed array, as we
saw already in Chapter 1, where we used vertex-indexed arrays to an-
swer connectivity queries. We use vertex-indexed arrays in numerous
implementations throughout the book.
As an example, suppose that we wish to know whether a vertex v
in a graph is isolated. Is v of degree 0? For the adjacency-lists represen-
tation, we can find this information immediately, simply by checking
whether adj[v] is null. But for the adjacency-matrix representation,
we need to check all V entries in the row or column corresponding to
v to know that each one is not connected to any other vertex; and for
the array-of-edges representation, we have no better approach than to
check all E edges to see whether there are any that involve v. Instead
of these potentially time-consuming computations, we could imple-
ment a simple online algorithm that maintains a vertex-indexed array
such that we can find the degree of any vertex in constant time (see
Exercise 17.40). Such a substantial performance differential for such
a simple problem is typical in graph processing.
GRAPH PROPERTIES AND TYPES §17.5 33

Table 17.1 Worst-case cost of graph-processing operations

The performance characteristics of basic graph-processing ADT oper-


ations for different graph representations vary widely, even for simple
tasks, as indicated in this table of the worst-case costs (all within a
constant factor for large V and E). These costs are for the simple imple-
mentations we have described in previous sections; various modifications
that affect the costs are described in the text of this section.

array of edges adjacency matrix adjacency lists

space E V2 V +E
initialize empty 1 V2 V
copy E V2 E
destroy 1 V E
insert edge 1 1 1
find/remove edge E 1 V
is v isolated? E V 1
path from u to v? E lg∗ V V2 V +E

Table 17.1 shows the dependence of the cost of various simple


graph-processing operations on the representation that we use. This
table is worth examining before we consider the implementation of
more complicated operations; it will help you to develop an intuition
for the difficulty of various primitive operations. Most of the costs
listed follow immediately from inspecting the code, with the exception
of the bottom row, which we consider in detail at the end of this
section.
In several cases, we can modify the representation to make simple
operations more efficient, although we have to take care that doing so
does not increase costs for other simple operations. For example, the
entry for adjacency-matrix destroy is an artifact of the C array-of-
arrays allocation scheme for two-dimensional arrays (see Section 3.7).
It is not difficult to reduce this cost to be constant (see Exercise 17.23).
On the other hand, if graph edges are sufficiently complex structures
that the matrix entries are pointers, then to destroy an adjacency matrix
would take time proportional to V 2 .
34 §17.5 CHAPTER SEVENTEEN

Because of their frequent use in typical applications, we consider


the find edge and remove edge operations in detail. In particular, we
need a find edge operation to be able to remove or disallow parallel
edges. As we saw in Section 17.3, these operations are trivial if we
are using an adjacency-matrix representation—we need only to check
or set an array entry that we can index directly. But how can we
implement these operations efficiently in the adjacency-lists represen-
tation? One approach is described next, and another is described in
Exercise 17.48. Both approaches are based on symbol-table implemen-
tations. If we use, for example, dynamic hash tables (see Section 14.5),
both approaches take space proportional to E and allow us to perform
either operation in constant time (on the average, amortized).
Specifically, to implement find edge when we are using adjacency
lists, we could use an auxiliary symbol table for the edges. We can
assign an edge v-w the integer key v*V+w and use any of the symbol-
table implementations from Part 4. (For undirected graphs, we might
assign the same key to v-w and w-v.) We can insert each edge into
the symbol table, after first checking whether it has already been in-
serted. If we wish, we can disallow parallel edges (see Exercise 17.47),
maintain duplicate records in the symbol table for parallel edges, or
build a graph ADT implementation based entirely on these operations
(see Exercise 17.46). In the present context, our main interest in this
technique is that it provides a constant-time find edge implementation
for adjacency lists.
To remove edges, we can put a pointer in the symbol-table record
for each edge that refers to its representation in the adjacency-lists
structure. Even this information, however, is not sufficient to allow us
to remove the edge in constant time unless the lists are doubly linked
(see Section 3.4). Furthermore, in undirected graphs, it is not sufficient
to remove the node from the adjacency list, because each edge appears
on two different adjacency lists. One solution to this difficulty is to put
both pointers in the symbol table; another is to link together the two
list nodes that correspond to a particular edge (see Exercise 17.44).
With either of these solutions, we can remove an edge in constant
time.
Removing vertices is more expensive. In the adjacency-matrix
representation, we essentially need to remove a row and a column from
the array, which is not much less expensive than starting over again
GRAPH PROPERTIES AND TYPES §17.5 35

with a smaller array. If we are using an adjacency-lists representation,


we see immediately that it is not sufficient to remove nodes from the
vertex’s adjacency list, because each node on the adjacency list specifies
another vertex whose adjacency list we must search to remove the other
node that represents the same edge. We need the extra links to support
constant-time edge removal as described in the previous paragraph if
we are to remove a vertex in time proportional to V .
We omit implementations of these operations here because they
are straightforward programming exercises using basic techniques
from Part 1, because maintaining complex structures with multiple
pointers per node is not justified in typical applications that involve
static graphs, and because we wish to avoid getting bogged down in the
details of maintaining the other pointers when implementing graph-
processing algorithms that do not otherwise use them. In Chapter 22,
we do consider implementations of a similar structure that plays an
essential role in the powerful general algorithms that we consider in
that chapter.
For clarity in describing and developing implementations of al-
gorithms of interest, we use the simplest appropriate representation.
Generally, we strive to make a link or an auxiliary array in a piece
of code directly relevant to the task at hand. Many programmers
practice this kind of minimalism as a matter of course, knowing that
maintaining the integrity of a data structure with multiple disparate
components can be a challenging task, indeed.
We might also modify the basic data structures in a performance-
tuning process to save space or time, particularly when processing
huge graphs (or huge numbers of small graphs). For example, we
can dramatically improve the performance of algorithms that process
huge static graphs represented with adjacency lists by stripping down
the representation to use arrays of varying length instead of linked
lists to represent the set of vertices incident on each vertex. With this
technique, we can ultimately represent a graph with just 2E integers
less than V and V integers less than V 2 (see Exercises 17.51 and 17.53).
Such representations are attractive for processing huge static graphs.
Many of the algorithms that we consider adapt readily to all the
variations that we have discussed in this section, because they are based
on a few high-level abstract operations such as “perform the following
operation for each edge adjacent to vertex v.” Indeed, several of the
36 §17.5 CHAPTER SEVENTEEN

programs that we consider differ only in the way that such abstract
operations are implemented.
Why not develop these algorithms at a higher level of abstraction,
then discuss different options for representing the data and implement-
ing the associated operations, as we have done in so many instances
throughout this book? The answer to this question is not clearcut. Of-
ten, we are presented with a clear choice between adjacency lists, for
sparse graphs, and adjacency matrices, for dense graphs. We choose
to work directly with one of these two particular representations in
our primary implementations because the performance characteristics
of the algorithms are clearly exposed in code that works with the low-
level representation, and that code is generally no more difficult to read
and understand than code written in terms of higher-level abstractions.
In some instances, our algorithm-design decisions depend on cer-
tain properties of the representation. Working at a higher level of
abstraction might obscure our knowledge of that dependence. If we
know that one representation would lead to poor performance but
another would not, we would be taking an unnecessary risk were we
to consider the algorithm at the wrong level of abstraction. As usual,
our goal is to craft implementations such that we can make precise
statements about performance.
It is possible to address these issues with a rigorous abstract
approach, where we build layers of abstraction that culminate in the
abstract operations that we need for our algorithms. Adding an ADT
operation to test for the existence of an edge is one example (see
Exercise 17.19), and we could also arrange to have representation-
independent code for processing each of the vertices adjacent to a given
vertex (see Exercise 17.60). Such an approach is worthwhile in many
situations. In this book, however, we keep one eye on performance by
using code that directly accesses adjacency matrices when working with
dense graphs and adjacency lists when working with sparse graphs,
augmenting the basic structures as appropriate to suit the task at hand.
All of the operations that we have considered so far are simple,
albeit necessary, data-processing functions; and the essential net re-
sult of the discussion in this section is that basic algorithms and data
structures from Parts 1 through 3 are effective for handling them. As
we develop more sophisticated graph-processing algorithms, we face
more difficult challenges in finding the best implementations for spe-
GRAPH PROPERTIES AND TYPES §17.5 37

cific practical problems. To illustrate this point, we consider the last


row in Table 17.1, which gives the costs of determining whether there
is a path connecting two given vertices.
In the worst case, the simple algorithm in Section 17.7 examines
all E edges in the graph (as do several other methods that we consider
in Chapter 18). The entries in the center and right column on the bot-
tom row in Table 17.1 indicate, respectively, that the algorithm may
examine all V 2 entries in an adjacency-matrix representation, and all V
list heads and all E nodes on the lists in an adjacency-lists representa-
tion. These facts imply that the algorithm’s running time is linear in the
size of the graph representation, but they also exhibit two anomalies:
The worst-case running time is not linear in the number of edges in the
graph if we are using an adjacency-matrix representation for a sparse
graph or either representation for an extremely sparse graph (one with
a huge number of isolated vertices). To avoid repeatedly considering
these anomalies, we assume throughout that the size of the represen-
tation that we use is proportional to the number of edges in the graph.
This point is moot in the majority of applications because they involve
huge sparse graphs and thus require an adjacency-lists representation.
The left column on the bottom row in Table 17.1 derives from
the use of the union-find algorithms in Chapter 1 (see Section 1.3).
This method is attractive because it only requires space proportional
to V , but has the drawback that it cannot exhibit the path. This
entry highlights the importance of completely and precisely specifying
graph-processing problems.
Even after taking all of these factors into consideration, one of
the most significant challenges that we face when developing practical
graph-processing algorithms is assessing the extent to which the re-
sults of worst-case performance analyses, such as those in Table 17.1,
overestimate performance on graphs that we encounter in practice.
Most of the literature on graph algorithms describes performance in
terms of such worst-case guarantees, and, while this information is
helpful in identifying algorithms that can have unacceptably poor per-
formance, it may not shed much light on which of several simple,
direct programs may be most suitable for a given application. This sit-
uation is exacerbated by the difficulty of developing useful models of
average-case performance for graph algorithms, leaving us with (per-
haps unreliable) benchmark testing and (perhaps overly conservative)
38 §17.5 CHAPTER SEVENTEEN

worst-case performance guarantees to work with. For example, the


graph-search methods that we discuss in Chapter 18 are all effective
linear-time algorithms for finding a path between two given vertices,
but their performance characteristics differ markedly, depending both
upon the graph being processed and its representation. When using
graph-processing algorithms in practice, we constantly fight this dis-
parity between the worst-case performance guarantees that we can
prove and the actual performance characteristics that we can expect.
This theme will recur throughout the book.
Exercises
17.37 Develop an adjacency-matrix representation for dense multigraphs, and
provide an ADT implementation for Program 17.1 that uses it.

◦ 17.38 Why not use a direct representation for graphs (a data structure that
models the graph exactly, with vertices represented as allocated records and
edge lists containing links to vertices instead of vertex names)?
17.39 Write a representation-independent ADT function that returns a pointer
to a vertex-indexed array giving the degree of each vertex in the graph. Hint:
Use GRAPHedges.
17.40 Modify the adjacency-matrix ADT implementation (Program 17.3) to
include in the graph representation a vertex-indexed array that holds the degree
of each vertex. Add an ADT function GRAPHdeg that returns the degree of a
given vertex.
17.41 Do Exercise 17.40 for the adjacency-lists representation.
 17.42 Give a row to add to Table 17.1 for the problem of determining the
number of isolated vertices in a graph. Support your answer with function
implementations for each of the three representations.

◦ 17.43 Give a row to add to Table 17.1 for the problem of determining whether
a given digraph has a vertex with indegree V and outdegree 0. Support your
answer with function implementations for each of the three representations.
Note: Your entry for the adjacency-matrix representation should be V .
17.44 Use doubly-linked adjacency lists with cross links as described in the
text to implement a constant-time remove edge function GRAPHremoveE for the
adjacency-lists graph ADT implementation (Program 17.6).
17.45 Add a remove vertex function GRAPHremoveV to the doubly-linked
adjacency-lists graph ADT implementation described in the previous exercise.

◦ 17.46 Modify your solution to Exercise 17.15 to use a dynamic hash table,
as described in the text, such that insert edge and remove edge take constant
amortized time.
GRAPH PROPERTIES AND TYPES §17.5 39

17.47 Modify the adjacency-lists graph ADT implementation (Program 17.6)


to use a symbol table to ignore duplicate edges, such that it is functionally
equivalent to the adjacency-matrix graph ADT (Program 17.3). Use dynamic
hashing for your symbol-table implementation so that your implementation
uses space proportional to E and can insert and remove edges in constant time
(on the average, amortized).
17.48 Develop a graph ADT implementation based on an array-of-symbol-
tables representation (with one symbol table for each vertex, which contains
its list of adjacent edges). Use dynamic hashing for your symbol-table imple-
mentation so that your implementation uses space proportional to E and can
insert and remove edges in constant time (on the average, amortized).
17.49 Develop a graph ADT intended for static graphs, based upon a function
GRAPHconstruct that takes an array of edges as an argument and builds a
graph. Develop an implementation of GRAPHconstruct that uses GRAPHinit
and GRAPHinsert from Program 17.1. (Such an implementation might be
useful for performance comparisons with the implementations described in
Exercises 17.51 through 17.54.)
17.50 Develop an implementation of GRAPHinit and GRAPHconstruct from
Program 17.1 that uses the ADT described in the previous exercise. (Such
an implementation might be useful for backwards compatibility with driver
programs such as Program 17.2.)
17.51 Develop an implementation for the GRAPHconstruct function described
in Exercise 17.49 that use a compact representation based on the following
data structures:
struct node { int cnt; int* edges; };
struct graph { int V; int E; node *adj; };
A graph is a vertex count, an edge count, and an array of vertices. A vertex
contains an edge count and an array with one vertex index corresponding to
each adjacent edge. Implement GRAPHshow for this representation.
• 17.52 Add to your solution to Exercise 17.51 a function that eliminates self-
loops and parallel edges, as in Exercise 17.33.
◦ 17.53 Develop an implementation for the static-graph ADT described in Ex-
ercise 17.49 that uses just two arrays to represent the graph: one array of E
vertices, and another of V indices or pointers into the first array. Implement
GRAPHshow for this representation.
• 17.54 Add to your solution to Exercise 17.53 a function that eliminates self-
loops and parallel edges, as in Exercise 17.33.
17.55 Develop a graph ADT interface that associates (x, y) coordinates with
each vertex, so that you can work with graph drawings. Modify GRAPHinit
as appropriate and add functions GRAPHdrawV and GRAPHdrawE to initialize,
to draw a vertex, and to draw an edge, respectively.
17.56 Write a client program that uses your interface to produce drawings of
edges that are being added to a small graph.
40 §17.6 CHAPTER SEVENTEEN

17.57 Develop an implementation of your interface from Exercise 17.55 that


produces a PostScript program with drawings as output (see Section 4.3).
17.58 Find an appropriate graphics interface that allows you to develop an
implementation of your interface from Exercise 17.55 that directly draws
graphs in a window on your display.
• 17.59 Extend your solution to Exercises 17.55 and 17.58 to include func-
tions to erase vertices and edges and to draw them in different styles, so that
you can write client programs that provide dynamic graphical animations of
graph algorithms in operation.
◦ 17.60 Define a graph ADT function that will allow the clients to visit (run
a client-supplied function that takes an edge as argument for) all edges adja-
cent to a given vertex. Provide implementations of the function for both the
adjacency-matrix and adjacency-lists representations. To test your function,
use it to implement a representation-independent version of GRAPHshow.

17.6 Graph Generators


To develop further appreciation for the diverse nature of graphs as
combinatorial structures, we now consider detailed examples of the
types of graphs that we use later to test the algorithms that we study.
Some of these examples are drawn from applications. Others are
drawn from mathematical models that are intended both to have prop-
erties that we might find in real graphs and to expand the range of input
trials available for testing our algorithms.
To make the examples concrete, we present them as extensions to
the interface of Program 17.1, so that we can put them to immediate
use when we test implementations of the graph algorithms that we
consider. In addition, we consider a program that reads a sequence of
pairs of arbitrary names from standard input and builds a graph with
vertices corresponding to the names and edges corresponding to the
pairs.
The implementations that we consider in this section are based
upon the interface of Program 17.1, so they function properly, in
theory, for any graph representation. In practice, however, some com-
binations of interface and representation can have unacceptably poor
performance, as we shall see.
As usual, we are interested in having “random problem in-
stances,” both to exercise our programs with arbitrary inputs and
to get an idea of how the programs might perform in real applications.
GRAPH PROPERTIES AND TYPES §17.6 41

Program 17.7 Random graph generator (random edges)


This ADT function builds a graph by generating E random pairs of
integers between 0 and V-1, interpreting the integers as vertex labels
and the pairs of vertex labels as edges. It leaves the decision about
the treatment of parallel edges and self-loops to the implementation of
GRAPHinsertE and assumes that the ADT implementation maintains
counts of the number of graph edges and vertices in G->E and G->V,
repectively. This method is generally not suitable for generating huge
dense graphs because of the number of parallel edges that it generates.

int randV(Graph G)
{ return G->V * (rand() / (RAND_MAX + 1.0)); }
Graph GRAPHrand(int V, int E)
{ Graph G = GRAPHinit(V);
while (G->E < E)
GRAPHinsertE(G, EDGE(randV(G), randV(G)));
return G;
}

For graphs, the latter goal is more elusive than for other domains that
we have considered, although it is still a worthwhile objective. We
shall encounter various different models of randomness, starting with
these two:
Random edges This model is simple to implement, as indicated
by the generator given in Program 17.7. For a given number of vertices
V , we generate random edges by generating pairs of numbers between Figure 17.12
Two random graphs
0 and V − 1. The result is likely to be a random multigraph with self-
loops, rather than a graph as defined in Definition 17.1. A given pair Both of these random graphs have
50 vertices. The sparse graph at
could have two identical numbers (hence, self-loops could occur); and the top has 50 edges, while the
any pair could be repeated multiple times (hence, parallel edges could dense graph at the bottom has
occur). Program 17.7 generates edges until the graph is known to have 500 edges. The sparse graph is
E edges, and leaves to the implementation the decision of whether to not connected, with each vertex
connected only to a few others; the
eliminate parallel edges. If parallel edges are eliminated, the number of dense graph is certainly connected,
edges generated is substantially higher than then number of edges used with each vertex connected to 20
(E) for dense graphs (see Exercise 17.62); so this method is normally others, on the average. These di-
used for sparse graphs. agrams also indicate the difficulty
of developing algorithms that can
Random graph The classic mathematical model for random draw arbitrary graphs (the vertices
graphs is to consider all possible edges and to include each in the here are placed in random posi-
graph with a fixed probability p. If we want the expected number tion).
42 §17.6 CHAPTER SEVENTEEN

Program 17.8 Random graph generator (random graph)


Like Program 17.7, this graph client generates random pairs of integers
between 0 and V-1 to create a random graph, but it uses a different
probabilistic model where each possible edge occurs independently with
some probability p. The value of p is calculated such that the expected
number of edges (pV (V − 1)/2) is equal to E. The number of edges
in any particular graph generated by this code will be close to E but is
unlikely to be precisely equal to E. This method is primarily suitable
for dense graphs, because its running time is proportional to V 2 .

Graph GRAPHrand(int V, int E)


{ int i, j;
double p = 2.0*E/V/(V-1);
Graph G = GRAPHinit(V);
for (i = 0; i < V; i++)
for (j = 0; j < i; j++)
if (rand() < p*RAND_MAX)
GRAPHinsertE(G, EDGE(i, j));
return G;
}

of edges in the graph to be E, we can choose p = 2E/V (V − 1).


Program 17.8 is a function that uses this model to generate random
graphs. This model precludes duplicate edges, but the number of edges
in the graph is only equal to E on the average. This implementation
is well-suited for dense graphs, but not for sparse graphs, since it runs
in time proportional to V (V − 1)/2 to generate just E = pV (V − 1)/2
edges. That is, for sparse graphs, the running time of Program 17.8 is
quadratic in the size of the graph (see Exercise 17.67).
These models are well-studied and are not difficult to implement,
but they do not necessarily generate graphs with properties similar to
the ones that we see in practice. In particular, graphs that model
maps, circuits, schedules, transactions, networks, and other practi-
cal situations usually not only are sparse, but also exhibit a locality
property—edges are much more likely to connect a given vertex to
vertices in a particular set than to vertices that are not in the set. We
might consider many different ways of modeling locality, as illustrated
in the following examples.
GRAPH PROPERTIES AND TYPES §17.6 43

k-neighbor graph The graph depicted at the top in Figure 17.13


is drawn from a simple modification to a random-edges graph genera-
tor, where we randomly pick the first vertex v, then randomly pick the
second from among those whose indices are within a fixed constant k
of v (wrapping around from V − 1 to 0, when the vertices are arranged
in a circle as depicted). Such graphs are easy to generate and certainly
exhibit locality not found in random graphs.
Euclidean neighbor graph The graph depicted at the bottom in
Figure 17.13 is drawn from a generator that generates V points in the
plane with random coordinates between 0 and 1, and then generates
edges connecting any two points within distance d of one another. If
d is small, the graph is sparse; if d is large, the graph is dense (see
Exercise 17.73). This graph models the types of graphs that we might
expect when we process graphs from maps, circuits, or other appli-
cations where vertices are associated with geometric locations. They
are easy to visualize, exhibit properties of algorithms in an intuitive
manner, and exhibit many of the structural properties that we find in
such applications.
One possible defect in this model is that the graphs are not likely
to be connected when they are sparse; other difficulties are that the
graphs are unlikely to have high-degree vertices and that they do not
have any long edges. We can change the models to handle such sit-
Figure 17.13
uations, if desired, or we can consider numerous similar examples Random neighbor graphs
to try to model other situations (see, for example, Exercises 17.71 These figures illustrate two mod-
and 17.72). els of sparse graphs. The neighbor
Or, we can test our algorithms on real graphs. In many appli- graph at the top has 33 vertices
cations, there is no shortage of problem instances drawn from actual and 99 edges, with each edge re-
stricted to connect vertices whose
data that we can use to test our algorithms. For example, huge graphs indices differ by less than 10 (mod-
drawn from actual geographic data are easy to find; two more exam- ulo V ). The Euclidean neighbor
ples are listed in the next two paragraphs. The advantage of working graph at the bottom models the
with real data instead of a random graph model is that we can see types of graphs that we might find
in applications where vertices are
solutions to real problems as algorithms evolve. The disadvantage is tied to geometric locations. Ver-
that we may lose the benefit of being able to predict the performance tices are random points in the
of our algorithms through mathematical analysis. We return to this plane; edges connect any pair
topic when we are ready to compare several algorithms for the same of vertices within a specified dis-
tance d of each other. This graph
task, at the end of Chapter 18. is sparse (177 vertices and 1001
Transaction graph Figure 17.14 illustrates a tiny piece of a edges); by adjusting d, we can gen-
graph that we might find in a telephone company’s computers. It has a erate graphs of any desired density.
44 §17.6 CHAPTER SEVENTEEN

vertex defined for each phone number, and an edge for each pair i and
j with the property that i made a telephone call to j within some fixed
900-435-5100 201-332-4562 period. This set of edges represents a huge multigraph. It is certainly
415-345-3030 757-995-5030
757-310-4313 201-332-4562
sparse, since each person places calls to only a tiny fraction of the
747-511-4562 609-445-3260 available telephones. It is representative of many other applications.
900-332-3162 212-435-3562 For example, a financial institution’s credit card and merchant account
617-945-2152 408-310-4150 records might have similar information.
757-995-5030 757-310-4313
212-435-3562 803-568-8358 Function call graph We can associate a graph with any com-
913-410-3262 212-435-3562 puter program with functions as vertices and an edge connecting X
401-212-4152 907-618-9999 and Y whenever function X calls function Y . We can instrument the
201-232-2422 415-345-3120 program to create such a graph (or have a compiler do it). Two com-
913-495-1030 802-935-5112
609-445-3260 415-345-3120 pletely different graphs are of interest: the static version, where we
201-310-3100 415-345-3120 create edges at compile time corresponding to the function calls that
408-310-4150 802-935-5113 appear in the program text of each function; and a dynamic version,
708-332-4353 803-777-5834 where we create edges at run time when the calls actually happen. We
413-332-3562 905-828-8089
815-895-8155 208-971-0020 use static function call graphs to study program structure and dynamic
802-935-5115 408-310-4150 ones to study program behavior. These graphs are typically huge and
708-410-5032 212-435-3562 sparse.
201-332-4562 408-310-4150 In applications such as these, we face massive amounts of data,
815-511-3032 201-332-4562
301-292-3162 505-709-8080 so we might prefer to study the performance of algorithms on real
617-833-2425 208-907-9098 sample data rather than on random models. We might choose to try
800-934-5030 408-310-4150 to avoid degenerate situations by randomly ordering the edges, or by
408-982-3100 201-332-4562 introducing randomness in the decision making in our algorithms, but
415-345-3120 905-569-1313
413-435-4313 415-345-3120 that is a different matter from generating a random graph. Indeed, in
747-232-8323 408-310-4150 many applications, learning the properties of the graph structure is a
802-995-1115 908-922-2239 goal in itself.
Figure 17.14 In several of these examples, vertices are natural named objects,
Transaction graph and edges appear as pairs of named objects. For example, a transaction
A sequence of pairs of numbers graph might be built from a sequence of pairs of telephone numbers,
like this one might represent a and a Euclidean graph might be built from a sequence of pairs of
list of telephone calls in a local cities or towns. Program 17.9 is a client program that we can use to
exchange, or financial transfers build a graph in this common situation. For the client’s convenience,
between accounts, or any sim-
ilar situation involving transac- it takes the set of edges as defining the graph, and deduces the set of
tions between entities with unique vertex names from their use in edges. Specifically, the program reads a
identifiers. The graphs are hardly sequence of pairs of symbols from standard input, uses a symbol table
random—some phones are far
to associate the vertex numbers 0 to V − 1 to the symbols (where V is
more heavily used than others and
some accounts are far more active the number of different symbols in the input), and builds a graph by
than others. inserting the edges, as in Programs 17.7 and 17.8. We could adapt any
GRAPH PROPERTIES AND TYPES §17.6 45

Program 17.9 Building a graph from pairs of symbols


This function uses a symbol table to build a graph by reading pairs of
symbols from standard input. The symbol-table ADT function STindex
associates an integer with each symbol: on unsuccessful search in a table
of size N it adds the symbol to the table with associated integer N+1; on
successful search, it simply returns the integer previously associated with
the symbol. Any of the symbol-table methods in Part 4 can be adapted
for this use; for example, see Program 17.10. The code to check that
the number of edges does not exceed Emax is omitted.

#include <stdio.h>
#include "GRAPH.h"
#include "ST.h"
Graph GRAPHscan(int Vmax, int Emax)
{ char v[100], w[100];
Graph G = GRAPHinit(Vmax);
STinit();
while (scanf("%99s %99s", v, w) == 2)
GRAPHinsertE(G, EDGE(STindex(v), STindex(w)));
return G;
}

symbol-table implementation to support the needs of Program 17.9;


Program 17.10 is an example that uses ternary search trees (TSTs)
(see Chapter 14). These programs make it easy for us to test our
algorithms on real graphs that may not be characterized accurately by
any probabilistic model.
Program 17.9 is also significant because it validates the assump-
tion we have made in all of our algorithms that the vertex names are
integers between 0 and V − 1. If we have a graph that has some other
set of vertex names, then the first step in representing a graph is to use
a program such as Program 17.9 to map the vertex names to integers
between 0 and V − 1.
Some graphs are based on implicit connections among items. We
do not focus on such graphs, but we note their existence in the next
few examples and devote a few exercises to them. When faced with
processing such a graph, we can certainly write a program to construct
explicit graphs by enumerating all the edges; but there also may be
46 §17.6 CHAPTER SEVENTEEN

Program 17.10 Symbol indexing for vertex names


This implementation of the symbol-table indexing function for string
keys that is described in the commentary for Program 17.9 accomplishes
the task by adding an index field to each node in an existence-table TST
(see Program 15.8). The index associated with each key is kept in
the index field in the node corresponding to its end-of-string character.
When we reach the end of a search key, we set its index if necessary and
set a global variable which is returned to the caller after all recursive
calls to the function have returned.
#include <stdlib.h>
typedef struct STnode* link;
struct STnode { int index, d; link l, m, r; };
static link head;
static int val, N;
void STinit()
{ head = NULL; N = 0; }
link stNEW(int d)
{ link x = malloc(sizeof *x);
x->index = -1; x->d = d;
x->l = NULL; x->m = NULL; x->r = NULL;
return x;
}
link indexR(link h, char* v, int w)
{ int i = v[w];
if (h == NULL) h = stNEW(i);
if (i == 0)
{
if (h->index == -1) h->index = N++;
val = h->index;
return h;
}
if (i < h->d) h->l = indexR(h->l, v, w);
if (i == h->d) h->m = indexR(h->m, v, w+1);
if (i > h->d) h->r = indexR(h->r, v, w);
return h;
}
int STindex(char* key)
{ head = indexR(head, key, 0); return val; }
GRAPH PROPERTIES AND TYPES §17.6 47

solutions to specific problems that do not require that we enumerate


all the edges and therefore can run in sublinear time.
Degrees-of-separation graph Consider a collection of subsets
drawn from V items. We define a graph with one vertex corresponding
to each element in the union of the subsets and edges between two
vertices if both vertices appear in some subset (see Figure 17.15). If
desired, the graph might be a multigraph, with edge labels naming the
appropriate subsets. All items incident on a given item v are said to
be 1 degree of separation from v. Otherwise, all items incident on
any item that is i degrees of separation from v (that are not already
known to be i or fewer degrees of separation from v) are (i+1) degrees
of separation from v. This construction has amused people ranging
from mathematicians (Erdös number) to movie buffs (separation from
Kevin Bacon).
Interval graph Consider a collection of V intervals on the real Alice Bob Carol
line (pairs of real numbers). We define a graph with one vertex cor- Bob Dave
responding to each interval, with edges between vertices if the corre- Carol Dave Eliza
sponding intervals intersect (have any points in common).
Alice Dave
de Bruijn graph Suppose that V is a power of 2. We define
Eliza Frank
a digraph with one vertex corresponding to each nonnegative integer
less than V , with edges from each vertex i to 2i and (2i + 1) mod lg V .
These graphs are useful in the study of the sequence of values that can Carol
occur in a fixed-length shift register for a sequence of operations where
we repeatedly shift all the bits one position to the left, throw away the Dave Alice
leftmost bit, and fill the rightmost bit with 0 or 1. Figure 17.16 depicts
the de Bruijn graphs with 8, 16, 32, and 64 vertices. Eliza Bob
The various types of graphs that we have considered in this sec- Frank

tion have a wide variety of different characteristics. However, they


all look the same to our programs: They are simply collections of Figure 17.15
edges. As we saw in Chapter 1, learning even the simplest facts about Degrees-of-separation graph
them can be a computational challenge. In this book, we consider The graph at the bottom is defined
numerous ingenious algorithms that have been developed for solving by the groups at the top, with one
vertex for each person and an edge
practical problems related to many types of graphs. connecting a pair of people when-
Based just on the few examples presented in this section, we can ever they are in the same group.
see that graphs are complex combinatorial objects, far more complex Shortest path lengths in the graph
correspond to degrees of separa-
than those underlying other algorithms that we studied in Parts 1
tion. For example, Frank is three
through 4. In many instances, the graphs that we need to consider degrees of separation from Alice
in applications are difficult or impossible to characterize. Algorithms and Bob.
48 §17.6 CHAPTER SEVENTEEN

that perform well on random graphs are often of limited applicability


because it is often difficult to be persuaded that random graphs have
structural characteristics the same as those of the graphs that arise
in applications. The usual approach to overcome this objection is
to design algorithms that perform well in the worst case. While this
approach is successful in some instances, it falls short (by being too
conservative) in others.
While we are often not justified in assuming that performance
studies on graphs generated from one of the random graph models
that we have discussed will give information sufficiently accurate to
allow us to predict performance on real graphs, the graph generators
that we have considered in this section are useful in helping us to
test implementations and to understand our algorithms’ performance.
Before we even attempt to predict performance for applications, we
must at least verify any assumptions that we might have made about
the relationship between the application’s data and whatever models
or sample data we may have used. While such verfication is wise when
we are working in any applications domain, it is particularly important
when we are processing graphs, because of the broad variety of types
of graphs that we encounter.
Exercises
 17.61 When we use Program 17.7 to generate random graphs of density αV ,
2
what fraction of edges produced are self-loops?
1 3 • 17.62 Calculate the expected number of parallel edges produced when we use
Program 17.7 to generate random graphs with V vertices of density α. Use
the result of your calculation to draw plots showing the fraction of parallel
0 4 edges produced as a function of α, for V = 10, 100, and 1000.
• 17.63 Find a large undirected graph somewhere online—perhaps based on
7 5 network-connectivity information, or a separation graph defined by coauthors
6 in a set of bibliographic lists or by actors in movies.

Figure 17.16  17.64 Write a client program that generates sparse random graphs for a well-
de Bruijn graphs chosen set of values of V and E, and prints the amount of space that it used
for the graph representation and the amount of time that it took to build it.
A de Bruijn digraph of order n has Test your program with a sparse-graph ADT implementation (Program 17.6)
2n vertices with edges from i to and with the random-graph generator (Program 17.7), so that you can do
2i mod n and (2i + 1) mod n, for meaningful empirical tests on graphs drawn from this model.
all i. Pictured here are the under-
lying undirected de Bruijn graphs  17.65 Write a client program that generates dense random graphs for a well-
of order 6, 5, 4, and 3 (top to bot- chosen set of values of V and E, and prints the amount of space that it used
tom). for the graph representation and the amount of time that it took to build it.
GRAPH PROPERTIES AND TYPES §17.6 49

Test your program with a dense-graph ADT implementation (Program 17.3)


and with the random-graph generator (Program 17.8), so that you can do
meaningful empirical tests on graphs drawn from this model.
• 17.66 Give the standard deviation of the number of edges produced by Pro-
gram 17.8.
• 17.67 Write a program that produces each possible graph with precisely the
same probability as does Program 17.8, but uses time and space proportional
to only V + E, not V 2 . Test your program as described in Exercise 17.64.
◦ 17.68 Write a program that produces each possible graph with precisely the
same probability as does Program 17.7, but uses time proportional to E,
even when the density is close to 1. Test your program as described in Exer-
cise 17.65.
• 17.69 Write a program that produces, with equal likelihood, each of the
possible graphs with V vertices and E edges (see Exercise 17.9). Test your
program as described in Exercise 17.64 (for low densities) and as described
in Exercise 17.65 (for high densities).
◦ 17.70 Write a √
program√that generates random graphs by connecting vertices
arranged in a V -by- V grid to their neighbors (see Figure 1.2), with k
extra edges connecting each vertex to a randomly chosen destination vertex
(each destination vertex equally likely). Determine how to set k such that
the expected number of edges is E. Test your program as described in Exer-
cise 17.64.
◦ 17.71 Write a program that
√ generates
√ random digraphs by randomly connect-
ing vertices arranged in a V -by- V grid to their neighbors, with each of the
possible edges occurring with probability p (see Figure 1.2). Determine how
to set p such that the expected number of edges is E. Test your program as
described in Exercise 17.64.
◦ 17.72 Augment your program from Exercise 17.71 to add R extra random
edges, computed as in Program 17.7. For large R, shrink the grid so that the
total number of edges remains about V .
• 17.73 Write a program that generates V random points in the plane, then
builds a graph consisting of edges connecting all pairs of points within a given
distance d of one another (see Figure 17.13 and Program 3.20). Determine
how to set d such that the expected number of edges is E. Test your pro-
gram as described in Exercise 17.64 (for low densities) and as described in
Exercise 17.65 (for high densities).
• 17.74 Write a program that generates V random intervals in the unit inter-
val, all of length d, then builds the corresponding interval graph. Determine
how to set d such that the expected number of edges is E. Test your pro-
gram as described in Exercise 17.64 (for low densities) and as described in
Exercise 17.65 (for high densities). Hint: Use a BST.
• 17.75 Write a program that chooses V vertices and E edges from the real
graph that you found for Exercise 17.63. Test your program as described
50 §17.7 CHAPTER SEVENTEEN

in Exercise 17.64 (for low densities) and as described in Exercise 17.65 (for
high densities).

◦ 17.76 One way to define a transportation system is with a set of sequences of


vertices, each sequence defining a path connecting the vertices. For example,
the sequence 0-9-3-2 defines the edges 0-9, 9-3, and 3-2. Write a program
that builds a graph from an input file consisting of one sequence per line, using
symbolic names. Develop input suitable to allow you to use your program to
build a graph corresponding to the Paris metro system.
17.77 Extend your solution to Exercise 17.76 to include vertex coordinates,
along the lines of Exercise 17.59, so that you can work with graphical repre-
sentations.

◦ 17.78 Apply the transformations described in Exercises 17.33 through 17.35


to various graphs (see Exercises 17.63–76), and tabulate the number of ver-
tices and edges removed by each transformation.

◦ 17.79 Design an appropriate extension that allows you to use Program 17.1
to build separation graphs without having to call a function for each implied
edge. That is, the number of function calls required to build a graph should
be proportional to the sum of the sizes of the groups. (Implementations of
graph-processing functions are left with the problem of efficiently handling
implied edges.)
17.80 Give a tight upper bound on the number of edges in any separation
graph with N different groups of k people.
 17.81 Draw graphs in the style of Figure 17.16 that, for V = 8, 16, and 32,
have V vertices numbered from 0 to V − 1 and an edge connecting each vertex
i with i/2.
17.82 Modify the ADT interface in Program 17.1 to allow clients to use
symbolic vertex names and edges to be pairs of instances of a generic Vertex
type. Hide the vertex-index representation and the symbol-table ADT usage
completely from clients.
17.83 Add a function to the ADT interface from Exercise 17.82 that supports
a join operation for graphs, and provide implementations for the adjacency-
matrix and adjacency-lists representations. Note: Any vertex or edge in either
graph should be in the join, but vertices that are in both graphs appear only
once in the join, and you should remove parallel edges.

17.7 Simple, Euler, and Hamilton Paths


Our first nontrivial graph-processing algorithms solve fundamental
problems concerning paths in graphs. They introduce the general re-
cursive paradigm that we use throughout the book, and they illustrate
GRAPH PROPERTIES AND TYPES §17.7 51

that apparently similar graph-processing problems can range widely in


difficulty.
These problems take us from local properties such as the exis-
tence of edges or the degrees of vertices to global properties that tell 0
us about a graph’s structure. The most basic such property is whether 6
1 2
two vertices are connected.
Simple path Given two vertices, is there a simple path in the 3
graph that connects them? In some applications, we might be satisfied 4
to know of the existence of the path; in other applications, we might 5
need an algorithm that can supply a specific path.
Program 17.11 is a direct solution that finds a path. It is based
on depth-first search, a fundamental graph-processing paradigm that 2-0 pathR(G, 0, 6)
we considered briefly in Chapters 3 and 5 and shall study in detail 0-1 pathR(G, 1, 6)
1-0
in Chapter 18. The algorithm is based on a recursive function that
1-2
determines whether there is a simple path from v to w by checking, for 0-2
each edge v-t incident on v, whether there is a simple path from t to 0-5 pathR(G, 5, 6)
w that does not go through v. It uses a vertex-indexed array to mark v 5-0
5-4 pathR(G, 4, 6)
so that no path through v will be checked in the recursive call.
4-2
The code in Program 17.11 simply tests for the existence of a 4-3 pathR(G, 3, 6)
path. How can we augment it to print the path’s edges? Thinking 3-2
recursively suggests an easy solution: 3-4
4-6 pathR(G, 6, 6)
• Add a statement to print t-v just after the recursive call in pathR
finds a path from t to w. Figure 17.17
• Switch w and v in the call on pathR in GRAPHpath. Trace for simple path search
Alone, the first change would cause the path from v to w to be printed This trace shows the operation
in reverse order: If the call to pathR(G, t, w) finds a path from t of the recursive function in Pro-
to w (and prints that path’s edges in reverse order), then printing t-v gram 17.11 for the call pathR(G,
2, 6) to find a simple path from
completes the job for the path from v to w. The second change reverses 2 to 6 in the graph shown at the
the order: To print the edges on the path from v to w, we print the top. There is a line in the trace for
edges on the path from w to v in reverse order. (This trick does not each edge considered, indented
work for digraphs.) We could use this same strategy to implement one level for each recursive call.
To check 2-0, we call pathR(G,
an ADT function that calls a client-supplied function for each of the 0, 6). This call causes us to check
path’s edges (see Exercise 17.87). 0-1, 0-2, and 0-5. To check 0-1,
Figure 17.17 gives an example of the dynamics of the recursion. we call pathR(G, 1, 6), which
As with any recursive program (indeed, any program with function causes us to check 1-0 and 1-2,
which do not lead to recursive
calls at all), such a trace is easy to produce: To modify Program 17.11 calls because 0 and 2 are marked.
to produce one, we can add a variable depth that is incremented For this example, the function dis-
on entry and decremented on exit to keep track of the depth of the covers the path 2-0-5-4-6.
52 §17.7 CHAPTER SEVENTEEN

Program 17.11 Simple path search (adjacency matrix)


The function GRAPHpath tests for the existence of a simple path con-
necting two given vertices. It uses a recursive depth-first search function
pathR, which, given two vertices v and w, checks each edge v-t adjacent
to v to see whether it could be the first edge on a path to w. The vertex-
indexed array visited keeps the function from revisiting any vertex,
and therefore keeps paths simple.
To have the function print the edges of the path (in reverse order),
add the statement printf("%d-%d ", t, v); just before the return 1
near the end of pathR (see text).

static int visited[maxV];


int pathR(Graph G, int v, int w)
{ int t;
if (v == w) return 1;
visited[v] = 1;
for (t = 0; t < G->V; t++)
if (G->adj[v][t] == 1)
if (visited[t] == 0)
if (pathR(G, t, w)) return 1;
return 0;
}
int GRAPHpath(Graph G, int v, int w)
{ int t;
for (t = 0; t < G->V; t++) visited[t] = 0;
return pathR(G, v, w);
}

recursion, then add code at the beginning of the recursive function to


print out depth spaces followed by the appropriate information (see
Exercises 17.85 and 17.86).
Program 17.11 is centered around checking all edges adjacent
to a given vertex. As we have noted, this operation is also easy to
implement if we use the adjacency-lists representation for sparse graphs
(see Exercise 17.89) or any of the variations that we have discussed.

Property 17.2 We can find a path connecting two given vertices in a


graph in linear time.
GRAPH PROPERTIES AND TYPES §17.7 53

The recursive depth-first search function in Program 17.11 immedi-


ately implies a proof by induction that the ADT function determines
whether or not a path exists. Such a proof is easily extended to es-
tablish that, in the worst case, Program 17.11 checks all the entries
in the adjacency matrix exactly once. Similarly, we can show that the
analogous program for adjacency lists checks all of the graph edges
exactly twice (once in each direction), in the worst case.
We use the phrase linear in the context of graph algorithms to
mean a quantity whose value is within a constant factor of V + E,
the size of the graph. As discussed at the end of Section 17.5, such a
value is also normally within a constant factor of the size of the graph
representation. Property 17.2 is worded so as to allow for the use of
the adjacency-lists representation for sparse graphs and the adjacency-
matrix representation for dense graphs, our general practice. It is not
appropriate to use the term “linear” to describe an algorithm that
uses an adjacency matrix and runs in time proportional to V 2 (even
0
though it is linear in the size of the graph representation) unless the
6
graph is dense. Indeed, if we use the adjacency-matrix representation 1 2
for a sparse graph, we cannot have a linear-time algorithm for any
graph-processing problem that could require examination of all the 3
edges. 4
5
We study depth-first search in detail in a more general setting in
the next chapter, and we consider several other connectivity algorithms
there. For example, a slightly more general version of Program 17.11 0
gives us a way to pass through all the edges in the graph, building 6
a vertex-indexed array that allows a client to test in constant time 1 2
whether there exists a path connecting any two vertices.
3
Property 17.2 can substantially overestimate the actual running
4
time of Program 17.11, because it might find a path after examining 5
only a few edges. For the moment, our interest is in knowing a method
that is guaranteed to find in linear time a path connecting any two
vertices in any graph. By contrast, other problems that appear similar Figure 17.18
are much more difficult to solve. For example, consider the following Hamilton tour
problem, where we seek paths connecting pairs of vertices, but add the The graph at the top has the
restriction that they visit all the other vertices in the graph, as well. Hamilton tour 0-6-4-2-1-3-5-0,
which visits each vertex exactly
Hamilton path Given two vertices, is there a simple path con-
once and returns to the start vertex,
necting them that visits every vertex in the graph exactly once? If but the graph at the bottom has no
the path is from a vertex back to itself, this problem is known as the such tour.
54 §17.7 CHAPTER SEVENTEEN

0
Program 17.12 Hamilton path
6
1 2 The function GRAPHpathH searches for a Hamilton path from v to w. It
uses a recursive function that differs from the one in Program 17.11 in
3 just two respects: First, it returns successfully only if it finds a path of
4 length V; second, it resets the visited marker before returning unsuc-
5 cessfully. Do not expect this function to finish except for tiny graphs
(see text).
0-1
1-2 static int visited[maxV];
2-3 int pathR(Graph G, int v, int w, int d)
3-4 { int t;
4-5 if (v == w)
4-6
2-4 { if (d == 0) return 1; else return 0; }
4-3 visited[v] = 1;
4-5 for (t = 0; t < G->V; t++)
4-6 if (G->adj[v][t] == 1)
0-2
2-1
if (visited[t] == 0)
2-3 if (pathR(G, t, w, d-1)) return 1;
3-4 visited[v] = 0;
4-5 return 0;
4-6
}
2-4
4-3 int GRAPHpathH(Graph G, int v, int w)
4-5 { int t;
4-6 for (t = 0; t < G->V; t++) visited[t] = 0;
0-5
return pathR(G, v, w, G->V-1);
5-4
4-2 }
2-1
2-3
4-3 Hamilton tour problem. Is there a cycle that visits every vertex in the
3-2 graph exactly once?
2-1 At first blush, this problem seems to admit a simple solution: We
4-6
can write down the simple recursive program for finding a Hamilton
0-6
path that is shown in Program 17.12. But this program is not likely
Figure 17.19 to be useful for many graphs, because its worst-case running time is
Hamilton-tour–search trace exponential in the number of vertices in the graph.
This trace shows the edges checked
by Program 17.12 when discover- Property 17.3 A recursive search for a Hamilton tour could take
ing that the graph shown at the top exponential time.
has no Hamilton tour. For brevity,
edges to marked vertices are omit- Proof : Consider a graph where vertex V-1 is isolated and the edges,
ted. with the other V − 1 vertices, constitute a complete graph. Pro-
GRAPH PROPERTIES AND TYPES §17.7 55

gram 17.12 will never find a Hamilton path, but it is easy to see by
induction that it will examine all of the (V − 1)! paths in the complete
graph, all of which involve V − 1 recursive calls. The total number of
recursive calls is therefore about V !, or about (V /e)V , which is higher
than any constant to the V th power.
Our implementations Program 17.11 for finding simple paths and
Program 17.12 for finding Hamilton paths are extremely similar, and
both programs terminate when all the elements of the visited array
are set to 1. Why are the running times so dramatically different?
Program 17.11 is guaranteed to finish quickly because it sets at least
one element of the visited array to 1 each time pathR is called.
Program 17.12, on the other hand, can set visited elements back to
0, so we cannot guarantee that it will finish quickly.
When searching for simple paths, in Program 17.11, we know
that, if there is a path from v to w, we will find it by taking one of
the edges v-t from v, and the same is true for Hamilton paths. But
there this similarity ends. If we cannot find a simple path from t to
w, then we can conclude that there is no simple path from v to w that
goes through t; but the analogous situation for Hamilton paths does
not hold. It could be that case that there is no Hamilton path to w
that starts with v-t, but there is one that starts with v-x-t for some
other vertex x. We have to make a recursive call from t corresponding
to every path that leads to it from v. In short, we may have to check
every path in the graph.
It is worthwhile to reflect on just how slow a factorial-time algo-
rithm is. If we could process a graph with 15 vertices in 1 second, it
would take 1 day to process a graph with 19 vertices, over 1 year for
21 vertices, and over 6 centuries for 23 vertices. Faster computers do
not help much, either. A computer that is 200,000 times faster than
our original one would still take more than a day to solve that 23-
vertex problem. The cost to process graphs with 100 or 1000 vertices
is too high to contemplate, let alone graphs of the size that we might
encounter in practice. It would take millions of pages in this book just
to write down the number of centuries required to process a graph that
contained millions of vertices.
In Chapter 5, we examined a number of simple recursive pro-
grams that are similar in character to Program 17.12 but that could
be drastically improved with top-down dynamic programming. This
56 §17.7 CHAPTER SEVENTEEN

recursive program is entirely different in character: The number of in-


termediate results that would have to be saved is exponential. Despite
many people doing an extensive amount of work on the problem, no
one has been able to find any algorithm that can promise reasonable
performance for large (or even medium-sized) graphs.
Now, suppose that we change the restriction from having to visit
all the vertices to having to visit all the edges. Is this problem easy, like
finding a simple path, or hopelessly difficult, like finding a Hamilton
path?
Euler path Is there a path connecting two given vertices that
uses each edge in the graph exactly once? The path need not be
simple—vertices may be visited multiple times. If the path is from a
vertex back to itself, we have the Euler tour problem. Is there a cyclic
path that uses each edge in the graph exactly once? We prove in the
corollary to Property 17.4 that the path problem is equivalent to the
0 tour problem in a graph formed by adding an edge connecting the two
6 vertices. Figure 17.20 gives two small examples.
1 2 This classical problem was first studied by L. Euler in 1736.
Indeed, some people trace the origin of the study of graphs and graph
3
4 theory to Euler’s work on this problem, starting with a special case
5 known as the bridges of Königsberg problem (see Figure 17.21). The
Swiss town of Königsberg had seven bridges connecting riverbanks
and islands, and people in the town found that they could not seem
0 to cross all the bridges without crossing one of them twice. Their
6
1 2
problem amounts to the Euler tour problem.
These problems are familiar to puzzle enthusiasts. They are com-
3 monly seen in the form of puzzles where you are to draw a given figure
4 without lifting your pencil from the paper, perhaps under the con-
5
straint that you must start and end at particular points. It is natural
for us to consider Euler paths when developing graph-processing algo-
Figure 17.20 rithms, because a Euler path is an efficient representation of the graph
Euler tour and path examples (putting the edges in a particular order) that we might consider as the
The graph at the top has the Euler basis for developing efficient algorithms.
tour 0-1-2-0-6-4-3-2-4-5-0, Euler showed that it is easy to determine whether or not a path
which uses all the edges exactly exists, because all that we need to do is to check the degree of each of
once. The graph at the bottom no
such tour, but it has the Euler path the vertices. The property is easy to state and apply, but the proof is a
1-2-0-1-3-4-2-3-5-4-6-0-5. tricky exercise in graph theory.
GRAPH PROPERTIES AND TYPES §17.7 57

Property 17.4 A graph has a Euler tour if and only if it is connected


and all its vertices are of even degree.

Proof : To simplify the proof, we allow self-loops and parallel edges,


though it is not difficult to modify the proof to show that this property
also holds for simple graphs (see Exercise 17.94).
If a graph has a Euler tour, then it must be connected because
the tour defines a path connecting each pair of vertices. Also, any
given vertex v must be of even degree because when we traverse the
tour (starting anywhere else), we enter v on one edge and leave on
a different edge (neither of which appear again on the tour); so the
number of edges incident upon v must be twice the number of times
we visit v when traversing the tour, an even number.
To prove the converse, we use induction on the number of edges.
2
The claim is certainly true for graphs with no edges. Consider any
connected graph that has more than one edge, with all vertices of 0 3
even degree. Suppose that, starting at any vertex v, we follow and
remove any edge, and we continue doing so until arriving at a vertex 1
that has no more edges. This process certainly must terminate, since
we delete an edge at every step, but what are the possible outcomes?
Figure 17.22 illustrates examples. Immediately, we see that we must Figure 17.21
Bridges of Königsberg
end up back at v, because we end at a vertex other than v if and only
A well-known problem studied
if that vertex had an odd degree when we started.
by Euler has to do with town of
One possibility is that we trace the full tour; if so, we are done. Königsberg, in which there is
Otherwise, all the vertices in the graph that remains have even degree, an island at the point where the
but it may not be connected. Still, each connected component has river Pregel forks. There are seven
bridges connecting the island with
a Euler tour by the inductive hypothesis. Moreover, the cyclic path the two banks of the river and the
just removed connects those tours together into a Euler tour for the land between the forks, as shown
original graph: traverse the cyclic path, taking detours to do the Euler in the diagram at top. Is there a
way to cross the seven bridges in a
tours for the connected components defined by deleting the edges on
continuous walk through the town,
the cyclic path (each detour is a proper Euler tour that takes us back without recrossing any of them? If
to the vertex on the cyclic path where it started). Note that a detour we label the island 0, the banks 1
may touch the cyclic path multiple times (see Exercise 17.99). In such and 2, and the land between the
forks 3 and define an edge corre-
a case, we take the detour only once (say, when we first encounter it).
sponding to each bridge, we get
the multigraph shown at the bot-
tom. The problem is to find a path
Corollary A graph has a Euler path if and only if it is connected and through this graph that uses each
exactly two of its vertices are of odd degree. edge exactly once.
58 §17.7 CHAPTER SEVENTEEN

0
Program 17.13 Euler path existence
6
1 2 This function, which is based upon the corollary to Property 17.4, tests
whether there is an Euler path from v to w in a connected graph, using
3 the GRAPHdeg ADT function from Exercise 17.40. It takes time propor-
4 tional to V , not including preprocessing time to check connectivity and
5 to build the vertex-degree table used by GRAPHdeg.
1-0-2-1
int GRAPHpathE(Graph G, int v, int w)
0
{ int t;
6 t = GRAPHdeg(G, v) + GRAPHdeg(G, w);
1 2 if ((t % 2) != 0) return 0;
for (t = 0; t < G->V; t++)
3 if ((t != v) && (t != w))
4
if ((GRAPHdeg(G, t) % 2) != 0) return 0;
5
return 1;
6-0-1-2-4-6
}
0
6
1 2
Proof : This statement is equivalent to Property 17.4 in the graph
formed by adding an edge connecting the two vertices of odd degree
3 (the ends of the path).
4
5 Therefore, for example, there is no way for anyone to traverse
2-0-1-2-3-4-2
all the bridges of Königsberg in a continuous path without retracing
their steps, since all four vertices in the corresponding graph have odd
0 degree (see Figure 17.21).
6 As discussed in Section 17.5, we can find all the vertex degrees
1 2
in time proportional to E for the adjacency-lists or set-of-edges rep-
3
resentation and in time proportional to V 2 for the adjacency-matrix
4 representation, or we can maintain a vertex-indexed array with ver-
5 tex degrees as part of the graph representation (see Exercise 17.40).
0-6-4-5-0-2-1-0 Given the array, we can check whether Property 17.4 is satisfied in
time proportional to V . Program 17.13 implements this strategy, and
Figure 17.22 demonstrates that determining whether a given graph has a Euler path
Partial tours is an easy computational problem. This fact is significant because we
Following edges from any vertex have little intuition to suggest that the problem should be easier than
in a graph that has an Euler tour determining whether a given graph has a Hamilton path.
always takes us back to that vertex,
as shown in these examples. The Now, suppose that we actually wish to find a Euler path. We are
cycle does not necessarily use all treading on thin ice because a direct recursive implementation (find
the edges in the graph. a path by trying an edge, then making a recursive call to find a path
GRAPH PROPERTIES AND TYPES §17.7 59

Program 17.14 Linear-time Euler path (adjacency lists)


The function pathEshow prints an Euler path between v and w. With
a constant-time implementation of GRAPHremoveE (see Exercise 17.44),
it runs in linear time. The auxiliary function path follows and removes
edges on a cyclic path and pushes vertices onto a stack, to be checked
for side loops (see text). The main loop calls path as long as there are
vertices with side loops to traverse.

#include "STACK.h"
int path(Graph G, int v)
{ int w;
for (; G->adj[v] != NULL; v = w)
{
STACKpush(v);
w = G->adj[v]->v;
GRAPHremoveE(G, EDGE(v, w));
}
return v;
}
void pathEshow(Graph G, int v, int w)
{
STACKinit(G->E);
printf("%d", w);
while ((path(G, v) == v) && !STACKempty())
{ v = STACKpop(); printf("-%d", v); }
printf("\n");
}

for the rest of the graph) will have the same kind of factorial-time
performance as Program 17.12. We expect not to have to live with
such performance because it is so easy to test whether a path exists,
so we seek a better algorithm. It is possible to avoid factorial-time
blowup with a fixed-cost test for determining whether or not to use an
edge (rather than unknown costs from the recursive call), but we leave
this approach as an exercise (see Exercises 17.96 and 17.97).
Another approach is suggested by the proof of Property 17.4.
Traverse a cyclic path, deleting the edges encountered and pushing
onto a stack the vertices that it encounters, so that (i) we can trace
60 §17.7 CHAPTER SEVENTEEN

0: 2 5 6 0 0: 6 0
Figure 17.23 6 6
1: 2 1:
Euler tour by removing cycles
2: 0 3 4 1 1 2 2: 3 4 1 2
This figure shows how Pro- 3: 4 2 3: 4 2
gram 17.14 discovers an Euler 4: 6 5 3 2 3 4: 3 2 3
tour from 0 back to 0 in a sample 5: 4 0 4 5: 4
graph. Thick black edges are those 6: 4 0 5 6: 0 5
on the tour, the stack contents are
1 1 2 0 5 4 6
listed below each diagram, and ad-
jacency lists for the non-tour edges
are shown at left. 0: 2 5 6 0 0: 0
First, the program adds the 1: 6 1: 6
edge 0-1 to the tour and removes 2: 0 3 4 1 2 2: 3 4 1 2
it from the adjacency lists (in two 3: 4 2 3: 4 2
places) (top left, lists at left). Sec- 4: 6 5 3 2 3 4: 3 2 3
ond, it adds 1-2 to the tour in 5: 4 0 4 5: 4
the same way (left, second from 6: 4 0 5 6: 5
top). Next, it winds up back at 0 1 2 1 2 0 5 4 6 0
but continues to do another cycle
0-5-4-6-0, winding up back at 0 0: 5 6 0 0: 0
with no more edges incident upon 1: 6 1: 6
0 (right, second from top). Then 2: 3 4 2: 3 4
1 2 1 2
it pops the isolated vertices 0 and 3: 4 2 3: 2
6 from the stack until 4 is at the 4: 6 5 3 2 4: 2
3 3
top and starts a tour from 4 (right, 5: 4 0 5:
4 4
third from from top), which takes 6: 4 0 6:
5 5
it to 3, 2, and back to 4, where-
upon it pops all the now-isolated 1 2 0 1 2 0 5 4 3
vertices 4, 2, 3, and so forth. The
sequence of vertices popped from 0: 6 0 0: 0
the stack defines the Euler tour 1: 6 1: 6
0-6-4-2-3-4-5-0-2-1-0 of the 2: 3 4 1 2 2: 4 1 2
whole graph. 3: 4 2 3:
4: 6 5 3 2 3 4: 2 3
5: 4 4 5: 4
6: 4 0 6:
5 5
1 2 0 5 1 2 0 5 4 3 2

0: 6 0 0: 0
1: 6 1: 6
2: 3 4 1 2 2: 1 2
3: 4 2 3:
4: 6 3 2 3 4: 3
5: 5:
4 4
6: 4 0 6:
5 5
1 2 0 5 4 1 2 0 5 4 3 2 4
GRAPH PROPERTIES AND TYPES §17.7 61

back along that path, printing out its edges, and (ii) we can check each
vertex for additional side paths (which can be spliced into the main
path). This process is illustrated in Figure 17.23.
Program 17.14 is an implementation along these lines, for an
adjacency-lists graph ADT. It assumes that a Euler path exists, and it
destroys the graph representation; so the client has responsibility to
use GRAPHpathE, GRAPHcopy, and GRAPHdestroy as appropriate. The
code is tricky—novices may wish to postpone trying to understand it
until gaining more experience with graph-processing algorithms in the
next few chapters. Our purpose in including it here is to illustrate that
good algorithms and clever implementations can be very effective for
solving some graph-processing problems.
Property 17.6 We can find a Euler tour in a graph, if one exists, in
linear time.
We leave a full induction proof as an exercise (see Exercise 17.101).
Informally, after the first call on path, the stack contains a path from
v to w, and the graph that remains (after removal of isolated vertices)
consists of the smaller connected components (sharing at least one
vertex with the path so far found) that also have Euler tours. We pop
isolated vertices from the stack and use path to find Euler tours that
contain the nonisolated vertices, in the same manner. Each edge in the
graph is pushed onto (and popped from) the stack exactly once, so the
total running time is proportional to E.
Despite their appeal as a systematic way to traverse all the edges
and vertices, we rarely use Euler tours in practice because few graphs
have them. Instead, we typically use depth-first search to explore
graphs, as described in detail in Chapter 18. Indeed, as we shall see,
doing depth-first search in an undirected graph amounts to computing
a two-way Euler tour: a path that traverses each edge exactly twice,
once in each direction.
In summary, we have seen in this section that it is easy to find
simple paths in graphs, that it is even easier to know whether we can
tour all the edges of a large graph without revisiting any of them (by
just checking that all vertex degrees are even), and that there is even a
clever algorithm to find such a tour; but that it is practically impossible
to know whether we can tour all the graph’s vertices without revisiting
any. We have simple recursive solutions to all these problems, but the
62 §17.7 CHAPTER SEVENTEEN

potential for exponential growth in the running time renders some of


the solutions useless in practice. Others provide insights that lead to
fast practical algorithms.
This range of difficulty among apparently similar problems that
is illustrated by these examples is typical in graph processing, and is
fundamental to the theory of computing. As discussed briefly in Sec-
tion 17.8 and in detail in Part 8, we must acknowledge what seems to
be an insurmountable barrier between problems that seem to require
exponential time (such as the Hamilton tour problem and many other
commonly encountered problems) and problems for which we know
algorithms that can guarantee to find a solution in polynomial time
(such as the Euler tour problem and many other commonly encoun-
tered problems). In this book, our primary objective is to develop
efficient algorithms for problems in the latter class.

Exercises
 17.84 Show, in the style of Figure 17.17, the trace of recursive calls (and
vertices that are skipped), when Program 17.11 finds a path from 0 to 5 in
the graph

3-7 1-4 7-8 0-5 5-2 3-8 2-9 0-6 4-9 2-6 6-4.

17.85 Modify the recursive function in Program 17.11 to print out a trace
like Figure 17.17, using a global variable as described in the text.

17.86 Do Exercise 17.85 by adding an argument to the recursive function


to keep track of the depth.

17.87 Using the method described in the text, give an implementation of


GRAPHpath that calls a client-supplied function for each edge on a path from
v to w, if any such path exists.

◦ 17.88 Modify Program 17.11 such that it takes a third argument d and
tests the existence of a path connecting u and v of length greater than d. In
particular, GRAPHpath(v, v, 2) should be nonzero if and only if v is on a
cycle.

 17.89 Modify GRAPHpath to use the adjacency-lists graph representation


(Program 17.6).

• 17.90 Run experiments to determine empirically the probability that Pro-


gram 17.11 finds a path between two randomly chosen vertices for various
graphs (see Exercises 17.63–76), and to calculate the average length of the
paths found for each type of graph.
GRAPH PROPERTIES AND TYPES §17.7 63

◦ 17.91 Consider the graphs defined by the following four sets of edges:
0-1 0-2 0-3 1-3 1-4 2-5 2-9 3-6 4-7 4-8 5-8 5-9 6-7 6-9 7-8
0-1 0-2 0-3 1-3 0-3 2-5 5-6 3-6 4-7 4-8 5-8 5-9 6-7 6-9 8-8
0-1 1-2 1-3 0-3 0-4 2-5 2-9 3-6 4-7 4-8 5-8 5-9 6-7 6-9 7-8
4-1 7-9 6-2 7-3 5-0 0-2 0-8 1-6 3-9 6-3 2-8 1-5 9-8 4-5 4-7
Which of these graphs have Euler tours? Which of them have Hamilton tours?

◦ 17.92 Give necessary and sufficient conditions for a directed graph to have
a (directed) Euler tour.
17.93 Prove that every connected undirected graph has a two-way Euler
tour.
17.94 Modify the proof of Property 17.4 to make it work for graphs with
parallel edges and self-loops.
 17.95 Show that adding one more bridge could give a solution to the
bridges-of-Königsberg problem.
• 17.96 Prove that a connected graph has a Euler path from v to w only if
it has an edge incident on v whose removal does not disconnect the graph
(except possibly by isolating v).
• 17.97 Use Exercise 17.96 to develop an efficient recursive method for find-
ing a Euler tour in a graph that has one. Beyond the basic graph ADT
functions, you may use the ADT functions GRAPHdeg (see Exercise 17.40) and
GRAPHpath (see Program 17.11). Implement and test your program for the
adjacency-matrix graph representation.
17.98 Develop a representation-independent version of Program 17.14 that
uses a general interface for visiting all edges adjacent to a given vertex (see
Exercise 17.60). Note: Be careful of interactions between your code and the
GRAPHremoveE function. Make sure that your implementation works properly
in the presence of parallel edges and self-loops.
 17.99 Give an example where the graph remaining after the first call to
path in Program 17.14 is not connected (in a graph that has a Euler tour).
 17.100 Describe how to modify Program 17.14 so that it can be used to
detect whether or not a given graph has a Euler tour, in linear time.
17.101 Give a complete proof by induction that the linear-time Euler tour
algorithm described in the text and implemented in Program 17.14 properly
finds a Euler tour.
◦ 17.102 Find the number of V -vertex graphs that have a Euler tour, for as
large a value of V as you can feasibly afford to do the computation.
• 17.103 Run experiments to determine empirically the average length of the
path found by the first call to path in Program 17.14 for various graphs (see
Exercises 17.63–76). Calculate the probability that this path is cyclic.
64 §17.8 CHAPTER SEVENTEEN

◦ 17.104 Write a program that computes a sequence of 2n + n − 1 bits in


which no two pairs of n consecutive bits match. (For example, for n = 3,
the sequence 0001110100 has this property.) Hint: Find a Euler tour in a de
Bruijn digraph.
 17.105 Show, in the style of Figure 17.19, the trace of recursive calls (and
vertices that are skipped), when Program 17.11 finds a Hamilton tour in the
graph
3-7 1-4 7-8 0-5 5-2 3-8 2-9 0-6 4-9 2-6 6-4.

◦ 17.106 Modify Program 17.12 to print out the Hamilton tour if it finds one.
• 17.107 Find a Hamilton tour of the graph
1-2 5-2 4-2 2-6 0-8 3-0 1-3 3-6 1-0 1-4 4-0 4-6 6-5 2-6
6-9 9-0 3-1 4-3 9-2 4-9 6-9 7-9 5-0 9-7 7-3 4-5 0-5 7-8
or show that none exists.
•• 17.108 Determine how many V -vertex graphs have a Hamilton tour, for as
large a value of V as you can feasibly afford to do the computation.

17.8 Graph-Processing Problems


Armed with the basic tools developed in this chapter, we consider
in Chapters 18 through 22 a broad variety of algorithms for solving
graph-processing problems. These algorithms are fundamental ones
and are useful in many applications, but they serve as only an introduc-
tion to the subject of graph algorithms. Many interesting and useful
algorithms have been developed that are beyond the scope of this book,
and many interesting problems have been studied for which good al-
gorithms have still not yet been invented. As is true in any domain,
the first challenge that we face is determining how difficult to solve
a given problem is. For graph processing, this decision might be far
more difficult than we might imagine, even for problems that appear
to be simple to solve. Moreover, our intuition is not always helpful in
distinguishing easy problems from difficult or hitherto unsolved ones.
In this section, we describe briefly important classical problems and
the state of our knowledge of them.
Given a new graph-processing problem, what type of challenge
do we face in developing an implementation to solve it? The unfortu-
nate truth is that there is no good method to answer this question for
any problem that we might encounter, but we can provide a general de-
scription of the difficulty of solving various classical graph-processing
GRAPH PROPERTIES AND TYPES §17.8 65

problems. To this end, we will roughly categorize the problems ac-


cording to the difficulty of solving them, as follows:
• Easy
• Tractable
• Intractable
• Unknown
These terms are intended to convey information relative to one another
and to the current state of knowledge about graph algorithms.
As indicated by the terminology, our primary reason for catego-
rizing problems in this way is that there are many graph problems,
such as the Hamilton tour problem, that no one knows how to solve
efficiently. We will eventually see (in Part 8) how to make that state-
ment meaningful in a precise technical sense; at this point, we can at
least be warned that we face significant obstacles to writing an efficient
program to solve these problems.
We defer full context on many of the graph-processing problems
until later in the book. Here, we present brief statements that are easily
understood, to introduce the general issue of classifying the difficulty
of graph-processing problems.
An easy graph-processing problem is one that can be solved by the
kind of elegant and efficient short programs to which we have grown
accustomed in Parts 1 through 4. We often find the running time to
be linear in the worst case, or bounded by a small-degree polynomial
in the number of vertices or the number of edges. Generally, as we
have done in many other domains, we can establish that a problem is
easy by developing a brute-force solution that, although it may be too
slow for huge graphs, is useful for small and even intermediate-sized
problems. Then, once we know that the problem is easy, we look for
efficient solutions that we might use in practice, and try to identify
the best among those. The Euler tour problem that we considered in
Section 17.7 is a prime example of such a problem, and we shall see
many others in Chapters 18 through 22 including, most notably, the
following.
Simple connectivity Is a given graph connected? That is, is
there a path connecting every pair of vertices? Is there a cycle in the
graph, or is it a forest? Given two vertices, are they on a cycle? We
first considered these basic graph-processing question in Chapter 1. We
consider numerous solutions to such problems in Chapter 18. Some
66 §17.8 CHAPTER SEVENTEEN

are trivial to implement in linear time; others have rather sophisticated


linear-time solutions that bear careful study.
Strong connectivity in digraphs Is there a directed path con-
necting every pair of vertices in a digraph? Given two vertices, are they
connected by directed paths in both directions (are they on a directed
cycle)? Implementing efficient solutions to these problems is a much
more challenging task than for the corresponding simple-connectivity
problem in undirected graphs, and much of Chapter 19 is devoted to
studying them. Despite the clever intricacies involved in solving them,
we classify the problems as easy because we can write a compact,
efficient, and useful implementation.
Transitive closure What set of vertices can be reached by fol-
lowing directed edges from each vertex in a digraph? This problem is
closely related to strong connectivity and to other fundamental com-
putational problems. We study classical solutions that amount to a
few lines of code in Chapter 19.
Minimum spanning tree In a weighted graph, find a minimum-
weight set of edges that connects all the vertices. This is one of the old-
est and best-studied graph-processing problems; Chapter 20 is devoted
to the study of various classical algorithms to solve it. Researchers still
seek faster algorithms for this problem.
Single-source shortest paths What are the shortest paths con-
necting a given vertex v with each other vertex in a weighted digraph
(network)? Chapter 21 is devoted to the study of this problem, which
is extremely important in numerous applications. The problem is de-
cidedly not easy if edge weights can be negative.
A tractable graph-processing problem is one for which an algo-
rithm is known whose time and space requirements are guaranteed to
be bounded by a polynomial function of the size of the graph (V + E).
All easy problems are tractable, but we make a distinction because
many tractable problems have the property that developing an effi-
cient practical program to solve is an extremely challenging, if not
impossible, task. Solutions may be too complicated to present in this
book, because implementations might require hundreds or even thou-
sands of lines of code. The following examples are two of the most
important problems in this class.
Planarity Can we draw a given graph without any of the lines
that represent edges intersecting? We have the freedom to place the
GRAPH PROPERTIES AND TYPES §17.8 67

vertices anywhere, so we can solve this problem for many graphs, but
it is impossible to solve for many other graphs. A remarkable classical
result known as Kuratowski’s theorem provides an easy test for de-
termining whether a graph is planar: it says that the only graphs that
cannot be drawn with no edge intersections are those that contain some
subgraph that, after removing vertices of degree 2, is isomorphic to
one of the graphs in Figure 17.24. A straightforward implementation
of that test, even without taking the vertices of degree 2 into consid-
eration, would be too slow for large graphs (see Exercise 17.111), but
in 1974 R. Tarjan developed an ingenious (but intricate) algorithm for
solving the problem in linear time, using a depth-first search scheme
that extends those that we consider in Chapter 18. Tarjan’s algorithm
does not necessarily give a practical layout; it just certifies that a layout
exists. As discussed in Section 17.1, developing a visually pleasing lay-
out in applications where vertices do not necessarily relate directly to
the physical world has turned out to be a challenging research problem.
Matching Given a graph, what is the largest subset of its edges
with the property that no two are connected to the same vertex? This
classical problem is known to be solvable in time proportional to a 1
polynomial function of V and E, but a fast algorithm that is suitable 2
for huge graphs is still an elusive research goal. The problem is easier
0
to solve when restricted in various ways. For example, the problem of
matching students to available positions in selective institutions is an 3
4
example of bipartite matching: We have two different types of vertices
(students and institutions) and we are concerned with only those edges
0 1 2
that connect a vertex of one type with a vertex of the other type. We
see a solution to this problem in Chapter 22.
The solutions to some tractable problems have never been written 3 4 5
down as programs, or have running times so high that we could not
contemplate using them in practice. The following example is in this
Figure 17.24
class. It also demonstrates the capricious nature of the mathematical Forbidden subgraphs in pla-
reality of the difficulty of graph processing. nar graphs
Even cycles in digraphs Does a given digraph have a cycle Neither of these graphs can be
of even length? This question would seem simple to resolve because drawn in the plane without inter-
the corresponding question for undirected graphs is easy to solve (see secting edges, nor can any graph
that contains either of these graphs
Section 18.4), as is the question of whether a digraph has a cycle of odd as a subgraph (after we remove
length. However, for many years, the problem was not sufficiently well vertices of degree two); but all
understood for us even to know whether or not there exists an efficient other graphs can be so drawn.
68 §17.8 CHAPTER SEVENTEEN

algorithm for solving it (see reference section). A theorem establishing


the existence of an efficient algorithm was proved in 1999, but the
method is so complicated that no mathematician or programmer would
attempt to implement it.
One of the important themes of Chapter 22 is that many tractable
graph problems are best handled by algorithms that can solve a whole
class of problems in a general setting. The shortest-paths algorithms
of Chapter 21, the network-flow algorithms of Chapter 22, and the
powerful network-simplex algorithm of Chapter 22 are capable of
solving many graph problems that otherwise might present a significant
challenge. Examples of such problems include the following.
Assignment This problem, also known as bipartite weighted
matching, is to find a perfect matching of minimum weight in a bipar-
tite graph. It is easily solved with network-flow algorithms. Specific
methods that attack the problem directly are known, but they have
been shown to be essentially equivalent to network-flow solutions.
General connectivity What is the minimum number of edges
whose removal will separate a graph into two disjoint parts (edge
connectivity)? What is the minimum number of vertices whose removal
will separate a graph into two disjoint parts (vertex connectivity)?
As we see in Chapter 22, these problems, although difficult to solve
directly, can both be solved with network-flow algorithms.
Mail carrier Given a graph, find a tour with a minimal number
of edges that uses every edge in the graph at least once (but is allowed
to use edges multiple times). This problem is much more difficult than
the Euler tour problem but much less difficult than the Hamilton tour
problem.
The step from convincing yourself that a problem is tractable to
providing software that can be used in practical situations can be a
large step, indeed. On the one hand, when proving that a problem is
tractable, researchers tend to brush past numerous details that have
to be dealt with in an implementation; on the other hand, they have
to account for numerous potential situations even though they may
not arise in practice. This gap between theory and practice is particu-
larly acute when investigators are considering graph algorithms, both
because mathematical research is filled with deep results describing a
bewildering variety of structural properties that we may need to take
into account when processing graphs, and because the relationships be-
GRAPH PROPERTIES AND TYPES §17.8 69

tween those results and the properties of graphs that arise in practice
are little understood. The development of general schemes such as the
network-simplex algorithm has been an extremely effective approach
to dealing with such problems.
An intractable graph-processing problem is one for which there
is no known algorithm that is guaranteed to solve the problem in a rea-
sonable amount of time. Many such problems have the characteristic
that we can use a brute-force method where we can try all possibilities
to compute the solution—we consider them to be intractable because
there are far too many possibilities to consider. This class of problems
is extensive, and includes many important problems that we would
like to know how to solve. The term NP-hard describes the prob-
lems in this class. Most experts believe that no efficient algorithms
exist for these problems. We consider the bases for this terminology
and this belief in more detail in Part 8. The Hamilton path problem
that we discussed in Section 17.7 is a prime example of an NP-hard
graph-processing problem, as are those on the following list.
Longest path What is the longest simple path connecting two
given vertices in a graph? Despite its apparent similarity to shortest-
paths problems, this problem is a version of the Hamilton tour prob-
lem, and is NP-hard.
Colorability Is there a way to assign one of k colors to each of
the vertices of a graph such that no edge connects two vertices of the
same color? This classical problem is easy for k = 2 (see Section 18.4),
but it is NP-hard for k = 3.
Independent set What is the size of the largest subset of the
vertices of a graph with the property that no two are connected by an
edge? Just as we saw when contrasting the Euler and Hamilton tour
problems, this problem is NP-hard, despite its apparent similarity to
the matching problem, which is solvable in polynomial time.
Clique What is size of the largest clique (complete subgraph) in
a given graph? This problem generalizes part of the planarity problem,
because if the largest clique has more than four nodes, the graph is not
planar.
These problems are formulated as existence problems—we are
asked to determine whether or not a particular subgraph exists. Some
of the problems ask for the size of the largest subgraph of a particular
type, which we can do by framing an existence problem where we test
70 §17.8 CHAPTER SEVENTEEN

for the existence of a subgraph of size k that satisfies the property of


interest, then use binary search to find the largest. In practice, we
actually often want to find a complete solution, which is potentially
much harder to do. Four example, the famous four-color theorem tells
us that it is possible use just four colors to color all the vertices of a
planar graph such that no edge connects two vertices of the same color.
But the theorem does not tell us how to do so for a particular planar
graph: knowing that a coloring exists does not help us find a complete
solution to the problem. Another famous example is the traveling
salesperson problem, which asks us to find the minimum-length tour
through all the vertices of a weighted graph. This problem is related to
the Hamilton path problem, but it is certainly no easier: if we cannot
find an efficient solution to the Hamilton path problem, we cannot
expect to find one for the traveling salesperson problem. As a rule,
when faced with difficult problems, we work with the simplest version
that we cannot solve. Existence problems are within the spirit of this
rule, but they also play an essential role in the theory, as we shall see
in Part 8.
The problems just listed are but a few of the thousands of NP-
hard problems that have been identified. They arise in all types of
computational applications, as we shall discuss in Part 8, but they are
particularly prevalent in graph processing; so we have to be aware of
their existence throughout this book.
Note that we are insisting that our algorithms guarantee effi-
ciency, in the worst case. Perhaps we should instead settle for algo-
rithms that are efficient for typical inputs (but not necessarily in the
worst case). Similarly, many of the problems involve optimization.
Perhaps we should instead settle for a long path (not necessarily the
longest) or a large clique (not necessarily the maximum). For graph
processing, it might be easy to find a good answer for graphs that
arise in practice, and we may not even be interested in looking for
an algorithm that could find an optimal solution in fictional graphs
that we will never see. Indeed, intractable problems can often be at-
tacked with straightforward or general-purpose algorithms similar to
Program 17.12 that, although they have exponential running time in
the worst case, can quickly find a solution (or a good approximation)
for specific problem instances that arise in practice. We would be re-
luctant to use a program that will crash or produce a bad answer for
GRAPH PROPERTIES AND TYPES §17.8 71

certain inputs, but we do sometimes find ourselves using programs that


run in exponential time for certain inputs. We consider this situation
in Part 8.
There are also many research results proving intractable prob-
lems to remain that way even when we relax various restrictions.
Moreover, there are many specific practical problems that we cannot
solve because no one knows a sufficiently fast algorithm. In this part
of the book, we will label problems NP-hard when we encounter them
and interpret this label as meaning, at the very least, that we are not
going to expect to find efficient algorithms to solve them and that we
are not going to attack them without using advanced techniques of the
type discussed in Part 8 (except perhaps to use brute-force methods to
solve tiny problems).
There are some graph-processing problems whose difficulty is
unknown. Neither is there an efficient algorithm known for them, nor
are they known to be NP-hard. It is possible, as our knowledge of
graph-processing algorithms and properties of graphs expands, that
some of these problems will turn out to be tractable, or even easy. The
following important natural problem, which we have already encoun-
tered (see Figure 17.2), is the best-known problem in this class.
Graph isomorphism Can we make two given graphs identical
by renaming vertices? Efficient algorithms are known for this problem
for many special types of graphs, but the difficulty of the general
problem remains open.
The number of significant problems whose intrinsic difficulty
is unknown is very small in comparison to the other categories that
we have considered, because of intense research in this field over the
past several decades. Certain problems in this class, such as graph
isomorphism, are of immense practical interest; other problems in
this class are of significance primarily by virtue of having resisted
classification.
For the class of easy algorithms, we are used to the idea of com-
paring algorithms with different worst-case performance characteris-
tics and trying to predict performance through analysis and empirical
tests. For graph processing, these tasks are particularly challenging
because of the difficulty of characterizing the types of graphs that
might arise in practice. Fortunately, many of the important classical
algorithms have optimal or near-optimal worst-case performance, or
72 §17.8 CHAPTER SEVENTEEN

Table 17.2 Difficulty of classical graph-processing problems

This table summarizes the discussion in the text about the relative diffi-
culty of various classical graph-processing problems, comparing them in
rough subjective terms. These examples indicate not only the range of
difficulty of the problems but also that classifying a given problem can
be a challenging task.

E T I ?

undirected graphs
connectivity ∗
general connectivity ∗
Euler tour ∗
Hamilton tour ∗
bipartite matching ∗
maximum matching ∗
planarity ∗
maximum clique ∗
2-colorability ∗
3-colorability ∗
shortest paths ∗
longest paths ∗
vertex cover ∗
isomorphism ∗
digraphs
transitive closure ∗
strong connectivity ∗
odd-length cycle ∗
even-length cycle ∗
weighted graphs
minimum spanning tree ∗
traveling salesperson ∗
networks
shortest paths (nonnegative weights) ∗
shortest paths (negative weights) ∗
maximum flow ∗
assignment ∗
minimum-cost flow ∗

Key:
E Easy—efficient classical algorithm known (see reference)
T Tractable—solution exists (implementation difficult)
I Intractable—no efficient solution known (NP-hard)
? Unknown
GRAPH PROPERTIES AND TYPES §17.8 73

have a running time that depends on only the number of vertices and
edges, rather than on the graph structure; so we can concentrate on
streamlining implementations and still can predict performance with
confidence.
In summary, there is a wide spectrum of problems and algorithms
known for graph processing. Table 17.2 summarizes some of the in-
formation that we have discussed. Every problem comes in different
versions for different types of graphs (directed, weighted, bipartite,
planar, sparse, dense), and there are thousands of problems and algo-
rithms to consider. We certainly cannot expect to solve every problem
that we might encounter, and some problems that appear to be simple
are still baffling the experts. Despite a natural a priori expectation
that we should have no problem distinguishing easy problems from
intractable ones, the many examples that we have discussed illustrate
that placing a problem even into these rough categories can turn into
a significant research challenge.
As our knowledge about graphs and graph algorithms expands,
given problems may move among these categories. Despite a flurry of
research activity in the 1970s and intensive work by many researchers
since then, the possibility still remains that all the problems that we
are discussing will someday be categorized as “easy” (solvable by an
algorithm that is compact, efficient, and possibly ingenious).
Having developed this context, we shall press on to consider
numerous useful graph-processing algorithms. Problems that we can
solve do arise often, the graph algorithms that we study serve well in
a great variety of applications, and these algorithms serve as the basis
for attacking numerous other problems that we need to handle even if
we cannot guarantee efficient solutions.

Exercises
• 17.109 Prove that neither of the two graphs depicted in Figure 17.24 is
planar.

17.110 Write a function for the adjacency-lists representation (Program 17.6)


that determines whether or not a graph contains one of the graphs depicted in
Figure 17.24, using a brute-force algorithm where you test all possible sub-
sets of five vertices for the clique and all possible subsets of six vertices for the
complete bipartite graph. Note: This test does not suffice to show whether
the graph is planar, because it ignores the condition that removing vertices of
degree 2 in some subgraph might give one of the two forbidden subgraphs.
74 §17.8 CHAPTER SEVENTEEN

17.111 Give a drawing of the graph


3-7 1-4 7-8 0-5 5-2 3-0 2-9 0-6 4-9 2-6
6-4 1-5 8-2 9-0 8-3 4-5 2-3 1-6 3-5 7-6
that has no intersecting edges, or prove that no such drawing exists.
17.112 Find a way to assign three colors to the vertices of the graph
3-7 1-4 7-8 0-5 5-2 3-0 2-9 0-6 4-9 2-6
6-4 1-5 8-2 9-0 8-3 4-5 2-3 1-6 3-5 7-6
such that no edge connects two vertices of the same color, or show that it is
not possible to do so.
17.113 Solve the independent-set problem for the graph
3-7 1-4 7-8 0-5 5-2 3-0 2-9 0-6 4-9 2-6
6-4 1-5 8-2 9-0 8-3 4-5 2-3 1-6 3-5 7-6.

17.114 What is the size of the largest clique in a de Bruijn graph of order n?
CHAPTER EIGHTEEN

Graph Search

W E OFTEN LEARN properties of a graph by systematically ex-


amining each of its vertices and each of its edges. Determining
some simple graph properties—for example, computing the degrees
of all the vertices—is easy if we just examine each edge (in any or-
der whatever). Many other graph properties are related to paths, so
a natural way to learn them is to move from vertex to vertex along
the graph’s edges. Nearly all the graph-processing algorithms that we
consider use this basic abstract model. In this chapter, we consider
the fundamental graph-search algorithms that we use to move through
graphs, learning their structural properties as we go.
Graph searching in this way is equivalent to exploring a maze.
Specifically, passages in the maze correspond to edges in the graph, and
points where passages intersect in the maze correspond to vertices in
the graph. When a program changes the value of a variable from vertex
v to vertex w because of an edge v-w, we view it as equivalent to a person
in a maze moving from point v to point w. We begin this chapter by
examining a systematic exploration of a maze. By correspondence with
this process, we see precisely how the basic graph-search algorithms
proceed through every edge and every vertex in a graph.
In particular, the recursive depth-first search (DFS) algorithm
corresponds precisely to the particular maze-exploration strategy of
Section 18.1. DFS is a classic and versatile algorithm that we use
to solve connectivity and numerous other graph-processing problems.
The basic algorithm admits two simple implementations: one that is
recursive, and another that uses an explicit stack. Replacing the stack

75
76 §18.1 CHAPTER EIGHTEEN

with a FIFO queue leads to another classic algorithm, breadth-first


search (BFS), which we use to solve another class of graph-processing
problems related to shortest paths.
The main topics of this chapter are DFS, BFS, their related algo-
rithms, and their application to graph processing. We briefly consid-
ered DFS and BFS in Chapter 5; we treat them from first principles
here, in the context of graph-search ADT functions that we can extend
to solve various graph-processing problems, and use them to demon-
strate relationships among various graph algorithms. In particular, we
consider a generalized graph-search method that encompasses a num-
ber of classical graph-processing algorithms, including both DFS and
BFS.
As illustrations of the application of these basic graph-searching
methods to solve more complicated problems, we consider algorithms
for finding connected components, biconnected components, span-
ning trees, and shortest paths, and for solving numerous other graph-
processing problems. These implementations exemplify the approach
that we shall use to solve more difficult problems in Chapters 19
through 22.
We conclude the chapter by considering the basic issues involved
in the analysis of graph algorithms, in the context of a case study com-
paring several different algorithms for finding the number of connected
components in a graph.

18.1 Exploring a Maze


Figure 18.1
Exploring a maze It is instructive to think about the process of searching through a graph
We can explore every passage- in terms of an equivalent problem that has a long and distinguished
way in a simple maze by following history (see reference section): finding our way through a maze that
a simple rule such as “keep your consists of passages connected by intersections. This section presents
right hand on the wall.” Follow- a detailed study of a basic method for exploring every passage in any
ing this rule in the maze at the top,
we explore the whole maze, going given maze. Some mazes can be handled with a simple rule, but most
through each passage once in each mazes require a more sophisticated strategy (see Figure 18.1). Using
direction. But if we follow this rule the terminology maze instead of graph, passage instead of edge, and
in a maze with a cycle, we return
intersection instead of vertex is making mere semantic distinctions,
to the starting point without explor-
ing the whole maze, as illustrated but, for the moment, doing so will help to give us an intuitive feel for
in the maze at the bottom. the problem.
GRAPH SEARCH §18.1 77

One trick for exploring a maze without getting lost that has been
known since antiquity (dating back at least to the legend of Theseus
and the Minotaur) is to unroll a ball of string behind us. The string
guarantees that we can always find a way out, but we are also interested
in being sure that we have explored every part of the maze, and we
do not want to retrace our steps unless we have to. To accomplish
these goals, we need some way to mark places that we have been. We
could use the string for this purpose as well, but we use an alternative
approach that models a computer implementation more closely.
We assume that there are lights, initially off, in every intersection,
and doors, initially closed, at both ends of every passage. We further
assume that the doors have windows and that the lights are sufficiently
strong and the passages sufficiently straight that we can determine,
by opening the door at one end of a passage, whether or not the
intersection at the other end is lit (even if the door at the other end is
closed). Our goals are to turn on all the lights and to open all the doors.
To reach them, we need a set of rules to follow, systematically. The
following maze-exploration strategy, which we refer to as Trémaux
exploration, has been known at least since the nineteenth century (see
reference section):
(i) If there are no closed doors at the current intersection, go to step
(iii). Otherwise, open any closed door to any passage leading out
of the current intersection (and leave it open).
(ii) If you can see that the intersection at the other end of that passage
is already lighted, try another door at the current intersection
(step (i)). Otherwise (if you can see that the intersection at the
other end of the passage is dark), follow the passage to that
intersection, unrolling the string as you go, turn on the light, and
go to step (i).
(iii) If all the doors at the current intersection are open, check whether
you are back at the start point. If so, stop. If not, use the string
to go back down the passage that brought you to this intersection
for the first time, rolling the string back up as you go, and look
for another closed door there (that is, return to step (i)).
Figures 18.2 and 18.3 depict a traversal of a sample graph and show
that, indeed, every light is lit and every door is opened for that ex-
ample. The figures depict just one of many possible outcomes of the
exploration, because we are free to open the doors in any order at each
78 §18.1 CHAPTER EIGHTEEN

Figure 18.2 0 2
0 2
Trémaux maze exploration
example 6
6
In this diagram, places that we 1 7
1 7
have not visited are shaded (dark) 3
3
and places that we have visited are
white (light). We assume that there 5 4
5 4
are lights in the intersections, and
that, when we have opened doors
into lighted intersections on both 0 2
0 2
ends of a passage, the passage is
lighted. To explore the maze, we 6
6
begin at 0 and take the passage to 1 7
2 (left, top). Then we proceed to 1 7
6, 4, 3 and 5, opening the doors to 3
3
the passages, lighting the intersec-
5 4
tions as we proceed through them, 5 4
and leaving a string trailing behind
us (left). When we open the door
0 2
that leads to 0 from 5, we can see 0 2
that 0 is lighted, so we skip that
passage (top right). Similarly, we 6
6
skip the passage from 5 to 4 (right, 1 7
1 7
second from top), leaving us with 3
nowhere to go from 5 except back 3
to 3 and then back to 4, rolling up 5 4
5 4
our ball of string. When we open
the doorway from the passage from
4 to 5, we can see that 5 is lighted 0 2
0 2
through the open door at the other
end, and we therefore skip that 6
passage (right, bottom). We never 6
1 7
walked down the passage connect- 1 7
ing 4 and 5, but we lighted it by 3
3
opening the doors at both ends.
5 4
5 4

0 2
0 2

6
6
1 7
1 7
3
3

5 4
5 4
GRAPH SEARCH §18.1 79

0 2
0 2 Figure 18.3
Trémaux maze exploration
6
6 example (continued)
1 7 Next, we proceed to 7 (top left),
1 7
3 open the door to see that 0 is
3
lighted (left, second from top),
5 4
5 4 and then proceed to 1 (left, third
from top). At this point, most of
the maze is traversed, and we use
0 2 our string to take us back to the
0 2
beginning, moving from 1 to 7 to 4
6 to 6 to 2 to 0. Back at 0, we com-
6
1 7 plete our exploration by checking
1 7 the passages to 5 (right, second
3 from bottom) and 7 (bottom right),
3
leaving all passages and intersec-
5 4
5 4 tions lighted. Again, the passages
connecting 0 to 5 and 0 to 7 are
both lighted because we opened
0 2
0 2 the doors at both ends, but we did
not walk through them.
6
6
1 7
1 7
3
3

5 4
5 4

0 2
0 2

6
6
1 7
1 7
3
3

5 4
5 4

0 2
0 2

6
6
1 7
1 7
3
3

5 4
5 4
80 §18.1 CHAPTER EIGHTEEN

intersection. Convincing ourselves that this method is always effective


is an interesting exercise in mathematical induction.

Property 18.1 When we use Trémaux maze exploration, we light


all lights and open all doors in the maze, and end up back where we
started.

Proof : To prove this assertion by induction, we first note that it holds,


trivially, for a maze that contains one intersection and no passages—
we just turn on the light. For any maze that contains more than one
intersection, we assume the property to be true for all mazes with fewer
intersections. It suffices to show that we visit all intersections, since we
open all the doors at every intersection that we visit. Now, consider
the first passage that we take from the first intersection, and divide the
intersections into two subsets: (i) those that we can reach by taking that
passage without returning to the start, and (ii) those that we cannot
reach from that passage without returning to the start. Applying the
inductive hypothesis, we know that we visit all intersections in (i)
(ignoring any passages back to the start intersection, which is lit) and
end up back at the start intersection. Then, applying the the inductive
hypothesis again, we know that we visit all intersections in (ii) (ignoring
the passages from the start to intersections in (i), which are lit).

From the detailed example in Figures 18.2 and 18.3, we see that
there are four different possible situations that arise for each passage
that we consider taking:
(i) The passage is dark, so we take it.
(ii) The passage is the one that we used to enter (it has our string in
Figure 18.4 it), so we use it to exit (and we roll up the string).
Decomposing a maze (iii) The door at the other end of the passage is closed (but the inter-
To prove by induction that section is lit), so we skip the passage.
Trémaux exploration takes us ev- (iv) The door at the other end of the passage is open (and the inter-
erywhere in a maze (top), we
break it into two smaller pieces, by section is lit), so we skip it.
removing all edges connecting the The first and second situations apply to any passage that we traverse,
first intersection with any intersec- first at one end and then at the other end. The third and fourth
tion that can be reached from the
situations apply to any passage that we skip, first at one end and
first passage without going back
through the first intersection (bot- then at the other end. Next, we see how this perspective on maze
tom). exploration translates directly to graph search.
GRAPH SEARCH §18.2 81

Exercises
 18.1 Assume that intersections 6 and 7 (and all the hallways connected to
them) are removed from the maze in Figures 18.2 and 18.3, and a hallway
is added that connects 1 and 2. Show a Trémaux exploration of the resulting
maze, in the style of Figures 18.2 and 18.3.

◦ 18.2 Which of the following could not be the order in which lights are turned
on at the intersections during a Trémaux exploration of the maze depicted in
Figures 18.2 and 18.3?
0-7-4-5-3-1-6-2
0-2-6-4-3-7-1-5
0-5-3-4-7-1-6-2
0-7-4-6-2-1-3-5

• 18.3 How many different ways are there to traverse the maze depicted in
Figures 18.2 and 18.3 with a Trémaux exploration?

18.2 Depth-First Search


Our interest in Trémaux exploration is that this technique leads us
immediately to the classic recursive function for traversing graphs: To
visit a vertex, we mark it as having been visited, then (recursively)
visit all the vertices that are adjacent to it and that have not yet been
marked. This method, which we considered briefly in Chapters 3 and 5
and used to solve path problems in Section 17.7, is called depth-first
search (DFS). It is one of the most important algorithms that we shall
encounter. DFS is deceptively simple because it is based on a familiar
concept and is easy to implement; in fact, it is a subtle and powerful
algorithm that we put to use for numerous difficult graph-processing
tasks.
Program 18.1 is a DFS implementation that visits all the vertices
and examines all the edges in a connected graph. It uses an adjacency-
matrix representation. As usual, the corresponding function for the
adjacency-lists representation differs only in the mechanics of accessing
the edges (see Program 18.2). Like the simple path-search functions
that we considered in Section 17.7, the implementation is based on a
recursive function that uses a global array and an incrementing counter
to mark vertices by recording the order in which they are visited.
Figure 18.5 is a trace that shows the order in which Program 18.1
visits the edges and vertices for the example depicted in Figures 18.2
82 §18.2 CHAPTER EIGHTEEN

0 2
Program 18.1 Depth-first search (adjacency-matrix)
6 This code is intended for use with a generic graph-search ADT function
1 7 that initializes a counter cnt to 0 and all of the entries in the vertex-
3 indexed array pre to -1, then calls search once for each connected com-
ponent (see Program 18.3), assuming that the call search(G, EDGE(v,
5 4 v)) marks all vertices in the same connected component as v (by setting
their pre entries to be nonnegative).
Here, we implement search with a recursive function dfsR that
01234567 visits all the vertices connected to e.w by scanning through its row in
0-0 0******* the adjacency matrix and calling itself for each edge that leads to an
0-2 0*1***** unmarked vertex.
2-0
2-6 0*1***2* #define dfsR search
6-2 void dfsR(Graph G, Edge e)
6-4 0*1*3*2* { int t, w = e.w;
4-3 0*143*2*
3-4
pre[w] = cnt++;
3-5 0*14352* for (t = 0; t < G->V; t++)
5-0 if (G->adj[w][t] != 0)
5-3 if (pre[t] == -1)
5-4
dfsR(G, EDGE(w, t));
4-5
4-6 }
4-7 0*143526
7-0
7-1 07143526 and 18.3 (see also Figure 18.17). Figure 18.6 depicts the same process
1-7 using standard graph drawings.
7-4
0-5 These figures illustrate the dynamics of a recursive DFS and show
0-7 the correspondence with Trémaux exploration of a maze. First, the
vertex-indexed array corresponds to the lights in the intersections:
Figure 18.5
DFS trace When we encounter an edge to a vertex that we have already visited
(see a light at the end of the passage), we do not make a recursive
This trace shows the order in
which DFS checks the edges and call to follow that edge (go down that passage). Second, the function
vertices for the adjacency-matrix call–return mechanism in the program corresponds to the string in
representation of the graph corre- the maze: When we have processed all the edges adjacent to a vertex
sponding to the example in Fig- (explored all the passages leaving an intersection), we “return” (in
ures 18.2 and 18.3 (top) and traces
the contents of the pre array (right) both senses of the word).
as the search progresses (asterisks In the same way that we encounter each passage in the maze
represent -1, for unseen vertices). twice (once at each end), we encounter each edge in the graph twice
There are two lines in the trace for
(once at each of its vertices). In Trémaux exploration, we open the
every graph edge (once for each
orientation). Indentation indicates doors at each end of each passage; in DFS of an undirected graph, we
the level of recursion. check each of the two representations of each edge. If we encounter
GRAPH SEARCH §18.2 83

Figure 18.6
Depth-first search
0 2 0 2 0
0 These diagrams are a graphical
2
6 6
view of the process depicted in
1 7 1 7 6 Figure 18.5, showing the DFS
4
recursive-call tree as it evolves.
3 3
Thick black edges in the graph cor-
3 respond to edges in the DFS tree
5 4 5 4
shown to the right of each graph
diagram. Shaded edges are the
candidates to be added to the tree
next. In the early stages (left) the
tree grows down in a straight line,
0 as we make recursive calls for 0,
0 2 0 0 2
2, 6, and 4 (left). Then we make
2
6
2
6
recursive calls for 3, then 5 (right,
1 7 1 7
6 top two diagrams); and return from
4 those calls to make a recursive call
3 3
for 7 from 4 (right, second from
3
5 4 5 4 bottom) and to 1 from 7 (right, bot-
5 tom).

0
0 2 0 0 2
2
2
6 6
6
1 7 6 1 7
4
3 3
3 7
5 4 5 4
5

0
0 2 0 0 2
2
2
6 6
6
1 7 6 1 7
4
3 4 3
3 7
5 4 5 4
5 1
84 §18.2 CHAPTER EIGHTEEN

an edge v-w, we either do a recursive call (if w is not marked) or skip


the edge (if w is marked). The second time that we encounter the
edge, in the opposite orientation w-v, we always ignore it, because the
destination vertex v has certainly already been visited (the first time
that we encountered the edge).
One difference between DFS as implemented in Program 18.1
and Trémaux exploration as depicted in Figures 18.2 and 18.3, al-
though it is inconsequential in many contexts, is worth taking the time
to understand. When we move from vertex v to vertex w, we have not
examined any of the entries in the adjacency matrix that correspond
to edges from w to other vertices in the graph. In particular, we know
that there is an edge from w to v and that we will ignore that edge when
we get to it (because v is marked as visited). That decision happens
at a time different from in the Trémaux exploration, where we open
the doors corresponding to the edge from v to w when we go to w for
the first time, from v. If we were to close those doors on the way in
and open them on the way out (having identified the passage with the
string), then we would have a precise correspondence between DFS
and Trémaux exploration.
We pass an edge to the recursive procedure, instead of passing
its destination vertex, because the edge tells us how we reached the
vertex. Knowing the edge corresponds to knowing which passage led
to a particular intersection in a maze. This information is useful in
many DFS functions. When we are simply keeping track of which
vertices we have visited, this information is of little consequence; but
more interesting problems require that we always know from whence
we came.
Figure 18.6 also depicts the tree corresponding to the recursive
calls as it evolves, in correspondence with Figure 18.5. This recursive-
call tree, which is known as the DFS tree, is a structural description of
the search process. As we see in Section 18.4, the DFS tree, properly
augmented, can provide a full description of the search dynamics, in
addition to just the call structure.
The same basic scheme is effective for an adjacency-lists graph
representation, as illustrated in Program 18.2. As usual, instead of
scanning a row of an adjacency matrix to find the vertices adjacent to a
given vertex, we scan through its adjacency list. As before, we traverse
(via a recursive call) all edges to vertices that have not yet been visited.
GRAPH SEARCH §18.2 85

Program 18.2 Depth-first search (adjacency-lists)


This implementation of dfsR is DFS for graphs represented with ad-
jacency lists. The algorithm is the same as for the adjacency-matrix 0
7 5 2
representation (Program 18.1): to visit a vertex, we mark it and then 1
7
scan through its incident edges, making a recursive call whenever we 2
0 6
encounter an edge to an unmarked vertex.
3
5 4
void dfsR(Graph G, Edge e) 4
6 5 7 3
{ link t; int w = e.w; 5
0 4 3
pre[w] = cnt++; 6
4 2
for (t = G->adj[w]; t != NULL; t = t->next) 7
1 0 4
if (pre[t->v] == -1)
dfsR(G, EDGE(w, t->v));
}
01234567
0-0 0*******
The DFS in Program 18.2 ignores self-loops and duplicate edges if they 0-7 0******1
are present, so that we do not necessarily have to take the trouble to 7-1 02*****1
remove them from the adjacency lists. 1-7
7-0
For the adjacency-matrix representation, we examine the edges 7-4 02**3**1
incident on each vertex in numerical order; for the adjacency-lists 4-6 02**3*41
representation, we examine them in the order that they appear on the 6-4
lists. This difference leads to a different recursive search dynamic, as 6-2 025*3*41
2-0
illustrated in Figure 18.7. The order in which DFS discovers the edges 2-6
and vertices in the graph depends entirely on the order in which the 4-5 025*3641
edges appear in the adjacency lists in the graph representation. We 5-0
might also consider examining the entries in each row in the adjacency 5-4
5-3 02573641
matrix in some different order, which would lead to another different 3-5
search dynamic (see Exercise 18.5). 3-4
Despite all of these possibilities, the critical fact remains that DFS 4-7
4-3
visits all the edges and all the vertices connected to the start vertex,
0-5
regardless of in what order it examines the edges incident on each 0-2
vertex. This fact is a direct consequence of Property 18.1, since the
proof of that property does not depend on the order in which the doors Figure 18.7
DFS trace (adjacency lists)
are opened at any given intersection. All the DFS-based algorithms that
we examine have this same essential property. Although the dynamics This trace shows the order in
which DFS checks the edges and
of their operation might vary substantially depending on the graph vertices for the adjacency-lists rep-
representation and details of the implementation of the search, the resentation of the same graph as in
recursive structure gives us a way to make relevant inferences about Figure 18.5.
86 §18.3 CHAPTER EIGHTEEN

the graph itself, no matter how it is represented and no matter which


order we choose to examine the edges incident upon each vertex.
Exercises
 18.4 Show, in the style of Figure 18.5, a trace of the recursive function calls
made for a standard adjacency-matrix DFS of the graph
0-2 0-5 1-2 3-4 4-5 3-5.
Draw the corresponding DFS recursive-call tree.
 18.5 Show, in the style of Figure 18.6, the progress of the search if the search
function is modified to scan the vertices in reverse order (from V-1 down to
0).
◦ 18.6 Implement DFS using your representation-independent ADT function
for processing edge lists from Exercise 17.60.

18.3 Graph-Search ADT Functions


DFS and the other graph-search methods that we consider later in this
chapter all involve following graph edges from vertex to vertex, with
the goal of systematically visiting every vertex and every edge in the
graph. But following graph edges from vertex to vertex can lead us to
all the vertices in only the same connected component as the starting
vertex. In general, of course, graphs may not be connected, so we
need one call on a search function for each connected component. We
will typically use a generic graph-search function that performs the
following steps until all of the vertices of the graph have been marked
as having been visited:
• Find an unmarked vertex (a start vertex).
• Visit (and mark as visited) all the vertices in the connected com-
ponent that contains the start vertex.
The method for marking vertices is not specified in this description,
but we most often use the same method that we used for the DFS
implementations in Section 18.2: We initialize all entries in a global
vertex-indexed array to a negative integer, and mark vertices by setting
their corresponding entry to a positive value. Using this procedure
corresponds to using a single bit (the sign bit) for the mark; most
implementations are also concerned with keeping other information
associated with marked vertices in the array (such as, for the DFS im-
plementations in Section 18.2, the order in which vertices are marked).
GRAPH SEARCH §18.3 87

Program 18.3 Graph search


We typically use code like this to process graphs that may not be con-
10
nected. The function GRAPHsearch assumes that search, when called
6 11 8
with a self-loop to v as its second argument, sets the pre entries corre-
7 2
sponding to each vertex connected to v to nonnegative values. Under this
assumption, this implementation calls search once for each connected 3 9 0
component in the graph—we can use it for any graph representation 12
and any search implementation. 5 1 4
Together with Program 18.1 or Program 18.2, this code com-
putes the order in which vertices are visited in pre. Other DFS-based im-
plementations do other computations, but use the same general scheme 0 1 2 3 4 5 6 7 8 9 10 11 12
of interpreting nonnegative entries in a vertex-indexed array as vertex * * * * * * * * * * * * *
marks. 0-0 0 0 * * 0 * * * * 0 * * *
2-2 0 0 0 0 0 0 0 0 * 0 0 * 0
static int cnt, pre[maxV]; 8-8 0 0 0 0 0 0 0 0 0 0 0 0 0
void GRAPHsearch(Graph G)
{ int v; Figure 18.8
cnt = 0; Graph search
for (v = 0; v < G->V; v++) pre[v] = -1; The table at the bottom shows ver-
for (v = 0; v < G->V; v++) tex marks (contents of the pre ar-
if (pre[v] == -1) ray) during a typical search of the
graph at the top. Initially, the func-
search(G, EDGE(v, v)); tion GRAPHsearch in Program 18.3
} unmarks all vertices by setting the
marks all to -1 (indicated by an
asterisk). Then it calls search for
The method for looking for a vertex in the next connected component the dummy edge 0-0, which marks
all of the vertices in the same con-
is also not specified, but we most often use a scan through the array in nected component as 0 (second
order of increasing index. row) by setting them to a non-
The GRAPHsearch ADT function in Program 18.3 is an implemen- negative values (indicated by 0s).
In this example, it marks 0, 1, 4,
tation that illustrates these choices. Figure 18.8 illustrates the effect
and 9 with the values 0 through 3
on the pre array of this function, in conjunction with Program 18.1 in that order. Next, it scans from
or Program 18.2 (or any graph-search function that marks all of the left to right to find the unmarked
vertices in the same connected component as its argument). The graph- vertex 2 and calls search for
the dummy edge 2-2 (third row),
search functions that we consider also examine all edges indicent upon
which marks the seven vertices in
each vertex visited, so knowing that we visit all vertices tells us that the same connected component
we visit all edges as well, as in Trémaux traversal. as 2. Continuing the left-to-right
In a connected graph, the ADT function is nothing more than scan, it calls search for 8-8 to
mark 8 and 11 (bottom row). Fi-
a wrapper that calls search once for 0-0 and then finds that all the nally, GRAPHsearch completes
other vertices are marked. In a graph with more than one connected the search by discovering that 9
component, the ADT function checks all the connected components through 12 are all marked.
88 §18.3 CHAPTER EIGHTEEN

in a straightforward manner. DFS is the first of several methods


that we consider for searching a connected component. No matter
which method (and no matter what graph representation) we use,
Program 18.3 is an effective method for visiting all the graph vertices.
Property 18.2 A graph-search function checks each edge and marks
each vertex in a graph if and only if the search function that it uses
marks each vertex and checks each edge in the connected component
that contains the start vertex.
Proof : By induction on the number of connected components.
Graph-search functions provide a systematic way of processing
each vertex and each edge in a graph. Generally, our implementations
are designed to run in linear or near-linear time, by doing a fixed
amount of processing per edge. We prove this fact now for DFS,
noting that the same proof technique works for several other search
strategies.
Property 18.3 DFS of a graph represented with an adjacency matrix
requires time proportional to V 2 .
Proof : An argument similar to the proof of Property 18.1 shows that
dfsR not only marks all vertices connected to the start vertex, but also
calls itself exactly once for each such vertex (to mark that vertex). An
argument similar to the proof of Property 18.2 shows that a call to
GRAPHsearch leads to exactly one call to dfsR for each graph vertex.
In dfsR, we check every entry in the vertex’s row in the adjacency
matrix. In other words, we check each entry in the adjacency matrix
precisely once.
Property 18.4 DFS of a graph represented with adjacency lists re-
quires time proportional to V + E.
Proof : From the argument just outlined, it follows that we call the re-
cursive function precisely V times (hence the V term), and we examine
each entry on each adjacency list (hence the E term).
The primary implication of Properties 18.3 and 18.4 is that they
establish the running time of DFS to be linear in the size of the data
structure used to represent the graph. In most situations, we are also
justified in thinking of the running time of DFS as being linear in the
size of the graph, as well: If we have a dense graph (with the number of
GRAPH SEARCH §18.3 89

edges proportional to V 2 ) then either representation gives this result;


if we have a sparse graph, then we assume use of an adjacency-lists
representation. Indeed, we normally think of the running time of DFS
as being linear in E. That statement is technically not true if we are
using adjacency matrices for sparse graphs or for extremely sparse
graphs with E << V and most vertices isolated, but we can usually
avoid the former situation and we can remove isolated vertices (see
Exercise 17.33) in the latter situation.
As we shall see, these arguments all apply to any algorithm that
has a few of the same essential features of DFS. If the algorithm marks
each vertex and examines all the latter’s incident vertices (and does
any other work that takes time per vertex bounded by a constant),
then these properties apply. More generally, if the time per vertex
is bounded by some function f (V, E), then the time for the search
is guaranteed to be proportional to E + V f (V, E). In Section 18.8,
we see that DFS is one of a family of algorithms that has just these
characteristics; in Chapters 19 through 22, we see that algorithms from
this family serve as the basis for a substantial fraction of the code that
we consider in this book.
Much of the graph-processing code that we examine is ADT-
implementation code for some particular task, where we extend a
basic search to compute structural information in other vertex-indexed
arrays. In essence, we re-implement the search each time that we
build an algorithm implementation around it. We adopt this approach
because many of our algorithms are best understood as augmented
graph-search functions. Typically, we uncover a graph’s structure by
searching it. We normally extend the search function with code that
is executed when each vertex is marked, instead of working with a
more generic search (for example, one that calls a specified function
each time a vertex is visited), solely to keep the code compact and self-
contained. Providing a more general ADT mechanism for clients to
process all the vertices with a client-supplied function is a worthwhile
exercise (see Exercises 18.12 and 18.13).
In Sections 18.5 and 18.6, we examine numerous graph-
processing functions that are based on DFS in this way. In Sections 18.7
and 18.8, we look at other implementations of search and at some
graph-processing functions that are based on them. Although we do
not build this layer of abstraction into our code, we take care to iden-
90 §18.3 CHAPTER EIGHTEEN

tify the basic graph-search strategy underlying each algorithm that


we develop. For example, we use the term DFS function to refer to
any implementation that is based on the recursive DFS scheme. The
simple-path–search function Program 17.11 is an example of a DFS
function.
Many graph-processing functions are based on the use of vertex-
indexed arrays. We typically include such arrays in one of three places
in implementations:
• As global variables
• In the graph representation
• As function parameters, supplied by the client
We use the first alternative when gathering information about the
search to learn facts about the structure of graphs that help us solve
various problems of interest. An example of such an array is the
pre array used in Programs 18.1 through 18.3. We use the second
alternative when implementing preprocessing functions that compute
information about the graph that enable efficient implementation of
associated ADT functions. For example, we might maintain such
an array to support an ADT function that returns the degree of any
given vertex. When implementing ADT functions whose purpose is to
compute a vertex-indexed array, we might use either the second or the
third alternative, depending on the context.
Our convention in graph-search functions is to initialize all en-
tries in vertex-indexed arrays to -1, and to set the entries corresponding
to each vertex visited to nonnegative values in the search function. Any
such array can play the role of the pre array (marking vertices as vis-
ited) in Programs 18.1 through 18.3. When a graph-search function is
based on using or computing a vertex-indexed array, we use that array
to mark vertices, rather than bothering with the pre array.
The specific outcome of a graph search depends not just on the
nature of the search function, but also on the graph representation
and even the order in which GRAPHsearch examines the vertices. For
the examples and exercises in this book, we use the term standard
adjacency-lists DFS to refer to the process of inserting a sequence of
edges into a graph ADT implemented with an adjacency-lists represen-
tation (Program 17.6), then doing a DFS with Programs 18.3 and 18.2.
For the adjacency-matrix representation, the order of edge insertion
does not affect search dynamics, but we use the parallel term standard
GRAPH SEARCH §18.4 91

adjacency-matrix DFS to refer to the process of inserting a sequence


of edges into a graph ADT implemented with an adjacency-matrix
representation (Program 17.3), then doing a DFS with Programs 18.3
and 18.1.
Exercises
18.7 Show, in the style of Figure 18.5, a trace of the recursive function calls
made for a standard adjacency-matrix DFS of the graph
3-7 1-4 7-8 0-5 5-2 3-8 2-9 0-6 4-9 2-6 6-4.

18.8 Show, in the style of Figure 18.7, a trace of the recursive function calls
made for a standard adjacency-lists DFS of the graph
3-7 1-4 7-8 0-5 5-2 3-8 2-9 0-6 4-9 2-6 6-4.

18.9 Modify the adjacency-matrix graph ADT implementation in Pro-


gram 17.3 to use a dummy vertex that is connected to all the other vertices.
Then, provide a simplified DFS implementation that takes advantage of this
change.
18.10 Do Exercise 18.9 for the adjacency-lists ADT implementation in Pro-
gram 17.6.
• 18.11 There are 13! different permutations of the vertices in the graph depicted
in Figure 18.8. How many of these permutations could specify the order in
which vertices are visited by Program 18.3?
18.12 Define an ADT function that calls a client-supplied function for each
vertex in the graph. Provide implementations for the adjacency-matrix and
the adjacency-lists graph representations.
18.13 Define an ADT function that calls a client-supplied function for each
edge in the graph. Provide implementations for the adjacency-matrix and the
adjacency-lists graph representations. (Such a function might be a reasonable
alternative to GRAPHedges in an ADT design.)

18.4 Properties of DFS Forests


As noted in Section 18.2, the trees that describe the recursive struc-
ture of DFS function calls give us the key to understanding how DFS
operates. In this section, we examine properties of the algorithm by
examining properties of DFS trees.
The pre array in our DFS implementations is the preorder num-
bering of the internal nodes of the DFS tree. It is also easy to compute
an explicit parent-link representation of the DFS tree: We initialize all
92 §18.4 CHAPTER EIGHTEEN

Figure 18.9 0 0 0
DFS tree representations
2 5 7 2 2
If we augment the DFS recursive-
call tree to represent edges that are 0 6 6 6
checked but not followed, we get
a complete description of the DFS 2 4 4 4
process (left). Each tree node has
3 5 6 7 3 7 3 7
a child representing each of the
nodes adjacent to it, in the order 4 5 0 1 4 5 0 1 5 1
they were considered by the DFS,
and a preorder traversal gives the 0 3 4 7 0 4

same information as Figure 18.5:


0 1 2 3 4 5 6 7
first we follow 0-0, then 0-2, then
pre 0 7 1 4 3 5 2 6
we skip 2-0, then we follow 2-6, st 0 7 0 4 6 3 2 4
then we skip 6-2, then we follow
6-4, then 4-3, and so forth. The
pre array specifies the order in
which we visit tree vertices during entries of a vertex-indexed array st to -1 and include the statement
this preorder walk, which is the
same as the order in which we visit
st[e.w] = e.v at the beginning of the recursive DFS function dfsR
graph vertices in the DFS. The st in Programs 18.1 and 18.2.
array is a parent-link representation If we add external nodes to the DFS tree to record the moments
of the DFS recursive-call tree (see
when we skipped recursive calls for vertices that had already been
Figure 18.6).
There are two links in the visited, we get the compact representation of the dynamics of DFS il-
tree for every edge in the graph, lustrated in Figure 18.9. This representation is worthy of careful study.
one for each of the two times it en- The tree is a representation of the graph, with a vertex corresponding
counters the edge. The first is to an
to each graph vertex and an edge corresponding to each graph edge.
unshaded node and either corre-
sponds to making a recursive call We can choose to show the two representations of the edge that we
(if it is to an internal node) or to process (one in each direction), as shown in the left part of the figure,
skipping a recursive call because it or just one representation of each edge, as shown in the center and
goes to an ancestor for which a re-
right parts of the figure. The former is useful in understanding that
cursive call is in progress (if it is to
an external node). The second is to the algorithm processes each and every edge; the latter is useful in un-
a shaded external node and always derstanding that the DFS tree is simply another graph representation.
corresponds to skipping a recursive Traversing the internal nodes of the tree in preorder gives the vertices
call, either because it goes back
to the parent (circles) or because
in the order in which DFS visits them; moreover, the order in which
it goes to a descendent of the par- we visit the edges of the tree as we traverse it in preorder is the same
ent for which a recursive call is in as the order in which DFS examines the edges of the graph.
progress (squares). If we eliminate
Indeed, the DFS tree in Figure 18.9 contains the same information
shaded nodes (center), then replace
the external nodes with edges, we as the trace in Figure 18.5 or the step-by-step illustration of Trémaux
get another drawing of the graph traversal in Figures 18.2 and 18.3. Edges to internal nodes represent
(right). edges (passages) to unvisited vertices (intersections), edges to external
nodes represent occasions where DFS checks edges that lead to previ-
GRAPH SEARCH §18.4 93

ously visited vertices (intersections), and shaded nodes represent edges


to vertices for which a recursive DFS is in progress (when we open a
door to a passage where the door at the other end is already open).
With these interpretations, a preorder traversal of the tree tells the
same story as that of the detailed maze-traversal scenario.
To study more intricate graph properties, we classify the edges
in a graph according to the role that they play in the search. We have
two distinct edge classes:
• Edges representing a recursive call (tree edges)
• Edges connecting a vertex with an ancestor in its DFS tree that is
not its parent (back edges)
When we study DFS trees for digraphs in Chapter 19, we examine
other types of edges, not just to take the direction into account, but
also because we can have edges that go across the tree, connecting
nodes that are neither ancestors nor descendants in the tree.
Since there are two representations of each graph edge that each
correspond to a link in the DFS tree, we divide the tree links into four
classes. We refer to a link from v to w in a DFS tree that represents a
tree edge as
• A tree link if w is unmarked
• A parent link if st[w] is v
and a link from v to w that represents a back edge as
• A back link if pre[w] < pre[v]
• A down link if pre[w] > pre[v]
Each tree edge in the graph corresponds to a tree link and a parent
link in the DFS tree, and each back edge in the graph corresponds to a
back link and a down link in the DFS tree.
In the graphical DFS representation illustrated in Figure 18.9, tree
links point to unshaded circles, parent links point to shaded circles,
back links point to unshaded squares, and down links point to shaded
squares. Each graph edge is represented either as one tree link and one
parent link or as one down link and one back link. These classifications
are tricky and worthy of study. For example, note that even though
parent links and back links both point to ancestors in the tree, they are
quite different: A parent link is just the other representation of a tree
link, but a back link gives us new information about the structure of
the graph.
94 §18.4 CHAPTER EIGHTEEN

The definitions just given provide sufficient information to dis-


tinguish among tree, parent, back, and down links in a DFS function
implementation. Note that parent links and back links both have
pre[w] < pre[v], so we have also to know that st[w] is not v to
know that v-w is a back link. Figure 18.10 depicts the result of print-
ing out the classification of the DFS tree link for each graph edge as
that edge is encountered during a sample DFS. It is yet another com-
plete representation of the basic search process that is an intermediate
step between Figure 18.5 and Figure 18.9.
0-0 tree The four types of tree links correspond to the four different
0-2 tree ways in which we treat edges during a DFS, as described (in maze-
2-0 parent
2-6 tree exploration terms) at the end of Section 18.1. A tree link corresponds
6-2 parent to DFS encountering the first of the two representations of a tree edge,
6-4 tree leading to a recursive call (to as-yet-unseen vertices); a parent link
4-3 tree corresponds to DFS encountering the other representation of the tree
3-4 parent
3-5 tree edge (when going through the adjacency list on that first recursive call)
5-0 back and ignoring it. A back link corresponds to DFS encountering the first
5-3 parent of the two representations of a back edge, which points to a vertex
5-4 back for which the recursive search function has not yet completed; a down
4-5 down
4-6 parent link corresponds to DFS encountering a vertex for which the recursive
4-7 tree search has completed at the time that the edge is encountered. In Fig-
7-0 back ure 18.9, tree links and back links connect unshaded nodes, represent
7-1 tree the first encounter with the corresponding edge, and constitute a repre-
1-7 parent
7-4 parent sentation of the graph; parent links and down links go to shaded nodes
0-5 down and represent the second encounter with the corresponding edge.
0-7 down We have considered this tree representation of the dynamic char-
Figure 18.10 acteristics of recursive DFS in detail not just because it provides a com-
DFS trace (tree link classifica- plete and compact description of both the graph and the operation of
tions) the algorithm, but also because it gives us a basis for understanding
This version of Figure 18.5 shows numerous important graph-processing algorithms. In the remainder of
the classification of the DFS tree this chapter, and in the next several chapters, we consider a number of
link corresponding to each graph examples of graph-processing problems that draw conclusions about
edge representation encountered.
Tree edges (which correspond to a graph’s structure from the DFS tree.
recursive calls) are represented as Search in a graph is a generalization of tree traversal. When
tree links on the first encounter invoked on a tree, DFS is precisely equivalent to recursive tree traversal;
and parent links on the second en-
for graphs, using it corresponds to traversing a tree that spans the graph
counter. Back edges are back links
on the first encounter and down and that is discovered as the search proceeds. As we have seen, the
links on the second encounter. particular tree traversed depends on how the graph is represented. DFS
GRAPH SEARCH §18.4 95

0 7 9 Figure 18.11
DFS forest
1 2 5 6 8 10 11 12
The DFS forest at the top repre-
0 0 0 3 4 7 9 9 12
sents a DFS of an adjacency-matrix
4 5 9 11 representation of the graph at the
3 5 6
bottom right. The graph has three
connected components, so the
0 4
forest has three trees. The pre
array is a preorder numbering of
0 the nodes in the tree (the order in
6 7 8 0 1 2 3 4 5 6 7 8 9 10 11 12 which they are examined by the
1 2 pre 0 1 2 4 5 3 6 7 8 9 10 11 12 DFS) and the st array is a parent-
st 0 0 0 5 3 0 4 7 7 9 9 9 11 link representation of the forest.
3 9 10 cc 0 0 0 0 0 0 0 1 1 2 2 2 2
4 The cc array associates each ver-
5 11 12 tex with a connected-component
index (see Program 18.4). As in
Figure 18.9, edges to circles are
tree edges; edges that go to squares
corresponds to preorder tree traversal. In Section 18.6, we examine are back edges; and shaded nodes
the graph-searching analog to level-order tree traversal and explore indicate that the incident edge was
encountered earlier in the search,
its relationship to DFS; in Section 18.7, we examine a general schema
in the other direction.
that encompasses any traversal method.
When traversing graphs, we have been assigning preorder num-
bers to the vertices in the order that we start processing them (just after
entering the recursive search function). We can also assign postorder
numbers to vertices, in the order that we finish processing them (just
before returning from the recursive search function). When processing
a graph, we do more than simply traverse the vertices—as we shall
see, the preorder and postorder numbering give us knowledge about
global graph properties that helps us to accomplish the task at hand.
Preorder numbering suffices for the algorithms that we consider in this
chapter, but we use postorder numbering in later chapters.
We describe the dynamics of DFS for a general undirected graph
with a DFS forest that has one DFS tree for each connected component.
An example of a DFS forest is illustrated in Figure 18.11.
With an adjacency-lists representation, we visit the edges con-
nected to each vertex in an order different from that for the adjacency-
matrix representation, so we get a different DFS forest, as illustrated
in Figure 18.12. DFS trees and forests are graph representations that
describe not only the dynamics of DFS but also the internal representa-
tion of the graph. For example, by reading the children of any node in
Figure 18.12 from left to right, we see the order in which they appear
96 §18.4 CHAPTER EIGHTEEN

Figure 18.12 0 7 9
Another DFS forest
5 2 1 6 8 11 10 12
This forest describes depth-first
search of the same graph as Fig- 0 4 3 0 0 7 9 12 9

ure 18.9, but using Program 18.2,


6 5 3 9 11
so the search order is different be-
cause it is determined by the order 4 0 5 4
that nodes appear in adjacency
lists. Indeed, the forest itself tells 0 1 2 3 4 5 6 7 8 9 10 11 12
us that order: it is the order in pre 0 6 5 4 2 1 3 7 8 9 12 10 11
which children are listed for each st 0 0 0 4 5 0 4 7 7 9 9 9 11
cc 0 0 0 0 0 0 0 1 1 2 2 2 2
node in the tree. For instance, the
nodes on 0’s adjacency list were
found in the order 5 2 1 6, the
nodes on 4’s list are in the order on the adjacency list of the vertex corresponding to that node. We can
6 5 3, and so forth. As before, all
have many different DFS forests for the same graph—each ordering of
vertices and edges in the graph are
examined during the search, in a nodes on adjacency lists leads to a different forest.
manner that is precisely described Details of the structure of a particular forest inform our under-
by a preorder walk of the tree. The standing of how DFS operates for particular graphs, but most of the
pre and st arrays depend upon
important DFS properties that we consider depend on graph properties
the graph representation and the
search dynamics and are different that are independent of the structure of the forest. For example, the
from Figure 18.9, but the array cc forests in Figures 18.11 and 18.12 both have three trees (as would any
depends on graph properties and is other DFS forest for the same graph) because they are just different
the same. representations of the same graph, which has three connected compo-
nents. Indeed, a direct consequence of the basic proof that DFS visits
all the nodes and edges of a graph (see Properties 18.2 through 18.4) is
that the number of connected components in the graph is equal to the
number of trees in the DFS forest. This example illustrates the basis
for our use of graph search throughout the book: A broad variety
of graph ADT function implementations are based on learning graph
properties by processing a particular graph representation (a forest
corresponding to the search).
Potentially, we could analyze DFS tree structures with the goal of
improving algorithm performance. For example, should we attempt to
speed up an algorithm by rearranging the adjacency lists before starting
the search? For many of the important classical DFS-based algorithms,
the answer to this question is no, because they are optimal—their
worst-case running time depends on neither the graph structure nor
the order in which edges appear on the adjacency lists (they essentially
process each edge exactly once). Still, DFS forests have a characteristic
structure that is worth understanding because it distinguishes them
GRAPH SEARCH §18.4 97

from other fundamental search schema that we consider later in this


chapter.
Figure 18.13 shows a DFS tree for a larger graph that illustrates
the basic characteristics of DFS search dynamics. The tree is tall and
thin, and it demonstrates several characteristics of the graph being
searched and of the DFS process:
• There exists at least one long path that connects a substantial
fraction of the nodes.
• During the search, most vertices have at least one adjacent vertex
that we have not yet seen.
• We rarely make more than one recursive call from any node.
• The depth of the recursion is proportional to the number of
vertices in the graph.
This behavior is typical for DFS, though these characteristics are not
guaranteed for all graphs. Verifying facts of this kind for graph models
of interest and various types of graphs that arise in practice requires
detailed study. Still, this example gives an intuitive feel for DFS-based
algorithms that is often borne out in practice. Figure 18.13 and similar
figures for other graph-search algorithms (see Figures 18.24 and 18.29)
help us understand differences in their behavior.
Exercises
18.14 Draw the DFS forest that results from a standard adjacency-matrix DFS
of the graph
3-7 1-4 7-8 0-5 5-2 3-8 2-9 0-6 4-9 2-6 6-4.

18.15 Draw the DFS forest that results from a standard adjacency-lists DFS
of the graph
3-7 1-4 7-8 0-5 5-2 3-8 2-9 0-6 4-9 2-6 6-4.

18.16 Write a DFS trace program for the adjacency-lists representation to


produce output that classifies each of the two representations of each graph
edge as corresponding to a tree, parent, back, or down link in the DFS tree, in
the style of Figure 18.10.
◦ 18.17 Write a program that computes a parent-link representation of the full
DFS tree (including the external nodes), using an array of E integers between
0 and V − 1. Hint: The first V entries in the array should be the same as those
in the st array described in the text.
◦ 18.18 Instrument our DFS implementation for the adjacency-lists representa-
tion (Program 18.2) to print out the height of the DFS tree, the number of
back edges, and the percentage of edges processed to see every vertex.
98 §18.4 CHAPTER EIGHTEEN

Figure 18.13
Depth-first search
This figure illustrates the progress
of DFS in a random Euclidean
near-neighbor graph (left). The
figures show the DFS tree ver-
tices and edges in the graph as the
search progresses through 1/4, 1/2,
3/4, and all of the vertices (top to
bottom). The DFS tree (tree edges
only) is shown at the right. As is
evident from this example, the
search tree for DFS tends to be
quite tall and thin for this type of
graph (as it is for many other types
of graphs commonly encountered
in practice). We normally find a
vertex nearby that we have not
seen before.
GRAPH SEARCH §18.5 99

• 18.19 Run experiments to determine empirically the average values of the


quantities described in Exercise 18.18 for graphs of various sizes, drawn from
various graph models (see Exercises 17.63–76).
• 18.20 Write a function that builds a graph by inserting edges from a given
array into an initially empty graph, in random order. Using this function
with an adjacency-lists implementation of the graph ADT, run experiments
to determine empirically properties of the distribution of the quantities de-
scribed in Exercise 18.18 for all the adjacency-lists representations of large
sample graphs of various sizes, drawn from various graph models (see Exer-
cises 17.63–76).

18.5 DFS Algorithms


Regardless of the graph structure or the representation, any DFS forest
allows us to identify edges as tree or back edges and gives us dependable
insights into graph structure that allow us to use DFS as a basis for
solving numerous graph-processing problems. We have already seen,
in Section 17.7, basic examples related to finding paths. In this section,
we consider DFS-based ADT function implementations for these and
other typical problems; in the remainder of this chapter and in the
next several chapters, we look at numerous solutions to much more
difficult problems.
Cycle detection Does a given graph have any cycles? (Is the
graph a forest?) This problem is easy to solve with DFS because any
back edge in a DFS tree belongs to a cycle consisting of the edge plus
the tree path connecting the two nodes (see Figure 18.9). Thus, we can
use DFS immediately to check for cycles: A graph is acyclic if and only
if we encounter no back (or down!) edges during a DFS. For example,
to test this condition in Program 18.1, we simply add an else clause
to the if statement to test whether t is equal to v. If it is, we have just
encountered the parent link w-v (the second representation of the edge
v-w that led us to w). If it is not, w-t completes a cycle with the edges
from t down to w in the DFS tree. Moreover, we do not need to examine
all the edges: We know that we must find a cycle or finish the search
without finding one before examining V edges, because any graph
with V or more edges must have a cycle. Thus, we can test whether
a graph is acyclic in time proportional to V with the adjacency-lists
representation, although we may need time proportional to V 2 (to find
the edges) with the adjacency-matrix representation.
100 §18.5 CHAPTER EIGHTEEN

Program 18.4 Graph connectivity (adjacency lists)


The DFS function GRAPHcc computes, in linear time, the number of con-
nected components in a graph and stores a component index associated
with each vertex in the vertex-indexed array G->cc in the graph repre-
sentation. (Since it does not need structural information, the recursive
function uses a vertex as its second argument instead of an edge as in
Program 18.2.) After calling GRAPHcc, clients can test whether any pair
of vertices are connected in constant time (GRAPHconnect).

void dfsRcc(Graph G, int v, int id)


{ link t;
G->cc[v] = id;
for (t = G->adj[v]; t != NULL; t = t->next)
if (G->cc[t->v] == -1) dfsRcc(G, t->v, id);
}
int GRAPHcc(Graph G)
{ int v, id = 0;
G->cc = malloc(G->V * sizeof(int));
for (v = 0; v < G->V; v++)
G->cc[v] = -1;
for (v = 0; v < G->V; v++)
if (G->cc[v] == -1) dfsRcc(G, v, id++);
return id;
}
int GRAPHconnect(Graph G, int s, int t)
{ return G->cc[s] == G->cc[t]; }

Simple path Given two vertices, is there a path in the graph


that connects them? We saw in Section 17.7 that a DFS function that
can solve this problem in linear time is easy to devise.
Simple connectivity As discussed in Section 18.3, we determine
whether or not a graph is connected whenever we use DFS, in linear
time. Indeed, our basic graph-search strategy is based upon calling a
search function for each connected component. In a DFS, the graph is
connected if and only if the graph-search function calls the recursive
DFS function just once (see Program 18.3). The number of connected
components in the graph is precisely the number of times that the recur-
sive function is called from GRAPHsearch, so we can find the number
GRAPH SEARCH §18.5 101

of connected components by simply keeping track of the number of


such calls.
More generally, Program 18.4 illustrates a DFS-based ADT
implementation for the adjacency-lists representation that supports
constant-time connectivity queries after a linear-time preprocessing
step. Each tree in the DFS forest identifies a connected component, so
we can arrange to decide quickly whether two vertices are in the same
component by including a vertex-indexed array in the graph represen-
tation, to be filled in by a DFS and accessed for connectivity queries. In
the recursive DFS function, we assign the current value of the compo-
nent counter to the entry corresponding to each vertex visited. Then,
we know that two vertices are in the same component if and only if
their entries in this array are equal. Again, note that this array reflects
structural properties of the graph, rather than artifacts of the graph
representation or of the search dynamics.
Program 18.4 typifies the basic approach that we shall use in
solving numerous graph-processing problems. We invest preprocess-
ing time and extend the graph ADT to compute structural graph prop-
erties that help us to provide efficient implementations of important
ADT functions. In this case, we preprocess with a (linear-time) DFS
and keep an array cc that allows us to answer connectivity queries
in constant time. For other graph-processing problems, we might use
more space, preprocessing time, or query time. As usual, our focus
is on minimizing such costs, although doing so is often challenging.
For example, much of Chapter 19 is devoted to solving the connec-
tivity problem for digraphs, where achieving the same performance
characteristics as Program 18.4 is an elusive goal.
How does the DFS-based solution for graph connectivity in Pro-
gram 18.4 compare with the union-find approach that we considered
in Chapter 1 for the problem of determining whether a graph is con-
nected, given an edge list? In theory, DFS is faster than union-find
because it provides a constant-time guarantee, which union- find does
not; in practice, this difference is negligible, and union-find is faster be-
cause it does not have to build a full representation of the graph. More
important, union-find is an online algorithm (we can check whether
two vertices are connected in near-constant time at any point), whereas
the DFS solution preprocesses the graph to answer connectivity queries
in constant time. Therefore, for example, we prefer union-find when
102 §18.5 CHAPTER EIGHTEEN

Program 18.5 Two-way Euler tour


This DFS function for the adjacency-matrix representation prints each
edge twice, once in each orientation, in a two-way–Euler-tour order. We
go back and forth on back edges and ignore down edges (see text).

void dfsReuler(Graph G, Edge e)


{ link t;
printf("-%d", e.w);
pre[e.w] = cnt++;
for (t = G->adj[e.w]; t != NULL; t = t->next)
if (pre[t->v] == -1)
dfsReuler(G, EDGE(e.w, t->v));
else if (pre[t->v] < pre[e.v])
printf("-%d-%d", t->v, e.w);
if (e.v != e.w)
printf("-%d", e.v);
else printf("\n");
}

determining connectivity is our only task or when we have a large num-


ber of queries intermixed with edge insertions but may find the DFS
solution more appropriate for use in a graph ADT because it makes
efficient use of existing infrastructure. Neither approach handles ef-
ficiently huge numbers of intermixed edge insertions, edge deletions,
and connectivity queries; both require a separate DFS to compute the
path. These considerations illustrate the complications that we face
when analyzing graph algorithms—we explore them in detail in Sec-
tion 18.9.
Two-way Euler tour Program 18.5 uses DFS to solve the prob-
lem of finding a path that uses all the edges in a graph exactly twice—
once in each direction (see Section 17.7). The path corresponds to a
Trémaux exploration in which we take our string with us everywhere
that we go, check for the string instead of using lights (so we have to
go down the passages that lead to intersections that we have already
visited), and first arrange to go back and forth on each back link (the
first time that we encounter each back edge), then ignore down links
(the second time that we encounter each back edge). We might also
GRAPH SEARCH §18.5 103

0 2
0 Figure 18.14
A two-way Euler tour
2 5 7

6 0 6
Depth-first search gives us a way
1 to explore any maze, traversing
7 2 4
both passages in each direction.
3 7 3 5 6
We modify Trémaux exploration
4 0 1 4 5 to take the string with us wherever
7 0 3 4 we go and take a back-and-forth
4
5 trip on passages without any string
0 1 2 3 4 5 6 7 in them that go to intersections
pre 0 5 1 6 3 7 2 4 that we have already visited. This
figure shows a different traversal
order than shown in Figures 18.2
and 18.3, primarily so that we
choose to ignore the back links (first encounter) and to go back and can draw the tour without cross-
forth on down links (second encounter) (see Exercise 18.23). ing itself. This ordering might re-
Spanning tree Given a connected graph with V vertices, find a sult, for example, if the edges were
processed in some different order
set of V − 1 edges that connects the vertices. DFS solves this problem when building an adjacency-lists
because any DFS tree is a spanning tree. Our DFS implementations representation of the graph, or,
make precisely V −1 recursive calls for a connected graph, one for each we might explicitly modify DFS
edge on a spanning tree, and can be easily instrumented to produce a to take the geometric placement
of the nodes into account (see Ex-
parent-link representation of the tree, as discussed at the beginning of ercise 18.24). Moving along the
Section 18.4. If the graph has C connected components, we get a span- lower track leading out of 0, we
ning forest with V − C edges. In an ADT function implementation, we move from 0 to 2 to 6 to 4 to 7,
might choose to add the parent-link array to the graph representation, then take a trip from 7 to 0 and
back because pre[0] is less than
in the style of Program 18.4, or to compute it in a client-supplied array. pre[7]. Then we go to 1, back
Vertex search How many vertices are in the same connected to 7, back to 4, to 3, to 5, from
component as a given vertex? We can solve this problem easily by start- 5 to 0 and back, from 5 to 4 and
back, back to 3, back to 4, back to
ing a DFS at the vertex and counting the number of vertices marked. In
6, back to 2, and back to 0. This
a dense graph, we can speed up the process considerably by stopping path may be obtained by a recur-
the DFS after we have marked V vertices—at that point, we know that sive pre- and postorder walk of the
no edge will take us to a vertex that we have not yet seen, so we will DFS tree (ignoring the shaded ver-
tices that represent the second time
be ignoring all the rest of the edges. This improvement is likely to
we encounter the edges) where we
allow us to visit all vertices in time proportional to V log V , not E (see print out the vertex name, recur-
Section 18.8). sively visit the subtrees, then print
Two-colorability, bipartiteness, odd cycle Is there a way to out the vertex name again.
assign one of two colors to each vertex of a graph such that no edge
connects two vertices of the same color? Is a given graph bipartite
(see Section 17.1)? Does a given graph have a cycle of odd length?
These three problems are all equivalent: The first two are different
nomenclature for the same problem; any graph with an odd cycle is
104 §18.5 CHAPTER EIGHTEEN

0 clearly not two-colorable, and Program 18.6 demonstrates that any


graph that is free of odd cycles can be two-colored. The program is a
2
DFS-based ADT function implementation that tests whether a graph is
6 bipartite, two-colorable, and free of odd cycles. The recursive function
4
is an outline for a proof by induction that the program two-colors any
graph with no odd cycles (or finds an odd cycle as evidence that a
3 7
graph that is not free of odd cycles cannot be two-colored). To two-
5 0 1 color a graph with a given color assigned to a vertex v, two-color the
remaining graph, assigning the other color to each vertex adjacent to
0 4
v. This process is equivalent to assigning alternate colors on levels as
we proceed down the DFS tree, checking back edges for consistency in
0
the coloring, as illustrated in Figure 18.15. Any back edge connecting
3
two vertices of the same color is evidence of an odd cycle.
4 These basic examples illustrate ways in which DFS can give us
11 5 insight into the structure of a graph. They illustrate that we can
12 0 learn various important graph properties in a single linear-time sweep
through the graph, where we examine every edge twice, once in each
9
direction. Next, we consider an example that shows the utility of DFS
6 2 10
in discovering more intricate details about the graph structure, still in
7 1 linear time.
8 0
Exercises
9
◦ 18.21 Give an ADT function implementation for the adjacency-lists graph
representation that, in time proportional to V , finds a cycle and prints it, or
Figure 18.15
reports that none exists.
Two-coloring a DFS tree
To two-color a graph, we alternate 18.22 Describe a family of graphs with V vertices for which a standard
colors as we move down the DFS adjacency-matrix DFS requires time proportional to V 2 for cycle detection.
tree, then check the back edges
for inconsistencies. In the tree at  18.23 Specify a modification to Program 18.5 that will produce a two-way
the top, a DFS tree for the sample Euler tour that does the back-and-forth traversal on down edges instead of
graph illustrated in Figure 18.9, back edges.
the back edges 5-4 and 7-0 prove • 18.24 Modify Program 18.5 such that it always produces a two-way Euler
that the graph is not two-colorable tour that, like the one in Figure 18.14, can be drawn such that it does not
because of the odd-length cycles cross itself at any vertex. For example, if the search in Figure 18.14 were to
4-3-5-4 and 0-2-6-4-7-0, re- take the edge 4-3 before the edge 4-7, then the tour would have to cross itself;
spectively. In the tree at the bot- your task is to ensure that the algorithm avoids such situations.
tom, a DFS tree for the bipartite
graph illustrated in Figure 17.5, 18.25 Develop a version of Program 18.5 for the adjacency-lists graph repre-
there are no such inconsistencies, sentation (Program 17.6) that sorts the edges of a graph in order of a two-way
and the indicated shading is a two- Euler tour. Your program should return a link to a circular linked list of nodes
coloring of the graph. that corresponds to a two-way Euler tour.
GRAPH SEARCH §18.5 105

Program 18.6 Two-colorability


This DFS function assigns the values 0 or 1 to the vertex-indexed array
G->color and indicates in the return value whether or not it was able
to do the assignment such that, for each graph edge v-w, G->color[v]
and G->color[w] are different.
int dfsRcolor(Graph G, int v, int c)
{ link t;
G->color[v] = 1-c;
for (t = G->adj[v]; t != NULL; t = t->next)
if (G->color[t->v] == -1)
{ if (!dfsRcolor(G, t->v, 1-c)) return 0; }
else if (G->color[t->v] != c) return 0;
return 1;
}
int GRAPHtwocolor(Graph G)
{ int v, id = 0;
G->color = malloc(G->V * sizeof(int));
for (v = 0; v < G->V; v++)
G->color[v] = -1;
for (v = 0; v < G->V; v++)
if (G->color[v] == -1)
if (!dfsRcolor(G, v, 0)) return 0;
return 1;
}

18.26 Modify your solution to Exercise 18.25 such that you can use it for
huge graphs, where you might not have the space to make a copy of the
list nodes corresponding to each edge. That is, use the list nodes that were
allocated to build the graph, and destroy the original adjacency-lists graph
representation.

18.27 Prove that a graph is two-colorable if and only if it contains no odd


cycle. Hint: Prove by induction that Program 18.6 determines whether or not
any given graph is two-colorable.

◦ 18.28 Explain why the approach taken in Program 18.6 does not generalize
to give an efficient method for determining whether a graph is three-colorable.

18.29 Most graphs are not two-colorable, and DFS tends to discover that
fact quickly. Run empirical tests to study the number of edges examined by
106 §18.6 CHAPTER EIGHTEEN

Program 18.6, for graphs of various sizes, drawn from various graph models
(see Exercises 17.63–76).

◦ 18.30 Prove that every connected graph has a vertex whose removal will not
disconnect the graph, and write a DFS function that finds such a vertex. Hint:
Consider the leaves of the DFS tree.
18.31 Prove that every graph with more than one vertex has at least two
vertices whose removal will not increase the number of connected components.

18.6 Separability and Biconnectivity


To illustrate the power of DFS as the basis for graph-processing algo-
rithms, we turn to problems related to generalized notions of connec-
tivity in graphs. We study questions such as the following: Given two
vertices, are there two different paths connecting them?
If it is important that a graph be connected in some situation, it
might also be important that it stay connected when an edge or a vertex
is removed. That is, we may want to have more than one route between
each pair of vertices in a graph, so as to handle possible failures. For
example, we can fly from New York to San Francisco even if Chicago
is snowed in by going through Denver instead. Or, we might imagine
0 a wartime situation where we want to arrange our railroad network
6 7 8 such that an enemy must destroy at least two stations to cut our rail
1 2 lines. Similarly, we might expect the main communications lines in an
10
integrated circuit or a communications network to be connected such
3 9
4
that the rest of the circuit still can function if one wire is broken or
5 11 12 one link is down.
These examples illustrate two distinct concepts: In the circuit and
in the communications network, we are interested in staying connected
Figure 18.16 if an edge is removed; in the air or train routes, we are interested in
An edge-separable graph staying connected if a vertex is removed. We begin by examining the
This graph is not edge connected. former in detail.
The edges 0-5, 6-7, and 11-12
(shaded) are separating edges Definition 18.5 A bridge in a graph is an edge that, if removed,
(bridges). The graph has 4 edge- would separate a connected graph into two disjoint subgraphs. A
connected components: one com-
prising vertices 0, 1, 2, and 6; an-
graph that has no bridges is said to be edge-connected.
other comprising vertices 3, 4, 9,
When we speak of removing an edge, we mean to delete that edge from
and 11; another comprising ver-
tices 7, 8, and 10; and the single the set of edges that define the graph, even when that action might
vertex 12. leave one or both of the edge’s vertices isolated. An edge-connected
GRAPH SEARCH §18.6 107

0 Figure 18.17
DFS tree for finding bridges
5 1 6
Nodes 5, 7, and 12 in this DFS
0 4 3 2 0
tree for the graph in Figure 18.16
3 9 5 11 6 1 all have the property that no back
edge connects a descendant with
4 5 4 11 7 2 0
an ancestor, and no other nodes
9 12 4 10 6 8 have that property. Therefore, as
indicated, breaking the edge be-
11 8 7
tween one of these nodes and its
10 7 parent would disconnect the sub-
tree rooted at that node from the
0 1 2 3 4 5 6 7 8 9 10 11 12
pre 0 7 8 3 2 1 9 10 12 4 11 5 6 rest of the graph. That is, the edges
low 0 0 0 1 1 1 0 10 10 2 10 2 6 0-5, 11-12, and 6-7 are bridges.
We use the vertex-indexed array
low to keep track of the lowest
preorder number referenced by any
graph remains connected when we remove any single edge. In some back edge in the subtree rooted at
contexts, it is more natural to emphasize our ability to disconnect the the vertex. For example, the value
graph rather than the graph’s ability to stay connected, so we freely of low[9] is 2 because one of the
back edges in the subtree rooted
use alternate terminology that provides this emphasis: We refer to
at 9 points to 4 (the vertex with
a graph that is not edge-connected as an edge-separable graph, and preorder number 2), and no other
we call bridges separation edges. If we remove all the bridges in an back edge points higher in the tree.
edge-separable graph, we divide it into edge-connected components or Nodes 5, 7, and 12 are the ones
for which the low value is equal to
bridge-connected components: maximal subgraphs with no bridges.
the pre value.
Figure 18.16 is a small example that illustrates these concepts.
Finding the bridges in a graph seems, at first blush, to be a
nontrivial graph-processing problem, but it actually is an application
of DFS where we can exploit basic properties of the DFS tree that we
have already noted. Specifically, back edges cannot be bridges because
we know that the two nodes they connect are also connected by a path
in the DFS tree. Moreover, we can add a simple test to our recursive
DFS function to test whether or not tree edges are bridges. The basic
idea, stated formally next, is illustrated in Figure 18.17.
Property 18.5 In any DFS tree, a tree edge v-w is a bridge if and
only if there are no back edges that connect a descendant of w to an
ancestor of w.
Proof : If there is such an edge, v-w cannot be a bridge. Conversely, if
v-w is not a bridge, then there has to be some path from w to v in the
graph other than w-v itself. Every such path has to have some such
edge.
108 §18.6 CHAPTER EIGHTEEN

Program 18.7 Edge connectivity (adjacency lists)


This recursive DFS function prints and counts the bridges in a graph.
It assumes that Program 18.3 is augmented with a counter bnct and
a vertex-indexed array low that are initialized in the same way as cnt
and pre, respectively. The low array keeps track of the lowest preorder
number that can be reached from each vertex by a sequence of tree edges
followed by one back edge.

void bridgeR(Graph G, Edge e)


{ link t; int v, w = e.w;
pre[w] = cnt++; low[w] = pre[w];
for (t = G->adj[w]; t != NULL; t = t->next)
if (pre[v = t->v] == -1)
{
bridgeR(G, EDGE(w, v));
if (low[w] > low[v]) low[w] = low[v];
if (low[v] == pre[v])
bcnt++; printf("%d-%d\n", w, v);
}
else if (v != e.v)
if (low[w] > pre[v]) low[w] = pre[v];
}

Asserting this property is equivalent to saying that the only link


in the subtree rooted at w that points to a node not in the subtree is
the parent link from w back to v. This condition holds if and only if
every path connecting any of the nodes in w’s subtree to any node that
is not in w’s subtree includes v-w. In other words, removing v-w would
disconnect from the rest of the graph the subgraph corresponding to
w’s subtree.
Program 18.7 shows how we can augment DFS to identify bridges
in a graph, using Property 18.5. For every vertex v, we use the recursive
function to compute the lowest preorder number that can be reached by
a sequence of zero or more tree edges followed by a single back edge
from any node in the subtree rooted at v. If the computed number
is equal to v’s preorder number, then there is no edge connecting a
descendant with an ancestor, and we have identified a bridge. The
computation for each vertex is straightforward: We proceed through
GRAPH SEARCH §18.6 109

12 Figure 18.18
11 Another DFS tree for finding
9 12 4 bridges
4 11 This diagram shows a different
3 9 5 11 DFS tree for than the one in Fig-
4 5 ure 18.17 for the graph in Fig-
ure 18.16, where we starting the
0 4 3
search at a different node. Al-
5 1 6
though we visit the nodes and
2 0
edges in a completely different or-
6 1 der, we still find the same bridges
7 2 0 (of course). In this tree, 0, 7, and
10 6 8 11 are the ones for which the low
8 7 value is equal to the pre value, so
10 7
the edges connecting each of them
to their parents (12-11, 5-0, and
0 1 2 3 4 5 6 7 8 9 10 11 12 6-7, respectively) are bridges.
pre 6 7 8 4 3 5 9 10 12 2 11 1 0
low 6 6 6 3 1 3 6 10 10 1 10 1 0

the adjacency list, keeping track of the minimum of the numbers that
we can reach by following each edge. For tree edges, we do the
computation recursively; for back edges, we use the preorder number
of the adjacent vertex. If the call to the recursive function for an edge
w-v does not uncover a path to a node with a preorder number less
than v’s preorder number, then w-v is a bridge.
Property 18.6 We can find a graph’s bridges in linear time.
Proof : Program 18.7 is a minor modification to DFS that involves
adding a few constant-time tests, so it follows directly from Proper-
ties 18.3 and 18.4 that finding the bridges in a graph requires time
proportional to V 2 for the adjacency-matrix representation and to
V + E for the adjacency-lists representation.
In Program 18.7, we use DFS to discover properties of the graph.
The graph representation certainly affects the order of the search, but
it does not affect the results because bridges are a characteristic of the
graph rather than of the way that we choose to represent or search
the graph. As usual, any DFS tree is simply another representation
of the graph, so all DFS trees have the same connectivity properties.
The correctness of the algorithm depends on this fundamental fact.
For example, Figure 18.18 illustrates a different search of the graph,
110 §18.6 CHAPTER EIGHTEEN

starting from a different vertex, that (of course) finds the same bridges.
Despite Property 18.6, when we examine different DFS trees for the
same graph, we see that some search costs may depend not just on
properties of the graph, but also on properties of the DFS tree. For
example, the amount of space needed for the stack to support the
recursive calls is larger for the example in Figure 18.18 than for the
example in Figure 18.17.
As we did for regular connectivity in Program 18.4, we may wish
to use Program 18.7 to build an ADT function for testing whether a
graph is edge-connected or to count the number of edge-connected
components. If desired, we can proceed as for Program 18.4 to create
ADT functions that gives clients the ability to call a linear-time pre-
processing function, then respond in constant time to queries that ask
whether two vertices are in the same edge-connected component (see
Exercise 18.35).
We conclude this section by considering other generalizations
of connectivity, including the problem of determining which vertices
are critical to keeping a graph connected. By including this material
biconnected component here, we keep in one place the basic background material for the more
complex algorithms that we consider in Chapter 22. If you are new
to connectivity problems, you may wish to skip to Section 18.7 and
return here when you study Chapter 22.
bridge When we speak of it removing a vertex, we also mean that we
edge-connected
remove all its incident edges. As illustrated in Figure 18.19, removing
component either of the vertices on a bridge would disconnect a graph (unless the
articulation point bridge were the only edge incident on one or both of the vertices), but
there are also other vertices, not associated with bridges, that have the
same property.
Definition 18.6 An articulation point in a graph is an vertex that, if
Figure 18.19 removed, would separate a connected graph into at least two disjoint
Graph separability terminol- subgraphs.
ogy
We also refer to articulation points as separation vertices or cut vertices.
This graph has two edge-connected
components and one bridge. The
We might use the term “vertex connected” to describe a graph that has
edge-connected component above no separation vertices, but we use different terminology based on a
the bridge is also biconnected; the related characterization that turns out to be equivalent.
one below the bridge consists of
two biconnected components that Definition 18.7 A graph is said to be biconnected if every pair of
are joined at an articulation point. vertices is connected by two disjoint paths.
GRAPH SEARCH §18.6 111

The requirement that the paths be disjoint is what distinguishes bi-


connectivity from edge connectivity. An alternate definition of edge
connectivity is that every pair of vertices is connected by two edge-
disjoint paths—these paths can have a vertex (but no edge) in com-
mon. Biconnectivity is a stronger condition: An edge-connected graph
remains connected if we remove any edge, but a biconnected graph
remains connected if we remove any vertex (and all that vertex’s in-
cident edges). Every biconnected graph is edge-connected, but an
edge-connected graph need not be biconnected. We also use the term
separable to refer to graphs that are not biconnected, because they
can be separated into two pieces by removal of just one vertex. The
separation vertices are the key to biconnectivity.
Property 18.7 A graph is biconnected if and only if it has no sepa-
ration vertices (articulation points).
Proof : Assume that a graph has a separation vertex. Let s and t
be vertices that would be in two different pieces if the separation
0
vertex were removed. All paths between s and t must contain the 6 7 8
separation vertex, therefore the graph is not biconnected. The proof 1 2
in the other direction is more difficult and is a worthwhile exercise for 10
the mathematically inclined reader (see Exercise 18.39). 3 9
4
We have seen that we can partition the edges of a graph that is not 5 11 12
connected into a set of connected subgraphs, and that we can partition
the edges of a graph that is not edge-connected into a set of bridges and
edge-connected subgraphs (which are connected by bridges). Similarly, Figure 18.20
Articulation points (separa-
we can divide any graph that is not biconnected into a set of bridges tion vertices)
and biconnected components, which are each biconnected subgraphs.
This graph is not biconnected.
The biconnected components and bridges are not a proper partition of The vertices 0, 4, 5, 6, 7, and 11
the graph because articulation points may appear on multiple bicon- (shaded) are articulation points.
nected components (see, for example, Figure 18.20). The biconnected The graph has five biconnected
components are connected at articulation points, perhaps by bridges. components: one comprising
edges 4-9, 9-11, and 4-11; an-
A connected component of a graph has the property that there other comprising edges 7-8, 8-10,
exists a path between any two vertices in the graph. Analogously, a and 7-10; another comprising
biconnected component has the property that there exist two disjoint edges 0-1, 1-2, 2-6, and 6-0; an-
paths between any pair of vertices. other comprising edges 3-5, 4-5,
and 3-4; and the single vertex 12.
We can use the same DFS-based approach that we used in Pro- Adding an edge connecting 12 to
gram 18.7 to determine whether or not a graph is biconnected and to 7, 8, or 10 would biconnect the
identify the articulation points. We omit the code because it is very graph.
112 §18.6 CHAPTER EIGHTEEN

similar to Program 18.7, with an extra test to check whether the root
of the DFS tree is an articulation point (see Exercise 18.42). Develop-
ing code to print out the biconnected components is also a worthwhile
exercise that is only slightly more difficult than the corresponding code
for edge connectivity (see Exercise 18.43).

Property 18.8 We can find a graph’s articulation points and bicon-


nected components in linear time.

Proof : As for Property 18.7, this fact follows from the observation that
the solutions to Exercises 18.42 and 18.43 involve minor modifications
to DFS that amount to adding a few constant-time tests per edge.

Biconnectivity generalizes simple connectivity. Further general-


izations have been the subjects of extensive studies in classical graph
theory and in the design of graph algorithms. These generalizations
indicate the scope of graph-processing problems that we might face,
many of which are easily posed but less easily solved.

Definition 18.8 A graph is k-connected if there are at least k vertex-


disjoint paths connecting every pair of vertices in the graph. The vertex
connectivity of a graph is the minimum number of vertices that need
to be removed to separate it into two pieces.

In this terminology, “1-connected” is the same as “connected”


and “2-connected” is the same as “biconnected.” A graph with an
articulation point has vertex connectivity 1 (or 0), so Property 18.7
says that a graph is 2-connected if and only if its vertex connectivity is
not less than 2. It is a special case of a classical result from graph theory,
known as Whitney’s theorem, which says that a graph is k-connected if
and only if its vertex connectivity is not less than k. Whitney’s theorem
follows directly from Menger’s theorem (see Section 22.7), which says
that the minimum number of vertices whose removal disconnects two
vertices in a graph is equal to the maximum number of vertex-disjoint
paths between the two vertices (to prove Whitney’s theorem, apply
Menger’s theorem to every pair of vertices).

Definition 18.9 A graph is k–edge-connected if there are at least k


edge-disjoint paths connecting every pair of vertices in the graph. The
edge connectivity of a graph is the minimum number of edges that
need to be removed to separate it into two pieces.
GRAPH SEARCH §18.6 113

In this terminology, “2–edge-connected” is the same as “edge-connected”


(that is, an edge-connected graph has edge connectivity greater than 1)
and a graph with at least one bridge has edge connectivity 1. Another
version of Menger’s theorem says that the minimum number of ver-
tices whose removal disconnects two vertices in a graph is equal to the
maximum number of vertex-disjoint paths between the two vertices,
which implies that a graph is k–edge-connected if and only if its edge
connectivity is k.
With these definitions, we are led to generalize the connectivity
problems that we considered at the beginning of this section.
st-connectivity What is the minimum number of edges whose
removal will separate two given vertices s and t in a given graph?
What is the minimum number of vertices whose removal will separate
two given vertices s and t in a given graph?
General connectivity Is a given graph k-connected? Is a given
graph k–edge-connected? What is the edge connectivity and the vertex
connectivity of a given graph?
Although these problems are much more difficult to solve than
are the simple connectivity problems that we have considered in this
section, they are members of a large class of graph-processing problems
that we can solve using the general algorithmic tools that we consider in
Chapter 22 (with DFS playing an important role); we consider specific
solutions in Section 22.7.
Exercises
 18.32 If a graph is a forest, all its edges are separation edges; but which
vertices are separation vertices?
 18.33 Consider the graph
3-7 1-4 7-8 0-5 5-2 3-8 2-9 0-6 4-9 2-6 6-4.
Draw the standard adjacency-lists DFS tree. Use it to find the bridges and the
edge-connected components.
18.34 Prove that every vertex in any graph belongs to exactly one edge-
connected component.
◦ 18.35 Expand Program 18.7, in the style of Program 18.4, to support an
ADT function for testing whether two vertices are in the same edge-connected
component.
 18.36 Consider the graph
3-7 1-4 7-8 0-5 5-2 3-8 2-9 0-6 4-9 2-6 6-4.
114 §18.7 CHAPTER EIGHTEEN

Draw the standard adjacency-lists DFS tree. Use it to find the articulation
points and the biconnected components.
 18.37 Do the previous exercise using the standard adjacency-matrix DFS tree.
18.38 Prove that every edge in a graph either is a bridge or belongs to exactly
one biconnected component.
• 18.39 Prove that any graph with no articulation points is biconnected. Hint:
Given a pair of vertices s and t and a path connecting them, use the fact
that none of the vertices on the path are articulation points to construct two
disjoint paths connecting s and t.
18.40 Modify Program 18.3 to derive a program for determining whether a
graph is biconnected, using a brute-force algorithm that runs in time propor-
tional to V (V + E). Hint: If you mark a vertex as having been seen before
you start the search, you effectively remove it from the graph.
◦ 18.41 Extend your solution to Exercise 18.40 to derive a program that deter-
mines whether a graph is 3-connected. Give a formula describing the approx-
imate number of times your program examines a graph edge, as a function of
V and E.
18.42 Prove that the root of a DFS tree is an articulation point if and only if
it has two or more (internal) children.
• 18.43 Write an ADT function for the adjacency-lists representation that prints
the biconnected components of the graph.
18.44 What is the minimum number of edges that must be present in any
biconnected graph with V vertices?
18.45 Modify Programs 18.3 and 18.7 to implement an ADT function that
determines whether a graph is edge-connected (returning as soon as it identifies
a bridge if the graph is not). Run empirical tests to study the number of edges
examined by your function, for graphs of various sizes, drawn from various
graph models (see Exercises 17.63–76).
◦ 18.46 Instrument Programs 18.3 and 18.7 to print out the number of artic-
ulation points, bridges, and biconnected components.
• 18.47 Run experiments to determine empirically the average values of the
quantities described in Exercise 18.46 for graphs of various sizes, drawn from
various graph models (see Exercises 17.63–76).
18.48 Give the edge connectivity and the vertex connectivity of the graph
0-1 0-2 0-8 2-1 2-8 8-1 3-8 3-7 3-6 3-5 3-4 4-6 4-5 5-6 6-7 7-8.

18.7 Breadth-First Search


Suppose that we want to find a shortest path between two specific
vertices in a graph—a path connecting the vertices with the property
GRAPH SEARCH §18.7 115

that no other path connecting those vertices has fewer edges. The
classical method for accomplishing this task, called breadth-first search
(BFS), is also the basis of numerous algorithms for processing graphs,
so we consider it in detail in this section. DFS offers us little assistance
in solving this problem, because the order in which it takes us through
the graph has no relationship to the goal of finding shortest paths. In
contrast, BFS is based on this goal. To find a shortest path from v to w,
we start at v and check for w among all the vertices that we can reach
by following one edge, then we check all the vertices that we can reach
by following two edges, and so forth.
When we come to a point during a graph search where we have
more than one edge to traverse, we choose one and save the others
to be explored later. In DFS, we use a pushdown stack (that is man-
aged by the system to support the recursive search function) for this
purpose. Using the LIFO rule that characterizes the pushdown stack
corresponds to exploring passages that are close by in a maze: We
choose, of the passages yet to be explored, the one that was most re-
cently encountered. In BFS, we want to explore the vertices in order of
their distance from the start. For a maze, doing the search in this order
might require a search team; within a computer program, however, it
is easily arranged: We simply use a FIFO queue instead of a stack.
Program 18.8 is an implementation of BFS for graphs represented
with an adjacency matrix. It is based on maintaining a queue of all
edges that connect a visited vertex with an unvisited vertex. We put
a dummy self-loop to the start vertex on the queue, then perform the
following steps until the queue is empty:
• Take edges from the queue until finding one that points to an
unvisited vertex.
• Visit that vertex; put onto the queue all edges that go from that
vertex to unvisited vertices.
Figure 18.21 shows the step-by-step development of BFS on a sample
graph.
As we saw in Section 18.4, DFS is analogous to one person ex-
ploring a maze. BFS is analogous to a group of people exploring by
fanning out in all directions. Although DFS and BFS are different in
many respects, there is an essential underlying relationship between
the two methods—one that we noted when we briefly considered the
methods in Chapter 5. In Section 18.8, we consider a generalized
116 §18.7 CHAPTER EIGHTEEN

Figure 18.21
Breadth-first search
This figure traces the operation of
BFS on our sample graph. We be- 0 2
0 0 2 0
gin with all the edges adjacent to 2 5 7
6 6
the start vertex on the queue (top 6
1 7 1 7
left). Next, we move edge 0-2
from the queue to the tree and pro- 3 3
cess its incident edges 2-0 and 5 4 5 4
2-6 (second from top, left). We do
not put 2-0 on the queue because 0-2 0-5 0-7 5-3 5-4 7-1 7-4 6-4
0 is already on the tree. Third, we
move edge 0-5 from the queue to
the tree; again 5’s incident edge
(to 0) leads nowhere new, but we
0 2 0 0 2 0
add 5-3 and 5-4 to the queue
(third from top, left). Next, we add 2 2 5 7
6 6
0-7 to the tree and put 7-1 on the 6 3
1 7 1 7
queue (bottom left).
The edge 7-4 is printed in 3 3
gray because we could also avoid 5 4 5 4
putting it on the queue, since there
is another edge that will take us 0-5 0-7 2-6 5-4 7-1 7-4 6-4 3-4
to 4 that is already on the queue.
To complete the search, we take
the remaining edges off the queue,
completely ignoring the gray edges 0
0 2 0 0 2
when they come to the front of
the queue (right). Edges enter and 2 5 2 5 7
6 6
leave the queue in order of their 1 7 1 7
6 3 4
distance from 0.
3 3

5 4 5 4

0-7 2-6 5-3 5-4 7-1 7-4 6-4 3-4

0 2 0 0 2 0

2 5 7 2 5 7
6 6
6 3 4 1
1 7 1 7
3 3

5 4 5 4

2-6 5-3 5-4 7-1 7-4


GRAPH SEARCH §18.7 117

Program 18.8 Breadth-first search (adjacency matrix)


This implementation of search for the adjacency-matrix representation
is breadth-first search (BFS). To visit a vertex, we scan through its in-
cident edges, putting any edges to unvisited vertices onto the queue of
vertices to be visited.
We mark the nodes in the order they are visited in a vertex-indexed
array pre and build an explicit parent-link representation of the BFS tree
(the edges that first take us to each node) in another vertex-indexed array
st. This code assumes that we add a call to QUEUEinit (and code to
declare and initialize st) to GRAPHsearch in Program 18.3.

#define bfs search


void bfs(Graph G, Edge e)
{ int v, w;
QUEUEput(e);
while (!QUEUEempty())
if (pre[(e = QUEUEget()).w] == -1)
{
pre[e.w] = cnt++; st[e.w] = e.v;
for (v = 0; v < G->V; v++)
if (G->adj[e.w][v] == 1)
if (pre[v] == -1)
QUEUEput(EDGE(e.w, v));
}
}

graph-searching method that we can specialize to include these two


algorithms and a host of others. Each algorithm has particular dy-
namic characteristics that we use to solve associated graph-processing
problems. For BFS, the distance from each vertex to the start vertex
(the length of a shortest path connecting the two) is the key property
of interest.
Property 18.9 During BFS, vertices enter and leave the FIFO queue
in order of their distance from the start vertex.
Proof : A stronger property holds: The queue always consists of zero
or more vertices of distance k from the start, followed by zero or more
vertices of distance k + 1 from the start, for some integer k. This
stronger property is easy to prove by induction.
118 §18.7 CHAPTER EIGHTEEN

For DFS, we understood the dynamic characteristics of the algo-


0 rithm with the aid of the DFS search forest that describes the recursive-
call structure of the algorithm. An essential property of that forest is
2 5 7
that the forest represents the paths from each vertex back to the place
0 6 0 3 4 0 1 4
that the search started for its connected component. As indicated in
2 4 4 5 3 5 6 7 7 the implementation and shown in Figure 18.22, such a spanning tree
also helps us to understand BFS. As with DFS, we have a forest that
0 1 2 3 4 5 6 7
pre 0 7 1 5 6 2 4 3
characterizes the dynamics of the search, one tree for each connected
st 0 7 0 5 5 0 2 0 component, one tree node for each graph vertex, and one tree edge for
each graph edge. BFS corresponds to traversing each of the trees in
Figure 18.22
BFS tree this forest in level order. As with DFS, we use a vertex-indexed array
to represent explicitly the forest with parent links. For BFS, this forest
This tree provides a compact de-
scription of the dynamic proper- carries essential information about the graph structure:
ties of BFS, in a manner similar to
the tree depicted in Figure 18.9. Property 18.10 For any node w in the BFS tree rooted at v, the tree
Traversing the tree in level order path from v to w corresponds to a shortest path from v to w in the
tells us how the search proceeds, corresponding graph.
step by step: first we visit 0; then
we visit 2, 5, and 7; then we check Proof : The tree-path lengths from nodes coming off the queue to the
from 2 that 0 was visited and visit
6; and so forth. Each tree node
root are nondecreasing, and all nodes closer to the root than w are on
has a child representing each of the queue; so no shorter path to w was found before it comes off the
the nodes adjacent to it, in the or- queue, and no path to w that is discovered after it comes off the queue
der they were considered by the can be shorter than w’s tree path length.
BFS. As in Figure 18.9, links in the
BFS tree correspond to edges in As indicated in Figure 18.21 and noted in Chapter 5, there is
the graph: if we replace edges to
external nodes by lines to the indi- no need to put an edge on the queue with the same destination vertex
cated node, we have a drawing of as any edge already on the queue, since the FIFO policy ensures that
the graph. Links to external nodes we will process the old queue edge (and visit the vertex) before we
represent edges that were not put get to the new edge. One way to implement this policy is to use a
onto the queue because they led to
marked nodes: they are either par- queue ADT implementation where such duplication is disallowed by
ent links or cross links that point to an ignore-the-new-item policy (see Section 4.7). Another choice is
a node either on the same level or to use the global vertex-marking array for this purpose: Instead of
one level closer to the root. marking a vertex as having been visited when we take it off the queue,
The st array is a parent-link
representation of the tree, which we do so when we put it on the queue. Testing whether a vertex is
we can use to find a shortest path marked (whether its entry has changed from its initial sentinel value)
from any node to the root. For then stops us from putting any other edges that point to the same
example, 3-5-0 is a path in the vertex on the queue. This change, shown in Program 18.9, gives a
graph from 3 to 0, since st[3] is
5 and st[5] is 0. No other path BFS implementation where there are never more than V edges on the
from 3 to 0 is shorter. queue (one edge pointing to each vertex, at most).
GRAPH SEARCH §18.7 119

Program 18.9 Improved BFS


To guarantee that the queue that we use during BFS has at most V
entries, we mark the vertices as we put them on the queue.

void bfs(Graph G, Edge e)


{ int v, w;
QUEUEput(e); pre[e.w] = cnt++;
while (!QUEUEempty())
{
e = QUEUEget();
w = e.w; st[w] = e.v;
for (v = 0; v < G->V; v++)
if ((G->adj[w][v] == 1) && (pre[v] == -1))
{ QUEUEput(EDGE(w, v)); pre[v] = cnt++; }
}
}

The code corresponding to Program 18.9 for BFS in graphs repre-


sented with adjacency lists is straightforward and derives immediately
from the generalized graph search that we consider in Section 18.8, so
we do not include it here. As we did for DFS, we consider BFS to be a
linear-time algorithm.

Property 18.11 BFS visits all the vertices and edges in a graph in
time proportional to V 2 for the adjacency-matrix representation and
to V + E for the adjacency-lists representation.

Proof : As we did in proving the analogous DFS properties, we note by


inspecting the code that we check each entry in the adjacency-matrix
row or in the adjacency list precisely once for every vertex that we
visit, so it suffices to show that we visit each vertex. Now, for each
connected component, the algorithm preserves the following invariant:
All vertices that can be reached from the start vertex (i) are on the BFS
tree, (ii) are on the queue, or (iii) can be reached from a vertex on the
queue. Each vertex moves from (iii) to (ii) to (i), and the number of
vertices in (i) increases on each iteration of the loop, so that the BFS
tree eventually contains all the vertices that can be reached from the
start vertex.
120 §18.7 CHAPTER EIGHTEEN

With BFS, we can solve the spanning tree, connected components,


vertex search, and several other basic connectivity problems that we
described in Section 18.4, since the solutions that we considered de-
pend on only the ability of the search to examine every node and edge
connected to the starting point. More important, as mentioned at
the outset of this section, BFS is the natural graph-search algorithm
for applications where we want to know a shortest path between two
specified vertices. Next, we consider a specific solution to this problem
and its extension to solve two related problems.
Shortest path Find a shortest path in the graph from v to w. We
can accomplish this task by starting a BFS that maintains the parent-
link representation st of the search tree at v, then stopping when we
reach w. The path up the tree from w to v is a shortest path. For
example, the following code prints out the path connecting w to v:
for (t = w; t !=v; t = st[t]) printf("%d-", t);
printf("%d\n", t);
If we want the path from v to w, we can replace the printf operations
in this code by stack pushes, then go into a loop that prints the vertex
indices as we pop them from the stack. Or, we start the search at w
and stop at v in the first place.
Single-source shortest paths Find shortest paths connecting a
given vertex v with each other vertex in the graph. The full BFS tree
rooted at v provides a way to accomplish this task: The path from each
vertex to the root is a shortest path to the root. Therefore, to solve the
problem, we run BFS to completion starting at v. The st array that
results from this computation is a parent-link prepresentation of the
BFS tree, and the code in the previous paragraph will give the shortest
path to any other vertex w.
All-pairs shortest paths Find shortest paths connecting each
pair of vertices in the graph. The way to accomplish this task is to run
BFS to solve the single-source problem for each vertex in the graph and,
to support ADT functions that can handle huge numbers of shortest-
path queries efficiently, store the path lengths and parent-link tree
representations for each vertex (see Figure 18.23). This preprocessing
requires time proportional to V E and space proportional to V 2 , a
potentially prohibitive cost for huge sparse graphs. However, it allows
us to build an ADT with optimal performance: After investing in
the preprocessing (and the space to hold the results), we can return
GRAPH SEARCH §18.7 121

Figure 18.23
All-pairs shortest paths exam-
0 2
0
0 2
4 ple
6
7 5 2 6 7 6 5 3 These figures depict the result of
1 7 1 7
1 0 2 doing BFS from each vertex, thus
4 1 3 6 computing the shortest paths con-
3 3
necting all pairs of vertices. Each
5 4 5 4
search gives a BFS tree that defines
the shortest paths connecting all
graph vertices to the vertex at the
0 2 0 2 5 root. The results of all the searches
1
are summarized in the two matri-
6 6 4 3 0
7 ces at the bottom. In the left ma-
1 7 1 7
4 0
7 6 2 trix, the entry in row v and column
3 3
1 w gives the length of the shortest
6 5 3 2
5 4 5 4 path from v to w (the depth of v
in w’s tree). Each row of the right
matrix contains the st array for the
corresponding search. For exam-
0 2 0 2 6
2 ple, the shortest path from 3 to 2
4 2 has three edges, as indicated by
6 6 0 6
1 7 1 7
the entry in row 3 and column 2 of
7 5 3 0
4 7 5 the left matrix. The third BFS tree
3 3
1 from the top on the left tells us that
3 1
5 4 5 4 the path is 3-4-6-2, and this in-
formation is encoded in row 2 in
the right matrix. The matrix is not
0 2 0 2 7
necessarily symmetric when there
3 is more than one shortest path, be-
4 1 0 cause the paths found depend on
6 5 4 6
1 7 1 7 6 5 3 2 the BFS search order. For example,
0 7 6
3 3 the BFS tree at the bottom on the
2 1 left and row 3 of the right matrix
5 4 5 4
tell us that the shortest path from 2
to 3 is 2-0-5-3.

0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7
0 0 2 1 2 2 1 2 1 0 0 7 0 5 7 0 2 0
1 2 0 3 3 2 3 3 1 1 7 1 0 4 7 4 4 1
2 1 3 0 3 2 2 1 2 2 2 7 2 4 6 0 2 0
3 2 3 3 0 1 1 2 2 3 5 7 0 3 3 3 4 4
4 2 2 2 1 0 1 1 1 4 7 7 6 4 4 4 4 4
5 1 3 2 1 1 0 2 2 5 5 7 0 5 5 5 4 4
6 2 3 1 2 1 2 0 2 6 2 7 6 4 6 4 6 4
7 1 1 2 2 1 2 2 0 7 7 7 0 4 7 4 4 7
122 §18.7 CHAPTER EIGHTEEN

Figure 18.24
Breadth-first search shortest-path lengths in constant time and the paths themselves in time
This figure illustrates the progress proportional to their length (see Exercise 18.53).
of BFS in random Euclidean near- These BFS-based solutions are effective, but we do not consider
neighbor graph (left), in the same implementations in any further detail here because they are special
style as Figure 18.13. As is evident
from this example, the search tree
cases of algorithms that we consider in detail in Chapter 21. The term
for BFS tends to be quite short and shortest paths in graphs is generally taken to describe the correspond-
wide for this type of graph (and ing problems for digraphs and networks. Chapter 21 is devoted to this
many other types of graphs com- topic. The solutions that we examine there are strict generalizations
monly encountered in practice).
That is, vertices tend to be con- of the BFS-based solutions described here.
nected to one another by rather The basic characteristics of BFS search dynamics contrast sharply
short paths. The contrast between with those for DFS search, as illustrated in the large graph depicted in
the shapes of the DFS and BFS
Figure 18.24, which you should compare with Figure 18.13. The tree
trees is striking testimony to the
differing dynamic properties of the is shallow and broad, and demonstrates a set of facts about the graph
algorithms. being searched different from those shown by DFS. For example,
• There exists a relatively short path connecting each pair of ver-
tices in the graph.
• During the search, most vertices are adjacent to numerous unvis-
ited vertices.
Again, this example is typical of the behavior that we expect from BFS,
but verifying facts of this kind for graph models of interest and graphs
that arise in practice requires detailed analysis.
GRAPH SEARCH §18.7 123

DFS wends its way through the graph, storing on the stack the
points where other paths branch off; BFS sweeps through the graph,
using a queue to remember the frontier of visited places. DFS explores
the graph by looking for new vertices far away from the start point,
taking closer vertices only when dead ends are encountered; BFS com-
pletely covers the area close to the starting point, moving farther away
only when everything nearby has been examined. The order in which
the vertices are visited depends on the graph structure and representa-
tion, but these global properties of the search trees are more informed
by the algorithms than by the graphs or their representations.
The key to understanding graph-processing algorithms is to real-
ize that not only are various different search strategies effective ways
to learn various different graph properties, but also we can imple-
ment many of them uniformly. For example, the DFS illustrated in
Figure 18.13 tells us that the graph has a long path, and the BFS illus-
trated in Figure 18.24 tells us that it has many short paths. Despite
these marked dynamic differences, DFS and BFS are similar, essen-
tially differing in only the data structure that we use to save edges that
are not yet explored (and the fortuitous circumstance that we can use
a recursive implementation for DFS, to have the system maintain an
implicit stack for us). Indeed, we turn next to a generalized graph-
search algorithm that encompasses DFS, BFS, and a host of other use-
ful strategies, and will serve as the basis for solutions to numerous
classic graph-processing problems.

Exercises
18.49 Draw the BFS forest that results from a standard adjacency-lists BFS of
the graph

3-7 1-4 7-8 0-5 5-2 3-8 2-9 0-6 4-9 2-6 6-4.

18.50 Draw the BFS forest that results from a standard adjacency-matrix BFS
of the graph

3-7 1-4 7-8 0-5 5-2 3-8 2-9 0-6 4-9 2-6 6-4.

18.51 Give the all-shortest-path matrices (in the style of Figure 18.23) for the
graph
3-7 1-4 7-8 0-5 5-2 3-8 2-9 0-6 4-9 2-6 6-4,
assuming that you use the adjacency-matrix representation.
124 §18.8 CHAPTER EIGHTEEN

◦ 18.52 Give a BFS implementation (a version of Program 18.9) that uses a


standard FIFO queue of vertices. Include a test in the BFS search code to
ensure that no duplicates go on the queue.
18.53 Develop ADT functions for the adjacency-lists implementation of the
graph ADT (Program 17.6) that supports shortest-path queries after prepro-
cessing to compute all shortest paths. Specifically, add two array pointers to the
graph representation, and write a preprocessing function GRAPHshortpaths
that assigns values to all their entries as illustrated in Figure 18.23. Then, add
two query functions GRAPHshort(v, w) (that returns the shortest-path length
between v and w) and GRAPHpath(v, w) (that returns the vertex adjacent to v
that is on a shortest path between v and w).

 18.54 What does the BFS tree tell us about the distance from v to w when
neither is at the root?

18.55 Develop a graph ADT function that returns the path length that suf-
fices to connect any pair of vertices. (This quantity is known as the graph’s
diameter). Note: You need to define a convention for the return value in the
case that the graph is not connected.

18.56 Give a simple optimal recursive algorithm for finding the diameter of a
tree (see Exercise 18.55).

◦ 18.57 Instrument a BFS implementation for the adjacency-lists representation


(see Program 18.10) to print out the height of the BFS tree and the percentage
of edges that must be processed for every vertex to be seen.

• 18.58 Run experiments to determine empirically the average values of the


quantities described in Exercise 18.57 for graphs of various sizes, drawn from
various graph models (see Exercises 17.63–76).

18.8 Generalized Graph Search


DFS and BFS are fundamental and essential graph-traversal methods
that lie at the heart of numerous graph-processing algorithms. Know-
ing their essential properties, we now move to a higher level of abstrac-
tion, where we see that both methods are special cases of a generalized
strategy for moving through a graph, one that is suggested by our BFS
implementation (Program 18.9).
The basic idea is simple: We revisit our description of BFS from
Section 18.6, but we use the term generic term fringe, instead of queue,
to describe the set of edges that are possible candidates for being next
added to the tree. We are led immediately to a general strategy for
searching a connected component of a graph. Starting with a self-loop
GRAPH SEARCH §18.8 125

Figure 18.25
Stack-based DFS
Together with Figure 18.21, this
0 2 0 0 2 0 figure illustrates that BFS and DFS
7 differ only in the underlying data
6 6
4 structure. For BFS, we used a
1 7 1 7
6 queue; for DFS we use a stack. We
3 3 begin with all the edges adjacent
2
5 4 5 4
to the start vertex on the stack (top
left). Second, we move edge 0-7
0-2 0-5 0-7 0-2 0-5 7-1 4-3 4-5 from the stack to the tree and push
onto the stack its incident edges
that go to vertices not yet on the
tree 7-1, 7-4, and 7-6 (second
from top, left). The LIFO stack dis-
0 2 0 0 2 0 cipline implies that, when we put
7 7 an edge on the stack, any edges
6 6
4 that points to the same vertex are
1 7 1 7
6 5 obsolete and will be ignored when
3 3 they reach the top of the stack.
2
5 4 5 4 Such edges are printed in gray.
Third, we move edge 7-6 from the
0-2 0-5 7-1 7-4 0-2 0-5 7-1 4-3 5-3 stack to the tree and push its inci-
dent edges on the stack (third from
top, left). Next, we pop 4-6 push
its incident edges on the stack, two
of which will take us to new ver-
0 2 0 0 2 0 tices (bottom left). To complete
7 7 the search, we take the remaining
6 6
1 7
4 1 7
4 edges off the stack, completely ig-
6 5 noring the gray edges when they
3 3
2 3 come to the top of the stack (right).
5 4 5 4

0-2 0-5 7-1 4-3 4-5 4-6 0-2 0-5 7-1 4-3

0 2 0 0 2 0
7 7
6 6
4 4 1
1 7 1 7
6 6 5
3 3
2 3
5 4 5 4

0-2 0-5 7-1 4-3 4-5 6-2


126 §18.8 CHAPTER EIGHTEEN

to a start vertex on the fringe and an empty tree, perform the following
operation until the fringe is empty:
Move an edge from the fringe to the tree. If the vertex to
which it leads is unvisited, visit that vertex, and put onto
the fringe all edges that lead from that vertex to unvisited
vertices.
This strategy describes a family of search algorithms that will visit all
the vertices and edges of a connected graph, no matter what type of
generalized queue we use to hold the fringe edges.
When we use a queue to implement the fringe, we get BFS, the
topic of Section 18.6. When we use a stack to implement the fringe, we
get DFS. Figure 18.25, which you should compare with Figures 18.6
and 18.21, illustrates this phenomenon in detail. Proving this equiva-
lence between recursive and stack-based DFS is an interesting exercise
in recursion removal, where we essentially transform the stack under-
lying the recursive program into the stack implementing the fringe (see
unseen vertex Exercise 18.61). The search order for the DFS depicted in Figure 18.25
differs from the one depicted in Figure 18.6 only because the stack dis-
cipline implies that we check the edges incident on each vertex in the
fringe edge reverse of the order that we encounter them in the adjacency matrix (or
the adjacency lists). The basic fact remains that, if we change the data
tree vertex structure used by Program 18.8 to be a stack instead of a queue (which
is trivial to do because the ADT interfaces of the two data structures
tree edge
differ in only the function names), then we change that program from
fringe vertex BFS to DFS.
Now, as we discussed in Section 18.7, this general method may
not be as efficient as we would like, because the fringe becomes clut-
tered up with edges that point to vertices that are moved to the tree
Figure 18.26 during the time that the edge is on the fringe. For FIFO queues, we
Graph search terminology
avoid this situation by marking destination vertices when we put edges
During a graph search, we main- on the queue. We ignore edges to fringe vertices because we know that
tain a search tree (black) and a
fringe (gray) of edges that are can- they will never be used: The old one will come off the queue (and
didates to be next added to the the vertex visited) before the new one does (see Program 18.9). For
tree. Each vertex is either on the a stack implementation, we want the opposite: When an edge is to
tree (black), the fringe (gray), or be added to the fringe that has the same destination vertex as the one
not yet seen (white). Tree vertices
are connected by tree edges, and already there, we know that the old edge will never be used, because
each fringe vertex is connected by the new one will come off the stack (and the vertex visited) before the
a fringe edge to a tree vertex. old one. To encompass these two extremes and to allow for fringe
GRAPH SEARCH §18.8 127

DFS BFS general Figure 18.27


Graph search strategies
This figure illustrates different pos-
sibilities when we take a next step
in the search illustrated in Fig-
ure 18.26. We move a vertex from
the fringe to the tree (in the cen-
ter of the wheel at the top right)
and check all its edges, putting
those to unseen vertices on the
implementations that can use some other policy to disallow edges on fringe and using an algorithm-
the fringe that point to the same vertex, we modify our general scheme specific replacement rule to decide
whether those to fringe vertices
as follows: should be skipped or should re-
Move an edge from the fringe to the tree. Visit the vertex place the fringe edge to the same
that it leads to, and put all edges that lead from that vertex vertex. In DFS, we always replace
to unvisited vertices onto the fringe, using a replacement the old edges; in BFS, we always
skip the new edges; and in general,
policy on the fringe that ensures that no two edges on the
we might replace some and skip
fringe point to the same vertex. others.
The no-duplicate-destination-vertex policy on the fringe guarantees
that we do not need to test whether the destination vertex of the
edge coming off the queue has been visited. For BFS, we use a queue
implementation with an ignore-the-new-item policy; for DFS, we need
a stack with a forget-the-old-item policy; but any generalized queue
and any replacement policy at all will still yield an effective method
for visiting all the vertices and edges of the graph in linear time and
extra space proportional to V . Figure 18.27 is a schematic illustration
of these differences. We have a family of graph-searching strategies
that includes both DFS and BFS and whose members differ only in
the generalized-queue implementation that they use. As we shall see,
this family encompasses numerous other classical graph-processing
algorithms.
Program 18.10 is an implementation based on these ideas for
graphs represented with adjacency lists. It puts fringe edges on a
generalized queue, and uses the usual vertex-indexed arrays to identify
fringe vertices, so that it can use an explicit update ADT operation
whenever it encounters another edge to a fringe vertex. The ADT
implementation can choose to ignore the new edge or to replace the
old one.
Property 18.12 Generalized graph searching visits all the vertices
and edges in a graph in time proportional to V 2 for the adjacency-
128 §18.8 CHAPTER EIGHTEEN

Program 18.10 Generalized graph search


This implementation of search for Program 18.3 generalizes BFS and
DFS and supports numerous other graph-processing algorithms (see Sec-
tion 21.2 for a discussion of these algorithms and alternate implemen-
tations). It maintains a generalized queue of edges called the fringe.
We initialize the fringe with a self-loop to the start vertex; then, while
the fringe is not empty, we move an edge e from the fringe to the tree
(attached at P|e.v|) and scan e.w’s adjacency list, moving unseen vertices
to the fringe and calling GQupdate for new edges to fringe vertices.
This code makes judicious use of pre and st to guarantee that
no two edges on the fringe point to the same vertex. A vertex v is the
destination vertex of a fringe edge if and only if it is marked (pre[v] is
nonnegative) but it is not yet on the tree (st[v] is -1).

#define pfs search


void pfs(Graph G, Edge e)
{ link t; int v, w;
GQput(e); pre[e.w] = cnt++;
while (!GQempty())
{
e = GQget(); w = e.w; st[w] = e.v;
for (t = G->adj[w]; t != NULL; t = t->next)
if (pre[v = t->v] == -1)
{ GQput(EDGE(w, v)); pre[v] = cnt++; }
else if (st[v] == -1)
GQupdate(EDGE(w, v));
}
}

matrix representation and to V + E for the adjacency-lists representa-


tion plus, in the worst case, the time required for V insert, V remove,
and E update operations in a generalized queue of size V .
Proof : The proof of Property 18.12 does not depend on the queue
implementation, and therefore applies. The stated extra time require-
ments for the generalized-queue operations follow immediately from
the implementation.
There are many other effective ADT designs for the fringe that we
might consider. For example, as with our first BFS implementation,
we could stick with our first general scheme and simply put all the
GRAPH SEARCH §18.8 129

Program 18.11 Random queue implementation


When we remove an item from this data structure, it is equally likely to
be any one of the items currently in the data structure. We can use this
code to implement the generalized-queue ADT for graph searching to
search a graph in a “random” fashion (see text).

#include <stdlib.h>
#include "GQ.h"
static Item *s;
static int N;
void RQinit(int maxN)
{ s = malloc(maxN*sizeof(Item)); N = 0; }
int RQempty()
{ return N == 0; }
void RQput(Item x)
{ s[N++] = x; }
void RQupdate(Item x)
{ }
Item RQget()
{ Item t;
int i = N*(rand()/(RAND_MAX + 1.0));
t = s[i]; s[i] = s[N-1]; s[N-1] = t;
return s[--N];
}

edges on the fringe, then ignore those that go to tree vertices when
we take them off. The disadvantage of this approach, as with BFS,
is that the maximum queue size has to be E instead of V . Or, we
could handle updates implicitly in the ADT implementation, just by
specifying that no two edges with the same destination vertex can
be on the queue. But the simplest way for the ADT implementation
to do so is essentially equivalent to using a vertex-indexed array (see
Exercises 4.51 and 4.54), so the test fits more comfortably into the
client graph-search program.
The combination of Program 18.10 and the generalized-queue
abstraction gives us a general and flexible graph-search mechanism.
To illustrate this point, we now consider briefly two interesting and
useful alternatives to BFS and DFS.
130 §18.8 CHAPTER EIGHTEEN

The first alternative strategy is based on randomized queues (see


Section 4.6). In a randomized queue, we delete items randomly: Each
item on the data structure is equally likely to be the one removed.
Program 18.11 is an implementation that provides this functionality.
If we use this code to implement the generalized queue ADT for Pro-
gram 18.10, then we get a randomized graph-searching algorithm,
where each vertex on the fringe is equally likely to be the next one
added to the tree. The edge (to that vertex) that is added to the tree
depends on the implementation of the update operation. The imple-
mentation in Program 18.11 does no updates, so each fringe vertex is
added to the tree with the edge that caused it to be moved to the fringe.
Alternatively, we might choose to always do updates (which results in
the most recently encountered edge to each fringe vertex being added
to the tree), or to make a random choice.
Another strategy, which is critical in the study of graph-
processing algorithms because it serves as the basis for a number of
the classical algorithms that we address in Chapters 20 through 22, is
to use a priority-queue ADT (see Chapter 9) for the fringe: We assign
priority values to each edge on the fringe, update them as appropriate,
and choose the highest-priority edge as the one to be added next to the
tree. We consider this formulation in detail in Chapter 20. The queue-
Figure 18.28 maintenance operations for priority queues are more costly than are
Fringe sizes for DFS, random- those for stacks and queues because they involve implicit comparisons
ized graph search, and BFS
among items on the queue, but they can support a much broader class
These plots of the fringe size dur-
ing the searches illustrated in Fig- of graph-search algorithms. As we shall see, several critical graph-
ures 18.13, 18.24, and 18.29 indi- processing problems can be addressed simply with judicious choice
cate the dramatic effects that the of priority assignments in a priority-queue–based generalized graph
choice of data structure for the search.
fringe can have on graph search-
ing. When we use a stack, in DFS All generalized graph-searching algorithms examine each edge
(top), we fill up the fringe early in just once and take extra space proportional to V in the worst case;
the search as we find new nodes at they do differ, however, in some performance measures. For example,
every step, then we end the search Figure 18.28 shows the size of the fringe as the search progresses
by removing everything. When
we use a randomized queue (cen- for DFS, BFS, and randomized search; Figure 18.29 shows the tree
ter), the maximum queue size is computed by randomized search for the same example as Figure 18.13
much lower. When we use a FIFO and Figure 18.24. Randomized search has neither the long paths of
queue in BFS (bottom), the maxi-
DFS nor the high-degree nodes of BFS. The shapes of these trees and
mum queue size is still lower, and
we discover new nodes throughout the fringe plots depend on the structure of the particular graph being
the search. searched, but they also characterize the different algorithms.
GRAPH SEARCH §18.8 131

Figure 18.29
Randomized graph search
This figure illustrates the progress
of randomized graph searching
(left), in the same style as Fig-
ures 18.13 and 18.24. The search
tree shape falls somewhere be-
tween the BFS and DFS shapes.
The dynamics of these three al-
gorithms, which differ only in the
data structure for work to be com-
pleted, could hardly be more dif-
ferent.
132 §18.8 CHAPTER EIGHTEEN

We could generalize graph searching still further by working with


a forest (not necessarily a tree) during the search. Although we stop
short of working at this level of generality throughout, we consider a
few algorithms of this sort in Chapter 20.
Exercises
◦ 18.59 Discuss the advantages and disadvantages of a generalized graph-
searching implementation that is based on the following policy: “Move an
edge from the fringe to the tree. If the vertex that it leads to is unvisited, visit
that vertex and put all its incident edges onto the fringe.”
18.60 Modify the adjacency-lists ADT to include edges (in addition to des-
tination vertices) on the lists, then implement a graph search based on the
strategy described in Exercise 18.59 that visits every edge but destroys the
graph, taking advantage of the fact that you can move all of a vertex’s edges
to the fringe with a single link change.
• 18.61 Prove that recursive DFS (Programs 18.3 and 18.2) is equivalent to
generalized graph search using a stack (Program 18.10), in the sense that
both programs will visit all vertices in precisely the same order for all graphs
if and only if the programs scan the adjacency lists in opposite orders.
18.62 Give three different possible traversal orders for randomized search
through the graph
3-7 1-4 7-8 0-5 5-2 3-8 2-9 0-6 4-9 2-6 6-4.

18.63 Could randomized search visit the vertices in the graph


3-7 1-4 7-8 0-5 5-2 3-8 2-9 0-6 4-9 2-6 6-4
in numerical order of their indices? Prove your answer.
18.64 Provide a generalized-queue implementation for graph edges that disal-
lows edges with duplicate vertices on the queue, using an ignore-the-new-item
policy.
◦ 18.65 Develop a randomized graph search that chooses each edge on the
fringe with equal likelihood. Hint: See Program 18.8.
◦ 18.66 Describe a maze-traversal strategy that corresponds to using a standard
pushdown stack for generalized graph searching (see Section 18.1).
◦ 18.67 Instrument generalized graph searching (see Program 18.10) to print
out the height of the tree and the percentage of edges processed for every vertex
to be seen.
• 18.68 Run experiments to determine empirically the average values of the
quantities described in Exercise 18.67 for generalized graph search with a
random queue in graphs of various sizes, drawn from various graph models
(see Exercises 17.63–76).
GRAPH SEARCH §18.9 133

• 18.69 Write a client program that does dynamic graphical animations of gen-
eralized graph search for graphs that have (x, y) coordinates associated with
each vertex (see Exercises 17.55 through 17.59). Test your program on ran-
dom Euclidean neighbor graphs, using as many points as you can process in
a reasonable amount of time. Your program should produce images like the
snapshots shown in Figures 18.13, 18.24, and 18.29, although you should
feel free to use colors instead of shades of gray to denote tree, fringe, and
unseen vertices and edges.

18.9 Analysis of Graph Algorithms


We have for our consideration a broad variety of graph-processing
problems and methods for solving them, so we do not always compare
numerous different algorithms for the same problem, as we have in
other domains. Still, it is always valuable to gain experience with our
algorithms by testing them on real data, or on artificial data that we
understand and that have relevant characteristics that we might expect
to find in actual applications.
As we discussed briefly in Chapter 2, we seek—ideally—natural
input models that have three critical properties:
• They reflect reality to a sufficient extent that we can use them to
predict performance.
• They are sufficiently simple that they are amenable to mathemat-
ical analysis.
• We can write generators that provide problem instances that we
can use to test our algorithms.
With these three components, we can enter into a design-analysis-
implementation-test scenario that leads to efficient algorithms for solv-
ing practical problems.
For domains such as sorting and searching, we have seen spec-
tacular success along these lines in Parts 3 and 4. We can analyze
algorithms, generate random problem instances, and refine implemen-
tations to provide extremely efficient programs for use in a host of
practical situations. For some other domains that we study, various
difficulties can arise. For example, mathematical analysis at the level
that we would like is beyond our reach for many geometric problems,
and developing an accurate model of the input is a significant challenge
for many string-processing algorithms (indeed, doing so is an essential
part of the computation). Similarly, graph algorithms take us to a
134 §18.9 CHAPTER EIGHTEEN

situation where, for many applications, we are on thin ice with respect
to all three properties listed in the previous paragraph:
• The mathematical analysis is challenging, and many basic ana-
lytic questions remain unanswered.
• There is a huge number of different types of graphs, and we
cannot reasonably test our algorithms on all of them.
• Characterizing the types of graphs that arise in practical problems
is, for the most part, a poorly understood problem.
Graphs are sufficiently complicated that we often do not fully under-
stand the essential properties of the ones that we see in practice or of
the artificial ones that we can perhaps generate and analyze.
The situation is perhaps not as bleak as just described for one
primary reason: Many of the graph algorithms that we consider are
optimal in the worst case, so predicting performance is a trivial ex-
ercise. For example, Program 18.7 finds the bridges after examining
each edge and each vertex just once. This cost is the same as the cost of
building the graph data structure, and we can confidently predict, for
example, that doubling the number of edges will double the running
time, no matter what kind of graphs we are processing.
When the running time of an algorithm depends on the structure
of the input graph, predictions are much harder to come by. Still,
when we need to process huge numbers of huge graphs, we want
efficient algorithms for the same reasons that we want them for any
other problem domain, and we will continue to pursue the goals of
understanding the essential properties of algorithms and applications,
striving to identify those methods that can best handle the graphs that
might arise in practice.
To illustrate some of these issues, we revisit the study of graph
connectivity, a problem that we considered already in Chapter 1 (!).
Connectivity in random graphs has fascinated mathematicians for
years, and it has been the subject of an extensive literature. That
literature is beyond the scope of this book, but it provides a backdrop
that validates our use of the problem as the basis for some experimen-
tal studies that help us understand the basic algorithms that we use
and the types of graphs that we are considering.
For example, growing a graph by adding random edges to a set of
initially isolated vertices (essentially, the process behind Program 17.7)
is a well-studied process that has served as the basis for classical ran-
GRAPH SEARCH §18.9 135

Table 18.1 Connectivity in two random graph models

This table shows the number of connected components and the size of the
largest connected component for 100000-vertex graphs drawn from two
different distributions. For the random graph model, these experiments
support the well-known fact that the graph is highly likely to consist
primarily of one giant component if the average vertex degree is larger
than a small constant. The right two columns give experimental results
when we restrict the edges to be chosen from those that connect each
vertex to just one of 10 specified neighbors.

random edges random 10-neighbors

E C L C L
1000 99000 5 99003 3
2000 98000 4 98010 4
5000 95000 6 95075 5
10000 90000 8 90300 7
20000 80002 16 81381 9
50000 50003 1701 57986 27
100000 16236 79633 28721 151
200000 1887 98049 3818 6797
500000 4 99997 19 99979
1000000 1 100000 1 100000

Key:
C number of connected components
L size of largest connected component

dom graph theory. It is well known that, as the number of edges grows,
the graph coalesces into just one giant component. The literature on
random graphs gives extensive information about the nature of this
process, for example:

Property 18.13 If E > 12 V ln V + μV (with μ positive), a random


graph with V vertices and E edges consists of a single connected com-
ponent and an average of less than e−2μ isolated vertices, with proba-
bility approaching 1 as V approaches infinity.
136 §18.9 CHAPTER EIGHTEEN

Proof : This fact was established by seminal work of Erdös and Renyi
in 1960. The proof is beyond the scope of this book (see reference
section).
This property tells us that we can expect large nonsparse random
graphs to be connected. For example, if V > 1000 and E > 10V , then
μ > 10 − 12 ln 1000 > 6.5 and the average number of vertices not in
the giant component is (almost surely) less than e−13 < .000003. If we
generate a million random 1000-vertex graphs of density greater than
10, we might get a few with a single isolated vertex, but the rest will
all be connected.
Figure 18.30 compares random graphs with random neighbor
graphs, where we allow only edges that connect vertices whose indices
are within a small constant of one another. The neighbor-graph model
yields graphs that are evidently substantially different in character from
random graphs. We eventually get a giant component, but it appears
suddenly, when two large components merge.
Table 18.2 shows that these structural differences between ran-
dom graphs and neighbor graphs persist for V and E in ranges of prac-
tical interest. Such structural differences certainly may be reflected in
the performance of our algorithms.
Table 18.3 gives empirical results for the cost of finding the num-
ber of connected components in a random graph, using various al-
gorithms. Although the algorithms perhaps are not subject to direct
comparison for this specific task because they are designed to handle
different tasks, these experiments do validate a subset of the general
conclusions that we have drawn.
First, it is plain from the table that we should not use the
adjacency-matrix representation for large sparse graphs (and cannot
use it for huge ones), not just because the cost of initializing the array
is prohibitive, but also because the algorithm inspects every entry in
the array, so its running time is proportional to the size (V 2 ) of the
array rather than to the number of 1s in it (E). The table shows, for
example, that it takes about as long to process a graph that contains
1000 edges as it does to process one that contains 100000 edges when
we are using an adjacency matrix.
Second, it is also plain from Table 18.3 that the cost of allocating
memory for the list nodes is significant when we build adjacency lists
for large sparse graphs. The cost of building the lists is more than five
GRAPH SEARCH §18.9 137

Figure 18.30
Connectivity in random
graphs
This figures show the evolution
of two types of random graphs at
10 equal increments as a total of
2E edges are added to initially
empty graphs. Each plot is a his-
togram of the number of vertices
in components of size 1 through
V − 1 (left to right). We start out
with all vertices in components of
size 1 and end with nearly all ver-
tices in a giant component. The
plot at left is for a standard random
graph: the giant component forms
quickly, and all other components
are small. The plot at right is for a
random neighbor graph: compo-
nents of various sizes persist for a
longer time.
138 §18.9 CHAPTER EIGHTEEN

Table 18.2 Empirical study of graph-search algorithms

This table shows relative timings for various algorithms for the task of
determining the number of connected components (and the size of the
largest one) for graphs with various numbers of vertices and edges. As
expected, algorithms that use the adjacency-matrix representation are
slow for sparse graphs, but competitive for dense graphs. For this spe-
cialized task, the union-find algorithms that we considered in Chapter 1
are the fastest, because they build a data structure tailored to solve the
problem and do not need otherwise to represent the graph. Once the
data structure representing the graph has been built, however, DFS and
BFS are faster, and more flexible. Adding a test to stop when the graph
is known to consist of a single connected component significantly speeds
up DFS and union-find (but not BFS) for dense graphs.

adjacency matrix adjacency lists

E U U* I D D* I D D* B B*

5000 vertices
500 1 0 255 312 356 1 0 0 0 1
1000 0 1 255 311 354 1 0 0 0 1
5000 1 2 258 312 353 2 2 1 2 1
10000 3 3 258 314 358 5 2 1 2 1
50000 12 6 270 315 202 25 6 4 5 6
100000 23 7 286 314 181 52 9 2 10 11
500000 117 5 478 248 111 267 54 16 56 47
100000 vertices
5000 5 3 3 8 7 24 24
10000 4 5 6 7 7 24 24
50000 18 18 26 12 12 28 28
100000 34 35 51 28 24 34 34
500000 133 137 259 88 89

Key:
U weighted quick union with halving (Program 1.4)
I initial construction of the graph representation
D recursive DFS (Programs 18.1 and 18.2)
B BFS (Programs 18.9 and 18.10)
* exit when graph is found to be fully connected
GRAPH SEARCH §18.9 139

times the cost of traversing them. In the typical situation where we are
going to perform numerous searches of various types after building the
graph, this cost is acceptable. Otherwise, we might consider preallo-
cating the array to reduce the cost of memory allocation, as discussed
in Chapter 2.
Third, the absence of numbers in the DFS columns for large
sparse graphs is significant. These graphs cause excessive recursion
depth, which (eventually) cause the program to crash. If we want to
use DFS on such graphs, we need to use the nonrecursive version that
we discussed in Section 18.7.
Fourth, the table shows that the union-find–based method from
Chapter 1 is faster than DFS or BFS, primarily because it does not
have to represent the entire graph. Without such a representation,
however, we cannot answer simple queries such as “Is there an edge
connecting v and w?” so union-find–based methods are not suitable
if we want to do more than what they are designed to do (answer “Is
there a path between v and w?” queries intermixed with adding edges).
Once the internal representation of the graph has been built, it is not
worthwhile to implement a union-find algorithm just to determine
whether it is connected, because DFS or BFS can provide the answer
about as quickly.
When we run empirical tests that lead to tables of this sort,
various anomalies might require further explanation. For example,
on many computers, the cache architecture and other features of the
memory system might have dramatic impact on performance for large
graphs. Improving performance in critical applications may require
detailed knowledge of the machine architecture in addition to all the
factors that we are considering.
Careful study of these tables will reveal more properties of these
algorithms than we are able to address. Our aim is not to do an ex-
haustive comparison but to illustrate that, despite the many challenges
that we face when we compare graph algorithms, we can and should
run empirical studies and make use of any available analytic results,
both to get a feeling for the algorithms’ important characteristics and
to predict performance.
Exercises
◦ 18.70 Do an empirical study culminating in a table like Table 18.2 for the
problem of determining whether or not a graph is bipartite (two-colorable).
140 §18.9 CHAPTER EIGHTEEN

18.71 Do an empirical study culminating in a table like Table 18.2 for the
problem of determining whether or not a graph is biconnected.
◦ 18.72 Do an empirical study to find the expected size of the second largest
connected component in sparse graphs of various sizes, drawn from various
graph models (see Exercises 17.63–76).
18.73 Write a program that produces plots like those in Figure 18.30, and
test it on graphs of various sizes, drawn from various graph models (see
Exercises 17.63–76).
◦ 18.74 Modify your program from Exercise 18.73 to produce similar his-
tograms for the sizes of edge-connected components.
•• 18.75 The numbers in the tables in this section are results for only one sample.
We might wish to prepare a similar table where we run 1000 experiments for
each entry and give the sample mean and standard deviation, but we probably
could not include nearly as many entries. Would this approach be a better use
of computer time? Defend your answer.
CHAPTER NINETEEN

Digraphs and DAGs

W HEN WE ATTACH significance to the order in which the two


vertices are specified in each edge of a graph, we have an entirely
different combinatorial object known as a digraph, or directed graph.
Figure 19.1 shows a sample digraph. In a digraph, the notation s-t
describes an edge that goes from s to t, but provides no information 0
about whether or not there is an edge from t to s. There are four 6 7 8
different ways in which two vertices might be related in a digraph: 1 2
no edge; an edge s-t from s to t; an edge t-s from t to s; or two
3 9 10
edges s-t and t-s, which indicate connections in both directions. The 4
one-way restriction is natural in many applications, easy to enforce in 5 11 12
our implementations, and seems innocuous; but it implies added com-
binatorial structure that has profound implications for our algorithms 4-2 11-12 4-11 5-4
and makes working with digraphs quite different from working with 2-3 12-9 4-3 0-5
3-2 9-10 3-5 6-4
undirected graphs. Processing digraphs is akin to traveling around in a 0-6 9-11 7-8 6-9
city where all the streets are one-way, with the directions not necessar- 0-1 8-9 8-7 7-6
ily assigned in any uniform pattern. We can imagine that getting from 2-0 10-12
one point to another in such a situation could be a challenge indeed.
We interpret edge directions in digraphs in many ways. For Figure 19.1
example, in a telephone-call graph, we might consider an edge to be A directed graph (digraph)
directed from the caller to the person receiving the call. In a transaction A digraph is defined by a list of
graph, we might have a similar relationship where we interpret an edge nodes and edges (bottom), with
as representing cash, goods, or information flowing from one entity the order that we list the nodes
to another. We find a modern situation that fits this classic model on when specifying an edge implying
that the edge is directed from the
the Internet, with vertices representing Web pages and edges the links first node to the second. When
between the pages. In Section 19.4, we examine other examples, many drawing a digraph, we use arrows
of which model situations that are more abstract. to depict directed edges (top).

141
142 CHAPTER NINETEEN

One common situation is for the edge direction to reflect a prece-


dence relationship. For example, a digraph might model a manufac-
turing line: Vertices correspond to jobs to be done and an edge exists
from vertex s to vertex t if the job corresponding to vertex s must be
done before the job corresponding to vertex t. Another way to model
the same situation is to use a PERT chart: edges represent jobs and
vertices implicitly specify the precedence relationships (at each vertex,
all incoming jobs must complete before any outgoing jobs can begin).
How do we decide when to perform each of the jobs so that none of
the precedence relationships are violated? This is known as a schedul-
ing problem. It makes no sense if the digraph has a cycle so, in such
situations, we are working with directed acyclic graphs (DAGs). We
shall consider basic properties of DAGs and algorithms for this simple
scheduling problem, which is known as topological sorting, in Sec-
tions 19.5 through 19.7. In practice, scheduling problems generally
involve weights on the vertices or edges that model the time or cost of
each job. We consider such problems in Chapters 21 and 22.
The number of possible digraphs is truly huge. Each of the
V 2 possible directed edges (including self-loops) could be present or
2
not, so the total number of different digraphs is 2V . As illustrated
in Figure 19.2, this number grows very quickly, even by comparison
with the number of different undirected graphs and even when V is
undirected small. As with undirected graphs, there is a much smaller number of
V graphs digraphs
classes of digraphs that are isomorphic to each other (the vertices of
2 8 16
one can be relabeled to make it identical to the other), but we cannot
3 64 512
4 1024 65536 take advantage of this reduction because we do not know an efficient
5 32768 33554432 algorithm for digraph isomorphism.
6 2097152 68719476736 Certainly, any program will have to process only a tiny fraction of
7 268435456 562949953421312
the possible digraphs; indeed, the numbers are so large that we can be
Figure 19.2 certain that virtually all digraphs will not be among those processed by
Graph enumeration any given program. Generally, it is difficult to characterize the digraphs
While the number of different that we might encounter in practice, so we design our algorithms such
undirected graphs with V vertices that they can handle any possible digraph as input. On the one hand,
is huge, even when V is small,
this situation is not new to us (for example, virtually none of the
the number of different digraphs
with V vertices is much larger. For 1000! permutations of 1000 elements have ever been processed by any
undirected graphs, the number is sorting program). On the other hand, it is perhaps unsettling to know
given by the formula 2V (V +1)/2 ; for that, for example, even if all the electrons in the universe could run
2
digraphs, the formula is 2V . supercomputers capable of processing 1010 graphs per second for the
DIGRAPHS AND DAGS 143

estimated lifetime of the universe, those supercomputers would see far


fewer than 10−100 percent of the 10-vertex digraphs (see Exercise 19.9).
This brief digression on graph enumeration perhaps underscores
several points that we cover whenever we consider the analysis of algo-
rithms and indicates their particular relevance to the study of digraphs.
Is it important to design our algorithms to perform well in the worst
case, when we are so unlikely to see any particular worst-case digraph?
Is it useful to choose algorithms on the basis of average-case analysis,
or is that a mathematical fiction? If our intent is to have implementa-
tions that perform efficiently on digraphs that we see in practice, we are
immediately faced with the problem of characterizing those digraphs.
Mathematical models that can convincingly describe the digraphs that
we might expect in applications are even more difficult to develop than
are models for undirected graphs.
In this chapter, we revisit, in the context of digraphs, a subset of
the basic graph-processing problems that we considered in Chapter 17,
and we examine several problems that are specific to digraphs. In
particular, we look at DFS and several of its applications, including
cycle detection (to determine whether a digraph is a DAG); topological
sort (to solve, for example, the scheduling problem for DAGs that
was just described); and computation of the transitive closure and
the strong components (which have to do with the basic problem of
determining whether or not there is a directed path between two given
vertices). As in other graph-processing domains, these algorithms
range from the trivial to the ingenious; they are both informed by and
give us insight into the complex combinatorial structure of digraphs.
Exercises
• 19.1 Find a large digraph somewhere online—perhaps a transaction graph
in some online system, or a digraph defined by links on Web pages.
• 19.2 Find a large DAG somewhere online—perhaps one defined by function-
definition dependencies in a large software system or by directory links in a
large file system.
19.3 Make a table like Figure 19.2, but exclude from the counts graphs and
digraphs with self-loops.
19.4 How many digraphs are there that contain V vertices and E edges?
◦ 19.5 How many digraphs correspond to each undirected graph that contains
V vertices and E edges?
144 §19.1 CHAPTER NINETEEN

 19.6 How many digits do we need to express the number of digraphs that
have V vertices as a base-10 number?
◦ 19.7 Draw the nonisomorphic digraphs that contain 3 vertices.
••• 19.8 How many different digraphs are there with V vertices and E edges if
we consider two digraphs to be different only if they are not isomorphic?
◦ 19.9 Compute an upper bound on the percentage of 10-vertex digraphs that
could ever be examined by any computer, under the assumptions described in
the text and the additional ones that the universe has less than 1080 electrons
and that the age of the universe will be less than 1020 years.

19.1 Glossary and Rules of the Game


Our definitions for digraphs are nearly identical to those in Chapter 17
for undirected graphs (as are some of the algorithms and programs
that we use), but they are worth restating. The slight differences in
the wording to account for edge directions imply structural properties
that will be the focus of this chapter.
Definition 19.1 A digraph (or directed graph) is a set of vertices plus
a set of directed edges that connect ordered pairs of vertices (with no
duplicate edges). We say that an edge goes from its first vertex to its
second vertex.
As we did with undirected graphs, we disallow duplicate edges in
this definition but reserve the option of allowing them when convenient
for various applications and implementations. We explicitly allow self-
loops in digraphs (and usually adopt the convention that every vertex
has one) because they play a critical role in the basic algorithms.
Figure 19.3
A grid digraph Definition 19.2 A directed path in a digraph is a list of vertices in
This small digraph is similar to
which there is a (directed) digraph edge connecting each vertex in the
the large grid network that we list to its successor in the list. We say that a vertex t is reachable from
first considered in Chapter 1, ex- a vertex s if there is a directed path from s to t.
cept that it has a directed edge
on every grid line, with the di- We adopt the convention that each vertex is reachable from itself
rection randomly chosen. Even and normally implement that assumption by ensuring that self-loops
though the graph has relatively few are present in our digraph representations.
nodes, its connectivity properties
are not readily apparent. Is there Understanding many of the algorithms in this chapter requires
a directed path from the upper left understanding the connectivity properties of digraphs and the effect
corner to the lower right corner? of these properties on the basic process of moving from one vertex
DIGRAPHS AND DAGS §19.1 145

to another along digraph edges. Developing such an understanding


is more complicated for digraphs than for undirected graphs. For
0 1 2 3 4 5 6 7 8 9 10 11 12
example, we might be able to tell at a glance whether a small undirected 0 1 1 0 0 0 1 1 0 0 0 0 0 0
graph is connected or contains a cycle; these properties are not as easy 1 0 1 0 0 0 0 0 0 0 0 0 0 0
2 1 0 1 1 0 0 0 0 0 0 0 0 0
to spot in digraphs, as indicated in the typical example illustrated in 3 0 0 1 1 0 1 0 0 0 0 0 0 0
Figure 19.3. 4 0 0 1 1 1 0 0 0 0 0 0 1 0
5 0 0 0 0 1 1 0 0 0 0 0 0 0
While examples like this highlight differences, it is important 6 0 0 0 0 1 0 1 0 0 1 0 0 0
7 0 0 0 0 0 0 1 1 1 0 0 0 0
to note that what a human considers difficult may or may not be 8 0 0 0 0 0 0 0 1 1 1 0 0 0
relevant to what a program considers difficult—for instance, writing 9 0 0 0 0 0 0 0 0 0 1 1 1 0
10 0 0 0 0 0 0 0 0 0 0 1 0 1
a DFS function to find cycles in digraphs is no more difficult than 11 0 0 0 0 0 0 0 0 0 0 0 1 1
for undirected graphs. More important, digraphs and graphs have 12 0 0 0 0 0 0 0 0 0 1 0 0 1
essential structural differences. For example, the fact that t is reachable
0
from s in a digraph indicates nothing about whether s is reachable from 0 5 1 6
1
t. This distinction is obvious, but critical, as we shall see. 1
2
2 0 3
As mentioned in Section 17.3, the representations that we use for
3
3 5 2
digraphs are essentially the same as those that we use for undirected
4
4 3 11 2
graphs. Indeed, they are more straightforward because we represent
5
5 4
each edge just once, as illustrated in Figure 19.4. In the adjacency-list 6
6 9 4
representation, an edge s-t is represented as a list node containing t 7
7 6 8
in the linked list corresponding to s. In the adjacency-matrix repre- 8
8 7 9
sentation, we need to maintain a full V -by-V array and to represent 9
9 11 10
an edge s-t by a 1 bit in row s and column t. We do not put a 1 bit 10
10 12
in row t and column s unless there is also an edge t-s. In general, the 11
11 12
adjacency matrix for a digraph is not symmetric about the diagonal. In 12
12 9
both representations, we typically include self-loops (representations
of s-s for each vertex s).
Figure 19.4
There is no difference in these representations between an undi- Digraph representations
rected graph and a directed graph with self-loops at every vertex and The adjacency-array and adjacency-
two directed edges for each edge connecting distinct vertices in the lists representations of a digraph
undirected graph (one in each direction). Thus, we can use the algo- have only one representation of
each edge, as illustrated in the ad-
rithms that we develop in this chapter for digraphs to process undi- jacency array (top) and adjacency
rected graphs, provided that we interpret the results appropriately. In lists (bottom) representation of the
addition, we use the programs that we considered in Chapter 17 as the graph depicted in Figure 19.1.
These representations both include
basis for our digraph programs, taking care to remove references to self-loops at every vertex, which
and implicit assumptions about the second representation of each edge is typical for digraph-processing
that is not a self-loop. For example, to adapt for digraphs our pro- algorithms.
146 §19.1 CHAPTER NINETEEN

0
6 7 8 Program 19.1 Reversing a digraph (adjacency lists)
1 2 Given a digraph represented with adjacency lists, this function creates a
new adjacency-lists representation of a digraph that has the same vertices
3 9 10 and edges but with the edge directions reversed.
4
5 11 12 Graph GRAPHreverse(Graph G)
{ int v; link t;
0 1 2 3 4 5 6 7 8 9 10 11 12 Graph R = GRAPHinit(G->V);
0 1 0 1 0 0 0 0 0 0 0 0 0 0 for (v = 0; v < G->V; v++)
1 1 1 0 0 0 0 0 0 0 0 0 0 0
2 0 0 1 1 1 0 0 0 0 0 0 0 0 for (t = G->adj[v]; t != NULL; t = t->next)
3 0 0 1 1 1 0 0 0 0 0 0 0 0 GRAPHinsertE(R, EDGE(t->v, v));
4 0 0 0 0 1 1 1 0 0 0 0 0 0
5 1 0 0 1 0 1 0 0 0 0 0 0 0 return R;
6 1 0 0 0 0 0 1 1 0 0 0 0 0 }
7 0 0 0 0 0 0 0 1 1 0 0 0 0
8 0 0 0 0 0 0 0 1 1 0 0 0 0
9 0 0 0 0 0 0 1 0 1 1 0 0 1
10 0 0 0 0 0 0 0 0 0 1 1 0 0 grams for generating, building, and showing graphs (Programs 17.1
11 0 0 0 0 1 0 0 0 0 1 0 1 0
12 0 0 0 0 0 0 0 0 0 0 1 1 1 through 17.9), we remove the statement
G->adj[w] = NEW(v, G->adj[w]);
0
0 2 from the function GRAPHinsertE in the adjacency-lists version (Pro-
1
1 0 gram 17.6), remove the references to G->adj[w][v] from the functions
2
2 4 3 GRAPHinsertE and GRAPHremoveE in the adjacency-matrix version
3
3 4 2 (Program 17.3), and make the appropriate adjustments to GRAPHedges
4
4 6 5 in both versions.
5
5 3 0 The indegree of a vertex in a digraph is the number of directed
6
6 7 0 edges that lead to that vertex. The outdegree of a vertex in a digraph
7
7 8 is the number of directed edges that emanate from that vertex. No
8
8 7 vertex is reachable from a vertex of outdegree 0, which is called a
9
9 12 8 6 sink; a vertex of indegree 0, which is called a source, is not reachable
10
10 9 from any other vertex. A digraph where self-loops are allowed and
11
11 9 4 every vertex has outdegree 1 is called a map (a function from the set
12
12 11 10 of integers from 0 to V − 1 onto itself). We can easily compute the
indegree and outdegree of each vertex, and find sources and sinks, in
Figure 19.5 linear time and space proportional to V , using vertex-indexed arrays
Digraph reversal (see Exercise 19.19).
Reversing the edges of a digraph The reverse of digraph is the digraph that we obtain by switching
corresponds to transposing the
adjacency matrix but requires re- the direction of all the edges. Figure 19.5 shows the reverse and its
building the adjacency lists (see representations for the digraph of Figure 19.1. We use the reverse in
Figures 19.1 and 19.4). digraph algorithms when we need to know from where edges come
DIGRAPHS AND DAGS §19.1 147

because our standard representations tell us only where the edges go.
For example, indegree and outdegree change roles when we reverse a
digraph.
For an adjacency-matrix representation, we could compute the
reverse by making a copy of the array and transposing it (interchanging
its rows and columns). If we know that the graph is not going to be
modified, we can actually use the reverse without any extra computa-
tion by simply interchanging our references to the rows and columns
when we want to refer to the reverse. That is, an edge s-t in a digraph
G is indicated by a 1 in G->adj[s][t], so, if we were to compute the
reverse R of G, it would have a 1 in R->adj[t][s]; we do not need
to do so, however, because, if we want to check whether there is an
edge from s to t in the reverse of G, we can just check G->adj[t][s].
This opportunity may seem obvious, but it is often overlooked. For
an adjacency-lists representation, the reverse is a completely different
data structure, and we need to take time proportional to the number
of edges to build it, as shown in Program 19.1.
Yet another option, which we shall address in Chapter 22, is
to maintain two representations of each edge, in the same manner as
we do for undirected graphs (see Section 17.3) but with an extra bit
that indicates edge direction. For example, to use this method in an
adjacency-lists representation we would represent an edge s-t by a 0
6 7 8
node for t on the adjacency list for s (with the direction bit set to
1 2
indicate that to move from s to t is a forward traversal of the edge)
and a node for s on the adjacency list for t (with the direction bit set to 3 9 10
indicate that to move from t to s is a backward traversal of the edge). 4
This representation supports algorithms that need to traverse edges in 5 11 12
digraphs in both directions. It is also generally convenient to include
pointers connecting the two representations of each edge in such cases. 2-3 11-12 3-5 6-4
We defer considering this representation in detail to Chapter 22, where 0-6 9-12 8-7 6-9
0-1 9-10 5-4 7-6
it plays an essential role. 2-0 9-11 0-5
In digraphs, by analogy to undirected graphs, we speak of di-
rected cycles, which are directed paths from a vertex back to itself, Figure 19.6
A directed acyclic graph
and simple directed paths and cycles, where the vertices and edges are (DAG)
distinct. Note that s-t-s is a cycle of length 2 in a digraph but that
This digraph has no cycles, a prop-
cycles in undirected graphs must have at least three distinct vertices. erty that is not immediately appar-
In many applications of digraphs, we do not expect to see any ent from the edge list or even from
cycles, and we work with yet another type of combinatorial object. examining its drawing.
148 §19.1 CHAPTER NINETEEN

Definition 19.3 A directed acyclic graph (DAG) is a digraph with no


directed cycles.
We expect DAGs, for example, in applications where we are
using digraphs to model precedence relationships. DAGs not only
arise naturally in these and other important applications, but also, as
we shall see, in the study of the structure of general digraphs. A sample
DAG is illustrated in Figure 19.6.
Directed cycles are therefore the key to understanding connectiv-
ity in digraphs that are not DAGs. An undirected graph is connected
if there is a path from every vertex to every other vertex; for digraphs,
we modify the definition as follows:
Definition 19.4 A digraph is strongly connected if every vertex is
strongly connected reachable from every vertex.
component
The graph in Figure 19.1 is not strongly connected because, for
example, there are no directed paths from vertices 9 through 12 to any
of the other vertices in the graph.
As indicated by strong, this definition implies a stronger relation-
ship between each pair of vertices than reachability. In any digraph, we
sink
say that a pair of vertices s and t are strongly connected or mutually
source reachable if there is a directed path from s to t and a directed path
from t to s. (Our convention that each vertex is reachable from itself
directed cycle implies that each vertex is strongly connected to itself.) A digraph
is strongly connected if and only if all pairs of vertices are strongly
connected. The defining property of strongly connected digraphs is
one that we take for granted in connected undirected graphs: if there
is a path from s to t, then there is a path from t to s. In the case of
Figure 19.7 undirected graphs, we know this fact because the same path fits the
Digraph terminology bill, traversed in the other direction; in digraphs, it must be a different
Sources (vertices with no edges path.
coming in) and sinks (vertices with Another way of saying that a pair of vertices is strongly connected
no edges going out) are easy to
is to say that they lie on some directed cyclic path. Recall that we use
identify in digraph drawings like
this one, but directed cycles and the term cyclic path instead of cycle to indicate that the path does
strongly connected components are not need to be simple. For example, in Figure 19.1, 5 and 6 are
more difficult to identify. What is strongly connected because 6 is reachable from 5 via the directed path
the longest directed cycle in this
5-4-2-0-6 and 5 is reachable from 6 via the directed path 6-4-3-5;
digraph? How many strongly con-
nected components with more than and these paths imply that 5 and 6 lie on the directed cyclic path
one vertex are there? 5-4-2-0-6-4-3-5, but they do not lie on any (simple) directed cycle.
DIGRAPHS AND DAGS §19.1 149

Note that no DAG that contains more than one vertex is strongly
connected.
Like simple connectivity in undirected graphs, this relation is
transitive: If s is strongly connected to t and t is strongly connected
to u, then s is strongly connected to u. Strong connectivity is an
equivalence relation that divides the vertices into equivalence classes
containing vertices that are strongly connected to each other. (See
Section 19.4 for a detailed discussion of equivalence relations.) Again,
strong connectivity provides a property for digraphs that we take for
granted with respect to connectivity in undirected graphs.
Property 19.1 A digraph that is not strongly connected comprises a
set of strongly connected components (strong components, for short),
which are maximal strongly connected subgraphs, and a set of directed
edges that go from one component to another.
Proof : Like components in undirected graphs, strong components in
digraphs are induced subgraphs of subsets of vertices: Each vertex is
in exactly one strong component. To prove this fact, we first note that
every vertex belongs to at least one strong component, which contains
(at least) the vertex itself. Then we note that every vertex belongs to
at most one strong component: If a vertex were to belong to two dif-
ferent ones, then there would be paths through that vertex connecting
vertices in those components to each other, in both directions, which
contradicts the maximality of both components.
For example, a digraph that consists of a single directed cycle
has just one strong component. At the other extreme, each vertex in
a DAG is a strong component, so each edge in a DAG goes from one
component to another. In general, not all edges in a digraph are in
the strong components. This situation is in contrast to the analogous
situation for connected components in undirected graphs, where every
vertex and every edge belongs to some connected component, but
similar to the analogous situation for edge-connected components in
undirected graphs. The strong components in a digraph are connected
by edges that go from a vertex in one component to a vertex in another,
but do not go back again.
Property 19.2 Given a digraph D, define another digraph K(D)
with one vertex corresponding to each strong component of D and
one edge in K(D) corresponding to each edge in D that connects
150 §19.1 CHAPTER NINETEEN

vertices in different strong components (connecting the vertices in K


that correspond to the strong components that it connects in D). Then,
K(D) is a DAG (which we call the kernel DAG of D).
Proof : If K(D) were to have a directed cycle, then vertices in two
different strong components of D would fall on a directed cycle, and
that would be a contradiction.
Figure 19.8 shows the strong components and the kernel DAG
for a sample digraph. We look at algorithms for finding strong com-
ponents and building kernel DAGs in Section 19.6.
From these definitions, properties, and examples, it is clear that
0
6 7 8 we need to be precise when referring to paths in digraphs. We need to
1 2 consider at least the following three situations:
Connectivity We reserve the term connected for undirected
3 9 10 graphs. In digraphs, we might say that two vertices are connected if
4
they are connected in the undirected graph defined by ignoring edge
5 11 12
directions, but we generally avoid such usage.
Reachability In digraphs, we say that vertex t is reachable
0 1 2 3 4 5 6 7 8 9 10 11 12
sc 2 1 2 2 2 2 2 3 3 0 0 0 0
from vertex s if there is a directed path from s to t. We generally avoid
the term reachable when referring to undirected graphs, although we
might consider it to be equivalent to connected because the idea of one
2 3
vertex being reachable from another is intuitive in certain undirected
1 0 graphs (for example, those that represent mazes).
Strong connectivity Two vertices in a digraph are strongly con-
nected if they are mutually reachable; in undirected graphs, two con-
Figure 19.8 nected vertices imply the existence of paths from each to the other.
Strong components and kernel
DAG Strong connectivity in digraphs is similar in certain ways to edge con-
nectivity in undirected graphs.
This digraph (top) consists of four
strong components, as identified We wish to support digraph ADT operations that take two ver-
(with arbitrary integer labels) by tices s and t as arguments and allow us to test whether
the vertex-indexed array sc (cen- • t is reachable from s
ter). Component 0 consists of the • s and t are strongly connected (mutually reachable)
vertices 9, 10, 11 and 12; compo-
nent 1 consists of the single ver- What resource requirements are we willing to expend for these opera-
tex 1; component 2 consists of the tions? As we saw in Section 17.5, DFS provides a simple solution for
vertices 0, 2, 3, 4, 5, and 6; and connectivity in undirected graphs that takes time proportional to V ,
component 3 consists of the ver- but if we are willing to invest preprocessing time proportional to V +E
tices 7 and 8. If we draw the graph
defined by the edges between dif-
and space proportional to V , we can answer connectivity queries in
ferent components, we get a DAG constant time. Later in this chapter, we examine algorithms for strong
(bottom). connectivity that have these same performance characteristics.
DIGRAPHS AND DAGS §19.1 151

But our primary aim is to address the fact that reachability queries
in digraphs are more difficult to handle than connectivity or strong con-
nectivity queries. In this chapter, we examine classical algorithms that
require preprocessing time proportional to V E and space proportional
to V 2 , develop implementations that can achieve constant-time reach-
ability queries with linear space and preprocessing time for some di-
graphs, and study the difficulty of achieving this optimal performance
for all digraphs.
Exercises
 19.10 Give the adjacency-lists structure that is built by Program 17.6, modi-
fied as described in the text to process digraphs, for the digraph
3-7 1-4 7-8 0-5 5-2 3-8 2-9 0-6 4-9 2-6 6-4.

 19.11 Implement an ADT for sparse digraphs, based on Program 17.6, mod-
ified as described in the text. Include a random-digraph generator based on
Program 17.7. Write a client program to generate random digraphs for a well-
chosen set of values of V and E, such that you can use it to run meaningful
empirical tests on digraphs drawn from this model.
 19.12 Implement an ADT for dense digraphs, based on Program 17.3, mod-
ified as described in the text. Include a random-digraph generator based on
Program 17.8. Write a client program to generate random graphs for a well-
chosen set of values of V and E, such that you can use it to run meaningful
empirical tests on graphs drawn from this model.
◦ 19.13 Write a program
√ that
√ generates random digraphs by connecting ver-
tices arranged in a V -by- V grid to their neighbors, with edge directions
randomly chosen (see Figure 19.3).
◦ 19.14 Augment your program from Exercise 19.13 to add R extra random
edges (all possible eges equally likely). For large R, shrink the grid so that the
total number of edges remains about V . Test your program as described in
Exercise 19.11.
◦ 19.15 Modify your program from Exercise 19.14 such that an extra edge
goes from a vertex s to a vertex t with probability inversely proportional to
the Euclidean distance between s and t.
◦ 19.16 Write a program that generates V random intervals in the unit interval,
all of length d, then builds a digraph with an edge from interval s to interval t if
and only if at least one of the endpoints of s falls within t (see Exercise 17.73).
Determine how to set d so that the expected number of edges is E. Test your
program as described in Exercise 19.11 (for low densities) and as described
in Exercise 19.12 (for high densities).
• 19.17 Write a program that chooses V vertices and E edges from the real
digraph that you found for Exercise 19.1. Test your program as described
152 §19.2 CHAPTER NINETEEN

in Exercise 19.11 (for low densities) and as described in Exercise 19.12 (for
high densities).
• 19.18 Write a program that produces each of the possible digraphs with V
vertices and E edges with equal likelihood (see Exercise 17.69). Test your
program as described in Exercise 19.11 (for low densities) and as described
in Exercise 19.12 (for high densities).
 19.19 Add digraph ADT functions that return the number of sources and
sinks in the digraph. Modify the adjacency-lists ADT implementation (see
Exercise 19.11) such that you can implement the functions in constant time.
◦ 19.20 Use your program from Exercise 19.19 to find the average number of
sources and sinks in various types of digraphs (see Exercises 19.11–18).
 19.21 Show the adjacency-lists structure that is produced when you use Pro-
gram 19.1 to find the reverse of the digraph
3-7 1-4 7-8 0-5 5-2 3-8 2-9 0-6 4-9 2-6 6-4.

◦ 19.22 Characterize the reverse of a map.


19.23 Design a digraph ADT that explicitly provides clients with the capability
to refer to both a digraph and its reverse, and provide an implementation, using
an adjacency-matrix representation.
19.24 Provide an implementation for your ADT in Exercise 19.23 that main-
tains adjacency-lists representations of both the digraph and its reverse.
 19.25 Describe a family of strongly connected digraphs with V vertices and
no (simple) directed cycles of length greater than 2.
◦ 19.26 Give the strong components and a kernel DAG of the digraph
3-7 1-4 7-8 0-5 5-2 3-8 2-9 0-6 4-9 2-6 6-4.

• 19.27 Give a kernel DAG of the grid digraph shown in Figure 19.3.
19.28 How many digraphs have V vertices, all of outdegree k?
• 19.29 What is the expected number of different adjacency-list representations
of a random digraph? Hint: Divide the total number of possible representa-
tions by the total number of digraphs.

19.2 Anatomy of DFS in Digraphs


We can use our DFS code for undirected graphs from Chapter 18 to
visit each edge and each vertex in a digraph. The basic principle behind
the recursive algorithm holds: To visit every vertex that can be reached
from a given vertex, we mark the vertex as having been visited, then
DIGRAPHS AND DAGS §19.2 153

(recursively) visit all the vertices that can be reached from each of the
0 7
vertices on its adjacency list.
5 1 6 6 8
In an undirected graph, we have two representations of each edge,
4 9 4 7 9
but the second representation that is encountered in a DFS always leads
3 11 2
to a marked vertex and is ignored (see Section 18.2). In a digraph,
5 2 12
we have just one representation of each edge, so we might expect DFS
0 3 9
algorithms to be more straightforward. But digraphs themselves are
11 10
more complicated combinatorial objects than undirected graphs, so
12
this expectation is not justified. For example, the search trees that
0 1 2 3 4 5 6 7 8 9 10 11 12
we use to understand the operation of the algorithm have a more pre 0 9 4 3 2 1 10 11 12 7 8 5 6
complicated structure for digraphs than for undirected graphs. This post 10 8 0 1 6 7 9 12 11 3 2 5 4

complication makes digraph-processing algorithms more difficult to


Figure 19.9
devise. For example, as we will see, it is more difficult to make in- DFS forest for a digraph
ferences about directed paths in digraphs than it is to make inferences
This forest describes a standard
about paths in graphs. adjacency-lists DFS of the sample
As we did in Chapter 18, we use the term standard adjacency- digraph in Figure 19.1. External
lists DFS to refer to the process of inserting a sequence of edges into nodes represent previously visited
internal nodes with the same la-
a digraph ADT implemented with an adjacency-lists representation bel; otherwise the forest is a rep-
(Program 17.6, modified to insert just one directed edge), then doing resentation of the digraph, with all
a DFS with Programs 18.3 and 18.2 and the parallel term standard edges pointing down. There are
four types of edges: tree edges,
adjacency-matrix DFS to refer to the process of inserting a sequence of
to internal nodes; back edges, to
edges into a digraph ADT implemented with an adjacency-matrix rep- external nodes representing ances-
resentation (Program 17.3, modified to insert just one directed edge), tors (shaded circles); down edges,
then doing a DFS with Programs 18.3 and 18.1. to external nodes representing de-
scendants (shaded squares); and
For example, Figure 19.9 shows the recursive-call tree that de- cross edges, to external nodes rep-
scribes the operation of a standard adjacency-matrix DFS on the sam- resenting nodes that are neither
ple digraph in Figure 19.1. Just as for undirected graphs, such trees ancestors nor descendants (white
have internal nodes that correspond to calls on the recursive DFS func- squares). We can determine the
type of edges to visited nodes, by
tion for each vertex, with links to external nodes that correspond to comparing the preorder and post-
edges that take us to vertices that have already been seen. Classifying order numbers (bottom) of their
the nodes and links gives us information about the search (and the source and destination:
digraph), but the classification for digraphs is quite different from the pre post example type
< > 4-2 down
classification for undirected graphs.
> < 2-0 back
In undirected graphs, we assigned each link in the DFS tree to > > 7-6 cross
one of four classes according to whether it corresponded to a graph For example, 7-6 is a cross edge
edge that led to a recursive call and to whether it corresponded to the because 7‘s preorder and postorder
first or second representation of the edge encountered by the DFS. In numbers are both larger than 6’s.
154 §19.2 CHAPTER NINETEEN

digraphs, there is a one-to-one correspondence between tree links and


graph edges, and they fall into four distinct classes:
• Those representing a recursive call (tree edges)
• Those from a vertex to an ancestor in its DFS tree (back edges)
• Those from a vertex to a descendant in its DFS tree (down edges)
• Those from a vertex to another vertex that is neither an ancestor
nor a descendant in its DFS tree (cross edges)
A tree edge is an edge to an unvisited vertex, corresponding to a
recursive call in the DFS. Back, cross, and down edges go to visited
vertices. To identify the type of a given edge, we use preorder and
postorder numbering.
Property 19.3 In a DFS forest corresponding to a digraph, an edge
to a visited node is a back edge if it leads to a node with a higher
postorder number; otherwise, it is a cross edge if it leads to a node
with a lower preorder number and a down edge if it leads to a node
with a higher preorder number.
Proof : These facts follow from the definitions. A node’s ancestors in a
DFS tree have lower preorder numbers and higher postorder numbers;
its descendants have higher preorder numbers and lower postorder
numbers. Both numbers are lower in previously visited nodes in other
DFS trees and both numbers are higher in yet-to-be-visited nodes in
other DFS trees but we do not need code that tests for these cases.
Program 19.2 is a prototype DFS digraph search function that
identifies the type of each edge in the digraph. Figure 19.10 illustrates
its operation on the example digraph of Figure 19.1. During the
search, testing to see whether an edge leads to a node with a higher
postorder number is equivalent to testing whether a postorder number
has yet been assigned. Any node for which a preorder number has been
assigned but for which a postorder number has not yet been assigned is
an ancestor in the DFS tree and will therefore have a postorder number
higher than that of the current node.
As we saw in Chapter 17 for undirected graphs, the edge types
are properties of the dynamics of the search, rather than of only the
graph. Indeed, different DFS forests of the same graph can differ
remarkably in character, as illustrated in Figure 19.11. For example,
even the number of trees in the DFS forest depends upon the start
vertex.
DIGRAPHS AND DAGS §19.2 155

Program 19.2 DFS of a digraph


This DFS function for digraphs represented with adjacency lists is in-
strumented to show the role that each edge in the graph plays in the
DFS. It assumes that Program 18.3 is augmented to declare and initial-
ize the array post and the counter cntP in the same way as pre and
cnt, respectively. See Figure 19.10 for sample output and a discussion 0-0 tree
about implementing show. 0-5 tree
5-4 tree
void dfsR(Graph G, Edge e) 4-3 tree
{ link t; int i, v, w = e.w; Edge x; 3-5 back
show("tree", e); 3-2 tree
pre[w] = cnt++; 2-0 back
2-3 back
for (t = G->adj[w]; t != NULL; t = t->next) 4-11 tree
if (pre[t->v] == -1) 11-12 tree
dfsR(G, EDGE(w, t->v)); 12-9 tree
else 9-11 back
9-10 tree
{ v = t->v; x = EDGE(w, v); 10-12 back
if (post[v] == -1) show("back", x); 4-2 down
else if (pre[v] > pre[w]) show("down", x); 0-1 tree
else show("cross", x); 0-6 tree
6-9 cross
} 6-4 cross
post[w] = cntP++; 7-7 tree
} 7-6 cross
7-8 tree
8-7 back
Despite these differences, several classical digraph-processing al- 8-9 cross
gorithms are able to determine digraph properties by taking appropri-
Figure 19.10
ate action when they encounter the various types of edges during a Digraph DFS trace
DFS. For example, consider the following basic problem:
This DFS trace for the example di-
Directed cycle detection Does a given digraph have any di- graph in Figure 19.1 corresponds
rected cycles? (Is the digraph a DAG?) In undirected graphs, any edge precisely to a preorder walk of the
to a visited vertex indicates a cycle in the graph; in digraphs, we must DFS tree depicted in Figure 19.9.
As for Figure 17.17 and other sim-
restrict our attention to back edges.
ilar traces, we can modify Pro-
Property 19.4 A digraph is a DAG if and only if we encounter no gram 19.2 to produce precisely
this output by adding a global vari-
back edges when we use DFS to examine every edge.
able depth that keeps track of the
Proof : Any back edge belongs to a directed cycle that consists of the depth of the recursion and an im-
plementation of show that prints
edge plus the tree path connecting the two nodes, so we will find no
out depth spaces followed by an
back edges when using DFS on a DAG. To prove the converse, we appropriate printf of its argu-
show that, if the digraph has a cycle, then the DFS encounters a back ments.
156 §19.2 CHAPTER NINETEEN

Figure 19.11 1 2 7 6 7
DFS forests for a digraph 0 3 6 8 9 4 6 8
5 1 6 7 9 11 10 3 11 2 7 9
These forests describes depth-first
4 9 4 12 12 5 2
search of the same graph as Fig- 9 4 0 3
3 11 2
ure 19.9, when the graph search 5 1 6
5 2 12
function checks the vertices (and 9
calls the recursive function for 11 10 7
the unvisited ones) in the order 12 6 8
s, s+1, ..., V-1, 0, 1, ..., s-1 9 4 7 9

for each s. The forest structure is 2 7


11 10 3 11 2
12 12 5 2
determined both by the search dy- 0 3 6 8
9 4 0 3
namics and the graph structure. 5 1 6 7 9
5 1 6
Each node has the same children 4 9 4
(the nodes on its adjacency list, in 3 11 2
8
order) in every forest. The leftmost 5 2 12
7 9
tree in each forest contains all the 9
6 8
nodes reachable from its root, but 11 10
9 4
reachability inferences about other 12
11 10 3 11 2
nodes are complicated because of 12 12 5 2
back, cross, and down edges. Even 3 7 9 4 0 3

the number of trees in the forest 5 2 6 8 5 1 6


4 7 9
depends on the starting node, so
3 11 2 7
we do not necessarily have a direct 9 0
12 0 3 11 10 5 1 6 6 8
correspondence between trees in
9 5 1 6 12 12 4 9 4 7 9
the forest and strong components,
11 10 9 4 9 3 11 2
the way that we did for compo- 12 5 2
nents in undirected graphs. For 0 3
example, we see that all vertices 4 7
are reachable from 8 only when 3 11 2 6 8 10 0 7
we start the DFS at 8. 5 2 7 9 12 5 1 6 6 8
9 4 9 4 7 9
4 0 3
11 10 3 11 2
5 1 6
12 5 2
9 4
0 3
11 10
12 12
11 0 7
9
12 5 1 6 6 8
9 4 9 4 7 9
5 7 11 10 3 11 2
4 6 8 12 5 2
3 11 2 7 9 0 3
5 2
0 3 12 0 7
5 1 6 9 5 1 6 6 8
9 4 11 10 4 9 4 7 9

11 10 12 12 3 11 2

12 12 5 2
0 3
9
DIGRAPHS AND DAGS §19.2 157

edge. Suppose that v is the first of the vertices on the cycle that is
visited by the DFS. That vertex has the lowest preorder number of all
the vertices on the cycle. The edge that points to it will therefore be a
back edge: It will be encountered during the recursive call for v (for
a proof that it must be, see Property 19.5); and it points from some
node on the cycle to v, a node with a lower preorder number (see
Property 19.3).
We can convert any digraph into a DAG by doing a DFS and
removing any graph edges that correspond to back edges in the DFS.
For example, Figure 19.9 tells us that removing the edges 2-0, 3-5,
2-3, 9-11, 10-12, 4-2, and 8-7 makes the digraph in Figure 19.1 a
DAG. The specific DAG that we get in this way depends on the graph
representation and the associated implications for the dynamics of the
DFS (see Exercise 19.38). This method is a useful way to generate
large arbitrary DAGs randomly (see Exercise 19.79) for use in testing
DAG-processing algorithms.
Directed cycle detection is a simple problem, but contrasting
the solution just described with the solution that we considered in
Chapter 18 for undirected graphs gives insight into the necessity to
consider the two types of graphs as different combinatorial objects,
even though their representations are similar and the same programs
work on both types for some applications. By our definitions, we seem
to be using the same method to solve this problem as for cycle detection
in undirected graphs (look for back edges), but the implementation
that we used for undirected graphs would not work for digraphs.
For example, in Section 18.5 we were careful to distinguish between
parent links and back links, since the existence of a parent link does
not indicate a cycle (cycles in undirected graphs must involve at least
three vertices). But to ignore links back to a node’s parents in digraphs
would be incorrect; we do consider a doubly-connected pair of vertices
in a digraph to be a cycle. Theoretically, we could have defined back
edges in undirected graphs in the same way as we have done here, but
then we would have needed an explicit exception for the two-vertex
case. More important, we can detect cycles in undirected graphs in
time proportional to V (see Section 18.5), but we may need time
proportional to E to find a cycle in a digraph (see Exercise 19.33).
The essential purpose of DFS is to provide a systematic way to
visit all the vertices and all the edges of a graph. It therefore gives
158 §19.2 CHAPTER NINETEEN

us a basic approach for solving reachability problems in digraphs,


although, again, the situation is more complicated than for undirected
graphs.
Single-source reachability Which vertices in a given digraph
can be reached from a given start vertex s? How many such vertices
are there?
Property 19.5 With a recursive DFS starting at s, we can solve the
single-source reachability problem for a vertex s in time proportional
to the number of edges in the subgraph induced by the reachable
vertices.
Proof : This proof is essentially the same as the proof of Property 18.1,
but it is worth restating to underline the distinction between reacha-
bility in digraphs and connectivity in undirected graphs. The property
Figure 19.12 is certainly true for a digraph that has one vertex and no edges. For
Decomposing a digraph
any digraph that has more than one vertex, we assume the property to
To prove by induction that DFS
be true for all digraphs that have fewer vertices. Now, the first edge
takes us everywhere reachable
from a given node in a digraph, that we take from s divides the digraph into the subgraphs induced by
we use essentially the same proof two subsets of vertices (see Figure 19.12): (i) the vertices that we can
as for Trémaux exploration. The reach by directed paths that begin with that edge and do not otherwise
key step is depicted here as a include s; and (ii) the vertices that we cannot reach with a directed path
maze (top), for comparison with
Figure 18.4. We break the graph that begins with that edge without returning to s. We apply the induc-
into two smaller pieces (bottom), tive hypothesis to these subgraphs, noting that there are no directed
induced by two sets of vertices: edges from a vertex in the first subgraph to any vertex other than s in
those vertices that can be reached the second subgraph (such an edge would be a contradiction because
by following the first edge from
the start vertex without revisiting its destination vertex should be in the first subgraph), that directed
it (bottom piece); and those ver- edges to s will be ignored because it has the lowest preorder number,
tices that cannot be reached by and that all the vertices in the first subgraph have lower preorder num-
following the first edge without go- bers than any vertex in the second subgraph, so all directed edges from
ing back through the start vertex
(top piece). Any edge that goes
a vertex in the second subgraph to a vertex in the first subgraph will
from a vertex in the first set to the be ignored.
start vertex is skipped during the
search of the first set because of By contrast with undirected graphs, a DFS on a digraph does not
the mark on the start vertex. Any give full information about reachability from any vertex other than
edge that goes from a vertex in the the start node, because tree edges are directed and because the search
second set to a vertex in the first structures have cross edges. When we leave a vertex to travel down
set is skipped because all vertices
in the first set are marked before a tree edge, we cannot assume that there is a way to get back to that
the search of the second subgraph vertex via digraph edges; indeed, there is not, in general. For example,
begins. there is no way to get back to 4 after we take the tree edge 4-11
DIGRAPHS AND DAGS §19.2 159

in Figure 19.9. Moreover, when we ignore cross and forward edges


(because they lead to vertices that have been visited and are no longer
active), we are ignoring information that they imply (the set of vertices
that are reachable from the destination). For example, following the
cross edge 6-9 in Figure 19.9 is the only way for us to find out that
10, 11, and 12 are reachable from 6.
To determine which vertices are reachable from another vertex,
we apparantly need to start over with a new DFS from that vertex
(see Figure 19.11). Can we make use of information from previous
searches to make the process more efficient for later ones? We consider
such reachability questions in Section 19.7.
To determine connectivity in undirected graphs, we rely on know-
ing that vertices are connected to their ancestors in the DFS tree,
through (at least) the path in the tree. By contrast, the tree path
goes in the wrong direction in a digraph: There is a directed path from
a vertex in a digraph to an ancestor only if there is a back edge from a
descendant to that or a more distant ancestor. Moreover, connectivity
in undirected graphs for each vertex is restricted to the DFS tree rooted
at that vertex; in contrast, in digraphs, cross edges can take us to any
previously visited part of the search structure, even one in another tree
in the DFS forest. For undirected graphs, we were able to take advan-
tage of these properties of connectivity to identify each vertex with a
connected component in a single DFS, then to use that information as
the basis for a constant-time ADT operation to determine whether any
two vertices are connected. For digraphs, as we see in this chapter, this
goal is elusive.
We have emphasized throughout this and the previous chapter
that different ways of choosing unvisited vertices lead to different
search dynamics for DFS. For digraphs, the structural complexity of
the DFS trees leads to differences in search dynamics that are even more
pronounced than those we saw for undirected graphs. For example,
Figure 19.11 illustrates that we get marked differences for digraphs
even when we simply vary the order in which the vertices are exam-
ined in the top-level search function. Only a tiny fraction of even these
possibilities is shown in the figure—in principle, each of the V ! dif-
ferent orders of examining vertices might lead to different results. In
Section 19.7, we shall examine an important algorithm that specifically
takes advantage of this flexibility, processing the unvisited vertices at
160 §19.2 CHAPTER NINETEEN

the top level (the roots of the DFS trees) in a particular order that
immediately exposes the strong components.
Exercises
 19.30 Add code to Program 19.2 to print out an indented DFS trace, as
described in the commentary to Figure 19.10.
19.31 Draw the DFS forest that results from a standard adjacency-lists DFS
of the digraph
3-7 1-4 7-8 0-5 5-2 3-8 2-9 0-6 4-9 2-6 6-4.

19.32 Draw the DFS forest that results from a standard adjacency-matrix DFS
of the digraph
3-7 1-4 7-8 0-5 5-2 3-8 2-9 0-6 4-9 2-6 6-4.

◦ 19.33 Describe a family of digraphs with V vertices and E edges for which a
standard adjacency-lists DFS requires time proportional to E for cycle detec-
tion.
 19.34 Show that, during a DFS in a digraph, no edge connects a node to
another node whose preorder and postorder numbers are both smaller.
◦ 19.35 Show all possible DFS forests for the digraph
0-1 0-2 0-3 1-3 2-3.
Tabulate the number of tree, back, cross, and down edges for each forest.
19.36 If we denote the number of tree, back, cross, and down edges by t, b,
c, and d, respectively, then we have t + b + c + d = E and t < V for any DFS
of any digraph with V vertices and E edges. What other relationships among
these variables can you infer? Which of the values are dependent solely on
graph properties and which are dependent on dynamic properties of the DFS?
 19.37 Prove that every source in a digraph must be a root of some tree in the
forest corresponding to any DFS of that digraph.
◦ 19.38 Construct a connected DAG that is a subgraph of Figure 19.1 by
deleting five edges (see Figure 19.11).
19.39 Define a digraph ADT function that provides the capability for a client
to check that a digraph is indeed a DAG, and provide a DFS-based implemen-
tation for the adjacency-matrix representation.
19.40 Use your solution to Exercise 19.39 to estimate (empirically) the prob-
ability that a random digraph with V vertices and E edges is a DAG for various
types of digraphs (see Exercises 19.11–18).
19.41 Run empirical studies to determine the relative percentages of tree,
back, cross, and down edges when we run DFS on various types of digraphs
(see Exercises 19.11–18).
DIGRAPHS AND DAGS §19.3 161

19.42 Describe how to construct a sequence of directed edges on V vertices


for which there will be no cross or down edges and for which the number of
back edges will be proportional to V 2 in a standard adjacency-lists DFS.

◦ 19.43 Describe how to construct a sequence of directed edges on V vertices


for which there will be no back or down edges and for which the number of
cross edges will be proportional to V 2 in a standard adjacency-lists DFS.

19.44 Describe how to construct a sequence of directed edges on V vertices


for which there will be no back or cross edges and for which the number of
down edges will be proportional to V 2 in a standard adjacency-lists DFS.

◦ 19.45 Give rules corresponding to Trémaux traversal for a maze where all the
passages are one-way.

• 19.46 Extend your solutions to Exercises 17.55 through 17.60 to include


arrows on edges (see the figures in this chapter for examples).
0 1 2 3 4 5
1 2 0 1 0 1 0 0 1
1 1 1 0 0 0 0
19.3 Reachability and Transitive Closure 0 3 2 0 1 1 0 0 0
3 0 0 1 1 1 0
4 0 0 0 0 1 1
To develop efficient solutions to reachability problems in digraphs, we 5 4 5 0 0 0 0 1 1
begin with the following fundamental definition.
0 1 2 3 4 5
1 2 0 1 1 1 0 1 1
Definition 19.5 The transitive closure of a digraph is a digraph with 1 1 1 1 0 1 1
the same vertices but with an edge from s to t in the transitive closure 0 3 2 1 1 1 0 1 1
3 1 1 1 1 1 1
if and only if there is a directed path from s to t in the given digraph. 4 0 0 0 0 1 1
5 4 5 0 0 0 0 1 1

In other words, the transitive closure has an edge from each


vertex to all the vertices reachable from that vertex in the digraph. Figure 19.13
Transitive closure
Clearly, the transitive closure embodies all the requisite information
for solving reachability problems. Figure 19.13 illustrates a small This digraph (top) has just eight
directed edges, but its transitive
example. closure (bottom) shows that there
One appealing way to understand the transitive closure is based are directed paths connecting 19
on adjacency-matrix digraph representations, and on the following of the 30 pairs of vertices. Struc-
tural properties of the digraph are
basic computational problem. reflected in the transitive closure.
Boolean matrix multiplication A Boolean matrix is a matrix For example, rows 0, 1, and 2
whose entries are all binary values, either 0 or 1. Given two Boolean in the adjacency matrix for the
transitive closure are identical (as
matrices A and B, compute a Boolean product matrix C, using the are columns 0, 1, and 2) because
logical and and or operations instead of the arithmetic operations + those vertices are on a directed cy-
and *, respectively. cle in the digraph.
162 §19.3 CHAPTER NINETEEN

The textbook algorithm for computing the product of two V -by-


V matrices computes, for each s and t, the dot product of row s in the
first matrix and row t in the second matrix, as follows:
for (s = 0; s < V; s++)
for (t = 0; t < V; t++)
for (i = 0, C[s][t] = 0; i < V; i++)
C[s][t] += A[s][i]*B[i][t];
In matrix notation, we write this operation simply as C = A ∗ B.
This operation is defined for matrices comprising any type of entry for
which 0, +, and * are defined. In particular, if we interpret a+b to be
the logical or operation and a*b to be the logical and operation, then
we have Boolean matrix multiplication. In C, we can use the following
version:
for (s = 0; s < V; s++)
for (t = 0; t < V; t++)
for (i = 0, C[s][t] = 0; i < V; i++)
if (A[s][i] && B[i][t]) C[s][t] = 1;
To compute C[s][t] in the product, we initialize it to 0, then set it
to 1 if we find some value i for which both A[s][i] and B[i][t] are
both 1. Running this computation is equivalent to setting C[s][t] to
1 if and only if the result of a bitwise logical and of row s in A with
column t in B has a nonzero entry.
Now suppose that A is the adjacency matrix of a digraph A and
that we use the preceding code to compute C = A ∗ A ≡ A2 (simply by
changing the reference to B in the code into a reference to A). Reading
the code in terms of the interpretation of the adjacency-matrix entries
immediately tells us what it computes: For each pair of vertices s
and t, we put an edge from s to t in C if and only if there is some
vertex i for which there is both a path from s to i and a path from i
to t in A. In other words, directed edges in A2 correspond precisely
to directed paths of length 2 in A. If we include self-loops at every
vertex in A, then A2 also has the edges of A; otherwise, it does not.
This relationship between Boolean matrix multiplication and paths
in digraphs is illustrated in Figure 19.14. It leads immediately to an
elegant method for computing the transitive closure of any digraph.
DIGRAPHS AND DAGS §19.3 163

0 1 2 3 4 5 0 1 2 3 4 5 Figure 19.14
1 2 1 2
0 0 0 1 0 0 1 0 0 1 0 0 1 0 Squaring an adjacency matrix
1 1 0 0 0 0 0 1 0 0 1 0 0 1
0 3 2 0 1 0 0 0 0 2 1 0 0 0 0 0 0 3 If we put 0s on the diagonal of a
3 0 0 1 0 1 0 3 0 1 0 0 0 1 digraph’s adjacency matrix, the
4 0 0 0 0 0 1 4 0 0 0 0 1 0
5 4 5 0 0 0 0 1 0 5 0 0 0 0 0 1 5 4 square of the matrix represents a
graph with an edge correspond-
0 1 2 3 4 5 0 1 2 3 4 5 ing to each path of length 2 (top).
1 2 0 1 0 1 0 0 1 0 1 1 1 0 1 1 1 2 If we put 1s on the diagonal, the
1 1 1 0 0 0 0 1 1 1 1 0 0 1 square of the matrix represents a
0 3 2 0 1 1 0 0 0 2 1 1 1 0 0 0 0 3 graph with an edge correspond-
3 0 0 1 1 1 0 3 0 1 1 1 1 1 ing to each path of length 1 or 2
4 0 0 0 0 1 1 4 0 0 0 0 1 1
5 4 5 0 0 0 0 1 1 5 0 0 0 0 1 1 5 4 (bottom).

Property 19.6 We can compute the transitive closure of a digraph


by constructing the latter’s adjacency matrix A, adding self-loops for
every vertex, and computing AV .

Proof : Continuing the argument in the previous paragraph, A3 has an


edge for every path of length less than or equal to 3 in the digraph,
A4 has an edge for every path of length less than or equal to 4 in the
digraph, and so forth. We do not need to consider paths of length
greater than V because of the pigeonhole principle: Any such path
must revisit some vertex (since there are only V of them) and therefore
adds no information to the transitive closure because the same two
vertices are connected by a directed path of length less than V (which
we could obtain by removing the cycle to the revisited vertex).

Figure 19.15 shows the adjacency-matrix powers for a sample


digraph converging to transitive closure. This method takes V matrix
multiplications, each of which takes time proportional to V 3 , for a
grand total of V 4 . We can actually compute the transitive closure for
any digraph with just lg V  Boolean matrix-multiplication operations:
We compute A2 , A4 , A8 , . . . until we reach an exponent greater than
or equal to V . As shown in the proof of Property 19.6, At = AV
for any t > V ; so the result of this computation, which requires time
proportional to V 3 lg V , is AV —the transitive closure.
Although the approach just described is appealing in its simplic-
ity, an even simpler method is available. We can compute the transitive
164 §19.3 CHAPTER NINETEEN

closure with just one operation of this kind, building up the transitive
closure from the adjacency matrix in place, as follows:
0 1 2 3 4 5
1 2 0 1 0 1 0 0 1 for (i = 0; i < V; i++)
1 1 1 0 0 0 0 for (s = 0; s < V; s++)
0 3 2 0 1 1 0 0 0
3 0 0 1 1 1 0 for (t = 0; t < V; t++)
4 0 0 0 0 1 1 if (A[s][i] && A[i][t]) A[s][t] = 1;
5 4 5 0 0 0 0 1 1
This classical method, invented by S. Warshall in 1962, is the method
0 1 2 3 4 5 of choice for computing the transitive closure of dense digraphs. The
1 2 0 1 1 1 0 1 1 code is similar to the code that we might try to use to square a Boolean
1 1 1 1 0 0 1
0 3 2 1 1 1 0 0 0 matrix in place: The difference (which is significant!) lies in the order
3 0 1 1 1 1 1
4 0 0 0 0 1 1
of the for loops.
5 4 5 0 0 0 0 1 1
Property 19.7 With Warshall’s algorithm, we can compute the tran-
0 1 2 3 4 5 sitive closure of a digraph in time proportional to V 3 .
1 2 0 1 1 1 0 1 1
1 1 1 1 0 1 1 Proof : The running time is immediately evident from the structure of
0 3 2 1 1 1 0 0 1
3 1 1 1 1 1 1
the code. We prove that it computes the transitive closure by induction
5 4
4 0 0 0 0 1 1 on i. After the first iteration of the loop, the matrix has a 1 in row
5 0 0 0 0 1 1
s and column t if and only if we have either the paths s-t or s-0-t.
0 1 2 3 4 5 The second iteration checks all the paths between s and t that include
1 2 0 1 1 1 0 1 1 1 and perhaps 0, such as s-1-t, s-1-0-t, and s-0-1-t. We are led to
1 1 1 1 0 1 1
0 3 2 1 1 1 0 1 1 the following inductive hypothesis: the ith iteration of the loop sets
3 1 1 1 1 1 1 the bit in row s and column t in the matrix to 1 if and only if there
4 0 0 0 0 1 1
5 4 5 0 0 0 0 1 1 is a directed path from s to t in the digraph that does not include any
vertices with indices greater than i (except possibly the endpoints s
Figure 19.15 and t). As just argued, the condition is true when i is 0, after the first
Adjacency matrix powers and iteration of the loop. Assuming that it is true for the ith iteration of the
directed paths loop, there is a path from s to t that does not include any vertices with
This sequence shows the first, indices greater than i+1 if and only if (i) there is a path from s to t that
second, third, and fourth pow- does not include any vertices with indices greater than i, in which case
ers (right, top to bottom) of the
adjacency matrix at the top right, A[s][t] was set on a previous iteration of the loop (by the inductive
which gives graphs with edges for hypothesis); or (ii) there is a path from s to i+1 and a path from i+1
each of the paths of lengths less to t, neither of which includes any vertices with indices greater than
than 1, 2, 3, and 4, respectively, i (except endpoints), in which case A[s][i+1] and A[i+1][t] were
(left, top to bottom) in the graph
that the matrix represents. The bot- previously set to 1 (by hypothesis), so the inner loop sets A[s][t].
tom graph is the transitive closure We can improve the performance of Warshall’s algorithm with a
for this example, since there are no
paths of length greater than 4 that simple transformation of the code: We move the test of A[s][i] out
connect vertices not connected by of the inner loop because its value does not change as t varies. This
shorter paths. move allows us to avoid executing the t loop entirely when A[s][i] is
DIGRAPHS AND DAGS §19.3 165
0 1 2 3 4 5
1 2 0 1 0 1 0 0 1 Figure 19.16
1 1 1 0 0 0 0 Warshall’s algorithm
0 3 2 0 1 1 0 0 0
3 0 0 1 1 1 0 This sequence shows the devel-
4 0 0 0 0 1 1 opment of the transitive closure
5 4 5 0 0 0 0 1 1 (bottom) of an example digraph
(top) as computed with Warshall’s
0 1 2 3 4 5 0 1 2 3 4 5 algorithm. The first iteration of
1 2 0 1 0 1 0 0 1 1 2 0 1 1 1 0 0 1 the loop (left column, top) adds
1 1 1 1 0 0 1 1 1 1 1 0 0 1 the edges 1-2 and 1-5 because
0 3 2 0 1 1 0 0 0 0 3 2 1 1 1 0 0 1 of the paths 1-0-2 and 1-0-5,
3 0 0 1 1 1 0 3 1 1 1 1 1 1
which include vertex 0 (but no
4 0 0 0 0 1 1 4 0 0 0 0 1 1
5 4 5 0 0 0 0 1 1 5 4 5 0 0 0 0 1 1 vertex with a higher number); the
second iteration of the loop (left
0 1 2 3 4 5 0 1 2 3 4 5 column, second from top) adds
1 2 0 1 0 1 0 0 1 1 2 0 1 1 1 0 0 1 the edges 2-0 and 2-5 because
1 1 1 1 0 0 1 1 1 1 1 0 0 1 of the paths 2-1-0 and 2-1-0-5,
0 3 2 1 1 1 0 0 1 0 3 2 1 1 1 0 0 1 which include vertex 1 (but no ver-
3 0 0 1 1 1 0 3 1 1 1 1 1 1
4 0 0 0 0 1 1 4 0 0 0 0 1 1 tex with a higher number); and
5 4 5 0 0 0 0 1 1 5 4 5 0 0 0 0 1 1 the third iteration of the loop (left
column, bottom) adds the edges
0 1 2 3 4 5 0 1 2 3 4 5 0-1, 3-0, 3-1, and 3-5 because
1 2 0 1 1 1 0 0 1 1 2 0 1 1 1 0 1 1 of the paths 0-2-1, 3-2-1-0,
1 1 1 1 0 0 1 1 1 1 1 0 1 1 3-2-1, and 3-2-1-0-5, which in-
0 3 2 1 1 1 0 0 1 0 3 2 1 1 1 0 1 1
clude vertex 2 (but no vertex with
3 1 1 1 1 1 1 3 1 1 1 1 1 1
4 0 0 0 0 1 1 4 0 0 0 0 1 1 a higher number). The right col-
5 4 5 0 0 0 0 1 1 5 4 5 0 0 0 0 1 1 umn shows the edges added when
paths through 3, 4, and 5 are con-
sidered. The last iteration of the
loop (right column, bottom) adds
zero. The savings that we achieve from this improvement depends on the edges from 0, 1, and 2, to 4,
the digraph and is substantial for many digraphs (see Exercises 19.54 because the only directed paths
from those nodes to 4 include 5,
and 19.55). Program 19.3 implements this improvement and packages the highest-numbered vertex.
Warshall’s method as a pair of ADT functions for digraphs such that
clients can preprocess a digraph (compute the transitive closure), then
compute the answer to any reachability query in constant time.
We are interested in pursuing more efficient solutions, particu-
larly for sparse digraphs. We would like to reduce both the prepro-
cessing time and the space because both make the use of Warshall’s
method prohibitively costly for huge sparse digraphs.
In modern applications, abstract data types provide us with the
ability to separate out the idea of an operation from any particular
implementation, so we can focus on efficient implementations. For the
transitive closure, this point of view leads to a recognition that we do
not necessarily need to compute the entire matrix to provide clients
166 §19.3 CHAPTER NINETEEN

Program 19.3 Warshall’s algorithm


This ADT implementation of Warshall’s algorithm provides clients with
the capability to test whether any vertex in a digraph is reachable from
any other vertex, after calling GRAPHtc to compute the transitive closure.
The array pointer tc in the graph representation is used to store the
transitive closure matrix.
void GRAPHtc(Graph G)
{ int i, s, t;
G->tc = MATRIXint(G->V, G->V, 0);
for (s = 0; s < G->V; s++)
for (t = 0; t < G->V; t++)
G->tc[s][t] = G->adj[s][t];
for (s = 0; s < G->V; s++) G->tc[s][s] = 1;
for (i = 0; i < G->V; i++)
for (s = 0; s < G->V; s++)
if (G->tc[s][i] == 1)
for (t = 0; t < G->V; t++)
if (G->tc[i][t] == 1) G->tc[s][t] = 1;
}
int GRAPHreach(Graph G, int s, int t)
{ return G->tc[s][t]; }

with the transitive-closure abstraction. One possibility might be that


the transitive closure is a huge sparse matrix, so an adjacency-lists
representation is called for because we cannot store the array repre-
sentation. Even when the transitive closure is dense, client programs
might test only a tiny fraction of possible pairs of edges, so computing
the whole matrix is wasteful.
We use the term abstract transitive closure to refer to an ADT that
provides clients with the ability to test reachability after preprocessing
a digraph, like Program 19.3. In this context, we need to measure
an algorithm not just by its cost to compute the transitive closure
(preprocessing cost), but also by the space required and the query time
achieved. That is, we rephrase Property 19.7 as follows:
Property 19.8 We can support constant-time reachability testing (ab-
stract transitive closure) for a digraph, using space proportional to V 2
and time proportional to V 3 for preprocessing.
DIGRAPHS AND DAGS §19.3 167

This property follows immediately from the basic performance char-


acteristics of Warshall’s algorithm.
For most applications, our goal is not just to compute the tran-
sitive closure of a digraph quickly, but also to support constant query
time for the abstract transitive closure using far less space and far less
preprocessing time than specified in Property 19.8. Can we find an
implementation that will allow us to build clients that can afford to
handle such digraphs? We return to this question in Section 19.8.
There is an intimate relationship between the problem of com-
puting the transitive closure of a digraph and a number of other fun-
damental computational problems, and that relationship can helps us
to understand this problem’s difficulty. We conclude this section by
considering two examples of such problems.
First, we consider the relationship between the transitive closure
and the all-pairs shortest-paths problem (see Section 18.7). For di-
graphs, the problem is to find, for each pair of vertices, a directed
path with a minimal number of edges. The BFS-based solution for
undirected graphs that we considered in Section 18.7 also works for
digraphs (appropriately modified), but in the present context we are in-
terested in adapting Warshall’s algorithm to solve this problem. Short-
est paths are the subject of Chapter 21, so we defer considering detailed
performance comparisons until then.
Given a digraph, we initialize a V -by-V integer matrix A by
setting A[s][t] to 1 if there is an edge from s to t and to the sentinel
value V if there is no such edge. Our goal is to set A[s][t] equal to
the length of (the number of edges on) a shortest directed path from s
to t, using the sentinel value V to indicate that there is no such path.
The following code accomplishes this objective:
for (i = 0; i < V; i++)
for (s = 0; s < V; s++)
for (t = 0; t < V; t++)
if (A[s][i] + A[i][t] < A[s][t])
A[s][t] = A[s][i] + A[i][t];
This code differs from the version of Warshall’s algorithm that we saw
just before Property 19.7 in only the if statement in the inner loop.
Indeed, in the proper abstract setting, the computations are precisely
the same (see Exercises 19.56 and 19.57). Converting the proof of
Property 19.7 into a direct proof that this method accomplishes the
168 §19.3 CHAPTER NINETEEN

desired objective is straightforward. This method is a special case of


Floyd’s algorithm for finding shortest paths in weighted graphs (see
Chapter 21).
Second, as we have seen, the transitive-closure problem is also
closely related to the Boolean matrix-multiplication problem. The ba-
sic algorithms that we have seen for both problems require time pro-
portional to V 3 , using similar computational schema. Boolean matrix
multiplication is known to be a difficult computational problem: Algo-
rithms that are asymptotically faster than the straightforward method
are known, but it is debatable whether the savings are sufficiently large
to justify the effort of implementing any of them. This fact is signifi-
cant in the present context because we could use a fast algorithm for
Boolean matrix multiplication to develop a fast transitive-closure al-
gorithm (slower by just a factor of lg V ) using the repeated-squaring
method illustrated in Figure 19.15. Conversely, we have a lower bound
on the difficulty of computing the transitive closure:
Property 19.9 We can use any transitive-closure algorithm to com-
pute the product of two Boolean matrices with at most a constant-
factor difference in running time.
Proof : Given two V -by-V Boolean matrices A and B, we construct
the following 3V -by-3V matrix:
⎛ ⎞
I A 0
⎝0 I B⎠
0 0 I
Here, 0 denotes the V -by-V matrix with all entries equal to 0, and
I denotes the V -by-V identity matrix with all entries equal to 0 ex-
cept those on the diagonal, which are equal to 1. Now, we consider
this matrix to be the adjacency matrix for a digraph and compute its
transitive closure by repeated squaring:
⎛ ⎞ ⎛ ⎞
I A 0 2 I A A∗B
⎝0 I B⎠ = ⎝0 I B ⎠
0 0 I 0 0 I
The matrix on the right-hand side of this equation is the transitive
closure because further multiplications give back the same matrix. But
this matrix has the V -by-V product A ∗ B in its upper-right corner.
Whatever algorithm we use to solve the transitive-closure problem, we
DIGRAPHS AND DAGS §19.3 169

can use it to solve the Boolean matrix-multiplication problem at the


same cost (to within a constant factor).
The significance of this property depends on the conviction of ex-
perts that Boolean matrix multiplication is difficult: Mathematicians
have been working for decades to try to learn precisely how difficult
it is, and the question is unresolved, with the best known results say-
ing that the running time should be proportional to about V 2.5 (see
reference section). Now, if we could find a linear-time (proportional
to V 2 ) solution to the transitive-closure problem, then we would have
a linear-time solution to the Boolean matrix-multiplication problem
as well. This relationship between problems is known as reduction:
We say that the Boolean matrix-multiplication problem reduces to the
transitive-closure problem (see Section 21.6 and Part 8). Indeed, the
proof actually shows that Boolean matrix multiplication reduces to
finding the paths of length 2 in a digraph.
Despite a great deal of research by many people, no one has been
able to find a linear-time Boolean matrix-multiplication algorithm, so
we cannot present a simple linear-time transitive-closure algorithm.
On the other hand, no one has proved that no such algorithm exists,
so we hold open that possibility for the future. In short, we take Prop-
erty 19.9 to mean that, barring a research breakthrough, we cannot
expect the worst-case running time of any transitive-closure algorithm
that we can concoct to be proportional to V 2 . Despite this conclusion,
we can develop fast algorithms for certain classes of digraphs. For
example, we have already touched on a simple method for computing
the transitive closure that is much faster than Warshall’s algorithm for
sparse digraphs.
Property 19.10 With DFS, we can support constant query time for
the abstract transitive closure of a digraph, with space proportional to
V 2 and time proportional to V (E + V ) for preprocessing (computing
the transitive closure).
Proof : As we observed in the previous section, DFS gives us all the
vertices reachable from the start vertex in time proportional to E, if
we use the adjacency-lists representation (see Property 19.5 and Fig-
ure 19.11). Therefore, if we run DFS V times, once with each vertex as
the start vertex, then we can compute the set of vertices reachable from
each vertex—the transitive closure—in time proportional to V (E +V ).
170 §19.3 CHAPTER NINETEEN

Program 19.4 DFS-based transitive closure


This program is functionally equivalent to Program 19.3. It computes
the transitive closure by doing a separate DFS starting at each vertex to
compute its set of reachable nodes. Each call on the recursive procedure
adds an edge from the start vertex and makes recursive calls to fill the
corresponding row in the transitive-closure matrix. The matrix also
serves to mark the visited vertices during the DFS.

void TCdfsR(Graph G, Edge e)


{ link t;
G->tc[e.v][e.w] = 1;
for (t = G->adj[e.w]; t != NULL; t = t->next)
if (G->tc[e.v][t->v] == 0)
TCdfsR(G, EDGE(e.v, t->v));
}
void GRAPHtc(Graph G, Edge e)
{ int v, w;
G->tc = MATRIXint(G->V, G->V, 0);
for (v = 0; v < G->V; v++)
TCdfsR(G, EDGE(v, v));
}
int GRAPHreach(Graph G, int s, int t)
{ return G->tc[s][t]; }

The same argument holds for any linear-time generalized search (see
Section 18.8 and Exercise 19.69).
Program 19.4 is an implementation of this search-based transitive-
closure algorithm. The result of running this program on the sample
digraph in Figure 19.1 is illustrated in the first tree in each forest in
Figure 19.11. The implementation is packaged in the same way as
we packaged Warshall’s algorithm in Program 19.3: a preprocessing
function that computes the transitive closure, and a function that can
determine whether any vertex is reachable from any other in constant
time by testing the indicated entry in the transitive-closure array.
For sparse digraphs, this search-based approach is the method
of choice. For example, if E is proportional to V , then Program 19.4
computes the transitive closure in time proportional to V 2 . How can
it do so, given the reduction to Boolean matrix multiplication that we
DIGRAPHS AND DAGS §19.3 171

just considered? The answer is that this transitive-closure algorithm


does indeed give an optimal way to multiply certain types of Boolean
matrices (those with O(V ) nonzero entries). The lower bound tells
us that we should not expect to find a transitive-closure algorithm
that runs in time proportional to V 2 for all digraphs, but it does
not preclude the possibility that we might find algorithms, like this
one, that are faster for certain classes of digraphs. If such graphs are
the ones that we need to process, the relationship between transitive
closure and Boolean matrix multiplication may not be relevant to us.
It is easy to extend the methods that we have described in this
section to provide clients with the ability to find a specific path con-
necting two vertices by keeping track of the search tree, as described in
Section 17.8. We consider specific ADT implementations of this sort in
the context of the more general shortest-paths problems in Chapter 21.
Table 19.1 shows empirical results comparing the elementary
transitive-closure algorithms described in this section. The adjacency-
lists implementation of the search-based solution is by far the fastest
method for sparse digraphs. The implementations all compute an
adjacency matrix (of size V 2 ), so none of them are suitable for huge
sparse digraphs.
For sparse digraphs whose transitive closure is also sparse, we
might use an adjacency-lists implementation for the closure so that
the size of the output is proportional to the number of edges in the
transitive closure. This number certainly is a lower bound on the
cost of computing the transitive closure, which we can achieve for
certain types of digraphs using various algorithmic techniques (see Ex-
ercises 19.67 and 19.68). Despite this possibility, we generally view
the objective of a transitive-closure computation to be the adjacency-
matrix representation, so we can easily answer reachability queries,
and we regard transitive-closure algorithms that compute the matrix
in time proportional to V 2 as being optimal since they take time pro-
portional to the size of their output.
If the adjacency matrix is symmetric, it is equivalent to an undi-
rected graph, and finding the transitive closure is the same as find-
ing the connected components—the transitive closure is the union of
complete graphs on the vertices in the connected components (see Ex-
ercise 19.49). Our connectivity algorithms in Section 18.5 amount
to an abstract–transitive-closure computation for symmetric digraphs
172 §19.3 CHAPTER NINETEEN

Table 19.1 Empirical study of transitive-closure algorithms

This table shows running times that exhibit dramatic performance dif-
ferences for various algorithms for computing the transitive closure of
random digraphs, both dense and sparse. For all but the adjacency-lists
DFS, the running time goes up by a factor of 8 when we double V ,
which supports the conclusion that it is essentially proportional to V 3 .
The adjacency-lists DFS takes time proportional to V E, which explains
the running time roughly increasing by a factor of 4 when we double both
V and E (sparse graphs) and by a factor of about 2 when we double E
(dense graphs), except that list-traversal overhead degrades performance
for high-density graphs.

sparse (10V edges) dense (250 vertices)

V W W* A L E W W* A L

25 0 0 1 0 5000 289 203 177 23


50 3 1 2 1 10000 300 214 184 38
125 35 24 23 4 25000 309 226 200 97
250 275 181 178 13 50000 315 232 218 337
500 2222 1438 1481 54 100000 326 246 235 784

Key:
W Warshall’s algorithm (Section 19.3)
W* Improved Warshall’s algorithm (Program 19.3)
A DFS, adjacency-matrix representation (Exercise 19.64)
L DFS, adjacency-lists representation (Program 19.4)

(undirected graphs) that uses space proportional to V and still sup-


ports constant-time reachability queries. Can we do as well in general
digraphs? Can we reduce the preprocessing time still further? For
what types of graphs can we compute the transitive closure in linear
time? To answer these questions, we need to study the structure of
digraphs in more detail, including, specifically, that of DAGs.
Exercises
 19.47 What is the transitive closure of a digraph that consists solely of a
directed cycle with V vertices?
19.48 How many edges are there in the transitive closure of a digraph that
consists solely of a simple directed path with V vertices?
DIGRAPHS AND DAGS §19.3 173

 19.49 Give the transitive closure of the undirected graph


3-7 1-4 7-8 0-5 5-2 3-8 2-9 0-6 4-9 2-6 6-4.

• 19.50 Show how to construct a digraph with V vertices and E edges with the
property that the number of edges in the transitive closure is proportional to
t, for any t between E and V 2 . As usual, assume that E > V .
19.51 Give a formula for the number of edges in the transitive closure of a
digraph that is a directed forest as a function of structural properties of the
forest.
19.52 Show, in the style of Figure 19.15, the process of computing the tran-
sitive closure of the digraph
3-7 1-4 7-8 0-5 5-2 3-8 2-9 0-6 4-9 2-6 6-4
through repeated squaring.
19.53 Show, in the style of Figure 19.16, the process of computing the tran-
sitive closure of the digraph
3-7 1-4 7-8 0-5 5-2 3-8 2-9 0-6 4-9 2-6 6-4
with Warshall’s algorithm.
◦ 19.54 Give a family of sparse digraphs for which the improved version of
Warshall’s algorithm for computing the transitive closure (Program 19.3) runs
in time proportional to V E.
◦ 19.55 Find a sparse digraph for which the improved version of Warshall’s
algorithm for computing the transitive closure (Program 19.3) runs in time
proportional to V 3 .
◦ 19.56 Develop an ADT for integer matrices with appropriate implementations
such that we can have a single client program that encompasses both Warshall’s
algorithm and Floyd’s algorithm. (This exercise is a version of Exercise 19.57
for people who are more familiar with abstract data types than with abstract
algebra.)
• 19.57 Use abstract algebra to develop a generic algorithm that encompasses
both Warshall’s algorithm and Floyd’s algorithm. (This exercise is a version of
Exercise 19.56 for people who are more familiar with abstract algebra than
with abstract data types.)
◦ 19.58 Show, in the style of Figure 19.16, the development of the all-shortest
paths matrix for the example graph in the figure as computed with Floyd’s
algorithm.
19.59 Is the Boolean product of two symmetric matrices symmetric? Explain
your answer.
19.60 Modify Programs 19.3 and 19.4 to provide implementations of a di-
graph ADT function that returns the number of edges in the transitive closure
of the digraph.
174 §19.4 CHAPTER NINETEEN

19.61 Implement the function described in Exercise 19.60 by maintaining the


count as part of the digraph ADT, and modifying it when edges are added or
deleted. Give the cost of adding and deleting edges with your implementation.
 19.62 Add a digraph ADT function for use with Programs 19.3 and 19.4
that returns a vertex-indexed array that indicates which vertices are reachable
from a given vertex. You may require the client to have called GRAPHtc for
preprocessing.
◦ 19.63 Run empirical studies to determine the number of edges in the transitive
closure, for various types of digraphs (see Exercises 19.11–18).
 19.64 Implement the DFS-based transitive-closure algorithm for the adjacency-
matrix representation.
• 19.65 Consider the bit-array graph representation described in Exercise 17.21.
Which method can you speed up by a factor of B (where B is the number of
bits per word on your computer): Warshall’s algorithm or the DFS-based
algorithm? Justify your answer by developing an implementation that does
so.
◦ 19.66 Develop a representation-independent abstract digraph ADT (see Ex-
ercise 17.60), including a transitive-closure function. Note: The transitive
closure itself should be an abstract digraph without reference to any particular
representation.
◦ 19.67 Give a program that computes an adjacency-lists representation of the
transitive closure of a digraph that is a directed forest in time proportional to
the number of edges in the transitive closure.
◦ 19.68 Implement an abstract–transitive-closure algorithm for sparse graphs
that uses space proportional to T and can answer reachability requests in
constant time after preprocessing time proportional to V E + T , where T is
the number of edges in the transitive closure. Hint: Use dynamic hashing.
 19.69 Provide a version of Program 19.4 that is based on generalized graph
search (see Section 18.8).

19.4 Equivalence Relations and Partial Orders


This section is concerned with basic concepts in set theory and their
relationship to abstract–transitive-closure algorithms. Its purposes
are to put the ideas that we are studying into a larger context and
to demonstrate the wide applicability of the algorithms that we are
considering. Mathematically inclined readers who are familiar with
set theory may wish to skip to Section 19.5 because the material that
we cover is elementary (although our brief review of terminology may
be helpful); readers who are not familiar with set theory may wish
DIGRAPHS AND DAGS §19.4 175

to consult an elementary text on discrete mathematics because our


treatment is rather succinct. The connections between digraphs and
these fundamental mathematical concepts are too important for us to
ignore.
Given a set, a relation among its objects is defined to be a set
of ordered pairs of the objects. Except possibly for details relating to
parallel edges and self-loops, this definition is the same as our defini-
tion of a digraph: Relations and digraphs are different representations
of the same abstraction. The mathematical concept is somewhat more
powerful because the sets may be infinite, whereas our computer pro-
grams all work with finite sets, but we ignore this difference for the
moment.
Typically, we choose a symbol R and use the notation sRt as
shorthand for the statement “the ordered pair (s, t) is in the relation
R.” For example, we use the symbol “<” to represent the “less than”
relation among numbers. Using this terminology, we can characterize
various properties of relations. For example, a relation R is said to be
symmetric if sRt implies that tRs for all s and t; it is said to be reflexive
if sRs for all s. Symmetric relations are the same as undirected graphs.
Reflexive relations correspond to graphs in which all vertices have
self-loops; relations that correspond to graphs where no vertices have
self-loops are said to be irreflexive.
A relation R is said to be transitive when sRt and tRu implies
that sRu for all s, t, and u. The transitive closure of a relation is a
well-defined concept; but instead of redefining it in set-theoretic terms,
we appeal to the definition that we gave for digraphs in Section 19.3.
Any relation is equivalent to a digraph, and the transitive closure of
the relation is equivalent to the transitive closure of the digraph. The
transitive closure of any relation is transitive.
In the context of graph algorithms, we are particularly interested
in two particular transitive relations that are defined by further con-
straints. These two types, which are widely applicable, are known as
equivalence relations and partial orders.
An equivalence relation ≡ is a transitive relation that is also re-
flexive and symmetric. Note that a symmetric, transitive relation that
includes each object in some ordered pair must be an equivalence re-
lation: If s ≡ t, then t ≡ s (by symmetry) and s ≡ s (by transitivity).
Equivalence relations divide the objects in a set into subsets known
176 §19.4 CHAPTER NINETEEN

as equivalence classes. Two objects s and t are in the same equiva-


lence class if and only if s ≡ t. The following examples are typical
equivalence relations:
Modular arithmetic Any positive integer k defines an equiva-
lence relation on the set of integers, with s ≡ t (mod k) if and only
if the remainder that results when we divide s by k is equal to the the
remainder that results when we divide t by k. The relation is obvi-
ously symmetric; a short proof establishes that it is also transitive (see
Exercise 19.70) and therefore is an equivalence relation.
Connectivity in graphs The relation “is in the same connected
component as” among vertices is an equivalence relation because it is
7
symmetric and transitive. The equivalence classes correspond to the
connected components in the graph.
6 5 3 When we build a graph ADT that gives clients the ability to test
whether two vertices are in the same connected component, we are
implementing an equivalence-relation ADT that provides clients with
4 2 1
the ability to test whether two objects are equivalent. In practice, this
correspondence is significant because the graph is a succinct represen-
0 tation of the equivalence relation (see Exercise 19.74). In fact, as we
0 000 φ saw in Chapters 1 and 18, to build such an ADT we need to maintain
1 001 {c} only a single vertex-indexed array.
2 010 {b} A partial order ≺ is a transitive relation that is also irreflexive.
3 011 {b, c}
4 100 {a} As a direct consequence of transitivity and irreflexivity, it is trivial to
5 101 {a, c} prove that partial orders are also asymmetric: If s ≺ t and t ≺ s, then
6 110 {a, b} s ≺ s (by transitivity), which contradicts irreflexivity, so we cannot
7 111 {a, b, c} have both s ≺ t and t ≺ s. Moreover, extending the same argument
shows that a partial order cannot have a cycle, such as s ≺ t, t ≺ u,
Figure 19.17 and u ≺ s. The following examples are typical partial orders:
Set-inclusion DAG Subset inclusion The relation “includes but is not equal to” (⊂)
In the DAG at the top, we interpret among subsets of a given set is a partial order—it is certainly irreflexive,
vertex indices to represent subsets and if s ⊂ t and t ⊂ u, then certainly s ⊂ u.
of a set of 3 elements, as shown in
Paths in DAGs The relation “can be reached by a nonempty
the table at the bottom. The transi-
tive closure of this DAG represents directed path from” is a partial order on vertices in DAGs with no
the subset inclusion partial order: self-loops because it is transitive and irreflexive. Like equivalence rela-
there is a directed path between tions and undirected graphs, this particular partial order is significant
two nodes if and only if the subset
for many applications because a DAG provides a succinct implicit
represented by the first is included
in the subset represented by the representation of the partial order. For example, Figure 19.17 illus-
second. trates DAGs for subset containment partial orders whose number of
DIGRAPHS AND DAGS §19.4 177

edges is only a fraction of the cardinality of the partial order (see


Exercise 19.76).
Indeed, we rarely define partial orders by enumerating all their
ordered pairs, because there are too many of such pairs. Instead,
we generally specify an irreflexive relation (a DAG) and consider its
transitive closure. This usage is a primary reason for considering
abstract–transitive-closure ADT implementations for DAGs. Using
DAGs, we consider examples of partial orders in Section 19.5.
A total order T is a partial order where either sT t or tT s for all
s = t. Familiar examples of total orders are the “less than” relation
among integers or real numbers and lexicographic ordering among
strings of characters. Our study of sorting and searching algorithms in
Parts 3 and 4 was based on a total-order ADT implementation for sets.
In a total order, there is one and only one way to arrange the elements
in the set such that sT t whenever s is before t in the arrangement;
in a partial order that is not total, there are many ways to do so. In
Section 19.5, we examine algorithms for this task.
In summary, the following correspondences between sets and
graph models help us to understand the importance and wide applica-
bility of fundamental graph algorithms.
• Relations and digraphs
• Symmetric relations and undirected graphs
• Transitive relations and paths in graphs
• Equivalence relations and paths in undirected graphs
• Partial orders and paths in DAGs
This context places in perspective the types of graphs and algorithms
that we are considering and provides one motivation for us to move
on to consider basic properties of DAGs and algorithms for processing
those DAGs.

Exercises
19.70 Show that “has the same remainder after dividing by k” is a transitive
relation (and therefore is an equivalence relation) on the set of integers.

19.71 Show that “is in the same edge-connected component as” is an equiva-
lence relation among vertices in any graph.

19.72 Show that “is in the same biconnected component as” is not an equiv-
alence relation among vertices in all graphs.
178 §19.5 CHAPTER NINETEEN

19.73 Prove that the transitive closure of an equivalence relation is an equiv-


alence relation, and that the transitive closure of a partial order is a partial
order.
 19.74 The cardinality of a relation is its number of ordered pairs. Prove that
the cardinality of an equivalence relation is equal to the sum of the squares of
the cardinalities of that relation’s equivalence classes.

◦ 19.75 Using an online dictionary, build a graph that represents the equivalence
relation “has k letters in common with” among words. Determine the number
of equivalence classes for k = 1 through 5.
19.76 The cardinality of a partial order is its number of ordered pairs. What
is the cardinality of the subset containment partial order for an n-element set?
 19.77 Show that “is a factor of” is a partial order among integers.

19.5 DAGs
In this section, we consider various applications of directed acyclic
graphs (DAGs). We have two reasons to do so. First, because they
serve as implicit models for partial orders, we work directly with DAGs
in many applications and need efficient algorithms to process them.
Second, these various applications give us insight into the nature of
DAGs, and understanding DAGs is essential to understanding general
digraphs.
Since DAGs are a special type of digraph, all DAG-processing
problems trivially reduce to digraph-processing problems. Although
we expect processing DAGs to be easier than processing general di-
graphs, we know when we encounter a problem that is difficult to solve
on DAGs we should not expect to do any better solving the same prob-
lem on general digraphs. As we shall see, the problem of computing
the transitive closure lies in this category. Conversely, understanding
the difficulty of processing DAGs is important because every digraph
has a kernel DAG (see Property 19.2), so we encounter DAGs even
when we work with digraphs that are not DAGs.
The prototypical application where DAGs arise directly is called
scheduling. Generally, solving scheduling problems has to do with ar-
ranging for the completion of a set of tasks, under a set of constraints,
by specifying when and how the tasks are to be performed. Con-
straints might involve functions of the time taken or other resources
consumed by the tasks. The most important type of constraints are
DIGRAPHS AND DAGS §19.5 179

precedence constraints, which specify that certain tasks must be per-


formed before certain others, thus comprising a partial order among
the tasks. Different types of additional constraints lead to many dif-
ferent types of scheduling problems, of varying difficulty. Literally
thousands of different problems have been studied, and researchers
still seek better algorithms for many of them. Perhaps the simplest
nontrivial scheduling problem may be stated as follows:
Scheduling Given a set of tasks to be completed, with a partial
order that specifies that certain tasks have to be completed before
certain other tasks are begun, how can we schedule the tasks such that
they are all completed while still respecting the partial order?
In this basic form, the scheduling problem is called topological
sorting; it is not difficult to solve, as we shall see in the next section by
examining two algorithms that do so. In more complicated practical
applications, we might need to add other constraints on how the tasks
might be scheduled, and the problem can become much more difficult.
For example, the tasks might correspond to courses in a student’s
schedule, with the partial order specifying prerequisites. Topological
sorting gives a feasible course schedule that meets the prerequisite
requirements, but perhaps not one that respects other constraints that
need to be added to the model, such as course conflicts, limitations on
enrollments, and so forth. As another example, the tasks might be part
of a manufacturing process, with the partial order specifying sequential
requirements of the particular process. Topological sorting gives us a
way to schedule the tasks, but perhaps there is another way to do so
that uses less time, money, or some other resources not included in the
model. We examine versions of the scheduling problem that capture
more general situations such as these in Chapters 21 and 22.
Despite many similarities with digraphs, a separate ADT for
DAGs is appropriate when we are implementing DAG-specific algo-
rithms. In such cases, we use an ADT interface like Program 17.1,
substituting DAG for GRAPH everywhere. Sections 19.6 and 19.7 are
devoted to implementations of the ADT functions for topological sort-
ing (DAGts) and reachability in DAGs (DAGtc and DAGreach); Pro-
gram 19.13 is an example of a client of this ADT.
Often, our first task is to check whether or not a given DAG
indeed has no directed cycles. As we saw in Section 19.2, we can
test whether a general digraph is a DAG in linear time, by running
180 §19.5 CHAPTER NINETEEN

a standard DFS and checking that the DFS forest has no back edges.
A separate ADT for DAGs should include an ADT function allowing
client programs to perform such a check (see Exercise 19.78).
In a sense, DAGs are part tree, part graph. We can certainly
take advantage of their special structure when we process them. For
example, we can view a DAG almost as we view a tree, if we wish.
The following simple program is like a recursive tree traversal:
void traverseR(Dag D, int w)
{ link t;
visit(w);
for (t = D->adj[w]; t != NULL; t = t->next)
traverseR(D, t->v);
8
}
5 3 The result of this program is to traverse the vertices of the DAG D as
though it were a tree rooted at w. For example, the result of traversing
3 2 2 1
the two DAGs in Figure 19.18 with this program would be the same.
2 1 1 1 1 1
We rarely use a full traversal, however, because we normally want to
take advantage of the same economies that save space in a DAG to
1 1 save time in traversing it (for example, by marking visited nodes in
a normal DFS). The same idea applies to a search, where we make a
8 recursive call for only one link incident on each vertex. In such an
5 algorithm, the search cost will be the same for the DAG and the tree,
3
but the DAG uses far less space.
2
Because they provide a compact way to represent trees that have
identical subtrees, we often use DAGs instead of trees when we rep-
1
resent computational abstractions. In the context of algorithm de-
1
sign, the distinction between the DAG representation and the tree
representation of a program in execution is the essential distinction
Figure 19.18 behind dynamic programming (see, for example, Figure 19.18 and
DAG model of Fibonacci Exercise 19.81). DAGs are also widely used in compilers as interme-
computation
diate representations of arithmetic expressions and programs (see, for
The tree at the top shows the de-
pendence of computing each Fi- example, Figure 19.19), and in circuit-design systems as intermediate
bonacci number on computing its representations of combinational circuits.
two predecessors. The DAG at the Along these lines, an important example that has many applica-
bottom shows the same depen-
dence with only a fraction of the tions arises when we consider binary trees. We can apply the same
nodes. restriction to DAGs that we applied to trees to define binary trees:
DIGRAPHS AND DAGS §19.5 181

- - Figure 19.19
* * * * DAG representation of an
arithmetic expression
c + + + c +
Both of these DAGs are represen-
a b a b + e + e
tations of the arithmetic expression
a b a b (c*(a+b))-((a+b))*((a+b)+e)).
In the binary parse tree at left, leaf
nodes represent operands and in-
ternal nodes each represent op-
Definition 19.6 A binary DAG is a directed acyclic graph with two erators to be applied to the ex-
pressions represented by their two
edges leaving each node, identified as the left edge and the right edge,
subtrees (see Figure 5.31). The
either or both of which may be null. DAG at right is a more compact
representation of the same tree.
The distinction between a binary DAG and a binary tree is that in the More important, we can com-
binary DAG we can have more than one link pointing to a node. As pute the value of the expression
did our definition for binary trees, this definition models a natural rep- in time proportional to the size
resentation, where each node is a structure with a left link and a right of the DAG, which is typically
significantly less than the size of
link that point to other nodes (or are null), subject to only the global the tree (see Exercises 19.114
restriction that no directed cycles are allowed. Binary DAGs are sig- and 19.115).
nificant because they provide a compact way to represent binary trees
in certain applications. For example, we can compress an existence
trie into a binary DAG without changing the search implementation,
as shown Figure 19.20 and Program 19.5.
An equivalent application is to view the trie keys as correspond-
ing to rows in the truth table of a Boolean function for which the
function is true (see Exercises 19.87 through 19.90). The binary DAG
is a model for an economical circuit that computes the function. In
this application, binary DAGs are known as binary decision diagrams
(BDD)s.
Motivated by these applications, we turn, in the next two sec-
tions, to the study of DAG-processing algorithms. Not only do these
algorithms lead to efficient and useful DAG ADT function implemen-
tations, but also they provide insight into the difficulty of process-
ing digraphs. As we shall see, even though DAGs would seem to
be substantially simpler structures than general digraphs, some basic
problems are apparently no easier to solve.
Exercises
 19.78 Define an ADT interface that is suitable for processing DAGs, and
build adjacency-lists and adjacency-matrix implementations. Include an ADT
function for verifying that the DAG has no cycles, implemented with DFS.
182 §19.5 CHAPTER NINETEEN

Program 19.5 Representing a binary tree with a binary DAG


This recursive program is a postorder walk that constructs a compact
representation of a binary DAG corresponding to a binary tree struc-
ture (see Chapter 12) by identifying common subtrees. We use an index
function like STindex in Program 17.10, modified to accept integer
instead of string keys, to assign a unique integer to each distinct tree
structure for use in representing the DAG as an array of 2-integer struc-
1 0 0 tures (see Figure 19.20). The empty tree (null link) is assigned index 0,
2 1 1 the single-node tree (node with two null links) is assigned index 1, and
9
3 1 2 6 8 so forth.
4 2 0 4 5 7 We compute the index corresponding to each subtree, recursively.
5 4 2 2 3 3 2 Then we create a key such that any node with the same subtrees will
6 3 5 1 1 1
have the same index and return that index after filling in the DAG’s edge
7 3 0 (subtree) links.
8 7 1
9 6 8 int compressR(link h)
{ int l, r, t;
Figure 19.20
Binary tree compression if (h == NULL) return 0;
l = compressR(h->l);
The table of nine pairs of integers
at the bottom left is a compact rep- r = compressR(h->r);
resentation of a binary DAG (bot- t = STindex(l*Vmax + r);
tom right) that is a compressed ver- adj[t].l = l; adj[t].r = r;
sion of the binary tree structure at return t;
top. Node labels are not explicitly
stored in the data structure: The }
table represents the eighteen edges
1-0, 1-0, 2-1, 2-1, 3-1, 3-2, and
so forth, but designates a left edge ◦ 19.79 Write a program that generates random DAGs by generating random
and a right edge leaving each node digraphs, doing a DFS from a random starting point, and throwing out the
(as in a binary tree) and leaves the back edges (see Exercise 19.41). Run experiments to decide how to set
source vertex for each edge im- parameters in your program to expect DAGs with E edges, given V .
plicit in the table index.
An algorithm that depends  19.80 How many nodes are there in the tree and in the DAG corresponding
only upon the tree shape will work to Figure 19.18 for FN , the Nth Fibonacci number?
effectively on the DAG. For ex-
ample, suppose that the tree is an 19.81 Give the DAG corresponding to the dynamic-programming example
existence trie for binary keys cor- for the knapsack model from Chapter 5 (see Figure 5.17).
responding to the leaf nodes, so
it represents the keys 0000, 0001,
◦ 19.82 Develop an ADT for binary DAGs.
0010, 0110, 1100, and 1101. A • 19.83 Can every DAG be represented as a binary DAG (see Property 5.4)?
successful search for the key 1101
in the trie moves right, right, left, ◦ 19.84 Write a function that performs an inorder traversal of a single-source
and right to end at a leaf node. In binary DAG. That is, the function should visit all vertices that can be reached
the DAG, the same search goes via the left edge, then visit the source, then visit all the vertices that can be
from 9 to 8 to 7 to 2 to 1. reached via the right edge.
DIGRAPHS AND DAGS §19.6 183

 19.85 In the style of Figure 19.20, give the existence trie and corresponding
binary DAG for the keys 01001010 10010101 00100001 11101100 01010001
00100001 00000111 01010011 .
19.86 Implement an ADT based on building an existence trie from a set of
32-bit keys, compressing it as a binary DAG, then using that data structure to
support existence queries.
◦ 19.87 Draw the BDD for the truth table for the odd parity function of four
variables, which is 1 if and only if the number of variables that have the value
1 is odd.
19.88 Write a function that takes a 2n -bit truth table as argument and returns
the corresponding BDD. For example, given the input 1110001000001100,
your program should return a representation of the binary DAG in Fig-
ure 19.20. 0
n 6 7 8
19.89 Write a function that takes a 2 -bit truth table as argument, computes 1 2
every permutation of its argument variables, and, using your solution to Ex-
ercise 19.88, finds the permutation that leads to the smallest BDD.
3 9 10
• 19.90 Run empirical studies to determine the effectiveness of the strategy of 4
Exercise 19.90 for various Boolean functions, both standard and randomly 5 11 12
generated.
19.91 Write a program like Program 19.5 that supports common subexpres-
sion removal: Given a binary tree that represents an arithmetic expression, 0
compute a binary DAG that represents the same expression with common 6 5 4
subexpressions removed. 1 2

◦ 19.92 Draw all the nonisomorphic DAGs with two, three, four, and five
vertices. 3 9 10
7
•• 19.93 How many different DAGs are there with V vertices and E edges? 8 11 12
••• 19.94 How many different DAGs are there with V vertices and E edges, if
we consider two DAGs to be different only if they are not isomorphic? 0 1 2 3 4 5 6 7 8 9 10 11 12
tsI 0 1 2 3 7 8 6 5 4 9 10 11 12

19.6 Topological Sorting Figure 19.21


Topological sort (relabeling)
The goal of topological sorting is to be able to process the vertices of a Given any DAG (top), topologi-
DAG such that every vertex is processed before all the vertices to which cal sorting allows us to relabel its
vertices so that every edge points
it points. There are two natural ways to define this basic operation;
from a lower-numbered vertex to
they are essentially equivalent. Both tasks call for a permutation of a higher-numbered one (bottom).
the integers 0 through V-1, which we put in vertex-indexed arrays, as In this example, we relabel 4, 5, 7,
usual. and 8 to 7, 8, 5, and 4, respec-
tively, as indicated in the array
Topological sort (relabel) Given a DAG, relabel its vertices
tsI. There are many possible la-
such that every directed edge points from a lower-numbered vertex to belings that achieve the desired
a higher-numbered one (see Figure 19.21). result.
184 §19.6 CHAPTER NINETEEN

Figure 19.22
Topological sorting (rear-
rangement).
This diagram shows another way to 0 1 2 3 8 7 6 4 5 9 10 11 12
look at the topological sort in Fig-
ure 19.21, where we specify a way
to rearrange the vertices, rather 0 1 2 3 4 5 6 7 8 9 10 11 12
than relabel them. When we place ts 0 1 2 3 8 7 6 4 5 9 10 11 12
the vertices in the order specified tsI 0 1 2 3 7 8 6 5 4 9 10 11 12
in the array ts, from left to right,
then all directed edges point from
left to right. The inverse of the per-
mutation ts is the permutation tsI
Topological sort (rearrange) Given a DAG, rearrange its ver-
that specifies the relabeling de- tices on a horizontal line such that all the directed edges point from
scribed in Figure 19.21. left to right (see Figure 19.22).
As indicated in Figure 19.22, it is easy to establish that the re-
labeling and rearrangement permutations are inverses of one another:
Given a rearrangement, we can obtain a relabeling by assigning the la-
bel 0 to the first vertex on the list, 1 to the second label on the list, and
so forth. For example, if an array ts has the vertices in topologically
sorted order, then the loop
for (i = 0; i < V; i++) tsI[ts[i]] = i;
defines a relabeling in the vertex-indexed array tsI. Conversely, if we
have the relabeling in an array tsI, then we can get the rearrangement
with the loop
for (i = 0; i < V; i++) ts[tsI[i]] = i;
which puts the vertex that would have label 0 first in the list, the vertex
that would have label 1 second in the list, and so forth. Most often,
we use the term topological sort to refer to the rearrangement version
of the problem.
In general, the vertex order produced by a topological sort is not
unique. For example,
8 7 0 1 2 3 6 4 9 10 11 12 5
0 1 2 3 8 6 4 9 10 11 12 5 7
0 2 3 8 6 4 7 5 9 10 1 11 12
8 0 7 6 2 3 4 9 5 1 11 12 10

are all topological sorts of the example DAG in Figure 19.6 (and there
are many others). In a scheduling application, this situation arises
whenever one task has no direct or indirect dependence on another and
DIGRAPHS AND DAGS §19.6 185

Figure 19.23
Reverse topological sort.
In this reverse topological sort of
5 12 11 10 9 4 6 3 2 1 0 7 8 our sample digraph, the edges all
point from right to left. Number-
ing the vertices as specified by
0 1 2 3 4 5 6 7 8 9 10 11 12 the inverse permutation tsI gives
ts 5 12 11 10 9 4 6 3 2 1 0 7 8 a graph where every edge points
tsI 10 9 8 7 5 0 6 11 12 4 3 2 1 from a higher-numbered vertex to a
lower-numbered vertex.

thus they can be performed either before or after the other (or even in
parallel). The number of possible schedules grows exponentially with
the number of such pairs of tasks.
As we have noted, it is sometimes useful to interpret the edges in
a digraph the other way around: We say that an edge directed from
s to t means that vertex s “depends” on vertex t. For example, the
vertices might represent terms to be defined in a book, with an edge
from s to t if the definition of s uses t. In this case, it would be
useful to find an ordering with the property that every term is defined
before it is used in another definition. Using this ordering corresponds
to positioning the vertices in a line such that edges all go from right
to left—a reverse topological sort. Figure 19.23 illustrates a reverse
topological sort of our sample DAG.
Now, it turns out that we have already seen an algorithm for
reverse topological sorting: our standard recursive DFS! When the
input graph is a DAG, a postorder numbering puts the vertices in
reverse topological order. That is, we number each vertex as the final
action of the recursive DFS function, as in the post array in the DFS
implementation in Program 19.2. As illustrated in Figure 19.24, using
this numbering is equivalent to numbering the nodes in the DFS forest
in postorder. Taking the vertices in this example in order of their
postorder numbers, we get the vertex order in Figure 19.23—a reverse
topological sort of the DAG.

Property 19.11 Postorder numbering in DFS yields a reverse topo-


logical sort for any DAG.

Proof : Suppose that s and t are two vertices such that s appears before
t in the postorder numbering even though there is a directed edge s-t
in the graph. Since we are finished with the recursive DFS for s at the
186 §19.6 CHAPTER NINETEEN

time that we assign s its number, we have examined, in particular, the


edge s-t. But if s-t were a tree, down, or cross edge, the recursive
DFS for t would be complete, and t would have a lower number;
however, s-t cannot be a back edge because that would imply a cycle.
This contradiction implies that such an edge s-t cannot exist.

Thus, we can easily adapt a standard DFS to do a topological sort,


as shown in Program 19.6. Depending on the application, we might
wish to package an ADT function for topological sorting that fills in the
client-supplied vertex-indexed array with the postorder numbering, or
we might wish to return the inverse of that permutation, which has the
vertex indices in topologically sorted order. In either case, we might
wish to do a forward or reverse topological sort, for a total of four
different possibilities that we might wish to handle.
Computationally, the distinction between topological sort and
reverse topological sort is not crucial. We have at least three ways
to modify DFS with postorder numbering if we want it to produce a
0 7 8
proper topological sort:
5 6 2 3 1 6 7 • Do a reverse topological sort on the reverse of the given DAG.
9 4 3 • Rather than using it as an index for postorder numbering, push
11 10 12 9 5 4 the vertex number on a stack as the final act of the recursive
procedure. After the search is complete, pop the vertices from
12
the stack. They come off the stack in topological order.
0 1 2 3 4 5 6 7 8 9 10 11 12
post 5 12 11 10 9 4 6 3 2 1 0 7 8 • Number the vertices in reverse order (start at V − 1 and count
down to 0). If desired, compute the inverse of the vertex num-
Figure 19.24 bering to get the topological order.
DFS forest for a DAG
The proofs that these changes give a proper topological ordering are
A DFS forest of a digraph has no
back edges (edges to nodes with left for you to do as an exercise (see Exercise 19.99).
a higher postorder number) if and To implement the first of the options listed in the previous para-
only if the digraph is a DAG. The graph for sparse graphs (represented with adjacency lists), we would
non-tree edges in this DFS forest
for the DAG of Figure 19.21 are
need to use Program 19.1 to compute the reverse graph. Doing so
either down edges (shaded squares) essentially doubles our space usage, which may thus become onerous
or cross edges (unshaded squares). for huge graphs. For dense graphs (represented with an adjacency ma-
The order in which vertices are trix), as noted in Section 19.1, we can do DFS on the reverse without
encountered in a postorder walk
of the forest, shown at the bottom, using any extra space or doing any extra work, simply by exchang-
is a reverse topological sort (see ing rows and columns when referring to the matrix, as illustrated in
Figure 19.23). Program 19.7.
DIGRAPHS AND DAGS §19.6 187

Program 19.6 Reverse topological sort (adjacency lists)


This version of DFS returns an array that contains vertex indices of a
DAG such that the source vertex of every edge appears to the right of
the destination vertex (a reverse topological sort). Comparing the last
line of TSdfsR with, for example, the last line of dfsR in Program 19.2
is an instructive way to understand the computation of the inverse of
the postorder-numbering permutation (see Figure 19.23).

static int cnt0;


static int pre[maxV];
void DAGts(Dag D, int ts[])
{ int v;
cnt0 = 0;
for (v = 0; v < D->V; v++)
{ ts[v] = -1; pre[v] = -1; }
for (v = 0; v < D->V; v++)
if (pre[v] == -1) TSdfsR(D, v, ts);
}
void TSdfsR(Dag D, int v, int ts[])
{ link t;
pre[v] = 0;
for (t = D->adj[v]; t != NULL; t = t->next)
if (pre[t->v] == -1) TSdfsR(D, t->v, ts);
ts[cnt0++] = v;
}

Next, we consider an alternative classical method for topological


sorting that is more like breadth-first search (BFS) (see Section 18.7).
It is based on the following property of DAGs.
Property 19.12 Every DAG has at least one source and at least one
sink.
Proof : Suppose that we have a DAG that has no sinks. Then, starting at
any vertex, we can build an arbitrarily long directed path by following
any edge from that vertex to any other vertex (there is at least one edge,
since there are no sinks), then following another edge from that vertex,
and so on. But once we have been to V + 1 vertices, we must have
seen a directed cycle, by the pigeonhole principle (see Property 19.6),
which contradicts the assumption that we have a DAG. Therefore,
188 §19.6 CHAPTER NINETEEN

Program 19.7 Topological sort (adjacency matrix)


This adjacency-array DFS computes a topological sort (not the reverse)
because we replace the reference to a[v][w] in the DFS by a[w][v],
thus processing the reverse graph (see text).

void TSdfsR(Dag D, int v, int ts[])


{ int w;
pre[v] = 0;
for (w = 0; w < D->V; w++)
if (D->adj[w][v] != 0)
if (pre[w] == -1) TSdfsR(D, w, ts);
ts[cnt0++] = v;
}

every DAG has at least one sink. It follows that every DAG also has
at least one source: its reverse’s sink.

From this fact, we can derive a (relabel) topological-sort algo-


rithm: Label any source with the smallest unused label, then remove it
and label the rest of the DAG, using the same algorithm. Figure 19.25
is a trace of this algorithm in operation for our sample DAG.
Implementing this algorithm efficiently is a classic exercise in
data-structure design (see reference section). First, there may be multi-
ple sources, so we need to maintain a queue to keep track of them (any
generalized queue will do). Second, we need to identify the sources in
the DAG that remains when we remove a source. We can accomplish
this task by maintaining a vertex-indexed array that keeps track of the
indegree of each vertex. Vertices with indegree 0 are sources, so we
can initialize the queue with one scan through the DAG (using DFS or
any other method that examines all of the edges). Then, we perform
the following operations until the source queue is empty:
• Remove a source from the queue and label it.
• Decrement the entries in the indegree array corresponding to the
destination vertex of each of the removed vertex’s edges.
• If decrementing any entry causes it to become 0, insert the corre-
sponding vertex onto the source queue.
Program 19.8 is an implementation of this method, using a FIFO
queue, and Figure 19.26 illustrates its operation on our sample DAG,
DIGRAPHS AND DAGS §19.6 189

0 0
6 7 8 6 7 8
Figure 19.25
1 2 1 2
Topologically sorting a DAG
by removing sources
3 9 10 3 9 10 Since it is a source (no edges point
4 4 to it), 0 can appear first in a topo-
5 11 12 5 11 12 logical sort of this example graph
(left, top). If we remove 0 (and
0 0 all the edges that point from it to
6 7 8 6 7 8 other vertices), then 1 and 2 be-
1 2 1 2 come sources in the resulting DAG
(left, second from top), which we
3 9 10 3 9 10 can then sort using the same al-
4 4 gorithm. This figure illustrates the
5 11 12 5 11 12 operation of Program 19.8, which
picks from among the sources (the
0 0 shaded nodes in each diagram) us-
6 7 8 6 7 8 ing the FIFO discipline, though any
1 2 1 2 of the sources could be chosen at
each step. See Figure 19.26 for the
3 9 10 3 9 10 contents of the data structures that
4 4 control the specific choices that
5 11 12 5 11 12 the algorithm makes. The result of
the topological sort illustrated here
0 0 is the node order 0 8 2 1 7 3 6 5 4
6 7 8 6 7 8 9 11 10 12.
1 2 1 2

3 9 10 3 9 10
4 4
5 11 12 5 11 12

0 0
6 7 8 6 7 8
1 2 1 2

3 9 10 3 9 10
4 4
5 11 12 5 11 12

0 0
6 7 8 6 7 8
1 2 1 2

3 9 10 3 9 10
4 4
5 11 12 5 11 12
190 §19.6 CHAPTER NINETEEN

Program 19.8 Source-queue–based topological sort


0 1 2 3 4 5 6 7 8 9 10 11 12 This implementation maintains a queue of sources and uses a table that
0 1 1 2 2 2 2 1 0 2 1 1 2 0 8
keeps track of the indegree of each vertex in the DAG induced by the
0 0 0 1 2 1 1 1 0 2 1 1 2 8 2 1
0 0 0 1 2 1 1 0 0 2 1 1 2 2 1 7
vertices that have not been removed from the queue. When we remove a
0 0 0 0 2 1 1 0 0 2 1 1 2 1 7 3 source from the queue, we decrement the indegree entries corresponding
0 0 0 0 2 1 1 0 0 2 1 1 2 7 3 to each of the vertices on its adjacency list (and put on the queue any
0 0 0 0 2 1 0 0 0 2 1 1 2 3 6 vertices corresponding to entries that become 0). Vertices come off the
0 0 0 0 1 0 0 0 0 2 1 1 2 6 5 queue in topologically sorted order.
0 0 0 0 0 0 0 0 0 1 1 1 2 5 4
0 0 0 0 0 0 0 0 0 1 1 1 2 4 #include "QUEUE.h"
0 0 0 0 0 0 0 0 0 0 1 1 2 9
0 0 0 0 0 0 0 0 0 0 0 0 1 11 10 static int in[maxV];
0 0 0 0 0 0 0 0 0 0 0 0 0 10 12 void DAGts(Dag D, int ts[])
0 0 0 0 0 0 0 0 0 0 0 0 0 12
{ int i, v; link t;
for (v = 0; v < D->V; v++)
Figure 19.26
Indegree table and queue con- { in[v] = 0; ts[v] = -1; }
tents for (v = 0; v < D->V; v++)
This sequence depicts the contents for (t = D->adj[v]; t != NULL; t = t->next)
of the indegree table (left) and the in[t->v]++;
source queue (right) during the ex- QUEUEinit(D->V);
ecution of Program 19.8 on the
for (v = 0; v < D->V; v++)
sample DAG corresponding to Fig-
ure 19.25. At any given point in if (in[v] == 0) QUEUEput(v);
time, the source queue contains for (i = 0; !QUEUEempty(); i++)
the nodes with indegree 0. Read- {
ing from top to bottom, we remove
ts[i] = (v = QUEUEget());
the leftmost node from the source
queue, decrement the indegree en- for (t = D->adj[v]; t != NULL; t = t->next)
try corresponding to every edge if (--in[t->v] == 0) QUEUEput(t->v);
leaving that node, and add any ver- }
tices whose entries become 0 to
}
the source queue. For example,
the second line of the table reflects
the result of removing 0 from the
source queue, then (because the providing the details behind the dynamics of the example in Fig-
DAG has the edges 0-1, 0-2, 0-3, ure 19.25.
0-5, and 0-6) decrementing the
indegree entries corresponding to
The source queue does not empty until every vertex in the DAG
1, 2, 3, 5, and 6 and adding 2 and is labeled, because the subgraph induced by the vertices not yet labeled
1 to the source queue (because is always a DAG, and every DAG has at least one source. Indeed, we
decrementing made their indegree can use the algorithm to test whether a graph is a DAG by inferring
entries 0). Reading the leftmost
entries in the source queue from that there must be a cycle in the subgraph induced by the vertices not
top to bottom gives a topological yet labeled if the queue empties before all the vertices are labeled (see
ordering for the graph. Exercise 19.106).
DIGRAPHS AND DAGS §19.6 191

Processing vertices in topologically sorted order is a basic tech-


nique in processing DAGs. A classic example is the problem of finding
the length of the longest path in a DAG. Considering the vertices in
reverse topologically sorted order, the length of the longest path origi-
nating at each vertex v is easy to compute: add one to the maximum
of the lengths of the longest paths originating at each of the vertices
reachable by a single edge from v. The topological sort ensures that
all those lengths are known when v is processed, and that no other
paths from v will be found afterwards. For example, taking a left-
to-right scan of the reverse topological sort shown in Figure 19.23 we
can quickly compute the following table of lengths of the longest paths
originating at each vertex in the sample graph in Figure 19.21.
5 12 11 10 9 4 6 3 2 1 0 7 8
0 0 1 0 2 3 4 4 5 0 6 5 6

For example, the 6 corresponding to 0 (third column from the right)


says that there is a path of length 6 originating at 0, which we know
because there is an edge 0-2, we previously found the length of the
longest path from 2 to be 5, and no other edge from 0 leads to a node
having a longer path.
Whenever we use topological sorting for such an application, we
have several choices in developing an implementation:
• Use DAGts in a DAG ADT, then proceed through the array it
computes to process the vertices.
• Process the vertices after the recursive calls in a DFS.
• Process the vertices as they come off the queue in a source-queue–
based topological sort.
All of these methods are used in DAG-processing implementations in
the literature, and it is important to know that they are all equiv-
alent. We will consider other topological-sort applications in Exer-
cises 19.113 and 19.116 and in Sections 19.7 and 21.4.

Exercises
 19.95 Add a topological sort function to your DAG ADT from Exer-
cise 19.78, then add an ADT function that checks whether or not a given
permutation of a DAG’s vertices is a proper topological sort of that DAG.

19.96 How many different topological sorts are there of the DAG that is
depicted in Figure 19.6?
192 §19.6 CHAPTER NINETEEN

 19.97 Give the DFS forest and the reverse topological sort that results from
doing a standard adjacency-lists DFS (with postorder numbering) of the DAG
3-7 1-4 7-8 0-5 5-2 3-8 2-9 0-6 4-9 2-6 6-4 4-3 2-3.

◦ 19.98 Give the DFS forest and the topological sort that results from building
a standard adjacency-lists representation of the DAG
3-7 1-4 7-8 0-5 5-2 3-8 2-9 0-6 4-9 2-6 6-4 4-3 2-3,
then using Program 19.1 to build the reverse, then doing a standard adjacency-
lists DFS with postorder numbering.
• 19.99 Prove the correctness of each of the three suggestions given in the
text for modifying DFS with postorder numbering such that it computes a
topological sort instead of a reverse topological sort.
 19.100 Give the DFS forest and the topological sort that results from do-
ing a standard adjacency-matrix DFS with implicit reversal (and postorder
numbering) of the DAG
3-7 1-4 7-8 0-5 5-2 3-8 2-9 0-6 4-9 2-6 6-4 4-3 2-3
(see Program 19.7).
• 19.101 Given a DAG, does there exist a topological sort that cannot re-
sult from applying a DFS-based algorithm, no matter what order the vertices
adjacent to each vertex are chosen? Prove your answer.
 19.102 Show, in the style of Figure 19.26, the process of topologically sort-
ing the DAG
3-7 1-4 7-8 0-5 5-2 3-8 2-9 0-6 4-9 2-6 6-4 4-3 2-3
with the source-queue algorithm (Program 19.8).
 19.103 Give the topological sort that results if the data structure used in the
example depicted in Figure 19.25 is a stack rather than a queue.
• 19.104 Given a DAG, does there exist a topological sort that cannot result
from applying the source-queue algorithm, no matter what queue discipline is
used? Prove your answer.
19.105 Modify the source-queue topological-sort algorithm to use a gener-
alized queue. Use your modified algorithm with a LIFO queue, a stack, and a
randomized queue.
 19.106 Use Program 19.8 to provide an implementation for the ADT func-
tion for verifying that a DAG has no cycles (see Exercise 19.78).
◦ 19.107 Convert the source-queue topological-sort algorithm into a sink-
queue algorithm for reverse topological sorting.
19.108 Write a program that generates all possible topological orderings of
a given DAG, or, if the number of such orderings exceeds a bound taken as an
argument, prints that number.
DIGRAPHS AND DAGS §19.7 193

19.109 Write a program that converts any digraph with V vertices and E
edges into a DAG by doing a DFS-based topological sort and changing the
orientation of any back edge encountered. Prove that this strategy always
produces a DAG.
•• 19.110 Write a program that produces each of the possible DAGs with V
vertices and E edges with equal likelihood (see Exercise 17.69).
19.111 Give necessary and sufficient conditions for a DAG to have just one
possible topologically sorted ordering of its vertices.
19.112 Run empirical tests to compare the topological-sort algorithms given
in this section for various DAGs (see Exercise 19.2, Exercise 19.79, Ex-
ercise 19.109, and Exercise 19.110). Test your program as described in
Exercise 19.11 (for low densities) and as described in Exercise 19.12 (for
high densities).
 19.113 Modify Program 19.8 so that it computes the number of different
simple paths from any source to each vertex in a DAG.
◦ 19.114 Write a program that evaluates DAGs that represent arithmetic ex-
pressions (see Figure 19.19). Use the adjacency-lists graph ADT, extended to
include a double corresponding to each vertex (to hold its value). Assume
that values corresponding to leaves have been established.
◦ 19.115 Describe a family of arithmetic expressions with the property that
the size of the expression tree is exponentially larger than the size of the cor-
responding DAG (so the running time of your program from Exercise 19.114
for the DAG is proportional to the logarithm of the running time for the tree).
◦ 19.116 Write a program that finds the longest simple directed path in a DAG,
in time proportional to V . Use your program to implement an ADT function
that finds a Hamilton path in a given DAG, if it has one.

19.7 Reachability in DAGs


To conclude our study of DAGs, we consider the problem of computing
the transitive closure of a DAG. Can we develop algorithms for DAGs
that are more efficient than the algorithms for general digraphs that
we considered in Section 19.3?
Any method for topological sorting can serve as the basis for a
transitive-closure algorithm for DAGs, as follows: We proceed through
the vertices in reverse topological order, computing the reachability
vector for each vertex (its row in the transitive-closure matrix) from
the rows corresponding to its adjacent vertices. The reverse topological
sort ensures that all those rows have already been computed. In total,
we check each of the V entries in the vector corresponding to the
194 §19.7 CHAPTER NINETEEN

destination vertex of each of the E edges, for a total running time


0 7 8
proportional to V E. Although it is simple to implement, this method
5 6 2 3 1 6 7
is no more efficient for DAGs than for general digraphs.
9 4 3 When we use a standard DFS for the topological sort (see Pro-
11 10 12 9 5 4
gram 19.7), we can improve performance for some DAGs, as shown
in Program 19.9. Since there are no cycles in a DAG, there are no back
12
edges in any DFS. More important, both cross edges and down edges
5: 0 0 0 0 0 1 0 0 0 0 0 0 0 point to nodes for which the DFS has completed. To take advantage
12: 0 0 0 0 0 0 0 0 0 0 0 0 1 of this fact, we develop a recursive function to compute all vertices
11: 0 0 0 0 0 0 0 0 0 0 0 1 1
10: 0 0 0 0 0 0 0 0 0 0 1 0 0 reachable from a given start vertex, but (as usual in DFS) we make
9: 0 0 0 0 0 0 0 0 0 1 1 1 1 no recursive calls for vertices for which the reachable set has already
4: 0 0 0 0 1 0 0 0 0 1 1 1 1
6: 0 0 0 0 1 0 1 0 0 1 1 1 1
been computed. In this case, the reachable vertices are represented by
3: 0 0 0 1 1 1 0 0 0 1 1 1 1 a row in the transitive closure, and the recursive function takes the
2: 0 0 1 1 1 1 0 0 0 1 1 1 1 logical or of all the rows associated with its adjacent edges. For tree
1: 0 1 0 0 0 0 0 0 0 0 0 0 0
0: 1 1 1 1 1 1 1 0 0 1 1 1 1 edges, we do a recursive call to compute the row; for cross edges, we
7: 0 0 0 0 1 0 1 1 0 1 1 1 1 can skip the recursive call because we know that the row has been
8: 0 0 0 0 1 0 1 1 1 1 1 1 1
computed by a previous recursive call; for down edges, we can skip
the whole computation, because any reachable nodes that would add
Figure 19.27
Transitive closure of a DAG have already been accounted for in the set of reachable nodes for the
destination vertex (lower and earlier in the DFS tree).
This sequence of row vectors is
the transitive closure of the DAG Using this version of DFS might be characterized as using dy-
in Figure 19.21, with rows cre- namic programming to compute the transitive closure, because we
ated in reverse topological order, make use of results that have already been computed to avoid making
computed as the last action in a
unnecessary recursive calls. Figure 19.27 illustrates the computation
recursive DFS function (see Pro-
gram 19.9). Each row is the log- of the transitive closure for the sample DAG in Figure 19.6.
ical or of the rows for adjacent
vertices, which appear earlier in Property 19.13 With dynamic programming and DFS, we can sup-
the list. For example, to compute port constant query time for the abstract transitive closure of a DAG
the row for 0 we take the logical with space proportional to V 2 and time proportional to V 2 + V X
or of the rows for 5, 2, 1, and 6
for preprocessing (computing the transitive closure), where X is the
(and put a 1 corresponding to 0 it-
self) because the edges 0-5, 0-2, number of cross edges in the DFS forest.
0-1, and 0-6 take us from 0 to
any vertex that is reachable from Proof : The proof is immediate by induction from the recursive function
any of those vertices. We can ig- in Program 19.9. We visit the vertices in reverse topological order.
nore down edges because they add Every edge points to a vertex for which we have already computed all
no new information. For example, reachable vertices, and we can therefore compute the set of reachable
we ignore the edge from 0 to 3 be-
cause the vertices reachable from vertices of any vertex by merging together the sets of reachable vertices
3 are already accounted for in the associated with the destination vertex of each edge. Taking the logical
row corresponding to 2. or of the specified rows in the adjacency matrix accomplishes this
DIGRAPHS AND DAGS §19.7 195

Program 19.9 Transitive closure of a DAG


This code computes the transitive closure of a DAG with a single DFS.
We recursively compute the reachable vertices from each vertex from
the reachable vertices of each of its children in the DFS tree. We make
recursive calls for tree edges, use previously computed values for cross
edges, and ignore down edges.

void DAGtc(Dag D)
{ int v;
D->tc = MATRIXint(D->V, D->V, 0);
for (v = 0; v < D->V; v++) pre[v] = -1;
for (v = 0; v < D->V; v++)
if (pre[v] == -1) TCdfsR(D, EDGE(v, v));
}
void TCdfsR(Dag D, Edge e)
{ int u, i, v = e.w;
pre[v] = cnt++;
for (u = 0; u < D->V; u++)
if (D->adj[v][u] != 0)
{
D->tc[v][u] = 1;
if (pre[u] > pre[v]) continue;
if (pre[u] == -1) TCdfsR(D, EDGE(v, u));
for (i = 0; i < D->V; i++)
if (D->tc[u][i] == 1) D->tc[v][i] = 1;
}
}
int DAGreach(Dag D, int s, int t)
{ return D->tc[s][t]; }

merge. We access a row of size V for each tree edge and each cross edge.
There are no back edges, and we can ignore down edges because we
accounted for any vertices they reach when we processed any ancestors
of both nodes earlier in the search.
If our DAG has no down edges (see Exercise 19.43), the running
time of Program 19.9 is proportional to V E and represents no im-
provement over the transitive-closure algorithms that we examined for
general digraphs in Section 19.3 (such as, for example, Program 19.4)
196 §19.8 CHAPTER NINETEEN

or the approach based on topological sorting that is described at the


beginning of this section. On the other hand, if the number of down
edges is large (or, equivalently, the number of cross edges is small),
Program 19.9 will be significantly faster than these methods.
The problem of finding an optimal algorithm (one that is guaran-
teed to finish in time proportional to V 2 ) for computing the transitive
closure of dense DAGs is still unsolved. The best known worst-case
performance bound is V E. However, we are certainly better off us-
ing an algorithm that runs faster for a large class of DAGs, such as
Program 19.9, than we are using one that always runs in time propor-
tional to V E, such as Program 19.4. As we see in Section 19.9, this
performance improvement for DAGs has direct implications for our
ability to compute the transitive closure of general digraphs, as well.

Exercises
◦ 19.117 Show, in the style of Figure 19.27, the reachability vectors that result
when we use Program 19.9 to compute the transitive closure of the DAG

3-7 1-4 7-8 0-5 5-2 3-8 2-9 0-6 4-9 2-6 6-4 4-3 2-3.

19.118 Develop a version of Program 19.9 for an adjacency-matrix DAG


representation.

◦ 19.119 Develop a version of Program 19.9 that uses an adjacency-lists rep-


resentation
 of the transitive closure and that runs in time proportional to
V 2 + e v(e), where the sum is over all edges in the DAG and v(e) is the
number of vertices reachable from the destination vertex of edge e. This cost
will be significantly less than V E for some sparse DAGs (see Exercise 19.68).

◦ 19.120 Develop an ADT implementation for the abstract transitive closure


for DAGs that uses extra space at most proportional to V (and is suitable
for huge DAGs). Use topological sorting to provide quick response when the
vertices are not connected, and use a source-queue implementation to return
the length of the path when they are connected.

◦ 19.121 Develop a transitive-closure implementation based on a sink-queue–


based reverse topological sort (see Exercise 19.107).

◦ 19.122 Does your solution to Exercise 19.121 require that you examine all
edges in the DAG, or are there edges that can be ignored, such as the down
edges in DFS? Give an example that requires examination of all edges, or
characterize the edges that can be skipped.
DIGRAPHS AND DAGS §19.8 197

19.8 Strong Components in Digraphs


Undirected graphs and DAGs are both simpler structures than general
digraphs because of the structural symmetry that characterizes the
reachability relationships among the vertices: In an undirected graph,
if there is a path from s to t, then we know that there is also a path
from t to s; in a DAG, if there is a directed path from s to t, then we
know that there is no directed path from t to s. For general digraphs,
knowing that t is reachable from s gives no information about whether
s is reachable from t.
To understand the structure of digraphs, we consider strong con-
nectivity, which has the symmetry that we seek. If s and t are strongly
connected (each reachable from the other), then, by definition, so are
t and s. As discussed in Section 19.1, this symmetry implies that the
vertices of the digraph divide into strong components, which consist
of mutually reachable vertices. In this section, we discuss three algo-
rithms for finding the strong components in a digraph.
We use the same interface as for connectivity in our general
graph-searching algorithms for undirected graphs (see Program 18.3).
The goal of our algorithms is to assign component numbers to each
vertex in a vertex-indexed array, using the labels 0, 1, . . ., for the
strong components. The highest number assigned is the number of
strong components, and we can use the component numbers to pro-
vide a constant-time test of whether two vertices are in the same strong
component.
A brute-force algorithm to solve the problem is simple to develop.
Using an abstract–transitive-closure ADT, check every pair of vertices
s and t to see whether t is reachable from s and s is reachable from
t. Define an undirected graph with an edge for each such pair: The
connected components of that graph are the strong components of the
digraph. This algorithm is simple to describe and to implement, and
its running time is dominated by the costs of the abstract–transitive-
closure implementation, as described by, say, Property 19.10.
The algorithms that we consider in this section are triumphs
of modern algorithm design that can find the strong components of
any graph in linear time, a factor of V faster than the brute-force
algorithm. For 100 vertices, these algorithms will be 100 times faster
than the brute-force algorithm; for 1000 vertices, they will be 1000
198 §19.8 CHAPTER NINETEEN

times faster; and we can contemplate addressing problems involving


billions of vertices. This problem is a prime example of the power of
good algorithm design, one which has motivated many people to study
graph algorithms closely. Where else might we contemplate reducing
resource usage by a factor of 1 billion or more with an elegant solution
to an important practical problem?
The history of this problem is instructive (see reference section).
In the 1950s and 1960s, mathematicians and computer scientists began
to study graph algorithms in earnest in a context where the analysis of
algorithms itself was under development as a field of study. The broad
variety of graph algorithms to be considered—coupled with ongoing
developments in computer systems, languages, and our understanding
of performing computations efficiently—left many difficult problems
unsolved. As computer scientists began to understand many of the
basic principles of the analysis of algorithms, they began to understand
which graph problems could be solved efficiently and which could not
and then to develop increasingly efficient algorithms for the former set
of problems. Indeed, R. Tarjan introduced linear-time algorithms for
strong connectivity and other graph problems in 1972, the same year
that R. Karp documented the intractability of the travelling-salesperson
problem and many other graph problems. Tarjan’s algorithm has
been a staple of advanced courses in the analysis of algorithms for
many years because it solves an important practical problem using
simple data structures. In the 1980s, R. Kosaraju took a fresh look
at the problem and developed a new solution; people later realized
that a paper that describes essentially the same method appeared in
the Russian scientific literature in 1972. Then, in 1999, H. Gabow
found a simple implementation of one of the first approaches tried in
the 1960s, giving a third linear-time algorithm for this problem.
The point of this story is not just that difficult graph-processing
problems can have simple solutions, but also that the abstractions that
we are using (DFS and adjacency lists) are more powerful than we
might realize. As we become more accustomed to using these and
similar tools, we should not be surprised to discover simple solutions
to other important graph problems, as well. Researchers still seek con-
cise implementations like these for numerous other important graph
algorithms; many such algorithms remain to be discovered.
DIGRAPHS AND DAGS §19.8 199

0 1 9 Figure 19.28
0 2 0 6 8 12 Computing strong compo-
6 7 8 nents (Kosaraju’s algo-
3 4 10 11
1 2 rithm)
4 2 9 4 9

3 9 10
To compute the strong components
6 5
4 of the digraph at the lower left, we
7 0 0 3 first do a DFS of its reverse (top
5 11 12
8 left), computing a postorder ar-
7
ray that gives the vertex indices
in the order in which the recursive
0 1 2 3 4 5 6 7 8 9 10 11 12 DFS completed (top). This order
post 8 7 6 5 4 3 2 0 1 10 11 12 9
is equivalent to a postorder walk
of the DFS forest (top right). Then
1 7
we use the reverse of that order to
9 0
0 do a DFS of the original digraph
6 7 8 11 10 5 1 6 6 8 (bottom). First we check all nodes
1 2 12 12 4 9 4 7 9 reachable from 9, then we scan
from right to left through the ar-
3 9 10 9 3 11 2
ray to find that 1 is the rightmost
4 5 2 unvisited vertex, so we do the re-
5 11 12 cursive call for 1, and so forth. The
0 3
trees in the DFS forest that results
0 1 2 3 4 5 6 7 8 9 10 11 12 from this process define the strong
G->sc 2 1 2 2 2 2 2 3 3 0 0 0 0 components: all vertices in each
tree have the same value in the
vertex-indexed id array (bottom).

Kosaraju’s method is simple to explain and implement. To find


the strong components of a graph, first run DFS on its reverse, comput-
ing the permutation of vertices defined by the postorder numbering.
(This process constitutes a topological sort if the digraph is a DAG.)
Then, run DFS again on the graph, but to find the next vertex to search
(when calling the recursive search function, both at the outset and each
time that the recursive search function returns to the top-level search
function), use the unvisited vertex with the highest postorder number.
The magic of the algorithm is that, when the unvisited vertices
are checked according to the topological sort in this way, the trees in
the DFS forest define the strong components just as trees in a DFS
forest define the connected components in undirected graphs—two
vertices are in the same strong component if and only if they belong
to the same tree in this forest. Figure 19.28 illustrates this fact for our
example, and we will prove it in a moment. Therefore, we can assign
component numbers as we did for undirected graphs, incrementing the
component number each time that the recursive function returns to the
200 §19.8 CHAPTER NINETEEN

Program 19.10 Strong components (Kosaraju’s algorithm)


This implementation finds the strong components of a digraph rep-
resented with adjacency lists. Like our solutions to the connectivity
problem for undirected graphs in Section 18.5, it sets values in the
vertex-indexed array sc such that the entries corresponding to any pair
of vertices are equal if and only if they are in the same strong component.
First, we build the reverse digraph and do a DFS to compute a
postorder permutation. Next, we do a DFS of the original digraph,
using the reverse of the postorder from the first DFS in the search loop
that calls the recursive function. Each recursive call in the second DFS
visits all the vertices in a strong component.

static int post[maxV], postR[maxV];


static int cnt0, cnt1;
void SCdfsR(Graph G, int w)
{ link t;
G->sc[w] = cnt1;
for (t = G->adj[w]; t != NULL; t = t->next)
if (G->sc[t->v] == -1) SCdfsR(G, t->v);
post[cnt0++] = w;
}
int GRAPHsc(Graph G)
{ int v; Graph R;
R = GRAPHreverse(G);
cnt0 = 0; cnt1 = 0;
for (v = 0; v < G->V; v++) R->sc[v] = -1;
for (v = 0; v < G->V; v++)
if (R->sc[v] == -1) SCdfsR(R, v);
cnt0 = 0; cnt1 = 0;
for (v = 0; v < G->V; v++) G->sc[v] = -1;
for (v = 0; v < G->V; v++) postR[v] = post[v];
for (v = G->V-1; v >=0; v--)
if (G->sc[postR[v]] == -1)
{ SCdfsR(G, postR[v]); cnt1++; }
GRAPHdestroy(R);
return cnt1;
}
int GRAPHstrongreach(Graph G, int s, int t)
{ return G->sc[s] == G->sc[t]; }
DIGRAPHS AND DAGS §19.8 201

top-level search function. Program 19.10 is a full implementation of


the method.

Property 19.14 Kosaraju’s method finds the strong components of


a graph in linear time and space.

Proof : The method consists of minor modifications to two DFS pro-


cedures, so the running time is certainly proportional to V 2 for dense
graphs and V + E for sparse graphs (using an adjacency-lists repre-
sentation), as usual. To prove that it computes the strong components
properly, we have to prove that two vertices s and t are in the same tree
in the DFS forest for the second search if and only if they are mutually
reachable.
If s and t are mutually reachable, they certainly will be in the
same DFS tree because when the first of the two is visited, the second
is unvisited and is reachable from the first and so will be visited before
the recursive call for the root terminates.
To prove the converse, we assume that s and t are in the same
tree, and let r be the root of the tree. The fact that s is reachable from
r (through a directed path of tree edges) implies that there is a directed
path from s to r in the reverse digraph. Now, the key to the proof
is that there must also be a path from r to s in the reverse digraph
because r has a higher postorder number than s (since r was chosen
first in the second DFS at a time when both were unvisited) and there
is a path from s to r: If there were no path from r to s, then the
path from s to r in the reverse would leave s with a higher postorder
number. Therefore, there are directed paths from s to r and from r
to s in the digraph and its reverse: s and r are strongly connected.
The same argument proves that t and r are strongly connected, and
therefore s and t are strongly connected.

The implementation for Kosaraju’s algorithm for the adjacency-


matrix digraph representation is even simpler than Program 19.10
because we do not need to compute the reverse explicitly; that problem
is left as an exercise (see Exercise 19.128).
Program 19.10 is packaged as an ADT that represents an optimal
solution to the strong reachability problem that is analogous to our
solutions for connectivity in Chapter 18. In Section 19.9, we examine
the task of extending this solution to compute the transitive closure
202 §19.8 CHAPTER NINETEEN

Program 19.11 Strong components (Tarjan’s algorithm)


With this implementation for the recursive DFS function, a standard
adjacency-lists digraph DFS will result in strong components being iden-
tified in the vertex-indexed array sc, according to our conventions.
We use a stack s (with stack pointer N) to hold each vertex until
determining that all the vertices down to a certain point at the top of the
stack belong to the same strong component. The vertex-indexed array
low keeps track of the lowest preorder number reachable via a series of
down links followed by one up link from each node (see text).

void SCdfsR(Graph G, int w)


{ link t; int v, min;
pre[w] = cnt0++; low[w] = pre[w]; min = low[w];
s[N++] = w;
for (t = G->adj[w]; t != NULL; t = t->next)
{
if (pre[t->v] == -1) SCdfsR(G, t->v);
if (low[t->v] < min) min = low[t->v];
}
if (min < low[w]) { low[w] = min; return; }
do
{ G->sc[(v = s[--N])] = cnt1; low[v] = G->V; }
while (s[N] != w);
cnt1++;
}

and to solve the reachability (abstract transitive closure) problem for


digraphs.
First, however, we consider Tarjan’s algorithm and Gabow’s
algorithm—ingenious methods that require only a few simple modifi-
cations to our basic DFS procedure. They are preferable to Kosaraju’s
algorithm because they use only one pass through the graph and be-
cause they do not require computation of the reverse for sparse graphs.
Tarjan’s algorithm is similar to the program that we studied in
Chapter 17 for finding bridges in undirected graphs (see Program 18.7).
The method is based on two observations that we have already made
in other contexts. First, we consider the vertices in reverse topological
order so that when we reach the end of the recursive function for a
vertex we know we will not encounter any more vertices in the same
DIGRAPHS AND DAGS §19.8 203

strong component (because all the vertices that can be reached from
that vertex have been processed). Second, the back links in the tree
provide a second path from one vertex to another and bind together
the strong components.
The recursive DFS function uses the same computation as Pro-
gram 18.7 to find the highest vertex reachable (via a back edge) from
any descendant of each vertex. It also uses a vertex-indexed array to
keep track of the strong components and a stack to keep track of the
current search path. It pushes the vertex names onto a stack on en-
try to the recursive function, then pops them and assigns component
numbers after visiting the final member of each strong component.
The algorithm is based on our ability to identify this moment with a
simple test (based on keeping track of the highest ancestor reachable
via one up link from all descendants of each node) at the end of the
recursive procedure that tells us that all vertices encountered since en-
try (except those already assigned to a component) belong to the same
strong component.
The implementation in Program 19.11 is a succinct and complete
description of the algorithm that fills in the details missing from the
brief sketch just given. Figure 19.29 illustrates the operation of the
algorithm for our sample digraph from Figure 19.1.

Property 19.15 Tarjan’s algorithm finds the strong components of a


digraph in linear time.

Proof sketch: If a vertex s has no descendants or up links in the DFS


tree, or if it has a descendant in the DFS tree with an up link that points
to s and no descendants with up links that point higher up in the tree,
then it and all its descendants (except those vertices that satisfy the
same property and their descendants) constitute a strong component.
To establish this fact, we note that every descendant t of s that does
not satisfy the stated property has some descendant that has an up link
pointing higher than t in the tree. There is a path from s to t down
through the tree and we can find a path from t to s as follows: go
down from t to the vertex with the up link that reaches past t, then
continue the same process from that vertex until reaching s.
As usual, the method is linear time because it consists of adding
a few constant-time operations to a standard DFS.
204 §19.8 CHAPTER NINETEEN

Figure 19.29
Computing strong compo- 0 7
nents (Tarjan and Gabow
algorithms) 5 1 6 6 8
4 9 4 7 9
Tarjan’s algorithm is based on a re-
cursive DFS, augmented to push 3 11 2

vertices on a stack. It computes a 5 2 12


component index for each vertex 0 3 9
in a vertex-indexed array G->sc,
using auxiliary arrays pre and 11 10
low (center). The DFS tree for our 12

sample graph is shown at the top


and an edge trace at the bottom
left. In the center at the bottom 0 1 2 3 4 5 6 7 8 9 10 11 12
is the main stack: we push ver- pre 0 9 4 3 2 1 10 11 12 7 8 5 6
tices reached by tree edges. Using low 0 9 0 0 0 0 0 11 11 5 6 5 5
a DFS to consider the vertices in G->sc 2 1 2 2 2 2 2 3 3 0 0 0 0
reverse topological order, we com-
pute, for each v, the highest point
reachable via a back link from an
ancestor (low[v]). When a vertex
v has pre[v] = low[v] (vertices
11, 1, 0, and 7 here) we pop it and 0-0 0 0
all the vertices above it (shaded) 0-5 0 5 0 5
and assign them all the next com- 5-4 0 5 4 0 5 4
ponent number. 4-3 0 5 4 3 0 5 4 3
3-5 0 5 4 3 0 5
In Gabow’s algorithm, we
3-2 0 5 4 3 2 0 5 2
push vertices on the main stack, 2-0 0 5 4 3 2 0
just as in Tarjan’s algorithm, but we 2-3 0 5 4 3 2 0
also keep a second stack (bottom 4-11 0 5 4 3 2 11 0 11
right) with vertices on the search 11-12 0 5 4 3 2 11 12 0 11 12
path that are known to be in dif- 12-9 0 5 4 3 2 11 12 9 0 11 12 9
9-11 0 5 4 3 2 11 12 9 0 11
ferent strong components, by pop-
9-10 0 5 4 3 2 11 12 9 10 0 11 10
ping all vertices after the destina- 10-12 0 5 4 3 2 11
11 12 9 10 0 11
tion of each back edge. When we 11 0 5 4 3 2 0
complete a vertex v with v at the 4-2 0 5 4 3 2 0
top of this second stack (shaded), 0-1 0 5 4 3 2 1 0 1
we know that all vertices above v 1 0 5 4 3 2 0
0-6 0 5 4 3 2 6 0 6
on the main stack are in the same
6-9 0 5 4 3 2 6 0 9
strong component. 6-4 0 5 4 3 2 6 0
0
7-7 7 7
7-6 7 7
7-8 7 8 7 8
8-7 7 8 7
8-9 7 8 7
7
DIGRAPHS AND DAGS §19.8 205

Program 19.12 Strong components (Gabow’s algorithm)


This program performs the same computation as Program 19.11, but
uses a second stack path instead of the vertex-indexed array low to
decide when to pop the vertices in each strong component from the
main stack (see text).

void SCdfsR(Graph G, int w)


{ link t; int v;
pre[w] = cnt0++;
s[N++] = w; path[p++] = w;
for (t = G->adj[w]; t != NULL; t = t->next)
if (pre[t->v] == -1) SCdfsR(G, t->v);
else if (G->sc[t->v] == -1)
while (pre[path[p-1]] > pre[t->v]) p--;
if (path[p-1] != w) return; else p--;
do G->sc[s[--N]] = cnt1; while (s[N] != w);
cnt1++;
}

In 1999 Gabow discovered the version of Tarjan’s algorithm in


Program 19.12. The algorithm maintains the same stack of vertices
in the same way as does Tarjan’s algorithm, but it uses a second stack
(instead of a vertex-indexed array of preorder numbers) to decide when
to pop all the vertices in each strong component from the main stack.
The second stack contains vertices on the search path. When a back
edge shows that a sequence of such vertices all belong to the same
strong component, we pop that stack to leave only the destination
vertex of the back edge, which is nearer the root of the tree than are
any of the other vertices. After processing all the edges for each vertex
(making recursive calls for the tree edges, popping the path stack for
the back edges, and ignoring the down edges), we check to see whether
the current vertex is at the top of the path stack. If it is, it and all the
vertices above it on the main stack make a strong component, and we
pop them and assign the next strong component number to them, as
we did in Tarjan’s algorithm.
The example in Figure 19.29 also shows the contents of this
second stack. Thus, this figure also illustrates the operation of Gabow’s
algorithm.
206 §19.8 CHAPTER NINETEEN

Property 19.16 Gabow’s algorithm finds the strong components of


a digraph in linear time.
Formalizing the argument just outlined and proving the relationship
between the stack contents that it depends upon is an instructive ex-
ercise for mathematically inclined readers (see Exercise 19.136). As
usual, the method is linear time, because it consists of adding a few
constant-time operations to a standard DFS.
The strong-components algorithms that we have considered in
this section are all ingenious and are deceptively simple. We have
considered all three because they are testimony to the power of funda-
mental data structures and carefully crafted recursive programs. From
a practical standpoint, the running time of all the algorithms is pro-
portional to the number of edges in the digraph, and performance
differences are likely to be dependent upon implementation details.
For example, pushdown-stack ADT operations constitute the inner
loop of Tarjan’s and Gabow’s algorithm. Our implementations use
explicitly coded stacks; implementations that use a stack ADT may
be slower. The implementation of Kosaraju’s algorithm is perhaps the
simplest of the three, but it suffers the slight disadvantage (for sparse
digraphs) of requiring three passes through the edges (one to make the
reverse and two DFS passes).
Next, we consider a key application of computing strong com-
ponents: building an efficient reachability (abstract transitive closure)
ADT for digraphs.
Exercises
 19.123 Describe what happens when you use Kosaraju’s algorithm to find
the strong components of a DAG.
 19.124 Describe what happens when you use Kosaraju’s algorithm to find
the strong components of a digraph that consists of a single cycle.
•• 19.125 Can we avoid computing the reverse of the digraph in the adjacency-
lists version of Kosaraju’s method (Program 19.10) by using one of the three
techniques mentioned in Section 19.4 for avoiding the reverse computation
when doing a topological sort? For each technique, give either a proof that it
works or a counterexample that shows that it does not work.
◦ 19.126 Show, in the style of Figure 19.28, the DFS forests and the contents
of the auxiliary vertex-indexed arrays that result when you use Kosaraju’s
algorithm to compute the strong components of the reverse of the digraph in
Figure 19.5. (You should have the same strong components.)
DIGRAPHS AND DAGS §19.8 207

19.127 Show, in the style of Figure 19.28, the DFS forests and the contents
of the auxiliary vertex-indexed arrays that result when you use Kosaraju’s
algorithm to compute the strong components of the digraph
3-7 1-4 7-8 0-5 5-2 3-8 2-9 0-6 4-9 2-6 6-4.

◦ 19.128 Implement Kosaraju’s algorithm for finding the strong components


of a digraph for an ADT that uses the adjacency-matrix representation. Do not
explicitly compute the reverse. Hint: Consider using two different recursive
DFS functions.
 19.129 Describe what happens when you use Tarjan’s algorithm to find the
strong components of a DAG.
 19.130 Describe what happens when you use Tarjan’s algorithm to find the
strong components of a digraph that consists of a single cycle.

◦ 19.131 Show, in the style of Figure 19.29, the DFS forest, stack contents
during the execution of the algorithm, and the final contents of the auxiliary
vertex-indexed arrays that result when you use Tarjan’s algorithm to compute
the strong components of the reverse of the digraph in Figure 19.5. (You
should have the same strong components.)
19.132 Show, in the style of Figure 19.29, the DFS forest, stack contents
during the execution of the algorithm, and the final contents of the auxiliary
vertex-indexed arrays that result when you use Tarjan’s algorithm to compute
the strong components of the digraph
3-7 1-4 7-8 0-5 5-2 3-8 2-9 0-6 4-9 2-6 6-4.

◦ 19.133 Modify the implementations of Tarjan’s algorithm in Program 19.11


and of Gabow’s algorithm in Program 19.12 such that they use sentinel values
to avoid the need to check explicitly for cross links.
19.134 Modify the implementations of Tarjan’s algorithm in Program 19.11
and of Gabow’s algorithm in Program 19.12 such that they use a stack ADT.
19.135 Show, in the style of Figure 19.29, the DFS forest, contents of both
stacks during the execution of the algorithm, and the final contents of the
auxiliary vertex-indexed arrays that result when you use Gabow’s algorithm
to compute the strong components of the digraph
3-7 1-4 7-8 0-5 5-2 3-8 2-9 0-6 4-9 2-6 6-4.

• 19.136 Give a full proof of Property 19.16.


◦ 19.137 Develop a version of Gabow’s algorithm that finds bridges and edge-
connected components in undirected graphs.
• 19.138 Develop a version of Gabow’s algorithm that finds articulation points
and biconnected components in undirected graphs.
208 §19.9 CHAPTER NINETEEN

19.139 Develop a table in the spirit of Table 18.1 to study strong connectivity
in random digraphs (see Table 19.2). Let S be the set of vertices in the largest
strong component. Keep track of the size of S and study the percentages of
edges in the following four classes: those connecting two vertices in S, those
pointing out of S, those pointing in to S, those connecting two vertices not in
S.
19.140 Run empirical tests to compare the brute-force method for comput-
ing strong components described at the beginning of this section, Kosaraju’s
algorithm, Tarjan’s algorithm, and Gabow’s algorithm, for various types of
digraphs (see Exercises 19.11–18).
••• 19.141 Develop a linear-time algorithm for strong 2-connectivity: Determine
whether a strongly connected digraph has the property that it remains strongly
connected after deleting any vertex (and all its incident edges).

19.9 Transitive Closure Revisited


By putting together the results of the previous two sections, we can
develop an algorithm to solve the abstract–transitive-closure problem
for digraphs that—although it offers no improvement over a DFS-
based solution in the worst case—will provide an optimal solution in
many situations.
The algorithm is based on preprocessing the digraph to build the
latter’s kernel DAG (see Property 19.2). The algorithm is efficient if
the kernel DAG is small relative to the size of the original digraph. If
the digraph is a DAG (and therefore is identical to its kernel DAG)
or if it has just a few small cycles, we will not see any significant cost
savings; however, if the digraph has large cycles or large strong com-
ponents (and therefore a small kernel DAG), we can develop optimal
or near-optimal algorithms. For clarity, we assume the kernel DAG
to be sufficiently small that we can afford an adjacency-matrix rep-
resentation, although the basic idea is still effective for larger kernel
DAGs.
To implement the abstract transitive closure, we preprocess the
digraph as follows:
• Find its strong components
• Build its kernel DAG
• Compute the transitive closure of the kernel DAG
We can use Kosaraju’s, Tarjan’s, or Gabow’s algorithm to find the
strong components; a single pass through the edges to build the kernel
DIGRAPHS AND DAGS §19.9 209

DAG (as described in the next paragraph); and DFS (Program 19.9) to
compute its transitive closure. After this preprocessing, we can imme-
diately access the information necessary to determine reachability.
Once we have a vertex-indexed array with the strong compo-
nents of a digraph, building an adjacency-array representation of its
kernel DAG is a simple matter. The vertices of the DAG are the com-
ponent numbers in the digraph. For each edge s-t in the original
digraph, we simply set D->adj[sc[s]][sc[t]] to 1. We would have
to cope with duplicate edges in the kernel DAG if we were using an
adjacency-lists representation—in an adjacency array, duplicate edges
simply correspond to setting an array entry to 1 that has already been
set to 1. This small point is significant because the number of duplicate
edges is potentially huge (relative to the size of the kernel DAG) in this
application.
Property 19.17 Given two vertices s and t in a digraph D, let sc(s)
and sc(t), respectively, be their corresponding vertices in D’s kernel
DAG K. Then, t is reachable from s in D if and only if sc(t) is
reachable from sc(s) in K.
This simple fact follows from the definitions. In particular, this prop-
erty assumes the convention that a vertex is reachable from itself (all
vertices have self-loops). If the vertices are in the same strong compo-
nent (sc(s) = sc(t)), then they are mutually reachable.
We determine whether a given vertex t is reachable from a given
vertex s in the same way as we built the kernel DAG: We use the
vertex-indexed array computed by the strong-components algorithm
to get component numbers sc(s) and sc(t) (in constant time), then
use those numbers to index into the transitive closure of the kernel
DAG (in constant time), which tells us the result. Program 19.13 is an
implementation of the abstract–transitive-closure ADT that embodies
these ideas.
We use an abstract–transitive-closure interface for the kernel
DAG as well. For purposes of analysis, we suppose that we use
an adjacency-matrix representation for the kernel DAG because we
expect the kernel DAG to be small, if not also dense.
Property 19.18 We can support constant query time for the abstract
transitive closure of a digraph with space proportional to V + v 2 and
time proportional to E + v 2 + vx for preprocessing (computing the
210 §19.9 CHAPTER NINETEEN

Program 19.13 Strong-component–based transitive closure


This program computes the abstract transitive closure of a digraph by
computing its strong components, kernel DAG, and the transitive clo-
sure of the kernel DAG (see Program 19.9). The vertex-indexed array
sc gives the strong component index for each vertex, or its correspond-
ing vertex index in the kernel DAG. A vertex t is reachable from a
vertex s in the digraph if and only if sc[t] is reachable from sc[s] in
the kernel DAG.
Dag K;
void GRAPHtc(Graph G)
{ int v, w; link t; int *sc = G->sc;
K = DAGinit(GRAPHsc(G));
for (v = 0; v < G->V; v++)
for (t = G->adj[v]; t != NULL; t = t->next)
DAGinsertE(K, dagEDGE(sc[v], sc[t->v]));
DAGtc(K);
}
int GRAPHreach(Graph G, int s, int t)
{ return DAGreach(K, G->sc[s], G->sc[t]); }

transitive closure), where v is the number of vertices in the kernel


DAG and x is the number of cross edges in its DFS forest.

Proof : Immediate from Property 19.13.

If the digraph is a DAG, then the strong-components compu-


tation provides no new information, and this algorithm is the same
as Program 19.9; in general digraphs that have cycles, however, this
algorithm is likely to be significantly faster than Warshall’s algorithm
or the DFS-based solution. For example, Property 19.18 immediately
implies the following result.

Property 19.19 We can support constant query time for the abstract

transitive closure of any digraph whose kernel DAG has less than 3 V
vertices with space proportional to V and time proportional to E + V
for preprocessing.

Proof : Take v < 3 V in Property 19.18. Then, xv < V since x < v 2 .
DIGRAPHS AND DAGS §19.9 211

Table 19.2 Properties of random digraphs

This table shows the numbers of edges and vertices in the kernel DAGs
for random digraphs generated from two different models (the directed
versions of the models in Table 18.1). In both cases, the kernel DAG
becomes small (and is sparse) as the density increases.

random edges random 10-neighbors

E v e v e

1000 vertices
1000 983 981 916 755
2000 424 621 713 1039
5000 13 13 156 313
10000 1 1 8 17
20000 1 1 1 1
10000 vertices
50000 144 150 1324 150
100000 1 1 61 123
200000 1 1 1 1

Key:
v Number of vertices in kernel DAG
e Number of edges in kernel DAG

We might consider other variations on these bounds. For exam-


ple, if we are willing to use space proportional to E, we√can achieve
the same time bounds when the kernel DAG has up to 3 E vertices.
Moreover, these time bounds are conservative because they assume
that the kernel DAG is dense with cross edges—and certainly it need
not be so.
The primary limiting factor in the applicability of this method is
the size of the kernel DAG. The more similar our digraph is to a DAG
(the larger its kernel DAG), the more difficulty we face in computing its
transitive closure. Note that (of course) we still have not violated the
lower bound implicit in Property 19.9, since the algorithm runs in time
proportional to V 3 for dense DAGs; we have, however, significantly
212 §19.10 CHAPTER NINETEEN

broadened the class of graphs for which we can avoid this worst-
case performance. Indeed, constructing a random-digraph model that
produces digraphs for which the algorithm is slow is a challenge (see
Exercise 19.146).
Table 19.2 displays the results of an empirical study; it shows
that random digraphs have small kernel DAGs even for moderate
densities and even in models with severe restrictions on edge placement.
Although there can be no guarantees in the worst case, we can expect
to see huge digraphs with small kernel DAGs in practice. When we
do have such a digraph, we can provide an efficient implementation of
the abstract–transitive-closure ADT.

Exercises
• 19.142 Develop a version of the implementation of the abstract transitive
closure for digraphs based on using an adjacency-lists representation of the
kernel DAG. Your challenge is to eliminate duplicates on the list without using
an excessive amount of time or space (see Exercise 19.68).

 19.143 Show the kernel DAG computed by Program 19.13 and its transitive
closure for the digraph

3-7 1-4 7-8 0-5 5-2 3-8 2-9 0-6 4-9 2-6 6-4.

◦ 19.144 Convert the strong-component–based abstract–transitive-closure im-


plementation (Program 19.13) into an efficient program that computes the
adjacency matrix of the transitive closure for a digraph represented with an
adjacency matrix, using Gabow’s algorithm to compute the strong components
and the improved Warshall’s algorithm to compute the transitive closure of
the DAG.

19.145 Do empirical studies to estimate the expected size of the kernel DAG
for various types of digraphs (see Exercises 19.11–18).

•• 19.146 Develop a random-digraph model that generates digraphs that have


large kernel DAGs. Your generator must generate edges one at a time, but it
must not make use of any structural properties of the resulting graph.

19.147 Develop an implementation of the abstract transitive closure in a


digraph by finding the strong components and building the kernel DAG, then
answering reachability queries in the affirmative if the two vertices are in the
same strong component, and doing a DFS in the DAG to determine reachability
otherwise.
DIGRAPHS AND DAGS §19.10 213

19.10 Perspective
In this chapter, we have considered algorithms for solving the
topological-sorting, transitive-closure, and shortest-paths problems for
digraphs and for DAGs, including fundamental algorithms for finding
cycles and strong components in digraphs. These algorithms have nu-
merous important applications in their own right and also serve as the
basis for the more difficult problems involving weighted graphs that
we consider in the next two chapters. Worst-case running times of
these algorithms are summarized in Table 19.3.
In particular, a common theme through the chapter has been
the solution of the abstract–transitive-closure problem, where we wish
to support an ADT that can determine quickly, after preprocessing,
whether there is a directed path from one given vertex to another.
Despite a lower bound that implies that our worst-case preprocessing
costs are significantly higher than V 2 , the method discussed in Sec-
tion 19.7 melds the basic methods from throughout the chapter into
a simple solution that provides optimal performance for many types
of digraphs—the significant exception being dense DAGs. The lower
bound suggests that better guaranteed performance on all graphs will
be difficult to achieve, but we can use these methods to get good per-
formance on practical graphs.
The goal of developing an algorithm with performance charac-
teristics similar to the union-find algorithms of Chapter 1 for dense di-
graphs remains elusive. Ideally, we would like to define an ADT where
we can add directed edges or test whether one vertex is reachable from
another and to develop an implementation where we can support all
the operations in constant time (see Exercises 19.157 through 19.159).
As discussed in Chapter 1, we can come close to that goal for undi-
rected graphs, but comparable solutions for digraphs or DAGs are still
not known. (Note that deleting edges presents a challenge even for
undirected graphs.) Not only does this dynamic reachability problem
have both fundamental appeal and direct application in practice, but
also it plays a critical role in the development of algorithms at a higher
level of abstraction. For example, reachability lies at the heart of the
problem of implementing the network simplex algorithm for the min-
cost flow problem, a problem-solving model of wide applicability that
we consider in Chapter 22.
214 §19.10 CHAPTER NINETEEN

Table 19.3 Worst-case cost of digraph-processing operations

This table summarizes the cost (worst-case running time) of algorithms


for various digraph-processing problems considered in this chapter, for
random graphs and graphs where edges randomly connect each vertex
to one of 10 specified neighbors. All costs assume use of the adjacency-
list representation; for the adjacency-matrix representation the E entries
become V 2 entries, so, for example, the cost of computing all shortest
paths is V 3 . The linear-time algorithms are optimal, so the costs will
reliably predict the running time on any input; the others may be overly
conservative estimates of cost, so the running time may be lower for
certain types of graphs. Performance characteristics of the fastest algo-
rithms for computing the transitive closure of a digraph depend on the
digraph’s structure, particularly the size of its kernel DAG.

problem cost algorithm

digraphs
cycle detect E DFS
transitive closure V(E+V) DFS from each vertex
single-source shortest paths E DFS
all shortest paths V(E+V) DFS from each vertex
strong components E Kosaraju, Tarjan, or Gabow
transitive closure E + v(v + x) kernel DAG
DAGs
acyclic verify E DFS or source queue
topological sort E DFS or source queue
transitive closure V(V+E) DFS
transitive closure V(V+X) DFS/dynamic programming

Many other algorithms for processing digraphs and DAGs have


important practical applications and have been studied in detail, and
many digraph-processing problems still call for the development of
efficient algorithms. The following list is representative.
Dominators Given a DAG with all vertices reachable from a
single source r, a vertex s dominates a vertex t if every path from r to
t contains s. (In particular, each vertex dominates itself.) Every vertex
v other than the source has an immediate dominator that dominates v
but does not dominate any dominator of v but v and itself. The set of
DIGRAPHS AND DAGS §19.10 215

immediate dominators is a tree that spans all vertices reachable from


the source. This structure is important in compiler implementations.
The dominator tree can be computed in linear time with a DFS-based
approach that uses several ancillary data structures, although a slightly
slower version is typically used in practice.
Transitive reduction Given a digraph, find a digraph that has
the same transitive closure and the smallest number of edges among
all such digraphs. This problem is tractable (see Exercise 19.154); but
if we restrict it to insist that the result be a subgraph of the original
graph, it is NP-hard.
Directed Euler path Given a digraph, is there a directed path
connecting two given vertices that uses each edge in the digraph exactly
once? This problem is easy by essentially the same arguments as we
used for the corresponding problem for undirected graphs, which we
considered in Section 17.7 (see Exercise 17.92).
Directed mail carrier Given a digraph, find a directed tour with
a minimal number of edges that uses every edge in the graph at least
once (but is allowed to use edges multiple times). As we shall see in
Section 22.7, this problem reduces to the mincost-flow problem and is
therefore tractable.
Directed Hamilton path Find the longest simple directed path
in a digraph. This problem is NP-hard, but it is easy if the digraph is
a DAG (see Exercise 19.116).
Uniconnected subgraph A digraph is said to be uniconnected if
there is at most one directed path between any pair of vertices. Given
a digraph and an integer k, determine whether there is a uniconnected
subgraph with at least k edges. This problem is known to be NP-hard
for general k.
Feedback vertex set Decide whether a given digraph has a
subset of at most k vertices that contains at least one vertex from every
directed cycle in G. This problem is known to be NP-hard.
Even cycle Decide whether a given digraph has a cycle of even
length. As mentioned in Section 17.8, this problem, while not in-
tractable, is so difficult to solve that no one has yet devised an algo-
rithm that is useful in practice.
Just as for undirected graphs, myriad digraph-processing prob-
lems have been studied, and knowing whether a problem is easy or in-
tractable is often a challenge (see Section 17.8). As indicated through-
216 §19.10 CHAPTER NINETEEN

out this chapter, some of the facts that we have discovered about
digraphs are expressions of more general mathematical phenomena,
and many of our algorithms have applicability at levels of abstrac-
tion different from that at which we have been working. On the one
hand, the concept of intractability tells us that we might encounter
fundamental roadblocks in our quest for efficient algorithms that can
guarantee efficient solutions to some problems. On the other hand, the
classic algorithms described in this chapter are of fundamental impor-
tance and have broad applicability, as they provide efficient solutions
to problems that arise frequently in practice and would otherwise be
difficult to solve.
Exercises
19.148 Adapt Programs 17.13 and 17.14 to implement an ADT function
for printing an Euler path in a digraph, if one exists. Explain the purpose of
any additions or changes that you need to make in the code.
 19.149 Draw the dominator tree of the digraph
3-7 1-4 7-8 0-5 5-2 3-0 2-9 0-6 4-9 2-6
6-4 1-5 8-2 9-0 8-3 4-5 2-3 1-6 3-5 7-6.

•• 19.150 Write an ADT function that uses DFS to create a parent-link repre-
sentation of the dominator tree of a given digraph (see reference section).
◦ 19.151 Find a transitive reduction of the digraph
3-7 1-4 7-8 0-5 5-2 3-0 2-9 0-6 4-9 2-6
6-4 1-5 8-2 9-0 8-3 4-5 2-3 1-6 3-5 7-6.

• 19.152 Find a subgraph of the digraph


3-7 1-4 7-8 0-5 5-2 3-0 2-9 0-6 4-9 2-6
6-4 1-5 8-2 9-0 8-3 4-5 2-3 1-6 3-5 7-6
that has the same transitive closure and the smallest number of edges among
all such subgraphs.
◦ 19.153 Prove that every DAG has a unique transitive reduction, and give an
efficient ADT function implementation for computing the transitive reduction
of a DAG.
• 19.154 Write an efficient ADT function for digraphs that computes a transi-
tive reduction.
19.155 Give an algorithm that determines whether or not a given digraph is
uniconnected. Your algorithm should have a worst-case running time propor-
tional to V E.
DIGRAPHS AND DAGS §19.10 217

19.156 Find the largest uniconnected subgraph in the digraph


3-7 1-4 7-8 0-5 5-2 3-0 2-9 0-6 4-9 2-6
6-4 1-5 8-2 9-0 8-3 4-5 2-3 1-6 3-5 7-6.

 19.157 Develop a package of digraph ADT functions for constructing a


graph from an array of edges, inserting an edge, deleting an edge, and testing
whether two vertices are in the same strong component, such that construction,
insertion, and deletion all take linear time and strong-connectivity queries take
constant time, in the worst case.
◦ 19.158 Solve Exercise 19.157, in such a way that insertion, deletion, and
strong-connectivity queries all take time proportional to log V in the worst
case.
••• 19.159 Solve Exercise 19.157, in such a way that insertion, deletion, and
strong-connectivity queries all take near-constant time (as they do for the
union-find algorithms for connectivity in undirected graphs).
This page intentionally left blank
CHAPTER TWENTY

Minimum Spanning Trees

A PPLICATIONS OFTEN CALL for a graph model where we as-


sociate weights or costs with each edge. In an airline map where
edges represent flight routes, these weights might represent distances
or fares. In an electric circuit where edges represent wires, the weights 0-6 .51
0-1 .32 .29 2
might represent the length of the wire, its cost, or the time that it takes 0-2 .29 0 .3 .51
1
a signal to propagate through it. In a job-scheduling problem, weights 4-3 .34 .25 6

.32
5-3 .18 7
might represent time or the cost of performing tasks or of waiting for 7-4 .46 1
.2
tasks to be performed. 5-4 .40 1

.60
0-5 .60

.46

.51
Questions that entail cost minimization naturally arise for such
6-4 .51
situations. We examine algorithms for two such problems: (i) find the 7-0 .31 3 .3
4

8
.1
lowest-cost way to connect all of the points, and (ii) find the lowest- 7-6 .25
.40
7-1 .21 5 4
cost path between two given points. The first type of algorithm, which
is useful for undirected graphs that represent objects such as electric
Figure 20.1
circuits, finds a minimum spanning tree; it is the subject of this chap- A weighted undirected graph
ter. The second type of algorithm, which is useful for digraphs that and its MST
represent objects such as an airline route map, finds the shortest paths; A weighted undirected graph is a
it is the subject of Chapter 21. These algorithms have broad applica- set of weighted edges. The MST
bility beyond circuit and map applications, extending to a variety of is a set of edges of minimal total
weight that connects the vertices
problems that arise on weighted graphs.
(black in the edge list, thick edges
When we study algorithms that process weighted graphs, our in the graph drawing). In this par-
intuition is often supported by thinking of the weights as distances: ticular graph, the weights are pro-
we speak of “the vertex closest to x,” and so forth. Indeed, the term portional to the distances between
“shortest path” embraces this bias. Despite numerous applications the vertices, but the basic algo-
rithms that we consider are appro-
where we actually do work with distance and despite the benefits of priate for general graphs and make
geometric intuition in understanding the basic algorithms, it is impor- no assumptions about the weights
tant to remember that the weights do not need to be proportional to a (see Figure 20.2).

219
220 CHAPTER TWENTY

distance at all; they might represent time or cost or an entirely different


variable. Indeed, as we see in Chapter 21, weights in shortest-paths
problems can even be negative.
To appeal to intuition in describing algorithms and examples
while still retaining general applicability, we use ambiguous termi-
nology where we refer interchangeably to edge lengths and weights.
When we refer to a “short” edge, we mean a “low-weight” edge, and
so forth. For most of the examples in this chapter, we use weights that
are proportional to the distances between the vertices, as shown in
Figure 20.1. Such graphs are more convenient for examples, because
we do not need to carry the edge labels and can still tell at a glance
that longer edges have weights higher than those of shorter edges.
When the weights do represent distances, we can consider algorithms
that gain efficiency by taking into account geometric properties (Sec-
tions 20.7 and 21.5). With that exception, the algorithms that we
consider simply process the edges and do not take advantage of any
implied geometric information (see Figure 20.2).
0-6 .39 The problem of finding the minimum spanning tree of an arbi-
0-1 .08 .99 2
0-2 .99 0 .4 .39 trary weighted undirected graph has numerous important applications,
9
4-3 .65
.65 6 and algorithms to solve it have been known since at least the 1920s;
.08

5-3 .37 7
7-4 .12 .2
8 but the efficiency of implementations varies widely, and researchers still
5-4 .78 1 seek better methods. In this section, we examine three classical algo-
.65

0-5 .65
.01
.12

6-4 .01
rithms that are easily understood at a conceptual level; in Sections 20.3
7-0 .49 3 .6 through 20.5, we examine implementations of each in detail; and in
5
7
.3

7-6 .65
7-1 .28 5
.78
4 Section 20.6, we consider comparisons of and improvements on these
basic approaches.
Figure 20.2 Definition 20.1 A minimum spanning tree (MST) of a weighted
Arbitrary weights
graph is a spanning tree whose weight (the sum of the weights of its
In this example, the edge weights edges) is no larger than the weight of any other spanning tree.
are arbitrary and do not relate to
the geometry of the drawn graph If the edge weights are all positive, it suffices to define the MST as
representation at all. This example
also illustrates that the MST is not the set of edges with minimal total weight that connects all the vertices,
necessarily unique if edge weights as such a set of edges must form a spanning tree. The spanning-tree
may be equal: we get one MST by condition in the definition is included so that it applies for graphs that
including 3-4 (shown) and a differ- may have negative edge weights (see Exercises 20.2 and 20.3).
ent MST by including 0-5 instead
(although 7-6, which has the same If edges can have equal weights, the minimum spanning tree may
weight as those two edges, is not not be unique. For example, Figure 20.2 shows a graph that has two
in any MST). different MSTs. The possibility of equal weights also complicates the
MINIMUM SPANNING TREES 221

descriptions and correctness proofs of some of our algorithms. We


have to consider equal weights carefully, because they are not unusual
in applications and we want to know that our algorithms operate
correctly when they are present.
Not only might there be more than one MST, but also the nomen-
clature does not capture precisely the concept that we are minimizing
the weight rather than the tree itself. The proper adjective to describe
a specific tree is minimal (one having the smallest weight). For these
reasons, many authors use more accurate terms like minimal spanning
tree or minimum-weight spanning tree. The abbreviation MST, which
we shall use most often, is universally understood to capture the basic
concept.
Still, to avoid confusion when describing algorithms for networks
that may have edges with equal weights, we do take care to be precise
to use the term “minimal” to refer to “an edge of minimum weight”
(among all edges in some specified set) and “maximal” to refer to “an
edge of maximum weight.” That is, if edge weights are distinct, a
minimal edge is the shortest edge (and is the only minimal edge); but
if there is more than one edge of minimum weight, any one of them
might be a minimal edge.
We work exclusively with undirected graphs in this chapter. The
problem of finding a minimum-weight directed spanning tree in a di-
graph is different, and is more difficult.
Several classical algorithms have been developed for the MST
problem. These methods are among the oldest and most well-known
algorithms in this book. As we have seen before, the classical methods
provide a general approach, but modern algorithms and data struc-
tures can give us compact and efficient implementations. Indeed, these
implementations provide a compelling example of the effectiveness
of careful ADT design and proper choice of fundamental ADT data
structure and algorithm implementations in solving increasingly diffi-
cult algorithmic problems.

Exercises
20.1 Assume that the weights in a graph are positive. Prove that you can
rescale them by adding a constant to all of them or by multiplying them all by
a constant without affecting the MSTs, provided only that the rescaled weights
are positive.
222 §20.1 CHAPTER TWENTY

20.2 Show that, if edge weights are positive, a set of edges that connects all
the vertices whose weights sum to a quantity no larger than the sum of the
weights of any other set of edges that connects all the vertices is an MST.
20.3 Show that the property stated in Exercise 20.2 holds for graphs with
negative weights, provided that there are no cycles whose edges all have non-
positive weights.
◦ 20.4 How would you find a maximum spanning tree of a weighted graph?
 20.5 Show that if a graph’s edges all have distinct weights, the MST is unique.
 20.6 Consider the assertion that a graph has a unique MST only if its edge
weights are distinct. Give a proof or a counterexample.
• 20.7 Assume that a graph has t < V edges with equal weights and that all
other weights are distinct. Give upper and lower bounds on the number of
different MSTs that the graph might have.

20.1 Representations
In this chapter, we concentrate on weighted undirected graphs—the
most natural setting for MST problems. Extending the basic graph rep-
resentations from Chapter 17 to represent weighted graphs is straight-
forward: In the adjacency-matrix representation, the matrix can con-
tain edge weights rather than Boolean values; in the adjacency-lists
representation, we add a field to the list elements that represent edges,
for the weights.
In our examples, we generally assume that edge weights are real
numbers between 0 and 1. This decision does not conflict with various
alternatives that we might need in applications, because we can explic-
itly or implicitly rescale the weights to fit this model (see Exercises 20.1
and 20.8). For example, if the weights are positive integers less than
a known maximum value, we can divide them all by the maximum
value to convert them to real numbers between 0 and 1.
We use the same basic graph ADT interface that we used in
Chapter 17 (see Program 17.1), except that we add a weight field to
the edge data type, as follows:
typedef struct { int v; int w; double wt; } Edge;
Edge EDGE(int, int, double);
To avoid proliferation of simple types, we use double for edge weights
throughout this chapter and Chapter 21. If we wanted to do so,
we could build a more general ADT interface and use any data type
MINIMUM SPANNING TREES §20.1 223

0 1 2 3 4 5 6 7
0 * .32 .29 * * .60 .51 .31 0 7 .31 5 .60 2 .29 1 .32 6 .51

1 .32 * * * * * * .21 1 7 .21 0 .32

2 .29 * * * * * * * 2 0 .29

3 * * * * .34 .18 * * 3 5 .18 4 .34

4 * * * .34 * .40 .51 .46 4 6 .51 5 .40 7 .46 3 .34

5 .60 * * .18 .40 * * * 5 0 .60 4 .40 3 .18

6 .51 * * * .51 * * .25 6 7 .25 4 .51 0 .51

7 .31 .21 * * .46 * .25 * 7 1 .21 6 .25 0 .31 4 .46

Figure 20.3
that supports addition, subtraction, and comparisons, since we do Weighted-graph representa-
little more with the weights than to accumulate sums and to make tions (undirected)
decisions based on their values. In Chapter 22, our algorithms are The two standard representations
concerned with comparing linear combinations of edge weights, and of weighted undirected graphs
include weights with each edge
the running time of some algorithms depends on arithmetic properties representation, as illustrated in
of the weights, so we switch to integer weights to allow us to more the adjacency-matrix (left) and
easily analyze the algorithms. adjacency-lists (right) representa-
tion of the graph depicted in Fig-
We use sentinel weights to indicate the absence of an edge. An-
ure 20.1. The adjacency matrix is
other straightforward approach would be to use the standard adja- symmetric and the adjacency lists
cency matrix to indicate the existence of edges and a parallel matrix contain two nodes for each edge,
to hold weights. With sentinels, many of our algorithms do not need as in unweighted directed graphs.
Nonexistent edges are represented
to explicitly test whether or not an edge exists. The adjacency-matrix
by a sentinel value in the matrix
representation of our sample graph is shown in Figure 20.3; Pro- (indicated by asterisks in the figure)
gram 20.1 gives the implementation details of the weighted-graph ADT and are not present at all in the
for an adjacency-matrix representation. It uses an auxiliary function lists. Self-loops are absent in both
of the representations illustrated
that allocates a matrix of weights and fills it with the sentinel weight. here because MST algorithms are
Inserting an edge amounts to storing the weight in two places in the simpler without them; other al-
matrix—one for each orientation of the edge. The sentinel weight value gorithms that process weighted
that indicates the absence of an edge is larger than all other weights, graphs use them (see Chapter 21).
not 0, which represents an edge of length 0 (an alternative would be
to disallow edges of length 0). As is true of algorithms that use the
adjacency-matrix representation for unweighted graphs, the running
time of any algorithm that uses this representation is proportional to
V 2 (to initialize the matrix) or higher.
Similarly, Program 20.2 gives the implementation details of the
weighted-graph ADT for an adjacency-lists representation. A vertex-
indexed array associates each vertex with a linked list of that vertex’s
224 §20.1 CHAPTER TWENTY

Program 20.1 Weighted-graph ADT (adjacency matrix)


For dense weighted undirected graphs, we use a matrix of weights, with
the entries in row v and column w and in row w and column v containing
the weight of the edge v-w. The sentinel value maxWT indicates the
absence of an edge. This code assumes that edge weights are of type
double, and uses an auxiliary routine MATRIXdouble to allocate a V-by-V
array of weights with all entries initialized to maxWT (see Program 17.4).
To adapt this code for use as a weighted digraph ADT implementation
(see Chapter 21), remove the last line of GRAPHinsertE.

#include <stdlib.h>
#include "GRAPH.h"
struct graph { int V; int E; double **adj; };
Graph GRAPHinit(int V)
{ int v;
Graph G = malloc(sizeof *G);
G->adj = MATRIXdouble(V, V, maxWT);
G->V = V; G->E = 0;
return G;
}
void GRAPHinsertE(Graph G, Edge e)
{
if (G->adj[e.v][e.w] == maxWT) G->E++;
G->adj[e.v][e.w] = e.wt;
G->adj[e.w][e.v] = e.wt;
}

incident edges. Each list node represents an edge, and contains a


weight. An adjacency-lists representation of our sample graph is also
shown in Figure 20.3.
As with our undirected-graph representations, we do not explic-
itly test for parallel edges in either representation. Depending upon
the application, we might alter the adjacency-matrix representation to
keep the parallel edge of lowest or highest weight, or to effectively
coalesce parallel edges to a single edge with the sum of their weights.
In the adjacency-lists representation, we can allow parallel edges to
remain in the data structure, or build more powerful data structures
to eliminate them using one of the rules just mentioned for adjacency
matrices (see Exercise 17.47).
MINIMUM SPANNING TREES §20.1 225

Program 20.2 Weighted-graph ADT (adjacency lists)


This adjacency-lists representation is appropriate for sparse undirected
weighted graphs. As with undirected unweighted graphs, we represent
each edge with two list nodes, one on the adjacency list for each of the
edge’s vertices. To represent the weights, we add a weight field to the
list nodes.
This implementation does not check for duplicate edges. Beyond
the factors that we considered for unweighted graphs, a design decision
has to be made about whether or not to allow multiple edges of differing
weights connecting the same pair of nodes, which might be appropriate
in certain applications.
To adapt this code for use as a weighted digraph ADT implemen-
tation, (see Chapter 21), remove the line that adds the edge from w to v
(the next-to-last line of GRAPHinsertE).

#include "GRAPH.h"
typedef struct node *link;
struct node { int v; double wt; link next; };
struct graph { int V; int E; link *adj; };
link NEW(int v, double wt, link next)
{ link x = malloc(sizeof *x);
x->v = v; x->wt = wt; x->next = next;
return x;
}
Graph GRAPHinit(int V)
{ int i;
Graph G = malloc(sizeof *G);
G->adj = malloc(V*sizeof(link));
G->V = V; G->E = 0;
for (i = 0; i < V; i++) G->adj[i] = NULL;
return G;
}
void GRAPHinsertE(Graph G, Edge e)
{ link t;
int v = e.v, w = e.w;
if (v == w) return;
G->adj[v] = NEW(w, e.wt, G->adj[v]);
G->adj[w] = NEW(v, e.wt, G->adj[w]);
G->E++;
}
226 §20.1 CHAPTER TWENTY

0
0 7 .31 2 .29
0-2 .29 2 7
1 7 .21
4-3 .34 1 4 6
2 0 .29
5-3 .18 3
3 5 .18 4 .34
7-4 .46
5
4 7 .46 3 .34
7-0 .31
5 3 .18
7-6 .25 0 1 2 3 4 5 6 7
6 7 .25
7-1 .21 st 0 7 0 4 7 3 7 0
7 1 .21 6 .25 0 .31 4 .46
val 0 .21 .29 .34 .46 .18 .25 .31

Figure 20.4
MST representations How should we represent the MST itself? The MST of a graph
This figure depicts various rep-
G is a subgraph of G that is also a tree, so we have numerous options.
resentations of the MST in Fig- Chief among them are
ure 20.1. The most straightforward • A graph
is a list of its edges, in no particu-
• A linked list of edges
lar order (left). The MST is also a
sparse graph, and might be repre- • An array of edges
sented with adjacency lists (center). • A vertex-indexed array with parent links
The most compact is a parent-link Figure 20.4 illustrates these options for the example MST in Fig-
representation: we choose one of
the vertices as the root and keep ure 20.1. Another alternative is to define and use an ADT for trees.
two vertex-indexed arrays, one The same tree might have many different representations in any
with the parent of each vertex in of the schemes. In what order should the edges be presented in the
the tree, the other with the weight
list-of-edges representation? Which node should be chosen as the
of the edge from each vertex to
its parent (right). The orientation root in the parent-link representation (see Exercise 20.15)? Generally
of the tree (choice of root vertex) speaking, when we run an MST algorithm, the particular MST repre-
is arbitrary, not a property of the sentation that results is an artifact of the algorithm used, rather than
MST. We can convert from any
reflecting any important features of the MST.
one of these representations to any
other in linear time. From an algorithmic point of view, the choice of MST repre-
sentation is of little consequence, because we can convert easily from
each of these representations to any of the others. To convert from the
graph representation to an array of edges, we can use the GRAPHedges
function in the graph ADT. To convert from the parent-link represen-
tation in an array st to an array of edges in an array mst, we can use
the simple loop
for (k = 1; k < G->V; k++) mst[k] = EDGE(k, st[k]);
This code is for the typical case where the MST is rooted at 0, and it
does not put the dummy edge 0-0 onto the MST edge list.
These two conversions are trivial, but how do we convert from
the array-of-edges representation to the parent-link representation?
MINIMUM SPANNING TREES §20.1 227

We have the basic tools to accomplish this task easily as well: We


convert to the graph representation using a loop like the one just given
(changed to call GRAPHinsert for each edge), then run a a DFS starting
at any vertex to compute the parent-link representation of the DFS tree,
in linear time.
In short, the choice of MST representation is a matter of con-
venience for our algorithms. Dependent on the needs of applications,
we can add wrapper functions to give client programs the flexibility
that they might need. The parent-link representation is more natu-
ral for some of the algorithms that we consider; other representations
are more natural for other algorithms. Our goal in this chapter is
to develop efficient implementations that can support a graph ADT
function GRAPHmst; we do not specify details of the interface, so we
leave flexibility for simple wrapper functions that can meet the needs
of clients while allowing implementations that produce MSTs in a
manner natural for each algorithm (see, for example, Exercises 20.18
through 20.20).

Exercises
 20.8 Build a graph ADT that uses integer weights, but keep track of the
minimum and maximum weights in the graph and include an ADT function
that always returns weights that are numbers between 0 and 1.

 20.9 Modify the sparse-random-graph generator in Program 17.7 to assign


a random weight (between 0 and 1) to each edge.

 20.10 Modify the dense-random-graph generator in Program 17.8 to assign


a random weight (between 0 and 1) to each edge.

20.11 Write a program√that generates


√ random weighted graphs by connecting
vertices arranged in a V -by- V grid to their neighbors (as in Figure 19.3,
but undirected) with random weights (between 0 and 1) assigned to each edge.

20.12 Write a program that generates a random complete graph that has
weights chosen from a Gaussian distribution.

• 20.13 Write a program that generates V random points in the plane, then
builds a weighted graph by connecting each pair of points within a given
distance d of one another with an edge whose weight is the distance. (see
Exercise 17.60). Determine how to set d so that the expected number of
edges is E.

• 20.14 Find a large weighted graph online—perhaps a map with distances,


telephone connections with costs, or an airline rate schedule.
228 §20.2 CHAPTER TWENTY

20.15 Write down an 8-by-8 matrix that contains parent-link representations


of all the orientations of the MST of the graph in Figure 20.1. Put the parent-
link representation of the tree rooted at i in the ith row of the matrix.
 20.16 Add GRAPHscan, GRAPHshow, and GRAPHedges functions to the adjacency-
matrix and adjacency-lists implementations in Programs 20.1 and 20.2 (see
Programs 17.5 and 17.6).
 20.17 Provide an implementation for the MATRIXdouble function that is used
in Program 20.1 (see Program 17.4).
 20.18 Assume that a function GRAPHmstE produces an array-of-edges repre-
sentation of an MST in an array mst. Add a wrapper ADT function GRAPHmst
to the graph ADT that calls GRAPHmstE but puts a parent-link representation
of the MST into an array that is passed as an argument by the client.
◦ 20.19 Assume that a function GRAPHmstV produces a parent-link representa-
tion of an MST in an array st with corresponding weights in another array
wt. Add a wrapper ADT function GRAPHmst to the graph ADT that calls
GRAPHmstV but puts an array-of-edges representation of the MST into an ar-
ray that is passed as an argument by the client.
 20.20 Define a Tree ADT. Then, under the assumptions of Exercise 20.19,
add a wrapper ADT function to the graph ADT that calls GRAPHmst but returns
the MST in a Tree.

20.2 Underlying Principles of MST Algorithms


The MST problem is one of the most heavily studied problems that we
encounter in this book. Basic approaches to solving it were studied
long before the development of modern data structures and modern
techniques for studying the performance of algorithms, at a time when
finding the MST of a graph that contained, say, thousands of edges was
a daunting task. As we shall see, several new MST algorithms differ
from old ones essentially in their use and implementation of modern
algorithms and data structures for basic tasks, which (coupled with
modern computing power) makes it possible for us to compute MSTs
with millions or even billions of edges.
One of the defining properties of a tree (see Section 5.4) is that
adding an edge to a tree creates a unique cycle. This property is the
basis for proving two fundamental properties of MSTs, which we now
consider. All the algorithms that we encounter are based on one or
both of these two properties.
The first property, which we refer to as the cut property, has to
do with identifying edges that must be in an MST of a given graph. The
MINIMUM SPANNING TREES §20.2 229

few basic terms from graph theory that we define next make possible
2
a concise statement of this property, which follows. 0
6
Definition 20.2 A cut in a graph is a partition of the vertices into 7

two disjoint sets. A crossing edge is one that connects a vertex in one 1
set with a vertex in the other. 3

We sometimes specify a cut by specifying a set of vertices, leav- 5 4


ing implicit the assumption that the cut comprises that set and its
complement. Generally, we use cuts where both sets are nonempty— 2
0
otherwise, for example, there are no crossing edges.
6
7
Property 20.1 (Cut property) Given any cut in a graph, every min-
imal crossing edge belongs to some MST of the graph, and every MST 1

contains a minimal crossing edge. 3

Proof : The proof is by contradiction. Suppose that e is a minimal 5 4

crossing edge that is not in any MST and let T be any MST; or suppose
that T is an MST that contains no minimal crossing edge and let e be 2
0
any minimal crossing edge. In either case, T is an MST that does not 6
contain the minimal crossing edge e. Now consider the graph formed 7

by adding e to T . This graph has a cycle that contains e, and that cycle 1
must contain at least one other crossing edge—say, f , which is equal 3
or higher weight than e (since e is minimal). We can get a spanning
5 4
tree of equal or lower weight by deleting f and adding e, contradicting
either the minimality of T or the assumption that e is not in T .
2
0
If a graph’s edge weights are distinct, it has a unique MST; and
6
the cut property says that the shortest crossing edge for every cut must 7
be in the MST. When equal weights are present, we may have multiple 1
minimal crossing edges. At least one of them will be in any given MST
3
and the others may be present or absent.
5 4
Figure 20.5 illustrates several examples of this cut property. Note
that there is no requirement that the minimal edge be the only MST
edge connecting the two sets; indeed, for typical cuts there are several Figure 20.5
MST edges that connect a vertex in one set with a vertex in the other. If Cut property
we could be sure that there were only one such edge, we might be able These four examples illustrate
to develop divide-and-conquer algorithms based on judicious selection Property 20.1. If we color one set
of vertices gray and another set
of the sets; but that is not the case. white, then the shortest edge con-
We use the cut property as the basis for algorithms to find MSTs, necting a gray vertex with a white
and it also can serve as an optimality condition that characterizes one belongs to an MST.
230 §20.2 CHAPTER TWENTY

2 MSTs. Specifically, the cut property implies that every edge in an MST
0
is a minimal crossing edge for the cut defined by the vertices in the two
6
7 subtrees connected by the edge.
1 The second property, which we refer to as the cycle property, has
to do with identifying edges that do not have to be in a graph’s MST.
3
That is, if we ignore these edges, we can still find an MST.
5 4
Property 20.2 (Cycle property) Given a graph G, consider the graph
G defined by adding an edge e to G. Adding e to an MST of G and
2
0 deleting a maximal edge on the resulting cycle gives an MST of G .
6
7
Proof : If e is longer than all the other edges on the cycle, it cannot
1 be on an MST of G , because of Property 20.1: Removing e from
3 any such MST would split the latter into two pieces, and e would not
be the shortest edge connecting vertices in each of those two pieces,
5 4
because some other edge on the cycle must do so. Otherwise, let t be
a maximal edge on the cycle created by adding e to the MST of G.
2
0 Removing t would split the original MST into two pieces, and edges of
6 G connecting those pieces are no shorter than t; so e is a minimal edge
7
in G connecting vertices in those two pieces. The subgraphs induced
1
by the two subsets of vertices are identical for G and G , so an MST
3 for G consists of e and the MSTs of those two subsets.
5 4 In particular, note that if e is maximal on the cycle, then we have
shown that there exists an MST of G that does not contain e (the MST
of G).
Figure 20.6
Cycle property Figure 20.6 illustrates this cycle property. Note that the process
Adding the edge 1-3 to the graph of taking any spanning tree, adding an edge that creates a cycle, and
in Figure 20.1 invalidates the MST
then deleting a maximal edge on that cycle gives a spanning tree of
(top). To find the MST of the new
graph, we add the new edge to weight less than or equal to the original. The new tree weight will be
the MST of the old graph, which less than the original if and only if the added edge is shorter than some
creates a cycle (center). Deleting edge on the cycle.
the longest edge on the cycle (4-7) The cycle property also serves as the basis for an optimality
yields the MST of the new graph
(bottom). One way to verify that condition that characterizes MSTs: It implies that every edge in a
a spanning tree is minimal is to graph that is not in a given MST is a maximal edge on the cycle that it
check that each edge not on the forms with MST edges.
MST has the largest weight on the The cut property and the cycle property are the basis for the
cycle that it forms with tree edges.
For example, in the bottom graph, classical algorithms that we consider for the MST problem. We con-
4-6 has the largest weight on the sider edges one at a time, using the cut property to accept them as
cycle 4-6-7-1-3-4. MST edges or the cycle property to reject them as not needed. The
MINIMUM SPANNING TREES §20.2 231

algorithms differ in their approaches to efficiently identifying cuts and


cycles.
The first approach to finding the MST that we consider in detail
is to build the MST one edge at a time: start with any vertex as a
single-vertex MST, then add V − 1 edges to it, always taking next a
minimal edge that connects a vertex on the MST to a vertex not yet on
the MST. This method is known as Prim’s algorithm; it is the subject
of Section 20.3.
Property 20.3 Prim’s algorithm computes an MST of any connected
graph.
Proof : As described in detail in Section 20.2, the method is a general-
ized graph-search method. Implicit in the proof of Property 18.12 is
the fact that the edges chosen are a spanning tree. To show that they
are an MST, apply the cut property, using vertices on the MST as the
first set, vertices not on the MST as the second set.
Another approach to computing the MST is to apply the cy-
cle property repeatedly: We add edges one at a time to a putative
MST, deleting a maximal edge on the cycle if one is formed (see Exer-
cises 20.28 and 20.66). This method has received less attention than
the others that we consider because of the comparative difficulty of
maintaining a data structure that supports efficient implementation of
the “delete the longest edge on the cycle” operation.
The second approach to finding the MST that we consider in de-
tail is to process the edges in order of their length (shortest first), adding
to the MST each edge that does not form a cycle with edges previously
added, stopping after V − 1 edges have been added. This method is
known as Kruskal’s algorithm; it is the subject of Section 20.4.
Property 20.4 Kruskal’s algorithm computes an MST of any con-
nected graph.
Proof : We prove by induction that the method maintains a forest of
MST subtrees. If the next edge to be considered would create a cycle, it
is a maximal edge on the cycle (since all others appeared before it in the
sorted order), so ignoring it still leaves an MST, by the cycle property.
If the next edge to be considered does not form a cycle, apply the cut
property, using the cut defined by the set of vertices connected to one
of the edge’s vertex by MST edges (and its complement). Since the
232 §20.2 CHAPTER TWENTY

edge does not create a cycle, it is the only crossing edge, and since we
consider the edges in sorted order, it is a minimal edge and therefore
in an MST. The basis for the induction is the V individual vertices;
once we have chosen V − 1 edges, we have one tree (the MST). No
unexamined edge is shorter than an MST edge, and all would create
a cycle, so ignoring all of the rest of the edges leaves an MST, by the
0 1 cycle property.

The third approach to building an MST that we consider in detail


is known as Boruvka’s algorithm; it is the subject of Section 20.4. The
3 2
first step is to add to the MST the edges that connect each vertex to
its closest neighbor. If edge weights are distinct, this step creates a
0 1 forest of MST subtrees (we prove this fact and consider a refinement
that does so even when equal-weight edges are present in a moment).
Then, we add to the MST the edges that connect each tree to a closest
neighbor (a minimal edge connecting a vertex in one tree with a vertex
3 2
in any other), and iterate the process until we are left with just one
tree.
Figure 20.7
Cycles in Boruvka’s algorithm Property 20.5 Boruvka’s algorithm computes the MST of any con-
In the graph of four vertices and nected graph.
four edges shown here, the edges
are all the same length. When we First, suppose that the edge weights are distinct. In this case, each
connect each vertex to a near- vertex has a unique closest neighbor, the MST is unique, and we know
est neighbor, we have to make a that each edge added is an MST edge by applying the cut property
choice between minimal edges. In
the example at the top, we choose (it is the shortest edge crossing the cut from a vertex to all the other
1 from 0, 2 from 1, 3 from 2, and vertices). Since every edge chosen is from the unique MST, there can
0 from 3, which leads to a cycle in be no cycles, each edge added merges two trees from the forest into
the putative MST. Each of the edges a bigger tree, and the process continues until a single tree, the MST,
are in some MST, but not all are
in every MST. To avoid this prob- remains.
lem, we adopt a tie-breaking rule, If edge weights are not distinct, there may be more than one
as shown in the bottom: choose closest neighbor, and a cycle could form when we add the edges to
the minimal edge to the vertex
closest neighbors (see Figure 20.7). Put another way, we might include
with the lowest index. Thus, we
choose 1 from 0, 0 from 1, 1 from two edges from the set of minimal crossing edges for some vertex,
2, and 0 from 3, which yields an when only one belongs on the MST. To avoid this problem, we need
MST. The cycle is broken because an appropriate tie-breaking rule. One choice is to choose, among
highest-numbered vertex 3 is not
the minimal neighbors, the one with the lowest vertex number. Then
chosen from either of its neighbors
2 or 1, and it can choose only one any cycle would present a contradiction: if v is the highest-numbered
of them (0). vertex in the cycle, then neither neighbor of v would have led to its
MINIMUM SPANNING TREES §20.2 233

choice as the closest, and v would have led to the choice only one of
its lower-numbered neighbors, not both.

These algorithms are all special cases of a general paradigm that


is still being used by researchers seeking new MST algorithms. Specif-
ically, we can apply in arbitrary order the cut property to accept an
edge as an MST edge or the cycle property to reject an edge, continu-
ing until neither can increase the number of accepted or rejected edges.
At that point, any division of the graph’s vertices into two sets has
an MST edge connecting them (so applying the cut property cannot
increase the number of MST edges), and all graph cycles have at least
one non-MST edge (so applying the cycle property cannot increase the
number of non-MST edges). Together, these properties imply that a
complete MST has been computed.
More specifically, the three algorithms that we consider in detail
can be unified with a generalized algorithm where we begin with a
forest of single-vertex MST subtrees (each with no edges) and perform
the step of adding to the MST a minimal edge connecting any two
subtrees in the forest, continuing until V −1 edges have been added and
a single MST remains. By the cut property, no edge that causes a cycle
need be considered for the MST, since some other edge was previously
a minimal edge crossing a cut between MST subtrees containing each
of its vertices. With Prim’s algorithm, we grow a single tree an edge at
a time; with Kruskal’s and Boruvka’s algorithms, we coalesce trees in
a forest.
As described in this section and in the classical literature, the
algorithms involve certain high-level abstract operations, such as the
following:
• Find a minimal edge connecting two subtrees.
• Determine whether adding an edge would create a cycle.
• Delete the longest edge on a cycle.
Our challenge is to develop algorithms and data structures that imple-
ment these operations efficiently. Fortunately, this challenge presents
us with an opportunity to put to good use basic algorithms and data
structures that we developed earlier in this book.
MST algorithms have a long and colorful history that is still
evolving; we discuss that history as we consider them in detail. Our
evolving understanding of different methods of implementing the basic
234 §20.2 CHAPTER TWENTY

abstract operations has created some confusion surrounding the ori-


gins of the algorithms over the years. Indeed, the methods were first
described in the 1920s, pre-dating the development of computers as we
know them, as well as pre-dating our basic knowledge about sorting
and other algorithms. As we now know, the choices of underlying
algorithm and data structure can have substantial influences on per-
formance, even when we are implementing the most basic schemes. In
recent years, research on the MST problem has concentrated on such
implementation issues, still using the classical schemes. For consis-
tency and clarity, we refer to the basic approaches by the names listed
here, although abstract versions of the algorithms were considered ear-
lier, and modern implementations use algorithms and data structures
invented long after these methods were first contemplated.
As yet unsolved in the design and analysis of algorithms is the
quest for a linear-time MST algorithm. As we shall see, many of our
implementations are linear-time in a broad variety of practical situa-
tions, but they are subject to a nonlinear worst case. The development
of an algorithm that is guaranteed to be linear-time for sparse graphs
is still a research goal.
Beyond our normal quest in search of the best algorithm for
this fundamental problem, the study of MST algorithms underscores
the importance of understanding the basic performance characteristics
of fundamental algorithms. As programmers continue to use algo-
rithms and data structures at increasingly higher levels of abstraction,
situations of this sort become increasingly common. Our ADT im-
plementations have varying performance characteristics—as we use
higher-level ADTs as components when solving more yet higher-level
problems, the possibilities multiply. Indeed, we often use algorithms
that are based on using MSTs and similar abstractions (enabled by the
efficient implementations that we consider in this chapter) to help us
solve other problems at a yet higher level of abstraction.
Exercises
 20.21 Label the following points in the plane 0 through 5, respectively:
(1, 3) (2, 1) (6, 5) (3, 4) (3, 7) (5, 3).
Taking edge lengths to be weights, give an MST of the graph defined by the
edges
1-0 3-5 5-2 3-4 5-1 0-3 0-4 4-2 2-3.
MINIMUM SPANNING TREES §20.3 235

20.22 Suppose that a graph has distinct edge weights. Does its shortest edge
have to belong to the MST? Prove that it does or give a counterexample.
20.23 Answer Exercise 20.22 for the graph’s longest edge.
20.24 Give a counterexample that shows why the following strategy does not
necessarily find the MST: “Start with any vertex as a single-vertex MST, then
add V − 1 edges to it, always taking next a minimal edge incident upon the
vertex most recently added to the MST.”
20.25 Suppose that a graph has distinct edge weights. Does a minimal edge
on every cycle have to belong to the MST? Prove that it does or give a coun-
terexample.
20.26 Given an MST for a graph G, suppose that an edge in G is deleted.
Describe how to find an MST of the new graph in time proportional to the
number of edges in G.
20.27 Show the MST that results when you repeatedly apply the cycle property
to the graph in Figure 20.1, taking the edges in the order given.
20.28 Prove that repeated application of the cycle property gives an MST.
20.29 Describe how each of the algorithms described in this section can be
adapted (if necessary) to the problem of finding a minimal spanning forest of
a weighted graph (the union of the MSTs of its connected components).

20.3 Prim’s Algorithm and Priority-First Search


Prim’s algorithm is perhaps the simplest MST algorithm to implement,
and it is the method of choice for dense graphs. We maintain a cut of
the graph that is comprised of tree vertices (those chosen for the MST)
and nontree vertices (those not yet chosen for the MST). We start by
putting any vertex on the MST, then put a minimal crossing edge on
the MST (which changes its nontree vertex to a tree vertex) and repeat
the same operation V − 1 times, to put all vertices on the tree.
A brute-force implementation of Prim’s algorithm follows di-
rectly from this description. To find the edge to add next to the MST,
we could examine all the edges that go from a tree vertex to a nontree
vertex, then pick the shortest of the edges found to put on the MST. We
do not consider this implementation here because it is overly expensive
(see Exercises 20.30 through 20.32). Adding a simple data structure to
eliminate excessive recomputation makes the algorithm both simpler
and faster.
Adding a vertex to the MST is an incremental change: To im-
plement Prim’s algorithm, we focus on the nature of that incremental
236 §20.3 CHAPTER TWENTY

2 2
Figure 20.8 0
0
0 0
Prim’s MST algorithm 6 6
2 7
7 7
The first step in computing the 1 6
1 1
MST with Prim’s algorithm is to
add 0 to the tree. Then we find all 3 3
the edges that connect 0 to other
5 4 5 4
vertices (which are not yet on the
tree) and keep track of the shortest 0-2 0-7 0-1 0-6 0-5 7-4 0-5
(top left). The edges that connect
tree vertices with nontree vertices
(the fringe) are shadowed in gray
and listed below each graph draw- 2 2
0 0 0
ing. For simplicity in this figure, 0
6 6 2 7
we list the fringe edges in order 7 2 7
of their length, so that the short- 1 6 4
1 1
est is the first in the list. Different
implementations of Prim’s algo- 3 3
rithm use different data structures 5 4 5 4
to maintain this list and to find the
minimum. The second step is to 0-7 0-1 0-6 0-5 4-3 4-5
move the shortest edge 0-2 (along
with the vertex that it takes us to)
from the fringe to the tree (second
from top, left). Third, we move 2 2 0
0 0 0
0-7 from the fringe to the tree, re-
6 6 2 7
place 0-1 by 7-1 and 0-6 by 7-6 7 2 7 7
on the fringe (because adding 7 1 6 4
1 1
to the tree brings 1 and 6 closer 3
to the tree), and add 7-4 to the 3 3
fringe (because adding 7 to the tree 5 4 5 4
makes 7-4 an edge that connects
a tree vertex with a nontree ver- 7-1 7-6 7-4 0-5 3-5
tex) (third from top, left). Next, we
move edge 7-1 to the tree (bottom,
left). To complete the computation,
we take 7-6, 7-4, 4-3, and 3-5 2 2 0
0 0 0
off the queue, updating the fringe
6 6 2 7
after each insertion to reflect any 7 2 7 7
shorter or new paths discovered 1
1 6 4
1 1
(right, top to bottom). 3
An oriented drawing of the 3 3
5
growing MST is shown at the right 5 4 5 4
of each graph drawing. The ori-
entation is an artifact of the algo- 7-6 7-4 0-5
0 1 2 3 4 5 6 7
rithm: we generally view the MST st 0 7 0 4 7 3 7 0
itself as a set of edges, unordered wt 0 .21 .29 .34 .46 .18 .25 .31
and unoriented.
MINIMUM SPANNING TREES §20.3 237

change. The key is to note that our interest is in the shortest distance
from each nontree vertex to the tree. When we add a vertex v to the
tree, the only possible change for each nontree vertex w is that adding
v brings w closer than before to the tree. In short, we do not need to
check the distance from w to all tree vertices—we just need to keep
track of the minimum and check whether the addition of v to the tree
necessitates that we update that minimum.
To implement this idea, we need data structures that can give us
the following information:
• For each tree vertex, its parent in the MST
• For each nontree vertex, the closest tree vertex
• For each tree vertex, the length of its parent link
• For each nontree vertex, its distance to the tree
The simplest implementation for each of these data structures is a
vertex-indexed array, though we have various options to save space by
combining arrays or using structures.
Program 20.3 is an implementation of Prim’s algorithm for an
adjacency-matrix graph ADT implementation. It uses the arrays st,
fr, and wt for these four data structures, with st and fr for the first
two (respectively), and wt for both the third and fourth. For a tree
vertex v, the entry wt[v] is the length of the parent link (corresponding
to st[v]); for a nontree vertex w, the entry wt[w] is the distance to the
tree (corresponding to fr[w]). The implementation is packaged as a
function GRAPHmstV that takes st and wt as client-supplied arrays. If
desired, we could add a wrapper function GRAPHmst to build an edge
list (or some other) MST representation, as discussed in Section 20.1.
After adding a new edge (and vertex) to the tree, we have two
tasks to accomplish:
• Check to see whether adding the new edge brought any nontree
vertex closer to the tree.
• Find the next edge to add to the tree.
The implementation in Program 20.3 accomplishes both of these tasks
with a single scan through the nontree vertices, updating wt[w] and
fr[w] if v-w brings w closer to the tree, then updating the current
minimum if wt[w] (the distance from w to fr[w] indicates that w is
closer to the tree than any other vertex with a lower index.
Property 20.6 Using Prim’s algorithm, we can find the MST of a
dense graph in linear time.
238 §20.3 CHAPTER TWENTY

Program 20.3 Prim’s MST algorithm


This implementation of Prim’s algorithm is the method of choice for
dense graphs. The outer loop grows the MST by choosing a minimal
edge crossing the cut between the vertices on the MST and vertices not
on the MST. The w loop finds the minimal edge while at the same time
(if w is not on the MST) maintaining the invariant that the edge from w
to fr[w] is the shortest edge (of weight wt[w]) from w to the MST.

static int fr[maxV];


#define P G->adj[v][w]
void GRAPHmstV(Graph G, int st[], double wt[])
{ int v, w, min;
for (v = 0; v < G->V; v++)
{ st[v] = -1; fr[v] = v; wt[v] = maxWT; }
st[0] = 0; wt[G->V] = maxWT;
for (min = 0; min != G->V; )
{
v = min; st[min] = fr[min];
for (w = 0, min = G->V; w < G->V; w++)
if (st[w] == -1)
{
if (P < wt[w])
{ wt[w] = P; fr[w] = v; }
if (wt[w] < wt[min]) min = w;
}
}
}

Proof : It is immediately evident from inspection of the program that


the running time is proportional to V 2 and therefore is linear for dense
graphs.
Figure 20.8 shows an example MST construction with Prim’s
algorithm; Figure 20.9 shows the evolving MST for a larger example.
Program 20.3 is based upon the observation that we can inter-
leave the find the minimum and update operations in a single loop
where we examine all the nontree edges. In a dense graph, the number
of edges that we may have to examine to update the distance from the
nontree vertices to the tree is proportional to V , so looking at all the
MINIMUM SPANNING TREES §20.3 239

nontree edges to find the one that is closest to the tree does not repre-
sent excessive extra cost. But in a sparse graph, we can expect to use
substantially fewer than V steps to perform each of these operations.
The crux of the strategy that we will use to do so is to focus on the set
of potential edges to be added next to the MST—a set that we call the
fringe. The number of fringe edges is typically substantially smaller
than the number of nontree edges, and we can recast our description
of the algorithm as follows. Starting with a self loop to a start vertex
on the fringe and an empty tree, we perform the following operation
until the fringe is empty:
Move a minimal edge from the fringe to the tree. Visit the
vertex that it leads to, and put onto the fringe any edges
that lead from that vertex to an nontree vertex, replacing
the longer edge when two edges on the fringe point to the
same vertex.
From this formulation, it is clear that Prim’s algorithm is nothing
more than a generalized graph search (see Section 18.8), where the
fringe is a priority queue based on a delete the minimum operation
(see Chapter 9). We refer to generalized graph searching with priority
queues as priority-first search (PFS). With edge weights for priorities,
PFS implements Prim’s algorithm.
This formulation encompasses a key observation that we made
already in connection with implementing BFS in Section 18.7. An
even simpler general approach is to simply keep on the fringe all of
the edges that are incident upon tree vertices, letting the priority-
queue mechanism find the shortest one and ignore longer ones (see
Exercise 20.37). As we saw with BFS, this approach is unattractive
because the fringe data structure becomes unnecessarily cluttered with
edges that will never get to the MST. The size of the fringe could grow
to be proportional to E (with whatever attendant costs having a fringe
this size might involve), while the PFS approach just outlined ensures
that the fringe will never have more than V entries.
As with any implementation of a general algorithm, we have
a number of available approaches for interfacing with priority-queue
ADTs. One approach is to use a priority queue of edges, as in our gener-
alized graph-search implementation of Program 18.10. Program 20.4
is an implementation that is essentially equivalent to Program 18.10
but uses a vertex-based approach so that it can use the index priority-
240 §20.3 CHAPTER TWENTY

Figure 20.9
Prim’s MST algorithm
This sequence shows how the MST
grows as Prim’s algorithm discovers
1/4, 1/2, 3/4, and all of the edges
in the MST (top to bottom). An
oriented representation of the full
MST is shown at the right.
MINIMUM SPANNING TREES §20.3 241

queue interface of Section 9.6. We identify the fringe vertices, the


subset of nontree vertices that are connected by fringe edges to tree
vertices, and keep the same vertex-indexed arrays st, fr, and wt as in
Program 20.3. The priority queue contains the index of each fringe
vertex, and that entry gives access to the fringe vertex’s closest tree
vertex and distance to the tree, through the second and third arrays.
Since we seek an MST, Program 20.4 assumes the graph to be
connected. It acutally finds the MST in the connected component that
contains vertex 0. To augment it to find minimal spanning forests in
graphs that are not connected, we can add a loop as in Program 18.1
(see Exercise 20.29).
Property 20.7 Using a PFS implementation of Prim’s algorithm that
uses a heap for the priority-queue implementation, we can compute an
MST in time proportional to E lg V .
Proof : The algorithm directly implements the generic idea of Prim’s
algorithm (add next to the MST a minimal edge that connects a ver-
tex on the MST with a vertex not on the MST). Each priority-queue
operation requires less than lg V steps. Each vertex is chosen with a
delete the minimum operation; and, in the worst case, each edge might
require a change priority operation.
Priority-first search is a proper generalization of breadth-first
and depth-first search, because those methods also can be derived
through appropriate priority settings. For example, we can (somewhat
artificially) use a variable cnt to assign a unique priority cnt++ to each
vertex when we put that vertex on the priority queue. If we define
P to be cnt, we get preorder numbering and DFS, because newly
encountered nodes have the highest priority. If we define P to be V-cnt,
we get BFS, because old nodes have the highest priority. These priority
assignments make the priority queue operate like a stack and a queue,
respectively. This equivalence is purely of academic interest, since the
priority-queue operations are unnecessary for DFS and BFS. Also, as
discussed in Section 18.8, a formal proof of equivalence would require
a precise attention to replacement rules, to obtain the same sequence
of vertices as result from the classical algorithms.
As we shall see, PFS encompasses not just DFS, BFS, and Prim’s
MST algorithm, but also several other classical algorithms. The var-
ious algorithms differ only in their priority functions. Thus, the run-
242 §20.3 CHAPTER TWENTY

Program 20.4 Priority-first search (adjacency lists)


This program is a generalized graph search that uses a priority queue
to manage the fringe (see Section 18.8). The priority P is defined such
that the ADT function GRAPHpfs implements Prim’s MST algorithm
for sparse (connected) graphs. Other priority definitions implement
different graph-processing algorithms.
The program moves the highest priority (lowest weight) edge from
the fringe to the tree, then checks every edge adjacent to the new tree
vertex to see whether it implies changes in the fringe. Edges to vertices
not on the fringe or the tree are added to the fringe; shorter edges to
fringe vertices replace corresponding fringe edges.
We use the priority-queue ADT interface from Section 9.6, mod-
ified to substitute PQdelmin for PQdelmax and PQdec for PQchange (to
emphasize that we change priorities only by decreasing them). The static
variable priority and the function less allow the priority-queue func-
tions to use vertex names as handles and to compare priorities that are
maintained by this code in the wt array.

#define GRAPHpfs GRAPHmst


static int fr[maxV];
static double *priority;
int less(int i, int j)
{ return priority[i] < priority[j]; }
#define P t->wt
void GRAPHpfs(Graph G, int st[], double wt[])
{ link t; int v, w;
PQinit(); priority = wt;
for (v = 0; v < G->V; v++)
{ st[v] = -1; fr[v] = -1; }
fr[0] = 0; PQinsert(0);
while (!PQempty())
{
v = PQdelmin(); st[v] = fr[v];
for (t = G->adj[v]; t != NULL; t = t->next)
if (fr[w = t->v] == -1)
{ wt[w] = P; PQinsert(w); fr[w] = v; }
else if ((st[w] == -1) && (P < wt[w]))
{ wt[w] = P; PQdec(w); fr[w] = v; }
}
}
MINIMUM SPANNING TREES §20.3 243

ning times of all these algorithms depend on the performance of the


priority-queue ADT. Indeed, we are led to a general result that encom-
passes not just the two implementations of Prim’s algorithms that we
have examined in this section, but also a broad class of fundamental
graph-processing algorithms.
Property 20.8 For all graphs and all priority functions, we can com-
pute a spanning tree with PFS in linear time plus time proportional to
the time required for V insert, V delete the minimum, and E decrease
key operations in a priority queue of size at most V .
Proof : The proof of Property 20.7 establishes this more general result.
We have to examine all the edges in the graph; hence the “linear
time” clause. The algorithm never increases the priority (it changes
the priority to only a lower value); by more precisely specifying what
we need from the priority-queue ADT (decrease key, not necessarily
change priority), we strengthen this statement about performance.
In particular, use of an unordered-array priority-queue imple-
mentation gives an optimal solution for dense graphs that has the same
worst-case performance as the classical implementation of Prim’s al-
gorithm (Program 20.3). That is, Properties 20.6 and 20.7 are special
cases of Property 20.8; throughout this book we shall see numerous
other algorithms that essentially differ in only their choice of priority
function and their priority-queue implementation.
Property 20.7 is an important general result: The time bound
stated is a worst-case upper bound that guarantees performance within
a factor of lg V of optimal (linear time) for a large class of graph-
processing problems. Still, it is somewhat pessimistic for many of the
graphs that we encounter in practice, for two reasons. First, the lg V
bound for priority-queue operation holds only when the number of
vertices on the fringe is proportional to V , and even then is just an
upper bound. For a real graph in a practical application, the fringe
might be small (see Figures 20.10 and 20.11), and some priority-
queue operations might take many fewer than lg V steps. Although
Figure 20.10
noticeable, this effect is likely to account for only a small constant
PFS implementation of Prim’s
factor in the running
√ time; for example, a proof that the fringe never MST algorithm
has more than V vertices on it would improve the bound by only a With PFS, Prim’s algorithm pro-
factor of 2. More important, we generally perform many fewer than E cesses just the vertices and edges
decrease key operations, since we do that operation only when we find closest to the MST (in gray).
244 §20.3 CHAPTER TWENTY

an edge to a fringe node that is shorter than the current best-known


edge to a fringe node. This event is relatively rare: Most edges have
no effect on the priority queue (see Exercise 20.35). It is reasonable
to regard PFS as an essentially linear-time algorithm unless V lg V is
significantly greater than E.
The priority-queue ADT and generalized graph-searching ab-
stractions make it easy for us to understand the relationships among
various algorithms. Since these abstractions (and software mecha-
nisms to support their use) were developed many years after the basic
methods, relating the algorithms to classical descriptions of them be-
comes an exercise for historians. However, knowing basic facts about
the history is useful when we encounter descriptions of MST algo-
rithms in the research literature or in other texts, and understanding
how these few simple abstractions tie together the work of numerous
researchers over a time span of decades is persuasive evidence of their
value and power; so we consider briefly the origins of these algorithms
here.
An MST implementation for dense graphs essentially equivalent
to Program 20.3 was first presented by Prim in 1961, and, indepen-
dently, by Dijkstra soon thereafter. It is usually referred to as Prim’s
algorithm, although Dijkstra’s presentation was more general, so some
scholars refer to the MST algorithm as a special case of Dijkstra’s al-
gorithm. But the basic idea was also presented by Jarnik in 1939, so
some authors refer to the method as Jarnik’s algorithm, thus character-
izing Prim’s (or Dijkstra’s) role as finding an efficient implementation
of the algorithm for dense graphs. As the priority-queue ADT came
into use in the early 1970s, its application to finding MSTs of sparse
graphs was straightforward; the fact that MSTs of sparse graphs could
be computed in time proportional to E lg V became widely known
without attribution to any particular researcher. Since that time, as we
Figure 20.11 discuss in Section 20.6, many researchers have concentrated on finding
Fringe size for PFS implemen-
tation of Prim’s algorithm efficient priority-queue implementations as the key to finding efficient
The plot at the bottom shows the
MST algorithms for sparse graphs.
size of the fringe as the PFS pro- Exercises
ceeds for the example in Fig-
ure 20.10. For comparison, the  20.30 Analyze the performance of the brute-force implementation of Prim’s
corresponding plots for DFS, ran- algorithm mentioned at the beginning of this section for a complete weighted
domized search, and BFS from Fig- graph with
 V vertices. Hint: The following combinatorial sum might be
ure 18.28 are shown above in gray. useful: 1≤k<V
k(V − k) = (V + 1)V (V − 1)/6.
MINIMUM SPANNING TREES §20.3 245

◦ 20.31 Answer Exercise 20.30 for graphs in which all vertices have the same
fixed degree t.
◦ 20.32 Answer Exercise 20.30 for general sparse graphs that have V vertices
and E edges. Since the running time depends on the weights of the edges and
on the degrees of the vertices, do a worst-case analysis. Exhibit a family of
graphs for which your worst-case bound is confirmed.
20.33 Show, in the style of Figure 20.8, the result of computing the MST of
the network defined in Exercise 20.21 with Prim’s algorithm.
• 20.34 Describe a family of graphs with V vertices and E edges for which
the worst-case running time of the PFS implementation of Prim’s algorithm is
confirmed.
•• 20.35 Develop a reasonable generator for random graphs with V vertices
and E edges such that the running time of the PFS implementation of Prim’s
algorithm (Program 20.4) is nonlinear.
20.36 Convert Program 20.4 for use in an adjacency-matrix graph ADT
implementation.
◦ 20.37 Modify Program 20.4 so that it works like Program 18.8, in that it
keeps on the fringe all edges incident upon tree vertices. Run empirical studies
to compare your implementation with Program 20.4, for various weighted
graphs (see Exercises 20.9–14).
• 20.38 Provide an implementation of Prim’s algorithm that makes use of your
representation-independent graph ADT from Exercise 17.51.
20.39 Suppose that you use a priority-queue implementation that maintains
a sorted list. What would be the worst-case running time for graphs with V
vertices and E edges, to within a constant factor? When would this method
be appropriate, if ever? Defend your answer.
◦ 20.40 An MST edge whose deletion from the graph would cause the MST
weight to increase is called a critical edge. Show how to find all critical edges
in a graph in time proportional to E lg V .
20.41 Run empirical studies to compare the performance of Program 20.3
to that of Program 20.4, using an unordered array implementation for the
priority queue, for various weighted graphs (see Exercises 2