Řešení úloh školního kola 51. ročníku
Tento pracovní materiál není určen přímo studentům - řešitelům
olympiády. Má pomoci učitelům na školách při přípravě konzultací
a pracovních seminářů pro řešitele soutěže, pracovníkům
regionálních výborů MO slouží jako podklad pro opravování úloh
domácího kola MO kategorie P. Studentům lze tyto komentáře
poskytnout až po termínu stanoveném pro odevzdání řešení úloh
domácího kola MO - P jako informaci, jak bylo možné úlohy správně
řešit, a pro jejich odbornou přípravu na účast ve druhém kole
soutěže.
P-I-1
Úlohu nejprve převedeme do terminologie teorie grafů. Bipartitní graf
je takový graf, ve kterém můžeme vrcholy
rozdělit do dvou disjunktních množin R a S tak, aby každá
hrana grafu spojovala některý vrchol z množiny R s některým
vrcholem z množiny S. Čtvercovou matici A nul a jedniček
rozměrů n x n můžeme chápat jako reprezentaci bipartitního
grafu G s 2n vrcholy, kde vrcholy r1,r2,... rn
odpovídají řádkům a vrcholy s1,s2,...,sn sloupcům matice.
Vrcholy ri a sj jsou spojeny hranou právě tehdy, když prvek
A[i,j]=1. Hranu mezi ri a sj budeme značit (ri,sj).
Řekneme, že bipartitní graf má perfektní párování, jestliže
se jeho vrcholy dají uspořádat do dvojic tak, že v každé dvojici
je jeden vrchol z množiny R a jeden vrchol z množiny S a tyto
dva vrcholy jsou spojeny hranou. Každý vrchol grafu se přitom
musí nacházet právě v jedné takovéto dvojici.
Ukážeme, že jestliže bipartitní graf G má perfektní párování,
potom v naší matici A lze přerovnat řádky a sloupce takovým
způsobem, aby na hlavní diagonále byly samé jedničky. Pokud
párování obsahuje dvojice (ri1,sj1), (ri2,sj2),
..., (rin,sjn), pak stačí řádky uspořádat v pořadí
i1, i2, ..., in a sloupce v pořadí j1, j2, ...,
jn. Označme takto přerovnanou matici A'. Platí
A'[k,k]=A[ik,jk]. Jelikož mezi vrcholy rik a sjk
v grafu G vede hrana, platí A[ik,jk]=1, a proto matice
A' má na hlavní diagonále samé jedničky.
Naopak, pokud lze matici A přerovnat tak, aby měla na hlavní
diagonále samé jedničky, pak v grafu G existuje perfektní
párování. Nechť v přerovnané matici A' jsou řádky v pořadí
i1, i2, ..., in a sloupce v pořadí j1, j2, ...,
jn. Víme, že pro všechna k platí A'[k,k]=1, a proto také
A[ik,jk]=1. Proto v grafu G dvojice (ri1,sj1),
(ri2,sj2), ..., (rin,sjn) tvoří perfektní
párování. Dokázali jsme tedy následující tvrzení:
Matici A je možné transformovat na tvar se samými jedničkami na
hlavní diagonále právě tehdy, když existuje perfektní párování
v grafu G.
Jestliže tedy chceme zjistit, jak zadanou matici A
přetransformovat na tvar se samými jedničkami na hlavní
diagonále, nalezneme nejprve perfektní párování v bipartitním
grafu G. Pokud perfektní párování existuje, přerovnáme řádky
a sloupce matice podle postupu uvedeného výše. Pokud takové
párování neexistuje, matici nelze transformovat do požadovaného
tvaru. Jediným problémem v tuto chvíli zůstává, jak nalézt
perfektní párování bipartitního grafu.
Připomeňme si, že v perfektním párování jsou v každé dvojici
vrcholy spojeny hranou. Proto můžeme perfektní párování chápat
také jako množinu hran M takovou, že každý vrchol grafu je
koncovým vrcholem právě jedné hrany z M. Podobně můžeme
definovat párování jako množinu hran M takovou, že každý
vrchol grafu je koncovým vrcholem nejvýše jedné hrany z M. To
znamená, že v párování, které není perfektní, nemusí být každý
vrchol zařazen do některé dvojice. Ukážeme si nyní algoritmus,
jak nalézt v bipartitním grafu maximální párování, tj.
párování s nejvyšším možným počtem hran.
Maximální párování budeme hledat postupně. Začneme s prázdným
párováním M=0 a v každém kroku zvýšíme počet párovacích
hran o jedna. Když se nám v některém kroku nepodaří zvýšit počet
hran v párování, prohlásíme aktuální nalezené párování za
maximální a výpočet ukončíme.
Počet hran v párování budeme zvyšovat pomocí takzvaných
zlepšujících cest. Uvažujme libovolné pevně zvolené párování
M. Alternující cesta pro párování M je posloupnost
vrcholů ri1,sj1,ri2,sj2,...,rik,sjk,
která začíná řádkovým vrcholem, končí sloupcovým vrcholem, každá
dvojice po sobě jdoucích vrcholů je spojena hranou a střídají se
párovací a nepárovací hrany, přičemž první hrana
(ri1,sj1) je nepárovací. Zlepšující cesta je
alternující cesta, která začíná i končí nespárovaným vrcholem.
Všimněte si, že zlepšující cesta P =
ri1,sj1,ri2,sj2,...,rik,sjk se skládá
z k-1 párovacích a k nepárovacích hran. Mějme tedy párování
M a zlepšující cestu P. Jestliže všechny párovací hrany
v P změníme na nepárovací a naopak, dostaneme nové párování
M', které má o jednu hranu více (uvědomte si, že M' skutečně
splňuje podmínky párování).

