Skip to content

Commit af6ce61

Browse files
committed
Implement Algorithm\Tree\Undirected + 100% code coverage
1 parent 0513d3d commit af6ce61

File tree

2 files changed

+181
-1
lines changed

2 files changed

+181
-1
lines changed

src/Tree/Undirected.php

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
use Fhaculty\Graph\Algorithm\Search\Base as Search;
99
use Fhaculty\Graph\Algorithm\Search\StrictDepthFirst;
1010
use Fhaculty\Graph\Vertex;
11+
use Fhaculty\Graph\Edge\Base as Edge;
12+
use Fhaculty\Graph\Edge\UndirectedId as UndirectedEdge;
1113

1214
/**
1315
* Undirected tree implementation
@@ -41,6 +43,14 @@
4143
*/
4244
class Undirected extends Tree
4345
{
46+
/**
47+
* checks if this is a tree
48+
*
49+
* @return boolean
50+
* @uses Graph::isEmpty() to skip empty Graphs (an empty Graph is a valid tree)
51+
* @uses Graph::getVertexFirst() to get get get random "root" Vertex to start search from
52+
* @uses self::getVerticesSubtreeRecursive() to count number of vertices connected to root
53+
*/
4454
public function isTree()
4555
{
4656
if ($this->graph->isEmpty()) {
@@ -50,8 +60,13 @@ public function isTree()
5060
// every vertex can represent a root vertex, so just pick one
5161
$root = $this->graph->getVertexFirst();
5262

53-
// TODO: recurse $root to get sub-vertices
5463
$vertices = array();
64+
try {
65+
$this->getVerticesSubtreeRecursive($root, $vertices, null);
66+
}
67+
catch (UnexpectedValueException $e) {
68+
return false;
69+
}
5570

5671
return (count($vertices) === $this->graph->getNumberOfVertices());
5772
}
@@ -79,4 +94,55 @@ public function isVertexInternal(Vertex $vertex)
7994
{
8095
return ($vertex->getDegree() >= 2);
8196
}
97+
98+
/**
99+
* get subtree for given Vertex and ignore path to "parent" ignoreVertex
100+
*
101+
* @param Vertex $vertex
102+
* @param Vertex[] $vertices
103+
* @param Vertex|null $ignore
104+
* @throws UnexpectedValueException for cycles or directed edges (check isTree()!)
105+
* @uses self::getVerticesNeighboor()
106+
* @uses self::getVerticesSubtreeRecursive() to recurse into sub-subtrees
107+
*/
108+
private function getVerticesSubtreeRecursive(Vertex $vertex, &$vertices, Vertex $ignore = null)
109+
{
110+
if (isset($vertices[$vertex->getId()])) {
111+
// vertex already visited => must be a cycle
112+
throw new UnexpectedValueException('Vertex already visited');
113+
}
114+
$vertices[$vertex->getId()] = $vertex;
115+
116+
foreach ($this->getVerticesNeighboor($vertex) as $vertexNeighboor) {
117+
if ($vertexNeighboor === $ignore) {
118+
// ignore source vertex only once
119+
$ignore = null;
120+
continue;
121+
}
122+
$this->getVerticesSubtreeRecursive($vertexNeighboor, $vertices, $vertex);
123+
}
124+
}
125+
126+
/**
127+
* get neighboor vertices for given start vertex
128+
*
129+
* @param Vertex $vertex
130+
* @throws UnexpectedValueException for directed edges
131+
* @return Vertex[] (might include possible duplicates)
132+
* @uses Vertex::getEdges()
133+
* @uses Edge::getVertexToFrom()
134+
* @see Vertex::getVerticesEdge()
135+
*/
136+
private function getVerticesNeighboor(Vertex $vertex)
137+
{
138+
$vertices = array();
139+
foreach ($vertex->getEdges() as $edge) {
140+
/* @var Edge $edge */
141+
if (!($edge instanceof UndirectedEdge)) {
142+
throw new UnexpectedValueException('Directed edge encountered');
143+
}
144+
$vertices[] = $edge->getVertexToFrom($vertex);
145+
}
146+
return $vertices;
147+
}
82148
}

tests/Tree/UndirectedTest.php

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
<?php
2+
3+
use Fhaculty\Graph\Graph;
4+
5+
use Fhaculty\Graph\Algorithm\Tree\Undirected;
6+
7+
class UndirectedTest extends TestCase
8+
{
9+
protected function createTree(Graph $graph)
10+
{
11+
return new Undirected($graph);
12+
}
13+
14+
public function testGraphEmpty()
15+
{
16+
$graph = new Graph();
17+
18+
$tree = $this->createTree($graph);
19+
20+
$this->assertTrue($tree->isTree());
21+
$this->assertSame(array(), $tree->getVerticesInternal());
22+
$this->assertSame(array(), $tree->getVerticesLeaf());
23+
}
24+
25+
public function testGraphTrivial()
26+
{
27+
$graph = new Graph();
28+
$graph->createVertex('v1');
29+
30+
$tree = $this->createTree($graph);
31+
$this->assertTrue($tree->isTree());
32+
$this->assertSame(array(), $tree->getVerticesInternal());
33+
$this->assertSame(array(), $tree->getVerticesLeaf());
34+
}
35+
36+
public function testGraphSimplePair()
37+
{
38+
// v1 -- v2
39+
$graph = new Graph();
40+
$graph->createVertex('v1')->createEdge($graph->createVertex('v2'));
41+
42+
$tree = $this->createTree($graph);
43+
$this->assertTrue($tree->isTree());
44+
$this->assertSame(array(), $tree->getVerticesInternal());
45+
$this->assertSame($graph->getVertices(), $tree->getVerticesLeaf());
46+
}
47+
48+
public function testGraphSimpleLine()
49+
{
50+
// v1 -- v2 -- v3
51+
$graph = new Graph();
52+
$graph->createVertex('v1')->createEdge($graph->createVertex('v2'));
53+
$graph->getVertex('v2')->createEdge($graph->createVertex('v3'));
54+
55+
$tree = $this->createTree($graph);
56+
$this->assertTrue($tree->isTree());
57+
$this->assertSame(array($graph->getVertex('v2')), array_values($tree->getVerticesInternal()));
58+
$this->assertSame(array($graph->getVertex('v1'), $graph->getVertex('v3')), array_values($tree->getVerticesLeaf()));
59+
}
60+
61+
public function testGraphPairParallelIsNotTree()
62+
{
63+
// v1 -- v2 -- v1
64+
$graph = new Graph();
65+
$graph->createVertex('v1')->createEdge($graph->createVertex('v2'));
66+
$graph->getVertex('v1')->createEdge($graph->getVertex('v2'));
67+
68+
$tree = $this->createTree($graph);
69+
$this->assertFalse($tree->isTree());
70+
}
71+
72+
public function testGraphLoopIsNotTree()
73+
{
74+
// v1 -- v1
75+
$graph = new Graph();
76+
$graph->createVertex('v1')->createEdge($graph->getVertex('v1'));
77+
78+
$tree = $this->createTree($graph);
79+
$this->assertFalse($tree->isTree());
80+
}
81+
82+
public function testGraphCycleIsNotTree()
83+
{
84+
// v1 -- v2 -- v3 -- v1
85+
$graph = new Graph();
86+
$graph->createVertex('v1')->createEdge($graph->createVertex('v2'));
87+
$graph->getVertex('v2')->createEdge($graph->createVertex('v3'));
88+
$graph->getVertex('v3')->createEdge($graph->getVertex('v1'));
89+
90+
$tree = $this->createTree($graph);
91+
$this->assertFalse($tree->isTree());
92+
}
93+
94+
public function testGraphDirectedIsNotTree()
95+
{
96+
// v1 -> v2
97+
$graph = new Graph();
98+
$graph->createVertex('v1')->createEdgeTo($graph->createVertex('v2'));
99+
100+
$tree = $this->createTree($graph);
101+
$this->assertFalse($tree->isTree());
102+
}
103+
104+
public function testGraphMixedIsNotTree()
105+
{
106+
// v1 -- v2 -> v3
107+
$graph = new Graph();
108+
$graph->createVertex('v1')->createEdge($graph->createVertex('v2'));
109+
$graph->getVertex('v2')->createEdgeTo($graph->createVertex('v3'));
110+
111+
$tree = $this->createTree($graph);
112+
$this->assertFalse($tree->isTree());
113+
}
114+
}

0 commit comments

Comments
 (0)