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

Судар бројева

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

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

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

Под претпоставком да се задат број X састоји из M цифара, док задати број Y садржи N цифара, сложеност описаног алгоритма је \mathcal{O}(M+N), односно линеарна по броју цифара.

1 Like

Намештање

Аутор: Павле Мартиновић
Текст и тест примери: Тони Шкријељ
Анализа решења: Драган Урошевић
Тестирање: Владимир Миловановић

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

Претпоставимо да је низ A сортиран у неопадајућем поретку, да је укупан број елемената низа N и да су елементи низа индексирани бројевима од 0 до N-1. Тада је медијана низа елемент са индексом N/2 ()при чему се рачуна целобројни део количника. Нека је B број поена који Маркос жели као границу. Тада разликујемо три случаја

Ако је A[N/2] = B, онда је медијана већ једнака броју B и Маркос гласа 0 пута.

Ако је B < A[N/2] одређујемо индекс i последњег елемента који није већи од B (ако такав не постоји, онда је i=-1). Тада постоји i+1 елемената који су мањи од или једнаки B и N-i-1 елемената који су већи од B, при чему је i+1 \leq N-i-1. Да би обезбедили да B буде медијана потребно је да група мањих или једнаких има бар један елемент више од групе већих, па је потребно убацити N-i-1-(i+1)+1 = N-2i-1 гласова.

Ако је B>A[N/2] одређујемо индекс i првог елемента који није мањи од B ( ако такав не постоји, онда је i=N). Тада постоји i елемената који су мањи од B и N-i елемената који су већи од или B, при чему је i \geq N-i. Да би обезбедили да B буде медијана потребно је да група већих или једнаких има бар онолико елемента колико има мањих, па је потребно убацити i - (N - i) = 2i-N гласова.

Сложеност алгоритма је одређена сложеношћу сортирања (која може бити \Theta(N\log N)) и сложеношћу проналажења последњег елемента који није већи од B, односно првог елемента који није мањи од B. Ако ово реализујемо као секвенцијално претраживање, сложеност дела који се односи на одређивање одговора на питања ће бити \Theta(QN), па ће укупна сложеност бити \Theta(N\log N + NQ). Ако проналажење описаног елемента реализујемо коришћењем бинарне претраге (коју свакако можемо користити, ако смо низ сортирали), онда је сложеност одређивања одговора на питања \Theta(Q\log N), па је сложеност комплетног алгоритма \Theta((N+Q)\log N).

1 Like

Санте

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

Корисно је уочити да санте формирају граф од N чворова и M грана, где свака грана има тежину 0 или 1. Могуће је без промене стања машине кретати се само преко грана оне тежине које је тренутно стање (0 - машина не ради, 1 - машина ради). Почетно стање је 0.

Решење када M = N-1 и из сваке санте иде једна или две гране

Овако задате санте формирају низ где је свака повезана са претходном и следећом (осим прве и последње).
Можемо доћи од једне до друге санте на јединствен начин (покушамо да идемо лево од Мике док не стигнемо до краја, или до Лазе, и исто тако на десно), и симулирати промене
стања.

Решење кад је пут између сваке две санте тешко проходан

Овде је једини могући одговор 1 или -1, зависно од тога да ли је могуће доћи од Микине до Лазине санте. Ово можемо проверити неким алгортимом попут DFS-а (рекурзивни обилазак сваке санте са памћењем посећених).

Решење кад је M = N-1 и увек се може доћи од једне до друге санте

Граф који санте описују у овој групи примера је стабло. Као у првом подзадатку, јединствен је начин да се дође од једне санте до друге (особина стабла), само је овај пут
потребно користити неки алгоритам за налажење пута (овде је пут скуп грана, а не термин из текста) између два чвора (као BFS), и симулирати промене стања машине на њему.

Решење за пун број поена