Plné čáry jsou párovací, čárkované jsou nepárovací
Dokážeme nyní, že naším postupem vždy dostaneme maximální
párování, tzn. že vždy existuje zlepšující cesta pro párování,
které není maximální.
Tvrzení
Jestliže párování M není maximální, existuje pro něj zlepšující
cesta v G.
Důkaz tvrzení
Nechť M je párování, které není maximální. Jelikož M není
maximální, musí existovat nějaké párování M', které obsahuje
více hran než M. Hrany patřící do M, ale nepatřící do M',
nazveme
modré, zatímco hrany patřící do M' a ne do M
nazveme
červené. Protože |M'|>|M|, červených hran je více
než modrých.
Uvažujme graf G' tvořený všemi modrými a červenými hranami.
Každý vrchol v G' sousedí s nejvýše jednou modrou a jednou
červenou hranou. Každá komponenta souvislosti grafu G' proto
musí být buď kružnice nebo cesta, přičemž na každé kružnici
i cestě se střídají červené a modré hrany. To znamená, že každá
kružnice obsahuje stejný počet modrých a červených hran, počty
červených a modrých hran na každé cestě se liší nejvýše o 1.
Protože červených hran je více než modrých, musí existovat
komponenta P, která obsahuje více červených hran než modrých.
Tato komponenta musí být cestou, která začíná i končí červenou
hranou. Cesta P tedy obsahuje lichý počet hran. Z toho vyplývá,
že jeden z jejích konců musí být řádkovým vrcholem (nazveme ho
ri) a druhý sloupcovým vrcholem (nazveme ho sj). Jelikož
ri resp. sj je nespárovaný řádkový resp. sloupcový vrchol
a každá druhá hrana patří do párování M, musí být P
zlepšující cesta pro M.
Implementace algoritmu.
Graf G reprezentujeme samotnou maticí, tj. dvojrozměrným polem
A. Pro každý vrchol r
i (s
j) si budeme v poli
par_r (
par_s)
pamatovat, zda je spárovaný s nějakým
vrcholem a pokud ano, číslo tohoto vrcholu. Začneme se všemi
vrcholy nespárovanými. Funkce
Zlepsi hledá zlepšující cestu
a když ji najde, použije ji na zlepšení stávajícího párování.
Hledání zlepšující cesty budeme realizovat prohledáváním do šířky
ze všech vrcholů r
i, které ještě nejsou spárované. Jakmile
najdeme nějakou alternující cestu do nespárovaného vrcholu
s
j, prohledávání ukončíme. V tomto okamžiku je třeba projít po
nalezené cestě z s
j zpět do r
i a změnit párovací hrany na
nepárovací a naopak, což znamená aktualizování záznamů v polích
par_r, (
par_s) pro všechny vrcholy na nalezené
cestě. Funkce
Zlepsi bude volána opakovaně v cyklu tak
dlouho, dokud bude možné zvyšovat počet hran v párování.
Podaří-li se vylepšit párování n krát, máme nalezeno perfektní
párování, které použijeme na přerovnání matice do požadovaného
tvaru. V opačném případě program podá zprávu o tom, že se zadaná
matice uspořádat nedá.
Efektivita
Každé volání funkce
Zlepsi potřebuje čas O(n
2), celkově
se provede nejvýše n volání této funkce, což dává celkovou
časovou složitost O(n
3). Na uložení matice A potřebujeme
paměť O(n
2).
program P_I_1;
const MAXN = 100;
var f,g : text;
A : array[1..MAXN, 1..MAXN] of integer;
par_r, par_s : array[1..MAXN] of integer;
N : integer;
function Zlepsi: boolean;
{ak sa dá, zvýši počet párov o 1}
var buf, back: array[0..MAXN] of integer;
zbuf, kbuf, vi,vj, i,j, tmp : integer;
nasiel : boolean;
Begin
zbuf := 0; kbuf := 0;
for i:=1 to N do
if par_r[i]<=0 then begin
buf[kbuf]:=i; Inc(kbuf);
end;
for j:=1 to N do back[j]:=0;
nasiel := false;
while (zbuf<kbuf) and not nasiel do begin
vi:= buf[zbuf]; Inc(zbuf);
for j:=1 to N do begin
if nasiel then break;
if (A[vi,j]=0) or (j=par_r[vi]) or (back[j]>0) then continue;
if par_s[j]<=0 then begin
{našli sme zlepšujúcu cestu}
nasiel := true;
vj := j;
while true do begin
par_s[vj] := vi;
tmp := par_r[vi];
par_r[vi] := vj;
vj := tmp;
if vj<=0 then break;
vi := back[vj];
end;
end else begin
{pokračujeme v hľadaní}
back[j] := vi;
buf[kbuf]:=par_s[j];
Inc(kbuf);
end;
end;
end;
Zlepsi := nasiel;
End;
procedure Uprac_stlpce;
var i,j, tmp, ii : integer;
Begin
for i:=1 to n do
while par_r[i]<>i do begin
ii:=par_r[i];
for j:=1 to n do begin
tmp:=A[i,j]; A[i,j]:=A[ii,j]; A[ii,j]:=tmp;
end;
par_r[i]:=par_r[ii];
par_r[ii]:=ii;
end;
End;
var i,j: integer;
ok : boolean;
Begin
Assign(f,'matica.in'); Reset(f);
Assign(g,'matica.out'); ReWrite(g);
ReadLn(f,N);
for i:=1 to N do for j:=1 to N do
Read(f,A[i,j]);
{párovanie}
for i:=1 to N do begin
ok := Zlepsi;
if not ok then break;
end;
if not ok then WriteLn(g, 'Maticu nemozno transformovat.')
else begin
Uprac_stlpce;
for i:=1 to n do
for j:=1 to n do begin
Write(g, A[i,j]);
if j<n then Write(g, ' ') else WriteLn(g);
end;
end;
Close(g); Close(f);
End.
P-I-2
Úlohu nejprve převedeme do terminologie teorie grafů. Uzly
nacházející se v naší soustavě potrubí budeme nazývat
vrcholy,
trubky nazveme
hranami a celou soustavu potrubí
nazveme
grafem. Posloupnost vrcholů a hran grafu
v
0,e
1,v
1,e
2,...,v
k-1e
k,v
k označíme jako
sled,
jestliže každá hrana e
i spojuje vrcholy v
i-1 a v
i.
Pokud navíc v
0 = v
k, tento sled nazveme
uzavřeným.
V řeči teorie grafů je naším úkolem v daném souvislém grafu G
nalézt uzavřený sled, který projde každou hranou grafu právě
dvakrát.
Ukážeme si algoritmus, který v libovolném souvislém grafu najde
sled požadovaných vlastností. Tento algoritmus tedy bude zároveň
důkazem, že trasa pro robota vždy existuje. Řešení je založeno na
prohledávání grafu do hloubky. O každém vrcholu si budeme
pamatovat, zda jsme ho již během prohledávání někdy navštívili.
Pro každý navštívený vrchol v nechť rodic[v]
označuje vrchol, z něhož robot do v poprvé přišel. Vrchol
rodic[v] je rodičem vrcholu v, naopak vrchol
v nazveme potomkem vrcholu rodic[v]. Pro
vrchol 1 není rodic[1] definován.
Prohledávání do hloubky začíná ve vrcholu 1 a probíhá takto:
- Označ aktuální vrchol v jako navštívený.
- Postupně kontroluj všechny hrany vedoucí z v a vždy, když
najdeš hranu, která vede do dosud nenavštíveného vrcholu u,
projdi robotem do vrcholu u a pokračuj rekurzívně
v prohledávání z u (v se stane rodičem vrcholu u).
- Když jsou všechny hrany vedoucí z v zkontrolovány
a v<>1, vrať se do vrcholu rodic[v] (a pokračuj
v kontrolování jeho hran). Jestliže v=1, skonči.
Uvažujme množinu hran, po nichž robot poprvé přišel do nějakého
vrcholu. Jsou to právě hrany (rodic[v],v) pro ty
vrcholy v, pro které je rodic[v] definováno. Tyto
hrany nazveme
stromové (nazývají se tak proto, že tvoří
souvislý acyklický graf, tzv. strom). Každou stromovou hranou
(u,v), kde u je rodičem v, projde robot při prohledávání
právě dvakrát -- nejprve z vrcholu u poprvé navštíví vrchol
v a podruhé při návratu z vrcholu v do vrcholu u. Při
prohledávání robot nikdy neprojde po žádné nestromové hraně.
Jelikož náš graf G je souvislý, robot při prohledávání navštíví
všechny vrcholy. Kdyby totiž existoval nenavštívený vrchol, musel
by existovat takový navštívený vrchol u a nenavštívený vrchol
v, že u a v jsou spojeny hranou. To však není možné, neboť
algoritmus prohledávání se z vrcholu u do rodic[u]
nevrátí, dokud nejsou všechny sousední vrcholy vrcholu u
navštíveny.
Podle dosud popsaného prohledávání do hloubky tedy robot navštíví
každý vrchol a projde každou stromovou hranou právě dvakrát.
Zbývá rozhodnout, kdy a jak bude robot procházet nestromové
hrany. Během prohledávání lze snadno detekovat všechny nestromové
hrany. Jestliže se robot nachází ve vrcholu u a kontroluje
hranu (u,v), tato hrana je nestromová tehdy, pokud vrchol v
již byl navštíven a u není rodičem v ani v není rodičem
u. Když robot nacházející se ve vrcholu u narazí na
nestromovou hranu (u,v), může příslušnou trubku vyčistit tak,
že jí projde z vrcholu u do v a zpět.
Implementace algoritmu
Prohledávání do hloubky můžeme snadno implementovat rekurzívní
procedurou. Jelikož rekurze si pro aktuální vrchol automaticky
pamatuje svého rodiče, není třeba explicitně udržovat záznamy
rodic[v]. Graf lze v programu reprezentovat maticí
velikosti n x n, z čehož vyplývá časová i paměťová
složitost programu O(n
2). Při použití efektivnější
reprezentace hran spojovým seznamem je možné dosáhnout časové
i paměťové složitosti O(m+n), kde m je počet hran grafu,
ovšem za cenu o něco komplikovanějšího programu.
program P_I_2;
const MAXN = 100; {max. počet vrcholov }
HR_HRANA = 1; {typy hrán - neprejdená, }
HR_STROMOVA = 2; { stromová a }
HR_PREJDENA = 3; { nestromová prejdená }
var f,g : text;
n,m : integer;
hr : array[1..MAXN,1..MAXN] of integer;
bol : array[1..MAXN] of boolean;
procedure Prehladaj(v : integer);
var i : integer;
Begin
WriteLn(g, v);
bol[v] := true;
for i:=1 to n do begin
if hr[v][i]<>1 then continue;
if bol[i] then begin {nestromová neprejdená hrana}
WriteLn(g, i);
hr[v,i]:= HR_PREJDENA;
hr[i,v]:= HR_PREJDENA;
end else begin {stromová neprejdená hrana}
hr[v,i]:= HR_STROMOVA;
hr[i,v]:= HR_STROMOVA;
Prehladaj(i);
end;
WriteLn(g, v);
end;
End; {Prehľadaj}
var i,j, aa,bb : integer;
Begin
Assign(f, 'rury.in'); Reset(f);
Assign(g, 'rury.out'); ReWrite(g);
ReadLn(f, n, m);
for i:=1 to n do for j:=1 to n do hr[i,j]:=0;
for i:=1 to m do begin
ReadLn(f, aa, bb);
hr[aa,bb] := HR_HRANA; hr[bb,aa] := HR_HRANA;
end;
for i:=1 to n do bol[i] := false;
Prehladaj(1);
Close(g); Close(f);
End.
P-I-3
Dokážeme nejprve, že spravedlivá přímka vždy existuje. Uvažujme
libovolnou orientovanou přímku vedoucí bodem [0,0]. Nechť x
je rozdíl počtu bílých bodů nalevo a černých bodů napravo od ní.
Jestliže je x rovno nule, přímka je spravedlivá. Pokud ne,
budeme přímkou otáčet kolem bodu [0,0]. Vždy, když přímka
projde přes bílý nebo černý bod, hodnota x se sníží nebo zvýší
o jedna. Když přímku otočíme přesně o 180
o, naše
sledovaná hodnota se změnila z počátečního x na -x (neboť
všechny body, které byly původně nalevo, jsou nyní napravo od
přímky a naopak). To znamená, že hodnota musela být aspoň při
jedné poloze přímky nulová. V této poloze byla přímka
spravedlivá.
Řekneme, že bod B leží nalevo od bodu A, jestliže je nalevo
od orientované přímky vedoucí z bodu [0,0] do A. Základem
algoritmu je následující pozorování. Uvažujme nějakou
spravedlivou přímku p. Nechť A je první bod, na který
narazíme, pokud budeme přímkou p otáčet ve směru hodinových
ručiček kolem bodu [0,0]. Bod A a všechny body, které leží od
něj napravo, se nacházejí v jedné polorovině určené přímkou p.
Body, které jsou od A nalevo, leží ve druhé polorovině. Vidíme,
že nezáleží na konkrétní poloze přímky p, rozdělení bodů je
určeno polohou "hraničního" bodu A.
Stačí tedy uvažovat každý ze zadaných bodů jako kandidáta na bod
A, spočítat počet bílých a počet černých bodů nalevo a napravo
od něj a zkontrolovat, zda tyto počty splňují podmínky
spravedlivé přímky. Když najdeme úspěšného kandidáta,
spravedlivou přímku sestrojíme tak, že přímku vedoucí z [0,0]
tímto bodem trochu pootočíme kolem bodu [0,0] proti směru
hodinových ručiček tak, abychom s ní při tomto otáčení nepřešli
přes žádný jiný bod. To lze provést například tak, že určíme
první bod X, přes který bychom při takovémto otáčení přímkou
přešli, a výslednou přímku vedeme středem mezi kandidátem A
a bodem X. Popsaný algoritmus má časovou složitost O(n2),
neboť pro každého kandidáta zjišťujeme polohu každého bodu
vzhledem k tomuto kandidátovi.
Algoritmus lze ještě zefektivnit tím, že si všechny bílé a černé
body nejprve setřídíme podle jejich pořadí ve směru hodinových
ručiček kolem bodu [0,0]. Kandidáty na hraniční bod A pak
budeme zkoušet v tomto utříděném pořadí. Kdykoliv budeme
přecházet od kandidáta i-1 ke kandidátovi i, nemusíme již pro
každý bod znovu zjišťovat, zda leží od bodu i nalevo nebo
napravo, neboť mnoho bodů zůstane v původní polorovině. Budeme si
proto v každém kroku udržovat informaci o počtu bílých a černých
bodů ležících v pravé polorovině a index j, který ukazuje na
poslední bod v pravé polorovině ve směru hodinových ručiček. Když
se přesuneme na kandidáta i, stačí bod i-1 přemístit do levé
poloroviny (tj. odebrat ho z počtu bodů příslušné barvy, které
jsou v pravé polorovině) a potom posouvat index j ve směru
hodinových ručiček, dokud nenajdeme první bod, který leží nalevo
od bodu i. Všechny body, přes které jsme s indexem j přešli,
se přesunou z levé poloroviny do pravé a je proto třeba
připočítat je k zaznamenanému počtu bodů příslušné barvy. Pokud
při posouvání bodu j dojdeme na konec pole, budeme pokračovat
cyklicky opět od začátku. Nesmíme zapomenout ošetřit okrajové
případy, když například všechny body leží od bodu j nalevo nebo
napravo.
Utřídit body ve směru hodinových ručiček můžeme v čase O(n log n)
některým ze standardních třídicích algoritmů, pouze namísto
běžného porovnávání hodnot si musíme napsat funkci, jež pro dva
body určí, který z nich má větší úhel. Pro první bod musíme
projít všechny ostatní body a zjistit, které leží vlevo a které
vpravo (v čase O(n)). Pro každý další bod už jen budeme
posouvat indexy i a j a sledovat pouze body, přes které
přecházíme. Přes každý bod přejdeme každým indexem nejvýše
jednou, takže celkový čas posouvání pro všechny kandidátské body
dohromady bude O(n). Výsledná časová složitost algoritmu bude
proto O(n log n) a paměťová složitost O(n).
program p_I_3;
const napriamke = 0; napravo = 1; nalavo = 2; {smery}
biela = 0; cierna = 1; {farby}
type bod = record x,y : longint; farba: integer; end;
var n : integer; {počet bielych/čiernych bodov}
m : integer; {celkový počet bodov}
a : array[1..1000] of bod; {pole bodov}
{pocet bielych a čiernych bodov napravo do priamky}
pocet : array[biela..cierna] of integer;
start : bod; {štartovací bod triedenia}
function poloha(a, b : bod):integer;
{Zisti, kde je bod b vzhľadom na priamku cez body (0,0),a}
var vektsucin : longint;
begin
vektsucin := a.x * b.y - b.x * a.y;
if vektsucin > 0 then poloha := nalavo
else if vektsucin < 0 then poloha := napravo
else poloha := napriamke
end; {poloha}
function mensi(var a, b : bod):boolean;
{Vráti, či bod a je menší ako bod b v našom usporiadaní}
var pol1, pol2, pol3 : integer;
begin
pol1 := poloha(start, a);
pol2 := poloha(start, b);
pol3 := poloha(a, b);
mensi := (pol1 < pol2) or ((pol1 = pol2) and (pol3=napravo));
end; {mensi}
procedure tried;
{Utriedi body pomocou porovnania "mensi"}
var i, j, min : integer;
pom : bod;
begin
for i := 1 to m-1 do begin
min := i; {nájdi minimum v a[i..m]}
for j := i+1 to m do begin
if mensi(a[j], a[min]) then min := j;
end;
{ulož minimum do a[i]}
pom := a[i]; a[i] := a[min]; a[min] := pom;
end;
end; {tried}
procedure nacitaj;
{Načíta dáta zo súboru}
var t : text;
i : integer;
begin
assign(t,'priamka.in'); {otvor súbor}
reset(t);
read(t,n); {počet bielych/čiernych bodov}
m := 2*n; {celkový počet bodov}
for i := 1 to m do begin {čítaj body}
read(t, a[i].x, a[i].y);
if i <= n then a[i].farba := biela
else a[i].farba := cierna;
end;
close(t); {zavri súbor}
end; {nacitaj}
function prva_priamka:integer;
{Nájdi priamku idúcu tesne pred bod a[1],
vráť posledný bod napravo, inicializuj pole pocet}
var i : integer;
begin
pocet[biela] := 0; {vynuluj počty}
pocet[cierna] := 0;
i := 1; {kým sme napravo, zvyšuj pole pocet}
while (i <= m) and (poloha(a[1], a[i]) in [napravo, napriamke]) do begin
pocet[a[i].farba] := pocet[a[i].farba] + 1;
i := i+1;
end;
prva_priamka := i-1;
end; {prva_priamka}
function dalsia_priamka(i, j :integer) :integer;
{Posunie priamku spred bodu a[i-1] pred bod a[i]}
var koniec : boolean;
stare_j : integer;
begin
{a[i-1] sa posunie doľava - odčítaj ho}
pocet[a[i-1].farba] := pocet[a[i-1].farba] - 1;
koniec := false;
repeat {pričítaj body, ktoré sa posunú doprava}
stare_j := j;
j := j+1;
if j > m then j := 1;
if (poloha(a[i], a[j]) in [napravo, napriamke]) then begin
pocet[a[j].farba] := pocet[a[j].farba] + 1;
end
else koniec := true;
until koniec or (j = i);
dalsia_priamka := stare_j;
end; {dalsia_priamka}
procedure vypis(i, j :integer);
{Vypíše výsledok do súboru}
var t : text;
b1, b2 : bod;
begin
{b1 je a[i] premietnuté podľa (0,0)}
b1.x := -a[j].x; b1.y := -a[j].y;
{b2 je posledný bod pred a[i]}
if i > 1 then b2 := a[i-1]
else b2 := a[m];
{nájdi ten z b1, b2, ktorý je viac vpravo}
if (poloha(a[i],b2) = nalavo) and (poloha(b1,b2) = napravo) then
b1:=b2;
assign(t,'priamka.out'); {otvor súbor}
rewrite(t);
{výsledný bod je stred úsečky medzi a[i] a b1}
writeln(t, (a[i].x+b1.x)/2.0:0:1, ' ', (a[i].y+b1.y)/2.0:0:1);
close(t); {zatvor súbor}
end; {vypis}
var i,j:integer;
begin {hlavný program}
nacitaj; {načítaj vstup}
start:=a[1]; {od tohoto bodu začneme triediť}
tried; {utrieď proti smeru hodinových ručičiek}
i:=1; j:=prva_priamka; {nájdi prvú priamku}
{kým nenájdeš čo potrebuješ, skúšaj ďalšiu priamku}
while pocet[biela]<>n-pocet[cierna] do begin
i:=i+1;
j:=dalsia_priamka(i,j);
end;
vypis(i,j); {vypíš do výstupného súboru}
end.
P-I-4
Část a
Vstupní soubor může mít například následující formát (váš program
samozřejmě může používat jiný formát vstupních dat). Na prvním
řádku souboru je zadán počet vodičů a počet komparátorů v síti,
na druhém řádku jsou uvedeny jednotlivé vstupy sítě v pořadí od
vrchního vodiče po spodní a zbývající řádky obsahují popis sítě.
Každý komparátor je určen dvěma čísly zapsanými na jednom řádku.
Tato čísla udávají vrchní a spodní vodič, které jsou spojené
příslušným komparátorem. Vodiče v síti jsou očíslovány shora dolů
počínaje jedničkou. Komparátory jsou uvedeny v pořadí zleva
doprava, jednotlivé vrstvy sítě se oddělují řádkem obsahujícím
dvě nuly.
Program může pro jednoduchost zobrazovat výpočet sítě
semigraficky. Nejprve přečte vstupní údaje, přičemž informace
o všech komparátorech a oddělovačích vrstev uloží do jednoho
pole. Potom bude vykreslovat síť postupně zleva doprava. Pokaždé,
když program nakreslí komparátor, odsimuluje jeho činnost na
aktuálním stavu vodičů. To znamená, že porovná příslušné dvě
hodnoty v poli a pokud jsou v opačném pořadí, vymění je. Po
ukončení každé vrstvy (tj. vždy, když v seznamu komparátorů
narazíme na oddělovač vrstev) program vypíše aktuální stav hodnot
na všech vodičích.
Jediný problém, který je třeba vyřešit, spočívá v umístění
jednotlivých komparátorů téže vrstvy do sloupců pod sebe tak, aby
se žádné dva komparátory v jednom sloupci nepřekrývaly. Program
si proto bude pamatovat, které z vodičů jsou v aktuálním sloupci
už obsazeny komparátorem. Jestliže se další komparátor překrývá
s některým komparátorem zakresleným v aktuálním sloupci, založíme
nový sloupec a komparátor umístíme do nového sloupce.
Celková časová složitost takto navrženého programu je úměrná
velikosti nakresleného obrázku, tj. O(nk), kde n je počet
vodičů a k je počet sloupců, v nichž jsou komparátory
zobrazeny.
program P_I_4a;
{program semigraficky zobrazí priebeh výpočtu komparátorovej siete}
uses crt;
const MAXM = 100; {maximálny počet komparátorov}
MAXN = 100; {maximálny počet vodičov}
var n : integer; {počet vodičov}
poloziek : integer; {počet položiek v poli komparatory}
komparatory : array[1..2*MAXM, 1..2] of integer;
{zoznam komparátorov a oddeľovačov úrovní}
stav : array[1..MAXN] of integer;
{aktuálny stav vodičov po niekoľkých krokoch}
plne : array[1..MAXN] of boolean;
{či ide cez vodič čiara v aktuálnom stľpci}
procedure nacitaj;
var inp : text; {vstupný súbor}
i, komparatorov, bolo_komparatorov : integer;
begin
assign(inp, 'siete.in'); {otvor vstupný súbor}
reset(inp);
read(inp, n, komparatorov); {čítaj počet vodičov a komparátorov}
for i := 1 to n do {načítaj vstupy siete - (stav vodičov)}
read(inp, stav[i]);
{načítavaj komparátory a oddeľovače,
až kým nedosiahneš daný počet komparátorov}
bolo_komparatorov := 1; poloziek := 0;
while bolo_komparatorov <= komparatorov do begin
poloziek := poloziek+1;
read(inp, komparatory[poloziek, 1], komparatory[poloziek, 2]);
{ak položka bola komparátor, zvýs počet komparátorov}
if komparatory[poloziek, 1] <> 0 then
bolo_komparatorov := bolo_komparatorov+1;
end;
{za posledným načítaným komparátorom pridaj oddeľovač}
poloziek := poloziek+1;
komparatory[poloziek, 1] := 0;
komparatory[poloziek, 2] := 0;
close(inp); {zatvor vstupný súbor}
end; { nacitaj }
procedure prazdne_stlpce(stlpec, kolko : integer);
var i, j : integer;
begin {vypis <kolko> prázdnych stľpcov}
for i := 1 to n do begin
gotoxy(stlpec, i*2-1);
for j := 1 to kolko do write('-');
end;
end; { prazdne_stlpce }
procedure vypis_stav(stlpec, sirka : integer);
var i : integer;
begin
for i := 1 to n do begin {vypíš aktuálny stav vodičov}
gotoxy(stlpec, i*2-1);
write(stav[i]:sirka); {každé číslo má šírku <sirka>}
end;
end; { vypis_stav }
function najdi_sirku : integer;
var i, max, sirka, cislo : integer;
begin {najde šírku (počet miest) najširšieho čísla na vstupe}
max:=1;
for i := 1 to n do begin
sirka := 1;
cislo := stav[i]; {nájdi šírku čísla stav[i]}
while cislo>=10 do begin
sirka := sirka+1; {del 10, kým neklesneš pod 10}
cislo := cislo div 10;
end;
if sirka>max then max := sirka; {porovnaj s maximom}
end;
najdi_sirku := max;
end; { najdi_sirku }
function je_volno(odkial, pokial : integer) : boolean;
var i : integer;
ok : boolean;
begin {zisti, či môžeme zakresliť komparátor do stĺpca}
ok := true;
for i := odkial to pokial do begin
if plne[i] then ok := false; {ak sa kríži s iným, nemôžeme}
end;
je_volno := ok;
end; { je_volno }
procedure spracuj_komparator(odkial, pokial, stlpec : integer);
var i, pom : integer;
begin {vykresli jeden komparátor, zapíš do pola plne, meň stav}
gotoxy(stlpec,2*odkial-1); {vykresli horný koniec}
write('*');
gotoxy(stlpec,2*pokial-1); {vykresli dolný koniec}
write('*');
for i:= 2*odkial to 2*pokial-2 do begin {spojnica medzi}
gotoxy(stlpec,i);
write('|');
end;
for i := odkial to pokial do {zapíš komparátor do pola plne}
plne[i] := true;
{vykonaj porovnanie a výmenu danú komparátorom}
if stav[odkial]>stav[pokial] then begin
pom := stav[odkial];
stav[odkial] := stav[pokial];
stav[pokial] := pom;
end;
end; { vykresli_komparator }
procedure vykresli;
var i, j, stlpec, sirka : integer;
novy_stlpec : boolean;
begin {vykresli celý priebeh výpočtu}
clrscr; {zmaž obrazovku}
sirka := najdi_sirku; {nájdi šírku čísla na vodiči}
vypis_stav(1,sirka); {vypíš vstupy do stĺpca 1}
stlpec := sirka; {posledný použitý stĺpec je <sirka>}
novy_stlpec := true; {v tomto stĺpci nebol žiaden komparátor}
for i := 1 to poloziek do begin
if komparatory[i][1]=0 then begin {oddeľovač úrovní}
prazdne_stlpce(stlpec+1,2); {vykresli vodiče}
vypis_stav(stlpec+3,sirka); {vypíš aktuálny stav}
stlpec := stlpec+2+sirka;
novy_stlpec := true; {začneme nový stĺpec komparátorov}
end
else begin {komparátor}
if novy_stlpec
or not je_volno(komparatory[i][1],komparatory[i][2]) then
begin {ak treba začať nový stĺpec komparátorov}
prazdne_stlpce(stlpec+1,3); {vykresli vodiče}
for j := 1 to n do plne[j] := false; {inicializuj pole}
stlpec := stlpec+3;
novy_stlpec := false;
end;
{vykresli a simuluj komp., zapíš ho pola plne}
spracuj_komparator(komparatory[i][1],komparatory[i][2],stlpec);
end;
end; {for i}
end; { vykresli }
begin {hlavný program}
nacitaj; {načítaj vstup}
vykresli; {vykresli priebeh výpočtu siete}
readln; {počkaj na stlačenie klávesy enter}
clrscr; {zmaž obrazovku}
end.
Na závěr uvedeme pro ilustraci příklad vstupu a výstupu programu
napsaného podle výše uvedeného popisu (jedná se o komparátorovou
síť ze studijního textu):
Vstup
4 5
4 1 2 3
1 4
2 3
0 0
1 2
3 4
0 0
2 3
0 0
Výstup
4--*-----3--*--1-----1
| |
1--|--*--1--*--3--*--2
| | |
2--|--*--2--*--2--*--3
| |
3--*-----4--*--4-----4
Část b
Komparátorovou síť sestrojíme rekurzívně. Označme S
n síť,
která úlohu řeší pro n vstupů. Síť S
1 neobsahuje žádný
komparátor, neboť máme jen jeden vstup a ten je jistě setříděn.
Předpokládejme, že n>1. První vrstva sítě obsahuje pouze jediný
komparátor umístěný mezi vodiči n a n/2. Potom rozdělíme n
vodičů na dvě poloviny -- horní a dolní -- a na každou polovinu
vodičů použijeme síť S
n/2. Tyto dvě podsítě budou pracovat
paralelně.
Konstrukce sítě Sn je zobrazena na následujícím obrázku vlevo,
vpravo je příklad výsledné sítě pro n=8.

