Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1# -*- coding: utf-8 -*- 

2""" 

3@file 

4@brief Implémente un algorithme qui cherche le plus court chemin passant 

5par tous les noeuds d'un graphe (TSP). Applique un algorithme de Kruskal 

6puis cherche à améliorer le chemin localement. 

7Voir :ref:`l-tsp_kruskal`. La fonction principale est 

8@see fn tsp_kruskal_algorithm. 

9""" 

10import functools 

11import random 

12import math 

13import os 

14from pyquickhelper.loghelper import noLOG 

15from .tsp_bresenham import draw_line 

16from ..helpers.pygame_helper import wait_event, empty_main_loop 

17 

18 

19def construit_ville(n, x=1000, y=700): 

20 """ 

21 Tire aléatoirement *n* villes dans un carrée ``x * y``, on choisit 

22 ces villes de sortent qu'elles ne soient pas trop proches.""" 

23 # deux villes ne pourront pas être plus proches que mind 

24 mind = math.sqrt(x * x + y * y) / (n * 0.75) 

25 # liste vide 

26 lv = [] 

27 while n > 0: 

28 # on tire aléatoirement les coordonnées d'une ville 

29 xx = x * random.random() 

30 yy = y * random.random() 

31 # on vérifie qu'elle n'est pas trop proche d'aucune autre ville 

32 ajout = True 

33 for t in lv: 

34 d1 = t[0] - xx 

35 d2 = t[1] - yy 

36 d = math.sqrt(d1 * d1 + d2 * d2) 

37 if d < mind: 

38 ajout = False # ville trop proche 

39 # si la ville n'est pas trop proche des autres, on l'ajoute à la liste 

40 if ajout: 

41 lv.append((xx, yy)) 

42 n -= 1 # une ville en moins à choisir 

43 return lv 

44 

45 

46def distance_euclidian(p1, p2): 

47 """ 

48 Calcule la distance entre deux villes. 

49 """ 

50 x = p1[0] - p2[0] 

51 y = p1[1] - p2[1] 

52 return math.sqrt(x * x + y * y) 

53 

54 

55def vecteur_points(p1, p2): 

56 """ 

57 Retourne le vecteur entre les points *p1* et *p2*. 

58 """ 

59 return (p2[0] - p1[0], p2[1] - p1[1]) 

60 

61 

62def vecteur_norme(vec): 

63 """ 

64 Retourne la norme d'un vecteur. 

65 """ 

66 return math.sqrt(vec[0] * vec[0] + vec[1] * vec[1]) 

67 

68 

69def vecteur_cosinus(vec1, vec2): 

70 """ 

71 Retourne le cosinus entre deux vecteurs, 

72 utilise le produit scalaire. 

73 """ 

74 norm1 = vecteur_norme(vec1) 

75 norm2 = vecteur_norme(vec2) 

76 if norm1 == 0: 

77 return 1 

78 if norm2 == 0: 

79 return 1 

80 scal = vec1[0] * vec2[0] + vec1[1] * vec2[1] 

81 return scal / (norm1 * norm2) 

82 

83 

84def vecteur_sinus(vec1, vec2): 

85 """ 

86 Retourne le sinus entre deux vecteurs, 

87 utilise le produit vectoriel. 

88 """ 

89 norm1 = vecteur_norme(vec1) 

90 norm2 = vecteur_norme(vec2) 

91 if norm1 == 0: 

92 return 0 

93 if norm2 == 0: 

94 return 0 

95 scal = vec1[0] * vec2[1] - vec1[1] * vec2[0] 

96 return scal / (norm1 * norm2) 

97 

98 

99def oppose_vecteur(vec): 

100 """ 

101 retourne le vecteur opposé. 

102 """ 

103 return (-vec[0], -vec[1]) 

104 

105 

106def repartition_zone(villes, zone_taille, ask_zone=False): 

107 """ 

108 Répartit les villes en zones, retourne les villes rangées par zones, 

109 chaque éléments zones [z][k] contient : 

110 

111 - les coordonnées de la ville 

112 - ses coordonnées en zone, (zx, zy) 

113 - son indice dans la liste villes 

114 

115 La fonction retourne également le nombre de zones 

116 selon l'axe des abscisses et l'axe des ordonnées, 

117 retourne aussi le nombre de zones, si *ask_zone* est True, 

118 retourne un paramètre supplémentaire : *zone*. 

119 """ 

120 mx = min(v[0] for v in villes) 

121 my = min(v[1] for v in villes) 

122 X = max((v[0] - mx) / zone_taille for v in villes) 

123 Y = max((v[1] - my) / zone_taille for v in villes) 

124 X = int(X) + 1 

125 Y = int(Y) + 1 

126 

127 # attribution des zones 

128 zone = [] 

129 Zmax = 0 

130 for i in range(0, len(villes)): 

131 v = villes[i] 

132 x = int((v[0] - mx) / zone_taille) 

133 y = int((v[1] - my) / zone_taille) 

134 z = int(y * X + x) 

135 Zmax = max(z, Zmax) 

136 zone.append((z, v, (x, y), i)) 

137 

138 # rangement par zone 

139 Zmax += 1 

140 zones = [[] for i in range(0, Zmax)] 

141 for z in zone: 

142 zones[z[0]].append((z[1], z[2], z[3])) 

143 

144 if ask_zone: 

145 return zones, X, Y, mx, my, Zmax, zone 

146 return zones, X, Y, mx, my, Zmax 

147 

148 

149def voisinage_zone(z, Zmax, X, Y): 

