Общеизвестно, что компьютер был создан, чтобы производить вычисления. Процессор - это, по сути, высокопроизводительный калькулятор, но есть кое-что, что меня сильно усложняет, когда я работаю над разработкой программного обеспечения. За свою карьеру я видел, как многим программистам не хватает математических понятий, и этот недостаток не означает, что эти программисты не очень хороши в разработке программного обеспечения. На самом деле, я не вижу себя использующим математические концепции, которые я изучил в инженерном колледже, в своей повседневной работе.
С прошлого года я начал тратить время на изучение различных алгоритмов и решение задач кода, затем я понял, что мы можем больше говорить о математике и программировании, и, возможно, мы могли бы посмотреть другими глазами на проблемы, с которыми мы сталкиваемся каждый день.
Цель этой статьи - показать несколько примеров «математического способа» решения задач и побудить больше людей говорить об этом.
Алгоритм Флойда "черепаха и заяц"
Иногда нам нужно проверить, есть ли у связного списка цикл. Хороший способ проверки - повторение связанного списка и использование HashMap для хранения узлов, когда вы достигнете узла, который уже находится в HashMap, вы знаете, что в этом связанном списке есть цикл, и если вы дойдете до конца Связанный список означает, что цикла нет.
Это будет примерно так:
function hasCycle(head) { if (!head || !head.next) { return false } let pointer = head const hashmap = {} while (pointer) { if (hashmap[pointer]) { // cycle point return true } hashmap[pointer] = pointer pointer = pointer.next } return false }
Проблема с этим алгоритмом в том, что вам нужна дополнительная переменная с размером связного списка, поэтому пространственная сложность будет такой же, как временная сложность: O (n). Возможно, есть лучший способ найти цикл в связанном списке.
Алгоритм Флойда - это способ найти циклы и / или повторяющиеся записи в массиве (здесь для получения дополнительной информации) с очень хорошей производительностью. Алгоритм состоит в использовании двух переменных для перебора массива, где одна переменная будет черепахой, а другая - зайцем. Как всем известно, в реальной жизни Заяц быстрее черепахи, поэтому идея состоит в том, что Заяц повторяет в два раза больше скорости Черепахи, и они встречаются друг с другом в точке цикла.
function hasCycle(head) {
if (!head || !head.next) {
return false
}
let tortoise = head
let hare = head.next
while (hare && hare.next) {
if (tortoise === hare) { // cycle point
return true
}
tortoise = tortoise.next
hare = hare.next.next // 2 times tortoise
}
return false
}
Алгоритм Флойда имеет временную сложность O (n), как и предыдущий код, но пространственная сложность составляет O (1). Математическое объяснение этого алгоритма состоит в том, что у нас есть две функции, которые будут иметь одинаковое значение, но в разные моменты. Взгляните ниже:
Более короткое расстояние, необходимое для прохождения до точки встречи, равно x + y, в противном случае большее расстояние будет x + y + z + y, что должно привести к тому же положению уравнение более короткого расстояния. Если вы помните математические концепции средней школы, мы знаем, что способ найти пересечение двух функций - это сопоставление друг с другом, так как Заяц будет иметь в два раза большую скорость, чем Черепаха, мы можем установить 2 (x + y ) = x + y + z + y, потому что мы знаем, что они встретятся друг с другом в точке пересечения, поэтому положение должно быть таким же.
2(x + y) = x + y + z + y 2x + 2y = x + 2y + z x = z
Конечно, это поверхностное объяснение. Вы можете прочитать подробное и очень хорошее объяснение здесь.
Испытание кода HackerRank «Кенгуру»
Задача кенгуру заключается в том, чтобы найти двух кенгуру, которые стартовали в разных положениях (x1 и x2) и с разной скоростью (v1 и v2) когда-нибудь встретятся. Кто угодно мог решить проблему так:
if v1 == v2 and x1 != x2: # if same speed, they will never meet return('NO') if x1 < x2 and v1 < v2: return('NO') if x1 > x2 and v1 > v2: return('NO') if x1 < x2 and v1 > v2: while x1 != x2: x1 = x1 + v1 x2 = x2 + v2 if x1 == x2: return ('YES') if x1 > x2: return ('NO') break if x1 > x2 and v1 < v2: while x1 != x2: x1 = x1 + v1 x2 = x2 + v2 if x1 == x2: return ('YES') if x1 < x2: return ('NO') break
Это верный ответ и выполняет свою роль. По моему опыту, я попросил нескольких кандидатов, с которыми я проводил собеседование, попытаться решить эту проблему на доске, и 100% из них пытались решить, используя описанный выше способ.
Что ж, используя идею, о которой мы говорили в алгоритме черепахи и зайца Флойда, мы можем думать, что каждый кенгуру - это уравнение положения, где у нас есть положение и скорость, как в физике. Итак, думая так, мы можем написать уравнения и сопоставить друг друга, чтобы увидеть, пересекутся ли они в какой-то момент.
Например, если Kangoroo 1 имеет v1 = 1, x1 = 4, а Kangoroo 2 имеет v1 = 2, x1 = 2, мы можем написать их функции, например:
- Кангору 1 = 2x + 4
- Кангору 2 = 4x + 2
4x + 2 = 2x + 4 4x - 2x = 4 - 2 2x = 2 x = 1
И вы можете использовать значение x = 1, чтобы найти значение y
Using the Kangoroo 1 equation: y = 2x + 4 y = 2 * 1 + 4 y = 6 Using the Kangoroo 2 equation y = 4x + 2 y = 4 * 1 + 2 y = 6
И когда мы рисуем на графике, мы можем видеть точку пересечения в (1,6):
Итак, мы можем использовать эту идею, чтобы узнать, встретятся ли кенгуру в какой-то момент. Вместо реализации циклов, которые увеличивают временную сложность, мы можем легко использовать математику для решения проблемы и сохранить постоянную временную сложность O (1):
- Общее уравнение Кангуру будет выглядеть следующим образом: ax + y
- x1 и x2 равны y
- v1 и v2 - это a
- Уравнение Кангуру 1: x1 * x + v1
- Уравнение Кангуру 2: x2 * x + v2
x1 * x + v1 = x2 * x + v2 x ( x1 - x2 ) = v2 - v1 x = ( v2 - v1 ) / ( x1 - x2 )
По сути, нам нужно напрямую записать последнее условие, используя значение x, и проверить, действительно ли деление (это означает, что точка существует и является допустимой точкой), другими словами, результат должно быть больше 0 и % 1 должно быть 0:
const res = (x1 - x2)/(v2 - v1) if (res >= 0 && (res)%1 === 0) return 'YES'; return 'NO';
Накопительный массив
Если вы недавно смотрели телевизор, вы, вероятно, уже видели кумулятивный график случаев заболевания коронавирусом, подобный приведенному ниже:
Кумулятивная функция распределения - обычное дело для профессионалов статистики. Идея состоит в том, что на каждом «шаге» вы добавляете общую сумму к совокупному графику, и график обычно имеет тенденцию расти.
Мы можем думать, что каждая запись в массиве - это «шаг». Итак, в этом случае значение записи будет накапливаться каждый раз, когда мы повторяем массив, и мы можем преобразовать массив в совокупный массив. Посмотрите на пример ниже:
const A = [3,8,1,3,2,1,8,9,0] for (let i = 1 ; i < A.length ; i++) { A[i] += A[i - 1] } console.log(A) // [3, 11, 14, 17, 19, 20, 28, 37, 37]
Алгоритм Кадане использует этот принцип для нахождения максимальной суммы подмассива. По сути, идея состоит в том, чтобы накопить значение записей в переменной и проверить, является ли это значение самым большим.
function maximumSubArray(A) { let aux = 0 let sum = 0 for (let i = 0; i < A.length; i++) { aux = Math.max(0, aux + A[i]) // cumulative sum sum = Math.max(sum, aux) } return sum }
Когда вам нужно найти максимальную сумму двух неперекрывающихся подмассивов, вы можете использовать кумулятивную функцию и просто вычесть индексы, чтобы найти сумму в определенных диапазонах.
Например, если у вас есть массив A = [0,6,5,2,2,5,1,9,4], и следующие размеры для подмассивов L = 1 и M = 2. Используя кумулятивную форму A, легко найти maxSum. Суммарное значение A будет A = [0,6,11,13,15,20,21,30,34].
Следующим шагом будет просто вычесть индексы, используя значения L и M. Например, если мы с i, мы можем просто получить A [i] - A [i + L], чтобы найти значение, соответствующее подмассиву с размером L, начиная с точки i. Если i = 5, чем i + L = 7, A [5] = 20 и A [7] = 30 , значение подмассива с размером L, начиная с точки i = 5, составляет 30–20, то есть 10. Таким образом, нам не нужно повторять итерацию более одного раза, чтобы суммировать значения и проверить, какое из них самое большое, мы можем просто использовать индексы для «сдвига» и вычитания из нашей текущей точки в основном цикле.
class Solution { public int maxSumTwoNoOverlap(int[] A, int L, int M) { // this loop will transform A into a cumulative Array for (int i = 1; i < A.length ; i++) { A[i] += A[i - 1]; } int maxSum = A[L + M - 1]; int maxLfirst = A[L - 1]; int maxMfirst = A[M - 1]; for (int i = L + M; i < A.length ; i++) { // here you can simply subtract the indexes values // A[i - M] is the subarray M value // A[i - M - L] is the subarray L value maxLfirst = Math.max(maxLfirst, A[i - M] - A[i - M - L]); // we have the same math that above, but if the M is first maxMfirst = Math.max(maxMfirst, A[i - L] - A[i - L - M]); // in the end of the iteration, we check which one is the biggest maxSum = Math.max(maxSum, Math.max(maxLfirst + A[i] - A[i - M], maxMfirst + A[i] - A[i - L])); } return maxSum; } }
Очевидно, мы могли бы использовать рекурсивный подход и создать большой кеш для поиска максимальной суммы двух неперекрывающихся подмассивов, и, вероятно, мы найдем хороший ответ, но иногда с помощью нескольких математических методов мы иметь возможность решить проблему чистым способом и с гораздо большей производительностью.
Заключение
Цель этой статьи не в том, чтобы сказать, что мы всегда должны смотреть «математически» или что «математическое решение» будет лучше, чем обычный подход к программированию. Не поймите меня неправильно, моя цель - поговорить об этом и побудить других попробовать использовать математические концепции для решения повседневных задач. Может быть, мы поймем, что можно использовать математику для решения наших повседневных задач в гораздо большей степени, чем мы. считать.
Вот и все, ребята! Если у вас есть больше плюсов и минусов или что-то, о чем я здесь не упоминал, не стесняйтесь комментировать ниже!
Спасибо за чтение!