最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

swift - Algorithm to find the root of a directed graph using Union-Find? - Stack Overflow

programmeradmin0浏览0评论

I'm trying to write an algorithm that finds the (optional) tree root from a graph structure (SwiftGraph), that could represent any graph (not necessarily a tree). If a root is found the algorithm should return its index in the vertices list, otherwise it should return null.

This is my attempt:

Union-find by rank with path compression:

fileprivate class UnionFind {
    private var parent: [Int]
    private var rank: [Int]
    
    init(elements: [Int]) {
        self.parent = [Int].init(repeating: 0, count: elements.count)
        self.rank = [Int].init(repeating: 0, count: elements.count)
        
        self.makeSet(elements)
    }

    private final func makeSet(_ vertices: [Int]) {
        for i in 0..<vertices.count {
            parent[i] = i
            rank[i] = 0
        }
    }

    func find(_ x: Int) -> Int {
        if parent[x] != x {
            parent[x] = find(parent[x]) // Path compression
        }
        return parent[x]
    }

    func union(_ x: Int, _ y: Int) {
        let xRoot = find(x)
        let yRoot = find(y)
        
        if xRoot == yRoot {
            return
        }
        
        if rank[xRoot] < rank[yRoot] {
            parent[xRoot] = yRoot
        } else if rank[xRoot] > rank[yRoot] {
            parent[yRoot] = xRoot
        } else {
            parent[yRoot] = xRoot
            rank[xRoot] += 1
        }
    }
}

SwiftGraph extension:

extension Graph {
    /// If this graph is a valid tree, this algorihtm finds and returns the index of the root vertex in `self.vertices`.
    ///
    /// May require further testing.
    ///
    /// - Returns: The index of the root of the tree, if this `self` is a tree, `nil` otherwise. If the graph is a degenerate tree,
    /// i.e. an empty tree, this function returns nil.
    ///
    /// - Complexity: **Time:** O(V + E⋅α(V)) where α(V) is the inverse Ackermann function, that grows extremely slowly (it is considered
    /// constant for any common practical application). **Memory:** O(V) to create the sets for Union-Find by rank with path compression.
    public func findTreeRoot() -> Int? {
        let unionFind = UnionFind(elements: [Int](self.vertices.indices))
        
        for vertex in 0..<self.vertices.count {
            for neighbor in self.edges[vertex] {
                let rootVertex = unionFind.find(vertex)
                let rootNeighbor = unionFind.find(neighbor.v)
                
                if rootVertex == rootNeighbor {
                    return nil
                }
                
                unionFind.union(rootVertex, rootNeighbor)
            }
        }

        var roots = Set<Int>()
        for vertex in 0..<self.vertices.count {
            roots.insert(unionFind.find(vertex))
        }
    
        if roots.count == 1 {
            return roots.first
        } else {
            return nil
        }
    }
}

Unfortunately though there are situations where graph.findTreeRoot() returns the wrong vertex index, that is, it returns the index of a vertex that's not the root of the tree.

What am I doing wrong here?

EDIT

An example of tests that fails:

let originalGraph = WeightedGraph<String, Float>()
        
for component in ["topbar", "secondary topbar", "gallery", "toolbar", "captions"] {
    let _ = originalGraph.addVertex(component)
}
                
originalGraph.addEdge(fromIndex: 0, toIndex: 1, weight: 1.2, directed: true)
originalGraph.addEdge(fromIndex: 0, toIndex: 2, weight: 0.5, directed: true)
originalGraph.addEdge(fromIndex: 0, toIndex: 3, weight: 1.1, directed: true)
originalGraph.addEdge(fromIndex: 0, toIndex: 4, weight: 1.3, directed: true)
        
originalGraph.addEdge(fromIndex: 1, toIndex: 0, weight: 1.1, directed: true)
originalGraph.addEdge(fromIndex: 1, toIndex: 2, weight: 0.5, directed: true)
originalGraph.addEdge(fromIndex: 1, toIndex: 3, weight: 1.5, directed: true)
originalGraph.addEdge(fromIndex: 1, toIndex: 4, weight: 2.2, directed: true)

originalGraph.addEdge(fromIndex: 3, toIndex: 2, weight: 1.0, directed: true)
originalGraph.addEdge(fromIndex: 3, toIndex: 4, weight: 1.0, directed: true)
        
assert(originalGraph.findTreeRoot() == nil)

let msaOfTopbar = try originalGraph.msa(root: 1)
        
let msaAsGraph = WeightedGraph<String, Float>(vertices: originalGraph.vertices)
for edge in msaOfTopbar {
    msaAsGraph.addEdge(edge, directed: true)
}
        
assert(msaAsGraph.findTreeRoot() == 1) // assertion failed: 0 != 1

I'm trying to write an algorithm that finds the (optional) tree root from a graph structure (SwiftGraph), that could represent any graph (not necessarily a tree). If a root is found the algorithm should return its index in the vertices list, otherwise it should return null.

This is my attempt:

Union-find by rank with path compression:

fileprivate class UnionFind {
    private var parent: [Int]
    private var rank: [Int]
    
    init(elements: [Int]) {
        self.parent = [Int].init(repeating: 0, count: elements.count)
        self.rank = [Int].init(repeating: 0, count: elements.count)
        
        self.makeSet(elements)
    }

    private final func makeSet(_ vertices: [Int]) {
        for i in 0..<vertices.count {
            parent[i] = i
            rank[i] = 0
        }
    }

    func find(_ x: Int) -> Int {
        if parent[x] != x {
            parent[x] = find(parent[x]) // Path compression
        }
        return parent[x]
    }

    func union(_ x: Int, _ y: Int) {
        let xRoot = find(x)
        let yRoot = find(y)
        
        if xRoot == yRoot {
            return
        }
        
        if rank[xRoot] < rank[yRoot] {
            parent[xRoot] = yRoot
        } else if rank[xRoot] > rank[yRoot] {
            parent[yRoot] = xRoot
        } else {
            parent[yRoot] = xRoot
            rank[xRoot] += 1
        }
    }
}

SwiftGraph extension:

extension Graph {
    /// If this graph is a valid tree, this algorihtm finds and returns the index of the root vertex in `self.vertices`.
    ///
    /// May require further testing.
    ///
    /// - Returns: The index of the root of the tree, if this `self` is a tree, `nil` otherwise. If the graph is a degenerate tree,
    /// i.e. an empty tree, this function returns nil.
    ///
    /// - Complexity: **Time:** O(V + E⋅α(V)) where α(V) is the inverse Ackermann function, that grows extremely slowly (it is considered
    /// constant for any common practical application). **Memory:** O(V) to create the sets for Union-Find by rank with path compression.
    public func findTreeRoot() -> Int? {
        let unionFind = UnionFind(elements: [Int](self.vertices.indices))
        
        for vertex in 0..<self.vertices.count {
            for neighbor in self.edges[vertex] {
                let rootVertex = unionFind.find(vertex)
                let rootNeighbor = unionFind.find(neighbor.v)
                
                if rootVertex == rootNeighbor {
                    return nil
                }
                
                unionFind.union(rootVertex, rootNeighbor)
            }
        }

        var roots = Set<Int>()
        for vertex in 0..<self.vertices.count {
            roots.insert(unionFind.find(vertex))
        }
    
        if roots.count == 1 {
            return roots.first
        } else {
            return nil
        }
    }
}

Unfortunately though there are situations where graph.findTreeRoot() returns the wrong vertex index, that is, it returns the index of a vertex that's not the root of the tree.

What am I doing wrong here?

EDIT

An example of tests that fails:

let originalGraph = WeightedGraph<String, Float>()
        
for component in ["topbar", "secondary topbar", "gallery", "toolbar", "captions"] {
    let _ = originalGraph.addVertex(component)
}
                
