protected override int ExecuteTest() { if (NodeStates.FixedTargetNodeCount <= 1 || NodeStates.VariableTargetNodeCount > 0) { return(0); } var removedNodes = 0; var mst = new MinimalSpanningTree(NodeStates.FixedTargetNodeIndices.ToList(), DistanceLookup); mst.Span(StartNodeIndex); var maxEdgeDistance = mst.SpanningEdges.Max(e => e.Priority); for (var i = 0; i < SearchSpaceSize; i++) { if (NodeStates.IsTarget(i) || NodeStates.IsRemoved(i)) { continue; } // Theoretically, this can be sped up by using Voronoi partitions. The Voronoi base of i is the // terminal with the smallest distance to i by definition, so only the distance to that terminal // has to be checked. if (NodeStates.FixedTargetNodeIndices.All(t => DistanceLookup[i, t] >= maxEdgeDistance)) { EdgeSet.EdgesOf(i).ForEach(EdgeSet.Remove); NodeStates.MarkNodeAsRemoved(i); removedNodes++; } } return(removedNodes); }
private void RemoveNode(int index) { if (NodeStates.IsTarget(index)) { throw new ArgumentException("Target nodes can't be removed", "index"); } var neighbors = EdgeSet.NeighborsOf(index); switch (neighbors.Count) { case 0: break; case 1: EdgeSet.Remove(index, neighbors[0]); break; case 2: // Merge the two incident edges. var left = neighbors[0]; var right = neighbors[1]; var newWeight = EdgeSet[index, left].Weight + EdgeSet[index, right].Weight; EdgeSet.Remove(index, left); EdgeSet.Remove(index, right); if (newWeight <= DistanceLookup[left, right]) { EdgeSet.Add(left, right, newWeight); } break; default: throw new ArgumentException("Removing nodes with more than 2 neighbors is not supported", "index"); } NodeStates.MarkNodeAsRemoved(index); }
protected override int ExecuteTest() { var edges = new GraphEdge[SearchSpaceSize]; var removedNodes = 0; for (var i = 0; i < SearchSpaceSize; i++) { // This test only checks non-terminals. if (NodeStates.IsTarget(i)) { continue; } // Nodes with less than 3 neighbors are covered by DegreeTest // Nodes are limited to 7 neighbors because this test is exponential in the neighbor count. var neighbors = EdgeSet.NeighborsOf(i); if (neighbors.Count < 3 || neighbors.Count > 7) { continue; } // Cache the edges. They might be removed from EdgeSet when we need them. foreach (var neighbor in neighbors) { edges[neighbor] = EdgeSet[i, neighbor]; } // Check whether each subset satisfies the condition. var canBeRemoved = true; foreach (var subset in GetAllSubsets(neighbors)) { // Only subsets of at least size 3 are relevant. if (subset.Count < 3) { continue; } // Sum up the weights of all edges between the nodes of the subsets and i. var edgeSum = subset.Sum(j => edges[j].Weight); // Build the MST of the fully connected graph of the nodes in the subset with the bottleneck // Steiner distances as edge weights. var mst = new MinimalSpanningTree(subset, SMatrix); mst.Span(subset[0]); // Sum up the edge weights of the MST. var mstSum = mst.SpanningEdges.Sum(e => e.Priority); // The condition is only satisfied if edgeSum >= mstSum. if (edgeSum < mstSum) { canBeRemoved = false; break; } } if (!canBeRemoved) { continue; } // Remove i and replace its edges. foreach (var neighbor in neighbors) { // Remove the old edges between i and its neighbors. var edge = edges[neighbor]; EdgeSet.Remove(edge); // For each pair of neighbors create a new edge. foreach (var neighbor2 in neighbors) { if (neighbor >= neighbor2) { continue; } // The weight of the new edge between the two neighbors is the sum of their edge weights to i. var edge2 = edges[neighbor2]; var newEdgeWeight = edge.Weight + edge2.Weight; // Only add this edge if it wouldn't be removed by the Paths with many terminals test. if (newEdgeWeight <= SMatrix[neighbor, neighbor2]) { EdgeSet.Add(neighbor, neighbor2, newEdgeWeight); } } } NodeStates.MarkNodeAsRemoved(i); removedNodes++; } return(removedNodes); }
protected override int ExecuteTest() { var removedNodes = 0; var untested = new HashSet <int>(Enumerable.Range(0, SearchSpaceSize)); var dependentNodes = new Dictionary <int, List <int> >(); while (untested.Any()) { var i = untested.First(); untested.Remove(i); var neighbors = EdgeSet.NeighborsOf(i); if (!NodeStates.IsTarget(i)) { if (neighbors.Count == 1) { // Non target nodes with one neighbor can be removed. untested.Add(neighbors[0]); RemoveNode(i); removedNodes++; } else if (neighbors.Count == 2) { // Non target nodes with two neighbors can be removed and their neighbors // connected directly. untested.Add(neighbors[0]); untested.Add(neighbors[1]); RemoveNode(i); removedNodes++; } } else if (NodeStates.IsFixedTarget(i)) { if (neighbors.Count == 1) { var other = neighbors[0]; if (EdgeSet.NeighborsOf(other).Count > 2 || NodeStates.IsTarget(other)) { // Fixed target nodes with one neighbor can be merged with their neighbor since // it must always be taken. untested.Add(i); untested.Remove(other); untested.UnionWith(MergeInto(other, i)); removedNodes++; } else { // Node can only be merged once other has been processed. Other might be a dead end. Debug.Assert(untested.Contains(other)); dependentNodes.Add(other, i); } } else if (neighbors.Count > 1) { // Edges from one target node to another that are of minimum cost among the edges of // one of the nodes can be in any optimal solution. Therefore both target nodes can be merged. var minimumEdgeCost = neighbors.Min(other => DistanceLookup[i, other]); var minimumTargetNeighbors = neighbors.Where(other => DistanceLookup[i, other] == minimumEdgeCost && NodeStates.IsFixedTarget(other)); foreach (var other in minimumTargetNeighbors) { untested.Add(i); untested.Remove(other); untested.UnionWith(MergeInto(other, i)); removedNodes++; } } } List <int> dependent; if (dependentNodes.TryGetValue(i, out dependent)) { untested.UnionWith(dependent); } } return(removedNodes); }
/// <summary> /// Reduces the search space by removing nodes, removing edges and merging nodes. /// </summary> /// <returns>The nodes contained in the reduced search space.</returns> public IReadOnlyList <GraphNode> ReduceSearchSpace() { // Remove all non target nodes with 2 or less edges. (Steiner nodes always have more than 2 edges) var nodeCountBefore = _nodeStates.SearchSpaceSize; _nodeStates.SearchSpace = _nodeStates.SearchSpace.Where(n => n.Adjacent.Count > 2 || _nodeStates.IsTarget(n)).ToList(); Debug.WriteLine("First basic degree reduction:\n" + " removed nodes: " + (nodeCountBefore - _nodeStates.SearchSpaceSize) + " (of " + nodeCountBefore + " initial nodes)"); // Initialise the distance lookup. _data.DistanceLookup = new DistanceLookup(_nodeStates.SearchSpace); var distanceLookup = _data.DistanceLookup; // Distance indices were not set before. _nodeStates.ComputeFields(); // If a fixed target node is not connected to the start node, there obviously is no solution at all. if (_nodeStates.FixedTargetNodeIndices.Any(i => !distanceLookup.AreConnected(i, _data.StartNodeIndex))) { throw new GraphNotConnectedException(); } nodeCountBefore = _nodeStates.SearchSpaceSize; // Remove all unconnected nodes. _nodeStates.SearchSpace = distanceLookup.RemoveNodes(_nodeStates.SearchSpace.Where(n => !distanceLookup.AreConnected(n, StartNode))); Debug.WriteLine("Removed unconnected nodes:"); Debug.WriteLine(" removed nodes: " + (nodeCountBefore - _nodeStates.SearchSpaceSize)); // Even the basic degree reductions hurt the AdvancedSolver's performance. // Reenabling should be tested when other algorithm parts are changed/improved. // todo experiment with reductions and AdvancedSolver if (_nodeStates.VariableTargetNodeCount > 0) { return(_nodeStates.SearchSpace); } // Initialise node states and edges. Edges are calculated from the information in the // GraphNode. Adjacency information from GraphNode instances must not be used after this. _data.EdgeSet = ComputeEdges(); var initialNodeCount = _nodeStates.SearchSpaceSize; var initialEdgeCount = _data.EdgeSet.Count; Debug.WriteLine("Initial counts:\n" + " nodes: " + initialNodeCount + "\n" + " non targets: " + (_nodeStates.SearchSpaceSize - _nodeStates.TargetNodeCount) + "\n" + " variable targets: " + _nodeStates.VariableTargetNodeCount + "\n" + " fixed targets: " + _nodeStates.FixedTargetNodeCount + "\n" + " edges: " + initialEdgeCount); // Execute an initial DegreeTest. var degreeTest = new DegreeTest(_nodeStates, _data); var dummy = 0; degreeTest.RunTest(ref dummy, ref dummy); // Update _searchSpace, _edgeSet and _distanceLookup ContractSearchSpace(); // These values may become lower by merging nodes. Since the reductions based on these distance // don't help if there are many variable target nodes, it is not really worth it to always recalculate them. // It would either slow the preprocessing by like 30% or would need an approximation algorithm. _data.SMatrix = new BottleneckSteinerDistanceCalculator(distanceLookup).CalcBottleneckSteinerDistances(_nodeStates.FixedTargetNodeIndices); // The set of reduction test that are run until they are no longer able to reduce the search space. var tests = new List <SteinerReduction> { new FarAwayNonTerminalsTest(_nodeStates, _data), new PathsWithManyTerminalsTest(_nodeStates, _data), new NonTerminalsOfDegreeKTest(_nodeStates, _data), new NearestVertexTest(_nodeStates, _data) }; // Run every reduction test (each followed by a simple degree reduction test) until they are no longer able // to reduce the search space or 10 reduction rounds were executed. // (the 10 round limit is never actually reached from what I've seen) for (int i = 0; i < 10; i++) { var edgeElims = 0; var nodeElims = 0; foreach (var test in tests.Where(t => t.IsEnabled)) { if (!test.RunTest(ref edgeElims, ref nodeElims)) { test.IsEnabled = false; } degreeTest.RunTest(ref edgeElims, ref nodeElims); } Debug.WriteLine("Eliminations in round {0}:", i + 1); Debug.WriteLine(" removed nodes: " + nodeElims); Debug.WriteLine(" removed edges: " + edgeElims); // No test should be still enabled in this case so we can stop running them. if (edgeElims == 0) { break; } } // Calculate the final search space. ContractSearchSpace(); Debug.WriteLine("Final counts:\n" + " nodes: " + _nodeStates.SearchSpaceSize + " (of " + initialNodeCount + " after basic reductions)\n" + " non targets: " + (_nodeStates.SearchSpaceSize - _nodeStates.TargetNodeCount) + "\n" + " variable targets: " + _nodeStates.VariableTargetNodeCount + "\n" + " fixed targets: " + _nodeStates.FixedTargetNodeCount + "\n" + " edges: " + _data.EdgeSet.Count + " (of " + initialEdgeCount + " after basic reductions)"); return(_nodeStates.SearchSpace); }
/// <summary> /// Reduces the search space by removing nodes, removing edges and merging nodes. /// </summary> /// <returns>The nodes contained in the reduced search space.</returns> public SteinerPreprocessorResult ReduceSearchSpace() { // Remove all non target nodes with 2 or less edges. (Steiner nodes always have more than 2 edges) var nodeCountBefore = _nodeStates.SearchSpaceSize; _nodeStates.SearchSpace = _nodeStates.SearchSpace.Where(n => n.Adjacent.Count > 2 || _nodeStates.IsTarget(n)).ToList(); Debug.WriteLine("First basic degree reduction:\n" + " removed nodes: " + (nodeCountBefore - _nodeStates.SearchSpaceSize) + " (of " + nodeCountBefore + " initial nodes)"); // Initialize the distance lookup. _data.DistanceCalculator = new DistanceCalculator(_nodeStates.SearchSpace); var distanceLookup = _data.DistanceCalculator; #if PoESkillTree_EnableAlternativeFixedTargetNodeIndices foreach (var node in distanceLookup._nodes.Where(n => _nodeStates.TargetIds.Contains(n.Id)))//Update NodeIndexes { if (node.DistancesIndex == -1) #if DEBUG { Debug.WriteLine("Failed to index distance calculations for node:" + node.Id); #endif { _nodeStates.FixedTargetKeyedIndices.Remove(node.Id); //Remove from Dictionary if node has invalid index } #if DEBUG } #endif else { _nodeStates.FixedTargetKeyedIndices.AddOrUpdate(node.Id, node.DistancesIndex); } } #endif // Distance indices are set in ComputeFields() if PoESkillTree_DisableAlternativeFixedTargetNodeIndices is enabled.(old variation of the code otherwise Distance indice is updated in ReduceSearchSpace()) _nodeStates.ComputeFields(); // If a fixed target node is not connected to the start node, there obviously is no solution at all. if (_nodeStates.FixedTargetNodeIndices.Any(i => #if PoESkillTree_UseShortDistanceIndex _data.StartNodeIndex != null && #endif !distanceLookup.AreConnectedById(i, #if PoESkillTree_UseShortDistanceIndex (ushort) #endif _data.StartNodeIndex))) { throw new GraphNotConnectedException(); } nodeCountBefore = _nodeStates.SearchSpaceSize; // Remove all unconnected nodes. _nodeStates.SearchSpace = distanceLookup.RemoveNodes(_nodeStates.SearchSpace.Where(n => !distanceLookup.AreConnected(n, _data.StartNode))); Debug.WriteLine("Removed unconnected nodes:"); Debug.WriteLine(" removed nodes: " + (nodeCountBefore - _nodeStates.SearchSpaceSize)); // Even the basic degree reductions hurt the AdvancedSolver's performance. // Reenabling should be tested when other algorithm parts are changed/improved. // todo experiment with reductions and AdvancedSolver if (_nodeStates.VariableTargetNodeCount > 0) { return(CreateResult()); } // Initialize node states and edges. Edges are calculated from the information in the // GraphNode. Adjacency information from GraphNode instances must not be used after this. _data.EdgeSet = ComputeEdges(); var initialNodeCount = _nodeStates.SearchSpaceSize; var initialEdgeCount = _data.EdgeSet.Count; Debug.WriteLine("Initial counts:\n" + " nodes: " + initialNodeCount + "\n" + " non targets: " + (_nodeStates.SearchSpaceSize - _nodeStates.TargetNodeCount) + "\n" + " variable targets: " + _nodeStates.VariableTargetNodeCount + "\n" + " fixed targets: " + _nodeStates.FixedTargetNodeCount + "\n" + " edges: " + initialEdgeCount); // Execute an initial DegreeTest. var degreeTest = new DegreeTest(_nodeStates, _data); var dummy = 0; degreeTest.RunTest(ref dummy, ref dummy); // Update _searchSpace, _edgeSet and _distanceLookup ContractSearchSpace();//Potential bug here as Cache size decreases if distance indices not updated // These values may become lower by merging nodes. Since the reductions based on these distance // don't help if there are many variable target nodes, it is not really worth it to always recalculate them. // It would either slow the preprocessing by like 30% or would need an approximation algorithm. _data.SMatrix = new BottleneckSteinerDistanceCalculator(distanceLookup.DistanceLookup) .CalcBottleneckSteinerDistances(_nodeStates.FixedTargetNodeIndices); // The set of reduction test that are run until they are no longer able to reduce the search space. var tests = new List <SteinerReduction> { new FarAwayNonTerminalsTest(_nodeStates, _data), new PathsWithManyTerminalsTest(_nodeStates, _data), new NonTerminalsOfDegreeKTest(_nodeStates, _data), new NearestVertexTest(_nodeStates, _data) }; // Run every reduction test (each followed by a simple degree reduction test) until they are no longer able // to reduce the search space or 10 reduction rounds were executed. // (the 10 round limit is never actually reached from what I've seen) for (int i = 1; i < 11; ++i) { var edgeElims = 0; var nodeElims = 0; foreach (var test in tests.Where(t => t.IsEnabled)) { if (!test.RunTest(ref edgeElims, ref nodeElims)) { test.IsEnabled = false; } degreeTest.RunTest(ref edgeElims, ref nodeElims); } Debug.WriteLine("Eliminations in round {0}:", i); Debug.WriteLine(" removed nodes: " + nodeElims); Debug.WriteLine(" removed edges: " + edgeElims); // No test should be still enabled in this case so we can stop running them. if (edgeElims == 0) { break; } } // Calculate the final search space. ContractSearchSpace(); Debug.WriteLine("Final counts:\n" + " nodes: " + _nodeStates.SearchSpaceSize + " (of " + initialNodeCount + " after basic reductions)\n" + " non targets: " + (_nodeStates.SearchSpaceSize - _nodeStates.TargetNodeCount) + "\n" + " variable targets: " + _nodeStates.VariableTargetNodeCount + "\n" + " fixed targets: " + _nodeStates.FixedTargetNodeCount + "\n" + " edges: " + _data.EdgeSet.Count + " (of " + initialEdgeCount + " after basic reductions)"); return(CreateResult()); }
protected override int ExecuteTest() { var removedNodes = 0; var untested = new HashSet <int>(Enumerable.Range(0, SearchSpaceSize)); while (untested.Any()) { var i = untested.First(); untested.Remove(i); var neighbors = EdgeSet.NeighborsOf(i); if (!NodeStates.IsTarget(i)) { if (neighbors.Count == 1) { // Non target nodes with one neighbor can be removed. untested.Add(neighbors[0]); RemoveNode(i); removedNodes++; } else if (neighbors.Count == 2) { // Non target nodes with two neighbors can be removed and their neighbors // connected directly. untested.Add(neighbors[0]); untested.Add(neighbors[1]); RemoveNode(i); removedNodes++; } } else if (NodeStates.IsFixedTarget(i)) { if (neighbors.Count == 1) { var other = neighbors[0]; // Fixed target nodes with one neighbor can be merged with their neighbor since // it must always be taken. // If the neighbor is no target and of degree 1 or 2, it will be removed by the tests above, // after which this node will be processed again. if (EdgeSet.NeighborsOf(other).Count <= 2 && !NodeStates.IsTarget(other)) { continue; } // If this node is the only fixed target, the neighbor must not be merged because // if this is the only target, the neighbor and the rest of the tree will not be in the optimal solution, // if there are only variable target nodes, the neighbor will not be in the optimal solution if the variable nodes are not taken. if (NodeStates.FixedTargetNodeCount == 1) { continue; } untested.Add(i); untested.Remove(other); untested.UnionWith(MergeInto(other, i)); removedNodes++; } else if (neighbors.Count > 1) { // Edges from one target node to another that are of minimum cost among the edges of // one of the nodes can be in any optimal solution. Therefore both target nodes can be merged. var minimumEdgeCost = neighbors.Min(other => DistanceLookup[i, other]); var minimumTargetNeighbors = neighbors.Where(other => DistanceLookup[i, other] == minimumEdgeCost && NodeStates.IsFixedTarget(other)); foreach (var other in minimumTargetNeighbors) { untested.Add(i); untested.Remove(other); untested.UnionWith(MergeInto(other, i)); removedNodes++; } } } } return(removedNodes); }