[Решења задатака] 2024/2025 Квалификације - други круг

Просек и медијана

Аутор: Немања Мајски
Текст и тест примери: Немања Мајски
Анализа решења: Владимир Миловановић
Тестирање: Владимир Миловановић

Главно решење

Без смањења општости, претпоставимо да за два задата броја a и b важи a \le b (уколико то приликом учитавања и није случај једноставно можемо заменити вредности променљивама a и b). Пошто се у задатку тражи највећи број c за који важи \text{prosek}(a,b,c) \le \text{medijana}(a,b,c), то захтевано c сигурно неће бити мање од a и b, односно имамо a \le b \le c, па је \text{medijana}(a,b,c) = b, а како је \text{prosek}(a, b, c) = \dfrac{a+b+c}{3} \le b, што је еквивалентно a+b+c \le 3b, добија се c \le 2b-a, односно c = 2b-a, што се и тражило у задатку. Временска сложеност овог решења је константна \mathcal{O}(1) и не зависи од вредности a, b и c.

Изворни код у програмском језику Пајтон могао би изгледати рецимо:

a, b = sorted(map(int,input().split()))
print(2*b-a)

или

a, b = map(int,input().split())
print(2*max(a,b)-min(a,b))

док је за максималан број поена у програмским језицима C и C++, односно C# и Java, било неопходно променљиве a и b декларисати као long long, односно long, респективно, пошто већ могу узимати вредности до 10^{18}.

Срећа у матрици

Аутор: Милош Милутиновић
Текст и тест примери: Александар Вишњић
Анализа решења: Милош Милутиновић
Тестирање: Филип Бојковић

Решење када је A_{i,j} = 1

У овом подзадатку ће сваки скок који Перица направи повећати срећу, па је решење једнако броју скокова, прецизније једнако је 2 \cdot N - 1.

Решење када је N \leq 3

Како је N мало, могуће је проверити све случајеве примењених операција ручно и узети најбоље.

Решење када је N \leq 10

Слично као у претходном подзадатку могуће је проверити све случајеве примењених операција, али овај пут то мора да се одради на бољи начин од ручне провере. Могуће је на пример рекурзивно изгенерисати сва стања и затим израчунати срећу за свако од њих. Временска сложеност O(2^N \cdot N).

Главно решење

Назовимо колону фиксном уколико су вредности поља у њој исте парности. Сваки скок унутар фиксне колоне сигурно не мења срећу, док скок у осталим колонама сигурно повећава срећу за један. Преостаје да се максимизује срећа скокова између колона. За почетак посматрајмо прву и последњу фиксну колону. Пре прве и после последње колоне могуће је направити, помоћу операција, да сваки скок између колона повећа срећу. Затим посматрајмо две фиксне колоне такве да између њих нема ни једне фиксне колоне, наиме могуће је да сваку не-фиксну колону између те две фиксне колоне, наместити тако да скок са претходне колоне на њу повећа срећу, почев од најлевље па редом ка десно. На крају је преостало израчунати срећу тако добијене матрице. Укупна временска сложеност O(N).

Путешествије

Аутор: Марко Миленковић
Текст и тест примери: Марко Миленковић
Анализа решења: Марко Миленковић
Тестирање: Немања Мајски

Решење када је N = 10

Можемо приметити да је заправо објекат у задатку (продавнице и путеви између њих) граф. Такође, могуће је показати да решење увек постоји, што ће се видети из наше конструкције.

У овом подзадатку, довољно је испробати све могуће пермутације чворова. Односно, једна пермутација чворова нам представља потенцијалну путању кроз граф. На пример, за 5 чворова, пермутација 1,3,2,5,4 нам представља путању: 1-3-2-5-4-1. Потребно је само проверити да ли таква путања постоји, односно да ли су свака два суседна елемента у низу (укључујући први и последњи) суседи у графу.

Решење када важи да ће продавница свака нова продавница x = 5,6,\ldots,n бити саграђена у троуглу 1,2,\ldots, x-1.

Овај подзадатак има више могућих исправних решења. Прилажемо једно од њих. Можемо приметити да путања: 1->2->x->(n)->(n-1)->\ldots->4->3->1 задовољава услове из задатка.

Решење када је n \leq 100, n \leq 1.000, или n \leq 200.000

