Distribucijska mreža
V nalogi bomo delali s koordinatami otrok, ki jih mora obiskati Miklavž. Kot je znano, je možakar že v letih, zato namerava postaviti distribucijske centre, v katere bodo jeleni dostavili robo, on pa jo bo distribuiral od tam naprej. V nalogi mu bomo pomagali določiti čim boljšo razpostavitev teh centrov.
Koordinate so opisane s terko, na primer (3, -1)
, če gre za
koordinate v dvodimenzionalnem prostoru, ali, morda,
(4, 1, -4, 0, 12)
, če gre za koordinate v petdimenzionalnem
prostoru. Števila dimenzij ne smete predpostaviti, tako da bo program
uporaben tudi za Miklavža v kakem drugem številu dimenzij. Pač pa lahko
predpostavite, da imajo vse točke, s katerimi pokličemo funkcijo, enako
število dimenzij.
Testi
Testi: testi-distribucijska-mreza.py
Za oceno 5 (ogrevalna)
Napišite funkcijo razdalja(s, t)
, ki vrne razdaljo med točkama
s
in t
.
Rešitev
Če bi bila (pa ni!) naloga sestavljena tako, da bi bile vse točke na ravnini, bi bilo to nekoliko preprosteje, napisali bi le
Če je število dimenzij drugačno - sploh pa ne znano vnaprej - bomo potrebovali zanko. Takole, prosim, ne delajte:
Dovolj ste stari, da veste, da je to predolgo in nepregledno ter da nima smisla, saj je preprosteje
Po novem pa znamo tudi krajše. Funkcija mora vrniti vsoto kvadratov razlik parov koordinat.
Za oceno 6
Napišite funkcijo najblizji(x, s)
, ki prejme koordinato otroka,
x
, in množico s koordinat (možnih) distribucijskih centrov,
s
. Funkcija mora vrniti koordinato tistega distribucijskega centra,
ki je najbližji otroku.
Z drugimi besedami, funkcija naj vrne tisto točko (torej terko) iz
seznama terk s
, ki je najbližja točki x
.
Rešitev
Preverimo torej, ali smo se naučili, kar ponavljamo že ves semester, iskanje najmanjšega (ali največjega, saj to je isto) elementa z običajno komplikacijo: vrniti moramo točko, primerjati pa njene razdalje.
No, če smo kaj veliko vadili, smo tole. Tako da bi morali rešitev stresti iz rokava:
Z generatorji? Da, ampak. Ni preveč lepo.
Sestavimo terke, ki vsebujejo razdaljo in točko. Poiščemo "najmanjšo terko";
ker jih najprej primerja po prvem elementu, bomo dobili terko, v kateri je
točka, ki je najmanj oddaljena. Tisti [1]
na koncu pa je potreben,
da bomo iz te terke vzeli le točko, ne pa tudi razdalje. Če bi jo izpustili,
bi testi vračali par (razdalj, točka), tega pa nočemo.
Oklevanje, češ, da ni prav lepo, izvira iz tega, da gre lepše, vendar bi se morali za to učiti o lambda-funkcijah.
Funkciji min
smo dali poiskati "najmanjšo" točko iz
s
, pri čemer naj jih primerja po "ključu"
razdalja
. Da smo fiksirali en argument (x
), smo
uporabili lambdo, česar še ne poznamo in pri Programiranju 1 niti ne bomo
spoznali.
Ko se boste nekateri na drugi stopnji učili o funkcijskih jezikih, pa boste znali to "fiksiranje" izvesti še krajše.
Temu, kar smo uporabili, se reče
currying, po logiku
Haskllu Curryju. Iz funkcije razdalja
smo s
partial(razdalja, x)
, naredili novo funkcijo, ki je enaka
funkciji razdalja
, le da en argument že ima.
Tega vam ni potrebno znati. Napisal sem kot zgled, da obstajajo v jezikih (predvsem v takšnih, ki nima spremenljivk, temveč vezave in, še pomembneje, so v njih funkcije "prvorazredni objekti", kar pomenim, da se da z njimi delati vse, kar se da delati z drugimi rečmi, recimo števili in nizi) še precej naprednejši koncepti od teh, mehanskih, ki se jih učimo tu. Če jih obvladate, postanejo programi precej enostavnejši (primerjajte prvo in zadnjo funkcijo), ne da bi bili zaradi tega bolj zapleteni. Če, seveda, te reči obvladate dovolj, da znate brati takšne programe.
Tule je še nekaj, kar je - samostojno? morda ne - odkrilo veliko študentov.
Vse avtorje takšnih rešitev bi z veseljem vprašal (in eno leto sem naredil nekaj podobnega), ali vedo, kaj njihov program pravzaprav dela. Ne počnite tega. Če ne veste, kako deluje, ne uporabite. Če prepišete rešitev, ki je niti ne razumete, se niste naučili ničesar. Domačo nalogo - ki je priložnost, da se urite v programiranju, kar je nekaj, kar boste potrebovali - ste vrgli stran. Norčujete se iz sebe, iz asistentov, iz mene.
Če že morate, pa napišite
da se ne bom spraševal, kako lahko nekdo razume vezane metode in jih pošilja kot argumente funkcijam, izpeljanega slovarja pa ne zna sestaviti.
Za oceno 7
Miklavž, kot je znano, ne gradi hiš, temveč skriva distribucijske centre
po podstrešjih hiš, v kateri živijo otroci. V tem koraku bomo predpostavili,
da imamo neko množico otrok s koordinatami, zbranimi v s
. Zanimalo
nas bo, pri katerem od teh otrok se mu najbolj splača postaviti distribucijski
center. Z drugimi besedami, radi bi poiskali otroka, ki stanuje najbolj na
najbolj "osrednjem" mestu.
Najprej napišite funkcijo pop_razdalja(x, s)
, ki vrne poprečno
razdaljo med otrokom x
in vsemi otroki iz s
. Pri
tem je x
spet terka (koordinate otroka) in s
množica s koordinatami (terkami) vseh otrok.
Točka (1, 1) je od (0, 0) oddaljena za 1.4142135623730951 (koren iz 1 ** 2 + 1 ** 2), od (10, 0) je oddaljena za 9.055385138137417 (koren iz 9 ** 2 + 1 ** 2), od (10, 10) pa za 12.727922061357855 (koren iz 9 ** 2 + 2 ** 2). Poprečje teh treh števil je 7.732506920622789.
Napišite tudi funkcijo center(s)
, ki vrne tisto točko iz
s
, katere poprečna razdalja do ostalih točk iz s
je najmanjša.
Namig: center(s)
naj pokliče pop_razdalja(x, s)
za vsako točko x
iz s
in si zapomni (ter vrne) tisti
x
, pri katerem je pop_razdalja
vrnila najmanjšo
vrednost. To, da naloga pravi "do ostalih točk" naj te ne vznemirja, saj bo
prispevek te točke (če jo boš pustil v s
) tako ali tako vedno
enak.
V gornjem primeru imamo pet točk - štiri so oglišča štirikotnika, peta pa je približno na sredi. Ker je le-ta v poprečju najmanj oddaljena od drugih, funkcija vrne le-to.
Rešitev
Pri poprečnih razdaljah tokrat res ne bomo izgubljali besed: vsoto razdalij delimo z njihovim številom.
Center je podoben tistemu, kar smo reševali v nalogi za oceno 6: tam smo iskali točko z najmanjšo razdaljo do določene točke, tu iščemo točko z najmanjšo poprečno razdaljo do skupine točk.
Vseeno bomo zastavili malenkost drugače.
Razlika je v tem, da smo si prej, v nalogi za oceno 6 zapomnili le "najboljšo" točko in vsakič sproti računali razdaljo do nje, tu pa si zapomnimo tudi poprečno razdaljo, da je ne bi bilo potrebno vedno znova računati.
Mimogrede, tudi min
, ki jo lahko pokličemo, namesto da se
ubijamo s svojo funkcijo, se obnaša ekonomično in z vsako točko le enkrat
pokliče pop_razdalja
.
Spremenimo pop_razdalja
tako, da bo štela, kolikokrat je bila
poklicana.
(Vem, da sem prepovedal uporabo in predvsem spreminjanje globalnih spremenljivk, ampak tole je za diagnostično rabo. ;)
Zdaj napišimo eno od krajših različic iskanja najmanjšega elementa (nič hudega, če je ne razumemo povsem).
Pokličimo jo.
Program izpiše 5, kar pomeni, da se funkcija pop_razdalja
res
pokliče za vsako točko le po enkrat.
Za oceno 8
Miklavž, ko je znano, gradi hiše, v katerih postavlja distribucijske centre. S tem ni težav, ker so te hiše nevidne. Hišo bo postavil v točki, ki je najbolj "na sredi" - v težišču.
Napišite funkcijo tezisce(s)
, ki vrne težišče točk v
s
. Težišče je točka, ki jo dobiš tako, da izračunaš
poprečno koordinato x, koordinato y, koordinato z ... in tako naprej vseh točk
v s
.
Prva številka je poprečje koordinat x, (0 + 10 + 10) / 3. Druga je poprečje koordinat y (0 + 0 + 10) / 3. Funkcija mora seveda delovati tudi za več dimenzij in več točk.
Rešitev
Tale je bila kar nadležna. V te namene navadno uporabljamo primerne knjižnice
(recimo numpy, v katerem bi nalogo rešili z
numpy.sum(tocke, axis=0) / len(tocke)
).
Da bo reč preglednejša, najprej pripravimo le funkcijo, ki sešteje dva vektorji (ali točki, če hočete).
Ali, krajše:
Zdaj pa težišče. Če ne znamo dobiti dimenzije prve točke (malo sem vas
zafrknil z množicami ;), v začetku postavimo vsoto na None
in
prvo točko le shranimo, namesto da bi jo prišteli.
Zadnji del, deljenje, je neroden. Seveda znamo boljše:
Druga možnost je, da nekako pridemo do dimenzije točk. Ena možnost je, da dobimo točko tako, da jo vzamemo iz množice - a brž porinemo nazaj vanjo.
Lahko pa čez množico naženemo zanko in jo takoj prekinemo.
Zdaj, ko imamo točko, bomo z [0] * len(tocka)
sestavili
primerno dolg seznam ničel in raje seštevali vanj.
Če bi znali kaj več, bi napisali
Za oceno 9
Zdaj pa predpostavimo, da je Miklavž že vzpostavil distribucijske centre. Vsakemu otroku bo prinesel robo iz tistega distribucijskega centra, ki je otroku najbližji.
Napiši funkcijo razdeli(xs, s)
, ki prejme dve množici točk.
Prva, xs
, je množica koordinat distribucijskih centrov.
Druga, s
, je množica koordinat otrok. Funkcija naj vrne
slovar, katerega ključi so točke iz xs
, vrednosti
pa podmnožice točk iz s
. Funkcija naj za vsako točko iz
s
(otroka) odkrije, katera od točk izmed xs
(distribucijski center) ji je najbližja in jo da v pripadajoči element
slovarja.
Recimo, da bi funkcijo poklicali z
V tem primeru mora funkcija vrniti slovar
Še en primer:
Funkcija je morala razdeliti otroke
{(5, 5), (4, 5), (5, 9), (6, 11), (-20, -20), (16, 5)}
med
distribucijske centre
{(5, 5), (5, 10), (15, 5), (40, 50)}
. Ne spreglejte, recimo, da
je (-20, -20) sicer daleč od vsega, vendar pač najbližja točki (5, 5).
Še bolj ne spreglejte, da je center (40, 50) tako daleč, da nima nobenega
oskrbovanca, saj so vsi otroci bližji kakemu drugemu centru.
Zdaj pa naredimo obratno: predpostavimo, da so otroci razdeljeni v
množice in za vsako množico moramo poiskati idealno lokacijo distribucijskega
centra. Napiši funkcijo poisci_centre(ss)
, ki prejme
seznam množic točk in vrne množico njihovih optimalnih centrov (takšnih, kot
jih vrača funkcija center
, po encenter za vsako množico iz
ss
).
Center prve množice je (5, 5), center druge je (4, 5) in tako naprej.
Rešitev
Pri razdeli bi lahko uporabili slovarje s privzetimi vrednostmi - a tokrat
naredimo malo drugače. Kaj bodo ključi tega slovarja, že vemo: vsi centri,
xs
. Sestavili bomo torej slovar, katerega ključi bodo centri,
vrednosti pa prazne množice, {x: set() for x in xs}
.
Nato le gremo prek vseh točk in vsako dodelimo najbližjemu centru, pri
čemer si pomagamo s funkcijo najblizji
.
Pri iskanju centrov pa res ne bomo zapletali s čimerkoli drugim kot z izpeljano množico.
Za oceno 10
Zdaj pa bomo končali nalogo: za dane koordinate otrok bomo določili optimalne lokacije distribucijskih centrov.
Napiši funkcijo nacrtuj(s, k)
, ki kot argument dobi seznam koordinat
vseh otrok, s
in število distribucijskih centrov, ki jih bo
Miklavž postavil, k
ter določi optimalne distribucijske centre
takole. Najprej postavi distribucijske centre kar na podstrešja prvih
k
otrok. Za vsakega od teh distribucijskih centrov določi, katere
množice otrok mu pripadajo (torej, kateri otroci so določenemu centru bližji
kot kateremukoli drugemu). Na ta način dobi množice otrok; vendar nihče ne
zagotavlja, da bo distribucijski center, okrog katerega smo nabrali določeno
množico, tudi optimalni distribucijski center za to množico. Zato v naslednjem
koraku popravimo distribucijske centre: za vsako množico otrok izračunamo nove,
boljše centre. Zdaj, ko imamo boljše centre, ponovno razdelimo vse otroke med
te, nove centre. Tako smo spet dobili (nove) množice otrok, a centri, na
podlagi katerih smo jih določili, spet niso nujno najboljši. Zato v naslednjem
koraku spet izračunamo boljše centre, potem spet preračunamo množice in tako
naprej...
Z drugimi besedami, funkcija naredi tole:
- vzame prvih
k
točk izs
; tem točkam recimo "centri" (čeprav niso) - ponavlja:
- s funkcijo
razdeli
razporedi vse točke k najbližjim centrom; - s funkcijo
poisci_centre
poišče centre skupin, ki jih je naredil v prejšnjem koraku. Ti centri bodo zdaj novi centri, s katerimi bo ponovil vse skupaj.
Ponavljanje naj se konča takrat, ko se množica centrov ne spreminja več.
Funkcija naj vrne slovar, katerega ključi so koordinate distribucijskih centrov, pripadajoče vrednosti pa vsi otroci, ki jih oskrbuje posamezni center.
Rešitev
Nalogo za oceno 10 je bilo težje opisati kot sprogramirati.
Najprej poglejmo zanko: vse skupaj ponavljamo, dokler se centri spreminjajo.
Zato v vsakem koraku najprej shranimo trenutne centre
(scentri = centri
), nato izračunamo nov razpored, kdo bo oskrbovan
iz katerega centra (clustri = razdeli(centri, s)
) in na podlagi
teh skupin določimo nove centra
(centri = poisci_centre(clustri.values())
).
V pogoju zanke preverjamo ali so stari centri enaki novim. Da bi to delovalo
tudi v prvem krogu, bomo pred zanko postavili scentri = None
.
Ker bodo centri
in scentri
v začetku očitno različni,
se bo zanka vsaj enkrat izvedla.
Medtem ko se tisti, ki v programiranju še niso posebej doma, ukvarjajo z osnovnejšimi problemi, se morate tisti, ki rešujete naloge za višje ocene, počasi začeti učiti tudi elegance.
Dodatne naloge
Spremeni rešitve za oceni 9 in 10 tako, da centrov ne bosta določali s
funkcijo center
temveč s funkcijo tezisce
- torej
skladno z drugim scenarijem, po katerem Miklavž gradi nove hiše.
Ugotovi, kaj si pravkar sprogramiral in čemu ta reč služi. ;) Zgodbica o Miklavžu je v resnici le metafora za nek splošnejši in zelo uporaben postopek.