Знайдіть рік з найбільшою кількістю населення (найефективніше рішення)


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 році, а одна людина померла у 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
Я не кажу, що це не дійсно щодо ПЗ (я би проголосував, щоб закрити в такому випадку), мені просто цікаво, чи не отримаєте ви більше відповіді на CR.
Найджел Рен

@NigelRen Я не бачу шкоди в спробах. Хоча я хотів би залишити це відкритим на кілька днів. Якщо не отримає відповіді, я покладу на це щедрість.
Шериф

1
Таким чином, у вас є багато ваших проблемних питань, якщо ви шукаєте ключові слова смерті від народження. Недорогим поліпшенням було б покращення сортування: зробити масив довжини відрізком народження / смерті (кожна комірка - це значення, яке за замовчуванням має значення 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. thx для навчання мене існуванню Y-швидкого трие. Щодо альго: не потрібно перевіряти максимум після зменшення. Тільки після прирощення. Останнє, поки блок не є необхідним: розгляньте сортування двох відсортованих списків: вам просто потрібна голова обох (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

@grodzi Я почав розглядати сортування злиття, але зробив висновок, що це потребує додаткової обробки через те, як дублікати, а також порядок народження проти смерті впливає на кількість. Остання петля здається мені необхідною, коли існують роки смерті, які не мають рівних рокам народження. Ви вірні, що максимум у цьому циклі не потрібен.
גלעד ברקן

@ גלעדברקן Використовуйте сортування відра для лінійного часу.
Дейв

Я вже заявив про цю ідею у своїй відповіді: "Якщо діапазон років, м, є порядком 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, яке вимагає ОП.
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.