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


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.

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


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);

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


Этот ответ не пытается дать академическое объяснение 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;
        }

сдвиг массива - хороший вариант для памяти, но не для производительности, проверьте это 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 здесь немного не верен. Кроме того, ваш код немного сломан. Это терпит неудачу в ряде крайних случаев.
— Шериф
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.