Идејно решење за ове подзадатке је исто. Оно што се разликује јесте начин имплементације. На пример, чување троуглова у тродимензионалном низу одговоара подзадатку када је n \leq 100. Са друге стране, неоптимално испитивање да ли је у троуглу сазидана нова продавница или неелементарно коришћење напредних структура, попут мапе у C++, одговара подзадатку када је n \leq 1.000.

Решење ћемо градити индуктивно. У почетку, када имамо само 4 чвора, знамо да је једна од могућих путања 1->2->3->4->1. Надаље, наше решење ћемо градити тако да сваки троугао који ће касније имати у себи додат чвор, у графу има бар једну грану у траженој путањи која обилази све чворове тачно једном. Наиван, али нетачан, покушај би био следећи:

Посматрајмо тренутак када се чвор X дода у троугао ABC и повеже са сва три чвора. Претпоставимо (на основу тога што смо одлучили да тако градимо решење) да је грана AB она која се до сада налазила у траженој путањи. Просто можемо избацити грану AB из тражене путање и уместо ње ставимо гране AX и XB. Како је по индуктивној претпоставци претходна путања била коректна, тако ће и ова нова.

Овакав покушај је неисправан. Проблем настаје када два троугла деле исту грану у коначној путањи. Када избацимо ту заједничку грану за први троугао, касније, када будемо додавали чвор у другом троуглу, он неће имати грану у траженој путањи - што нас доводи до грешке.

Ипак, нисмо далеко од решења. Додаћемо строжу претпоставку индукцији: сваки троугао коме ће у неком тренутку бити додат чвор унутра, ће имати јединствену грану у траженој путањи. Како можемо ово да осигурамо?

У почетку, када имамо само 4 чвора, знамо да таква путања постоји: 1->2->3->4->1. Даље, када се чвор X дода у троугао ABC, можемо да претпоставимо по индукцији да јединствена грана додељена овом троуглу бива AB. Сада разликујемо три случаја (и ово је заправо део који је битно имплементирати оптимално):

  1. Троуглу ABX се неће додавати убудуће чворови у унутрашњост. Сада заменимо грану AB гранама AX и XB у траженој путањи. Затим, грану AX доделимо троуглу ACX као његову јединствену грану у траженој путањи. Аналогно, грану BX доделимо троуглу BCX. Приметите да сада троугао ABX нема јединствену грану у траженој путањи - али то је сасвим валидно, јер њему неће бити даље додавани чворови.
  2. Троуглу BCX се неће додавати убудуће чворови у унутрашњост. Сада заменимо грану AB гранама AX и XB у траженој путањи. Затим, грану AX доделимо троуглу ACX као његову јединствену грану у траженој путањи. Аналогно, грану BX доделимо троуглу ABX.
  3. Троуглу ACX се неће додавати убудуће чворови у унутрашњост. Овај случај је симетричан претходном.

Сада имамо комплетирано решење, односно путању која обилази сваки чвор тачно једном. Предлог за конкретну имплементацију је да се направи мапа која као кључ има троугао (тројку чворова, или корисничку структуру са тим подацима), а као вредност грану која му је додељена. За више детаља погледати комисијско решење.

За оне који желе да знају више

Овакав тип графова се назива планарним графовима. Односно, то су графови за које постоји цртање тако да се никоје две гране не секу. Додатно, граф из задатка је по још нечему карактеристичан: није могуће додати ниједну грану а да се не наруши планарност. Овакве графове такође зовемо и максмиланим планарним графовима. За више информација: Planar graph - Wikipedia

Далтонизација

Аутор: Марко Миленковић
Текст и тест примери: Алекса Милисављевић
Анализа решења: Алекса Милисављевић
Тестирање: Милош Милутиновић

Решење када је a_i = 1 за свако 1 \leq i \leq N и N \leq 300

У овом подзадатку важи да сви нфт-ови имају исту вредност, те је сваком брату потпуно свеједно да ли узима нфт са почетка или нфт са краја. i-ти брат ће укупно имати онолико нфт-а у вредности колико пута ће бити на потезу.

Решење када је N \leq 20

У овом подзадатку за сваког брата можемо да рекурзивно симулирамо све могуће његове одлуке и одлуке осталих. Ово решење ради у временској сложености O(N \cdot 2^N).

Решење када је M = 2 и N \leq 300

