Řešení úloh oblastního kola 50. ročníku
P-II-1 Trojúhelníky
Předpokládejme, že máme délky dřevěných tyček setříděny podle velikosti,
tj. d
1<d
2<...<d
N. Potom trojice indexů i<j<k určuje
tyčky tvořící trojúhelník právě tehdy, pokud d
k<d
i+d
j.
Zbývající dvě trojúhelníkové nerovnosti jsou totiž splněny triviálně,
neboť d
i,d
j<d
k. Pro libovolnou dvojici indexů i<j označme
symbolem l(i,j) největší číslo k takové,
že d
k<d
i+d
j; speciálně tedy platí l(i,j)>= j.
Zvolenou dvojici indexů i<j lze doplnit na trojici i<j<k určující
trojúhelník právě těmi k, pro která platí j<k<=l(i,j).
Pro pevnou dvojici indexů i a j tedy existuje právě
l(i,j)-j takových k, že tyčky s indexy i<j<k tvoří trojúhelník.
K určení počtu trojúhelníků proto stačí spočítat hodnoty l(i,j)
pro všechna i<j a sečíst výrazy l(i,j)-j.
Z definice l(i,j) plyne,
že N=l(i,N)>= l(i,N-1) >= l(i,N-2) >= ... >= l(i,i+1).
Náš program bude pracovat následovně: Pro každé i spočteme
hodnoty l(i,N-1),l(i,N-2),...,l(i,i+1) a současně budeme počítat
součet ( l(i,N-1) - (N-1) ) + ... + ( l(i,i+1) - (i+1) ) ,
který představuje počet trojúhelníků, jejichž nejkratší strana
má délku di.
Hodnotu l(i,j) spočítáme tak, že hodnotu l(i,j+1) budeme
zmenšovat o jedničku tak dlouho, dokud dl(i,j)>=di+dj.
Protože celkový počet zmenšení o jedničku během výpočtu
hodnot l(i,N-1),l(i,N-2),...,l(i,i+1) je nejvýše N-2, je
doba výpočtu pro jedno pevné i lineární v N.
Celková doba výpočtu pro všech N-2 možných hodnot
i je tedy O(N2). V tomtéž čase můžeme provést i úvodní setřídění
zadaných délek tyček a tedy časová složitost našeho algoritmu
je O(N2) a paměťová pak O(N).
program trojuhelniky; { P-II-1 }
const MAXN=1000;
var N:word; { počet tyček }
T:longint; { počet trojúhelníků }
i,j,k:word;
d:array[1..MAXN] of real; { délky tyček }
e:real;
begin
read(N);
for i:=1 to N do read(d[i]);
for i:=1 to N-1 do { setřídíme délky tyček }
for j:=i+1 to N do
if d[i]>d[j] then
begin
e:=d[i]; d[i]:=d[j]; d[j]:=e
end;
T:=0;
for i:=1 to N-2 do { i - nejkratší tyčka z trojice }
begin
j:=N-1; k:=N; { j - druhá nejkratší tyčka z trojice }
repeat
while (j<k) and (d[k]>=d[i]+d[j]) do { podmínku (j<k) lze vypustit }
dec(k); { hledáme nejdelší tyčku do trojice }
T:=T+k-j;
dec(j);
until j=i;
end;
writeln(T);
end.
P-II-2 Robot
Nejprve si rozmysleme, jak bychom zadanou úlohu řešili, kdybychom
chtěli najít nejkratší cestu robota mezi dvěma zadanými políčky.
Algoritmus pro řešení této úlohy je znám pod názvem prohledávaní
do šířky nebo též algoritmus vlny. Každému políčku v průběhu výpočtu
přiřadíme číslo, jež udává minimální počet kroků, které robot
potřebuje k přemístění z počátečního políčka na uvažované políčko.
Algoritmus pracuje ve fázích. Nejprve počátečnímu políčku přiřadí
nulu. V i-té fázi přiřadí číslo i všem políčkům, která sousedí s nějakým
políčkem, kterému bylo přiřazeno číslo i-1 a kterým jsme dosud
žádné číslo nepřiřadili. Je zřejmé, že takto přiřazená
čísla určují minimální počet kroků potřebný
k přemístění robota z počátečního políčka.
Nyní si rozmyslíme, jak lze tento algoritmus modifikovat tak, aby řešil
úlohu ze zadání. V i-té fázi nebudeme číslovat políčka ve vzdálenosti
i kroků, ale políčka, na které se lze přesunout cestou s i změnami
směru. Přesněji v i-té fázi budeme číslovat ta políčka, která leží
ve stejném řádku nebo sloupci jako některé políčko s číslem i-1 a nejsou
od něj v tomto řádku či sloupci oddělena překážkou. Správnost tohoto
algoritmu je zřejmá. Zbývá domyslet detaily jeho implementace. Políčka
si budeme uchovávat v poli tak, že políčka se stejným číslem budou tvořit
souvislé úseky a políčka s nižším číslem budou předcházet políčkům s vyšším
číslem. Pokud přijdeme v průběhu fáze na dosud neočíslované políčko,
zařadíme ho na konec pole. V průběhu algoritmu vyjmeme vždy první
nezpracované políčko z pole a prohledáme jeho řádek a sloupec. Pole,
se kterým se pracuje právě popsaným způsobem, se obvykle nazývá fronta -
políčka se stavějí na konec fronty a čekají, až na ně přijde řada (budou
zpracována). Nechť M a N jsou rozměry čtvercové sítě, potom zpracování
každého z MN políček vyžaduje čas O(M+N). Celková časová složitost
našeho algoritmu by tedy byla O(M2N+MN2).
Čas potřebný k výpočtu však lze ještě zlepšit. Jeden a tentýž souvislý úsek
v řádku bez překážek je totiž prohledáván několikrát - pro každé políčko
z tohoto souvislého úseku jednou. Přitom by ale stačilo prohledat ho
jenom z toho políčka, na které přijdeme nejdříve. Proto si budeme u každého
políčka pamatovat, zda jsme prohledali souvislý úsek řádku (sloupce)
bez překážek, ve kterém se toto políčko nachází. Před prohledáváním
řádku (sloupce) otestujeme tento příznak a pokud jsme již příslušný
souvislý úsek prohledali, tak vyjmeme ke zpracování další políčko z fronty.
Všimněte si, že pro každé políčko je třeba udržovat dva příznaky - jeden
pro souvislý úsek bez překážek v řádku a jeden pro úsek ve sloupci.
Celkový čas strávený načítáním vstupních dat, prací s frontou a výpisem
řešení je zřejmě O(MN). Zbývá stanovit čas
potřebný k prohledávání řádků a sloupců. Každý souvislý úsek
bez překážek je prohledán právě jednou a součet délek všech
souvislých úseků bez překážek je určitě nejvýše MN (tolik je
totiž políček ve čtvercové síti). Prohledání jednoho souvislého
úseku lze snadno provést v čase lineárním v jeho délce a tedy i
celková časová složitost našeho algoritmu je O(MN); paměťová
složitost je též O(MN).
program robot; { P-II-2 }
const MAX=20; { maximální rozměry čtvercové sítě v místnosti }
type policko = record
x,y : word;
end;
var sirka, vyska: word; { rozměry místnosti }
prekazky: array[1..MAX,1..MAX] of boolean;
{ rozložení překážek v místnosti }
navstiveno, svisle, vodorovne: array[1..MAX,1..MAX] of boolean;
{ indikátory navštívení políček v místnosti }
predchozi: array[1..MAX,1..MAX] of policko;
{ předchozí políčko na optimální cestě }
start, cil: policko;
{ počáteční a cílové políčko }
fronta: array[1..MAX*MAX] of policko;
zpracovano, vefronte: word;
{ fronta prohledávání do šířky }
x, y: word; { pomocné proměnné }
i: integer;
procedure vypis( x, y: word);
var x0, y0: word;
k: integer;
begin
x0:=predchozi[x,y].x; { předchozí políčko na optimální cestě }
y0:=predchozi[x,y].y;
if ( x0 = predchozi[x0,y0].x ) and ( y0 = predchozi[x0,y0].y ) then
begin
{ Jsme na počátečním políčku ... }
if x0 < x then write('Program pro robota (počáteční natočení DOLŮ):');
if x0 > x then write('Program pro robota (počáteční natočení NAHORU):');
if y0 < y then write('Program pro robota (počáteční natočení DOPRAVA):');
if y0 > y then write('Program pro robota (počáteční natočení DOLEVA):');
end
else
begin
{ Nejprve vypíšeme předchozí políčko a potom směr našeho otočení }
vypis(x0,y0);
k:=(x-x0)*(y0-predchozi[x0,y0].y)-(x0-predchozi[x0,y0].x)*(y-y0);
if k > 0 then write(' <DOPRAVA>');
if k < 0 then write(' <DOLEVA>');
end;
k:=x+y-x0-y0; { Spočítáme počet kroků, které je třeba udělat }
if k < 0 then k:=-k;
while k > 0 do
begin
write(' <KROK>');
dec(k)
end
end;
begin
{ Načteme rozměry sítě, překážky, počáteční a cílové políčko }
readln(vyska,sirka);
readln(start.x,start.y);
readln(cil.x,cil.y);
for x:= 1 to vyska do
for y:= 1 to sirka do
begin
read(i);
prekazky[x,y]:=(i=1);
navstiveno[x,y]:=false;
svisle[x,y]:=false;
vodorovne[x,y]:=false;
end;
{ Test na shodu počátečního a cílového políčka }
if ( start.x = cil.x ) and ( start.y = cil.y ) then
begin
writeln('Počáteční a cílové políčko jsou stejné.');
halt;
end;
{ Inicializace prohledávání do šířky }
zpracovano:=0;
vefronte:=1;
fronta[1]:=start;
navstiveno[start.x,start.y]:=true;
predchozi[start.x,start.y]:=start;
{ Cyklus prohledávání do šířky }
while ( zpracovano < vefronte ) do
begin
inc(zpracovano);
x:=fronta[zpracovano].x;
y:=fronta[zpracovano].y;
if not vodorovne[x,y] then
begin
{ Projedeme síť vodorovně ( řádek ) }
vodorovne[x,y]:=true;
i:=1;
while ( y+i <= sirka ) and not ( prekazky[x,y+i] ) do
begin
vodorovne[x,y+i]:=true;
if not navstiveno[x,y+i] then
begin
navstiveno[x,y+i]:=true;
predchozi[x,y+i]:=fronta[zpracovano];
inc(vefronte);
fronta[vefronte].x:=x;
fronta[vefronte].y:=y+i;
end;
inc(i);
end;
i:=-1;
while ( y+i >= 1 ) and not ( prekazky[x,y+i] ) do
begin
vodorovne[x,y+i]:=true;
if not navstiveno[x,y+i] then
begin
navstiveno[x,y+i]:=true;
predchozi[x,y+i]:=fronta[zpracovano];
inc(vefronte);
fronta[vefronte].x:=x;
fronta[vefronte].y:=y+i;
end;
dec(i);
end;
end;
if not svisle[x,y] then
begin
{ Projedeme síť svisle ( sloupeček ) }
svisle[x,y]:=true;
i:=1;
while ( x+i <= vyska ) and not ( prekazky[x+i,y] ) do
begin
svisle[x+i,y]:=true;
if not navstiveno[x+i,y] then
begin
navstiveno[x+i,y]:=true;
predchozi[x+i,y]:=fronta[zpracovano];
inc(vefronte);
fronta[vefronte].x:=x+i;
fronta[vefronte].y:=y;
end;
inc(i);
end;
i:=-1;
while ( x+i >= 1 ) and not ( prekazky[x+i,y] ) do
begin
svisle[x+i,y]:=true;
if not navstiveno[x+i,y] then
begin
navstiveno[x+i,y]:=true;
predchozi[x+i,y]:=fronta[zpracovano];
inc(vefronte);
fronta[vefronte].x:=x+i;
fronta[vefronte].y:=y;
end;
dec(i);
end;
end;
end;
if not navstiveno[cil.x,cil.y] then
begin
{ Na cílové políčko se nelze dostat ... }
writeln('Cesta z počátečního na cílové políčko neexistuje.');
halt
end;
{ A vypíšeme nalezenou cestu ... }
vypis(cil.x,cil.y);
writeln;
end.
P-II-3 Pepíček
Úlohu si nejdříve lehce přeformulujeme. Obraz je vlastně nějaký konvexní
n-úhelník a střihy jsou neprotínající se tětivy tohoto mnohoúhelníka. Úkolem
je nalézt mnohoúhelník s největším počtem vrcholů, ve kterém neleží žádná
tětiva (nadále budeme tento mnohoúhelník označovat
jako
největší mnohoúhelník).
Algoritmus nejdříve upraví popis každé tětivy tak, aby počáteční vrchol tětivy měl
menší číslo než vrchol koncový. Pak všechny tětivy setřídí. Při třídění se porovnává
nejdříve číslo počátečního vrcholu. Pokud je u více tětiv stejné, bere se obrácené
pořadí čísel jejich koncových vrcholů (tedy (1, 2) < (2, 3), ale (1, 2) > (1, 3)). Po
setřídění začne algoritmus hledat největší mnohoúhelník. Algoritmus při
hledání využívá toho, že v každém mnohoúhelníku (až na jeden) existuje právě
jedna tětiva přemosťující stranu (1, n). Problematickým mnohoúhelníkem
je ten, který stranu (1, n) obsahuje. Problémy s tímto mnohoúhelníkem řešíme
tak, že uvažujeme pomocnou tětivu (1, n). Přemosťující tětiva bude mít
ze všech tětiv a stran mnohoúhelníka nejmenší počáteční číslo vrcholu a
největší koncové číslo vrcholu (kdyby měla nějaká tětiva větší koncové i
počáteční číslo vrcholu, musela by přemosťující tětivu někde protnout.
Stejně tak pokud by byla obě čísla vrcholů menší. Pokud by bylo počáteční číslo
menší a koncové větší, nebyla by zase naše tětiva přemosťující). Také
platí, že každá tětiva je přemosťující pro právě jeden mnohoúhelník
(mnohoúhelník ohraničený tětivou (i, j) bude obsahovat vrcholy i, j a
ještě nějaké vrcholy z i...j).
A nyní již k hledání největšího mnohoúhelníku. To má následující ideu:
Algoritmus postupně prochází vrcholy n-úhelníka od 1 do n. Udržuje si
přitom zásobník, v němž jsou uloženy tětivy, u kterých prošel jejich počátečním
vrcholem, ale dosud ne jejich koncovým vrcholem. Jsou to tedy přemosťující
tětivy pro dosud neuzavřené mnohoúhelníky. U každé tětivy na zásobníku si také
udržuje dosud napočítaný počet vrcholů pro mnohoúhelník omezený danou tětivou.
Protože aktuální vrchol vždy patří k mnohoúhelníku omezenému nejpozději
začínající přemosťující tětivou, stačí vždy upravovat jen počet vrcholů u
tětivy na vrcholu zásobníku.
Konkrétní implementace hledání: Vždy když se v algoritmu posuneme do dalšího vrcholu,
odebereme ze zásobníku tětivy, které v tomto vrcholu končí. Prošli jsme totiž
všechny vrcholy, které mohly ležet v mnohoúhelnících ohraničených těmito
tětivami. K těmto tětivám už tedy byly spočítany počty vrcholů v jimi
ohraničných mnohoúhelnících, a tak stačí podle těchto hodnot upravit dosud
nalezené maximum. Při každém odebrání tětivy ze zásobníku algoritmus přičte jedna k počtu
vrcholů u tětivy na vrcholu zásobníku, protože do příslušného mnohoúhelníku se
dostal i aktuální vrchol. Pak přidá všechny tětivy začínající v daném
vrcholu do zásobníku. Počty vrcholů u nových tětiv nastavuje na jedna,
protože se musí započítat aktuální vrchol. Díky setřídění tětiv stačí pouze
tětivy po řadě odebírat z pole, dokud je jejich počáteční vrchol shodný s
aktuálním. Setřídění také zajistí, že z tětiv začínajících v aktuálním
vrcholu budou ty, které skončí později, vloženy do zásobníku dříve. Když
jsou přidány všechny tětivy začínající v aktuálním vrcholu, přičte se k tětivě na
vrcholu zásobníku jedna za vrchol, do kterého se algoritmus přesouvá.