150 """ 

151 Retourne la liste des voisins d'une zone *z* 

152 sachant qu'il y a *X* zones sur l'axe des abscisses 

153 et *Y* zones sur l'axe des ordonnées, 

154 *Zmax* est le nombre de zones, 

155 inclus *z* dans cette liste. 

156 """ 

157 x = z % X 

158 y = z // X 

159 voisin_ = [z] 

160 if x > 0: 

161 voisin_.append(z - 1) 

162 if x < X: 

163 voisin_.append(z + 1) 

164 if y > 0: 

165 voisin_.append(z - X) 

166 if y < Y: 

167 voisin_.append(z + X) 

168 if x > 0 and y > 0: 

169 voisin_.append(z - 1 - X) 

170 if x > 0 and y < Y: 

171 voisin_.append(z - 1 + X) 

172 if x < X and y > 0: 

173 voisin_.append(z + 1 - X) 

174 if x < X and y < Y: 

175 voisin_.append(z + 1 + X) 

176 voisin = [int(i) for i in voisin_ if Zmax > i >= 0] 

177 return voisin 

178 

179 

180def arbre_poids_minimal(villes, zone_taille, distance): 

181 """ 

182 Construit l'arbre de poids minimal, retourne une liste de 

183 listes, chaque sous-liste associée à une ville contient la liste des ses voisins, 

184 *zone_taille* permet de découper l'image en zones, 

185 les distances ne seront calculées que si 

186 deux éléments sont dans la même zone ou dans une zone voisine. 

187 

188 @param villes list of tuples (tuple = coordinates) 

189 @param zone_taille @see fn repartition_zone 

190 @param distance distance function which returns the distance between two 

191 elements 

192 @return list of lists: each sublist *r[i]* contains the indexes of 

193 neighbors of node *i* so that the whole graph is 

194 only one connected component 

195 """ 

196 

197 def tri_distance(u, v): 

198 if u[2] < v[2]: 

199 return -1 

200 elif u[2] > v[2]: 

201 return 1 

202 else: 

203 return 0 

204 

205 rz = repartition_zone(villes, zone_taille=zone_taille) 

206 zones, X, Y, mx, my, Zmax = rz[:6] 

207 

208 # calcul des distances 

209 li = [] 

210 for z in range(0, len(zones)): 

211 voisin = voisinage_zone(z, Zmax, X, Y) 

212 for v in zones[z]: 

213 for zz in voisin: 

214 for u in zones[zz]: 

215 d = distance(v[0], u[0]) 

216 li.append((v[2], u[2], d)) 

217 

218 # tri 

219 li = list(sorted(li, key=functools.cmp_to_key(tri_distance))) 

220 

221 # nombre de composantes connexes 

222 nb_comp = len(villes) 

223 

224 # indice de la composante d'une ville 

225 num_comp = [i for i in range(0, len(villes))] 

226 

227 # liste des voisins pour chaque ville 

228 arbre = [[] for i in range(0, len(villes))] 

229 

230 # liste des villes par composante connexe 

231 list_comp = [[i] for i in range(0, len(villes))] 

232 

233 while nb_comp > 1: 

234 iii = 0 

235 for c in li: 

236 iii += 1 

237 i, j = c[0], c[1] 

238 if num_comp[i] != num_comp[j]: 

239 # on relie les villes i et j car elles appartiennent 

240 # à des composantes connexes différentes 

241 arbre[i].append(j) # i est voisine de j 

242 arbre[j].append(i) # j est voisine de i 

243 cl = num_comp[i] # composante connexe restante 

244 # composante connexe à agréger à la précédente 

245 ki = num_comp[j] 

246 for k in list_comp[ki]: 

247 num_comp[k] = cl 

248 list_comp[cl].append(k) 

249 list_comp[ki] = [] 

250 nb_comp -= 1 # une composante connexe en moins 

251 

252 if nb_comp <= 1: 

253 break # il n'y a plus qu'une seule composante connexe, inutile de continuer 

254 

255 if nb_comp > 1: 

256 # it usually means that zone_taille is too small and some edges 

257 # we find lost connected components 

258 # so for these, assuming they are not too many 

259 # we look for the closest point outside the connected component 

260 first_count = min((len(l), i) 

261 for i, l in enumerate(list_comp) if len(l) > 0) 

262 comp = first_count[1] 

263 city = list_comp[comp][random.randint(0, len(list_comp[comp]) - 1)] 

264 # city is not the best choice, just a random one 

265 dist = min((distance(villes[city], v), i) for i, v in enumerate(villes) 

266 if city != i and num_comp[i] != num_comp[city]) 

267 li = [(city, dist[1])] 

268 

269 return dict(arbre=arbre, X=X, Y=Y, mx=mx, my=my, Zmax=Zmax) 

270 

271 

272def circuit_eulerien(villes, arbre, screen, pygame, fLOG): 

273 """ 

274 Définit un circuit eulérien, villes contient la liste des villes, 

275 tandis que arbre est une liste de listes, ``arbre[i]`` est la liste des villes 

276 connectées à la ville *i*, 

277 on suppose que arbre est un graphe de poids minimal non orienté, 

278 l'algorithme ne marche pas s'il existe des villes confondues, 

279 un circuit eulérien passe par tous les arêtes une et une seule fois. 

280 """ 

281 

282 # on choisit une ville qui est une extrémité et parmi celle-là on la 

283 # choisit au hasard 

284 has = [] 

285 for i in range(0, len(villes)): 

286 n = len(arbre[i]) 

287 if n == 1: 