У овом подзадатку постоје само два брата, те важи да је обојици циљ да прикупе што више (тј. не морамо да анализирамо њихове профите независно). За овај и наредне подзадатке решење проналазимо коришћењем техника динамичког програмирања. Означимо са s(l,r) суму a_l + a_{l+1} + ... + a_{r-1} + a_r. Нека је dp[i][l][r] зарада i-тог брата ако су остали нфт-ови на позицијама између l и r и брат i је на потезу. Приметимо да у том случају је зарада брата који није i-ти управо s(l,r)-dp[i][l][r], тј. узеће све оно што није узео i-ти брат. Тада је dp[1][l][r] = \max(a_l + (sum(l+1,r) - dp[2][l+1][r]),a_r + (sum(l,r-1) - dp[2][l][r-1])) и слично томе dp[2][l][r] = \max(a_l + (sum(l+1,r) - dp[1][l+1][r]),a_r + (sum(l,r-1) - dp[1][l][r-1])). Приметимо да је динамичко програмирање неопходно конструисати растуће у односу на дужину интервала [l,r].

Коначно први брат ће имати укупно dp[1][1][N], док ће други имати sum(1,N) - dp[1][1][N]. Ово решење ради у временској сложености O(N^2) и меморијској сложености O(N^2).

Решење када је N \leq 300

У овом подзадатку морамо да генерализујемо решење из претходног подзадатка. Приметимо прво да број нфт-ова који су преостали у низу једнозначно одређује брата који је на потезу. Нека је b(l,r) брат који је на потезу ако су остали само нфт-ови између позиција l и r. Овим елиминишемо једну димензију динамичког програмирања из претходног подзадатка. Дакле, dp[l][r] представља профит брата b(l,r) уколико се преостала браћа уроте против њега. Нека је b(l,r)=i. Он ће у том потезу изабрати или a_l или a_r. Претпоставимо да је изабрао a_l. Приметимо да се након потеза преостале браће број нфт-ова у низу смањио за тачно M-1. Према томе, наредни пут када брат i буде на потезу, преостаће му да бира са почетка или са краја једног од интервала [l+1,r-M+1], [l+2,r-M+2], …, [l+M-1,r-1], [l+M,r]. Како су се преосталих M-1 браће уротили против њега, знамо да су они одлуке доносили тако да његова укупна зарада буде што мања могућа. Дакле, брату i ће преостати интервал са најмањом зарадом, тј. зарадиће \min(dp[l+1][r-M+1],...,dp[l+M][r]). Слична анализа ради и у случају да он одабере нфт a_r. Коначно, ово динамичко програмирање можемо да рачунамо формулом: dp[l][r] = \max(a_l + \min(dp[l+1][r-M+1],...,dp[l+M][r]), a_r + \min(dp[l][r-M],...,dp[l+M-1][r-1])).

Да би добили решење за i-тог брата, морамо да анализирамо интервале када ће он бити први пут на потезу и да одаберемо онај који има најмању вредност, тј. \min(dp[1][N-i+1],dp[2][N-i+2],...,dp[i][N]). Ово решење ради у временској сложености O(N^3) и меморијској сложености O(N^2).

Решење када је N \leq 4.000

У овом подзадатку можемо да приметимо да је могуће искористити структуру података на решењу из претходног подзадатка. Приметимо да интервали од којих тражимо онај са минималном вредношћу у формули за рачунање динамичког програмирања сви имају исту дужину и да су ти интервали узастопни са том дужином (тј. почеци свака два суседна се разликују за тачно 1). Према томе, можемо да конструишемо сегментно стабло за сваку дужину. У сваком чвору сегментног стабла чувамо минимум за неки узастопни скуп интервала.

Ово решење ради у временској сложености O(N^2 \log N) и меморијској сложености O(N^2).

Главно решење

У овом подзадатку морамо да оптимизујемо временску и меморијску сложеност из претходног подзадатка. Прво, приметимо да је рачунање решења за сваког брата потпуно независно и да се не ослања на вредности динамичког програмирања за осталу браћу. Приметимо и да решење за i-тог брата искључиво зависи од његовог првог следећег потеза. Према томе, можемо сваког брата да процесирамо независно и растуће по дужини интервала који разматрамо. Претпоставимо да смо израчунали решење за све интервале дужине d. У наредном кораку рачунамо решење за све интервале дужине d+M. Чим то решење израчунамо, подаци о дужини d нам више не требају. Један начин да израчунамо решење за интервале дужине d+M је да применимо структуру из претходног подзадатка. Овим добијамо решење временске сложености O(N^2 \log N) и меморијске сложености O(N).

