Introduction
I will present some of the exercises I got from my perl programming mentor. These exercises helped me to get more in depth with perl, and learn the mechanism behind the language. You can try to solve these exercises also on your own, to improve your skills.
My solution is only one way to solve it. A problem has unlimited number of solutions, and if they all accomplish the task, they are all correct.
Usually I will first write the specification I got, then I will show my solution and also give some explanation.
Specification
You have a file with following data:
# cat info.text a b 4 b c 5 c d 6 d a 3 a b 2 a a 1
Dispalying it with a diagram:
Three columns seperated by spaces. First column is a source town, second column is the destination and third is the distance between these towns. Of course if the distance between a-b town is 4, the reverse is also true (b-a is also 4). You have to ask for 2 inputs the source and destination town, and then return the shortest path.
Solution
I needed some time and thinking to find a way I can get started with this problem. In game programing I read of Dijkstra algorithm, which is a graph search algorithm that solves the single-source shortest path problem for a graph with nonnegative edge path costs, producing the shortest path tree. I was thinking about using the methodology, but at last I decided to go with recursion.
After 3 days I had gotten the following solution:
#!/usr/bin/perl use strict; my %roadmap = (); my %visited; my @path; my @shortest=(0,""); sub findnext(\@$) { my ($findpath, $dest )= @_; foreach my $town ( keys %{$roadmap{$findpath->[-1]}}) { if (defined($visited{$town})) { if ( $town eq $dest) { push(@$findpath,$town); pathcost(@{$findpath}); } } else { $visited{$town} = '1'; push(@$findpath,$town); if ( $town eq $dest) { pathcost(@{$findpath}); } else { findnext($findpath, $dest); } pop(@$findpath); undef $visited{$town}; } } } sub pathcost ($) { my @pathin = @_; my $pos=0; my $length=$#pathin; my $cost=0; while ( $pos lt $length) { $cost += $roadmap{$pathin[$pos]}->{$pathin[$pos+1]}; $pos++; } if ( $cost < $shortest[0]) { $shortest[0]=$cost; $shortest[1]=join(',',@pathin); } } open ( my $fh, "<" , "info.text" ) or die "Cannot open file!"; while ( my $line = <$fh>) { chomp $line; my ( $from, $to , $cost ) = split( / /, $line ); #/ if (defined($roadmap{$from})) { if (defined($roadmap{$from}->{$to} )) { if ( $roadmap{$from}->{$to} gt $cost ) { $roadmap{$from}{$to} = $cost; $shortest[0]+=$cost; } } else { $roadmap{$from}{$to} = $cost; $shortest[0]+=$cost; } } else { $roadmap{$from} = { $to => $cost}; $shortest[0]+=$cost; } if (defined($roadmap{$to})) { if (defined($roadmap{$to}->{$from} )) { if ( $roadmap{$to}->{$from} gt $cost ) { $roadmap{$to}{$from} = $cost; $shortest[0]+=$cost; } } else { $roadmap{$to}{$from} = $cost; $shortest[0]+=$cost; } } else { $roadmap{$to} = { $from => $cost}; $shortest[0]+=$cost; } } close $fh; print "From: "; chomp ( my $readfrom = <> ); print "To: "; chomp ( my $readto = <> ); $path[0] = $readfrom; $visited{$readfrom} = '1'; findnext(@path, $readto); if ( $shortest[1] eq "") { print "No Path between the two towns.\n"; } else { print "Shortest Path: @shortest\n"; }
I will add explanation for the starters aswell.
First line is the basic interpreter. If you get error like:
-bash: ./admin.pl: /usr/bin1/perl: bad interpreter: No such file or directory
This means the interpreter cannot be found at the location. 2 possibilities, perl is not on your system, or it is on a different path. Usuall you can solve it with the following commands:
# which perl /usr/bin/perl # find / -name perl /usr/bin/perl
Moving to line 2. Lots of forums or guides show people recommending to turn off strict or warning messeages. In my opinion this is quite stupid. These errors are there for a reason, and need to be solved.Unsolved errors can result to unexpected outcomes.
Line 3-6 are variable declarations. The % declaration is for hashes (roadmap: the full graph of the map, visited: this could have been solved with a array, but I preferred to use a hash.), the @ are arrays (path: is the current full path being traversed, shortest is the shortest path at moment) These declarations are global, they will be used within the subs so thats they had to be declared at start.
Next we have two subs, line 8 and 32. These will be discussed later.
The program starts at line 47. The file which was specified is opened (info.text), or the program exits if not present. At line 48 we loop through the file line by line to get every data from it.
With chomp in line 49 carriage returns are removed. The lines are split into 3 variables according to the spaces, and are set as values for “from”, “to” and “cost” scalars.
The roadmap graph is also populated ( from line 65-80 the reverse of these steps are done for the reverse path).
First decision at line 51 is, is there already a path from this town.
- If yes we continue to line 52: Next question is there already a route defined to the destination. If there is already a defined route between the two towns (line 52), then we need to inspect if the current pathcost defined is greater than what we have now. If it is greater we need to set the new value and forget the old one as since we are only interested in the shortest path(line 53-54). Line 55 is just adding the cost to the shortest path,aswell as line 59,63, to make the shortest path as big as possible for start. If the current pathcost is smaller then what we have found we just skip it. Line 57-58 is the else for the decision if this is already defined. If the path is not defined we just define it.
- If no then we jump to line 61: if the town is not yet defined then we can go define the whole path into our hash.
As you can see I have used a 2 dimensional array.
The first hash $roadmap{town} are the towns listed. The variable assigned to these are also hashes that have the index of the destionation towns, with values of pathcost.
One can easily see that $roadmap{a}->{b}is the shortest path between two towns that we have for sure.
Line 81, the filehandle of the open file is closed, to keep everything clean.
Input is requested from user to give the “from” and “to” destionations, which are given to two scalars (Line 82-85)
The readform scalar is added to the path and also the visited hash (line 87-88).
At line 90 I will call the findnext sub with two arguments my path array which contains my source town and my destination town scalar. This sub will be recursivly called but that I will discuss later.
From line 91-95 a test is done if my shortest arrays second element has been modified (the pathcost sub should have modified it, if a path was found ), print a message that there was no path between the two towns, else print the shortest path.
Lets discuss the findnext sub which is declared from line 8 to 30. The sub has two arguments, an array ref and a scalar. At line 9 two arguments are set as the findpath and dest scalars. At line 9 the loop is started. Line 10 takes the last element of the array ($findpath->[-1], which at begining is the source town) and checks all the keys of the 2nd hash(roadmap{sourcetown}). Since the keys command expects a hash,so the hash_erf is given as a hash(that is the reason for %{}). At line 11 the town is checked if it is set in the already visited hash. If yes, there is the possibility that the source and destination are the same. The destination is pushed into the array and a pathcost count is done. Since the path array is an array ref called findpath,which needs to be handed down as an array, we lie with: @{}.
If the town isn’t declared in the visited hash,we skip to the else branch at line 17. It is added to the visited hash, then pushed into the array. Then a test is made to see if it is the destination or not. If it is then, similar to line 14 and line 22 a pathcost count is done. If not, a recursion is done with the findnext sub with the current town added. The sub will loop through all possibilities from the next town, going till it investigates all possbilities.
In line 26 and 27 the towns added at line 18-19 are removed.
Last sub at line 32 is the pathcost. This has one argument, the path which is added to the the pathin array at line 33. A pos scalar is defined with value 0 and length is going to be the length of the gotten array. The present cost is taken into a variable with 0 value(for incrementing it needs an initial value).
A loop is done while my current pos scalar is less then my array length (line 37) to always add the cost of the two neighboruring towns.
For example line 38: if path is “a b c” theninitial pos is zero. {$pathin[$pos]} is ‘a’ then {$pathin[$pos+1]} is ‘b’. $roadmap{a}->{b} would mean that cost is associated with the path between a-b.The position is also incremented to find the path between the next two towns.
After the loop is done we have a path cost, which needs to be tested if it is shorter then my shortest path(line 41). If it is then, it is added, seperated by a comma.
Output
Some examples of the output:
# ./admin.pl From: a To: b Shortest Path: 2 a,b # ./admin.pl From: a To: a Shortest Path: 1 a,a # ./admin.pl From: a To: d Shortest Path: 3 a,d # ./admin.pl From: a To: c Shortest Path: 7 a,b,c
Happy practicing.
