programmi veloci su grafi piccoli
diventano lenti su grafi grandi
esistono modi più rapidi
ordinamento dei nodi
tale che a precede b
se nel grafo c'è l'arco (a,b)
problema di esempio
modo lento e modo veloce di trovarlo
se il grafo contiene un arco (a,b)
allora a deve venire prima di b
sequenza.index(a) < sequenza.index(b)
se il grafo contiene un arco (a,b)
ma a non viene prima di b
allora l'ordine non e' topologico
sequenza.index(a) > sequenza.index(b) ⇒ ordine non topologico
# verifica un ordinamento topologico def verificatopologico(grafo, sequenza): for a,b in grafo.insiemearchi(): if sequenza.index(a) > sequenza.index(b): return False else: return True
se due nodi non sono nella sequenza giusta
l'ordine non è topologico
se non è così per nessun arco
l'ordine è topologico
sequenza ordinata = permutazione
itertools.permutations(grafo.insiemenodi())
trova tutte le permutazioni di nodi
tutte le sequenze di nodi
prova:
print(*itertools.permutations(grafo1.insiemenodi()), sep = '\n')
# trova un ordinamento topologico con le permutazioni def topologicopermutazioni(grafo): for o in itertools.permutations(grafo.insiemenodi()): if verificatopologico(grafo, o): return o return None
per ogni permutazione di nodi:
verifica se è un ordine topologico
le permutazioni sono:
(1, 2, 3, 4, 5, 6) (1, 2, 3, 4, 6, 5) (1, 2, 3, 5, 4, 6) (1, 2, 3, 5, 6, 4) (1, 2, 3, 6, 4, 5) … (6, 5, 4, 1, 3, 2) (6, 5, 4, 2, 1, 3) (6, 5, 4, 2, 3, 1) (6, 5, 4, 3, 1, 2) (6, 5, 4, 3, 2, 1)
l'unico ordine topologico è l'ultimo
numero di permutazioni:
n! = n × (n-1) × (n-2) × … × 2 × 1
solo l'ultima permutazione è un ordine topologico
vanno viste tutte
numero di verifiche simile a un esponenziale
stesso ordine di grandezza
n × (n-1) × (n-2) × … × 2 × 1 n × (n) × (n) × … × n × n
grafi di 6, 10 e 20 nodi:
un grafo di venti nodi è anche relativamente piccolo
facile che un grafo abbia anche cento o più nodi
computer che svolge un milione di operazioni al secondo
grafi di 6, 10 e 20 nodi:
il primo nodo della sequenza non può avere predecessori
se ha un predecessore
allora il predecessore deve venire prima
e quindi il nodo non può essere il primo
un nodo senza predecessori può essere il primo nodo della sequenza
non ha predecessori
mettendolo per primo si rispetta l'ordine
il primo nodo della sequenza è un qualsiasi nodo senza precedecessori
se non ci sono, il grafo non ha un ordine topologico
nodi successivi?
unico senza predecessori: 1
il nodo 1 è il primo
il vincolo che deve precedere 6 è rispettato
si può eliminare il nodo 1
il 6 non ha predecessori
può essere il primo della sequenza
ed è l'unico che può essere il primo
il nodo 6 è il secondo della sequenza
tutti gli altri vengono dopo
non serve più fare niente per rispettare l'ordine di 6 con gli
altri
i nodi senza predecessori sono 2 e 5
ecc.
se il grafo contiene un arco (p,n) allora n non va bene
se non ci sono archi (p,n) allora n va bene
arco (a,b) = b ha un predecessore
non è il primo nodo dell'ordine
for (a,b) in grafo.insiemearchi(): if b == c: # c ha un predecessore break else: # c non ha predecessori
diverso allineamento degli else:
serve un nodo c senza predecessori
per ogni nodo del grafo, si verifica se è senza predecessori
se non c'è, non esiste un ordinamento topologico
for c in grafo.insiemenodi(): for (a,b) in grafo.insiemearchi(): if b == c: # c ha un predecessore break else: # c non ha predecessori else: # non esiste c senza predecessori
per ogni nodo del grafo, si verifica se è senza predecessori
se non c'è, non esiste un ordinamento topologico
# trova un ordinamento topologico per esclusioni successive def topologicoesclusioni(grafo): risultato = [] for c in grafo.insiemenodi(): for (a,b) in grafo.insiemearchi(): if b == c: # 1. c ha un predecessore # non va bene per la prima posizione break else: # 2. c non ha predecessori # va bene per la prima prima posizione risultato += [c] break else: # 3. nessun nodo e' privo di predecessori # non esiste ordinamento topologico return None return risultato
# trova un ordinamento topologico per esclusioni successive
def topologicoesclusioni(grafo):
risultato = []
for c in grafo.insiemenodi():
for (a,b) in grafo.insiemearchi():
if b == c:
# 1. c ha un predecessore
# non va bene per la prima posizione
break
else:
# 2. c non ha predecessori
# va bene per la prima prima posizione
risultato += [c]
nodi = grafo.insiemenodi() - {c}
archi = {(a,b) for (a,b) in grafo.insiemearchi() if a != c and b != c}
grafo = Grafo(nodi, archi)
break
else:
# 3. nessun nodo e' privo di predecessori
# non esiste ordinamento topologico
return None
return risultato
se c non ha predecessori:
viene aggiunto all'ordinamento
viene tolto dal grafo
dopo aver tolto il nodo senza predecessori
si ripete
fino a che l'ordine non è completo
deve avere tutti i nodi del grafo
# trova un ordinamento topologico per esclusioni successive def topologicoesclusioni(grafo): risultato = [] # finche' l'ordinamento non contiene tutti i nodi del grafo... lunghezza = len(grafo.insiemenodi()) while len(risultato) < lunghezza: for c in grafo.insiemenodi(): for (a,b) in grafo.insiemearchi(): if b == c: # 1. c ha un predecessore # non va bene per la prima posizione print('no ', c, ': ', (a,b), sep='') break else: # 2. c non ha predecessori # a. va bene per la prima prima posizione # b. va tolto dal grafo print('ok', c) risultato += [c] nodi = grafo.insiemenodi() - {c} archi = {(a,b) for (a,b) in grafo.insiemearchi() if a != c and b != c} grafo = Grafo(nodi, archi) print(grafo) break else: # 3. nessun nodo e' privo di predecessori # non esiste ordinamento topologico return None return risultato
si ripete dall'inizio
fino a che ci sono ancora nodi nel grafo
solo allora si termina
# trova un ordinamento topologico per esclusioni successive
def topologicoesclusioni(grafo):
risultato = []
# finche' l'ordinamento non contiene tutti i nodi del grafo...
lunghezza = len(grafo.insiemenodi())
while len(risultato) < lunghezza:
for c in grafo.insiemenodi():
for (a,b) in grafo.insiemearchi():
if b == c:
# 1. c ha un predecessore
# non va bene per la prima posizione
print('no ', c, ': ', (a,b), sep='')
break
else:
# 2. c non ha predecessori
# a. va bene per la prima prima posizione
# b. va tolto dal grafo
print('ok', c)
risultato += [c]
nodi = grafo.insiemenodi() - {c}
archi = {(a,b) for (a,b) in grafo.insiemearchi() if a != c and b != c}
grafo = Grafo(nodi, archi)
print(grafo)
break
else:
# 3. nessun nodo e' privo di predecessori
# non esiste ordinamento topologico
return None
return risultato
N = numero nodi
A = numero archi
totale:
N × (N × A + 1 + A)
grandezza totale grafo: N+A
N × (N × A + 1 + A) ∼ N3
numero di operazioni dei due algoritmi:
| grandezza grafo |
metodo con permutazioni |
metodo con esclusioni |
|---|---|---|
| 6 | 720 | 216 |
| 10 | 3628800 | 1000 |
| 20 | 2432902008176640000 | 8000 |
con sei nodi la differenza non è tanto
720 contro 216
con venti nodi è enorme
2432902008176640000 contro 8000
1000000 operazioni al secondo
| grandezza grafo |
metodo con permutazioni |
metodo con esclusioni |
|---|---|---|
| 6 | 0.000720 secondi | 0.000216 secondi |
| 10 | 3.6288 secondi | 0.001 secondi |
| 20 | 74010 anni | 0.008 secondi |
costo N × (N × A + 1 + A)
parte principale: N × (N × A)
si può ridurre N × A?
# trova un ordinamento topologico per esclusioni successive con insiemi di predecessori
def topologicoinverso(grafo):
# insiemi di predecessori
predecessori = {}
for n in grafo.insiemenodi():
predecessori[n] = set()
for (a,b) in grafo.insiemearchi():
predecessori[b] |= {a}
print('predecessori:', predecessori)
# ordine topologico
risultato = []
candidati = grafo.insiemenodi()
while candidati:
print(risultato)
for c in candidati:
if predecessori[c] <= set(risultato):
risultato += [c]
candidati -= {c}
break
else:
return None
return risultato
nodo senza predecessore:
costo N invece di N × A
complessivo: N × N invece di N × N × A
| grandezza grafo |
metodo con permutazioni |
metodo con esclusioni |
metodo con predecessori |
|---|---|---|---|
| 6 | 720 | 216 | 36 |
| 10 | 3628800 | 1000 | 100 |
| 20 | 2432902008176640000 | 8000 | 400 |
1000000 operazioni al secondo
| grandezza grafo |
metodo con permutazioni |
metodo con esclusioni |
metodo con predecessori |
|---|---|---|---|
| 6 | 0.000720 secondi | 0.000216 secondi | 0.000036 secondi |
| 10 | 3.6288 secondi | 0.001 secondi | 0.0001 secondi |
| 20 | 74010 anni | 0.008 secondi | 0.0004 secondi |
l'ultima cosa vale anche per percorso più breve:
controllare ogni permutazione ha tempo esponenziale
ma gli altri algoritmi sono veloci
copertura minima no
si conoscono solo algoritmi lenti, al momento
come classificare i problemi di questo tipo?
def ricerca_insieme_o_sequenza(grafo): for s in itertools.permutations/combinations(grafo): if verifica_insieme_o_sequenza(grafo, s) return s return None
diversi metodi usano questo schema:
i problemi di questo tipo sono nella classe NP
NP contiene quindi i problemi di trovare
una copertura minima,
un percorso più breve,
un ordine topologico
def ricerca_insieme_o_sequenza(grafo): for s in itertools.permutations/combinations(grafo): if verifica_insieme_o_sequenza(grafo, s) return s return None
la verifica richiede tempo lineare
circa N
numero esponenziale di permutazioni o combinazioni
numero operazioni complessivo dell'ordine di 2N
a volte si può fare di meglio
classe NP = esiste una soluzione con itertools + verifica
numero di operazioni 2N
se esiste anche un algoritmo con costo lineare, o quadratico, o cubico
il problema è anche nella classe P
soluzione efficente
copertura minima dove si trova?
trovare la copertura minima:
non esiste attualmente un algoritmo veloce
uno dei problemi più difficili della classe NP
probabilmente un algoritmo veloce non esiste