Да би оптимизовали и временску сложеност, морамо да приметимо и да је број интервала у сваком од минимума динамичког програмирања тачно M. Према томе можемо да применимо sliding window технику. Процесирамо групу од првих M узастопних интервала дужине d. Потом се померимо на десно за једну позицију (тј. избацимо први интервал из групе и убацимо први наредни). Процесираћемо на овај начин једну по једну групу од M интервала дужине d и сваки пут проналазити минимум.

Одржаваћемо deque (тј. ред са два краја). Када се померимо на десно за један, прво проверимо да ли је интервал на почетку реда још увек део групе. Уколико није, избацимо га. Потом посматрамо елемент са краја реда. Све док елемент са краја реда има већу или једнаку вредност од новог интервала у групи, можемо да га обришемо са краја. Ово је тачно, јер нас интересује само минимум, а како се померамо на десно, тај интервал са већом вредношћу ће бити избачен пре интервала који је управо додат у групу коју разматрамо, те, према томе, он никада неће бити бољи избор. Када избришемо све интервале са краја који су имали већу или једнаку вредност, додамо нови интервал на крај. Приметимо да ова конструкција гарантује да елемент који је испред новододатог има строго мању вредност од њега. Према томе, најмања вредност у целом реду је заправо елемент на почетку реда.

Ово решење ради у временској сложености O(N^2) и меморијској сложености O(N).

Занимљиви графовски упити

Аутор: Милош Милутиновић
Текст и тест примери: Милош Милутиновић, Немања Мајски, Тони Шкријељ
Анализа решења: Милош Милутиновић
Тестирање: Алекса Милисављевић

Решење када је N, Q \leq 2.000 и граф је стабло

Можемо пустити DFS алгоритам од чвора a до чвора b и наћи све вредности на том путу. Затим је лако одредити медијану.

Решење када је N, Q \leq 2.000

Овај подзадатак је намењен имплементацијама са лошијим сложеностима.

Решење када је граф стабло

У овом подзадатку и за наредне ће нам бити потребна структура података перзистентно сегментно стабло. За сваки чвор ћемо чувати верзију сегментног стабла у коме ће бити вредности чворова на путу од корена стабла до баш тог чвора. Ово је могуће направити током једног DFS обиласка. Затим да би одговорили на упите, можемо урадити шетњу над перзистентним стаблом, конкретније над pst_a + pst_b - pst_{LCA(a,b)} - pst_{par_{LCA(a,b)}}, где pst_v означава верзију перзистентног сегментног стабла за чвор v, LCA(a,b) најнижег заједничког претка чворова a и b и par_v родитеља чвора v.

Како даље?

Можемо користити block-cut стабло. Tо стабло се добија када избришемо све артикулационе тачке и сваку компоненту коју добијемо, након брисања, назовемо једним блоком. Затим сваки блок представимо као нов чвор и њега повежемо са свим чворовима унутар тог блока и суседним артикулационим тачкама. Лако се види да ће то заправо представљати стабло. Након његове конструкције приметимо да сви чворови чворови који се налазе на неком простом путy од чвора a до чвора b се заправо налазе на простом путу од чвора a до чвора b у block-cut стаблу, уколико кажемо да “блок” чвор представља све његове суседне чворове.

Решење када је A_i \leq 3

Након конструкције можемо памтити број појављивања сваке вредности на путу од корена до сваког чвора. Затим је лако одговорити на упите.

Решење када је N, Q \leq 100.000 и P = 0

Могуће је применити паралелну бинарну претрагу по решењу и чувати сегментно стабло које ће нам чувати колико имамо појављивања чворова са вредностима A_v \leq mid ако је mid тренутни параметар у паралелној бинарној претрази, од корена до сваког чвора. Временска сложеност је O((N + Q) \cdot {{\log^2 N}}).

Решење када је N, Q \leq 100.000

Решење за овај подзадатак је исто као решење целог задатка, осим што је дозвољено користити бинарну претрагу по решењу уместо шетње по сегментном стаблу.

Главно решење

Могуће је направити перзистентно сегментно стабло над block-cut стаблом и упите одговарати исто као у подзадатку где је граф заправо стабло. Временска сложеност је O((N + Q) \cdot {\log N}).