Upload
skule
View
50
Download
1
Embed Size (px)
DESCRIPTION
Абстрактни типове с две противоречащи една на друга операции. Кр. Манев, 09.2008. Абстрактни типове. Абстрактен тип наричаме: Множество от (математически дефини-рани) обекти ; - PowerPoint PPT Presentation
Citation preview
Абстрактни типове с две противоречащи една на
друга операции
Кр. Манев, 09.2008
Абстрактни типове
Абстрактен тип наричаме: Множество от (математически
дефини-рани) обекти; Множество от операции.
Операциите могат да бъдат както вътрешни – ар-гументите им са само обекти от множеството, така и външни – аргументите им са както обекти от множеството, така и други обекти.
Имплементаци на абстрактните типове
Всеки абстрактен тип може да се имплементира по няколко различни начина: Обектите от множеството представяме в
някаква структура от данни. Ако желаем, можем да оформим структурата от данни като тип (typedef) или обект;
Всяка операция имплементираме с програмна функция/метод – функциите, типа и начина на задаване на аргументите, типа на резултата и т.н. наричаме интерфейс.
Програмиране с абстрактни типове
Когато трябва да решим приложна задача с помощта на имплементирания абстрактен тип, добре е да спазваме простото правило: всеки достъп до обект на типа да става само през интерфейса.
Резултатът ще бъде - винаги можем да подменим имплементацията на интерфейса без да променяме кода, който решава приложната задача.
Програмиране с абстрактни типове
Абстрактен тип
Стр. от данниФункции
Стр. от данниФункции
Стр. от данниФункции
Имплементации
Интерфейс
Приложна програма
Абстрактният типразбиване (на
множество)
Дефиниция: A = {a1, a2,…, aN} е множество с N елемента. R = {S1, S2,…, SK}, Si A наричаме разбиване на A, ако:
Si , i = 1, 2,…,K ;
Si Sj = , 1 i, j K, i j ;
i = 1, 2,…,K
Si = 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.log M) + 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.log M) + O(N) + 2.M. O(1) + (N – 1). O(N) = O(M.log M + 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.log M) + O(N) + 2.M. O(N) + (N – 1). O(1) = O(M.log M + 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.log M) + O(N) + 2.M. O(log* N)
+ (N – 1). O(1) = O(M.log M + M + N) – не зависи същестено от съотнешението на брой върхове и брой ребра.
МОБИФОНИ IOI’2001
В равнината е зададена квадратна мрежа с размери SS, S <= 1024. Всяко квадратче е покрито от клетка на мобилен оператор и в него във всеки момент се намират определен брой мобилни телефони. Броят на телефоните в едно квадратче непрекъснато се мени. В определени моменти всяка от клетките докладва на централата за настъпилите изменения, а от време навреме, в централата си задават въпроси за броя на работещите в момента телефони в някаква правоъгълна подобласт на мрежата.
МОБИФОНИ IOI’2001
Команда Описание
0 Инициализира всички клетки с 0
1 X Y A Добавя А телефона в клетката с координати (X,Y).
2 L B R T Колко са работещите телефони в правоъг. от (L,B) до (R,T)?
3 Край на работата
МОБИФОНИ IOI’2001 едномерен вариант
Команда Описание
0 Инициализира всички клетки с 0
1 X A Добавя А телефона в клетката с координата X
2 L R Пита се колко са работещите теле- фони в интервала (L,R)
3 Край на работата
Абстрактен тип Дефинираме абстрактен тип 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] Net M;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] Net M;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]
1 [1-20+1,1]=[1,1] 5 [5-20+1,5]=[5,5]
2 [2-21+1,2]=[1,2] 6 [6-21+1,6]=[5,6]
3 [3-20+1,3]=[3,3] 7 [7-20+1,7]=[7,7]
4 [4-22+1,4]=[1,4] 8 [8-23+1,8]=[1,8]
Индексно дърво
2 7 3 4 0 5 6 1
2 3 0 6
16
9 5
28
Операциите
typedef int a[1025] Net M;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 в момент
T2 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);}
}
Weight Depth Jump U
1
2
3
4
5
6
7
8