Приметимо да уколико је стање машине s, а налазимо се у чвору u, могуће је слободно (без повећања коначног одговора) се кретати по свим чворовима
до којих се гранама тежине s може доћи из u. Дакле, конструкција нашег решења може изгледати тако што прво обиђемо све чворове до којих се може доћи из почетног користећи гране
тежине 0, затим променити тежину и наћи све достижне преко грана тежине један од претходно достигнутих итд. (наравно не обилазећи већ посећене чворове).

Елегантну имплементацију ове идеје нам пружа такозвани 0-1 BFS. У реду са два краја (double ended queue) памтимо чвор као и стање у ком се налазимо кад га посећујемо (на почетку додамо Микину санту и 0),
и на почетак реда додајемо оне чворове који су повезани гранама тежине која одговара стању чвора,
док се на крај додају чворови повезани гранама супротне тежине (и у оба случаја ажурирамо цену доласка до неког чвора). Овако смо се осигурали да прво пролазимо кроз
све доступне из првог чвора без промене стања, а да даље не мењамо стање док нисмо обишли све који се достижу једном променом стања, итд.

На крају једноставно проверимо да ли је обиђен Лазин чвор, и која је цена његовог обиласка.
Временска и меморијска сложеност су O(N+M).

Решење 2 (за пун број поена)

Како је могуће слободно се кретати од неког чвора по одређеној групи чворова, можемо груписати чворове у 0-компоненте и 1-компоненте (зависно од тога каквим се гранама крећемо) DFS-ом,
а затим формирати нови граф у ком повезујемо две компоненте уколико не постоји чвор преко ког је могуће прећи из једне у другу (у овом чвору мењамо стање машине). Касније налазимо најкраћи пут између одговарајућих
компоненти у овом графу BFS-ом (с тим што је почетна удаљеност 1 уколико крећемо од 1-компоненте). Свака компонента је или 1 или 0 компонента и има најмање један чвор, те
њих има реда величине N, док грана између њих има највише N (јер један чвор представља грану између компоненти) - дакле ред величине сложености је поново линеаран.

Напомена: спорија имплементација идеје за 100 поена (нпр Дајкстрин алгоритам у квадратној сложености уместо 0-1 BFS) ће радити за вредности N и M до 5000

1 Like

Ваге

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

Решење када N, M \leq 1000

За свако питање можемо једноставно проћи кроз све битне ваге и проверити да ли могу измерити тренутно x_i.

Временска сложеност је O(NM), а меморијска O(N).

Решење када d_i=u_i

У овом случају потребно је одредити, за свако питање, колико вага којима је d_i једнако x_i има индекс од l_i до r_i. Пошто нам нису битне конкретне вредности d_i, u_i и x_i, већ само да ли су једнаки, ове вредности можемо компресовати, на пример користећи std::map, тако да све буду највише N. Када то урадимо, можемо за сваки различит елемент у низу d_i да направимо нови низ, који садржи, сортирано растуће, све индексе вага којима одговара та вредност d_i.

Сада, кад је потребно да одговоримо на питање, можемо користећи бинарну претрагу да нађемо колико елемената у низу индекса који одговара вредности x_i се налази на интервалу [l_i, r_i].

Временска сложеност је O((N+M)logN), а меморијска O(N).

Решење када не постоји x које могу да измере две различите ваге

Овај услов се другачије може написати да су интервали [d_i, u_i] дисјунктни. Сортирајмо интервале по левој граници, такође чувајући индекс сваког интервала. Након тога решавамо питања једно по једно, тако што бинарном претрагом тражимо последњи интервал коме је лева граница мања од тренутног x_i.

Уколико се x_i не налази у том интервалу или се индекс тог интервала не налази у интервалу [l_i, r_i], резултат за тренутно питање је 0. У супротном, резултат је 1.

Временска сложеност је O((N+M)logN), а меморијска O(N).

Решење када u_i \leq 20

Овај случај се решава слично случају када важи d_i = u_i, с тим што, уместо да убацимо сваки индекс у низ индекса само за вредност d_i, ми ћемо га убацити у низ индекса за сваку вредност у интервалу [d_i, u_i]. Након тога, одговор на питање налазимо на исти начин као раније. Потребно је посебно пазити за случај када x_i > 20.

