Božiček (in parkelj)
Ker bo jutri ravno božič (no, če smo natančni, божић, Рождество Христово), je ravno še čas za nalogo z božičkom. (Čeprav vso stvar dodatno zaplete dejstvo, da "božiček" nima popolnoma nič s pravoslavci, saj so ga izumili protestanti, ko so ukinili svetega Miklavža, saj nimajo svetnikov.)
Testi
Testi: testi-bozicek-in-parkelj.py
Ogrevalna naloga
Napiši razred Mesto
.
- Konstruktor ne dobi nobenih argumentov. Kaj naredi, je tvoja stvar; narediti mora vse, kar je potrebno, da bodo delovale ostale metode.
- Metoda
obdaruj(x, y)
, zabeleži, da je hiša na koordinatah (x, y) prejela darilo. - Metoda
je_obdarovana(x, y)
vrneTrue
, če je hiša na koordinatah (x, y) prejela darilo;False
, če ga ni. - Metoda
vse_obdarovane()
vrne množico koordinat vseh obdarovanih hiš.
Primer:
>>> m = Mesto()
>>> m.obdaruj(5, 1)
>>> m.obdaruj(42, 2016)
>>> m.je_obdarovana(1, 1)
False
>>> m.je_obdarovana(42, 2016)
True
>>> m.vse_obdarovane()
{(5, 1), (42, 2016)}
Rešitev
Kakšne podatke mora shranjevati Mesto
? Kaj mora početi? Znati mora povedati,
ali je bila določena hiša obdarovana in znati mora vrniti množico obdarovanih
hiš. Očitno si mora torej zapomniti, katere hiše so bile obdarovane. In najbolj
praktično bo, da to shrani v množici, katere elementi bodo koordinate (pari
števk) obdarovanih hiš. Zakaj ravno množica? Malo zaradi operacije, ki jo bomo
izvajali na njej - je_obdarovana
bomo potem naredili tako, da bomo vprašali,
ali množica vsebuje določen element in to Python naredi veliko hitreje, kot če
bi vprašali, ali seznam vsebuje določen element. Malo pa zato, ker je tako bolj
praktično, saj bo lahko vse_obdarovane
vračala kar to množico.
class Mesto:
def __init__(self):
self.obdarovane = set()
def obdaruj(self, x, y):
self.obdarovane.add((x, y))
def je_obdarovana(self, x, y):
return (x, y) in self.obdarovane
def vse_obdarovane(self):
return self.obdarovane
Obvezna naloga
Napiši razred Bozicek
.
- Konstruktor kot argument dobi mesto, ki mu bo delil darila. Božiček je v začetku na koordinatah (0, 0).
- Metoda
premik(c)
kot argument prejme znakv
,^
,<
ali>
in prestavi božička za eno "polje" dol, gor, levo in desno. Koordinata y se tokrat spreminja tako, da narašča navzgor (premik^
jo poveča inv
zmanjša). - Metoda
premiki(pot)
kot argument prejme niz, sestavljen iz teh znakov in premakne božička po tej poti. - Metoda
obdaruj()
obdaruje hišo na koordinatah, na katerih se trenutno nahaja božiček.
Primer:
>>> kranj = Mesto()
>>> tone = Bozicek(kranj)
>>> tone.premik(">")
>>> tone.premik("^")
>>> tone.obdaruj()
>>> kranj.vse_obdarovane()
{(1, 1)}
>>> tone.premiki(">>>>>>>>vvv")
>>> tone.obdaruj()
>>> kranj.vse_obdarovane()
{(9, -2), (1, 1)}
Ne spreglej, da metoda obdaruj
nima argumentov (razen self
).
Poleg tega napiši razred HitriBozicek
, ki je izpeljan iz razreda Bozicek
.
Njegov konstruktor poleg mesta prejme tudi hitrost. Hitri božiček se od
navadnega razlikuje po tem, da se ne premika za eno polje temveč za toliko,
kolikor je njegova hitrost, ki smo jo podali v konstruktorju.
Primer:
>>> peter = HitriBozicek(kranj, 3)
>>> kranj.vse_obdarovane()
{(9, -2), (1, 1)}
>>> peter.obdaruj()
>>> kranj.vse_obdarovane()
{(0, 0), (9, -2), (1, 1)}
>>> peter.premik(">")
>>> peter.obdaruj()
>>> kranj.vse_obdarovane()
{(3, 0), (0, 0), (9, -2), (1, 1)}
>>> peter.premiki(">^")
>>> peter.obdaruj()
>>> kranj.vse_obdarovane()
{(3, 0), (0, 0), (9, -2), (1, 1), (6, 3)}
Peter deluje v istem mestu kot Tone, zato dodaja v isto množico obdarovanih hiš. Mimogrede ne spreglej: gre za množico, zato vrstni red ni določen.
Rešitev
Ta naloga je bila koristna tudi zame, saj me je poučila o tem, kje imate težave.
Kot sem napisal na forumu: Bozicek
ni izpeljan iz Mesto
. Če bi bil, bi to
pomenilo, da je božiček neka posebna vrsta mesta. Vendar ni.
Kaj vas je zavedlo, je jasno, vendar na to pri sestavljanju naloge nisem
pomislil: oba imata metodo obdaruj
. Vendar je to precej drugačna metoda.
Tista v Mesto
zabeleži, da je določena hiša obdarovana. Tista v Bozicek
"sporoči" mestu, da je določena hiša obdarovana.
Za kaj gre, bo tokrat lažje videti iz programa.
class Bozicek:
def __init__(self, mesto):
self.x = self.y = 0
self.mesto = mesto
def premik(self, c):
self.x += (c == ">") - (c == "<")
self.y += (c == "^") - (c == "v")
def premiki(self, pot):
for c in pot:
self.premik(c)
def obdaruj(self):
self.mesto.obdaruj(self.x, self.y)
Kaj si mora zapomniti Bozicek
? Dve stvari: koordinate, na katerih je, in
mesto, ki ga obdaruje.
Koordinate in metode, povezane z njim, bi morale biti preproste, saj niso tako zelo drugačne od želve in drugih nalog, ki smo jih že delali.
No, pri premik
smo se malo poigrali z logičnimi izrazi. c == ">"
je
True
ali False
, kar je isto kot 1
ali 0
. K self.x
bomo torej prišteli
1, če je c == ">"
in odšteli 1, če je c == "<"
. Sicer pa bomo prišteli
in odšteli 0.
Problem je (sicer čisto kratka in preprosta - potem, ko jo vidiš) metoda
obdaruj
. V konstruktorju si zapomnimo mesto - shranimo ga v self.mesto
.
Metoda obdaruj
mora potem poklicati metodo obdaruj
tega mesta, kot
koordinate hiše pa podati lastne (božičkove) koordinate.
HitriBozicek
pa je potem relativno preprosta reč. V konstruktorju si zapomni
še hitrost, mesto pa prepusti podedovanemu konstruktorju.
class HitriBozicek(Bozicek):
def __init__(self, mesto, hitrost):
super().__init__(mesto)
self.hitrost = hitrost
def premik(self, c):
self.x += self.hitrost * ((c == ">") - (c == "<"))
self.y += self.hitrost * ((c == "^") - (c == "v"))
Pri premik
smo se spet malo igrali.
Imel sem tudi genialno idejo, kako nagoljufati premik
. Kot veliko mojih
genialnih idej, tudi ta ne deluje. :) Pokazati jo moram, ker ne deluje na
zelo poučen način.
class HitriBozicek(Bozicek):
def __init__(self, mesto, hitrost):
super().__init__(mesto)
self.hitrost = hitrost
def premik(self, c):
self.premiki(c * self.hitrost)
Recimo, da imamo hitrega božička, katerega hitrost je 3. Recimo, da pokličemo
premik("<")
. Izvedemo ga lahko preprosto tako, da pokličemo premiki("<<<")
.
Problem je v tem, da premiki
pokliče premik
. Vendar premiki
razreda
Bozicek
poklice premik
. A kot smo slišali na predavanjih (žal pa jaz nisem
poslušal, temveč govoril; ali pa ... no ja, človek se zmoti ;) bo premiki
poklical metodo premik
hitrega božička. Ta pa pokliče premiki
. Ki pokliče
premik
. No, tole je situacija, v kateri tudi jaz ne maram rekurzije. :(
Se da to popraviti? Poskrbeti, da premiki
ne bi poklicala premik
? Ne zlepa.
V resnici bi morali razreda obrniti: Bozicek
bi moral biti posebna vrsta
HitregaBozicka
. Hitri božiček se premika s poljubno hitrostjo, običajni
pa je tak, pri katerem je ta hitrost vedno 1.
Dodatna naloga
Nekateri otroci niso bili pridni.
Napiši funkcijo parkelj(x, y, hise)
, ki prejme koordinate, na katerih se
nahaja parkelj, in koordinate vseh hiš, kjer živijo otroci, ki jim je potrebno
vzeti darila. Funkcija naj vrne pot, ki pelje parklja mimo vseh teh hiš.
Parkelj naj obišče hiše v podanem vrstnem redu. V okviru tega pa naj prehodi
najkrajšo možno pot.
>>> parkelj(5, 2, [(8, 2), (8, -4), (5, -1)])
'>>>vvvvvv<<<^^^'
>>> parkelj(5, 2, [(5, 2), (5, 5)])
'^^^'
Rešitev ni unikatna. Klic parkelj(5, 2, [(6, 3)])
, recimo, lahko vrne
">^"
ali "^>"
. Pravilno je oboje.
Pravzaprav noben otrok v mestu ni bil priden.
Napiši funkcijo hitri_parkelj(x, y, mesto)
, ki prejme parkljeve začetne koordinate
in objekt, ki predstavlja Mesto. Funkcija naj vrne najkrajšo pot, po kateri
lahko parkelj pobere vsa darila. Tudi hiše naj obiskuje v takšnem vrstnem redu,
da bo njegova pot najkrajša.
Namig: poglej funkcijo itertools.permutations
. Sicer pa smo podobno nalogo
že enkrat reševali.
>>> n = Mesto()
>>> n.obdaruj(0, 1)
>>> n.obdaruj(2, 0)
>>> n.obdaruj(0, 10)
>>> hitri_parkelj(0, 0, n)
'>><<^^^^^^^^^^'
Pravilna rešitev je tudi '>>^<<^^^^^^^^^'
. Tule je pomembno, da gre najprej
v hišo (2, 0) in šele nato v (0, 1), sicer bo pot dva koraka daljša.
Rešitev
def parkelj(x, y, hise): return "".join("<" * max(x0 - x1, 0) + ">" * max(x1 - x0, 0) + "v" * max(y0 - y1, 0) + "^" * max(y1 - y0, 0) for (x0, y0), (x1, y1) in zip([(x, y)] + list(hise), hise))
Iti moramo prek vseh parov (x0, y0), (x1, y1)
. Namesto običajnega trika, s
katerim dobimo zaporedne pare (zip(s, s[1:])
), jih zdaj sestavimo tako, da na
začetek enega od seznamov potisnemo začetne koordinate.
Za vsak par sestavimo niz, ki nas spravi od ene hiše do druge.
"<" * max(x0 - x1, 0) + ">" * max(x1 - x0, 0) +
"v" * max(y0 - y1, 0) + "^" * max(y1 - y0, 0)
Levo gremo tolikokrat, kolikor je x0 - x1
. Če je to slučajno negativno, pa
gremo levo za 0. Podobno opravimo z ostalimi. Nato jih z "".join
sestavimo
v en sam niz.
Hitrega parklja pa le napišimo. Razloži si ga naj, kdor hoče.
def dolzina_poti(x, y, hise):
return sum(abs(x0 - x1) + abs(y0 - y1)
for (x0, y0), (x1, y1) in zip([(x, y)] + list(hise), hise))
from itertools import permutations
from functools import partial
def hitri_parkelj(x, y, mesto):
return parkelj(x, y, min(permutations(mesto.vse_obdarovane()),
key=partial(dolzina_poti, x, y)))