Professional Documents
Culture Documents
Majority
Majority
#include <stdio.h>
#define CDataType char
Ползата от това “подобрение” в общия случай обаче е доста съмнителна, тъй като то
изисква допълнително: едно събиране, едно изваждане и едно сравнение на всяка стъпка от
броенето. Т.е. в най-лошия случай това решение е по-лошо от предходното и сложността му
отново е O(n2).
Няма да се задълбочаваме повече в тази посока, тъй като съществуват по-ефективни
алгоритми. Да видим какво става, ако отслабим строгите ограничения, наложени в началото,
и допуснем тип на елементите, за който е дефинирана пълна наредба. В такъв случай бихме
могли да сортираме елементите на масива и да проверим дали средният му елемент е
мажорант. Получаваме следния алгоритъм (Доказателството, че ако сортираме масива,
средният му елемент се заема от мажоранта, ако има такъв, оставяме на читателя като леко
упражнение.):
Да се върнем обаче към изходното условие, при което не можем да разчитаме нито на
наредба, нито на предварително известен малък брой възможни стойности на елементите, и
да опитаме да приложим стратегията “разделяй и владей”. Да разделим масива на две почти
равни части (в случай на нечетен брой елементи, едната част ще има един елемент повече).
Основната идея е, че ако x е мажорант на изходния масив, той ще бъде мажорант на поне
една от двете части. (Доказателството е тривиално и го оставяме на читателя.) Оттук лесно
получаваме съответен рекурсивен алгоритъм. Разделяме масива на две почти равни части и
намираме мажоранта на всеки от тях. Налице са три случая:
1) И двата подмасива нямат мажорант. Тогава и изходният масив няма да има.
2) Единият подмасив има мажорант x, а другият — няма. Проверява се, дали x е
мажорант на изходния масив.
3) И двата подмасива имат мажоранти x и y, евентуално различни. Проверяват се
както x, така и y. (Разбира се, ако x се окаже мажорант, проверката за y може да се
пропусне.)
За намирането на мажоранта на всеки от подмасивите се прилага рекурсивно същият
алгоритъм, като процесът приключва при достигане на масив с един елемент, в който случай
този елемент се явява мажорант. Процесът би могъл да приключи и по-рано (например при
два елемента) от съображения за ефективност, но няма да се спираме на разсъждения в тази
посока. Ще отбележим, че поддържането на лява и дясна граница изисква малка промяна в
параметрите и на двете функции, като началното извикване става с лява граница на
разглеждания масив 0, и дясна — size–1.
if (left == right) {
*majority = m[left];
return 1;
}
return 0;
}
Дали не бихме могли да получим още по-добри резултати? Оказва се, че лесно можем
да получим линейна сложност на алгоритъма забелязвайки, че ако масивът има мажорант са
в сила следните твърдения:
Твърдение 1: Ако елементите m[i] и m[j] са различни и ги премахнем, мажорантът на
новия масив ще съвпада с мажоранта на стария.
Твърдение 2: Ако всички елементи в масива се срещат по двойки, то ако запазим
единия представител и изхвърлим другия за всяка двойка, то мажорантът на новия масив ще
съвпада с мажоранта на стария.
Доказателството на горните твърдения е леко и го оставяме на читателя.
Нека отново опитаме да опростим условието, като поискаме: 1) масивът гарантирано
да съдържа мажорант, и 2) позволява се промяна на масива така, че след приключване
работата на програмата той почти сигурно да се различава от изходния. Нека за момент
преположим, че масивът има четен брой елементи. Да опитаме отново да приложим
римската стратегия, разделяйки масива на две равни части. Вместо да търсим мажорант във
всяка от тях обаче, този път ще сравняваме елементите по двойки: единият елемент е от
едната част, а другият — от другата. Разглеждаме една конкретна двойка (последователни
елементи). Ако двата елемента са различни, изключваме и двата от по-нататъшни
разглеждания, а ако са равни — запазваме единия. Прилагайки това за всички двойки,
получаваме нов масив с най-много n/2 елемента. За него се прилага същата стратегия, като
процесът продължава до достигане на масив с един елемент: мажорантът (сигурни сме, че
има такъв).
Проблем възниква при нечетен брой елементи, при което един от елементите няма да
може да бъде включен в двойка. Тъй като този елемент може да бъде критичен и да определя
мажоранта, не бихме искали да го изхвърлим. (Например: AA BB A). От друга страна, не
бихме искали и винаги да го пазим, защото така можем да разрушим мажоранта (Например:
AA B).
Едно възможно решение е да го запазим, давайки му по-малко тегло и да го вземем
предвид, само ако в новия масив без него не може да се определи мажорантът. Получаваме
алгоритъм с линейна сложност, тъй като на всяка стъпка броят на елементите намаляват поне
наполовина. Наистина, ако означим броя на извършваните сравнения за n-елементен масив с
T(n), в най-лошия случай получаваме следните рекурентни зависимости:
T(1) = 0
T(n) = T(n/2) + n/2
След решаване на тази зависимост получаваме, че T(n) O(n). Предложената
програмна реализация не използва допълнителен масив, а на всяка стъпка последователно
копира по един представител на елементите от двойките с еднакви елементи в началото на
същия масив. Лесно се вижда, че това не е опасно, тъй като копирането става “зад гърба ни”.
За съжаление така се разрушава оригиналният масив, което ни лишава и от възможността за
извършване на горната проста проверка дали намереният елемент е наистина мажорант.
// Stack functions
#define STACK_SIZE 100
CDataType stack[STACK_SIZE];
unsigned stIndex;
void stackInit() { stIndex = 0; }
void stackPush(CDataType elem) { stack[stIndex++] = elem; }
CDataType stackPop() { return stack[--stIndex]; }
CDataType stackTop() { return stack[stIndex-1]; }
char stackIsEmpty() { return 0 == stIndex; }
stackInit();
for (stackPush(m[0]), i = 1; i < size; i++) {
if (stackIsEmpty())
stackPush(m[i]);
else if (stackTop() == m[i])
stackPush(m[i]);
else
stackPop();
}
if (stackIsEmpty())
return 0;
A A A C C B B C C C B C C
^
?:0
A A A C C B B C C C B C C
^
A:1
A A A C C B B C C C B C C
^
A:2
A A A C C B B C C C B C C
^
A:3
A A A C C B B C C C B C C
^
A:2
A A A C C B B C C C B C C
^
A:1
A A A C C B B C C C B C C
^
?:0
A A A C C B B C C C B C C
^
B:1
A A A C C B B C C C B C C
^
?:0
A A A C C B B C C C B C C
^
C:1
A A A C C B B C C C B C C
^
C:2
A A A C C B B C C C B C C
^
C:1
A A A C C B B C C C B C C
^
C:2
A A A C C B B C C C B C C
^
C:3
if (cnt > 0) {
for (i = cnt = 0; i < size; i++)
if (m[i] == *majority)
cnt++;
return (cnt > size/2);
}
return 0;
}
Задачи:
1. Докажете, че ако сортираме даден масив, съдържащ мажорант, средният му елемент се
заема от мажоранта.
2. Докажете, че ако x е мажорант на даден масив и разделим масива на две почти равни
части, то x е мажорант на поне едната от тях.
3. Докажете, че ако масивът m[] има мажорант, то ако елементите m[i] и m[j] са различни и
ги премахнем, мажорантът на новия масив ще съвпада с мажоранта на стария.
4. Докажете, че ако масивът m[] има мажорант, то ако всички елементи в масива се срещат
по двойки, то ако запазим единия представител и изхвърлим другия за всяка двойка, то
мажорантът на новия масив ще съвпада с мажоранта на стария.
5. Да се реализира програмно алгоритъмът за запълване на област с шаблони, описан в 5.9.
Литература:
[Кнут-1976] Кнут Д., “Искусство программирования для ЭВМ” том 3, “Мир”, Москва, 1976.