Aurreko mezuan , bikote guztien bide laburrenaren arazoa nola konpondu ikusi genuen erabiliz. Paralelismoa eta bektorizazioa erabiliz algoritmoaren errendimendua hobetzeko hainbat modu ere aztertu ditugu.
Hasteko, N
tamainako ( G
) grafiko bat N x N
tamainako ( W
) matrize gisa irudikatzen dugu, non W[i,j]
gelaxka bakoitzak i
erpinetik j
erpinera arteko ertz baten pisua duen (ikus 1. Irudia). Erpinen artean ertzerik ez dagoen kasuetan, gelaxka NO_EDGE
balio berezi batean ezartzen da (1. Irudian gelaxka ilun gisa agertzen da).
Hemendik aurrera, esaten dugu – W[i,j]
i
eta j
erpinen arteko distantzia dauka.
Ondoren, k
erpin bat hartu eta W[i,j]
erpin-pare guztietan zehar itertuko dugu i ⭢ k ⭢ j
distantzia gaur egun ezagutzen den i
eta j
arteko distantzia baino txikiagoa den egiaztatuz.
Baliorik txikiena W[i,j]
-n gordetzen da eta #3 urratsa hurrengo k
errepikatzen da G
ren erpin guztiak k
gisa erabili arte.
algorithm FloydWarshall(W) do for k = 0 to N - 1 do for i = 0 to N - 1 do for j = 0 to N - 1 do W[i,j] = min(W[i,j], W[i,k] + W[k,j]) end for end for end for end algorithm
Bukatutakoan, W
matrizearen W[i,j]
gelaxka bakoitzak i
eta j
erpinen arteko bide laburreneko distantzia edo NO_EDGE
balio bat dauka, haien artean biderik ez badago.
Floyd-Warshall algoritmoaren funtsa W[i,j]
distantzia ezaguneko konpartimentu bat da, i
-tik j
bitarteko k
bitarteko erpinetik zehar bide potentzial berriaren distantzia duena.
Hasieran, grafikoen ertz guztiak ezagutzen ditugu, eta horrek bide hauek ematen dizkigu: 0 ⭢ 1 :2
, 0 ⭢ 4 :10
, 1 ⭢ 3 :1
, 2 ⭢ 4 :1
, 3 ⭢ 2 :1
eta 3 ⭢ 4 :3
.
Aurreko argitalpeneko “tik” ⭢ “noraino” :”distantzia” formatua erabiltzen dut bideak idazteko.
- Egilearen oharra
Badakigu ere ez dagoela 0
erpinera daraman ertzerik, beraz k = 0
rako algoritmo bat exekutatzeak ez du zentzurik. Begi bistakoa da, gainera, ertz bakar bat dagoela ( 0 ⭢ 1
) 0
erpinetik 1
erpinera doana, eta horrek i != 0
guztiaren exekuzioa ere nahiko zentzugabe bihurtzen du ( i
-a hemengoa da) eta 1
erpinera delako. 2
eta 4
dituen ertzak ditu, ez du zentzurik 2
edo 4
ez den edozein j
ren algoritmoak exekutatzeko ( j
-a "to" da hemen).
Horregatik, gure lehen urratsa k = 1
, i = 0
eta j = 2,4
ren algoritmo bat exekutatzen aritzea izango da.
Urratsa | Bidea | Iruzkina |
---|---|---|
1 | 0 ⭢ 1 ⭢ 2 | Aurkitutako bidea. Distantzia = 3 (ez zen ezer) |
2 | 0 ⭢ 1 ⭢ 4 | Aurkitutako bidea. Distantzia = 8 (10 zen). |
Bi bide aurkitu ditugu: bide berri bat ( 0 ⭢ 1 ⭢ 2
) eta lasterbide bat ( 0 ⭢ 1 ⭢ 4
). Biak 1
erpinetik pasatzen dira. Ez badugu informazio hori ( 2
eta 4
1
iritsi garela oraintxe bertan) nonbait gordetzen, betirako galduko da (eta hori nahi dugunaren guztiz kontrakoa da).
Beraz, zer egin behar dugu? W
matrizea balio berriekin eguneratuko dugu (ikus 3a irudia) eta tamaina bereko R
matrize berri baten R[0,2]
eta R[0,4]
gelaxketan k
-ren balioa ere gordeko dugu ( k = 1
). W
matrize gisa baina NO_EDGE
balioekin hasieratuta (ikus 3b irudia).
Oraintxe bertan, ez zentratu R
matrizea zer den zehazki. Jarrai dezagun eta exekutatu hurrengo k = 2
algoritmoa.
Hemen, k = 1,
baina oraingoan, G
grafikoaren ordez W
matrizea erabiliko dugu. Begiratu W
matrizeari, batez ere 2. zutabean eta 2. errenkadan (4. irudia).
Hemen ikus dezakezu, 2
erpinerako bi bide daude 0
erpinetik eta 1
erpinetik (2. zutabea), eta bi bide daude 2
erpinetik 3
erpinera eta 4
erpinera (2. errenkada). Hori jakinda, zentzuzkoa da algoritmoa k = 2
, i = 0,1
eta j = 3,4
konbinazioetarako soilik exekutatzeko.
Urratsa | Bidea | Iruzkina |
---|---|---|
1 | 0 ⭢ 2 ⭢ 3 | Aurkitutako bidea. Distantzia = 4 (ez zen ezer) |
2 | 0 ⭢ 2 ⭢ 4 | Aurkitutako bidea. Distantzia = 6 (8 zen) |
3 | 1 ⭢ 2 ⭢ 3 | Aurkitutako bidea. Distantzia = 2 (ez zen ezer) |
4 | 1 ⭢ 2 ⭢ 4 | Aurkitutako bidea. Distantzia = 4 (6 zen) |
Lehenago egin dugun bezala, W[0,3]
, W[0,4]
, W[1,3]
, W[1,4]
gelaxkak eta R[0,3]
gelaxkekin eguneratzen ari gara. R[0,4]
, R[1,3]
eta R[1,4]
k = 2
(ikus 5. irudia).
k = 3
baino ez da geratzen prozesatzeko (grafikoan 4
erpinetik beste edozein erpinera doan ertzerik ez dagoelako).
Berriz ere, ikus dezagun W
matrizeari (6. irudia).
W
matrizearen arabera, 3
erpinerako hiru bide daude 0
, 1
eta 2
erpinetatik (3. zutabea), eta bide bat dago 3
erpinetik 4
erpinera (3. errenkada). Beraz, bide hauek ditugu prozesatzeko:
Urratsa | Bidea | Iruzkina |
---|---|---|
1 | 0 ⭢ 3 ⭢ 4 | Aurkitutako bidea. Distantzia = 5 (6 zen) |
2 | 1 ⭢ 3 ⭢ 4 | Aurkitutako bidea. Distantzia = 3 (4 zen) |
3 | 2 ⭢ 3 ⭢ 4 | Aurkitutako bidea. Distantzia = 2 (3 zen) |
Hau izan zen algoritmoaren azken iterazioa. W[0,4]
, W[1,4]
, W[2,4]
gelaxkak distantzia berriekin eguneratzea eta R[0,4]
, R[1,4]
, R[2,4]
gelaxkak ezartzea besterik ez da geratzen. R[2,4]
tik 3
.
Aurreko mezutik dakigunez, W
matrizeak bide laburrenen bikote guztiak ditu orain G
grafikoan. Baina zer gordetzen da R
matrizearen barruan?
Bide laburren berri bat aurkitzen genuen bakoitzean, R
matrizeari dagokion gelaxka k
uneko balioarekin eguneratzen ari ginen. Hasieran, ekintza honek misteriotsua dirudi, oso esanahi sinplea du: erpina gordetzen ari ginen, eta bertatik helmugako erpinera iritsi ginen: i ⭢ k ⭢ j
(hemen j
iristen gara k
zuzenean).
Momentu hau erabakigarria da. Etortzen garen erpin bat ezagutzeak bidea berreraikitzeko aukera ematen digulako atzeraka (otarrain bat bezala) j
erpinetik ("era") i
erpinera (""tik").
Hona hemen i
tik j
ibilbidea berreraikitzeko algoritmoaren testu-deskribapena:
X
matrize dinamiko hutsa.R[i,j]
gelaxkako z
balio bat irakurtzen.z
NO_EDGE
bada, i
tik j
rako ibilbidea aurkitzen da eta #7 urratsera jarraitu beharko genuke.z
X
matrize dinamiko bati.R[i,z]
gelaxkaren balioa z
.i
Xren aurretik.j
X
ri.X
array dinamikoak i
tik j
ibilbidea dauka.
- Egilearen oharra
algorithm RebuildRoute(i, j, R) x = array() z = R[i,j] while (z ne NO_EDGE) do x.prepend(z) z = R[i,z] end while x.prepend(i) x.append(j) return x end algorithm
Proba dezagun gure G
grafikoan eta berreraiki dezagun ibilbide bat 0
erpinetik 4
erpinera (ikus 8. irudia).
R[0,4]
ren balioa irakurtzen hasiko gara, eta horrek 3
lortzen du. Algoritmoaren arabera, horrek esan nahi du ibilbidea 4
erpinera doala 3
erpinetik (URDIZ ageri da).
R[0,4]
-ren balioa NO_EDGE
ez denez, jarraitu eta R[0,3]
-ren balioa irakurtzen dugu, 2
-n (BERDEZ erakusten da).
Berriz ere, R[0,3]
-ren balioa NO_EDGE
ez denez, jarraitu eta R[0,2]
-ren balioa irakurtzen dugu, 1
(GORRIZ ageri da).
Azkenik, R[0,1]
balio bat irakurriko dugu, eta horrek NO_EDGE
balioa ematen du, hau da, ez dago erpin gehiago ibilbidean laguntzen duten 0
eta 4
izan ezik. Beraz, ondoriozko ibilbidea hau da: 0 ⭢ 1 ⭢ 2 ⭢ 3 ⭢ 4
grafikoari erreparatuz gero 0
erpinetik 4
erpinerako biderik laburrenari dagokio.
Nola ziurtatu dezakegu R
matrizetik irakurtzen ditugun datu guztiak bide berekoak direla?
- Irakurle pentsakorra
Oso galdera ona da. Ziur gaude R
matrizea balio berri batekin eguneratzen dugulako W
matrizeko biderik laburrenaren balioa eguneratzen dugunean. Beraz, azkenean, R
matrizearen errenkada bakoitzak bide laburrenei lotutako datuak ditu. Ez gehiago, ez gutxiago.
Aurreko mezuan , Floyd-Warshall algoritmoaren jatorrizko bertsioa ezartzeaz gain, hainbat optimizazio ere integratu ditugu: grafiko eskasentzako euskarria, paralelismoa eta bektorializazioa, eta, azkenean, denak ere konbinatu ditugu.
Hedatu funtzioaren sinadura R
matrizea parametro bereizi gisa sartzeko - int[] routes
.
Gorde k routes
biderik laburrena aldatzen den bakoitzean (lerroak: 2 eta 14).
public void BaselineWithRoutes( int[] matrix, int[] routes, int sz) { for (var k = 0; k < sz; ++k) { for (var i = 0; i < sz; ++i) { for (var j = 0; j < sz; ++j) { var distance = matrix[i * sz + k] + matrix[k * sz + j]; if (matrix[i * sz + j] > distance) { matrix[i * sz + j] = distance; routes[i * sz + j] = k; } } } } }
Hedatu funtzioaren sinadura R
matrizea parametro bereizi gisa sartzeko - int[] routes
.
Iterazio bakoitzean, hasieratu k
balioko bektore berri bat (lerroa: 6).
Gorde k
balio bektorea routes
bide laburrena aldatzen den bakoitzean (lerroak: 31-32).
public void SpartialVectorOptimisationsWithRoutes( int[] matrix, int[] routes, int sz) { for (var k = 0; k < sz; ++k) { var k_vec = new Vector<int>(k); for (var i = 0; i < sz; ++i) { if (matrix[i * sz + k] == Constants.NO_EDGE) { continue; } var ik_vec = new Vector<int>(matrix[i * sz + k]); var j = 0; for (; j < sz - Vector<int>.Count; j += Vector<int>.Count) { var ij_vec = new Vector<int>(matrix, i * sz + j); var ikj_vec = new Vector<int>(matrix, k * sz + j) + ik_vec; var lt_vec = Vector.LessThan(ij_vec, ikj_vec); if (lt_vec == new Vector<int>(-1)) { continue; } var r_vec = Vector.ConditionalSelect(lt_vec, ij_vec, ikj_vec); r_vec.CopyTo(matrix, i * sz + j); var ro_vec = new Vector<int>(routes, i * sz + j); var rk_vec = Vector.ConditionalSelect(lt_vec, ro_vec, k_vec); rk_vec.CopyTo(routes, i * sz + j); } for (; j < sz; ++j) { var distance = matrix[i * sz + k] + matrix[k * sz + j]; if (matrix[i * sz + j] > distance) { matrix[i * sz + j] = distance; routes[i * sz + j] = k; } } } } }
Oroigarri txiki bat - Vector.ConditionalSelect
eragiketak bektore berri bat itzultzen du, non balioak sarrera-bektoreen dagozkien bi balioetatik txikiagoaren berdinak diren, hau da, lt_vec
bektorearen balioa -1
berdina bada, orduan eragiketak ij_vec
-tik balio bat hautatzen du, bestela, k_vec
tik balio bat hautatzen du.
- Egilearen oharra
Esperimentu guztiak aurreko mezuan erabilitako grafikoen multzoan exekutatu ziren: 300, 600, 1200, 2400 eta 4800 erpinak.
Iturburu-kodea eta grafiko esperimentalak GitHub-eko biltegian daude eskuragarri. Grafiko esperimentalak Data/Sparse-Graphs.zip
direktorioan aurki daitezke. Post honetako erreferentzia guztiak fitxategian ezartzen dira.
Jarraian, Baseline
eta BaselineWithRoutes
metodoak algoritmoaren jatorrizko bertsioarekin bat datozen eta SpartialVectorOptimisations
eta SpartialVectorOptimisationsWithRoutes
metodoak algoritmoaren bertsio bektorializatu bati dagozkio.
Metodoa | Tamaina | Batez bestekoa (ms) | Errorea (ms) | StdDev (ms) |
---|---|---|---|---|
Oinarrizko lerroa | 300 | 40.233 | 0,0572 | 0,0477 |
BaselineWithRoutes | 300 | 40.349 | 0,1284 | 0,1201 |
SpartialVectorOptimisations | 300 | 4.472 | 0,0168 | 0,0140 |
SpartialVectorOptimisationsWithRoutes | 300 | 4.517 | 0,0218 | 0,0193 |
Oinarrizko lerroa | 600 | 324.637 | 5.6161 | 4,6897 |
BaselineWithRoutes | 600 | 321.173 | 1.4339 | 1.2711 |
SpartialVectorOptimisations | 600 | 32.133 | 0,2151 | 0,1679 |
SpartialVectorOptimisationsWithRoutes | 600 | 34.646 | 0,1286 | 0,1073 |
Oinarrizko lerroa | 1200 | 2.656.024 | 6,9640 | 5,8153 |
BaselineWithRoutes | 1200 | 2.657.883 | 8.8814 | 7.4164 |
SpartialVectorOptimisations | 1200 | 361.435 | 2,5877 | 2.2940 |
SpartialVectorOptimisationsWithRoutes | 1200 | 381.625 | 3,6975 | 3.2777 |
Oinarrizko lerroa | 2400 | 21.059.519 | 38.2291 | 33,8891 |
BaselineWithRoutes | 2400 | 20.954.852 | 56,4719 | 50.0609 |
SpartialVectorOptimisations | 2400 | 3.029.488 | 12.1528 | 11.3677 |
SpartialVectorOptimisationsWithRoutes | 2400 | 3.079.006 | 8.6125 | 1918.7 |
Oinarrizko lerroa | 4800 | 164.869.803 | 547.6675 | 427,5828 |
BaselineWithRoutes | 4800 | 184.305. 980 | 210,9535 | 164,6986 |
SpartialVectorOptimisations | 4800 | 21.882.379 | 200.2820 | 177,5448 |
SpartialVectorOptimisationsWithRoutes | 4800 | 21.004.612 | 79,8752 | 70,8073 |
Honek oso nahasia dirudi (eta hala bada - eta ekin hau entzutea gomendatzen dizut, neurketetan zein gauza zailak eragin dezaketen hobeto ulertzeko). Nire iritzirik onena da portaera "nahasia" PUZaren cachearen errendimendu bikainak eragiten duela (grafikoak ez direlako nahiko handiak cacheak asetzeko). Partzialki, teoria hau " lodia " lagin batean oinarritzen da, non grafiko handienean degradazio nabarmena ikus dezakegun. Hala ere, ez nuen egiaztatu.
Oro har, erreferenteak erakusten du oso errendimendu handiko agertokiez eta grafiko erraldoiez ari ez bagara, ondo dago ibilbideen memorizazioa bi algoritmoetan lehenespenez integratzea (baina kontuan izan, memoria-kontsumoa bikoiztu egingo dela, behar dugulako). esleitu bi matrize W
eta R
baten ordez).
Ibilbideen berreraikitze algoritmoak C#-n inplementatzea erraza da eta ia guztiz islatzen du aurrez erakutsitako sasi-kodea ( LinkedList<T>
erabiltzen dugu matrize dinamikoa irudikatzeko):
public static IEnumerable<int> RebuildWithLinkedList( int[] routes, int sz, int i, int j) { var x = new LinkedList<int>(); var z = routes[i * sz + j]; while (z != Constants.NO_EDGE) { x.AddFirst(z); z = routes[i * sz + z]; } x.AddFirst(i); x.AddLast(j); return x; }
Goiko algoritmoa ez da perfektua eta hobetu daiteke zalantzarik gabe. Egin dezakegun hobekuntzarik errazena sz
tamainako array bat aurrez esleitu eta alderantzizko ordenan betetzea da:
public static IEnumerable<int> RebuildWithArray( int[] routes, int sz, int i, int j) { var x = new int[sz]; var y = sz - 1; // Fill array from the end x[y--] = j; var z = routes[i * sz + j]; while (z != Constants.NO_EDGE) { x[y--] = z; z = routes[i * sz + z]; } x[y] = i; // Create an array segment from 'y' to the end of the array // // This is required because 'sz' (and therefore length of the array x) // equals to the number of vertices in the graph // // So, in cases when route doesn't span through // all vertices - we need return a segment of the array return new ArraySegment<int>(x, y, sz - y); }
“Optimizazio” honek esleipen-kopurua batera murrizten duen arren, ez du zertan algoritmoa aurrekoa baino “azkarragoa” edo “memoria gutxiago esleitu” egiten. Kontua da i
tik j
bide bat ordenatuta eduki behar badugu, beti R
matrizetik ateratzen ditugun datuak “alderantzikatu” beharko ditugula, eta ez dago ihes egiteko modurik.
public static IEnumerable<int> RebuildWithReverseYield( int[] routes, int sz, int i, int j) { yield return j; var z = routes[i * sz + j]; while (z != Constants.NO_EDGE) { yield return z; z = routes[i * sz + z]; } yield return i; }
Kode hau "aldatu" edo "hobetu" daitekeen aldaera gehiago egon daitezke. Hemen une garrantzitsua gogoratzea da: edozein "aldaketak" alde txarrak ditu memoria edo CPU zikloei dagokienez.
fitxategian GitHub-en ibilbide-algoritmo guztien inplementazioak aurki ditzakezu.
Post honetan, Floyd-Warshall algoritmoaren atzean dagoen teorian sakondu dugu eta bide laburrenak "ibilbide" memorizatzeko aukerarekin zabaldu dugu. Ondoren, C# inplementazioak eguneratu ditugu (jatorrizkoak eta bektorializatuak) aurreko mezutik . Azkenean, algoritmoaren bertsio batzuk ezarri ditugu "ibilbideak" berreraikitzeko.