Patarimai olimpiadoms
Pastaba. Čia sudėti tik mano nuomone svarbiausi C++ “triukai” ir pavyzdžiai. Daugiau galima paskaityti čia arba paieškojus Google :). Šiame puslapyje bus aptariama:
- #define
- typedef
- cin ir cout pagreitinimas
- endl prieš “\n”
- Viena biblioteka visoms gyvenimo reikmėms
- Naudingi C++11 dalykai
- “algorithm” biblioteka
- Standartinės duomenų struktūros
- Dvejetainė paieška
#define
#define
- C++ preprocesoriaus komanda, leidžianti pakeisti vienas kodo
dalis kitomis. Sintaksė:
Paprastai tariant, parašius šią eilutę savo programoje, C++ kompiliavimo metu
suras visas kodo vietas, kur yra parašyta A
, ir vietoj jų įrašys B
. Galimas panaudojimo pavyzdys:
Taip pat naudojant #define
galima aprašyti tam tikras pagalbines funkcijas.
Pavyzdžiui:
Dažniausiai olimpiadose #define
naudojamas būtent sutrumpinti dažnai pasitaikančius kodo gabalus, kad sprendimą būtų galima parašyti gerokai greičiau.
typedef
Komanda typedef
naudojama apibrėžti naujiems tipams. Olimpiadose tai dažniausiai tiesiog būdas trumpinti kodą, tačiau, skirtingai nuo #define
, šis būdas naudojamas tik naujų tipų įvedimui. Sintaksė:
Naudingi pavyzdžiai:
cin ir cout pagreitinimas
C++ įprastos funkcijos cin
ir cout
yra patogios darbui su konsole, tačiau
turi tam tikrų problemų. Jei įvesties ir/ar išvesties kiekis yra didelis, šios
funkcijos gali būti gana lėtos. Tačiau tai gali būti pataisyta main
funkcijos pradžioje prirašant šias eilutes:
Taip pat galima ir išnaudoti jau aptartą #define
raktažodį:
endl prieš “\n”
Išvedant duomenis į konsolę su cout
yra įprasta naudoti endl
raktažodį, kai
norima padėti eilutės pabaigos simbolį. C++, kaip ir daugelyje programavimo
kalbų, išvedimas į konsolę vyksta maždaug taip: duomenys po truputį yra kaupiami
atmintyje (taip vadinamame buferyje) ir kartas nuo karto išvedami į konsolę
vienu ypu (ši operacija vadinama flush). Tai padeda paspartinti programos
veikimą, nes išvedimas į konsolę yra gerokai brangesnė operacija, negu duomenų
įrašymas į atmintį. Deja, endl
naudojimas ne tik spausdina naują eilutę,
tačiau ir iškart įvykdo flush operaciją, todėl programos veikimas gali
sulėtėti. Taigi, kuomet duomenų gali tekti išvesti daug, vietoj endl
verta
naudoti "\n"
, nes šiuo atveju kiekvienąkart nebus vykdoma flush operacija.
Vadinasi, cout << "Labas" << endl;
keisime į cout << "Labas" << "\n";
.
Viena biblioteka visoms gyvenimo reikmėms
Galbūt jau teko pastebėti, kad sprendžiant uždavinius, kuriems reikia daug
įvairiausių bibliotekų, programos #include
sąrašas gana greitai išsipučia.
Laimei, yra viena biblioteka, kurios viduje yra visos kitos, kokių tik gali
prireikti olimpiadose. Norint ją naudoti užtenka visus turimus #include
sakinius pakeisti į vieną:
Tiesa, ši biblioteka neveikia visose aplinkose, nes jai reikia būtent GCC kompiliatoriaus, tačiau Code::Blocks šį kompiliatorių naudoja kaip numatytąjį.
Naudingi C++11 dalykai
Nuo C++ 11 versijos kalboje atsirado nemažai naudingų dalykų, paspartinančių kodo rašymą bei palengvinančių skaitomumą. Čia aptarsime keletą iš jų.
C++11 įjungimas per Code::Blocks
- Viršutiniame meniu rinkitės
Settings -> Compiler
. - Atsidariusiame lange pasirinkite skiltį
Compiler settings
, o joje -Compiler Flags
. - Spustelėkite dešinį pelės mygtuką ant bet kurios eilutės, pasirinkite
New flag...
- Atsidariusiame lange užpildykite tokią informaciją:
- Name:
Enable C++11 flag
- Compiler flags:
-std=c++11
- Category:
Warnings
- Supersedes:
-std=c++98 -std=c++0x
- Name:
- Spustelėkite
OK
- Įsitikinkite, kad prie naujai pridėtos eilutės (su pavadinimu
Enable C++11 flag [-std=c++11]
) yra pažymėta varnelė. - Pasirinkite skiltį
Toolchain executables
. - Čia įsitikinkite, kad yra pasirinktas būtent Code::Blocks pateikiamas
kompiliatorius (
Compiler's installation directory
rodo į, pavyzdžiui,C:\Codeblocks\MinGW
).
Raktažodis auto
Senesnėse C++ versijose šalia kintamojo visada buvo privaloma rašyti ir jo tipą.
Nuo C++11 versijos atsirado naujas raktažodis auto
, kuri galima naudoti vietoj
kintamojo tipo, kuomet tipą gali nustatyti pats kompiliatorius automatiškai.
Pavyzdys:
Dar vienas pavyzdys, kuriame akivaizdžiai atsispindi auto
nauda:
Naujas for ciklas
C++11 versijoje atsirado naujas būdas rašyti for
ciklus. Jo sintaksė tokia:
Čia kolekcija
gali būti bet koks iteruojamas objektas (masyvas, vector
,
set
, map
ir t.t.). Konkretus pavyzdys:
Taip pat čia galime panaudoti ir jau aptartą auto
raktažodį:
“algorithm” biblioteka
Viena itin naudinga C++ pateikiama standartinė biblioteka - <algorithm>
. Joje
galima rasti nemažai naudingų funkcijų, kurias galima panaudoti, kad nereiktų
rašytis patiems ir gaišti laiko bei rizikuoti padaryti klaidų. Toliau aptarsime
kelis pavyzdžiui.
max ir min
Šios funkcijos priima du parametrus ir grąžina didesnį (arba mažesnį) iš jų. Pavyzdys:
swap
Ši funkcija priima du parametrus ir sukeičia jų reikšmes vietomis. Pavyzdys:
sort
Ši funkcija išrikiuoja nurodytą intervalą duomenų. Sintaksė: sort(begin, end,
cmp)
, čia begin
- nuoroda į rikiuojamų duomenų pradžią, end
- nuorodą į
rikiuojamų duomenų pabaigą, cmp
- funkcija, nusakanti rikiavimo tvarką (jei ši
funkcija nenurodyta, tai numatytas rikiavimo būdas yra didėjimo tvarka).
Pavyzdys:
Pastaba. Standartinių duomenų struktūrų pradžios ir pabaigos nuorodos gali
būti pasiekiamos naudojant .begin()
ir .end()
funkcijas, o masyvų: mas
ir
mas+n
, kur mas
- masyvo pavadinimas, o n
- jo dydis.
reverse
Ši funkcija apsuka duomenis pagal duotas pradžios iš pabaigos nuorodas.
Sintaksė: reverse(begin, end)
. Pavyzdys:
fill
Ši funkcija užpildo nurodytą duomenų intervalą viena reikšme. Sintaksė:
fill(begin, end, val)
. Pavyzdys:
next_permutation
Ši funkcija priima nuorodas į duomenų rinkinį (pvz. masyvą) ir pakeičia jį taip,
kad būtų gautas kitas (pagal leksikografinę tvarką) išdėstymas. Pavyzdžiui,
sąrašo [1, 2, 3]
kitas galimas išdestymas yra [1, 3, 2]
, o dar kitas -
[2, 1, 3]
. Funkcija grąžina true
, jei gautas išdestymas dar nėra paskutinis
galimas, ir false
kitu atveju. Dažniausiai ši funkcija naudojama norint
sugeneruoti duomenų rinkinio visas įmanomas permutacijas (t.y. išdėstymus).
Pavyzdys:
Pastaba. Prieš vykdant tokio tipo ciklą įsitikinkite, kad duomenys
išrikiuoti didėjimo tvarka, nes kitaip next_permutation
nesugeneruos visų
galimų išdėstymų (čia grėblys, ant kurio labai skaudžiai teko užlipti pačiam :) ).
Standartinės duomenų struktūros
Duomenų struktūros - vienas kertinių akmenų daugelio uždavinių sprendime. Geras jų išmanymas dažnai uždavinio sprendimą gali sutrumpinti kelis (jei ne keliasdešimt) kartų, o kai kurių uždavinių be jų išspręsti išvis neįmanoma. C++ standartinė biblioteka pateikia gausybę įvairių duomenų struktūrų, kurių geras įvaldymas kai kurių uždavinių sprendimo laiką gali sutrumpinti nuo valandos iki vos kelių minučių.
Kelių svarbiausių standartinių duomenų struktūrų pagrindai yra aptariami čia. Šiame skyriuje pažvelgsime į jau aptartas duomenų struktūras kiek įdėmiau, taip pat aptarsime ir dar daugiau naudingų duomenų struktūrų.
pair
Tai - ne visai tokios pat rūšies duomenų struktūra kaip kitos, aptariamos šiame
skyriuje, tačiau pair
dažnai būna naudojama kartu su kitomis struktūromis, tad
verta aptarti jos panaudojimą. Tipo aprašas: pair<T1, T2>
. Iš esmės pair
leidžia saugoti dvi reikšmes viename kintamajame (pirmosios tipas T1
,
antrosios - T2
). Pavyzdžiui, jei sprendžiame uždavinį, kuriame reikia dirbti
su taškų koordinatėmis, turime du sprendimo būdus. Pirmas būtų susikurti savo
struktūrą. Sprendimas atrodytų taip:
Kitas būdas yra išnaudoti pair
:
Pirmasis būdas yra skaitomesnis, tačiau antrasis aprašomas trumpiau ir greičiau.
Kadangi olimpiadose greitis yra itin svarbu, tai įprastai antras būdas būtų
laikomas geresniu. Žinoma, jei skaitomų duomenų struktūra yra sudėtingesnė, o ne
tik dvi reikšmės, tuomet verta mąstyti apie struct
naudojimą, tačiau šiuo
atveju tai neapsimoka.
Įdomumo dėlei, štai kaip galėtų atrodyti tas pats kodo pavyzdys išnaudojant jau aptartus “triukus”:
vector
vector
yra gana paprasta ir dažnai naudojama duomenų struktūra. Tai iš esmės
yra tas pats, kaip ir paprastas masyvas, tik yra vienas esminis skirtumas -
vector
dydžio iš anksto nebūtina apibrėžti, jis gali kisti laikui bėgant.
Pagrindinės vector
palaikomos funkcijos:
.push_back(x)
- pridėti reikšmęx
į vektoriaus galą;.pop_back()
- išimti paskutinę reikšmę iš vektoriaus;.front()
- grąžina vektoriaus priekyje esančią reikšmę;.back()
- grąžina vektoriaus gale esančią reikšmę;.assign(n, val)
- pakeičia vektoriaus dydį įn
ir visą jį užpildo reikšmęval
;.resize(n)
- pakeičia vektoriaus dydį įn
.
Taip pat vector
palaiko funkcijas, kurias palaiko beveik visos kitos
standartinės duomenų struktūros (toliau jos bus vadinamos standartinėmis
funkcijomis):
.size()
- grąžina vektoriuje esančių elementų kiekį;.empty()
-true
, jei vektorius tuščias,false
kitu atveju;.clear()
- išvalo vektorių (t.y. ištrina visus elementus).
Pagrindinės operacijos push_back
ir pop_back
abi veikia per O(1)
laiko.
Taigi, elementų pridėjimas į galą bei išėmimas iš galo vektoriuje yra itin
greita operacija. Taip pat, vektorių galima indeksuoti taip pat, kaip ir masyvą
(t.y. galima rašyti v[1]
norint gauti antrąjį elementą). Indeksavimo
sudėtingumas taip pat yra O(1)
.
set
set
(liet. aibė) yra duomenų struktūra, kurioje duomenys visada yra
išrikiuoti ir nėra pasikartojančių elementų. Pagrindinės funkcijos:
.insert(x)
- įdeda reikšmęx
į aibę. Jei tokia reikšmė jau yra, nieko neįvyksta;.erase(x)
- ištrina reikšmęx
iš aibės. Jei tokiosreikšms nėra, nieko neįvyksta;.find(x)
- grąžina iteratorių, rodantį į aibės elementą, lygųx
. Jeix
aibje nėra, grąžinamas pabaigos iteratoriusend()
;.lower_bound(x)
- grąžina nuorodą į pirma elementą aibje, kuris yra nemažesnis užx
;.upper_bound(x)
- grąžina nuorodą į pirma elementą aibėje, kuris yra didesnis užx
;- Standartinės funkcijos.
Visų šių funkcijų sudėtingumas yra O(log(N))
. Kodo pavyzdys:
Vienas svarbus pastebėjimas yra tas, kad su aibėmis, skirtingai nei su vektoriais, negalima naudoti indeksavimo. Vadinais, gauti elemento pagal indeksą ar pan. aibėje neįmanoma (indekso sąvoka aibėje išvis nelabai yra apibrėžta). Todėl aibės dažniausiai naudojamos surinkti elementus be pasikartojimų ar patikrinti, ar koks nors elementas jau yra aibėje (pvz. į aibę galima dėti grafe aplankytų viršūnių numerius ir taip gana greitai patikrinti, ar konkreti viršūnė jau buvo aplankyta, ar ne).
map
map
(dar vadinamas asociatyviu masyvu) yra galinga duomenų struktūra, kiek
primenanti vektorius ar masyvus. Skirtingai nei jau aptartos duomenų strukūros,
map
turi du tipo parametrus. Aprašas map<TK, TV>
reiškia asociatyvų masyvą,
kurio raktų tipas yra TK
, o rekšmių tipas yra TV
. Paprastai tariant,
raktas yra tai, kas naudojama indeksuojant duomenų struktūra (t.y. tai, ką
rašome tarp laužtinių skliaustų), o reikšmė - tai, ką gauname panaudoję
indeksavimą. Pavyzdžiui, masyvų ar vektorių atvejų indeksavimui visada naudojame
sveikuosius skaičius, tačiau map
atveju galime naudoti ką tik norime.
Pagrindinės map
funkcijos yra šios:
.erase(x)
- ištrina elementą su raktux
. Jei tokio rakto nėra, nieko nenutinka;.find(x)
- grąžina iteratorių, rodantį į elementą, kurio raktas yrax
. Jei raktox
nėra, grąžinamas pabaigos iteratoriusend()
;.lower_bound(x)
- grąžina nuorodą į pirma elementą, kurio raktas yra nemažesnis užx
;.upper_bound(x)
- grąžina nuorodą į pirma elementą, kurio raktas yra didesnis užx
;- Standartinės funkcijos.
map
veikia labai panašiai į set
, nes čia raktai taip pat visada būna
išrikiuoti didėjimo tvarka bei nebūna pasikartojančių reikšmių. Vienintelis
skirtumas lyginant su set
yra tas, kad kiekvienas raktas turi susijusią
reikšmę. Operacijų sudetingumai, kaip ir set
, yra O(log(N))
(įskaitant ir indeksavimo su laužtiniais skliaustais!).
Panagrinėkime pavyzdį. Tarkime, kad programos įvestis - n
taškų. Mūsų tikslas -
suskaičiuoti, kiek kartų kiekvienas taškas buvo įvestas. Tam susikursime
asociatyvų masyvą map<pii, int> cnt
, kuriame raktai bus patys taškai, o
reikšmės - kiek kartų konkretus taškas pasikartoja įvestyje. Kodas gali
atrodyti maždaug taip:
queue
queue
(liet. eilė) - tai ganėtinai paprasta duomenų struktūra. Kaip gali
išduoti pavadinimas, jos veikimo principas panašus į įprastas gyvenime
sutinkamas eiles, pavyzdžiui, žmonių eilę parduotuvės kasoje - naujai atėję
žmonės stoja į eilės galą, o aptarnavimas vyksta nuo eilės priekio. Tokio tipo
duomenų struktūros paprastai vadinamos FIFO (angl. First In First Out).
Pagrindinės eilės palaikomos funkcijos:
.push(x)
- įdeda reikšmęx
į eilės galą;.front()
- grąžina reikšmę, esančią eils priekyje (bet jos neišima);.pop()
- išima eilės priekyje esančią reikšmę;- Standartinės funkcijos.
Mažas eilės veikimo pavyzdys:
Keli svarbūs niuansai: eilė nėra indeksuojama, per ją negalima iteruoti (t.y. su
for(auto x : Q)
ciklus per eilės reikšmes perbėgti neįmanoma, reikia rašyti
tokį while
ciklą, koks pateiktas pavyzdyje). Taip pat: visų eilės funkcijų
sudėtingumas yra O(1)
.
stack
stack
- duomenų struktūra, priešinga eilei. Jei eilėje anksčiau įdeti
elementai anksčiau yra ir išimami, tai steke - atvirkščiai - paskutiniai įdėti
elementai yra išimami pirma. Tokio tipo duomenų struktūros vadinamos LIFO
(angl. Last In First Out). Steką lengviausia įsivaizduojant pasitelkiant
lėkščių krūvos analogiją: lėkštės yra dedamos ant krūvos viršaus, o nuimamos
taip pat nuo krūvos viršaus. Taip darant išeina, kad paskutinės padėtos lėkštės yra
nuimamoms pirmosios. Pagrindinėės steko funkcijos:
.push(x)
- įdeda reikšmęx
į steko viršų;.top()
- grąžina reikšmę, esančia steko viršuje (bet jos neišima);.pop()
- išima steko viršuje esančią funkciją;- Standartinės funkcijos.
Mažas steko veikimo pavyzdys:
Stekas, kaip ir eilė, nėra indeksuojamas ir per jį negalima iteruoti. Steko
funkcijų sudėtingumas taip pat yra O(1)
.
deque
deque
(angl. double-ended queue, liet. dekas) yra duomenų struktūra, analogiška eilei.
Vienintelis skirtumas - į deką reikšmes galima dėti tiek iš priekio, tiek iš
galo. Taip ir ir išimti reikšmes galima iš abiejų galų. Pagrindinės deko
funkcijos:
.push_front(x)
- įdeda reikšmęx
į deko priekį;.push_back(x)
- įdeda reikšmęx
į deko galą;.front()
- grąžina deko priekyje esančią reikšmę (bet jos neišima);.back()
- grąžina deko gale esančią reikšmę (bet jos neišima);.pop_front()
- išima deko priekyje esančią reikšmę;.pop_back()
- išima deko gale esančią reikšmę;- Standartinės funkcijos.
Mažas deko veikimo pavyzdys:
Dekas, skirtingai nei eilė ar stekas, yra indeksuojamas ir iteruojamas. Visų
operacijų sudėtingumas yra O(1)
.
priority_queue
priority_queue
(liet. prioritetinė eilė) veikia iš dalies panašiai kaip ir
eilė, tačiau turi vieną esminį, labai svarbų ir naudingą skirtumą - joje į
priekį visada “nustumiami” didžiausi elementai (tiesa, šia tvarką galima
keisti). Prioritetinės eilės operacijos:
.push(x)
- įdeda reikšmęx
į eilę;.top()
- grąžina eilės priekyje esančią reikšmę (bet jos neišima);.pop()
- išima eilės priekyje esančią reikšmę;- Standartinės funkcijos.
Prioritetinės eilės naudojimo pavyzdys:
Verta pastebėti, kad, skirtingai nei paprastoje eilėje, prioritetinės eilės
push
ir pop
funkcijų sudėtingumas yra ne O(1)
, o O(log(N))
.
Kitas svarbus aspektas kalbant apie prioritetines eiles yra rikiavimo tvarkos
nustatymas. Kaip matėme, numatytuoju atveju prioritetinė eilė į priekį “stumia”
didžiausius elementus. Norint tą pakeisti, eilės tipo aprašą reikia pakeisti
tokiu: priority_queue<T, vector<T>, greater<T>>
. Šis užrašas garantuoja, kad į
eilės priekį bus “stumiami” maži elementai. Žemiau pateiktas tas pats pavyzdys,
kaip ir ką tik buvęs, tik su eile, “stumiančia” į priekį mažus elementus:
Dvejetainė paieška
C++ standartinė biblioteka taip pat turi dvi patogias funkcijas, gebančias
atlikti dvejetainę paiešką kokioje nors kolekcijoje. Šios funkcijos yra
lower_bound
ir upper_bound
.
lower_bound(x)
- tai funkcija, kuri išrikiuotoje sekoje randa pirmą elementą,
kuris yra nemažesnis už x
. Panašiai upper_bound
yra funkcija, kuri
išrikiuotoje sekoje randa pirmą elementą, kuris yra griežtai didesnis už
x
. Šios funkcijos naudojamos kiek skirtingai, priklausomai nuo to, kokioje
duomenų struktūroje vykdoma paieška.
Paieška vektoriuje
Visų pirma panagrinėkime dvejetainės paieškos vektoriuje atvejį. Būtina sąlyga
prieš vykdant paiešką - vektorius privalo būti išrikiuotas. Tada pati
paieška vykdoma pasitelkiant funkcijas lower_bound(begin, end, val)
bei
upper_bound(begin, end, val)
. Štai pavyzdys:
Verta pastebėti gana neįprastą rašymo būdą: *it1
. Taip rašoma todėl, kad
lower_bound
ir upper_bound
funkcijos grąžina iteratorių, rodantį į tam
tikrą vektoriaus elementą. Apie iteratorius čia daugiau nebus prasiplečiama
(galima paieškoti papildomos literatūros internete), tačiau esmė tokia, kad,
norint išgauti reikšmę, į kurią rodo iteratorius, naudojama šiame pavyzdyje
matyta sintaksė su žvaigždute. Tiesa, su tokiais užrašais reikia elgtis
atsargiai, nes, jei reikiama reikšmė nebūtų rasta (t.y. funkcija grąžintų
v.end()
), tai rašymas *it
“nulaužtų” programą.
Vektoriaus atveju taip pat galima gauti rasto elemento indeksą:
Paieška aibėje bei asociatyviame masyve
Paieška aibėje (set
) bei asociatyviame masyve (map
) yra kiek paprastesnė nei
vektoriaus atveju. Šiuo atveju nereikia nuorodų į pradžią ir pabaigą, kadangi
lower_bound
bei upper_bound
funkcijos yra “įsiųtos” į pačias duomenų
struktūras. Štai kodo pavyzdys:
Čia vėlgi verta pabrėžti naujai atsiradusią sintaksę it->first
bei
it->second
. Kaip jau buvo aptarta anksčiau, it
yra iteratorius, rodantis į
rastą rezultatą. Kadangi map
duomenų struktūroje kiekvienas įrašas yra pair
tipo (t.y. pora iš rakto ir reikšmės), tai, norėdami pasiekti konkrečią rastą
rakto reikšmę, galėtume rašyti (*it).first
, ir analogiškai su reikšme. C++
būtent šiai sintaksei turi santrumpą: it->first
. Tai ir buvo panaudota
pavyzdyje.
Vienas esminis skirtumas nuo paieškos vektoriuje yra tas, kad vykdant paiešką su
set
ir map
neįmanoma gauti rasto elemento indekso, nes indeksai šiose
duomenų struktūrose tiesiog nėra apibrėžti.
Patikrinti, ar elementas buvo rastas, galima taip pat, kaip ir vektoriaus
atveju: tereikia gautą iteratorių palyginti su S.end()
(ar M.end()
map
atveju).