Právě uvedený algoritmus můžeme ještě zrychlit. Stačí si uvědomit, že je
zbytečné posouvat se po obvodu pouze po jednom vrcholu. Stačí nám vlastně jen
obejít vrcholy, ve kterých nějaká tětiva začíná nebo končí. To, o kolik se máme
posunout, snadno zjistíme jako minimum z konce tětivy na vrcholu zásobníku a
počátku první dosud nezařazené tětivy. Získáme tak algoritmus s časovou
složitostí O(k log k). Čas O(k log k) totiž strávíme tříděním. Samotný
průchod n-úhelníkem nám zabere pouze O(k), protože každou tětivu pouze
jednou přidáme na zásobník a jednou ji z něj odebereme. Počty vrcholů
upravujeme dohromady pouze 2k krát. Správnost algoritmu byla ukázána v
popisu.
Program je přímou implementací algoritmu:
program Pepik; { P-II-3 }
const
MAXK = 100;
type
Cut = record
a, b : Integer;
end;
CutA = Array[1..MAXK] of cut;
var
n, k : Integer; {Počet vrcholů; Počet střihů}
c : CutA; {Popis jednotlivých střihů}
{Načte vstup}
procedure ReadInp;
var
i, tmp : Integer;
begin
Write('Zadejte pocet vrcholu a pocet strihu: ');
Read(n, k);
Write('Zadejte jednotlive strihy: ');
{Načte popis střihu}
for i := 1 to k do begin
Read(c[i].a, c[i].b);
{První číslo bude menší}
if c[i].a > c[i].b then begin
tmp := c[i].a;
c[i].a := c[i].b;
c[i].b := tmp;
end;
end;
{Přidáme pomocnou tětivu}
Inc(k);
c[k].a := 1;
c[k].b := n;
end;
{Porovná dva střihy}
function CmpCut(a, b : cut) : ShortInt;
begin
if (a.a < b.a) or ((a.a = b.a) and (a.b > b.b)) then
CmpCut := -1
else if (a.a = b.a) and (a.b = b.b) then
CmpCut := 0
else
CmpCut := 1;
end;
{Setřídí pole se střihy QuickSortem}
procedure SortCut(d, u : Integer);
var
m, tmp : cut; {Pivot}
i, j : Integer;
begin
m := c[(d+u) div 2]; {Vybereme pivota}
i := d; j := u;
while i <= j do begin
{Nalezneme prvky ve špatných částech}
while CmpCut(c[i], m) = -1 do
Inc(i);
while CmpCut(c[j], m) = 1 do
Dec(j);
if i <= j then begin
{Zaměníme prvky ve špatných částech}
tmp := c[i];
c[i] := c[j];
c[j] := tmp;
Inc(i);
Dec(j);
end;
end;
if i < u then {Je co třídit v pravé části?}
SortCut(i, u);
if d < j then {Je co třídit v levé části?}
SortCut(d, j);
end;
{Nalezne největší mnohoúhelník}
function FindMax(n, d, u : Integer) : Integer;
var
StackC : Array[1..MAXK] of Integer;
StackN : Array[1..MAXK] of Integer;
AV, SP, AChord, Max : Integer;
begin
SP := 0;
AV := c[1].a;
AChord := 1;
Max := 0;
while True do begin
{Končí tu nějaký mnohoúhelník?}
while (SP > 0) and (c[StackC[SP]].b = AV) do begin
if StackN[SP] > Max then {Je největší?}
Max := StackN[SP];
Dec(SP);
if SP > 0 then
Inc(StackN[SP]); {Přidáme vrchol za právě ukončenou tětivu}
end;
if AV = n then {Už jsme prošli celý mnohoúhelník?}
break;
{Začíná zde nějaký mnohoúhelník?}
while (AChord <= k) and (AV = c[AChord].a) do begin
Inc(SP);
StackC[SP] := AChord;
StackN[SP] := 1;
Inc(AChord);
end;
{Začíná dříve nějaká tětiva, než končí jiná?}
if (AChord <= k) and (c[AChord].a < c[StackC[SP]].b) then begin
Inc(StackN[SP], c[AChord].a - AV);
AV := c[AChord].a;
end
else begin {Nějaká tětiva nejdříve končí}
Inc(StackN[SP], c[StackC[SP]].b - AV);
AV := c[StackC[SP]].b;
end;
end;
FindMax := Max;
end;
begin
ReadInp; {Načte vstup}
SortCut(1, k); {Setřídíme pole se střihy}
{Nalezne největší část a vypíše ji}
WriteLn('Nejvetsi cast ma ', FindMax(n, 1, k), ' vrcholu.');
end.
P-II-4 Dlaždice
Využijeme jednoduchého pozorování:
- Posloupnost x1,...,xn je symetrická
právě tehdy, když x1=xn a posloupnost x2,...,xn-1 je symetrická.
- Jednoprvková i prázdná posloupnost jsou vždy symetrické.
Sestrojíme dlaždicový program, který bude připouštět právě taková vydláždění,
v nichž každý řádek ověří rovnost krajních prvků posloupnosti a odstraní je
(přepíše na barvu "kolečko"),
přičemž poslední řádek akceptuje buďto prázdnou nebo jednoprvkovou posloupnost.
Takový program odpovídá
ano právě na symetrické vstupní posloupnosti:
pokud je posloupnost x
1,...,x
n symetrická, první řádek z ní odstraní
x
1 a x
n, druhý x
2 a x
n-1 atd. až poslední řádek akceptuje
buďto prostřední prvek původní posloupnosti (měla-li lichou délku) nebo prázdnou posloupnost
(byla-li její délka sudá).
A opačně: Pokud program posloupnost akceptuje, pak podle prvního řádku
je x
1=x
n, podle druhého x
2=x
n-1 atd., tudíž je zadaná posloupnost
symetrická. Proto náš program řeší zadanou úlohu se složitostí O(n).
Abychom dosáhli tohoto cíle, použijeme následující sadu dlaždic:

