Matematická olympiáda – kategorie P

Řešení úloh ústředního kola (2. soutěžní den) 53. ročníku

P-III-4 Psíci

Na poskakování psíků se můžeme dívat jako na hru. Stav hry lze jednoznačně popsat pozicí obou psíků. Skok psíků představuje tah. Když oba psíci skočí, změní stav hry. Povolené stavy hry budou ty, které odpovídají povoleným pozicím psíků. Pro každý stav hry budeme zkoumat, kolika tahy se do něho dá dostat z počátečního stavu (když jsou oba psíci na výchozích místech). Tento počet tahů budeme označovat jako vzdálenost daného stavu.

Vzdálenost počátečního stavu je 0. Všechny stavy, do nichž se lze z něho dostat jedním tahem, budou ve vzdálenosti 1. Nyní projdeme všechny stavy ve vzdálenosti 1 a hledáme, do kterých nových stavů se z nich dostaneme - ty budou zjevně ve vzdálenosti 2. Takto můžeme analogicky pokračovat pro stavy ve vzdálenosti 3, 4, ... Skončíme, když najdeme koncový stav (oba psíci jsou na svých cílových místech), nebo když už nenajdeme žádný nový stav. Tato technika prohledávání stavů se nazývá prohledávání do šířky.

Otázkou zůstává, jak pro každý stav určit, do kterých dalších (sousedních) stavů se z něho lze dostat jedním tahem. Pomohlo by nám, kdybychom uměli pro každé políčko na louce určit čísla jeho sousedů. Sousední stavy bychom potom určili snadno. Ze všech možných pohybů oběma psíky 6 směry (36 možností) vyškrtáme skákání stejným směrem, skákání na bodláky, skok některého psíka mimo louku a současný skok obou psíků na totéž políčko.

Abychom našli sousední políčka snadněji, ukážeme si, jak se dá šestiúhelníkový plán louky reprezentovat v obyčejném dvojrozměrném poli. Na políčku 1 si zvolíme dva směry. Jeden určuje rostoucí směr první souřadnice, druhý směr druhé souřadnice. Takto jsme přiřadili každému políčku souřadnice x, y, kterým odpovídají indexy v obyčejném dvojrozměrném poli. V dvojrozměrném poli je už nalezení sousedů lehké. Konkrétně při naší volbě souřadnicových os budou mít sousedi políčka (x,y) souřadnice (x,y+1), (x-1, y), (x-1, y-1), (x, y-1), (x+1, y) a (x+1, y+1).

Souřadnicový systém v šestiúhelníkové síti
Jak zjistíme pro políčko s číslem k jeho souřadnice? Všimněte si, že spirálu můžeme rozložit na vrstvy šestiúhelníkového tvaru. Nejprve určíme, na kolikáté vrstvě spirály se k nachází, potom stranu na této vrstvě, pozici políčka na straně a je to.

Nultá vrstva spirály obsahuje 1 políčko, i-tá vrstva pak 6i, jelikož každá vrstva má 6 stran a na každé straně je i políček. Celkový počet políček ve spirálách 0...v je tedy 1+(6*1+...+6*v) = 3v2+3v+1.

A jak zjistíme, kde se nachází políčko k? Nejdříve spočteme vrstvu - najdeme kladné řešení rovnice k=3v2+3v+1 a zaokrouhlíme ho nahoru. Po vyřešení dostaneme v=[-1/2+(k/3-1/12)1/2]. (Jednodušší a trochu pomalejší postup: zvyšujeme v, dokud počet políček nedosáhne k.)

Když už víme vrstvu v, kde se políčko nachází, pořadové číslo políčka ve vrstvě (číslováno od 0) dopočítáme snadno: odečteme od k celkový počet políček na dřívějších vrstvách a ještě 1. Na vrstvě v má každá strana v políček, rozdíl tedy stačí vydělit v. Číslo strany s bude podíl, pozice na straně p bude zbytek po tomto dělení.

Již umíme pro dané políčko k spočítat jeho vrstvu v, stranu s a pozici na straně p. Z těchto hodnot dostaneme souřadnice podle následující tabulky (platí pro v>=1):

