Подход «разделяй и властвуй» (O(nlogn))

Что такое проблема с максимальным подмассивом?

Постановка задачи проста. Обычно у вас есть массив, заполненный целыми числами (положительные и отрицательные числа), и вам необходимо предоставить самый большой непрерывный (по порядку) подмассив, который существует в этом массиве.

Подмассив и подход

Ну, во-первых, это помогает понять, что такое подмассив, поэтому давайте определим это, прежде чем продолжить.

Подмассив — это массив, который может быть частью исходного массива или самим массивом целиком.

Например, этот массив [1, 2, 3] может иметь следующие подмассивы: [1], [2], [3], [1,2], [1,3], [2,3] или [1 ,2,3].

Эту проблему можно решить различными способами, наиболее распространенным способом является использование алгоритма Кадане, который приводит к временной сложности O (n). Однако мы не будем обсуждать этот метод. Я думаю, что метод «разделяй и властвуй» дает уникальную перспективу, по крайней мере, для меня, и я поделюсь своими мыслями о нем и о том, как он работает.

Давайте используем этот массив ниже для всей статьи.

Что такое метод «разделяй и властвуй»?

Разделяй и властвуй – это метод решения проблем, состоящий из трех частей: Разделяй, Властвуй и Объединяй.

На шаге «Разделить» вы берете большую проблему и пытаетесь равномерно разделить ее на подзадачи.

Затем на этапе «Завоевание» вы берете эти более мелкие подзадачи и решаете их.

Наконец, вы берете результаты из небольших подзадач и объединяете их обратно в исходную часть, чтобы решить большую проблему.

Вы продолжаете делать эти шаги, пока, наконец, не решите исходную проблему.

Назад к проблеме

Теперь мысленно возвращаемся к проблеме. Он просит нас предоставить самый большой непрерывный подмассив. Что это значит? Это означает, что для массива какой самый большой подмассив встречается в последовательности.

Например, arr = [13, -3, -25, 20, -3, -16, -23, 18, 20, -7, 12, -5, -22, 15, -4, 7]
[13, -3, -25] является непрерывным подмассивом и
[20, -3, -16, -23] является непрерывным подмассивом

но [13, -25, -3] не потому, что это не произошло последовательно.

Если все значения в массиве положительны, какой будет самый большой подмассив? Это будет весь массив.

Почему?

представьте, что все массивы, которые мы используем, были положительными…

[13, 3, 25, 20 …] наибольшая сумма со всеми значениями вместе

Однако это будет не так просто. Массив может иметь и отрицательные значения.

Собираем все вместе

Теперь, когда мы обсудили постановку задачи и подход, разделяй и властвуй. Давайте сложим все вместе.

Если мы возьмем наш массив, разделим его на более мелкие подмассивы и решим их, мы получим меньшие решения. Что это значит? Если мы разделим массив на меньшие массивы и представим, что меньшие массивы были массивом для нашего вопроса, мы теперь решили меньшую проблему.

Как только мы решим эту меньшую подзадачу, мы соберем все вместе и решим большую проблему.

Чтобы разделить массив на более мелкие части, мы должны рекурсивно разделить массив равномерно по длине массива. У нас должна быть левая сторона и правая сторона. Почему? Потому что, когда мы разделим его поровну, у нас будет две части, левая и правая стороны.

Мы будем продолжать рекурсивно разбивать эти половины, пока не встретим наш базовый случай.

Каким должен быть наш базовый случай? А что, если бы в массиве был только один элемент, каков наш максимум? Сам массив.

Поэтому мы продолжаем разбиение, пока не встретим наш базовый случай и не вернем это решение для максимального подмассива для этой подзадачи. Это произойдет для левой и правой сторон, потому что мы пытаемся разбить эту большую проблему и постепенно наращивать ее, чтобы получить наше окончательное решение.

Мы знаем, что максимальное значение слева в нашем случае с использованием нашего массива подзадач ([13, -3]) вернет 13 слева и -3 справа для первых базовых случаев.

arr = [13, -3] (sub problem array)
left = 13
right = -3

Что это значит и как это помогает нам найти максимальный подмассив?

