Ř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).

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 x
1, y
1, x
2, y
2 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=l
1+...+l
N 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.