288 has.append(i) 

289 

290 bm = random.randint(0, len(has) - 1) 

291 bm = has[bm] 

292 

293 # vecteur, le circuit eulérien contient 

294 # nécessairement 2 * len (villes) noeuds puisque c'est 

295 # le graphe eulérien d'un arbre de poids minimal non orienté 

296 vec = (1, 1) 

297 chemin = [bm] 

298 done = set() 

299 done.add(bm) 

300 iter = [] 

301 while len(done) < len(villes): 

302 iter.append(len(done)) 

303 if len(iter) % 1000 == 0: 

304 fLOG(" circuit_eulerien: iter={0} len(done)={1} len(villes)={2}".format( 

305 len(iter), len(done), len(villes))) 

306 if len(done) == iter[-1000]: 

307 # there is apparently something wrong 

308 break 

309 v = villes[bm] 

310 ma = - math.pi - 1 

311 bvec = vec 

312 opvec = oppose_vecteur(vec) 

313 bl = None 

314 for k in range(0, len(arbre[bm])): 

315 la = arbre[bm][k] 

316 vec2 = vecteur_points(v, villes[la]) 

317 if vec2 == (0.0, 0.0): 

318 # same point, we keep the same direction 

319 if la not in done: 

320 bl = la 

321 bvec = vec2 

322 # no need to go further if the points are equal 

323 break 

324 # we skip 

325 continue 

326 if opvec == vec2: 

327 angle = -math.pi 

328 else: 

329 cos = vecteur_cosinus(vec, vec2) 

330 sin = vecteur_sinus(vec, vec2) 

331 angle = math.atan2(sin, cos) 

332 if angle > ma: 

333 ma = angle 

334 bl = la 

335 bvec = vec2 

336 

337 if bl is not None: 

338 if bl not in done: 

339 chemin.append(bl) 

340 done.add(bl) 

341 bm = bl 

342 if bvec != (0.0, 0.0): 

343 vec = bvec 

344 else: 

345 # something is wrong (it might an issue with duplicated points) 

346 rows = [] 

347 for i, p in enumerate(villes): 

348 rows.append("p{0}: {1},{2}".format(i, p[0], p[1])) 

349 for i, c in enumerate(chemin): 

350 rows.append("c{0}: i={1} -> {2},{3}".format(i, 

351 c, villes[c][0], villes[c][1])) 

352 rows.append("bm={0} ma={1} bvec={2} vec={3} bl={4}".format( 

353 bm, ma, vec2, vec, bl)) 

354 rows.append("arbre[{0}]={1}".format(bm, arbre[bm])) 

355 rows.append("arbre[{0}]={1}".format( 

356 arbre[bm][0], arbre[arbre[bm][0]])) 

357 mes = "\n".join(rows) 

358 raise Exception("this case should not happen\n" + mes) 

359 

360 if len(done) < len(villes): 

361 # something is wrong (it might an issue with duplicated points) 

362 rows = [] 

363 for i, p in enumerate(villes): 

364 rows.append("p{0}: {1},{2}".format(i, p[0], p[1])) 

365 for i, c in enumerate(chemin): 

366 rows.append("c{0}: i={1} -> {2},{3}".format(i, 

367 c, villes[c][0], villes[c][1])) 

368 rows.append("bm={0} ma={1} bvec={2} vec={3} bl={4}".format( 

369 bm, ma, vec2, vec, bl)) 

370 mes = "\n".join(rows) 

371 raise Exception("circuit_eulerien cannot give a path:\n" + mes) 

372 

373 return chemin 

374 

375 

376def circuit_hamiltonien(chemin): 

377 """ 

378 Extrait un circuit hamiltonien depuis un circuit eulérien, 

379 passe par tous les sommets une et une seule fois. 

380 """ 

381 nb = max(chemin) + 1 

382 res = [] 

383 coche = [False for i in range(0, nb)] 

384 for c in chemin: 

385 if coche[c]: 

386 continue 

387 res.append(c) 

388 coche[c] = True 

389 return res 

390 

391 

392def equation_droite(p1, p2): 

393 """ 

394 retourne l'équation d'une droite passant par p1 et p2, 

395 ax + by + c = 0, retourne les coefficients a,b,c 

396 """ 

397 vec = vecteur_points(p1, p2) 

398 a = vec[1] 

399 b = - vec[0] 

400 c = - a * p1[0] - b * p1[1] 

401 return a, b, c 

402 

403 

404def evaluation_droite(a, b, c, p): 

405 """ 

406 L'équation d'une droite est : ``ax + by + c``, retourne la valeur 

407 de cette expression au point *p*. 

408 """ 

409 return a * p[0] + b * p[1] + c 

410 

411 

412def intersection_segment(p1, p2, p3, p4): 

413 """ 

414 Dit si les segments *[p1 p2]* et *[p3 p4]* ont une intersection, 

415 ne retourne pas l'intersection. 

416 """ 

417 # équation de la droite (p1 p2) 

418 a1, b1, c1 = equation_droite(p1, p2) 

419 a2, b2, c2 = equation_droite(p3, p4) 

420 s1 = evaluation_droite(a2, b2, c2, p1) 

421 s2 = evaluation_droite(a2, b2, c2, p2) 

422 s3 = evaluation_droite(a1, b1, c1, p3) 

423 s4 = evaluation_droite(a1, b1, c1, p4) 

424 return s1 * s2 <= 0 and s3 * s4 <= 0 

425 

426 

427def longueur_chemin(chemin, distance): 