Levý okraj zdi barvy A, pravý barvy B a dolní barvy "kolečko".
Každý korektně vydlážděný řádek musí vypadat buďto takto:

(to odpovídá kontrole a odstranění krajních prvků posloupnosti) a nebo takto:

(takový řádek akceptuje libovolnou jednoprvkovou posloupnost; posloupnost prázdná - taková,
jejíž všechny prvky již byly přepsány na "kolečko" - je akceptována přímo dolním okrajem
zdi).
Poznámka
Časové složitosti lepší než lineární již není možno dosáhnout.
Idea důkazu:
Z definice vydláždění víme, že zeď sudé šířky je
možno vydláždit právě tehdy, je-li možno vydláždit její levou polovinu i její
pravou polovinu tak, aby v každém řádku měly dlaždice, jimiž se tyto
poloviny dotýkají, stejnou barvu společné hrany (těmto hranám budeme říkat
prostřední sloupec). Pro každý začátek posloupnosti
x
1,...,x
n/2 ovšem existuje právě jedno doplnění prvky x
n/2+1,...,x
n
takové, že x
1,...,x
n je symetrická posloupnost. Kdyby nějakým dvěma různým začátkům
x
1,...,x
n/2 a y
1,...,y
n/2 odpovídalo stejné obarvení středního sloupce,
pak by po doplnění prvky x
n/2,...,x
1 vydláždění pravé poloviny existovalo buď pro
oba začátky nebo pro žádný (závisí totiž pouze na pravé polovině vstupu a středním sloupci),
přestože pro první začátek má existovat a pro druhý nikoliv. Proto různých obarvení středního
sloupce (těch je <=b
h, kde b je počet barev vyskytujících se v programu a h maximalní
výška zdi, tedy složitost programu) musí být minimálně tolik, kolik je možných začátků
posloupnosti (10
n/2), a tak musí být
h >= log
b 10
n/2 = n (log
b 10) / 2.
To znamená, že složitost libovolného dlaždicového programu řešícího naši úlohu
musí být alespoň lineární.