Префиксный массив

В этом уроке ты
  • Освоишь технику «префиксный массив».
  • Решишь задачу с реального собеседования.
  • Научишься вычислять сумму на отрезке за O(1), чтобы уверенно пройти интервью.
Пример задачи с собеседования

Представь, что ты пришёл на собеседование. Интервьюер уже ждёт тебя и озвучивает условие задачи…

Условие

Реализуй структуру данных, которая позволяет быстро находить сумму всех элементов на отрезке [l, r] (включая границы). При этом массив не изменяется.

Другими словами, нужно реализовать:

  1. Конструктор (вызывается один раз).
  2. Метод sum (вызывается многократно, поэтому он должен быть максимально быстрым).
public class PrefixArray {
    // Конструктор
    public PrefixArray(int[] nums) {
        // Нужно реализовать
    }
    // Метод для нахождения суммы
    public int sum(int left, int right) {
        // Нужно реализовать
    }
}

Пример

Ввод: nums = [1, 4, 5, -3, 7, 2]
l = 0, r = 2
l = 0, r = 5
l = 3, r = 5

Вывод:
10
16
6
В чем сложность задачи?

Кандидат, который торопится скорее написать код, может не обратить внимание на важную формулировку в условии: быстро находить сумму всех элементов на отрезке, и напишет такое решение:

public class PrefixArray {
    private int[] prefix;
    // Конструктор
    public PrefixArray(int[] nums) {
        prefix = nums;
    }
    // Метод для нахождения суммы
    public int sum(int left, int right) {
        int total = 0;
        for (int i = left; i <= right; i++)
        {
            total = total + prefix[i];
        }
        return total;
    }
}

Метод sum каждый раз пересчитывает сумму элементов с индекса left до right и работает не оптимально по времени: за O(right - left).

Оптимальное решение

Чтобы избежать пересчёта сумм элементов, воспользуемся техникой префиксного массива:

public class PrefixArray {
    private int[] prefix;
    // Конструктор
    public PrefixArray(int[] nums) {
        prefix = new int[nums.Length + 1];
        prefix[0] = 0;
        for (int i = 0; i < nums.Length; i++)
        {
            prefix[i + 1] = prefix[i] + nums[i];
        }
    }
    // Метод для нахождения суммы
    public int sum(int left, int right) {
        return prefix[right + 1] - prefix[left];
    }
}

В оптимальном решении метод sum работает за O(1), что именно и ожидает интервьюер.

Давай разберём, как это устроено!

Префиксные массивы

Допустим, у нас есть массив: nums = [1, 4, 5, -3, 7, 0, 2, 4]. Нужно найти сумму элементов на отрезке с индекса 2 по 6.

Зная сумму первых семи элементов (prefix[7]) и первых двух элементов (prefix[2]), получаем:

sum = prefix[7] − prefix[2] = 16 − 5 = 11

Такой подход намного быстрее, чем пересчёт всех элементов вручную: 5 + (-3) + 7 + 0 + 2 = 11

Имея заранее посчитанные суммы, можно очень быстро находить сумму на отрезках, а именно за O(1).
Строим префиксный массив

Префиксный массив содержит накопленные суммы элементов исходного массива:

  1. Первый элемент равен 0.
  2. Каждый следующий — это сумма всех предыдущих элементов.

Например, для массива nums = [1, 4, 5, -3, 7, 0, 2, 4] префиксный массив будет:

prefix = [0, 1, 5, 10, 7, 14, 14, 16, 20].

Построить префиксный массив достаточно просто:

public class PrefixArray {
    private int[] prefix;
    // Конструктор
    public PrefixArray(int[] nums) {
        prefix = new int[nums.Length + 1];
        prefix[0] = 0;
        for (int i = 0; i < nums.Length; i++)
        {
            prefix[i + 1] = prefix[i] + nums[i];
        }
    }
}
Префиксный массив в действии

Чтобы найти сумму элементов массива nums с индекса L = 1 до R = 6, используем формулу:

sum = prefix[R + 1] - prefix[L]

Почему префиксный массив начинается с 0?

Дополнительный 0 спасает нас от выхода за границу массива. Даже если L = 0 и R = 7, всё будет корректно в формуле sum = prefix[R + 1] - prefix[L].

В итоге, мы получаем полную реализацию префиксного массива, которая работает оптимально:

public class PrefixArray {
    private int[] prefix;
    // Конструктор
    public PrefixArray(int[] nums) {
        prefix = new int[nums.Length + 1];
        prefix[0] = 0;
        for (int i = 0; i < nums.Length; i++)
        {
            prefix[i + 1] = prefix[i] + nums[i];
        }
    }
    // Метод для нахождения суммы
    public int sum(int left, int right) {
        return prefix[right + 1] - prefix[left];
    }
}
Что дальше

Не теряй время и переходи к решению практических задач — это позволит тебе закрепить свои знания.

Так ты сможешь быстро и уверенно справляться с трудными вопросами на собеседовании!