LeetCode-Problems / 0743. Network Delay Time / 0743. Network Delay Time.py
0743. Network Delay Time.py
Raw
from collections import defaultdict
import heapq

# well this is a basic directed graph question. 
# WITHOUT having studied this at all in Python, I'd start 
# by making the graph with a dict: the nodeID would be the key,
# and the val would be a list of tuples of (connected, send_time).
# We could add a thid val to this tuple, or create a copy of the dict,
# that tracks the current time to reach each node with a signal propogated
# from the given origin spot. Note, a signal may reach a node repeatedly, 
# so we only store the minimum. I guess each time we send a signal to a node
# that has not previously received it, we put its outputs on the stack to check
# if THEYVE all gotten it. Thus our signal is propogated. We could then iterate
# through our graph dict and look that A) all nodes are hit, and if so B) Max 
# time for sent signal (its additive as we store it). OR we could have created 
# a set of just the key nodes, and each time we reach one remove it from this set:
# if the set is non-empty by the time we stop propogating then we know our signal
# failed to reach all nodes. This is a tradeoff of time and space

# Likewise, we could have just kept a running MAX_TIME
# val as we propogate the signal, rather than traversing the list
class Solution:
    def networkDelayTime(self, times: List[List[int]], n: int, k: int) -> int:
        # Do we need to check for a base case? It looks like there could be just one
        # node, but then there is at least one time. How could there be a time from one node to
        # no other nodes? I guess it points to itself. So the initial send of the first signal 
        # DOESNT count as the first node receiving the signal? No self refernce edges... Constraints
        # are self contradictory and impossible to all be true at once... reported. 
        
        # graph = dict(x:[(y,z,0]) for x something something...) There may be a cool-ass generator but its beyond me
        
        # Build graph in dict and track all nodes.  
        # Almost certainly could be one thing, but I confused myself into claiming this made more sense
        graph = dict()
        node_times = dict()
        for x,y,z in times:
            if x not in node_times:
                node_times[x] = 6001 # could save MAXTIME, set to MAXTIME + 1
            if y not in node_times:
                node_times[y] = 6001  # to eliminate magic numbers    
            if x not in graph:
                graph[x] = [(y,z)]
            else:
                graph[x] += [(y,z)]
        visited = set([k])
        all_nodes = set(x+1 for x in range(n)) 
        # We CAN stop once we've seen all nodes as we are getting there fastest
        # No, we cant. We can stop adding things to the queue... sort of. 
        
        node_times[k] = 0
        stack = [k]
        # heapq.heapify(stack) 
        # This does nothing right? A heap is just a particularly ordered list in this case,
        # but an empty or trivial 1 element list is already a heap, its not like it denotes it
        # to be one. Its not a datatype, or structure, but a way of sorting one. As long as only
        
        while stack: # and visited != all_nodes:
            source = heappop(stack) #POP MIN PRIORITY QUEUE
            if source in graph:
                for target, cost in graph[source]:
                    if  node_times[source] + cost < node_times[target]:
                        visited.add(target)
                        node_times[target] = node_times[source] + cost
                        heappush(stack, target) #Push PRIORITY QUEUE VERSION
        
        return max(node_times.values()) if visited == all_nodes else -1








# While close, there is a fatal flaw here. I was capturing and eliminating
# visited nodes and saving the time for the FIRST signal to arrive there... but
# not the first signal in a time sense, only in an order traversal sense. The visited
# node may be directly linked to the root, but by a slow edge. It could be the signal 
# reaches this node after passing through dozens of others before that first edge signal
# arrives. We NEED to process the signals in speed order, not depth order. THIS is an excellent
# example for using a priority queue. Work throug the queue processing and inserting traversals
# by actual speed (curr_time + transit_time) is how to best simulate the process and efficiently
# track the structure. We can still keep a list of visited nodes, and use that to know when to stop
# processing early, but it as we are now checking "in real time", if we hit all the nodes we now know
# that the fastest paths were used to visit them all. Ties are friendly and do not need to be processed

#         # Build graph in dict and track all nodes.         
#         graph = dict()
#         visitednodes = dict()
#         for x,y,z in times:
#             visitednodes[x] = 0
#             visitednodes[y] = 0
#             if x not in graph:
#                 graph[x] = [(y,z)]
#             else:
#                 graph[x] += [(y,z)]
                
#         max_time = 0 
        
#         def send_sig(curr_node, curr_time):
#             if unvisitednodes:
#                 if curr_node in graph:
#                     for y,z in graph[curr_node]:
#                         if y in unvisitednodes:
#                             unvisitednodes.remove(y)
#                             send_sig(y, curr_time+z)
#                             max_time = max(max_time, curr_time)
                    
#         unvisitednodes.remove(k)
#         send_sig(k, 0)
        
#         return max_time if not visitednodes else -1