428 """ 

429 Retourne la longueur d'un chemin. 

430 """ 

431 s = 0 

432 nb = len(chemin) 

433 for i in range(0, nb): 

434 s += distance(chemin[i], chemin[(i + 1) % nb]) 

435 return s 

436 

437 

438def retournement_essai(chemin, i, j, negligeable=1e-5, distance=None): 

439 """ 

440 Dit s'il est judicieux de parcourir le chemin entre les sommets *i* et *j* 

441 en sens inverse, si c'est judicieux, change le chemin et retourne True, 

442 sinon, retourne False, 

443 si *j < i*, alors la partie à inverser est 

444 *i*, *i+1*, *i+2*, ..., *len(chemin)*, 

445 *-1*, *0*, *1*, ..., *j*. 

446 """ 

447 

448 nb = len(chemin) 

449 if i == j: 

450 return False 

451 if j - i == -1: 

452 return False 

453 if j - i - nb == -1: 

454 return False 

455 

456 ia = (i - 1 + nb) % nb 

457 ja = (j + 1) % nb 

458 # arcs enlevés 

459 d_ia_i = distance(chemin[ia], chemin[i]) 

460 d_j_ja = distance(chemin[j], chemin[ja]) 

461 # arcs ajoutés 

462 d_ia_j = distance(chemin[ia], chemin[j]) 

463 d_i_ja = distance(chemin[i], chemin[ja]) 

464 # amélioration ? 

465 d = d_ia_j + d_i_ja - d_j_ja - d_ia_i 

466 if d >= -negligeable: 

467 return False 

468 

469 # si amélioration, il faut retourner le chemin entre les indices i et j 

470 jp = j 

471 if jp < i: 

472 jp = j + nb 

473 ip = i 

474 

475 while ip < jp: 

476 i = ip % nb 

477 j = jp % nb 

478 ech = chemin[i] 

479 chemin[i] = chemin[j] 

480 chemin[j] = ech 

481 ip = ip + 1 

482 jp = jp - 1 

483 

484 return True 

485 

486 

487def retournement(chemin, taille, fLOG, distance): 

488 """ 

489 Amélioration du chemin par un algorithme simple, 

490 utilise des retournements de taille au plus <taille>, 

491 retourne le nombre de modifications. 

492 """ 

493 

494 # traitement des petits retournements 

495 nb = len(chemin) 

496 nb_change = 1 

497 nbtout = 0 

498 retour = {} 

499 while nb_change > 0: 

500 nb_change = 0 

501 for t in range(1, taille + 1): 

502 retour[t] = 0 

503 for i in range(0, nb): 

504 j = (i + t) % nb 

505 b = retournement_essai(chemin, i, j, distance=distance) 

506 if b: 

507 retour[t] += 1 

508 nb_change += 1 

509 nbtout += nb_change 

510 fLOG("nombre de retournements %d longueur : \t %10.0f --- \t" 

511 % (nbtout, longueur_chemin(chemin, distance)), " --- : ", retour) 

512 return nbtout 

513 

514 

515def echange_position_essai(chemin, a, b, x, inversion, negligeable=1e-5, distance=None): 

516 """ 

517 Echange la place des villes ka et kb pour les placer entre les villes *i* et *i+1*, 

518 si inversion est True, on inverse également le chemin inséré, si inversion est False, 

519 on ne l'inverse pas, 

520 si cela améliore, déplace les villes et retourne True, sinon, retourne False. 

521 """ 

522 

523 nb = len(chemin) 

524 xa = (x + 1) % nb 

525 ka = (a - 1 + nb) % nb 

526 kb = (b + 1) % nb 

527 

528 if not inversion: 

529 

530 if x == ka: 

531 return False 

532 if x == kb: 

533 return False 

534 if xa == ka: 

535 return False 

536 if b < a: 

537 if a <= x <= b + nb: 

538 return False 

539 elif a <= x <= b: 

540 return False 

541 if b < a: 

542 if a <= x + nb <= b + nb: 

543 return False 

544 elif a <= x + nb <= b: 

545 return False 

546 

547 # arcs enlevés 

548 d_x_xa = distance(chemin[x], chemin[xa]) 

549 d_ka_a = distance(chemin[ka], chemin[a]) 

550 d_b_kb = distance(chemin[b], chemin[kb]) 

551 # arcs ajoutés 

552 d_ka_kb = distance(chemin[ka], chemin[kb]) 

553 d_x_a = distance(chemin[x], chemin[a]) 

554 d_b_xa = distance(chemin[b], chemin[xa]) 

555 

556 d = d_ka_kb + d_x_a + d_b_xa - d_x_xa - d_ka_a - d_b_kb 

557 if d >= -negligeable: 

558 return False 

559 

560 # villes à déplacer 

561 ech = [] 

562 bp = b 

563 if bp < a: 

564 bp = b + nb 

565 for i in range(a, bp + 1): 

566 ech.append(chemin[i % nb]) 

567 diff = bp - a + 1 

568 

569 xp = x 

570 if xp < b: 

571 xp += nb 

572 

573 for le in range(b + 1, xp + 1): 

574 ll = le % nb 

575 bp = (a + le - b - 1) % nb 

576 chemin[bp] = chemin[ll] 

577 

578 for le in range(0, len(ech)): 

579 chemin[(x + le - diff + 1 + nb) % nb] = ech[le] 

580 

581 return True 

582 

583 else: 

584 

585 if x == ka: 

586 return False 

587 if x == kb: 

588 return False 

589 if xa == ka: 

590 return False 

