Швидке піднесення до степеня

4 хв. читання
05 листопада 2019
Швидке піднесення до степеня

Розв'язуючи одну із задач на InterviewBit (шкода вже не пам'ятаю, яку саме), я побачив магічний (і мені невідомий) метод піднесення до степеня, який містив пропорційну логарифмічній кількість арифметичних операцій. Якщо відкинути магію, то метод дуже елегантний і зрозумілий.

Одразу код. Якщо все зрозуміло, то далі можна і не читати:

long pow(long val, long exp) {
    long res = 1;
    while(exp != 0) {
        if((exp & 1) == 1) res *= val;
        val *= val;
        exp >>= 1;
    }
    return res;
}

Конкретизуємо задачу: піднести x до степеня n, де x > 0 , n >= 0 і обидва цілі числа.

Якби мене запитали піднести щось до степеня, то я використав би найпростіший алгоритм, який випливає із визначення, і виконується за лінійний час.

Тобто:

x^n = x*x*x*....*x (n разів)

Прискіпливо вдивіться у формулу і спробуйте подумати, як можна зменшити кількість операцій множення. Хвилинка паузи. Можна побачити, що x*x це забагато разів повторювана операція. Якби ми використали додаткове число, скажімо y=x*x, то x^n = y*y*y*....*y*x або x^n = y*y*y*....*y*y в залежності від парності n. Припустимо, що ми вже знаємо y, тоді для вирахування x^n нам необхідно виконати вдвічі менше операцій множення. Точно за таким самим принципом ми могли б замінити y і ще зменшити кількість арфиметичних операцій, і часові затрати відповідно.

Тепер перейдомо питання реалізації. Для цього використаємо одну із властивістей степеня:

x^(a+b) = (x^a) * (x^b).

Отже для n=a+b, ми шукаємо випадок, коли a якимось чином пов'язана із b. Нас цікавить випадок, коли a=2*b, тоді x^a = x^b * x^b.

Використаємо те, що будь-яке число можна записати як суму степенів двійки, оскільки це фактично є записом числа у двійковій системі числення. Візьмемо число 177 за приклад:

117 = 64+32+16+4+1 = 2^6+2^5+2^4+2^2+2^0. 

Введемо у суму (із множником 0) степені двійки, яких немає в розкладанні числа:

117 = 1*2^6+1*2^5+1*2^4+0*2^3+1*2^2+0*2^1+1*2^0 = 1*e6+1*e5+1*e4+0*e3+1*e2+0*e1+1*e0,
e0=1 (2^0)
e1=2*e0 (2^1)
e2=2*e1 (2^2)
e3=2*e2 (2^3)
...
n[]=[1110101]
n[0]=1
n[1]=0
n[2]=1
...

Тоді:

x^117 = x^(n[6]*e6)*x^(n[5]*e5)*x^(n[4]*e4)*x^(n[3]*e3)*x^(n[2]*e2)*x^(n[1]*e1)*x^(n[0]*e0)

Узагальнимо:

x^n = mult(x^(n[i]*ei)) 
...
i=0..log(n), ei=2^i
x^e(i+1) = x^ei*x^ei

Запишемо словами: для того, щоб отримати x^n достатньо перемножити між собою усі x^(2^i), для яких значення біту під i рівне 1 ( i — номер розряду числа у двійковому записі числа n).

Для обрахунку наступного x^(2^i) нам потрібно тримати в пам'яті лише попереднє значення: x^(2^i) = x^(2^(i-1)) * x^(2^(i-1)). Такий підхід суттєво зменшує загальну кількість необхідних оперцій. Для прикладу: x^117 ми можемо отримати через 11 операцій множення, а не 117, як із використанням звичайного алгоритму.

І ще раз глянемо на код:

long pow(long x, long n) {
    long res = 1; //результат множення
    while(n != 0) {
        if((n & 1) == 1) res *= val; //x йде в результат
        x *= x; //x = x*x - для наступного розряду
        n >>= 1; //ділення на 2 методом побітового зсуву
    }
    return res;
}

П.С.

Звичайно, ми могли б використати будь-який інший формат запису замість двійкового, але зручність двійкового в тому, що числа, у більшості мов програмування, мають безкоштовну двійкову форму запису і зручні методи роботи із нею.

П.П.С.

Для великих чисал чималу кількість часу займає саме операція множення, тому алгоритм працюватиме суттєво довше на великих числах.

Codeguida 8.3K
Приєднався: 3 місяці тому

Hosting Ukraine

Помітили помилку? Повідомте автору, для цього достатньо виділити текст з помилкою та натиснути Ctrl+Enter
Коментарі (0)

    Ще немає коментарів

Щоб залишити коментар необхідно авторизуватися.

Вхід / Реєстрація