Найдите год с наибольшим населением (наиболее эффективное решение)

9

Дано два массива; $birthsсодержащий список лет рождения, указывающих, когда кто-то родился, и $deathsсодержащий список лет смерти, указывающих, когда кто-то умер, как мы можем найти год, в котором население было самым высоким?

Например, приведены следующие массивы:

$births = [1984, 1981, 1984, 1991, 1996];
$deaths = [1991, 1984];

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

Вот бегущая математика по этому поводу:

| Рождение | Смерть | Население
| ------- | ------- | ------------ |
| 1981 | | 1 |
| 1984 | | 2 |
| 1984 | 1984 | 2 |
| 1991 | 1991 | 2 |
| 1996 | | 3 |

Предположения

Мы можем с уверенностью предположить, что год, в котором кто-то родился, население может увеличиться на один, и год, в котором кто-то умер, население может уменьшиться на один. Таким образом, в этом примере 2 человека родились в 1984 году, а 1 человек умер в 1984 году, то есть население увеличилось на 1 за этот год.

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

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

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

Требования

Мы должны написать функцию, которая будет возвращать год, в который произошло наибольшее население, учитывая эти два массива в качестве входных данных. Функция может возвращать 0, false, ""или NULL( любое значение falsey приемлемо ) , если входные массивы являются пустыми или если население всегда было на 0 в течение. Если наибольшая численность населения наблюдалась в течение нескольких лет, функция может возвращать первый год, в который была достигнута наибольшая численность населения, или любой последующий год.

Например:

$births = [1997, 1997, 1997, 1998, 1999];
$deaths = [1998, 1999];

/* The highest population was 3 on 1997, 1998 and 1999, either answer is correct */

Кроме того, было бы полезно включить Big O решения.


Моя лучшая попытка сделать это была бы следующей:

function highestPopulationYear(Array $births, Array $deaths): Int {

    sort($births);
    sort($deaths);

    $nextBirthYear = reset($births);
    $nextDeathYear = reset($deaths);

    $years = [];
    if ($nextBirthYear) {
        $years[] = $nextBirthYear;
    }
    if ($nextDeathYear) {
        $years[] = $nextDeathYear;
    }

    if ($years) {
        $currentYear = max(0, ...$years);
    } else {
        $currentYear = 0;
    }

    $maxYear = $maxPopulation = $currentPopulation = 0;

    while(current($births) !== false || current($deaths) !== false || $years) {

        while($currentYear === $nextBirthYear) {
            $currentPopulation++;
            $nextBirthYear = next($births);
        }

        while($currentYear === $nextDeathYear) {
            $currentPopulation--;
            $nextDeathYear = next($deaths);
        }

        if ($currentPopulation >= $maxPopulation) {
            $maxPopulation = $currentPopulation;
            $maxYear = $currentYear;
        }

        $years = [];

        if ($nextBirthYear) {
            $years[] = $nextBirthYear;
        }
        if ($nextDeathYear) {
            $years[] = $nextDeathYear;
        }
        if ($years) {
            $currentYear = min($years);
        } else {
            $currentYear = 0;
        }
    }

    return $maxYear;
}

Вышеприведенный алгоритм должен работать за полиномиальное время, учитывая, что в худшем O(((n log n) * 2) + k)случае nэто число элементов, которые должны быть отсортированы из каждого массива, и kчисло лет рождения ( поскольку мы знаем, что kэто всегдаk >= y ), где yчисло лет смерти. Однако я не уверен, есть ли более эффективное решение.

Мои интересы просто в улучшенном Big O вычислительной сложности по существующему алгоритму. Сложность памяти не имеет значения. Также не оптимизация во время выполнения. По крайней мере, это не главное . Любые второстепенные / существенные оптимизации времени исполнения приветствуются, но это не ключевой фактор.

шериф
источник
2
Поскольку у вас есть рабочее решение, будет ли оно лучше соответствовать codereview.stackexchange.com ?
Найджел Рен
1
Вопрос в том, чтобы найти наиболее эффективное решение, а не какое-либо рабочее решение. Я думаю, что это совершенно справедливо на SO.
Шериф
1
Я не говорю, что это не относится к SO (я бы проголосовал за закрытие в этом случае), я просто хотел бы знать, можете ли вы получить больше ответов по CR.
Найджел Рен
@NigelRen Я не вижу вреда в попытках. Хотя я бы хотел оставить это открытым на несколько дней. Если он не получит ответа, я наложу на него награду.
Шериф
1
У самого SO много проблемных вопросов, если вы ищете ключевые слова смерти при рождении. Дешевым улучшением было бы улучшение сортировки: сделать массив длины диапазоном рождения / смерти (каждая ячейка является датой, содержащей значение 0 по умолчанию). добавьте 1 или вычтите 1 в ячейку о рождении и смерти, затем накопите сумму и сохраните найденную максимальную сумму
grodzi

Ответы:

4

Я думаю, что у нас может быть O(n log n)время с O(1)дополнительным пространством, сначала отсортировав, а затем сохранив текущую совокупность и глобальный максимум во время итерации. Я пытался использовать текущий год в качестве ориентира, но логика все еще казалась немного хитрой, поэтому я не уверен, что она полностью проработана. Надеюсь, это может дать представление о подходе.

Код JavaScript (контрпримеры / ошибки приветствуются)

function f(births, deaths){
  births.sort((a, b) => a - b);
  deaths.sort((a, b) => a - b);

  console.log(JSON.stringify(births));
  console.log(JSON.stringify(deaths));
  
  let i = 0;
  let j = 0;
  let year = births[i];
  let curr = 0;
  let max = curr;

  while (deaths[j] < births[0])
    j++;

  while (i < births.length || j < deaths.length){
    while (year == births[i]){
      curr = curr + 1;
      i = i + 1;
    }
    
    if (j == deaths.length || year < deaths[j]){
      max = Math.max(max, curr);
      console.log(`year: ${ year }, max: ${ max }, curr: ${ curr }`);
    
    } else if (j < deaths.length && deaths[j] == year){
      while (deaths[j] == year){
        curr = curr - 1;
        j = j + 1;
      }
      max = Math.max(max, curr);
      console.log(`year: ${ year }, max: ${ max }, curr: ${ curr }`);
    }

    if (j < deaths.length && deaths[j] > year && (i == births.length || deaths[j] < births[i])){
      year = deaths[j];
      while (deaths[j] == year){
        curr = curr - 1;
        j = j + 1;
      }
      console.log(`year: ${ year }, max: ${ max }, curr: ${ curr }`);
    }

    year = births[i];
  }
  
  return max;
}

var input = [
  [[1997, 1997, 1997, 1998, 1999],
  [1998, 1999]],
  [[1, 2, 2, 3, 4],
  [1, 2, 2, 5]],
  [[1984, 1981, 1984, 1991, 1996],
  [1991, 1984, 1997]],
  [[1984, 1981, 1984, 1991, 1996],
  [1991, 1982, 1984, 1997]]
]

for (let [births, deaths] of input)
  console.log(f(births, deaths));

Если диапазон по годам mпорядка n, мы могли бы хранить значения для каждого года в диапазоне и иметь O(n)временную сложность. Если бы мы хотели получить фантазию, мы могли бы также иметь O(n * log log m)временную сложность, используя Y-быстрый метод, который позволяет искать преемника во O(log log m)времени.

גלעד ברקן
источник
1. Спасибо за то, что научил меня существованию Y-fast trie. Относительно алгоритма: нет необходимости проверять макс после уменьшения. Только после увеличения. Последний блок while является ненужным: рассмотрите возможность сортировки двух отсортированных списков: вам просто нужен заголовок обоих (i, j), выберите заголовок каждого и выдвиньте меньший. if(birth_i < death_j){//increment stuff + check max} else{//decrement}; birth_i||=infty; death_j||=infty, Также вы можете перебирать до min(birthSize, deathSize). если мин это рождение, остановись. если мин - смерть (подозрительно ..), остановись и проверь(max + birth.length-i)
гродзи
@grodzi Я начал с сортировки слиянием, но пришел к выводу, что это требует дополнительной обработки из-за того, как дублирование, а также порядок рождения и смерть влияют на количество. Последний цикл while кажется мне необходимым, когда есть годы смерти, не соответствующие годам рождения. Вы правы, что максимум в этом цикле не нужен.
גלעד ברקן
@ גלעדברקן Используйте сортировку ведра для линейного времени.
Дейв
Я уже высказал эту идею в своем ответе: «Если годовой диапазон, m, имеет порядок n, мы могли бы хранить значения для каждого года в этом диапазоне и иметь O (n) временную сложность».
גלעד ברקן
это не эффективность, я не знаю, зачем давать вам награду, хахаха
Эмилиано
4

Мы можем решить это за линейное время с сортировкой сегментов. Допустим, размер ввода равен n, а диапазон лет равен m.

O(n): Find the min and max year across births and deaths.
O(m): Create an array of size max_yr - min_yr + 1, ints initialized to zero. 
      Treat the first cell of the array as min_yr, the next as min_yr+1, etc...
O(n): Parse the births array, incrementing the appropriate index of the array. 
      arr[birth_yr - min_yr] += 1
O(n): Ditto for deaths, decrementing the appropriate index of the array.
      arr[death_yr - min_yr] -= 1
O(m): Parse your array, keeping track of the cumulative sum and its max value.

Самый большой совокупный максимум - ваш ответ.

Время работы составляет O (n + m), а необходимое дополнительное пространство - O (m).

Это линейное решение по n, если m равно O (n); то есть, если диапазон лет не растет быстрее, чем число рождений и смертей. Это почти наверняка верно для данных реального мира.

Дейв
источник
1
Можете ли вы включить рабочую реализацию, пожалуйста?
Шериф
1
Реализация @Sherif оставлена ​​читателю как упражнение ... В любом случае, это тривиально. Что-то не понятно?
Дейв
Отмечу, что из-за того, что у вас гранулярность - год, есть некоторая двусмысленность. в том смысле, что мы эффективно измеряем численность населения на конец года, и может быть какой-то другой момент времени в середине года, когда численность населения выше из-за сроков рождений и смертей.
Дейв
1
Каково это линейное время, если мы должны проанализировать «массив размером max_yr - min_yr + 1»? (cc @Sherif)
גלעד ברקן
1
@Dave: сложность не O (2n) для пунктов 1 и 2? 1. повторить один раз для всех рождений + смерти: O(n): Find the min and max year across births and deaths 2. повторить еще раз для всех рождений + смерти: O(n): Parse the births+death array, incrementing the appropriate index of the array затем вы выполните: O (m): проанализируйте ваш массив, отслеживая совокупную сумму и ее максимальное значение. (вам не нужно анализировать этот массив - вы можете отслеживать MAX при увеличении индексов в 2)
Антоний
3

Сначала объедините рождения и смерти в карту ( year => population change), отсортируйте их по ключам и рассчитайте численность населения по ним.

Это должно быть приблизительно O(2n + n log n), где nчисло рождений.

$births = [1984, 1981, 1984, 1991, 1996];
$deaths = [1991, 1984];

function highestPopulationYear(array $births, array $deaths): ?int
{
    $indexed = [];

    foreach ($births as $birth) {
        $indexed[$birth] = ($indexed[$birth] ?? 0) + 1;
    }

    foreach ($deaths as $death) {
        $indexed[$death] = ($indexed[$death] ?? 0) - 1;
    }

    ksort($indexed);

    $maxYear = null;
    $max = $current = 0;

    foreach ($indexed as $year => $change) {
        $current += $change;
        if ($current >= $max) {
            $max = $current;
            $maxYear = $year;
        }
    }

    return $maxYear;
}

var_dump(highestPopulationYear($births, $deaths));
Ричард ван Вельзен
источник
Как я вижу: при n = количестве событий (рождений + смертей) и m = количестве лет событий (лет с рождениями или смертями) это будет фактически O (n + m log m) . Если n >> m - это можно рассматривать как O (n) . Если у вас есть миллиарды рождений и смертей за (скажем) 100 лет - сортировка массива из 100 элементов ( ksort($indexed)) становится неактуальной.
Пол Шпигель
Вы можете обработать рождения с $indexed = array_count_values($births);.
Найджел Рен
3

Я решил эту проблему с требованием памяти O(n+m)[в худшем случае, в лучшем случае O(n)]

и, время сложность O(n logn).

Здесь n & mуказаны длины birthsи deathsмассивы.

Я не знаю PHP или JavaScript. Я реализовал это с помощью Java, и логика очень проста. Но я верю, что моя идея может быть реализована и на этих языках.

Детали техники:

Я использовал TreeMapструктуру Java для хранения записей о рождении и смерти.

TreeMapвставляет отсортированные данные (на основе ключа) в виде пары (ключ, значение), здесь ключ - это год, а значение - совокупная сумма рождений и смертей (отрицательная для смертей).

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

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

Пример ввода и вывода: 1

Births: [1909, 1919, 1904, 1911, 1908, 1908, 1903, 1901, 1914, 1911, 1900, 1919, 1900, 1908, 1906]

Deaths: [1910, 1911, 1912, 1911, 1914, 1914, 1913, 1915, 1914, 1915]

Year counts Births: {1900=2, 1901=1, 1903=1, 1904=1, 1906=1, 1908=3, 1909=1, 1911=2, 1914=1, 1919=2}

Year counts Birth-Deaths combined: {1900=2, 1901=1, 1903=1, 1904=1, 1906=1, 1908=3, 1909=1, 1910=-1, 1911=0, 1912=-1, 1913=-1, 1914=-2, 1915=-2, 1919=2}

Yearwise population: {1900=2, 1901=3, 1903=4, 1904=5, 1906=6, 1908=9, 1909=10, 1910=9, 1911=9, 1912=8, 1913=7, 1914=5, 1915=3, 1919=5}

maxPopulation: 10
yearOfMaxPopulation: 1909

Пример ввода и вывода: 2

Births: [1906, 1901, 1911, 1902, 1905, 1911, 1902, 1905, 1910, 1912, 1900, 1900, 1904, 1913, 1904]

Deaths: [1917, 1908, 1918, 1915, 1907, 1907, 1917, 1917, 1912, 1913, 1905, 1914]

Year counts Births: {1900=2, 1901=1, 1902=2, 1904=2, 1905=2, 1906=1, 1910=1, 1911=2, 1912=1, 1913=1}

Year counts Birth-Deaths combined: {1900=2, 1901=1, 1902=2, 1904=2, 1905=1, 1906=1, 1907=-2, 1908=-1, 1910=1, 1911=2, 1912=0, 1913=0}

Yearwise population: {1900=2, 1901=3, 1902=5, 1904=7, 1905=8, 1906=9, 1907=7, 1908=6, 1910=7, 1911=9, 1912=9, 1913=9}

maxPopulation: 9
yearOfMaxPopulation: 1906

Здесь случаи смерти ( 1914 & later) после последнего года рождения 1913вообще не учитывались, что позволяет избежать ненужных вычислений.

По общей сумме 10 millionданных (количество рождений и смертей) и более 1000 years range, программа 3 sec.завершилась.

Если данные того же размера 100 years range, это заняло 1.3 sec.

Все входы выбираются случайным образом.

User_67128
источник
1
$births = [1984, 1981, 1984, 1991, 1996];
$deaths = [1991, 1984];
$years = array_unique(array_merge($births, $deaths));
sort($years);

$increaseByYear = array_count_values($births);
$decreaseByYear = array_count_values($deaths);
$populationByYear = array();

foreach ($years as $year) {
    $increase = $increaseByYear[$year] ?? 0;
    $decrease = $decreaseByYear[$year] ?? 0;
    $previousPopulationTally = end($populationByYear);
    $populationByYear[$year] = $previousPopulationTally + $increase - $decrease;
}

$maxPopulation = max($populationByYear);
$maxPopulationYears = array_keys($populationByYear, $maxPopulation);

$maxPopulationByYear = array_fill_keys($maxPopulationYears, $maxPopulation);
print_r($maxPopulationByYear);

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

kmuenkel
источник
Этот ответ не пытается дать академическое объяснение Big O, которое запрашивает OP.
mickmackusa
0

Память целесообразно сохранить currentPopulationи currentYearпросчитать. Начать с сортировки $birthsи $deathsмассивов - очень хороший момент, потому что пузырьковая сортировка не такая уж тяжелая задача, но позволяет срезать некоторые углы:

<?php

$births = [1997, 1999, 2000];
$deaths = [2000, 2001, 2001];

function highestPopulationYear(array $births, array $deaths): Int {

    // sort takes time, but is neccesary for futher optimizations
    sort($births);
    sort($deaths);

    // first death year is a first year where population might decrase 
    // sorfar max population
    $currentYearComputing = $deaths[0];

    // year before first death has potential of having the biggest population
    $maxY = $currentYearComputing-1;

    // calculating population at the begining of the year of first death, start maxPopulation
    $population = $maxPop = count(array_splice($births, 0, array_search($deaths[0], $births)));

    // instead of every time empty checks: `while(!empty($deaths) || !empty($births))`
    // we can control a target time. It reserves a memory, but this slot is decreased
    // every iteration.
    $iterations = count($deaths) + count($births);

    while($iterations > 0) {
        while(current($births) === $currentYearComputing) {
            $population++;
            $iterations--;
            array_shift($births); // decreasing memory usage
        }

        while(current($deaths) === $currentYearComputing) {
            $population--;
            $iterations--;
            array_shift($deaths); // decreasing memory usage
        }

        if ($population > $maxPop) {
            $maxPop = $population;
            $maxY = $currentYearComputing;
        }

        // In $iterations we have a sum of birth/death events left. Assuming all 
        // are births, if this number added to currentPopulation will never exceed
        // current maxPoint, we can break the loop and save some time at cost of
        // some memory.
        if ($maxPop >= ($population+$iterations)) {
            break;
        }

        $currentYearComputing++;
    }

    return $maxY;
}

echo highestPopulationYear($births, $deaths);

не очень-то увлеченный погружением в Big O , оставь это тебе.

Кроме того, если вы заново открываете currentYearComputingкаждый цикл, вы можете изменить циклы на ifоператоры и оставить только один цикл.

    while($iterations > 0) {

        $changed = false;

        if(current($births) === $currentYearComputing) {
            // ...
            $changed = array_shift($births); // decreasing memory usage
        }

        if(current($deaths) === $currentYearComputing) {
            // ...
            $changed = array_shift($deaths); // decreasing memory usage
        }

        if ($changed === false) {
            $currentYearComputing++;
            continue;
        }
yergo
источник
сдвиг массива - хороший вариант для памяти, но не для производительности, проверьте это cmljnelson.blog/2018/10/16/phps-array_shift-performance
Эмилиано
Вы всегда можете сортировать по убыванию, идти с декрементом вместо увеличения, и с поп вместо замены.
yergo
0

Я заполняю очень удобно это решение, сложность Big O составляет n + m

<?php
function getHighestPopulation($births, $deaths){
    $max = [];
    $currentMax = 0;
    $tmpArray = [];

    foreach($deaths as $key => $death){
        if(!isset($tmpArray[$death])){
            $tmpArray[$death] = 0;    
        }
        $tmpArray[$death]--;
    }
    foreach($births as $k => $birth){
        if(!isset($tmpArray[$birth])){
            $tmpArray[$birth] = 0;
        }
        $tmpArray[$birth]++;
        if($tmpArray[$birth] > $currentMax){
            $max = [$birth];
            $currentMax = $tmpArray[$birth];
        } else if ($tmpArray[$birth] == $currentMax) {
            $max[] = $birth;
        }
    }

    return [$currentMax, $max];
}

$births = [1997, 1997, 1997, 1998, 1999];
$deaths = [1998, 1999];

print_r (getHighestPopulation($births, $deaths));
?>
Эмилиано
источник
Не должно $tmpArray--быть $tmpArray[$death]--? Также, пожалуйста, проверьте с $births=[1997,1997,1998]; $deaths=[];- он возвращается 1998как следует?
Пол Шпигель
да вы правы.
Эмилиано
Этот код не только терпит неудачу в сложных крайних случаях, но он даже терпит неудачу в самых простых случаях, таких как заданные входные массивы, $births = [3,1,2,1,3,3,2]и $deaths = [2,3,2,3,3,3]я ожидаю, что вернусь 2как год с наибольшим населением, но ваш код вернется 1. На самом деле ваш код не прошел 9 из 15 моих модульных тестов . Я не только не могу принять это как самый эффективный ответ, но я даже не могу принять это как эффективный ответ, так как он не работает вообще.
Шериф
Вы не смогли внимательно прочитать вопрос и, таким образом, не смогли дать хороший ответ. Вы делаете предположение, что я сказал вам не делать ( что массивы отсортированы ). Поэтому, пожалуйста, удалите свой оскорбительный комментарий в вопросе о том, как я присудил награду за неэффективный ответ, и это как-то « исправление ».
Шериф
0

Один из самых простых и понятных подходов к вашей проблеме.

$births = [1909, 1919, 1904, 1911, 1908, 1908, 1903, 1901, 1914, 1911, 1900, 1919, 1900, 1908, 1906];
$deaths = [1910, 1911, 1912, 1911, 1914, 1914, 1913, 1915, 1914, 1915];

/* for generating 1 million records

for($i=1;$i<=1000000;$i++) {
    $births[] = rand(1900, 2020);
    $deaths[] = rand(1900, 2020);
}
*/

function highestPopulationYear(Array $births, Array $deaths): Int {
    $start_time = microtime(true); 
    $population = array_count_values($births);
    $deaths = array_count_values($deaths);

    foreach ($deaths as $year => $death) {
        $population[$year] = ($population[$year] ?? 0) - $death;
    }
    ksort($population, SORT_NUMERIC);
    $cumulativeSum = $maxPopulation = $maxYear = 0;
    foreach ($population as $year => &$number) {
        $cumulativeSum += $number;
        if($maxPopulation < $cumulativeSum) {
            $maxPopulation = $cumulativeSum;
            $maxYear = $year;
        }
    }
    print " Execution time of function = ".((microtime(true) - $start_time)*1000)." milliseconds"; 
    return $maxYear;
}

print highestPopulationYear($births, $deaths);

вывод :

1909

сложность :

O(m + log(n))
Ронак Дхут
источник
за 1 миллион записей время выполнения просто 29.64 milliseconds
Ronak Dhoot
Как указано в вопросе, я не после оптимизаций во время выполнения, но следует отметить, что ваш расчет Big O здесь немного не верен. Кроме того, ваш код немного сломан. Это терпит неудачу в ряде крайних случаев.
Шериф