s  x       y
0  +v-1-p  +v    
1    -1-p  +v-1-p
2  -v        -1-p
3  -v+1+p  -v    
4    +1+p  -v+1+p
5  +v        +1+p

Implementace

Stav hry je jednoznačně reprezentován souřadnicemi x1, y1, x2, y2 obou psíků. Při prohledávání do šířky si pamatujeme seznam stavů, které jsme již dosáhli, ale ještě jsme z nich nezkoušeli prohledávat nové vrcholy. K tomu nám poslouží fronta. Dále potřebujeme umět pro každý stav rychle zjistit, zda jsme ho ještě neviděli, už viděli, nebo zda se do něj nedá jít (bodláky). K tomu používáme čtyřrozměrné pole, kde je to přímo zapsáno. (Přesně totéž se dalo reprezentovat dvojrozměrným polem, které bychom indexovali původními souřadnicemi. Navíc bychom ale potřebovali umět ze souřadnic určit původní číslo políčka.)

Abychom nemuseli při prohledávání do šířky stále kontrolovat, zda se nedostaneme ven z louky, postavíme okolo celé louky bodláky. Zakážeme také stavy, v nichž by byli oba psíci na stejném místě.

Časová i paměťová složitost prohledávání je lineární vzhledem k velikosti tohoto prostoru, tedy O(N2).

