Префиксен масив
Prefix Array
В тази тема ще покажем как бързо можем да намираме сумата в произволен под-интервал на масив чрез структурата данни "префиксен масив". Ще демонстрираме как може да се справяме с двумерни масиви и да правим константна заявка за цял под-регион.
Понякога ще ни трябват по-сложни структури данни, които не просто пазят информацията в удобен за достъп начин, ами имат методи, които бързо намират специфична информация. С най-лесната от тях ще се запознаем сега, като нататък ще разгледаме редица по-сложни и по-мощни такива.
Какво е куери?
Преди да ви запознаем с проблема, който искаме да решим, ще ви научим на един термин, който ще срещате много често оттук нататък. Това е думата куери (от английски query) или преведено на Български - заявка. Куерита може да имате за най-различни неща - от това колко различни елемента вече сме срещнали (с каквито задачи се справихме в предишните теми), през по-сложни от типа на колко е сумата на числата в даден интервал от масив, какъв е минимумът или максимумът, до много сложни питания, за които е нужна редица информация и съответно много сложна структура данни.Сума в интервал
Даден ви е масив с 1 ≤ N ≤ 100,000 числа с плаваща запетая. Ели се интересува от сумата на числата между индекси idx1 и idx2. Тя ще ви зададе 1 ≤ Q ≤ 100,000 въпроса от тип "Каква е сумата на числата в масива с индекси от idx1 до idx2, включително?". От вас се иска бързо да отговаряте на такива въпроси, за различни валидни индекси.
Този проблем често възниква като подзадача в по-сложни постановки. За него има ефективни решения както ако числата са статични (не се променят, както е в горепосочения случай), така и ако те може да се променят. За сега ще разгледаме много ефективно решение, което се справя с проблема, когато числата не се променят, а в темата за индексни дървета ще видим как да се справим и ако променяме числата.Наивно решение
Очевидно наивно решение е за всеки въпрос просто да обхождаме всички числа и да ги сумираме. Това, обаче, е доста неефективно. Представете си, че всички въпроси са за интервала от 0 (или индекс, блзък до 0) до N-1 (или индекс, близък до N-1). Каква би била сложността на това решение? Тъй като имаме Q такива куерита, и всяко от тях е с относителноO(N) сложност, то цялата сложност на нашето решение би била O(N∙Q). За дадените ограничения това би отнело приблизително 100,000 * 100,000 == 10,000,000,000, или би отнело приблизително 10 секунди на мощен компютър. Това е твърде бавно.Префиксен масив
Нещото, което не използвахме при тривиалния подход, е, че числата в масива са дадени предварително и не се променят. Помага ли ни това?Да! Представете си, че имаме масив, в който имаме сумите на числата от началото на масива, до съответния индекс. Примерно ако имаме начален масив
(3, 13, 5, 5, 9, 7, 42, 1), то този със сумите би бил (3, 16, 21, 26, 35, 42, 84, 85). С малко мислене можем да видим, че ако ни зададат въпрос за [idx1, idx2], то реално отговорът е sums[idx2] - sums[idx1 - 1], където sums[] е масивът със сумите. Разбира се, трябва да се справим с частния случай ако idx1 == 0, но освен него това би работело!Добре, остана да създадем масива sums[], и сме готови. Как да направим това ефективно? Очевидно, тъй като трябва да запълним
O(N) клетки, то сложността ни ще е поне O(N). Също така, понеже ни задават Q куерита, ще имаме поне O(Q) от това. Макар и тези сложности да не можем да ги избегнем, ще се постараем нашата крайна сложност да е точно толкова, а не поне толкова - тоест да постигнем O(N + Q). Следния код прави именно това:
double sums[MAX];
// Така питаме за сумата на елементите в интервала [idx1, idx2].
double query(int idx1, int idx2) {
return sums[idx2] - (idx1 > 0 ? sums[idx1 - 1] : 0.0);
}
// Тази стъпка изпълняваме само веднъж - преди да започнем с питанията.
void init(double a[MAX], int n) {
sums[0] = a[0];
for (int i = 1; i < n; i++) {
sums[i] = sums[i - 1] + a[i];
}
}Двумерен префиксен масив
Можем да разширим горната идея и за двумерен масив. В клетката(row, col) от масива ще пазим сумата на всички елементи, които се намират в правоъгълника (0, 0) -> (row, col). За да имплементираме куери за под-правоъгълник (row1, col1) -> (row2, col2), където row1 ≤ row2 и col1 ≤ col2, ще ползваме принципа на включване и изключване в неговия най-прост вариант:- прибавяме региона
(0, 0) -> (row2, col2); - изваждаме региона
(0, 0) -> (row2, col1 - 1); - изваждаме региона
(0, 0) -> (row1 - 1, col2); - прибавяме региона
(0, 0) -> (row1 - 1, col1 - 1).
(2, 3) -> (4, 7). В таблицата по-долу сме показали по колко пъти сме взели всяка клетка, след горните четири операции:
| +1-1-1+1=0 | +1-1-1+1=0 | +1-1-1+1=0 | +1-1=0 | +1-1=0 | +1-1=0 | +1-1=0 | +1-1=0 | 0 | 0 |
| +1-1-1+1=0 | +1-1-1+1=0 | +1-1-1+1=0 | +1-1=0 | +1-1=0 | +1-1=0 | +1-1=0 | +1-1=0 | 0 | 0 |
| +1-1=0 | +1-1=0 | +1-1=0 | +1=1 | +1=1 | +1=1 | +1=1 | +1=1 | 0 | 0 |
| +1-1=0 | +1-1=0 | +1-1=0 | +1=1 | +1=1 | +1=1 | +1=1 | +1=1 | 0 | 0 |
| +1-1=0 | +1-1=0 | +1-1=0 | +1=1 | +1=1 | +1=1 | +1=1 | +1=1 | 0 | 0 |
| 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
(2, 3) -> (4, 7)), са взети по веднъж - всички останали са прибавени в сумата по нула пъти.Примерна имплементация на горното нещо:
double query(int row1, int col1, int row2, int col2) {
double ans = sums[row2][col2];
if (row1 > 0) ans -= sums[row1 - 1][col2];
if (col1 > 0) ans -= sums[row2][col1 - 1];
if (row1 > 0 && col1 > 0) ans += sums[row1 - 1][col1 - 1];
return ans;
}
void init(double a[MAX][MAX], int n, int m) {
for (int row = 0; row < n; row++) {
double rowSum = 0;
for (int col = 0; col < m; col++) {
rowSum += a[row][col];
sums[row][col] = rowSum + (row > 0 ? sums[row - 1][col] : 0);
}
}
}Задачи за тренировка
Подготвили сме ви три задачки за тренировка. Задачата Cinema от първи кръг на Националната Олимпиада (2011-та година) изисква от вас да приложите същата техника, но в двумерен вариант. Задачата ShoeShopping изисква да съчетаете префиксния масив с друг стандартен алгоритъм, който вече разгледахме. Задачата Multi пък изисква малко креативност, но иначе нищо друго, освен префикесн масив. В задачата Sum of Primes също може да бъде приложен префиксен масив след като бъдат намерени простите числа с решето на Ератостен.
За да предложите корекция, селектирайте думата или текста, който искате да бъде променен,
натиснете Enter и изпратете Вашето предложение.
натиснете Enter и изпратете Вашето предложение.
Страницата е посетена 8440 пъти.