Ukážeme si, proč takto sestavená síť řeší zadanou úlohu.
Postupujeme indukcí podle počtu vodičů. Označme vstupní hodnoty
sítě x1,x2,...,xn (umístěny na vodičích v pořadí shora
dolů). Mohou nastat dva případy podle toho, zda je xn menší
nebo větší než xn/2. Předpokládejme nejprve, že xn >=
xn/2. V takovém případě komparátor v první vrstvě nevymění
vstupní hodnoty. Protože xn >= xn/2, prvek xn patří
v uspořádaném pořadí na některý z dolní poloviny vodičů.
("Dolní" polovinou vodičů budeme rozumět vodiče se vstupními
hodnotami xn/2+1 až xn, tzn. vodiče, které na schématu
komparátorové sítě kreslíme v dolní polovině obrázku.)
Po prvním kroku výpočtu tedy platí, že horní polovina je celá
utříděna a obsahuje n/2 nejmenších prvků ze vstupu. Dolní
polovina je utříděna s případnou výjimkou posledního prvku
a obsahuje n/2 největších prvků. Vstupy obou podsítí Sn/2
tudíž splňují podmínku ze zadání, že všechny hodnoty kromě
poslední mají být na vstupu utříděny (to nevylučuje případ, že
také poslední hodnota bude utříděna). Na výstupu proto budou
podle indukčního předpokladu obě poloviny v setříděném pořadí.
Jelikož už po prvním kroku každá polovina obsahovala správné
prvky, bude správně uspořádána i celá posloupnost.
Druhý případ nastane, jestliže xn < xn/2. V této situaci
komparátor v první vrstvě vymění hodnoty obou těchto vodičů.
Víme, že xn patří někam do horní poloviny prvků a dostane se
na nejspodnější vodič horní poloviny. Naopak, poslední prvek
z horní poloviny, tj. xn/2, patří ve skutečnosti do dolní
poloviny (při třídění je "vytlačen" prvkem xn). Prvek
xn/2 se však prvním komparátorem dostane na nejspodnější
vodič, tj. do dolní poloviny. Podobně jako v předchozím případě
i nyní tedy máme po prvním kroku v každé polovině ty prvky, které
tam v setříděném pořadí patří. Obě poloviny jsou navíc setříděny
s případnou výjimkou svého nejspodnějšího prvku. Po aplikování
podsítí Sn/2 proto opět podle indukčního předpokladu
dostaneme dvě utříděné poloviny, které společně vytvoří celou
správně uspořádanou posloupnost.
Hloubka rekurze je v naší konstrukci log2 n, neboť každá
úroveň rekurze sníží počet vodičů na polovinu. V každé úrovni
rekurze přidáme do sítě jednu vrstvu, celkový počet vrstev je
tedy také roven log2 n.
První vrstva obsahuje jeden komparátor, v každé další vrstvě se
počet komparátorů zdvojnásobí. Nechť počet vstupů je n=2k.
Potom počet použitých komparátorů je roven
1+2+4+...+2k-1=2k-1=n-1 (součet geometrické řady). Naše
síť má tedy O(log n) vrstev a používá O(n) komparátorů.