460 likes | 603 Views
Абстрактни типове с две противоречащи една на друга операции. Кр. Манев, 09.2008. Абстрактни типове. Абстрактен тип наричаме: Множество от (математически дефини-рани) обекти ;
E N D
Абстрактни типове с две противоречащи една на друга операции Кр. Манев, 09.2008
Абстрактни типове • Абстрактен тип наричаме: • Множество от (математически дефини-рани) обекти; • Множество от операции. Операциите могат да бъдат както вътрешни – ар-гументите им са само обекти от множеството, така и външни – аргументите им са както обекти от множеството, така и други обекти.
Имплементаци на абстрактните типове • Всеки абстрактен тип може да се имплементира по няколко различни начина: • Обектите от множеството представяме в някаква структура от данни. Ако желаем, можем да оформим структурата от данни като тип (typedef)или обект; • Всяка операция имплементираме с програмна функция/метод – функциите, типа и начина на задаване на аргументите, типа на резултата и т.н. наричаме интерфейс.
Програмиране с абстрактни типове • Когато трябва да решим приложна задача с помощта на имплементирания абстрактен тип, добре е да спазваме простото правило: всеки достъп до обект на типа да става само през интерфейса. • Резултатът ще бъде - винаги можем да подменим имплементацията на интерфейса без да променяме кода, който решава приложната задача.
Абстрактен тип Стр. от данни Стр. от данни Стр. от данни Имплементации Функции Функции Функции Интерфейс Приложна програма Програмиране с абстрактни типове
Абстрактният типразбиване (на множество) • Дефиниция: A = {a1, a2,…, aN} емножество с Nелемента.R={S1, S2,…, SK},SiA наричаме разбиване на A, ако: • Si ,i= 1, 2,…,K ; • Si Sj = ,1 i, j K, i j; • i= 1, 2,…,KSi=A . {{0, 3, 6, 9}, {1, 4, 7}, {2, 5, 8}}е разбиване на {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}.
Операции с абстрактния типразбиване (на множество) • Нека R={S1, S2,…, SK}еразбиване на A = {a1, a2,…, aN}. Дефинираме следните операции: • find(R, ai)– резултат Sj,ai Sj; • join(R, Si , Si) – резултат ново разбиване:R’={S1, …, Si-1,Si+1,…, Sj-1,Sj+1 ,…, SK, S}, S = Si Si; • init(R, N)– резултат R={{1}, {2},…, {N}}, и т.н. За нашия пример, find(R, 0)= S1, find(R, 1)= S2, join(R, S1, S2)={{0, 1, 3, 4, 6, 7, 9},{2,5,8}}.
Задача • Да разгледам сега една задача, която е подходяща за решаване с помощта на абстрактния тип разбиване: • Даден е графG(V,E) с тегла c:ERна ребрата. Да се построи Минимално покриващо дърво T(V,E’) на G, т.е. такова покриващо дърво, че сумата от теглата на ребрата му да е минимална.
Алгоритъм на Крускал • Сортираме ребрата на графа в нарастващ ред на теглата – и нека той е e1, e2,…, eM • От всеки връх vна графа правим отделно дърво Tv({v},) • for(i=1;i<=M;i++) {/*ei=(v,w)*/ if (v и w са в различни дървета) свързваме v и w с ребро;}
Интерфейс за разбиване • Нека сме дефинирали структура от данни, подходяща за представяне на АТ разбиване и сме я оформили като тип - Part. Eлементите на множеството - Elem,частите –Sub. • Дефинираме следните функции: • Sub find(Part *R,Elem a) • Part join(Part *R,Sub S1,Sub S2) • void init(Part *R, int N)
Имплементация на алгоритъма на Крускал int N,M,g[MM][3],t[MN][2];Part P; void Kruskal() {int j=1;Sub s1,s2; Sort(g); init(&P,N); for(int i=1;i<=M;i++) if (s1=find(&P,g[i][0])!= s2=find(&P,g[i][1])) { join(&P,s1,s2); t[j][0]=g[i][0]; t[j++][1]=g[i][1]; } }
Сложностпо време на алгоритъма на Крускал • Некаозначим с Tinit(n),Tfind(n)иTjoin(n)сложостите по време на имплементациите на функциите. • Тогава за сложността TKr(N,M) на имплeмeнтацията на алг. на Крускал: TKr(N,M) = O(M.logM)+Tinit(n) + + 2.M. Tfind(n) + (N – 1). Tjoin(n)
Имплементация на прост интерфейс за разбиване • typedef {int N,int P[MN];} Part; • Elem= Sub=int. • За всеки елемент a, в P[a]помним елемента с най-малка стойност в неговата част на разбиването – лидер на частта: Дефинираме следните функции: • void init(Part *R, int N) {R->N=N; for(int i=1;i<=N;i++)R->P[i]=i;}
Имплеметация на прост интерфейс за разбиване • int find(Part *R,int a) { return R->P[a];} • void join(Part *R,int S1,int S2) { int L=min(S1,S2);K=max(S1,S2); for(int i=1;i<=R->N;i++) if(R->P[i]==K) R->P[i]==L; }
Сложностпо време на имплементацията • Tinit(N)= O(N), • Tfind(N)= O(1), • Tjoin(N) = O(N). • Тогава за сложността TKr(N,M) на имплeмeнтацията на алг. на Крускал: TKr(N,M) = O(M.logM)+O(N) + 2.M. O(1) + (N – 1). O(N) = O(M.logM + N2) – неприятно за графи с малко ребра
Друга имплементация на интерфейс за разбиване • typedef {int N,int P[MN];} Part; • Elem= Sub=int. • За всеки елемент a, в P[a]помним елмента, корен (лидер) на дърво с всички ел. на частта: Дефинираме следните функции: • void init(Part *R, int N) {R->N=N; for(int i=1;i<=N;i++)R->P[i]=i;}
Имплеметация на прост интерфейс за разбиване • int find(Part *R,int a) { int x=a; while(R->P[x]!=x) x=R->P[x]; return x;} • void join(Part *R,int S1,int S2) { R->P[S2]=S1;}
Сложностпо време на новата имплементацията • Tinit(N)= O(N), • Tfind(N)= O(N), • Tjoin(N) = O(1). • Тогава за сложността TKr(N,M) на имплeмeнтацията на алг. на Крускал: TKr(N,M) = O(M.logM)+O(N) + 2.M. O(N) + (N – 1). O(1) = O(M.logM + M.N) – неприятно за графи с много ребра
Как да подобрим втората имплементация • Балансиране на дървото по височина: за целта добавяме в структурата масив h[] и за всяко дърво помним текущата му височина в h[i]. (h[i]=0 в началото) void join(Part *R,int S1,int S2) { if(h[S2]<h[S1])R->P[S2]=S1; else R->P[S1]=S2; if (h[S2]==h[S1]) h[S2]++; }
Как да подобрим втората имплементация • Повдигане на дърветата: за целта променяме малко функцията find: int find(Part *R,int a) { int x=a,y=a; while(R->P[x]!=x) x=R->P[x]; while(R->P[y]!=x) {z=R->P[y]; R->P[y]=x; y=z;} return x;}
Сложностпо време на имплементацията • Tinit(N)= O(N), • Tfind(N)= O(log* N) O(1), • Tjoin(N) = O(1). • Тогава за сложността TKr(N,M) на имплeмeнтацията на алг. на Крускал: TKr(N,M) = O(M.logM)+O(N) + 2.M. O(log* N) + (N – 1). O(1) = O(M.logM + M + N) – не зависи същестено от съотнешението на брой върхове и брой ребра.
МОБИФОНИ IOI’2001 В равнината е зададена квадратна мрежа с размери SS,S <=1024. Всяко квадратче е покрито от клетка на мобилен оператор и в него във всеки момент се намират определен брой мобилни телефони. Броят на телефоните в едно квадратче непрекъснато се мени. В определени моменти всяка от клетките докладва на централата за настъпилите изменения, а от време навреме, в централата си задават въпроси за броя на работещите в момента телефони в някаква правоъгълна подобласт на мрежата.
МОБИФОНИ IOI’2001 едномерен вариант
Абстрактен тип • Дефинирамеабстрактен тип Net –редица от S клетки с операции: • void init(Net* M,int S) - инициализирамрежаM с размер S. • void chng(Net* M,int X,int A) - регистрира измененията; • int find(Net* M,int L,int R)- дава отговори.
Решение scanf(”%d %d”,&code,&S); init(&M,S); while(1) {scanf(”%d”,&code);if(code==3) break; if(code==1) {scanf(”%d %d”,&X,&A);chng(&M,X,A);} else {scanf(”%d %d”,&L,&R); ptintf(”%d\n”,find(&M,L,R));} }
Имплементация на АТ typedef int a[1025] NetM; void init(Net* M,int S) { int i;for(i=1;i<=S;i++) M->a[i] =0; } void chng(Net* M,int X,int A) { M->a[X] +=A; } int find(Net* M,int L,int R) {int i, res=0; for(i=L;i<=R;i++) res+=M->a[i]; return res;}
Сложност • init – O(S) • chng - O(1) • find - O(S) • За целия алгоритъм O(S) + Q1. O(1) + Q2.O(S), където Qk е броят на заявките от тип k Много голям брой заявки от тип 2 ще компрометира решението.
Имплементация на АТ typedef int a[1025] NetM; void init(Net* M,int S) { int i; for(i=1;i<=S;i++) M->a[i] =0; } void chng(Net* M,int X,int A) { int i; for(i=x;i<=S;i++) M->a[X] +=A; } int find(Net* M,int L,int R) {return M->a[R]- M->a[L-1];}
Сложност • init – O(S) • chng - O(S) • find - O(1) • За целия алгоритъм O(S)+Q1. O(S)+ Q2. O(1), където Qk е броят на заявките от тип k Много голям брой заявки от тип 1 ще компрометира решението.
Как да излезем от ситуацията • Изходът от ситуацията в такива задачи (абстр. тип с две “противоречиви” операции) е да се опитаме да направим двете операции сравнително бързи • Обичайното решение е да се замени линейната имплементация (списък) с дървовидна, така че сложността и на двете операции да е сравнима с височината на дървото.
Индексно дърво • За всяко i = 1,2,…,S да пресметнем границите на интервала [i-2k(i)+1,i], където k(i)е броят на нулите отдясно в двоичното представяне на i. В d[i] ще държим сумата на числата в интервала [i-2k(i)+1,i]
Индексно дърво 28 16 9 5 2 3 0 6 6 2 7 3 4 0 5 1
Операциите typedef int a[1025] NetM; void init(Net* M,int S) { int i; for(i=1;i<=S;i++) M->a[i] =0; } void chng(Net* M,int X,int A) { while(X<=S)M->a[X]+=A;X+=k(X);} int find(Net* M,int L,int R) {int res=0; while(R>=1){res+=M->a[R];res-=k(R);} while(L>=1){res-=M->a[L];res-=k(L);} return res;}
Намиране на k(i) • Стойностите на k(i) могат да бъдат табулирани предварително и след това да бъдат вземани от един масив. Това ще добави O(S. log S) стъпки. • По-бързо ще стане ако в началото на всяка от операциите се постави 1 в променлива eи докато e&i != 0се измества eна една позиция наляво (е<<=1). При това за следващата стойност на iне се налага ново зареждане на e, а може да се продължи с текукщата стойност.
Сложност • init – O(S);chng - O(log S) • find - O(log S) • За целия алгоритъм O(S) + Q1. O(log S) + Q2.O(log S)=O(S + Q.log S), където Q е броят на заявките от тип k Реиението вече не зависи от броя заявки от различен тип.За двумерния случай, трябва да се приложи техниката в двете посоки. Направете го за домашно непременно.
ХАМАЛИ – НЕТ, Шумен’08 Бали боклук трябва да бъдат извозени до сметищетопрез канализация - N шахти, 1 е сметището.Всяка шахта Uе свързана с тръба към точно една шахта V<Uи течението ще отнесе до Vвсеки предмет, пуснат вU. Бала, пусната в Uможе да стигне до всяка шахта, която се намира на пътя от Uдо 1, стига в шахтите по пътя да има достатъчно вода, за да не заседне балата в тях. В 10.00:00 сутринта, всички шахти са празни, започва да вали дъжд и шахтите започват да се пълнят с 10 куб.см вода в секунда. Хамалите хвърлят бала в шахта X и я чакат в шахта Y, разположена по пътя от X до 1. Максималният обем, който може да бъде пренесен така, е равен на минималнoто количество вода в куб.см., което се съдържа в шахта по пътя от Xдо Y.
ХАМАЛИ – НЕТ, Шумен’08 Какъв обем може да бъде пренесен от X доY в зададен момент, ако конкурентите от „Цепи-Лепи” АД, не изпомпваха в здадени моменти зададено кол. вода от някои от шахтите. Вход: Първи ред: N и K; Втори ред: за всеки връх – бащатаСледват K заявки: 1 X Y T – колко обем може да мине от X до Yв момент T 2 T X V – конкурентите изпомпват от връх X, в момента T, V куб. см вода Изход. За всяка заявка от тип 1 – тъсеният обем
АНАЛИЗ • Нужни са ни две операции с дървото • void chng(vertex X, int V) • int find(int T,vertex X, vertex Y) • Решение с O(1) за chng() и O(h) за find() e очевидно. При изродени дрвета h=O(N) и сложността на алгоритъма е O(Q.N), кдето Q e общия бройна операциите.Търси се по-добро. Проблемът е, че зададеният тип вече е дърво!!!
АНАЛИЗ Да означим с • t(X) бащата на X, • p(X,Y) пътя от X до Y • W(X) – количеството вода изпомпена от X до момент t. • Тогава наличната водав X в момент t ще бъде C(X)=10.t – W(X) • Jump(X)=X’, като X’(X,1); • F(X) =max W(Y), Y(X,Jump(X))равносилно на търсения min C(Y), Y(X,Jump(X))
Иплементация int t[MAXN],W[MAXN],F[MAXN]; void chng(int X,int V) {W[X]+=V; за (всеки A,Xp(A,Jump(A)) F(A)= max{F(A),W[X]; } int find(int T,int X,int Y) {int i=X,M=0; while(i!=Y) {if(Jump(i)p(i,B)) {M=max(M,F(i);i=Jump(i);} else {M=max(M,W(i);i=t(i);} return 10*T-M; }
Сложност • Нека S e такова, че дължините на p(X,Jump(X)) са колкото може по-близки до Sбез да го надхвърлят – идеалът е точно S • Сложността на chng() е O(S), защото се обхожда един път от X до Jump(X) • Сложността на find() е O(N/S+S), защото се обхожда един път в дървото: първо на стъпки от по около S върха и после не повече от S стъпки по 1 връх
Сложност Сложността на алгоритъма ще бъде • Q1.O(S)+Q2.O(N/S+S), където Q1 и Q2 са брй на операциите отпърви и втори вид • Ако S=sqrt(N), тогава алгоритъмът е много приличен O(Q.sqrt(N)) • Ако се преброят предварително операциите, може да се опита и по-добра стойност за S.
Смятане на Jump(X) Depth[1]=1; Weight[…] = 1; Обхождаме дървото в пост-ордер и за всеки токущо обходен връхА { За(всеки наследникВнаА) { Weight(A)=Weight(A)+ Weight(B); Depth(B)=1+Depth(A); } }
Смятане на Jump(X) Jump(1)= 1; С обхождане на дървото в дълбочина, начален връх 1 иа всеки връхА ≠ 1 {Jump(A)= Jump((А)); while(Weight(Jump(A))–Weight(A)>S) {Jump(A)= наследника наJump(A) в(А,Jump(A);} }