Временска сложеност је O(N\cdot max(u_i) + MlogN), а меморијска O(N).

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

Како бисмо решили задатак за све бодове, потребно је да на питања одговарамо offline, односно у другачијем редоследу него што су дата, па ћемо их на крају исписати у тачном редоследу. На питања ћемо одговарати у редоследу растуће по x_i (уколико више питања има исто x_i, није битно на које одговарамо прво).

Знамо да нам је вага j битна само ако важи d_j \leq x_i \leq u_j. Дакле, како идемо кроз питања растуће по x_i, постоје две врсте догађаја:

  • Вага j постаје битна први пут када постављамо питање за које важи x_i \geq d_j;
  • Вага j престаје да буде битна при пут када постављамо питање за које важи x_i \geq u_j+1.

Убацимо свих 2N догађаја у један низ и сортирајмо га растуће по њиховим границама за x_i. Сада можемо заједно пролазити кроз низ питања и низ догађаја методом два показивача и одржавати низ нула и јединица дужине N који на i-тој позицији садржи 1 ако и само ако је вага са индексом i тренутно битна. Наравно, овај низ ћемо мењати сваки пут када на ред дође неки нови догађај.

Како бисмо ефикасно одговорили на питање, потребно је да ефикасно нађемо збир на интервалу [l_i, r_i] овог низа, у тренутку када смо обрадили све догађаје који долазе пре овог питања. За ово можемо користити сегментно или Фенвиково стабло како бисмо остварили добру сложеност.

Временска сложеност је O((N+M)logN), а меморијска O(N).

Златници 3

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

Решење у O(QN^2)

Покушајмо да пронађемо за сваки чвор u колико треба потеза да бисмо све златнике пренели у њега. Ово је сума \sum_{i=1}^nd(u,i)\cdot w_i, где w_i број златника у i, а d(u,v) дужина пута између чворова u и v. Ово се лако може израчунати пуштањем ДФС из сваког чвора.

Решење у O(QN)

Потребно је да мало дубље анализирамо који чвор ће бити одговор. Нека са V_v означавамо број потеза који је потребан да би сви златници стигли у чвор v. Потребно је наћи чвор тако да је V_u=\sum_{i=1}^nd(u,i)\cdot w_i минимално. Посматрајмо вредност V_u-V_v за неку грану uv. Из горенаведене формуле видимо да је ово заправо једнако разлици сума тежина у два стабла која добијемо када исечемо грану uv (уверите се!). Када смо у оптималном чвору, ова вредност треба да буде негативна за све гране, што је еквивалентно са тим да кад исечемо тај чвор, у сваком од добијених стабала је највише пола од укупне тежине (овакав чвор се зове центроидом стабла). Сада лако се види да је овакав чвор јединствен, или их је два која су спојена граном са разликом рењења од 0 (а у том слулају су оба валидно решење). Стога, свели смо задатак на тражење шта је центроид у овом стаблу, што може да се уради ДФС и чувају се величине свих подстабала, а затим се уради једноставна провера.

Решење када је стабло пут

У овом случају је решење тернарно претраживо по путу, и све дист функције се могу израчунати лако. Могуће је урадити тад у задатак O(N\log N + Q\log^2 N), за једну тачку претраге у тернарној нам треба само суме по префиксима и суфиксима, а промени тих можемо да пратимо у сегментном стаблу.

Решење када је стабло комплетно бинарно

Настављамо на решење из другог подзадатка. Оно што треба приметити је да је вредност V_v-V_u могуће израчунати брзо за неку грану. Заиста ако сместимо чворове у низ по улазном или излазном поретку ДФС, и имамо сегментно стабло над тим низом, пошто ће сви чворови у неком стаблу формирати интервал. Нама је потребна само тежина у два стабла која добихемо кад исечемо ову грану, а једно од тих стабала ће морати да буде подтабло укорењеног стабла. Тежину у другом можемо израчунати као укупна тежина минус то што смо израчунали. Стога, промену резултата проласком из једног чвора у други, (то јест да ли је боље ако пређемо) можемо израчунати у O(\log N).

Сада кад додамо нови чвор, ново решење ће се налазити на путу од тренутног решења до тог новог на који смо додали златнике. Стога, на том путу ће промене резултата бити позитивне, па негативне. Да бисмо решили задатак потребно је само итерирати кроз пут и наћи све гране кроз које кад прођемо побољшавамо резултат. Пошто су на овом стаблу сви путеви кратки, налазимо решење у O(N\log N + Q\log^2 N).

Решење без додатних ограничења

Коначно решење је мала надоградња претходног. Наиме, уместо да идемо редом по путу између старог и новог чвора, ураидмо бинарну претрагу на том путу да бисмо пронашли последњу грану која нам даје позитиван скор на резултат. По свему описаном у претходном подзадатку, ово ће заиста бити бинарно претраживо и стога ће ово решење радити у O(N\log N + Q\log^2 N).

Kada možemo da očekujemo rezultate?

3 Likes

Za par dana.

Tokom takmičenja sam pronašao alternativno rešenje za 70 bodova, koje se zapravo sastoji iz 3 skroz odvojena zadatka, koje ne zahteva nikakvo znanje o centroidima, ne pomaže pri rešavanju za 100 bodova, mada mi je zanimljivo bilo izvođenje, takođe sve tri tehnike koje sam koristio su krajnje nepovezane i sva rešenja se razlikuju od predloženih.

Rešenje kada nema upita

Pretpostavimo da je stablo ukorenjeno u čvoru v, neka je u čvoru i smešteno a_i novčića. Tada je f(v)=\sum_{i=1}^Na_i\cdot{dist(i,v)} broj koraka neophodan za premeštanje svih novčića do čvora v, gde je dist(u,v) udaljenost čvorova u i v u stablu.

Međutim ukoliko bismo ukorenili stablo u svakom od čvorova 1,2,..,N, svaki upit f(i) ima složenost O(N) pa je nakon svakog ažuriranja potrebno O(N^2) operacija.

Ovo se međutim može optimizovati pomoću “rerooting” tehnike. Dakle, neka je dp_v vrednost f(v), a sz_v suma svih a_i u podstablu čvora v, jednim dfs prolazom može se odrediti vrednost dp_u za svako u.

Rešenje kada je stablo bambus

Možemo modelirati stablo kao jednodimenzioni niz a dužine N, gde je a_i broj novčića u čvoru i. Pretpostavimo da je optimalno sve novčiće prebaciti na poziciju i. Ukoliko bismo novčiće prebacili na poziciju i-1 ukupna cena povećava se se za \sum_{j=i}^{N}a_j-\sum_{j=1}^{i-1}a_j. Primetimo da je ova suma, gledano zdesna, opadajuća do nekog k, a potom striktno rastuća. Dakle, moguće je binarnom pretragom ustanoviti optimalan indeks v gde treba prebaciti sve novčiće. Takođe, iz uslova zadatka da svaki čvor mora sadržati bar jedan novčić, ne postoje dva indeksa na kojima je cena prebacivanja jednaka. Dakle nije neophodno rešenje ternarnom pretragom, već korišćenjem nejednakosti se dolazi do zaključka da i binarna pretraga može rešiti zadatak.

Rešenje kada je stablo kompletno binarno stablo

Ovaj slučaj rešavamo time što na svakom nivou stabla održavamo najmanje rešenje, pa potom upoređujemo rešenje za svaki od \log{N} nivoa. Rešenje u korenu stabla je očigledno zbir proizvoda dubina čvorova i broja novčića u njima. Samim tim ažuriranje njegove vrednosti je jednostavno, pa je dovoljno pretprocesiranjem odrediti dubinu svakog čvora i time ažurirati ukupnu sumu.

Koristeći tranzicije iz reroot rešenja, primetimo da su sume na prvom nivou za čvor v jednake sumi u korenu stabla kada im oduzmemo broj novčića u podstablu čvora v, a dodamo sve ostale novčiće van tog podstabla. Sređivanjem nejednakosti dobija se da je promena u čvoru v jednaka tot-2\cdot{sum_v}, gde je tot ukupan broj novčića u celom stablu, a sum_v broj novčića u podstablu čvora v. Analogno dobijamo i rešenje za sve ostale nivoe, odnosno u opštem slučaju, rešenje u čvoru u je ans_{root}+nivo_u\cdot{tot}-2\cdot{\sum_{v-roditelj-u}{sum_v}}, gde su v čvorovi koji su preci čvora u, uključujući i samo u.

Budući da se stablo sastoji od najviše 18 nivoa, a svaka promena utiče samo na roditelje čvora b u koji ubacimo z zlatnika, lako se dobijaju jednakosti po kojima se menjaju sume predaka.

Informacije o svakom nivou možemo čuvati u setu, i u logaritamskom vremenu pronaći minimum na svakom nivou.

Kod je podeljen u tri funkcije, solve_binary za binarno stablo, solve_regular za reroot rešenje i solve_bamboo za bambus.

KOD
#include <bits/stdc++.h>
using namespace std;
const int N=2e5;
int n,m,par[N+5],lvl[N+5];
long long a[N+5],sum[N+5],sz[N+5];
vector<int> g[N+5],nodes[25];
set<pair<long long,int>> s[25];
struct bit{
	int n;
	vector<long long> a;
	bit(int n){
		this->n=n;
		a.resize(n+1);
	}
	void upd(int v,int val){
		for(;v<=n;v+=v&-v)a[v]+=val;
	}
	long long qry(int v){
		long long ret=0;
		for(;v;v-=v&-v)ret+=a[v];
		return ret;
	}
};
int check_bamboo(){
	for(int i=1;i<=n;i++)if((int)g[i].size()>2)return 0;
	return 1;
}
void dfs_bamboo(int v,int p,vector<int> &bamboo){
	bamboo.push_back(v);
	for(int u:g[v]){
		if(u==p)continue;
		dfs_bamboo(u,v,bamboo);
	}
}
void solve_bamboo(){
	int root=1;
	for(int i=1;i<=n;i++)if((int)g[i].size()<=1)root=i;
	bit tree(N);
	vector<int> bamboo(1);
	vector<int> ind(n+1);
	dfs_bamboo(root,root,bamboo);
	for(int i=1;i<=n;i++)ind[bamboo[i]]=i;
	for(int i=1;i<=n;i++)tree.upd(i,a[bamboo[i]]);
	int lo=1,hi=n;
	int res=1;
	for(;lo<=hi;){
		int mi=(lo+hi)>>1;
		if(2LL*tree.qry(mi)>=tree.qry(n)){
			res=mi;
			hi=mi-1;
		}else{
			lo=mi+1;
		}
	}
	cout<<bamboo[res]<<'\n';
	for(int i=1;i<=m;i++){
		int z,b;cin>>z>>b;
		tree.upd(ind[b],z);
		int lo=1,hi=n;
		int res=1;
		for(;lo<=hi;){
			int mi=(lo+hi)>>1;
			if(2LL*tree.qry(mi)>=tree.qry(n)){
				res=mi;
				hi=mi-1;
			}else{
				lo=mi+1;
			}
		}
		cout<<bamboo[res]<<'\n';
	}
}
void dfs_regular(int v,int p){
	sz[v]=a[v];
	sum[v]=0;
	for(int u:g[v]){
		if(u==p)continue;
		dfs_regular(u,v);
		sz[v]+=sz[u];
		sum[v]+=sum[u]+sz[u];
	}
}
void reroot(int v,int p,long long &ans,int &ind){
	if(ans>sum[v]){
		ind=v;
		ans=sum[v];
	}else if(ans==sum[v]){
		ind=min(ind,v);
	}
	for(int u:g[v]){
		if(u==p)continue;
		long long tmp=sum[v]-sum[u]-sz[u];
		long long tmp2=tmp+sz[v]-sz[u];
		long long tmp3=sz[v]-sz[u];
		sum[u]+=tmp2;
		sz[u]+=tmp3;
		reroot(u,v,ans,ind);
		sz[u]-=tmp3;
		sum[u]-=tmp2;
	}
}
void solve_regular(){
	long long ans=1e18;int ind=0;
	dfs_regular(1,1);
	reroot(1,1,ans,ind);
	cout<<ind<<'\n';
}
void dfs_furthest(int v,int p,int cur,int &root,int &dist){
	if(dist<=cur){
		dist=cur;
		root=v;
	}
	par[v]=p;
	for(int u:g[v]){
		if(u==p)continue;
		dfs_furthest(u,v,cur+1,root,dist);
	}
}
void dfs_binary(int v,int p,int d,int root){
	lvl[v]=d;
	sum[root]+=d*a[v];
	sz[v]=a[v];
	nodes[d].push_back(v);
	for(int u:g[v]){
		if(u==p)continue;
		dfs_binary(u,v,d+1,root);
		sz[v]+=sz[u];
		par[u]=v;
	}
}
void solve_binary(){
	int root=1;
	for(int i=1;i<=n;i++){
		if((int)g[i].size()==2){
			root=i;
		}
	}
	int f1=root,f2=root,d1=0,d2=0;
	dfs_furthest(g[root][0],root,0,f1,d1);
	dfs_furthest(g[root][1],root,0,f2,d2);
	vector<int> diam1,diam2;
	for(;f1!=root;){
		diam1.push_back(f1);
		f1=par[f1];
	}
	diam1.push_back(root);
	for(;f2!=root;){
		diam2.push_back(f2);
		f2=par[f2];
	}
	for(int i=(int)diam2.size()-1;i>=0;i--){
		diam1.push_back(diam2[i]);
	}
	root=diam1[(int)diam1.size()/2];
	if((int)g[root].size()!=2)root=diam1[(int)diam1.size()/2-1];
	dfs_binary(root,root,0,root);
	for(int i=1;i<25;i++){
		for(int j:nodes[i]){
			sum[j]=sum[par[j]]-2*sz[j];
			if(i==1)sum[j]-=sum[par[j]];
			s[i].insert({sum[j],j});
		}
	}
	pair<long long,int> res={sum[root],root};
	for(int j=1;j<25;j++){
		if(s[j].empty())continue;
		auto it=*s[j].begin();
		res=min(res,{it.first+j*sz[root]+sum[root],it.second});
	}
	cout<<res.second<<'\n';
	for(int i=1;i<=m;i++){
		int z,b;cin>>z>>b;
		sum[root]+=1LL*z*lvl[b];
		sz[root]+=z;
		for(;b!=root;){
			s[lvl[b]].erase({sum[b],b});
			sum[b]-=2LL*z*lvl[b];
			s[lvl[b]].insert({sum[b],b});
			b=par[b];
		}
		pair<long long,int> res={sum[root],root};
		for(int j=1;j<25;j++){
			if(s[j].empty())continue;
			auto it=*s[j].begin();
			res=min(res,{it.first+j*sz[root]+sum[root],it.second});
		}
		cout<<res.second<<'\n';
	}
}
void solve(){
	cin>>n;
	for(int i=1;i<n;i++){
		int u,v;cin>>u>>v;
		g[u].push_back(v);
		g[v].push_back(u);
	}
	for(int i=1;i<=n;i++)cin>>a[i];
	cin>>m;
	if(m==0){
		solve_regular();
	}else if(check_bamboo()){
		solve_bamboo();
	}else{
		solve_binary();
	}
}
signed main(){
	ios_base::sync_with_stdio(0);
	cin.tie(0);
	int t=1;
	for(;t;t--){
		solve();
	}
	return 0;
}
2 Likes