📄 miles_span.w
字号:
% This file is part of the Stanford GraphBase (c) Stanford University 1993@i boilerplate.w %<< legal stuff: PLEASE READ IT BEFORE MAKING ANY CHANGES!@i gb_types.w\def\title{MILES\_\,SPAN}\def\<#1>{$\langle${\rm#1}$\rangle$}\prerequisite{GB\_\,MILES}@* Minimum spanning trees.A classic paper by R. L. Graham and Pavol Hell about the history of@^Graham, Ronald Lewis@>@^Hell, Pavol@>algorithms to find the minimum-length spanning tree of a graph[{\sl Annals of the History of Computing \bf7} (1985), 43--57]describes three main approaches to that problem. Algorithm~1,``two nearest fragments,'' repeatedly adds a shortest edge that joinstwo hitherto unconnected fragments of the graph; this algorithm wasfirst published by J.~B. Kruskal in 1956. Algorithm~2, ``nearest@^Kruskal, Joseph Bernard@>neighbor,'' repeatedly adds a shortest edge that joins a particularfragment to a vertex not in that fragment; this algorithm was firstpublished by V. Jarn\'{\i}k in 1930. Algorithm~3, ``all nearest@^Jarn{\'\i}k, Vojt\u ech@>fragments,'' repeatedly adds to each existing fragment the shortestedge that joins it to another fragment; this method, seemingly themost sophisticated in concept, also turns out to be the oldest,being first published by Otakar Bor{\accent23u}vka in 1926.@^Bor{\accent23u}vka, Otakar@>The present program contains simple implementations of all threeapproaches, in an attempt to make practical comparisons of howthey behave on ``realistic'' data. One of the main goals of thisprogram is to demonstrate a simple way to make machine-independentcomparisons of programs written in \CEE/, by counting memoryreferences or ``mems.'' In other words, this program is intendedto be read, not just performed.The author believes that mem counting sheds considerable light onthe problem of determining the relative efficiency of competingalgorithms for practical problems. He hopes other researchers willenjoy rising to the challenge of devising algorithms that find minimumspanning trees in significantly fewer mem units than the algorithmspresented here, on problems of the size considered here.Indeed, mem counting promises to be significant for combinatorialalgorithms of all kinds. The standard graphs available in theStanford GraphBase should make it possible to carry out a largenumber of machine-independent experiments concerning the practicalefficiency of algorithms that have previously been studiedonly asymptotically.@ The graphs we will deal with are produced by the |miles| subroutine,found in the {\sc GB\_\,MILES} module. As explained there,|miles(n,north_weight,west_weight,pop_weight,0,max_degree,seed)| produces agraph of |n<=128| vertices based on the driving distances betweenNorth American cities. By default we take |n=100|, |north_weight=west_weight=pop_weight=0|, and |max_degree=10|; this gives billions of different sparsegraphs, when different |seed| values are specified, since a differentrandom number seed generally results in the selection of anotherone of the $\,128\,\choose100$ possible subgraphs.The default parameters can be changed by specifying options on thecommand line, at least in a \UNIX/ implementation, thereby obtaining avariety of special effects. For example, the value of |n| can beraised or lowered and/or the graph can be made more or less sparse.The user can bias the selection by ranking cities according to theirpopulation and/or position, if nonzero values are given to any of theparameters |north_weight|, |west_weight|, or |pop_weight|.Command-line options \.{-n}\<number>, \.{-N}\<number>, \.{-W}\<number>,\.{-P}\<number>, \.{-d}\<number>, and \.{-s}\<number>are used to specify non-default values of the respective quantities |n|,|north_weight|, |west_weight|, |pop_weight|, |max_degree|, and |seed|.If the user specifies a \.{-r} option, for example by saying `\.{miles\_span}\.{-r10}', this program will investigate the spanning trees of aseries of, say, 10 graphs having consecutive |seed| values. (Thisoption makes sense only if |north_weight=west_weight=pop_weight=0|,because |miles| chooses the top |n| cities by weight. The procedure rarelyneeds to use random numbers to break ties when the weights are nonzero,because cities rarely have exactly the same weight in that case.)The special command-line option \.{-g}$\langle\,$filename$\,\rangle$overrides all others. It substitutes an external graph previously saved by|save_graph| for the graphs produced by |miles|. @^UNIX dependencies@>Here is the overall layout of this \CEE/ program:@p#include "gb_graph.h" /* the GraphBase data structures */#include "gb_save.h" /* |restore_graph| */#include "gb_miles.h" /* the |miles| routine */@h@#@<Global variables@>@;@<Procedures to be declared early@>@;@<Priority queue subroutines@>@;@<Subroutines@>@;main(argc,argv) int argc; /* the number of command-line arguments */ char *argv[]; /* an array of strings containing those arguments */{@+unsigned long n=100; /* the desired number of vertices */ unsigned long n_weight=0; /* the |north_weight| parameter */ unsigned long w_weight=0; /* the |west_weight| parameter */ unsigned long p_weight=0; /* the |pop_weight| parameter */ unsigned long d=10; /* the |max_degree| parameter */ long s=0; /* the random number seed */ unsigned long r=1; /* the number of repetitions */ char *file_name=NULL; /* external graph to be restored */ @<Scan the command-line options@>; while (r--) { if (file_name) g=restore_graph(file_name); else g=miles(n,n_weight,w_weight,p_weight,0L,d,s); if (g==NULL || g->n<=1) { fprintf(stderr,"Sorry, can't create the graph! (error code %ld)\n", panic_code); return -1; /* error code 0 means the graph is too small */ } @<Report the number of mems needed to compute a minimum spanning tree of |g| by various algorithms@>; gb_recycle(g); s++; /* increase the |seed| value */ } return 0; /* normal exit */}@ @<Global...@>=Graph *g; /* the graph we will work on */@ @<Scan the command-line options@>=while (--argc) {@^UNIX dependencies@> if (sscanf(argv[argc],"-n%lu",&n)==1) ; else if (sscanf(argv[argc],"-N%lu",&n_weight)==1) ; else if (sscanf(argv[argc],"-W%lu",&w_weight)==1) ; else if (sscanf(argv[argc],"-P%lu",&p_weight)==1) ; else if (sscanf(argv[argc],"-d%lu",&d)==1) ; else if (sscanf(argv[argc],"-r%lu",&r)==1) ; else if (sscanf(argv[argc],"-s%ld",&s)==1) ; else if (strcmp(argv[argc],"-v")==0) verbose=1; else if (strncmp(argv[argc],"-g",2)==0) file_name=argv[argc]+2; else { fprintf(stderr, "Usage: %s [-nN][-dN][-rN][-sN][-NN][-WN][-PN][-v][-gfoo]\n", argv[0]); return -2; }}if (file_name) r=1;@ We will try out four basic algorithms that have received prominentattention in the literature. Graham and Hell's Algorithm~1 is representedby the |krusk| procedure, which uses Kruskal's algorithm after theedges have been sorted by length with a radix sort. Their Algorithm~2is represented by the |jar_pr| procedure, which incorporates a priority queue structure that we implement in two ways, either asa simple binary heap or as a Fibonacci heap. And their Algorithm~3is represented by the |cher_tar_kar| procedure, which implements amethod similar to Bor{\accent23u}vka's that was independentlydiscovered by Cheriton and Tarjan and later simplified and refined byKarp and Tarjan.@^Cheriton, David Ross@>@^Tarjan, Robert Endre@>@^Karp, Richard Manning@>@d INFINITY (unsigned long)-1 /* value returned when there's no spanning tree */@<Report the number...@>=printf("The graph %s has %ld edges,\n",g->id,g->m/2);sp_length=krusk(g);if (sp_length==INFINITY) printf(" and it isn't connected.\n");else printf(" and its minimum spanning tree has length %ld.\n",sp_length);printf(" The Kruskal/radix-sort algorithm takes %ld mems;\n",mems);@<Execute |jar_pr(g)| with binary heaps as the priority queue algorithm@>;printf(" the Jarnik/Prim/binary-heap algorithm takes %ld mems;\n",mems);@<Allocate additional space needed by the more complex algorithms; or |goto done| if there isn't enough room@>;@<Execute |jar_pr(g)| with Fibonacci heaps as the priority queue algorithm@>;printf(" the Jarnik/Prim/Fibonacci-heap algorithm takes %ld mems;\n",mems);if (sp_length!=cher_tar_kar(g)) { if (gb_trouble_code) printf(" ...oops, I've run out of memory!\n"); else printf(" ...oops, I've got a bug, please fix fix fix\n"); return -3;}printf(" the Cheriton/Tarjan/Karp algorithm takes %ld mems.\n\n",mems);done:;@ @<Glob...@>=unsigned long sp_length; /* length of the minimum spanning tree */@ When the |verbose| switch is nonzero, edges found by the variousalgorithms will call the |report| subroutine.@<Sub...@>=report(u,v,l) Vertex *u,*v; /* adjacent vertices in the minimum spanning tree */ long l; /* the length of the edge between them */{ printf(" %ld miles between %s and %s [%ld mems]\n", l,u->name,v->name,mems);}@*Strategies and ground rules.Let us say that a {\sl fragment\/} is any subtree of a minimumspanning tree. All three algorithms we implement make use of a basicprinciple first stated in full generality by R.~C. Prim in 1957:@^Prim, Robert Clay@>``If a fragment~$F$ does not include all the vertices, and if $e$~isa shortest edge joining $F$ to a vertex not in~$F$, then $F\cup e$is a fragment.'' To prove Prim's principle, let $T$ be a minimumspanning tree that contains $F$ but not~$e$. Adding $e$ to~$T$ createsa circuit containing some edge $e'\ne e$, where $e'$ runs from a vertexin~$F$ to a vertex not in~$F$. Deleting $e'$ from$T\cup e$ produces a spanning tree~$T'$ of total length no largerthan the total length of~$T$. Hence $T'$ is a minimum spanningtree containing $F\cup e$, QED.@ The graphs produced by |miles| have special properties, and it is fair gameto make use of those properties if we can.First, the length of each edge is a positive integer less than $2^{12}$.Second, the $k$th vertex $v_k$ of the graph is represented in \CEE/ programs bythe pointer expression |g->vertices+k|. If weights have been assigned,these vertices will be in order by weight. For example, if |north_weight=1|but |west_weight=pop_weight=0|, vertex $v_0$ will be the most northerly cityand vertex $v_{n-1}$ will be the most southerly.Third, the edges accessible from a vertex |v| appear in a linked liststarting at |v->arcs|. An edge from |v| to $v_j$ will precede anedge from |v| to $v_k$ in this list if and only if $j>k$.Fourth, the vertices have coordinates |v->x_coord| and |v->y_coord|that are correlated with the length of edges between them: TheEuclidean distance between the coordinates of two vertices tends to be smallif and only if those vertices are connected by a relatively short edge.(This is only a tendency, not a certainty; for example, some citiesaround Chesapeake Bay are fairly close together as the crow flies, but notwithin easy driving range of each other.)Fifth, the edge lengths satisfy the triangle inequality: Wheneverthree edges form a cycle, the longest is no longer than the sum ofthe lengths of the two others. (It can be proved thatthe triangle inequality is of no use in finding minimum spanningtrees; we mention it here only to exhibit yet another way in whichthe data produced by |miles| is known to be nonrandom.)Our implementation of Kruskal's algorithm will make use of the firstproperty, and it also uses part of the third to avoid considering anedge more than once. We will not exploit the other properties, but areader who wants to design algorithms that use fewer mems to find minimumspanning trees of these graphs is free to use any idea that helps.@ Speaking of mems, here are the simple \CEE/ instrumentation macros that weuse to count memory references. The macros are called |o|, |oo|, |ooo|,and |oooo|; hence Jon Bentley has called this a ``little oh analysis.''@^Bentley, Jon Louis@>Implementors who want to count mems are supposed to say, e.g., `|oo|,'just before an assignment statement or boolean expression that makestwo references to memory. The \CEE/ preprocessor will convert thisto a statement that increases |mems| by~2 as that statement or expressionis evaluated.The semantics of \CEE/ tell us that the evaluation of an expressionlike `|a&&(o,a->len>10)|' will increment |mems| if and only if thepointer variable~|a| is non-null. Warning: The parentheses are veryimportant in this example, because \CEE/'s operator |&&| (i.e.,\.{\&\&}) has higher precedence than comma.Values of significant variables, like |a| in the previous example,can be assumed to be in ``registers,'' and no charge is made forarithmetic computations that involve only registers. But the totalnumber of registers in an implementation must be finite and fixed,independent of the problem size.@^discussion of \\{mems}@>\CEE/ does not allow the |o| macros to appear in declarations, so we cannottake full advantage of \CEE/'s initialization mechanism when we arecounting mems. But it's easy to initialize variables in separatestatements after the declarations are done.@d o mems++@d oo mems+=2@d ooo mems+=3@d oooo mems+=4@<Glob...@>=long mems; /* the number of memory references counted */@ Examples of these mem-counting conventions appear throughout theprogram that follows. Some people will undoubtedly ask why the insertion ofmacros by hand is being recommended here, when it would be possible todevelop a fancy system that counts mems automatically. The authorbelieves that it is best to rely on programmers to introduce |o| and|oo|, etc., by themselves, for several reasons. (1)~The macros can beinserted easily and quickly using a text editor. (2)~An implementationneed not pay for mems that could be avoided by a suitable optimizingcompiler or by making the \CEE/ program text slightly more complex;thus, authors can use their good judgment to keep programs morereadable than if the code were overly hand-optimized. (3)~Theprogrammer should be able to see exactly where mems are being charged,as an aid to bottleneck elimination. Occurrences of |o| and |oo| makethis plain without messing up the program text. (4)~An implementationneed not be charged for mems that merely provide diagnostic output, ormems that do redundant computations just to double-check the validityof ``proven'' assertions as a program is being tested.@^discussion of \\{mems}@>Computer architecture is converging rapidly these days to thedesign of machines in which the exact running time of a programdepends on complicated interactions between pipelined circuitry andthe dynamic properties of cache mapping in a memory hierarchy,not to mention the effects of compilers and operating systems.But a good approximation to running time is usually obtained if weassume that the amount of computation is proportional to the activityof the memory bus between registers and main memory. Thisapproximation is likely to get even better in the future, asRISC computers get faster and faster in comparison to memory devices.Although the mem measure is far from perfect, it appears to besignificantly less distorted than any other measurement that canbe obtained without considerably more work. An implementation thatis designed to use few mems will almost certainly be efficienton today's sequential computers, as well as on the sequential computerswe can expect to be built in the foreseeable future. And the conversestatement is even more true: An algorithm that runs fast will notconsume many mems.Of course authors are expected to be reasonable and fair when they
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -