简体   繁体   中英

Using a seen set for a directed graph vs. undirected graph

I've been deepening my understanding of algorithms for an undirected graph vs. undirected graph problems on LeetCode. The key difference I realized is for a problem like 841 Keys and Rooms because this is directed I need to add the "0" node to the seen set. Specifically this line early on:

seen_rooms.add(0)

On the other hand, for 547. Number of Provinces , because the graph is undirected I never needed to add it "early" on. I could have added it later in my loop

Problem 547:

class Solution():    
    def findCircleNum(self, A):
#Finds your neighboring cities 
        seen_cities = set()
        def find_cities(cur_city):
            nei_cities = A[cur_city]
            #First iter (0) nei city 
            #cur_city = 0 
            #find_cities (0) go through neighbors of 0
            #Don't need to add b/c this is going through it each time so when 1 -> 2 we would have had 1 <- 2 as well 
            # seen_cities.add(cur_city)
            for nei_city, can_go in enumerate(nei_cities):
                if can_go == 1 and nei_city not in seen_cities:
                    seen_cities.add(nei_city)
                    find_cities(nei_city)
                    
        #Go a DFS on all neighboring cities
        provinces = 0
        for city in range(len(A)):
            #We haven't visited the city 
            if city not in seen_cities:
                # seen_cities.add(city)
                find_cities(city)
                #After the above DFS I would have found all neighboring cities and increase it's province by 1 
                #Then go onto the next one's 
                provinces += 1 
            
        return provinces

Problem 841

class Solution:
    def canVisitAllRooms(self, rooms: List[List[int]]) -> bool:

        #canvisitallrooms
        #pos means you can visit such rooms 
        
        #This one is directed so u needa add it ahead of time 
        total_rooms = []
        #When adding to the stack we needa add to seen as well 
        stack = [0]
        seen_rooms = set()
        seen_rooms.add(0)
        
        #We never necessairly mentioned room 0 so we need to add room 0 since it STARTS there as well compared to another prob like 547
       #[[1],[2],[3],[]]
        while stack:
            cur_room = stack.pop()
            nei_rooms = rooms[cur_room]
            for nei_room in nei_rooms:
                if nei_room not in seen_rooms:
                    seen_rooms.add(nei_room)
                    stack.append(nei_room)

        return len(seen_rooms) == len(rooms)
    

Is the reason why it can be done like this for an undirected graph, ie not having to add the positions to the seen early on like I stated above, is that because it's undirected, we'll visit such path again and can add it to the seen set to prevent us from seeing it again? Whereas in a directed graph like keys and rooms, we won't every "visit" room 0 again potentially?

The reason for the visited set is essentially cycle avoidance, which is an issue in both directed and undirected graphs. Undirected graphs are simply a special case of directed graphs where every edge from A to B has an opposite edge from B to A , so you can have cycles. In "provinces", the adjacency matrix is defined with self-cycles for every node.

Why is it that sometimes you initialize a visited set ahead of time and sometimes later? It has nothing to do with directedness; it's mainly an implementation detail having to do with where you make checks for terminal states. For example, on "keys and rooms", both of the below solutions are valid. As long as prior visitation is tested before exploring and nothing is accidentally marked visited before pushing its neighbors, it's pretty much the same.

def canVisitAllRooms(self, rooms: List[List[int]]) -> bool:
    visited = set([0]) # <--
    stack = [0]
    
    while stack:
        for key in rooms[stack.pop()]:
            if key not in visited:
                visited.add(key)
                stack.append(key)
                
    return len(visited) == len(rooms)
def canVisitAllRooms(self, rooms: List[List[int]]) -> bool:
    visited = set()
    stack = [0]
    
    while stack:
        i = stack.pop()
        visited.add(i) # <--
        
        for key in rooms[i]:
            if key not in visited:
                stack.append(key)
                
    return len(visited) == len(rooms)

On "provinces" it's the same story--you can move the checks and set insertions around as long as you've made sure to explore everything once.

For example, my solution below (which I wrote without looking at your code) performs the visited check and marks the root node visited one time at the start of the recursive call. Your version makes two checks, one in the main loop iterating over all nodes and a second time inside the neighbor loop.

If you really wanted to, you could go a step further and "prime" the visited set in the caller before the recursive call, but it's not a particularly elegant approach because it spreads more of the recursive logic into two places.

def findCircleNum(self, adj_mat: List[List[int]]) -> int:
    visited = set()
    
    def explore(i):
        if i not in visited:
            visited.add(i)
        
            for j, x in enumerate(adj_mat[i]):
                if x:
                    explore(j)
            
            return True
        
    return len([i for i, _ in enumerate(adj_mat) if explore(i)])

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM