Попалась мне одна интересная задача ,суть которой - найти наибольший отрезок в массиве единиц и нулей ,где суммы их кол-ва равны друг другу. Например ,имеем массив [0, 1, 0, 1, 0]. Длина наибольшего подмассива ,где кол-во нулей равно кол-ву единиц = 4. Под этот критерий подходит подмассив [{0, 1, 0, 1}, 0] ,а так же [0, {1, 0, 1, 0}]. В обоих случаях сумма всех нулей = 2 ,а сумма всех единиц равна тоже 2. Длина такой последовательности = 4 ,и это должно быть ответом.

Сперва можно немного поработать над данными ,чтобы в будущем можно было проще вычислять такие отрезки ,где суммы 1 и 0 равны друг другу. Например ,для отрезка [0, 1, 0, 1] общая сумма значений равна 2 (0 + 1 + 0 + 1). Можно сделать вывод ,что сумма 1 и 0 равна в этом отрезке ,если сумма значений равна половине длины подмассива. Для нашего случая получается ,что (0 + 1 + 0 + 1) = 2 = 4/2 (половина длины подмассива). Но гораздо проще было бы проводить расчеты ,если бы необходимая сумма значений была бы константой ,например 0. В таком случае мы можем заменить все нули на -1 ,и таким образом получим [-1, 1, -1, 1] ,где сумма таких элементов будет равна 0.

На JavaScript это можно сделать следующим образом: arr.map(x => x || -1).

Далее нам потребуется цикл по длине arr:

for (let i = 0; i < arr.length; i++) { ... }

На самом деле мы можем найти наибольшую последовательность за 1 такой цикл. Нам потребуется создать несколько вспомогательных переменных за пределами цикла:

let currentSum - будет хранить текущее значение суммы элементов с 0 по i.

let maxLength - будет хранить промежуточное значение наибольшей длины подмассива ,которую удалось найти на момент i.

const hashMap - временная хешмапа для хранения пар { [currentSum]: [i] }.

currentSum мы на каждой итерации обновляем ,суммируя с элементом текущей итерации:

currentSum += arr[i];

Далее суть решения сводится к тому ,чтобы на каждой итерации цикла проверять наличие currentSum в hashMap ,и если оно там есть ,то вычислить разницу между текущим i и hashMap[currentSum] ,который вернет индекс той позиции ,на которой первый раз было зафиксировано текущее значение currentSum. Иначе записать текущий currentSum в hashMap:

if (currentSum in hashMap) {
  maxLength = Math.max(maxLength, i — hashMap[currentSum]);
} else {
  hashMap[currentSum] = i;
}

Здесь мы используем метод Math.max для определения наибольшего из старого значения maxLength и вычисленного нового ,тк не факт ,что новое будет больше.

i - hashMap[currentSum] обозначает длину отрезка ,при котором сумма всех его значений будет равна 0 ,ведь для текущего i currentSum такой же ,как и для индекса извлеченного из hashMap[currentSum] ,а это индекс элемента ,на котором мы впервые получили эту сумму.

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

Не забываем также проверить ,не получилась ли текущая сумма равна 0 ,тогда стоит ее также провалидировать и записать в maxLength при необходимости:

if (currentSum === 0) {
  maxLength = Math.max(maxLength, i + 1);
}

В конце останется только вывести maxLength. Итоговое решение выглядит так:

function solve(inputArr) {
  const arr = inputArr.map(x => x || -1);
  const hashMap = {};
  
  let currentSum = 0;
  let maxLength = 0;
  
  for (let i = 0; i < arr.length; i++) {
    currentSum += arr[i];
    
    if (currentSum === 0) {
      maxLength = Math.max(maxLength, i + 1);
    }
    
    if (currentSum in hashMap) {
      maxLength = Math.max(maxLength, i - hashMap[currentSum]);
    } else {
      hashMap[currentSum] = i;
    }
  }
  
  return maxLength;
}

Сложность по памяти получается O(n) за счет того ,что мы сделали копию массива ,однако ,если ее не делать ,то будет O(k) ,где k - кол-во пар в hashMap. В худшем случае она будет равна n ,если все элементы последовательности будут одинаковы.

Временная сложность будет равна O(n) ,тк мы целиком проходим циклом по массиву. На самом деле в функции 2 цикла ,тк map тоже является циклом ,но общая сложность сводится к O(n) ,тк константа 2 просто отбросится.

Комментарии (20)


  1. vadimr
    10.01.2025 05:22

    HashMap занимает гораздо больше памяти, чем количество хранящихся в нём элементов.


    1. webnikler Автор
      10.01.2025 05:22

      Согласен ,но обычно затраты памяти берут условно при оценке алгоритма ,в данном случае оценка сводится к кол-ву элементов в хешмапе.


      1. vadimr
        10.01.2025 05:22

        Затраты памяти берут совсем не условно, а в виде конкретной оценки. Для хеш-таблицы затраты памяти будут O(N), где N – максимальное возможное количество ключей, которое он способен хранить (а не то, которое там фактически хранится). Поэтому меньше размера массива в данном случае никак не получится.


        1. webnikler Автор
          10.01.2025 05:22

          Я подумал ,что Вы имели ввиду какие-то дополнительные расходы на хранение значений ,помимо самих значений в HashMap. А так да ,Вы правы ,оценка происходит из худшего случая ,при котором N - кол-во элементов в массиве ,а не фактическое кол-во пар в хешмапе.


  1. AndronNSK
    10.01.2025 05:22

    >>В обоих случаях сумма всех нулей = 2

    Прикольное заявление)


    1. webnikler Автор
      10.01.2025 05:22

      Здесь имел ввиду кол-во нулей ,просмотрел этот момент


  1. GULT
    10.01.2025 05:22

    обоих случаях сумма всех нулей = 2 

    Точно сумма нулей == 2 ? А не 0? )))


  1. Vankir94
    10.01.2025 05:22

    Вы что тут устроили пехотинцы?


  1. IpGuru
    10.01.2025 05:22

    Отличная статья, как раз хотел начать изучение алгоритмов!!! Начну с нее)


    1. webnikler Автор
      10.01.2025 05:22

      Удачи в изучении! Сам нахожусь в процессе)


  1. Alexandroppolus
    10.01.2025 05:22

    Если currentSum равен 0, то можно просто взять i+1, это заведомо больше текущего maxLen

    Ну и вспомогательный массив не нужен, преобразовывать 0 к -1 можно в основном цикле, например currentSum += arr[i] || -1;


  1. sobeskiller
    10.01.2025 05:22

    А можно привести пример хоть одной реальной практической проблемы, которая бы свелась к этой задаче и необходимости её вот так помпезно решать? /s


    1. webnikler Автор
      10.01.2025 05:22

      Пример ,боюсь ,что не приведу. А необходимость в том ,чтобы научиться проходить алгоритмические секции собеседований. Я хотел привести свою модель рассуждений при решении подобных задач. Не без косяков ,конечно))


    1. Zara6502
      10.01.2025 05:22

      как и образование в целом - это возможность выстроить мышление определенным образом. Например находясь в какой-то компании легко вычислить человека который имеет ВО и который закончил 9 классов, и нет, не потому что человек с 9-ю классами тупой, это абсурдное утверждение. Разница в том как люди мыслят, какие отправные точки для формирования логики и т.д.

      Я пока не увлёкся решением задач highload писал код сильно иначе.


  1. Zara6502
    10.01.2025 05:22

    а есть ли смысл делать Max тут

    maxLength = Math.max(maxLength, i + 1);

    как я понимаю i+1 всегда будет максимальным значением.


    1. webnikler Автор
      10.01.2025 05:22

      Да ,смысла в этом действительно нет ,тк i + 1 будет всегда наибольшим. Недоглядел ,спасибо)


  1. velon
    10.01.2025 05:22

    Долго пытался понять формулировку: "суммы их кол-ва равны друг другу", суммы количества какие-то, почему было не написать: "количество нулей и единиц совпадает"?!

    Эту формулировку можно даже сократить до "кол-во 0 и 1 равно", если уж на символах начали экономить.

    Самой большой трудностью оказалось понять формулировку задачи. Поправьте как-нибудь


    1. webnikler Автор
      10.01.2025 05:22

      Согласен ,формулировка некорректная. Буду работать над этим в будущем. "Кол-во единиц и нулей совпадает" здесь гораздо понятнее прозвучало бы


      1. Alexandroppolus
        10.01.2025 05:22

        "подстрока, в которой поровну нулей и единиц", как то так


  1. Zara6502
    10.01.2025 05:22

    int[] arr = new int[] { 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 1, 1, 1 };
    Dictionary<int, int> hm = new Dictionary<int, int>();
    int cSum = 0, max = 0;
    for (int i = 0; i < arr.Length; i++)
    {
        cSum += arr[i] == 0 ? -1 : arr[i];
        {
            if (!hm.ContainsKey(cSum))
            {
                hm[cSum] = i;
                if (cSum == 0) max = i + 1;
            }
            else if (max < i - hm[cSum]) max = i - hm[cSum];
        }
    }
    Console.WriteLine(max);

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