◂ 25 décembre 2023
Snowverload : Trouver les trois pseudo-isthmes d'un graphe (trois arêtes qui séparent le graphe en deux parties connexes si on les enlève)
ssr: mkc zdr
ccn: hpf drf dnr
bpf: krh stt
lhf: zpv
⋮
- code.py
import itertools, math
from collections import defaultdict # pratique, mais on risque de ne pas voir qu'une valeur qui aurait dû être initialisée ne l'a pas été
Sommet = str
Aretes = dict[Sommet,set[Sommet]]
Arete = tuple[Sommet,Sommet]
f = open("input.txt", 'r', encoding='utf-8')
lines = [line[:-1] for line in f.readlines()]
MAX_DIST = len(lines) ** 2
aretes:Aretes = defaultdict(set)
for line in lines:
sommet, *voisins = line.split()
sommet = sommet[:-1]
aretes[sommet].update(voisins)
for v in voisins:
aretes[v].add(sommet)
sommets = list(aretes.keys())
""" Statistiquement, les isthmes d'un graphe suffisamment grand doivent être les plus utilisés quand on passe d'un sommet à un autre
par le chemin le plus court. On gardera les 5 passages les plus usités"""
### Il aurait été plus propre d'utiliser dist:dict[Arete,int] et dist[u,v] au lieu de dist[u][v]
### mais c'était 4 fois plus lent.
### Calculer les chemins entre toutes les paires de points (prend du temps, mais heureusement en O(n³) avec Floyd-Warshall
def floyd_warshall() -> tuple[dict[Sommet,dict[Sommet,int]], dict[Sommet,dict[Sommet,Sommet]]]:
dist: dict[Sommet,dict[Sommet,int]] = defaultdict(lambda:defaultdict(lambda:MAX_DIST))
prev: dict[Sommet,dict[Sommet,Sommet]] = defaultdict(defaultdict)
for u,t in aretes.items():
for v in t:
dist[u][v] = 1 # The weight of the edge (u, v)
dist[v][u] = 1 # The weight of the edge (u, v)
prev[u][v] = u
prev[v][u] = v
for v in sommets:
dist[v][v] = 0
prev[v][v] = v
n = 0
for k in sommets:
n += 1
print(f" {n}/{len(sommets)}...")
for i in sommets:
for j in sommets:
if dist[i][j] > dist[i][k] + dist[k][j]:
dist[i][j] = dist[i][k] + dist[k][j]
prev[i][j] = prev[k][j]
return dist,prev
def get_path(u:Sommet, v:Sommet, prev:dict[Sommet,dict[Sommet,Sommet]]) -> list[Sommet]:
if prev[u][v] is None:
return []
path = [v]
while not u == v:
v = prev[u][v]
path.append(v)
return path[::-1]
_, calcul_origines = floyd_warshall()
### Calculer les passages les plus fréquentés
nb_passages: dict[Arete,int] = defaultdict(int)
for u,v in itertools.combinations(sommets, 2):
for s1,s2 in itertools.pairwise(get_path(u, v, calcul_origines)):
if s1>s2:
nb_passages[(s2,s1)] += 1
else:
nb_passages[(s1,s2)] += 1
nb_passages = sorted(list(nb_passages.items()), key=lambda x:x[1], reverse=True)
print("Nb d'utilisations des passages les plus fréquentés:", [v for _,v in nb_passages[:10]])
### Vérifier si les passages fréquentés coupent bien en deux parties distinctes
def nb_comp_connexes(sommets:list[Sommet], aretes:Aretes, interdits:tuple[Arete,...]) -> list[int]:
sommets = set(sommets)
nb_cc: list[int] = []
while sommets:
a_traiter = set([sommets.pop()])
done = set()
while a_traiter:
s = a_traiter.pop()
done.add(s)
for voisin in aretes[s]:
if (s,voisin) in interdits or (voisin,s) in interdits:
continue
if voisin not in done:
a_traiter.add(voisin)
sommets -= set([voisin])
nb_cc.append(len(done))
return nb_cc
### ...quitte à utiliser après d'autres passages un peu moins fréquentés que les trois premiers.
interdits: tuple[Arete,...]
for interdits in itertools.combinations([arete for arete,_ in nb_passages[:10]], 3):
nb_cc = nb_comp_connexes(sommets, aretes, interdits)
if len(nb_cc) > 1:
texte_nb_cc = list(map(str, nb_cc))
texte_nb_cc[-2] += " et " + texte_nb_cc[-1]
texte_nb_cc = ', '.join(texte_nb_cc[:-1])
print("Trouvé parties connexes de taille:", texte_nb_cc)
print("Réponse partie 1:", math.prod(nb_cc))
# Pas besoin d'un break, c'est rapide et on termine pour vérifier l'unicité de la réponse