originalGraph.addEdge(fromIndex: 0, toIndex: 1, weight: 1.2, directed: true)
originalGraph.addEdge(fromIndex: 0, toIndex: 2, weight: 0.5, directed: true)
originalGraph.addEdge(fromIndex: 0, toIndex: 3, weight: 1.1, directed: true)
originalGraph.addEdge(fromIndex: 0, toIndex: 4, weight: 1.3, directed: true)
        
originalGraph.addEdge(fromIndex: 1, toIndex: 0, weight: 1.1, directed: true)
originalGraph.addEdge(fromIndex: 1, toIndex: 2, weight: 0.5, directed: true)
originalGraph.addEdge(fromIndex: 1, toIndex: 3, weight: 1.5, directed: true)
originalGraph.addEdge(fromIndex: 1, toIndex: 4, weight: 2.2, directed: true)

originalGraph.addEdge(fromIndex: 3, toIndex: 2, weight: 1.0, directed: true)
originalGraph.addEdge(fromIndex: 3, toIndex: 4, weight: 1.0, directed: true)
        
assert(originalGraph.findTreeRoot() == nil)

let msaOfTopbar = try originalGraph.msa(root: 1)
        
let msaAsGraph = WeightedGraph<String, Float>(vertices: originalGraph.vertices)
for edge in msaOfTopbar {
    msaAsGraph.addEdge(edge, directed: true)
}
        
assert(msaAsGraph.findTreeRoot() == 1) // assertion failed: 0 != 1
Share Improve this question edited Apr 6 at 7:01 Olivier 18.4k1 gold badge11 silver badges31 bronze badges asked Dec 18, 2024 at 10:56 Baffo rastaBaffo rasta 1151 gold badge5 silver badges25 bronze badges 8
  • 1 I don't really understand the problem. If your graph is a tree, finding its root is really trivial (it's the node that has no parent). If your graph is not a tree, there's no such thing as a root. Wouldn't it be easier for you to simply determine whether your graph is a tree or not first ? (if all nodes have at most one incoming edge, and if the graph is connected and has no cycle, then it is a tree). – m.raynal Commented Dec 18, 2024 at 15:36
  • @m.raynal: Of course I'm aware of your solution, but inspecting the inbound edges of a vertex takes O(E) raising the time to O(E^2+V) which I don't want to. – Baffo rasta Commented Dec 18, 2024 at 15:42
  • "but inspecting the inbound edges of a vertex takes O(E)" I guess that's a consequence of this decision to store all nodes' edges in a single array that needs to be filtered down repeatedly. That seems like an odd way to represent it; does it has some benefits I might not know about? – Alexander Commented Dec 18, 2024 at 18:53
  • 2 @Alexander: It follows from the fact that I'm creating an extension for a library that I don't own, that is, I don't have a say on this. – Baffo rasta Commented Dec 18, 2024 at 23:33
  • 2 Moreover, 'counting' the number of inbound edges for every vertex of the graph is cheaper, only O(E) time and O(V) space. Just create an integer array of size V, visit all your edges and increment the counter of the edge source. – m.raynal Commented Dec 19, 2024 at 9:39
 |  Show 3 more comments

2 Answers 2

Reset to default 0

I think @m.raynal has answered the question, but perhaps this will help you along in implementing it.

        // Pseudo-swift (I don't know swift)

        let numEdgesStartingHere = //( integer array with self.vertices.count elements; initialize all to 0 )
        let numEdgesEndingHere  = //( integer array with self.vertices.count elements; initialize all to 0 )
        for vertex in 0..<self.vertices.count {
            for neighbor in self.edges[vertex] {
                let edgeStart = vertex
                let edgeEnd = neighbor
                numEdgesStartingHere[edgeStart]++
                numEdgesEndingHere[edgeEnd]++

​Union-Find is designed for undirected graphs to manage disjoint sets and detect cycles. It doesn't account for edge direction, which is crucial in directed graphs. Therefore, using Union-Find to find roots in a directed graph isn't appropriate. Instead, consider computing the in-degree of each node; nodes with an in-degree of zero are potential roots.

发布评论

评论列表(0)

  1. 暂无评论