program Psici; const EPS = 1.0E-6; {max. chyba vzniklá v reálných číslech} MAX_V = 16; {největší možná vrstva (i se zarážkou)} MAX_STAVU = MAX_V*MAX_V*MAX_V*MAX_V; INF = 299999; {největší možný počet skoků} MAX_SMER = 6; {počet směrů, jimiž se mohou psíci hýbat} type TSour = record x,y : integer; end; {souřadnice jednoho psíka} TStav = record p1, p2 : TSour; end; {souřadnice dvou psíků} {prostor pro 2 psíky: [x1,y1,x2,y2] říká, zda tam mohou být} TMrizka = array [-MAX_V..MAX_V,-MAX_V..MAX_V, -MAX_V..MAX_V,-MAX_V..MAX_V] of integer; function dekVrstvu(k: integer): integer; {číslo vrstvy, kde se k nachází} begin dekVrstvu:= trunc(0.5 + sqrt(k/3.0 - 1.0/12.0 - EPS) ); end; function zakVrstvu(v: integer): integer; {poslední prvek na dané vrstvě} begin zakVrstvu:= 3*v*v - 3*v +1; end; function dekoduj(k: integer): TSour; {Dekóduj číslo políčka na souřadnice} var v, pv, s, ps: integer; sour: TSour; begin if k=1 then begin {pro k=1 (vrstva 0) naše vzorce nefungují} sour.x:= 0; sour.y:= 0; end else begin v:= dekVrstvu(k); pv:= k - (3*v*v - 3*v + 1); {pozice ve vrstvě} s:= pv div v; {strana šestiúhelníka, kde se pv nachází} ps:= pv mod v; {pozice na straně šestiúhelníka} case s of 0: begin sour.x:= +v-1-ps; sour.y:= +v ; end; 1: begin sour.x:= -1-ps; sour.y:= +v-1-ps; end; 2: begin sour.x:= -v ; sour.y:= -1-ps; end; 3: begin sour.x:= -v+1+ps; sour.y:= -v ; end; 4: begin sour.x:= +1+ps; sour.y:= -v+1+ps; end; 5: begin sour.x:= +v ; sour.y:= +1+ps; end; else writeln('Velká chyba v dekoduj'); end; end; dekoduj:=sour; end; var n, m, s1, t1, s2, t2: integer; {vstup} v: integer; {max. použitá vrstva pro dané n} A: TMrizka; {prohledávaný prostor} F: array [0..MAX_STAVU] of TStav; {fronta} {zakáže všechny situace, kde se nachází bodlák na daném místě} procedure pridejBodlak(b: TSour); var x, y: integer; begin for x:= -v to v do for y:= -v to v do begin A[x, y, b.x, b.y]:= INF; {2. psík stojí na bodláku} A[b.x, b.y, x, y]:= INF; {1. psík stojí na bodláku} end; end; {vyčistí celý prohledávaný prostor} procedure inicializace; var x1, y1, x2, y2, i, last: integer; begin {vyčištění prostoru} for x1:= -v to v do for y1:= -v to v do for x2:= -v to v do for y2:= -v to v do A[x1, y1, x2, y2]:= -1; {zarážky při okrajích - přidáme umělé bodláky} last:= zakVrstvu(v+1); for i:=n+1 to last do pridejBodlak(dekoduj(i)); {zakážeme být oběma psíkům na stejném místě} for x1:= -v to v do for y1:= -v to v do A[x1, y1, x1, y1]:= INF; end; {posunutí souřadnice daným směrem} function pohyb(a: TSour; s: integer): TSour; const smer: array [1..MAX_SMER] of TSour = ( (x: 0; y: 1), (x:-1; y: 0), (x:-1; y:-1), (x: 0; y:-1), (x: 1; y: 0), (x: 1; y: 1) ); begin a.x:= a.x+smer[s].x; a.y:= a.y+smer[s].y; pohyb:=a; end; function pracuj: integer; {prohledávání prostoru, kde se mohou psíci nacházet} var zac: integer; {pozice prvního prvku ve frontě} kon: integer; {pozice za posledním prvkem ve frontě = volné místo} i, j, vzd: integer; p1, p2, q1, q2: TSour; pt1, pt2: TSour; {dekódované pozice skrýší pro psíky} begin zac:=0; kon:=1; {přidáme počátek do fronty} p1:= dekoduj(s1); p2:= dekoduj(s2); F[zac].p1:= p1; F[zac].p2:= p2; A[p1.x, p1.y, p2.x, p2.y]:= 0; {začínáme ve vzdálenosti 0} pt1:=dekoduj(t1); pt2:=dekoduj(t2); while zac<>kon do begin {vybereme stav z fronty a najdeme k němu vzdálenost} p1:=F[zac].p1; p2:=F[zac].p2; vzd:= A[p1.x, p1.y, p2.x, p2.y]; inc(zac); {zkoušíme všechny kombinace směrů, kam mohou skákat} for i:=1 to MAX_SMER do for j:=1 to MAX_SMER do begin if i=j then continue; {nemohou skákat stejně} {nové pozice psíků} q1:= pohyb(p1, i); q2:= pohyb(p2, j); {již navštívená pozice resp. zarážka ?} if A[q1.x, q1.y, q2.x, q2.y] >= 0 then continue; {nově objevený stav -> přidáme do fronty} A[q1.x, q1.y, q2.x, q2.y]:= vzd+1; F[kon].p1:=q1; F[kon].p2:=q2; inc(kon); {našli jsme koncový stav?} if (pt1.x=q1.x) and (pt1.y=q1.y) and (pt2.x=q2.x) and (pt2.y=q2.y) then begin pracuj:=vzd+1; exit; end; end; end; pracuj:= -1; end; var x, i: integer; begin while true do begin read(n, m); if (n=0) and (m=0) then break; v:= dekVrstvu(n); inicializace(); read(s1, t1, s2, t2); for i:=1 to m do begin read(x); pridejBodlak(dekoduj(x)); end; if (s1=t1) and (s2=t2) then x:=0 {speciální případ} else x:=pracuj; if x>=0 then writeln(x) else writeln('nelze'); end; end.

P-III-5 AttoSoft

Označme S=l1+...+lN počet dní, které AttoSoft potřebuje na dokončení všech programů. Poslední program tedy dokončíme po S dnech.

Pro každý program spočítáme pokutu, kterou bychom za něj zaplatili, kdybychom ho dokončili až po S dnech. Program s nejmenší takovou pokutou zařadíme do rozvrhu jako poslední. Je-li to program číslo i, zbývá nám naplánovat všechny zbývající programy na prvních S-li dní, což provedeme stejným způsobem (tzn. opět vybereme jako poslední program s nejnižší pokutou po S-li dnech, atd.)

