Pike
Testi
Testi: testi-pike.py
Naloga
Pri opisu rešitve te naloge bom izpostavil nekaj študentov, a brez imen. Če bo med prikazanimi izdelki tudi vaš, se ne čutite se nagovorjene. Če ste ga bom pokazal kot primer nečesa, kar se ne dela: drži, vendar ste se do tega prebili v potu svojih prstov in se naučili veliko več kot nekdo, ki je oddal perfektno rešitev - ki ni njegova. Če bom pokazal vaš izdelek kot primer, kako se dela, v resnici pa ga sploh niste naredili sami: niste nategnili mene, temveč sebe. Domače naloge so namenjene vam, ne meni; jaz bi že znal tole sprogramirati (če ne, bi pa prosil Jureta. ;) Seveda pa ne bom omenjal imen, saj niso pomembna.
Naloga je bila posrečena, saj ste se uspešno spotikali ob kupe nastavljenih pasti. Mislim, da je bilo kar poučno - za tiste, ki ste se bili voljni učiti.
Programirali smo Dots. Če je ne boste preskusili sami (ne priporočam, včasih malo zasvoji), si jo oglejte, recimo, na You Tubu.
Različne barve so predstavljene s številkami od 1 do 5. Stanje igralne
plošče je shranjeno v seznamu seznamov. Imamo torej spremenljivko (navadno
jo bomo imenovali polje
), ki ima toliko elementov, kolikor je
stolpcev, in vsak element bo vseboval toliko števil, kolikor je vrstic. Elemente
stolpcev pišemo od spodaj navzgor. Če bi torej radi tretji element druge
vrstice (od spodaj), bomo rekli polje[3][2]
(pri čemer številčenje
seveda začnemo z 0, kot vedno).
Za razliko od prave igre, v kateri je polje vedno dimenzij 6x6, je naše polje lahko poljubno veliko ali pa celo nekoliko dolgočasno; imamo lahko tudi le eno samo vrstico ali en sam stolpec.
V nekaterih funkcijah bomo govorili tudi o poti: pot je seznam koordinat, na primer [(3, 1), (3, 2), (4, 2), (4, 3)]. Koordinate, ki jih vsebuje imajo lahko poljuben vrstni red.
Ogrevanje
Napiši funkcijo dopolni(polje, visina)
. Argument
polje
je seznam seznamov števil. Funkcija mora dopolniti vse
(pod)sezname tako, da bodo imeli visina
elementov. Dopolniti
ga mora z naključnimi števili med 1 in 5.
Funkcija ne vrača rezultata, temveč spreminja poslano polje.
Če imamo, recimo, polje = [[2, 4], [], [1, 2, 3]]
, mora
dopolni(polje, 3)
dodati en element v prvi podseznam, tri v drugega
in nobenega v tretjega.
Rešitev
Tole je bila le kar preprosta vaja iz zank in seznamov - pa razmišljanja o tem, kako jih uporabiti seveda.
Gremo po stolpcih (for stolpec in polje
) in v vsakega dodajamo
elemente (stolpec.append(randint(1, 5))
), dokler jih nima
toliko, kolikor jih mora imeti
(while len(stolpec) < visina
).
Za oceno 6: Izpis
Napiši funkcijo izpisi(polje)
, ki vrne niz z vsebino polja - če
bi ta niz izpisali, bi dobili polje, kot ga vidi igralec. Upoštevati mora
torej, da seznam polje
vsebuje stolpcev, zapisane od spodaj
navzgor. Tako mora
vrniti niz
Rešitev
Zanimivo, koliko vetra vam je dala ta funkcija. Eden od študentov jo je rešil takole:
Le malo jo olepšajmo: namesto i
in j
pišimo
x
in y
, da bomo vedeli, kaj je kaj. Pa meje
prve zanke spremenimo, da bo y
tekel prek pravih številk in
mu ne bo potrebno stalno odštevati enice.
To je bila nekako pričakovana rešitev (žal večina sicer ni bila tako elegantna, nekatere pa so bile tudi preveč elegantne v slabem pomenu besede).
Takole: v niz tmp
dodajamo polja,
tmp += str(polje[x][y])
. Poskrbeti moramo, da bosta zanki,
ki določata x
in y
tekli prek primernih
koordinat.
Ker izpisujemo po vrsticah, bo šla zunanja zanka prek y
. Ker
so stolpci shranjeni tako, da je tisto, kar je najvišje (in je torej
potrebno izpisati najprej), na koncu, bo tekla v nasprotno smer. Zato
for y in range(len(polje[0]) - 1, -1, -1)
. Notranja zanka,
x
, gre lepo od leve proti desni,
for x in range(len(polje))
. Z tmp += "\n"
na
koncu vsake vrstice dodamo znak za novo vrstico.
Če pogledamo malo natančneje, opazimo, da gre notranja zanka prek stolpcev.
Torej jo naženimo prek stolpcev, namesto da se hecamo z nekimi
x
i.
Sam sem v svoji rešitvi to (zmerno) stisnil tako, da sem zamenjal notranjo
zanko z join
in generatorjem.
Tole je še na meji prebavljivo preglednega. Tole pa je brez zveze, saj je takole mogoče le pisati, brati pa pravzaprav ne.
Nekateri ste ob izpisovanju spreminjali polje. Ko sem zagrozil, da tega ne počnite, ste se rešili tako, da ste ga skopirali. Vendar tole ne deluje, kot si je avtor zamislil:
Teste je prestalo zgolj zato, ker nisem preverjal, ali pustite polje pri
miru (ker pač nisem pomislil na to, da bi ga kdo hotel pri izpisu spreminjati.
Zakaj, kako? Mar polje.copy()
ne naredi kopije? Da, naredi: dobimo
novo polje, vendar vsebuje iste elemente, torej iste podsezname. Če jih
spreminjate, spreminjate podsezname originalnega polja. Še nekaj o tem bomo
videli pri naslednji nalogi, ki je bila bolj namenjena razumevanju teh reči.
Za oceno 7: Brisanje
Napiši funkcijo pobrisi_pot(pot, polje)
, ki
pobriše pike na poljih, ki se nahajajo na poti. Pike nad njimi naj "padejo" en
element nižje. Nekateri seznami bodo zaradi tega torej nekoliko krajši.
Poleg tega napiši funkcijo pobrisi_barvo(barva, polje)
, ki
odstrani vse pike določene barve.
Funkciji ne vračata ničesar, temveč spreminjata polje, ki sta ga dobili kot argument.
Rešitev
Na tole nalogo sem pa nekoliko ponosen. Učila vas je razlike med
del
in remove
, učila vas je, kako spreminjati
seznam, ki ga dobi funkcija (in kako se ga ne da spreminjati), poleg tega
pa vas je učila, kako izvajati zaporedna brisanja v seznamu, pri katerem
se vam elementi "izpodmikajo". Predvsem slednje vas je urilo tudi v
iskanju napak v svojem programu.
Brisanje barve - in kako spreminjati polje
Najprej napišimo drugo, manj poučno funkcijo.
Problem, ki ste ga imeli nekateri, je, da ste izpustili while
-
očitno v veri, da bo remove
odstranil vse pojavitve barve. Ne,
odstrani le prvo, zato jo je potrebno odstranjevati, dokler je popolnoma ne
izkoreninimo.
Variacija tega je
To je sicer podobno, vendar manj učinkovito: veliko hitreje je preveriti, ali stolpec vsebuje nekaj, kot šteti, kolikokrat vsebuje. V tej nalogi se to seveda prav nič ne pozna. Vedite pa za sicer.
Nekateri ste raje uporabljali izpeljane sezname. Če bi me kdo vprašal, ali ni to lepše, mu ne bi rekel, da ni. Vodi pa v poučen problem. Spodnja rešitev namreč ne deluje.
Problem je v tem, da funkcija ne spremeni vrednosti polja, ki ga je dobila
kot argument, temveč le priredi imenu polje
nek nov seznam.
Očitno boljša ideja je, da spraznimo seznam polje
in dodajamo
vrstice vanj. Predtem pa seveda shranimo staro vsebino polja v novo
spremenljivko.
Ideja je lepa, problem pa je v tem, da sta staro
in
polje
le dve imeni za en in isti seznam.
Oboje - varianta "staro" in varianta "novo" - je mogoče poflikati. Varianto
"novo" popravimo tako, da na koncu funkcije zamenjamo vsebino seznama
polje
z novo vsebino:
S tem zamenjamo trenutno vsebino polja ("od začetka do konca") z neko drugo vsebino.
Krajša različica slednjega, ki jo je napisal nekdo, je
Korektna rešitev z izpeljanim seznamom.
Kdor teh reči ne razume, naj nujno prebere zapiske predavanj o imenskih prostorih.
Flikanje variante "staro" je ravno nasprotno (in zato skoraj enako).
Imenu staro
priredimo nov seznam, ki vsebuje iste reči kot
polje
.
Nekateri ste uporabili deepcopy
.
Če napišemo staro = polje[:]
, bo staro
seznam, ki
vsebuje iste elemente kot polje
- torej iste sezname. Funkcija
deepcopy
pa skopira v globino - ne naredi le kopije polja, temveč
tudi kopije njegovih elementov in tako naprej. Seznam staro
bo po
tem vseboval sezname (stolpce), ki bodo enaki kot seznami, ki jih vsebuje
polje
, ne bodo pa isti.
Kaj je pravilno? [:]
je enostavnejše; če je tudi dovolj, je
boljše, saj ni potrebe po tem, da bi kopirali v globino. No, izkaže se, da je
dovolj: polje.clear()
sicer izprazni seznam, tako da vrže iz njega
vse njegove elemente - elementom, torej stolpcem, pa ne naredi hudega, torej jih
ni potrebno kopirati.
Še ena simpatična rešitev, ki jo je napisal nek študent, je
Zakaj to deluje? Ker prireja nove sezname seznamu polje
- torej
prav tistemu seznamu, ki ga je potrebno spreminjati. Kdor ne razume, zakaj
polje = ...
ni dobra ideja, polje[i] = ...
pa je,
naj si ogleda zapiske prej omenjenih predavanj in razmisli, kaj se dogaja v
ozadju.
Brisanje poti - in težave z izpodmikanjem
Tule vas je čakala prava skala spotike. Oglejmo si jo na nekoliko preprostejšem primeru.
Če pobrišemo ničti, prvi in drugi element, bi človek pričakoval, da bodo
v seznamu ostali 3, 4 in 5. Ker nesreča ne počiva in ima hudič prste vmes in
kar je še takšnega v ljudskem izročilu, pa je a
takšen:
Zakaj? Preprosto: ko pobrišemo ničti element, postane enica ničti element in dvojka prvi. Ko pobrišemo prvi element, pobrišemo dvojko (ne enice, kot smo hoteli). Zdaj se vse premakne še za eno mesto; no, ne vse - enka ostane, kjer je bila, vsekakor pa na drugo mesto pride štirica. Tako takrat, ko pobrišemo drugi element, pobrišemo štirico namesto dvojke.
Funkcija
za katero so mnogi za sveto trdili, da je gotovo pravilna, ne deluje iz istega razloga.
Napaka je bila za večino tako nepričakovana, da ste se ob njej urili v tem, da ste raziskovali, kaj neki je narobe v vašem programu. Koristna vaja; upam, da ste jo čimbolj vadili sami.
Zdaj pa rešitve. Oglejmo si tri možnosti.
Prvi je bil med vašimi rešitvami najpopularnejši: vse, kar je potrebno
pobrisati, zamenjamo z -1
, None
, "x"
ali čim podobno nemogočim. Šele, ko je vse označeno, pobrišemo
vse -1, None in "x".
Tole je eden od študentskih izdelkov.
V drugi zanki razpoznamo isto reč kot brisanje barve. Nekateri so se dejansko znašli in pisali
Lepo!
Med pregledovanjem nalog sem naleten na še eno odlično različico istega. Tole mi ni prišlo na misel. Če se je avtor tega spomnil sam: bravo!
Tole je žlahtna kombinacija objektnega in funkcijskega programiranja: oni čudni stavek pravi "izberi vse tiste, za katere 999 pravi, da jim ni enaka.
Drugi obvoz je enako očiten, vendar zahteva - če nočemo, da postane zapleten, nekaj programerske spretnosti.
Zanka for i, (x, y) in enumerate(pot)
pomeni približno isto
kot for x, y in pot
, le da zraven še šteje, kolikšen del
poti smo že prehodili. Nato brišemo, vendar namesto
del polje[x][y]
pišemo
del polje[x][y - izpodmik(pot[:i], x, y)]
, pri čemer nam
bo funkcija
pot[:i]
) izpodmaknila
element, ki bi moral biti na x, y
(v resnici bi morali pisati
pot[:i - 1]
, vendar bi to povzročilo težave pri i=0, to, da smo
v resnici vzeli en element preveč, pa bo minilo brez posledic).
Kaj mora narediti izpodmik
? No, povedati, koliko elementov
stolpca x
z indeksom manjšim od y
so že
pobrisali. Kaj počne y0 < y for x0, y0 in pot if x0 == x
?
Vzame vse koordinate na dosedanji poti; označimo jih z x0
in
y0
. Zanimajo nas samo takšne, ki so v pravem stolpcu, torej
x == x0
. Pri vseh takšnih preverimo, ali velja
y0 < y
. Rezultat tega je True
ali
False
. Če vemo, da je True
isto kot 1 in
False
isto kot 0, bo vsota teh reči ravno število elementov,
ki so bili v istem stolpcu in je njihova koordinata y0 manjša od podanega
y.
Kot rečeno, ni zapletno, le nekaj spretnosti zahteva.
Tretji obvoz je eleganten. Če bi morali pobrisati prvi, drugi in tretji
element, bi imeli težavo, če tretjega, drugega in prvega, pa bi bilo vse
v redu. Ker pa gre načelno za eno in isto, se lahko znajdemo tako, da
pot uredimo v padajočem vrstnem redu, torej
reversed(sorted(pot))
ali, učinkoviteje,
sorted(pot, reverse=True)
. Rešitev naloge je torej kar
Pri tem se nam ni potrebno ukvarjati s tem, ali bomo urejali po x ali po y. Kot smo se že parkrat omenili, Python primerja terke "leksikografsko": najprej primerja prve elemente, če so ti enaki, druge in tako naprej. Tu bo torej uredil padajoče po koordinatah x (stolpci), znotraj stolpcev pa padajoče po koordinatah y. To je za naše potrebe čisto v redu.
V oči mi je padlo tole:
Zanimivo se mi zdi, da je tu nekdo odkril najkrajšo rešitev, obenem pa brez potrebe pisal
namesto jedrnatega Pythonovskega
Občutek imam, da je še precej takšnih, ki sicer znate programirati že od prej, ni pa vam do tega, da bi se naučili programirati v Pythonu. Kot moj brat, ki je med potovanjem po Češkem natepaval dunajske zrezke namesto (odličnih, to je bilo pred petindvajsetimi leti, pred komercializacijo in množičnim turizmom) knedličke z golažem.
Tole razpakiranje ni izključno Pythonovska ideja; prišel je iz funkcijskih jezikov, kjer je to kar običajna reč, prej ko slej pa ga bodo vključili tudi v Javascript, v katerem boste gotovo nekoč programirali (Firefox in Opera to že podpirata). (Sicer pa je tudi Javascript funkcijskih jezik; zadnjič sem bral nekoga, ki pravi, da je to Lisp s C-jevsko sintakso.)
del
in remove
V funkciji pobrisi_barvo
je bilo potrebno pobrisati element
z znano vrednostjo, za katerega nismo vedeli, kje je. To se dela z
remove
. Seveda bi ga lahko poiskali sami (ali pa uporabili
index
) in ga pobrisali z del
, torej nekako tako:
del stolpec[stolpec.index[barva]]
. Vendar je
stolpec.remove(barva)
, ki naredi natančno isto, očitno
preprostejši.
V funkciji pobrisi_pot
natančno vemo, kje je reč, ki jo hočemo
brisati, ne vemo pa, kakšna je. No, pravzaprav vemo: če hočemo pobrisati
polje[x][y]
, potem vemo, da je njegova vsebina pač vrednost
polje[x][y]
. Vendar je polje[x].remove(polje[x][y])
zelo slaba ideja, saj pobriše prvo (torej najnižjo) pojavitev vrednosti
polje[x][y]
v stolpcu polje[x]
. Če slučajno hočemo
pobrisati drugo ... bo narobe.
Nekdo je odlično sprogramiral vso domačo nalogo, vključno s to funkcijo,
vendar je na koncu uporabil remove
namesto del
:
Ta funkcija na deluje, če v (dolgočasnem, eno-stolpčnem) polju
[[1, 2, 3, 2]]
poskusimo pobrisati pot [(0, 3)]
, to
je, drugo dvojko. Metoda remove
bo pobrisala prvo dvojko, na
katero bo naletela in rezultat bo [[1, 3, 2]]
namesto
[[1, 2, 3]]
.
Nekateri namesto del
uporabljate pop
. S tem ni nič
narobe, vsa razlika med del s[i]
in s.pop(i)
je v
tem, da pop
vrne rezultat, namreč pobrisani element,
del
pa ne. (No, še ena je: pop
je izraz in ga lahko
uporabimo na kakem mestu, na katerem del
a ne moremo.) Vseeno
priporočam del
, zaradi berljivosti: ko vidim del
,
vem, da brišemo. Ko vidim pop
si predstavljam, da se bo s tem
elementom nekaj zgodilo in moram še enkrat pogledati, da vidim, da rezultata
niste nikjer uporabili.
Za oceno 8: Preverjanje poti
Napiši funkcijo preveri_pot(pot, polje)
, ki pove, ali je pot
pravilna in vrne True
ali False
.
Pot je pravilna, če velja naslednje:
- Pot je dolga vsaj dve točki.
- V vsakem koraku gre pot za eno polje gor, dol, levo ali desno.
- Nobeno polje se ne ponovi; izjema so sklenjene poti, v katerih je prvo polje enako zadnjemu. To je dovoljeno.
- Celotna pot se nahaja znotraj polja - nobena koordinata ni levo ali desno od polje oziroma nad ali pod njim.
- Celotna pot teče prek točk iste barve.
Rešitev
Pri reševanju te naloge ste bili res ustvarjalni. Morda je bolj kot druge pokazala, koliko rutinske spretnosti premorete. Nekateri ste jo kar dobro zapletli, drugi ste bili kar učinkoviti.
Storiti nam je tole: zapisati moramo gornje pogoje. Čim kateri od njih ne
drži, bomo vrnili False
. Pojdimo lepo po vrsti:
Pot je dolga vsaj dve točki: tu ni kaj,
len(pot) >= 2
Nobeno polje se ne ponovi; izjema so sklenjene poti, v katerih je prvo polje enako zadnjemu. Spomnimo se, kako preverjamo, ali vsebuje nek seznam samo unikatne elemente: spremenimo ga v množico in preverimo, ali je ta enako dolga kot seznam,
len(pot) == len(set(pot))
. To skoraj deluje, razen takrat, ko je zadnji element enak prvemu. V tem primeru bolen(set(pot))
za 1 premajhen. No, pa prištejmo na desni strani šepot[0] == pot[-1]
. Če sta enaka, bo toTrue
,True
pa je (kar se tiče aritmetika) isto kot 1. Se pravi,len(pot) == len(set(pot)) + (pot[0] == pot[-1])
. Ne pozabimo na oklepaje: če bi pisali brez,len(pot) == len(set(pot)) + pot[0] == pot[-1]
, bi bilo to isto kotlen(pot) == (len(set(pot)) + pot[0]) == pot[-1]
, saj ima seštevanje prednost pred dvojnim enačajem. (Program pa bi javil napako, saj ne more sešteti številalen(set(pot))
in terkepot[0]
.)Celotna pot se nahaja znotraj polja - nobena koordinata ni levo ali desno od polje oziroma nad ali pod njim. Če znamo uporabiti
all
, je to preprosto: za vsak par x, y iz poti (for x, y in pot
, mora veljati, da sta znotraj polja:all(0 <= x < len(polje) and 0 <= y < len(polje[0] for x, y in pot)
.V vsakem koraku gre pot za eno polje gor, dol, levo ali desno: šli bomo prek vseh zaporednih koordinat - tega ne delamo prvič, zato hitro rečemo
for (x1, y1), (x2, y2) in zip(pot, pot[1:])
. Kaj mora veljati za par koordinat? Izračunamo razliko po koordinati x in razliko po koordinati y, torejabs(x1 - x2)
inabs(y1 - y2)
. Eden (in natančno eden) od njiju se mora spremeniti za 1 in drugi za 0. Oba sta pozitivna. Torej le izračunamo vsoto in ta mora biti 1,abs(x1 - x2) + abs(y1 - y2) == 1
. To mora veljati za vse pare, se pravi:all(abs(x1 - x2) + abs(y1 - y2) == 1 for (x1, y1), (x2, y2) in zip(pot, pot[1:]))
- Celotna pot teče prek točk iste barve: tole je nekoliko podobno kot
prej, le preprosteje: vsako polje mora biti enake barve kot polje pred
njim,
all(polje[x1][y1] == polje[x2][y2] in zip(pot, pot[1:]))
.
Zdaj le zložimo vse pogoje v funkcijo:
Opazimo še, da imamo v zadnjih dveh enako zanko, torej ju lahko združimo v
Če niste tako okretni z izpeljanimi seznami, je lahko rešitev seveda nekoliko daljša. Ne bomo jih kazali, saj jih je precej in vsaka je zavozlana na svoj način. Nekateri ste se sicer lepo organizirali in za vsako pogoj napisali ločeno funkcijo. To je lepa navada. Tudi v resničnem življenju ne gredo vsi pogoji v eno vrstico, zato je lepo, da se jih naučite razbiti na funkcije.
Če počnete to, vedite, da lahko v Pythonu (in drugih sodobnih jezikih) pišemo funkcije tudi znotraj funkcij. Navadite se tega sloga.
Tule je še primer, kako ne programirajte.
Tega ne objavljam, da bi se posmehovali: avtor te rešitve se je res potrudil. Bolj kot mnogi drugi. In program je po svoje v redu ... le očitno kaže, da bi ga bilo smiselno razdeliti na funkcije. Pa še nekaj: uporabljajte zgovornejša imena spremenljivk. V takšni kodi se je nemogoče znajti...
Za oceno 9: Pot iz koordinat
Napiši funkcijo pot_iz_koordinat(opis)
, ki dobi niz, ki
opisuje pot in ga spremeni v seznam
koordinat. Niz se začne z dvema števkama, ki predstavljata koordinati
x in y, sledi pa jima zaporedje premikov levo, desno, gor in dol, opisanih
s črkami L, R, U in D ;(angleško, ker se v slovenščini desno in dol začneta
z isto črko).
Niz "35LLUR"
pomeni, da začnemo na (3, 5) in nadaljujemo
levo, levo, gor in desno, torej mora pot_iz_koordinat("35LLUL")
vrniti seznam [(3, 5), (2, 5), (1, 5), (1, 6), (2, 6)]
.
Rešitev
Tole je tehnikalija: kako spreminjati L, R, U in D v premike si lahko ogledate v sto in eni varianti lanskih nalog s čebelo, v starih izpitih se pojavljajo tudi neki pajki... Tule je ena od preprostejših različic.
Za oceno 10: Igra
Najprej sprogramiraj funkcijo ni_sosednjih(polje)
, ki vrne
True
, kadar na plošči ni več sosednjih točk iste barve. Vsaka
točka ima štiri sosede (ali manj, na robu in v vogalih); diagonalne sosede
ne štejejo.
Nato sestavi celotno igro - v tekstovnem načinu. Igra naj teče na plošči velikosti 6x6, ki jo obvezno napolni takole
Nato naj ponavlja naslednje: izpiše trenutni položaj, uporabnik vnese pot (kot je opisana v nalogi za oceno 9), program preveri, ali je pot veljavna in če ni, izpiše "Neveljavna pot" (in ponovi izpis polja), sicer pa pobriše vse pike na poti, obstoječe pike padejo v nastali prosto, stolpce pa dopolni z novimi naključno izbranimi pikami. Če je pot, ki jo je vnesel uporabnik, sklenjena, pa s plošče pobriše vse točke tiste barve.
Igre je konec, ko na plošči ni več nobenih sosednjih točk iste barve.
Rešitev
Preverjanje sosednosti gre takole: najprej razmislimo, kako bi za en sam
stolpec ugotovili, ali ima kje dve sosednji enaki polji - in v tem primeru
vrnemo False
.
To poženemo seveda čez vse stolpce.
Potem ugotovimo, kako za dva stolpca ugotoviti, ali se v kaki vrstici ujemata.
To bomo pognali prek vseh sosednjih stolpcev.
Celotna funkcija je torej
Gre tudi bolj jedrnato.
Zdaj pa še celotna igra.