591 if b < a: 

592 if a <= x <= b + nb: 

593 return False 

594 elif a <= x <= b: 

595 return False 

596 if b < a: 

597 if a <= x + nb <= b + nb: 

598 return False 

599 elif a <= x + nb <= b: 

600 return False 

601 

602 # arcs enlevés 

603 d_x_xa = distance(chemin[x], chemin[xa]) 

604 d_ka_a = distance(chemin[ka], chemin[a]) 

605 d_b_kb = distance(chemin[b], chemin[kb]) 

606 # arcs ajoutés 

607 d_ka_kb = distance(chemin[ka], chemin[kb]) 

608 d_x_b = distance(chemin[x], chemin[b]) 

609 d_a_xa = distance(chemin[a], chemin[xa]) 

610 

611 d = d_ka_kb + d_x_b + d_a_xa - d_x_xa - d_ka_a - d_b_kb 

612 if d >= -negligeable: 

613 return False 

614 

615 # villes à déplacer 

616 ech = [] 

617 bp = b 

618 if bp < a: 

619 bp = b + nb 

620 for i in range(a, bp + 1): 

621 ech.append(chemin[i % nb]) 

622 ech.reverse() 

623 diff = bp - a + 1 

624 

625 xp = x 

626 if xp < b: 

627 xp += nb 

628 

629 for le in range(b + 1, xp + 1): 

630 ll = le % nb 

631 bp = (a + le - b - 1) % nb 

632 chemin[bp] = chemin[ll] 

633 

634 for le in range(0, len(ech)): 

635 chemin[(x + le - diff + 1 + nb) % nb] = ech[le] 

636 

637 return True 

638 

639 

640def dessin_arete_zone(chemin, taille_zone, X, Y): 

641 """ 

642 Retourne une liste de listes de listes, 

643 ``res[i][j]`` est une liste des arêtes passant près de la zone ``(x,y) = [i][j]``, 

644 si *k* in ``res[i][j]``, alors l'arête *k*, *k+1* est dans la zone *(i,j)*, 

645 *X* est le nombre de zones horizontalement, *Y* est le nombre de zones verticalement, 

646 *taille_zone* est la longueur du côté du carré d'une zone. 

647 """ 

648 res = [[[] for j in range(0, Y + 1)] for i in range(0, X + 1)] 

649 nb = len(chemin) 

650 mx = min(_[0] for _ in chemin) 

651 my = min(_[1] for _ in chemin) 

652 for i in range(0, nb): 

653 a = chemin[i] 

654 b = chemin[(i + 1) % nb] 

655 x1, x2 = int( 

656 (a[0] - mx) // taille_zone), int((b[0] - mx) // taille_zone) 

657 y1, y2 = int( 

658 (a[1] - my) // taille_zone), int((b[1] - my) // taille_zone) 

659 line = draw_line(x1, y1, x2, y2) 

660 for x, y in line: 

661 res[x][y].append(i) 

662 return res 

663 

664 

665def voisinage_zone_xy(x, y, X, Y): 

666 """ 

667 Retourne la liste des voisins d'une zone *(x,y)* 

668 sachant qu'il y a *X* zones sur l'axe des abscisses 

669 et *Y* zones sur l'axe des ordonnées, 

670 inclus *z* dans cette liste 

671 """ 

672 voisin = [(x, y)] 

673 if x > 0: 

674 voisin.append((x - 1, y)) 

675 if x < X - 1: 

676 voisin.append((x + 1, y)) 

677 if y > 0: 

678 voisin.append((x, y - 1)) 

679 if y < Y - 1: 

680 voisin.append((x, y + 1)) 

681 if x > 0 and y > 0: 

682 voisin.append((x - 1, y - 1)) 

683 if x > 0 and y < Y - 1: 

684 voisin.append((x - 1, y + 1)) 

685 if x < X - 1 and y > 0: 

686 voisin.append((x + 1, y - 1)) 

687 if x < X - 1 and y < Y - 1: 

688 voisin.append((x + 1, y + 1)) 

689 return voisin 

690 

691 

692def echange_position(chemin, taille, taille_zone, X, Y, grande=0.5, fLOG=None, distance=None): 

693 """ 

694 Regarde si on ne peut pas déplacer un segment de longueur taille 

695 pour supprimer les arêtes les plus longues, 

696 au maximum <grande> longues arêtes, 

697 retourne le nombre de changement effectués, 

698 *X* est le nombre de zones horizontalement, 

699 *Y* est le nombre de zones verticalement, 

700 *taille_zone* est la longueur d'un côté du carré d'une zone. 

701 """ 

702 

703 nb = len(chemin) 

704 

705 def tri_arete(x, y): 

706 """pour trier la liste l par ordre décroissant""" 

707 if x[2] < y[2]: 

708 return 1 

709 elif x[2] > y[2]: 

710 return -1 

711 else: 

712 return 0 

713 

714 tmx = min(v[0] for v in chemin) 

715 tmy = min(v[1] for v in chemin) 

716 

717 # list des arêtes triés par ordre décroissant 

718 la = [] 

719 for i in range(0, nb): 

720 im = (i + 1) % nb 

721 la.append((i, im, distance(chemin[i], chemin[im]))) 

722 la = list(sorted(la, key=functools.cmp_to_key(tri_arete))) 

723 

724 # zone associée à chaque arête 

725 zone = dessin_arete_zone(chemin, taille_zone, X, Y) 

726 

727 dseuil = la[int(nb * grande)][2] 

728 nbtout = 0 

729 nb_change = 0 

730 iarete = 0 

731 retour = {} 

732 for t in range(1, taille + 1): 

733 retour[t] = 0 

734 

735 while iarete < nb: 

736 nb_change = 0 

737 arete = la[iarete] 

738 iarete += 1 

739 x = arete[0] 

740 xm = arete[1] 

741 a = chemin[x] 

742 b = chemin[xm] 

743 d = distance(a, b) 

744 if d < dseuil: 

745 break # arête trop petite 

746 

747 # zone traversée par la ligne 

748 x1, x2 = (int((a[0] - tmx) // taille_zone), 

749 int((b[0] - tmx) // taille_zone)) 

750 y1, y2 = (int((a[1] - tmy) // taille_zone), 

751 int((b[1] - tmy) // taille_zone)) 

752 ens = draw_line(x1, y1, x2, y2) 

753 ville = [] 

754 for k, l in ens: 

755 voisin = voisinage_zone_xy(k, l, X, Y) 

756 for u, v in voisin: 

757 ville.extend(zone[u][v]) 

758 

759 # on supprime les doubles 

760 ville.sort() 

761 if len(ville) == 0: 

762 continue 

763 sup = [] 

764 mx = -1 

765 for v in ville: 

766 if v == mx: 

767 sup.append(v) 

768 mx = v 

769 for s in sup: 

770 ville.remove(s) 

771 

772 # on étudie les possibilités de casser l'arête (x,xm) aux alentours des villes 

773 # comprise dans l'ensemble ville 

774 for t in range(1, taille + 1): 

775 

776 for i in ville: 

777 

778 # on essaye d'insérer le sous-chemin (x- t + 1 + nb) --> x 

779 # au milieu de l'arête i,i+1 

780 b = echange_position_essai( 

781 chemin, (x - t + 1 + nb) % nb, x, i, False, distance=distance) 

782 if b: 

783 nb_change += 1 

784 retour[t] += 1 

785 continue 

786 

787 # on essaye d'insérer le sous-chemin (xm+ t - 1) --> xm 

788 # au milieu de l'arête i,i+1 

789 b = echange_position_essai( 

790 chemin, (xm + t - 1) % nb, xm, i, False, distance=distance) 

791 if b: 

792 nb_change += 1 

793 retour[t] += 1 

794 continue 

795 

796 # on essaye de casser l'arête x,xm en insérant 

797 # le sous-chemin i --> (i+t) % nb 

798 b = echange_position_essai( 

799 chemin, i, (i + t) % nb, x, False, distance=distance) 

800 if b: 

801 nb_change += 1 

802 retour[t] += 1 

803 continue 

804 # idem 

805 b = echange_position_essai( 

806 chemin, i, (i + t) % nb, x, True, distance=distance) 

807 if b: 

808 retour[t] += 1 

809 nb_change += 1 

810 continue 

811 # idem 

812 b = echange_position_essai( 

813 chemin, (i - t + nb) % nb, i, x, False, distance=distance) 

814 if b: 

815 nb_change += 1 

816 retour[t] += 1 

817 continue 

818 # idem 

819 b = echange_position_essai( 

820 chemin, (i - t + nb) % nb, i, x, True, distance=distance) 

821 if b: 

822 retour[t] += 1 

823 nb_change += 1 

824 continue 

825 

826 nbtout += nb_change 

827 

828 fLOG("nombre de déplacements %d longueur : \t %10.0f --- \t" 

829 % (nbtout, longueur_chemin(chemin, distance=distance)), " --- : ", retour) 

830 return nbtout 

831 

832 

833def supprime_croisement(chemin, taille_zone, X, Y, fLOG, distance=None): 

834 """ 

835 Supprime les croisements d'arêtes, 

836 retourne le nombre de changement effectués, 

837 *X* est le nombre de zones horizontalement, 

838 *Y* est le nombre de zones verticalement, 

839 *taille_zone* est la longueur d'un côté du carré d'une zone 

840 """ 

841 

842 nb = len(chemin) 

843 tmx = min(v[0] for v in chemin) 

844 tmy = min(v[1] for v in chemin) 

845 

846 # zone associée à chaque arête 

847 zone = dessin_arete_zone(chemin, taille_zone, X, Y) 

848 nbtout = 0 

849 

850 for i in range(0, nb): 

851 im = (i + 1) % nb 

852 a = chemin[i] 

853 b = chemin[im] 

854 

855 # zone traversée par la ligne 

856 x1, x2 = (int((a[0] - tmx) // taille_zone), 

857 int((b[0] - tmx) // taille_zone)) 

858 y1, y2 = (int((a[1] - tmy) // taille_zone), 

859 int((b[1] - tmy) // taille_zone)) 

860 ens = draw_line(x1, y1, x2, y2) 

861 ville = [] 

862 for k, l in ens: 

863 voisin = voisinage_zone_xy(k, l, X, Y) 

864 for u, v in voisin: 

865 ville.extend(zone[u][v]) 

866 

867 # on supprime les doubles 

868 ville.sort() 

869 if len(ville) == 0: 

870 continue 

871 sup = [] 

872 mx = -1 

873 for v in ville: 

874 if v == mx: 

875 sup.append(v) 

876 mx = v 

877 for s in sup: 

878 ville.remove(s) 

879 

880 nb_change = 0 

881 for v in ville: 

882 b = retournement_essai(chemin, i, v, distance=distance) 

883 if b: 

884 nb_change += 1 

885 continue 

886 b = retournement_essai(chemin, im, v, distance=distance) 

887 if b: 

888 nb_change += 1 

889 continue 

890 

891 nbtout += nb_change 

892 

893 fLOG("nombre de croisements %d longueur : \t %10.0f --- \t" 

894 % (nbtout, longueur_chemin(chemin, distance=distance))) 

895 return nbtout 

896 

897 

898def amelioration_chemin(chemin, taille_zone, X, Y, taille=10, screen=None, 

899 fLOG=None, pygame=None, max_iter=None, images=None, 

900 distance=None): 

901 """ 

902 Amélioration du chemin par un algorithme simple, 

903 utilise des retournements de taille au plus *taille*, 

904 traite les arcs qui se croisent, 

905 traite les grands arcs, utilise un quadrillage de taille *window*, 

906 *X* est le nombre de zones horizontalement, 

907 *Y* est le nombre de zones verticalement, 

908 *taille_zone* est la longueur d'un côté du carré d'une zone. 

909 """ 

910 

911 white = 255, 255, 255 

912 

913 if pygame is not None and images is not None: 

914 images.append(screen.copy()) 

915 

916 # première étape rapide 

917 iter = 0 

918 nb = 1 

919 while nb > 0 and (max_iter is None or iter < max_iter): 

920 nb = retournement(chemin, taille, fLOG=fLOG, distance=distance) 

921 if screen is not None: 

922 screen.fill(white) 

923 display_chemin(chemin, 0, screen, pygame=pygame) 

924 pygame.display.flip() 

925 if images is not None: 

926 images.append(screen.copy()) 

927 empty_main_loop(pygame) 

928 iter += 1 

929 

930 # amélioration 

931 nb = 1 

932 while nb > 0 and (max_iter is None or iter < max_iter): 

933 nb = retournement(chemin, taille, fLOG=fLOG, distance=distance) 

934 if screen is not None: 

935 screen.fill(white) 

936 display_chemin(chemin, 0, screen=screen, pygame=pygame) 

937 pygame.display.flip() 

938 if images is not None: 

939 images.append(screen.copy()) 

940 empty_main_loop(pygame) 

941 nb += echange_position(chemin, taille // 2, 

942 taille_zone, X, Y, fLOG=fLOG, 

943 distance=distance) 

944 if screen is not None: 

945 screen.fill(white) 

946 display_chemin(chemin, 0, screen=screen, pygame=pygame) 

947 pygame.display.flip() 

948 if images is not None: 

949 images.append(screen.copy()) 

950 empty_main_loop(pygame) 

951 nb += supprime_croisement(chemin, taille_zone, 

952 X, Y, fLOG=fLOG, distance=distance) 

953 if screen is not None: 

954 screen.fill(white) 

955 display_chemin(chemin, 0, screen=screen, pygame=pygame) 

956 pygame.display.flip() 

957 if images is not None: 

958 images.append(screen.copy()) 

959 empty_main_loop(pygame) 

960 iter += 1 

961 

962 

963def tsp_kruskal_algorithm(points, size=20, length=10, max_iter=None, 

964 fLOG=noLOG, pygame=None, screen=None, images=None, 

965 distance=None): 

966 """ 

967 Finds the shortest path going through all points, 

968 points require to be a 2 dimensional space. 

969 

970 @param points list 2-tuple (X,Y) 

971 @param size the 2D plan is split into square zones 

972 @param length sub path 

973 @param max_iter max number of iterations 

974 @param pygame pygame for simulation 

975 @param screen with pygame 

976 @param fLOG logging function 

977 @param images save intermediate images 

978 @param distance distance function 

979 @return path 

980 

981 The distance is a function which takes two tuples and returns a distance:: 

982 

983 def distance(p1, p2): 

984 # ... 

985 return d 

986 

987 Les points identiques sont enlevés puis ajoutés à la fin. 

988 """ 

989 # verification 

990 if distance is None: 

991 distance = distance_euclidian 

992 unique = set() 

993 for point in points: 

994 if isinstance(point, list): 

995 raise TypeError("points cannot be list") 

996 unique.add(point) 

997 

998 # remove duplicates 

999 groups = {} 

1000 for p in points: 

1001 x, y = p[:2] 

1002 if (x, y) in groups: 

1003 groups[x, y].append(p) 

1004 else: 

1005 groups[x, y] = [p] 

1006 

1007 before = len(points) 

1008 points = [v[0] for v in groups.values()] 

1009 fLOG("[tsp_kruskal_algorithm] with no duplicates {0} <= {1}".format( 

1010 len(points), before)) 

1011 

1012 # begin of the algortihm 

1013 fLOG("[tsp_kruskal_algorithm] arbre_poids_minimal nb={0}".format( 

1014 len(points))) 

1015 di = arbre_poids_minimal(points, size, distance=distance) 

1016 arbre = di["arbre"] 

1017 X, Y = di["X"], di["Y"] 

1018 if screen is not None: 

1019 display_arbre(points, arbre, screen=screen, pygame=pygame) 

1020 pygame.display.flip() 

1021 if images is not None: 

1022 c = screen.copy() 

1023 for i in range(0, 5): 

1024 images.append(c) 

1025 fLOG("[tsp_kruskal_algorithm] circuit_eulerien X={0} Y={1}".format(X, Y)) 

1026 chemin = circuit_eulerien(points, arbre, screen, pygame, fLOG) 

1027 

1028 if len(chemin) != len(points): 

1029 raise Exception("The path should include all points: path:{0} points:{1}".format( 

1030 len(chemin), len(points))) 

1031 

1032 if screen is not None: 

1033 display_chemin([points[c] for c in chemin], 0, 

1034 screen=screen, pygame=pygame) 

1035 pygame.display.flip() 

1036 if images is not None: 

1037 c = screen.copy() 

1038 for i in range(0, 5): 

1039 images.append(c) 

1040 

1041 fLOG("[tsp_kruskal_algorithm] circuit_hamiltonien") 

1042 neurone = circuit_hamiltonien(chemin) 

1043 neurones = [points[i] for i in neurone] 

1044 if screen is not None: 

1045 display_chemin(neurones, 0, screen=screen, pygame=pygame) 

1046 fLOG("[tsp_kruskal_algorithm] amelioration_chemin") 

1047 amelioration_chemin(neurones, size, X, Y, length, screen, 

1048 fLOG=fLOG, pygame=pygame, max_iter=max_iter, 

1049 images=images, distance=distance) 

1050 

1051 # we add duplicates back 

1052 final = [] 

1053 for p in neurones: 

1054 x, y = p[:2] 

1055 g = groups[x, y] 

1056 if len(g) == 1: 

1057 final.append(p) 

1058 else: 

1059 final.extend(g) 

1060 return final 

1061 

1062 

1063def display_ville(villes, screen, bv, pygame): 

1064 """ 

1065 dessine les villes à l'écran 

1066 """ 

1067 color = 255, 0, 0 

1068 color2 = 0, 255, 0 

1069 for v in villes: 

1070 pygame.draw.circle(screen, color, (int(v[0]), int(v[1])), 3) 

1071 v = villes[bv] 

1072 pygame.draw.circle(screen, color2, (int(v[0]), int(v[1])), 3) 

1073 

1074 

1075def display_chemin(neurones, bn, screen, pygame): 

1076 """ 

1077 dessine le chemin à l'écran 

1078 """ 

1079 color = 0, 0, 255 

1080 color2 = 0, 255, 0 

1081 for n in neurones: 

1082 pygame.draw.circle(screen, color, (int(n[0]), int(n[1])), 3) 

1083 v = neurones[bn] 

1084 pygame.draw.circle(screen, color2, (int(v[0]), int(v[1])), 3) 

1085 if len(neurones) > 1: 

1086 pygame.draw.lines(screen, color, True, neurones, 2) 

1087 

1088 

1089def display_arbre(villes, arbre, mult=1, screen=None, pygame=None): 

1090 """ 

1091 dessine le graphe de poids minimal dꧩni par arbre 

1092 """ 

1093 if mult == 2: 

1094 color = 0, 255, 0 

1095 li = 4 

1096 else: 

1097 li = 1 

1098 color = 0, 0, 255 

1099 

1100 for i in range(0, len(villes)): 

1101 for j in arbre[i]: 

1102 v = (villes[i][0] * mult, villes[i][1] * mult) 

1103 vv = (villes[j][0] * mult, villes[j][1] * mult) 

1104 pygame.draw.line(screen, color, v, vv, li) 

1105 

1106 

1107def pygame_simulation(size=(800, 500), zone=20, length=10, max_iter=None, 

1108 nb=700, fLOG=noLOG, pygame=None, folder=None, 

1109 first_click=False, distance=None, flags=0): 

1110 """ 

1111 @param pygame module pygame 

1112 @param nb number of cities 

1113 @param first_click attend la pression d'un clic de souris avant de commencer 

1114 @param folder répertoire où stocker les images de la simulation 

1115 @param size taille de l'écran 

1116 @param delay delay between two tries 

1117 @param folder folder where to save images 

1118 @param first_click pause 

1119 @param fLOG logging function 

1120 @param distance distance function 

1121 @param flags see `pygame.display.set_mode <https://www.pygame.org/docs/ref/display.html#pygame.display.set_mode>`_ 

1122 @return @see fn tsp_kruskal_algorithm 

1123 

1124 La simulation ressemble à ceci : 

1125 

1126 .. raw:: html 

1127 

1128 <video autoplay="" controls="" loop="" height="250"> 

1129 <source src="http://www.xavierdupre.fr/enseignement/complements/tsp_kruskal.mp4" type="video/mp4" /> 

1130 </video> 

1131 

1132 Pour lancer la simulation:: 

1133 

1134 from ensae_teaching_cs.special.tsp_kruskal import pygame_simulation 

1135 import pygame 

1136 pygame_simulation(pygame) 

1137 

1138 Voir :ref:`l-tsp_kruskal`. 

1139 """ 

1140 pygame.init() 

1141 size = x, y = size[0], size[1] 

1142 white = 255, 255, 255 

1143 screen = pygame.display.set_mode(size, flags) 

1144 screen.fill(white) 

1145 points = construit_ville(nb, x, y) 

1146 

1147 if first_click: 

1148 wait_event(pygame) 

1149 

1150 images = [] if folder is not None else None 

1151 tsp_kruskal_algorithm(points, size=zone, length=length, max_iter=max_iter, 

1152 fLOG=fLOG, pygame=pygame, screen=screen, images=images, 

1153 distance=distance) 

1154 fLOG("folder", folder) 

1155 fLOG("images", len(images)) 

1156 

1157 if first_click: 

1158 wait_event(pygame) 

1159 

1160 if folder is not None: 

1161 fLOG("saving images") 

1162 for it, screen in enumerate(images): 

1163 if it % 10 == 0: 

1164 fLOG("saving image:", it, "/", len(images)) 

1165 image = os.path.join(folder, "image_%04d.png" % it) 

1166 pygame.image.save(screen, image)