Správnost uvedeného algoritmu dokážeme indukcí vzhledem k počtu programů, které potřebujeme dokončit. Pokud je třeba dokončit jeden program, existuje jen jediný možný rozvrh, a náš algoritmus tedy funguje jistě správně.

Nechť tedy počet programů, které je třeba dokončit, je N a nechť pro libovolný menší počet programů náš algoritmus funguje správně. Označme G řešení získané naším algoritmem (v tomto řešení je posledním programem program číslo i). Nechť existuje jiné, levnější řešení O, které končí programem číslo j. Jestliže i=j, pak rozdíl mezi G a O musí být v pořadí prvních N-1 programů. Podle indukčního předpokladu však toto pořadí v řešení G je optimální, proto řešení O nemůže být levnější.

V opačném případě vytvoříme nový rozvrh O' následujícím způsobem. Nechť rozvrh O dokončuje programy v pořadí o1,o2,...,oN a nechť ok=i. Podle rozvrhu O' dokončíme programy v následujícím pořadí: o1,o2,...,ok-1,ok+1,...,oN,i. Všimněte si, že řešení O' je nejvýše tak drahé, jako řešení O. Pokuta za programy ok+1,...,oN je totiž nižší než v řešení O, neboť je dokončíme dříve (pokuta roste s počtem dní po termínu). Navíc pokuta za program i dokončený po S dnech určitě nepřesahuje pokutu za program oN dokončený po S dnech, jelikož program i jsme vybrali tak, aby tato pokuta byla nejmenší možná.

Řešení O' ale nemůže být levnější než řešení G (platí tu stejný argument, jako v předchozím případě). Proto ani řešení O nemůže být levnější než G. Dokázali jsme, že žádné levnější řešení než G neexistuje, řešení určené naším algoritmem je tedy optimální.

V každém kroku algoritmu musíme spočítat příslušnou pokutu pro každý program, který jsme dosud nezařadili do rozvrhu. Proto časová složitost algoritmu je O(N2).

Při výpočtu si ještě musíme dávat pozor na to, že pokuta může být až 5 000 * 100 0003 + 5 000 * 100 0002 + ... + 5 000, tedy přibližně 5 * 1018, a tak velké číslo se již nevejde do longintu a nemůžeme si dovolit použít ani typ real, jelikož potřebujeme i u tak velkých čísel rozlišovat rozdíly na řádu jednotek, Většina překladačů naštěstí nabízí 64-bitový celočíselný typ - v Turbo Pascalu je to typ comp, ve Free Pascalu například typ QWord.

program Attosoft; const MAXN = 10000; var a,b,c,d,l: array [1..MAXN] of longint; pouzite: array [1..MAXN] of boolean; N,S: longint; function Cena(prog:integer; den: comp): comp; begin cena:=((a[prog]*den+b[prog])*den+c[prog])*den+d[prog]; end; {function Cena} procedure Nacti; var i: integer; begin readln(N); S:=0; for i:=1 to N do begin readln(l[i],a[i],b[i],c[i],d[i]); S:=S+l[i]; end; end; {procedure Nacti} procedure Spocitej_rozvrh(d: longint); var minprog: integer; min,cc: comp; i: integer; begin {najdi nepoužitý program, který má nejnižší pokutu po d dnech} minprog:=-1; for i:=1 to N do begin if not pouzite[i] then begin cc:=Cena(i,d); if (minprog=-1) or (cc<min) then begin min:=cc; minprog:=i; end; end; end; if minprog>-1 then begin {označ program jako použitý} pouzite[minprog]:=true; {sestav zbytek rozvrhu} Spocitej_rozvrh(d-l[minprog]); {vypiš poslední program na konci rozvrhu} writeln(minprog); end; end; {procedure Spocitej_rozvrh} var i:integer; begin Nacti; for i:=1 to N do pouzite[i]:=false; Spocitej_rozvrh(S); end.