Най-голям общ делител и най-малко общо кратно
Greatest Common Divisor and Least Common Multiple
Какво е най-голям общ делител?
Какво е най-малко общо кратно?
Разширен алгоритъм на Евклид и приложения
Какво е най-малко общо кратно?
Разширен алгоритъм на Евклид и приложения
Математиката е неизменна част от информатиката независимо дали някои го отричат или не! Следователно логично е да се решават математически задачи в програмирането, което се случва много често по състезания. Най-много такива задачи могат да се срещнат в TopCoder и на руски състезания. Математическите задачи често включват намиране на НОД и НОК (за опростяване на дроби или в задачи свързани с теория на числата) – понякога това директно ни дава отговора, а друг път е само малка, но важна част от голяма заплетена задача.
Най-голям общ делител
? | Ще отбелязваме най-големия общ делител на A и B като НОД(A, B) или GCD(A, B) . |
Дадени са две числа A и B. Да се намери най-големият им общ делител.
Наивният метод е да пробваме всеки един делител на едното число дали дели и другото. Този алгоритъм ще има сложност O(min(A, B))
. int gcdNaive(int A, int B) {
for (int i = min(A, B); i >= 1; i--) {
if (A % i == 0 && B % i == 0) {
return i;
}
}
}
O(sqrt(min(A, B)))
.Алгоритъм на Евклид (с изваждане)
Алгоритъмът на Евклид за пресмятане на НОД произлиза от теоремата на Безу: ако имаме две неотрицателни цели числа A и B, които имат НОД D, то съществуват цели числа X и Y за които е изпълнено уравнението:A * X + B * Y = D
. Следователно НОД на две числа не се променя ако заместим по-голямото с разликата между него и по-малкото. Ако повторим тази процедура докато едното число не стане нула, то в този момент другото число е равно на НОД на двете първоначални. Алтернативно можем да спрем още на стъпката в която двете числа са равни - тогава те са равни и на НОД-а си.Например за числата 84 и 18 имаме: НОД(84, 18) = НОД(84-18, 18) = НОД(66, 18) = НОД(48, 18) = НОД(30, 18) = НОД(12, 18) = НОД(6, 12) = НОД(6, 6) = 6. Този алгоритъм ще има сложност
O(max(A, B))
, която се достига при НОД(A, 1)
или НОД(1, B)
.
int gcdEuclidSubtraction(int A, int B) {
while (A != B) {
if (A < B) {
swap(A, B);
}
A -= B;
}
return A;
}
Алгоритъм на Евклид (с остатъци)
Предишният алгоритъм може да бъде оптимизиран още, тъй като при голяма разлика между двете числа ще трябва да се направят много изваждания. Можем вместо да заместваме по-голямото число с разликата, да го заместваме с остатъка при делението на по-голямото на по-малкото. При тази оптимизация е доказано, че никога не се правят повече от 5 * (броя цифри на по-малкото число) стъпки. Този алгоритъм ще има сложностO(log10(max(A, B)))
.
int gcdEuclidRemainderIter(int A, int B) {
while (B != 0) {
A %= B;
swap(A, B);
}
return A;
}
int gcdEuclidRemainder(int A, int B) {
return B == 0 ? A : gcdEuclidRemainder(B, A % B);
}
НОД на повече от две числа
НОД на повече от две числа можем да намерим като намерим НОД първо на първите две, после на тях и на следващото и т.н. Например за три числа имаме:GCD({A, B, C}) = GCD(GCD(A, B), C);
Най-малко общо кратно
? | Ще отбелязваме най-малкото общо кратно на A и B НОК(A, B) или LCM(A, B) . |
Дадени са две числа A и B. Да се намери най-малкото им общо кратно.
Наивният алгоритъм е с цикъл по отговора, което би значело да генерираме всички числа до A * B
и да проверим дали A и B се делят едновременно на някое от тях. Този алгоритъм ще ни даде сложност O(A∙B)
.
long long lcmNaive(int A, int B) {
for (long long i = max(A, B); i <= (long long)A * B; i++) {
if (i % A == 0 && i % B == 0) {
return i;
}
}
}
Ефективен алгоритъм
Ако A и B са взаимно прости (т.е. имат НОД = 1), тоLCM(A, B) = A * B
. Иначе LCM(A, B) < A * B
или по-точно LCM(A, B) = A * B / GCD(A, B)
. Допълнително можем да попроменим израза по следния начин за да намалим размера на междинните стойности: LCM(A, B) = A * (B / GCD(A, B))
. Този алгоритъм ще ни даде сложност O(log10(max(A, B)))
.
long long LCM(int A, int B) {
return (long long)A * (B / GCD(A, B));
}
НОК на повече от две числа
Както и за НОД, НОК на повече от две числа можем да намерим като намерим НОК първо на първите две, после на тях и на следващото и т.н. Например за три числа имаме:LCM({A, B, C}) = LCM(LCM(A, B), C);
Разширен алгоритъм на Евклид
Както казахме в началото, алгоритъмът на Евклид за пресмятане на НОД произлиза от теоремата на Безу: ако имаме две неотрицателни цели числа A и B, които имат НОД D, то съществуват цели числа X и Y за които е изпълнено уравнението:A * X + B * Y = D
. Понякога ни трябва не просто да намерим D, ами и самите коефициенти X и Y. За целта ползваме разширения алгоритъм на Евклид.Имплементация
! | Забележете, че резултатите от алгоритъма могат да бъдат отрицателни! |
O(log(max(A, B)))
и, реално, не се различава много - единствено на всяка стъпка пазим малко повече допълнителна информация. Макар и да не ползваме деление с остатък, реално изважданията, които правим, са еквивалентни на това.
void extendedGCD(int A, int B) {
long long currR = A, nextR = B;
long long currS = 1, nextS = 0;
long long currT = 0, nextT = 1;
while (nextR) {
long long quotient = currR / nextR;
long long prevR = currR; currR = nextR; nextR = prevR - quotient * nextR;
long long prevS = currS; currS = nextS; nextS = prevS - quotient * nextS;
long long prevT = currT; currT = nextT; nextT = prevT - quotient * nextT;
}
fprintf(stderr, "Greatest Common Divisor: %lld\n", currR);
fprintf(stderr, "Bézout coefficients: %lld %lld\n", currS, currT);
}
Multiplicative Inverse
Едно от най-честите приложения на разширения алгоритъм на Евклид е за да "делим" по модул. В повечето задачи, модулът M е просто число, което опростява делението, тъй като можем да ползваме факта, че AM-1 = 1 (по модул M), от където следва, че AM-2 = 1/A (по модул M). Така, за да разделим някакво число B на A по модул M просто умножаваме B * AM-2, което е еквивалентно наB * 1 / A = B / A
. Това, за съжаление, е вярно само когато модулът M е просто число - когато не е, трябва да прибегнем до разширения алгоритъм на Евклид.В случай, че модулът M не е просто число, деление (наличие на multiplicative inverse) е възможно само за числа, които са взаимно-прости с модула. Това следва отново от теоремата на Безу: ако искаме A да има обратен елемент (multiplicative inverse), то трябва да съществува такова X, че
A * X = 1
(по модул M). От теоремата имаме, че съществуват A * X + M * Y = GCD(A, M)
. Когато A и M са взаимно-прости, получаваме A * X + M * Y = 1 (mod M)
, което пък, тъй като е по модул M, можем да опростим до A * X = 1 (mod M)
.Ще ползваме леко опростена версия на функцията
extendedGCD()
, която показахме по-горе:int multiplicativeInverse(int A, int M) {
long long currX = 0, nextX = 1;
long long currR = M, nextR = A;
while (nextR) {
long long quotient = currR / nextR;
long long prevX = currX; currX = nextX; nextX = prevX - quotient * currX;
long long prevR = currR; currR = nextR; nextR = prevR - quotient * currR;
}
if (currR > 1) {
fprintf(stderr, "Given number doesn't have multiplicative inverse module the given M.\n");
return 0;
}
return (currX + M) % M;
}
Пример
Нека имаме модулM = 1,000,000
. Можем да намерим "обратен" елемент само на числа, които не се делят на 2 и на 5 (тъй като ако имат някое от тях във факторизацията си, тоест се делят на тях, те не биха били взаимно-прости с 1,000,000).Нека, например, искаме да разделим 1337 на 13 (по модул 1,000,000). Тоест резултатът
1337 / 13 = X
трябва да е такъв, че X * 13 = 1337
(по модул 1,000,000). За целта трябва да намерим "обратното" число на 13 по този модул (така наречения multiplicative inverse), след което да умножим 1337 по него. Използвайки разширения алгоритъм на Евклид за 13 и 1,000,000 получаваме, че обратният елемент на 13 е 923077. Така намираме, че X = 1337 / 13 = 1337 * 923077 = 153949 (mod 1000000). Наистина, 153949 * 13 = 1337 (по модул 1,000,000).Задачи за тренировка
- Levko and Permutation (Codeforces)
- Rational Numbers and Repeating Fractions (UVa)
- Simple Division (UVa)
- LCM Cardinality (UVa)
Референции
- Greatest Common Divisor (en.wikipedia.org)
- Least Common Multiple (en.wikipedia.org)
- Алгоритъм на Евклид (en.wikipedia.org)
За да предложите корекция, селектирайте думата или текста, който искате да бъде променен,
натиснете Enter и изпратете Вашето предложение.
натиснете Enter и изпратете Вашето предложение.
Страницата е посетена 25167 пъти.