forked from KevinCoble/AIToolbox
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Graph.swift
601 lines (512 loc) · 24.1 KB
/
Graph.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
//
// Graph.swift
// AIToolbox
//
// Created by Kevin Coble on 2/15/15.
// Copyright (c) 2015 Kevin Coble. All rights reserved.
//
import Foundation
// MARK: GraphEdge
/// Class for link between GraphNodes.
/// Only subclass if you need to override the cost function to be variable
open class GraphEdge {
let destinationNode : GraphNode
let cost : Double
required public init(destination : GraphNode) {
// Save the destination
destinationNode = destination
// Set the cost to 0
cost = 0.0
}
public init (destination: GraphNode, costValue: Double) {
// Save the destination
destinationNode = destination
// Save the cost
cost = costValue
}
/// Method to get cost
/// Override to make calculation. Edge is from passed in sourceNode to stored destinationNode
open func getCost(_ sourceNode: GraphNode) -> Double {return cost}
/// Method to determine if this edge has the passed in GraphNode as a destination
open func goesToNode(_ node: GraphNode) -> Bool {
return node === destinationNode
}
}
// MARK: -
// MARK: GraphNode
/// Subclass GraphNode to add data to a graph node
open class GraphNode {
var edgeList: [GraphEdge] = []
var visited = false
var leastCostToNode = Double.infinity
// Initializer
public init() {
}
/// Method to add a GraphEdge (or subclass) to a node
open func addEdge(_ newEdge: GraphEdge) {
edgeList.append(newEdge)
}
/// Convenience method to add two edges, making a bi-directional connection.
/// This only works with standard GraphEdge classes, not sub-classes.
/// If the optional second cost is not provided, the passed-in edge cost is duplicated
open func addBidirectionalEdge(_ newEdge: GraphEdge, reverseCost: Double?) {
// Add the first direction
edgeList.append(newEdge)
// Create the edge for the other direction
var edge : GraphEdge
if let cost = reverseCost {
edge = GraphEdge(destination: self, costValue: cost)
}
else {
edge = GraphEdge(destination: self, costValue: newEdge.cost)
}
// Add the edge to the other node
newEdge.destinationNode.addEdge(edge)
}
/// Method to remove all edges to a specified destination node
open func removeEdgesToNode(_ node: GraphNode) {
var index = edgeList.count-1
while (index >= 0) {
if edgeList[index].goesToNode(node) {
edgeList.remove(at: index)
}
index -= 1
}
}
/// Method to return the admissible heuristic value for an A* search
/// The value is a lower-bound estimate of the remaining cost to the goal
open func admissibleHeuristicCost() -> Double { return 0.0 }
}
// MARK: -
// MARK: Search Queue Entry
// Struct for queueing breadth-first nodes and search information on the queue
struct SearchQueueEntry<T> {
let graphNode : T
var pathToNode : [Int] = []
var costToNode : Double
init(node: T) {
graphNode = node
costToNode = 0.0
}
init(node: T, cost: Double) {
graphNode = node
costToNode = cost
}
}
// MARK: -
// MARK: Graph Class
/// Class for a graph of nodes. The nodes are subclasses of GraphNode, connected by
/// GraphEdge classes (or subclasses)
open class Graph<T : GraphNode> {
// Array of nodes in the list
var graphNodeList : [T] = []
// Current path in depth-first search
var currentPath: [Int] = []
// 'Queue' for breadth-first search. These are two arrays to allow sorting for pruning metheds
var currentQueueList = [SearchQueueEntry<T>]()
var nextDepthList = [SearchQueueEntry<T>]()
// Empty initializer
public init() {
}
/// Method to add a GraphNode (or subclass) to a graph
open func addNode(_ newNode: T) {
graphNodeList.append(newNode)
}
/// Method to remove a node from the graph, along with all edges to that node
open func removeNode(_ nodeToRemove: T) {
for index in 0 ..< graphNodeList.count {
if graphNodeList[index] === nodeToRemove {
graphNodeList.remove(at: index)
for node in graphNodeList {
node.removeEdgesToNode(nodeToRemove)
}
return
}
}
}
// MARK: Depth-First
/// Method to do depth-first search of graph to find path between nodes.
/// Returns an array of edge indices for the traverse path from the start to the end node.
/// Returns empty array if start and end nodes are the same. Returns nil if no path exists
open func getDepthFirstPath(fromNode: T, searchCriteria: (T) -> Bool) -> [Int]? {
// Clear the 'queue' arrays
currentQueueList = []
nextDepthList = []
// Clear the visited flags
for node in graphNodeList { node.visited = false }
// Push the first node
currentQueueList.append(SearchQueueEntry<T>(node:fromNode))
// Mark the first node as visited
fromNode.visited = true
// Use the 'find next path' function to find the path
return getDepthFirstNextPath(searchCriteria: searchCriteria)
}
/// Method to do depth-first search of graph to find path between nodes, starting at the last path found.
/// Returns an array of edge indices for the traverse path from the start to the end node.
/// Returns empty array if start and end nodes are the same. Returns nil if no path exists.
/// Modifying the graph between calls will result in unexpected behavior - possibly loops
open func getDepthFirstNextPath(searchCriteria: (T) -> Bool) -> [Int]? {
// Iterate until we find a path, or we run out of queue
while (true) {
if currentQueueList.count > 0 {
// Get the next entry for the current depth
let entry = currentQueueList.removeLast()
// Push each of the children onto the next level
var index = 0
for edge in entry.graphNode.edgeList {
if (!edge.destinationNode.visited) {
var newEntry = SearchQueueEntry<T>(node: (edge.destinationNode as! T))
newEntry.pathToNode = entry.pathToNode
newEntry.pathToNode.append(index)
nextDepthList.append(newEntry)
edge.destinationNode.visited = true
}
index += 1
}
// Put the next level at the end of the current queue
currentQueueList += Array(nextDepthList.reversed()) // reverse since we are taking last item off)
nextDepthList = []
// See if this is the node we want
if (searchCriteria(entry.graphNode)) {return entry.pathToNode}
}
else {
return nil
}
}
}
// MARK: Breadth-First
/// Method to do breadth-first search of graph to find path between nodes.
/// Returns an array of edge indices for the traverse path from the start to the end node.
/// Returns empty array if start and end nodes are the same. Returns nil if no path exists
open func getBreadthFirstPath(fromNode: T, searchCriteria: (T) -> Bool) -> [Int]? {
// Clear the 'queue' arrays
currentQueueList = []
nextDepthList = []
// Clear the visited flags
for node in graphNodeList { node.visited = false }
// Push the first node
currentQueueList.append(SearchQueueEntry<T>(node:fromNode))
// Mark the first node as visited
fromNode.visited = true
// Use the 'find next path' function to find the path
return getBreadthFirstNextPath(searchCriteria: searchCriteria)
}
/// Method to do breadth-first search of graph to find path between nodes, starting at the last path found.
/// Call getBreadthFirstPath to start search at beginning
/// Returns an array of edge indices for the traverse path from the start to the end node.
/// Returns empty array if start and end nodes are the same. Returns nil if no path exists.
/// Modifying the graph between calls will result in unexpected behavior - possibly loops
open func getBreadthFirstNextPath(searchCriteria: (T) -> Bool) -> [Int]? {
// Iterate until we find a path, or we run out of queue
while (true) {
if currentQueueList.count > 0 {
// Get the next entry for the current depth
let entry = currentQueueList.removeLast()
// Push each of the children onto the next level
var index = 0
for edge in entry.graphNode.edgeList {
if (!edge.destinationNode.visited) {
var newEntry = SearchQueueEntry<T>(node: (edge.destinationNode as! T))
newEntry.pathToNode = entry.pathToNode
newEntry.pathToNode.append(index)
nextDepthList.append(newEntry)
edge.destinationNode.visited = true
}
index += 1
}
// See if this is the node we want
if (searchCriteria(entry.graphNode)) {return entry.pathToNode}
}
else {
if (nextDepthList.count == 0) {return nil} // No more nodes, return nil to indicate no path
currentQueueList = Array(nextDepthList.reversed()) // Go down one more depth level (reverse since we are taking last item off)
nextDepthList = []
}
}
}
// MARK: Hill Climb
/// Method to do hill-climb search of graph to find path between nodes.
/// The heuristic is a closure/function that returns the relative value of the node compared to the goal. Larger values indicate closer to the goal.
/// Returns an array of edge indices for the traverse path from the start to the end node.
/// Returns empty array if start and end nodes are the same. Returns nil if no path exists
open func getHillClimbPath(fromNode: T, nodeHeuristic: (T) -> Double, searchCriteria: (T) -> Bool) -> [Int]? {
// Clear the 'queue' arrays
currentQueueList = []
nextDepthList = []
// Clear the visited flags
for node in graphNodeList { node.visited = false }
// Push the first node
currentQueueList.append(SearchQueueEntry<T>(node:fromNode))
// Mark the first node as visited
fromNode.visited = true
// Use the 'find next path' function to find the path
return getHillClimbNextPath(nodeHeuristic: nodeHeuristic, searchCriteria: searchCriteria)
}
/// Method to do hill-climb search of graph to find path between nodes, starting at the last path found.
/// Returns an array of edge indices for the traverse path from the start to the end node.
/// Returns empty array if start and end nodes are the same. Returns nil if no path exists.
/// Modifying the graph between calls will result in unexpected behavior - possibly loops
open func getHillClimbNextPath(nodeHeuristic: (T) -> Double, searchCriteria: (T) -> Bool) -> [Int]? {
// Iterate until we find a path, or we run out of queue
while (true) {
if currentQueueList.count > 0 {
// Get the next entry for the current depth
let entry = currentQueueList.removeLast()
// Push each of the children onto the next level
var index = 0
for edge in entry.graphNode.edgeList {
if (!edge.destinationNode.visited) {
var newEntry = SearchQueueEntry<T>(node: (edge.destinationNode as! T))
newEntry.pathToNode = entry.pathToNode
newEntry.pathToNode.append(index)
edge.destinationNode.visited = true
// Add the new node sorted, based on it's heuristic value
_ = enqueueCurrentOrdered(newEntry, orderHeuristic: nodeHeuristic)
}
index += 1
}
// See if this is the node we want
if (searchCriteria(entry.graphNode)) {return entry.pathToNode}
}
else {
return nil
}
}
}
// Enqueue on the current list with the higher heuristic going to the end
func enqueueCurrentOrdered(_ newItem: SearchQueueEntry<T>, orderHeuristic: (T) -> Double) -> Bool {
var max = currentQueueList.count
if (max == 0) {
currentQueueList.append(newItem)
return true
}
var min = 0
let newItemHeuristicValue = orderHeuristic(newItem.graphNode)
var mid : Int
while (min < max) {
mid = (min + max) / 2
if (orderHeuristic(currentQueueList[mid].graphNode) < newItemHeuristicValue) {
min = mid + 1;
}
else {
max = mid;
}
}
if (max == min) {
if (max != currentQueueList.count && orderHeuristic(currentQueueList[max].graphNode) == newItemHeuristicValue) {
return false
}
currentQueueList.insert(newItem, at: max)
return true
}
else {
return false
}
}
// MARK: Beam Search
/// Method to do beam search of graph to find path between nodes. The width of the 'beam' is a parameter.
/// The heuristic is a closure/function that returns the relative value of the node compared to the goal. Larger values indicate closer to the goal.
/// Returns an array of edge indices for the traverse path from the start to the end node.
/// Returns empty array if start and end nodes are the same. Returns nil if no path exists
open func getBeamPath(fromNode: T, nodeHeuristic: (T) -> Double, beamWidth: Int, searchCriteria: (T) -> Bool) -> [Int]? {
// Clear the 'queue' arrays
currentQueueList = []
nextDepthList = []
// Validate the input
if (beamWidth < 1) {return nil}
// Clear the visited flags
for node in graphNodeList { node.visited = false }
// Push the first node
currentQueueList.append(SearchQueueEntry<T>(node:fromNode))
// Mark the first node as visited
fromNode.visited = true
// Iterate until we find a path, or we run out of queue
while (true) {
if currentQueueList.count > 0 {
// Get the next entry for the current depth
let entry = currentQueueList.removeLast()
// Push each of the children onto the next level
var index = 0
for edge in entry.graphNode.edgeList {
if (!edge.destinationNode.visited) {
var newEntry = SearchQueueEntry<T>(node: (edge.destinationNode as! T))
newEntry.pathToNode = entry.pathToNode
newEntry.pathToNode.append(index)
nextDepthList.append(newEntry)
edge.destinationNode.visited = true
}
index += 1
}
// See if this is the node we want
if (searchCriteria(entry.graphNode)) {return entry.pathToNode}
}
else {
if (nextDepthList.count == 0) {return nil} // No more nodes, return nil to indicate no path
nextDepthList.sort(by: {nodeHeuristic($0.graphNode) > nodeHeuristic($1.graphNode)}) // Sort best first
while (nextDepthList.count > beamWidth) { nextDepthList.removeLast() } // Limit to the beam width
currentQueueList = Array(nextDepthList.reversed()) // Go down one more depth level (reverse since we are taking last item off)
nextDepthList = []
}
}
}
// MARK: Optimal Paths
/// Method to do a best-first search for the shortest path between the start and goal.
/// The 'length' of each edge traversal is retrieved using the 'getCost' method.
/// Returns an array of edge indices for the traverse path from the start to the end node.
/// Returns empty array if start and end nodes are the same. Returns nil if no path exists
open func getShortestPathByBestFirst(fromNode: T, searchCriteria: (T) -> Bool) -> [Int]? {
// Clear the 'queue' arrays
currentQueueList = []
nextDepthList = []
// Set the cost to each node to infinite
for node in graphNodeList { node.leastCostToNode = Double.infinity }
// Push the first node
currentQueueList.append(SearchQueueEntry<T>(node:fromNode))
// Mark the first node as visited with no cost
fromNode.leastCostToNode = 0.0
// Start with a best-goal cost of infinity
var bestGoalCost = Double.infinity
var result : [Int]?
// Iterate until we find a path, or we run out of queue
while (true) {
if currentQueueList.count > 0 {
// Get the next entry for the current depth
let entry = currentQueueList.removeLast()
// Push each of the children onto the next level
var index = 0
for edge in entry.graphNode.edgeList {
let newCost = entry.costToNode + edge.getCost(entry.graphNode)
if (newCost < edge.destinationNode.leastCostToNode) {
if (newCost < bestGoalCost) { // Skip any that are already have higher cost than a found goal
var newEntry = SearchQueueEntry<T>(node: (edge.destinationNode as! T), cost: newCost)
newEntry.pathToNode = entry.pathToNode
newEntry.pathToNode.append(index)
_ = enqueueOrderedByCost(newEntry)
edge.destinationNode.leastCostToNode = newCost
}
}
index += 1
}
// See if this is a goal node
if (searchCriteria(entry.graphNode)) {
// See if it is better than any other goal node we found at this depth
if (entry.costToNode < bestGoalCost) {
bestGoalCost = entry.costToNode
result = entry.pathToNode
}
}
}
else {
// Done checking, return the result
return result
}
}
}
/// Method to do an A* search for the shortest path between the start and destination node.
/// The 'length' of each edge traversal is retrieved using the 'getCost' method.
/// The admissible heuristic is retrieved using the 'getAdmissibleHeuristic' method
/// Returns an array of edge indices for the traverse path from the start to the end node.
/// Returns empty array if start and end nodes are the same. Returns nil if no path exists
open func getShortestPathByAStar(fromNode: T, destinationNode: T) -> [Int]? {
// Clear the 'queue' arrays
currentQueueList = []
nextDepthList = []
// Set the cost to each node to infinite
for node in graphNodeList { node.leastCostToNode = Double.infinity }
// Push the first node
currentQueueList.append(SearchQueueEntry<T>(node:fromNode))
// Mark the first node as visited with no cost
fromNode.leastCostToNode = 0.0
// Start with a best-goal cost of infinity
var bestGoalCost = Double.infinity
var result : [Int]?
// Iterate until we find a path, or we run out of queue
while (true) {
if currentQueueList.count > 0 {
// Get the next entry for the current depth
let entry = currentQueueList.removeLast()
let actualCost = entry.costToNode - entry.graphNode.admissibleHeuristicCost()
// Push each of the children onto the next level
var index = 0
for edge in entry.graphNode.edgeList {
let newCost = actualCost + edge.getCost(entry.graphNode)
let newEstimatedFinalCost = newCost + edge.destinationNode.admissibleHeuristicCost()
if (newCost < edge.destinationNode.leastCostToNode) {
if (newCost < bestGoalCost) { // Skip any that are already have higher cost than a found goal
var newEntry = SearchQueueEntry<T>(node: (edge.destinationNode as! T), cost: newEstimatedFinalCost)
newEntry.pathToNode = entry.pathToNode
newEntry.pathToNode.append(index)
_ = enqueueOrderedByCost(newEntry) // Sort based on estimated final cost
edge.destinationNode.leastCostToNode = newCost
}
}
index += 1
}
// See if this is a goal node
if (entry.graphNode === destinationNode) {
// See if it is better than any other goal node we found at this depth
if (entry.costToNode < bestGoalCost) {
bestGoalCost = entry.costToNode
result = entry.pathToNode
}
}
}
else {
// Done checking, return the result
return result
}
}
}
// Enqueue on the 'current' list with lower cost going at the end (first to pop
func enqueueOrderedByCost(_ newItem: SearchQueueEntry<T>) -> Bool {
var max = currentQueueList.count
if (max == 0) {
currentQueueList.append(newItem)
return true
}
var min = 0
var mid : Int
while (min < max) {
mid = (min + max) / 2
if (currentQueueList[mid].costToNode > newItem.costToNode) {
min = mid + 1;
}
else {
max = mid;
}
}
if (max == min) {
if (max != currentQueueList.count && currentQueueList[max].costToNode == newItem.costToNode) {
return false
}
currentQueueList.insert(newItem, at: max)
return true
}
else {
return false
}
}
// MARK: Convert Path
/// Method to convert a path (an array of edge indices) to a list
/// of nodes traversed by the path, including the start node. If the path is nil, the list will be nil.
/// If the path is empty, the list will be contain just the start node
open func convertPathToNodeList(_ startNode: T, path: [Int]?) -> [T]? {
// Check for a nil path
if let path = path {
// Create the node list
var nodeList = [T]()
// Add the start node
var currentNode = startNode
nodeList.append(currentNode)
// Traverse the path
for index in path {
currentNode = currentNode.edgeList[index].destinationNode as! T
nodeList.append(currentNode)
}
return nodeList
}
else {
return nil
}
}
}