Потому что мы знаем, что нашли решение слева и нашли решение справа. Это половинки, нам еще предстоит рассмотреть решение, которое может возникнуть во всем массиве.

Мы нашли ответ на нашу подзадачу для левой и для правой части массива подзадач. Однако может быть ответ, находящийся внутри самого массива подзадач. Так что нам нужно проверить и это. Наконец, когда у нас есть левое и правое и сам массив (пересечение), мы возвращаем их максимальное значение. Это помогает нам найти максимум, потому что мы знаем максимум слева, максимум справа, а теперь также максимум всего массива подзадач. Один из них должен быть общим максимумом, и это наш ответ.

arr = [13, -3] (sub problem array)
left = 13 (MAX)
right = -3 (MAX)

Now consider the full subarray
[13, -3] if we add these would this become the largest subarry? No, but we still
need to consider it.
crossing = 10

Return max(left, right, crossing)

Возвращаемое значение станет новым левым или правым для наших рекурсивных вызовов, а затем мы снова проверим, есть ли лучшее решение в самом полном массиве подзадач, и это будет продолжаться до тех пор, пока мы не вернемся к нашему исходному рекурсивному вызову. Почему мы делаем это и каждый раз возвращаем максимум? Поскольку нам нужен самый большой непрерывный подмассив, нам нужно сравнить прежний максимум с новым максимумом и получить самый большой подмассив.

Ниже приведен код, написанный на JAVA.

public class MaximumSubArray {

 public static void main(String[] args) {
  int [] a = new int[]{
    13,-3,-25,20,-3,-16,-23,18,20,-7,12,-5,-22,15,-4,7
  };
  Node res = maxSum(a, 0, 15);
  System.out.println("[begin: " + res.low  + " end: " + (res.high) + "] maxSum: " + res.sum);
 }
 
 public static class Node {
  int low;
  int high;
  int sum;

  public Node(int low, int high, int sum){
   this.low = low;
   this.high = high;
   this.sum = sum;
  }
 }

 private static Node maxSum(int[] arr, int low, int high) {
  if(high == low) {
   return new Node(low, high, arr[low]);
  }
  else {
   int mid = low + ((high-low)/2); 
   Node left = maxSum(arr, low, mid);
   Node right = maxSum(arr, mid+1, high);
   Node cross = cross(arr, low, mid, high);
   if(left.sum >= right.sum && left.sum>=cross.sum) {
    return left;
   } else if(right.sum >= left.sum && right.sum>=cross.sum) {
    return right;
   } else {
    return cross;
   }
  }
 }

 private static Node cross(int[] arr, int low, int mid, int high) {
  int leftSum = Integer.MIN_VALUE;
  int rightSum = Integer.MIN_VALUE;
  int sum = 0;
  int maxLeft = 0;
  int maxRight = 0;

  for(int i = mid; i>=low; i--) {
   sum = sum + arr[i];
   if(sum > leftSum) {
    leftSum = sum;
    maxLeft = i;
   }
  }
  
  sum = 0;
  for(int j = mid+1; j<=high; j++) {
   sum = sum + arr[j];
   if(sum > rightSum) {
    rightSum = sum;
    maxRight = j;
   }
  }
  
  return new Node(maxLeft, maxRight, leftSum+rightSum);
 }
}

В пересекающемся разделе кода все, что мы делаем, — это идем от левой стороны к началу и от правой стороны к концу нашего текущего массива подзадач. Мы отслеживаем самые большие значения, которые мы получаем с обеих сторон, а затем, поскольку они самые большие с левой и правой стороны, если мы их сложим, они должны быть самыми большими. Затем он возвращается для сравнения только с максимальным значением слева и только с максимальным значением справа (это уже решено, потому что метод пересечения вызывается после нахождения этих других максимальных значений) с этим максимальным значением (пересечение ) мы получаем из самого исходного массива подзадач.

Мы делаем это постоянно, пока не вернемся к исходному рекурсивному вызову, не выполним окончательное сравнение и не вернем максимальный подмассив.

Ссылки:
Кормен, Т. Х., Лейзерсон, К. Э., Ривест, Р. Л., и Штейн, К. (2009). Введение в алгоритмы. Мит Пресс.