Trova l'anno con la popolazione più alta (soluzione più efficiente)


9

Dati due array; $birthscontenente un elenco di anni di nascita che indica quando è nato qualcuno e $deathsun elenco di anni di morte che indica quando qualcuno è morto, come possiamo trovare l'anno in cui la popolazione era più alta?

Ad esempio, dati i seguenti array:

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

L'anno in cui la popolazione era più alta dovrebbe essere 1996, perché la 3gente era viva durante quell'anno, che era il conteggio della popolazione più alta di tutti quegli anni.

Ecco la matematica corrente su questo:

| Nascita | Morte | Popolazione |
| ------- | ------- | ------------ |
| 1981 | | 1 |
| 1984 | | 2 |
| 1984 | 1984 | 2 |
| 1991 | 1991 | 2 |
| 1996 | | 3 |

ipotesi

Possiamo tranquillamente supporre che l'anno in cui qualcuno nasce la popolazione può aumentare di uno e l'anno in cui qualcuno è morto la popolazione può diminuire di uno. Quindi, in questo esempio, 2 persone sono nate nel 1984 e 1 persona è morta nel 1984, il che significa che la popolazione è aumentata di 1 in quell'anno.

Possiamo anche presumere che il numero di decessi non supererà mai il numero di nascite e che nessuna morte può verificarsi quando la popolazione è a 0.

Possiamo anche presumere che gli anni in entrambi $deathse $birthsnon saranno mai valori negativi o in virgola mobile ( sono sempre numeri interi positivi maggiori di 0 ).

Non possiamo supporre che gli array saranno ordinati o che non ci saranno valori duplicati, tuttavia.

Requisiti

Dobbiamo scrivere una funzione per restituire l'anno in cui si è verificata la popolazione più elevata, dati questi due array come input. La funzione può restituire 0, false, ""o NULL( qualsiasi valore falsey è accettabile ) se gli array in input sono vuote o se la popolazione era sempre a 0 in tutto. Se la popolazione più elevata si è verificata in più anni, la funzione può restituire il primo anno in cui è stata raggiunta la popolazione più elevata o qualsiasi anno successivo.

Per esempio:

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

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

Inoltre, includere la Big O della soluzione sarebbe utile.


Il mio miglior tentativo di farlo sarebbe il seguente:

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

L'algoritmo sopra dovrebbe funzionare nel tempo polinomiale dato che è nel peggiore dei casi O(((n log n) * 2) + k)dove nè il numero di elementi da ordinare da ciascun array ed kè il numero di anni di nascita ( poiché sappiamo che kè semprek >= y ) dove yè il numero di anni di morte. Tuttavia, non sono sicuro che esista una soluzione più efficiente.

I miei interessi sono puramente in una Big O migliorata di complessità computazionale sull'algoritmo esistente. La complessità della memoria non è preoccupante. Né è l'ottimizzazione del runtime. Almeno non è una preoccupazione primaria . Eventuali ottimizzazioni di runtime minori / maggiori sono benvenute, ma non il fattore chiave qui.


2
Dato che hai una soluzione funzionante, sarebbe meglio adattarsi a codereview.stackexchange.com ?
Nigel Ren,

1
La domanda è cercare la soluzione più efficiente, non necessariamente una soluzione funzionante. Penso che sia perfettamente valido su SO.
Sceriffo il

1
Non sto dicendo che non è valido su SO (avrei votato per chiudere in quel caso), mi chiedo solo se potresti ricevere più di una risposta su CR.
Nigel Ren,

@NigelRen Non vedo il male nel provare. Anche se vorrei lasciarlo aperto per alcuni giorni. Se non ottiene una risposta, ci metto una taglia.
Sceriffo il

1
Lo stesso SO ha molte delle tue domande sul problema se cerchi parole chiave per morte per nascita. Un miglioramento economico sarebbe quello di migliorare l'ordinamento: rendere un array di lunghezza l'arco di nascita / morte (ogni cella è una data che tiene per valore 0 per impostazione predefinita). aggiungere 1 o sottrarre 1 alla cella per quanto riguarda la nascita e la morte, quindi sommare cumulativamente e mantenere la somma massima trovata
grodzi

Risposte:


4

Penso che possiamo avere O(n log n)tempo con O(1)spazio aggiuntivo prima ordinando, quindi mantenendo una popolazione attuale e il massimo globale mentre ripetiamo. Ho cercato di utilizzare l'anno in corso come punto di riferimento, ma la logica sembrava ancora un po 'complicata, quindi non sono sicuro che sia stato completamente risolto. Spero che possa dare un'idea dell'approccio.

Codice JavaScript (controesempi / bachi benvenuti)

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

Se l'intervallo dell'anno m, è dell'ordine di n, potremmo memorizzare i conteggi per ogni anno nell'intervallo e avere una O(n)complessità temporale. Se volessimo essere fantasiosi, potremmo anche avere una O(n * log log m)complessità temporale, usando un trie Y-fast che consente la ricerca successiva nel O(log log m)tempo.


1. grazie per avermi insegnato l'esistenza del trie Y-fast. Per quanto riguarda l'algo: non è necessario controllare il massimo dopo la diminuzione. Solo dopo l'incremento. L'ultimo blocco non è necessario: considera di ordinare due elenchi ordinati: hai solo bisogno della testa di entrambi (i, j), scegli la testa di ciascuno e fai avanzare quella più piccola. if(birth_i < death_j){//increment stuff + check max} else{//decrement}; birth_i||=infty; death_j||=infty. Inoltre puoi iterare fino a min(birthSize, deathSize). se min è la nascita, fermati. se min è la morte (sospetto ..), fermati e controlla(max + birth.length-i)
grodzi il

@grodzi Ho iniziato considerando la fusione, ma ho concluso che questo richiede una gestione extra a causa del modo in cui i duplicati e l'ordine di nascita contro la morte influiscono sul conteggio. L'ultimo ciclo while mi sembra necessario quando ci sono anni di morte senza eguali per anni di nascita. Hai ragione a dire che il massimo in quel ciclo non è necessario.
עדלעד ברקן

@ גלעדברקן Usa l'ordinamento bucket per un tempo lineare.
Dave

Ho già affermato questa idea nella mia risposta, "Se l'intervallo di anni, m, è nell'ordine di n, potremmo memorizzare i conteggi per ogni anno nell'intervallo e avere una complessità temporale O (n)".
גלעד ברקן

questa non è efficienza, non so perché darti la ricompensa ahahah
Emiliano

4

Possiamo risolverlo in tempo lineare con l'ordinamento della benna. Supponiamo che la dimensione dell'input sia n e che l'intervallo di anni sia 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.

Il massimo cumulativo massimo è la tua risposta.

Il tempo di esecuzione è O (n + m) e lo spazio aggiuntivo necessario è O (m).

Questa è una soluzione lineare in n se m è O (n); cioè, se l'intervallo di anni non sta crescendo più rapidamente del numero di nascite e morti. Questo è quasi certamente vero per i dati del mondo reale.


1
Puoi includere un'implementazione funzionante per favore?
Sceriffo

1
L'implementazione di @Sherif è lasciata come esercizio per il lettore ... È comunque banale. Qualcosa non è chiaro?
Dave

Noterò che poiché la tua granularità è l'anno, c'è qualche ambiguità. in quanto stiamo misurando efficacemente la popolazione alla fine dell'anno, e potrebbe esserci qualche altro punto temporale a metà anno in cui la popolazione è più alta a causa dei tempi delle nascite e delle morti.
Dave

1
Come è questo tempo lineare se dobbiamo analizzare un "array di dimensioni max_yr - min_yr + 1"? (cc @Sherif)
עדלעד ברקן

1
@Dave: la complessità non è O (2n) per i punti 1 e 2? 1. iterare una volta per tutte le nascite + morte: O(n): Find the min and max year across births and deaths 2. iterare di nuovo per tutte le nascite + morte: O(n): Parse the births+death array, incrementing the appropriate index of the array quindi si fa: O (m): analizzare il proprio array, tenendo traccia della somma cumulativa e del suo valore massimo. (non è necessario analizzare questo array - è possibile tenere traccia di MAX aumentando gli indici in 2)
Antony

3

In primo luogo aggregare le nascite e le morti in una mappa ( year => population change), ordinarle per chiave e calcolare la popolazione corrente su quella.

Questo dovrebbe essere approssimativamente O(2n + n log n), dov'è nil numero di nascite.

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

Come vedo: con n = numero di eventi (nascite + morti) e m = numero di anni di eventi (anni con nascite o morti) questo sarebbe effettivamente O (n + m log m) . Se n >> m - questo può essere considerato come O (n) . Se hai miliardi di nascite e morti in un periodo di (diciamo) 100 anni - l'ordinamento di un array con 100 elementi ( ksort($indexed)) diventa irrilevante.
Paul Spiegel,

Potresti elaborare le nascite con $indexed = array_count_values($births);.
Nigel Ren,

3

Ho risolto questo problema con un requisito di memoria di O(n+m)[nel peggiore dei casi, nel migliore dei casiO(n) ]

e, complessità temporale di O(n logn) .

Ecco n & mla lunghezza di birthsedeaths array.

Non conosco PHP o javascript. L'ho implementato con Java e la logica è molto semplice. Ma credo che la mia idea possa essere implementata anche in quelle lingue.

Dettagli di tecnica:

Ho usato java TreeMap struttura per memorizzare i record di nascite e morti.

TreeMapinserisce i dati ordinati (in base alla chiave ) come coppia (chiave, valore), qui la chiave è l'anno e il valore è la somma cumulativa di nascite e decessi (negativa per i decessi).

Non è necessario inserire il valore dei decessi verificatosi dopo il massimo anno di nascita .

Una volta popolata la TreeMap con i record di nascite e morti, tutte le somme cumulative vengono aggiornate e memorizzano la popolazione massima con l'anno man mano che procede.

Esempio di input e output: 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

Esempio di input e output: 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

Qui, i decessi si sono verificati ( 1914 & later) dopo l'ultimo anno di nascita 1913, non sono stati affatto contati, il che evita calcoli inutili.

Per un totale di 10 milliondati (nascite e morti combinati) e oltre 1000 years range, il programma ha iniziato 3 sec.a concludersi.

Se la stessa dimensione dei dati con 100 years range, ci sono voluti 1.3 sec.

Tutti gli input sono presi casualmente.


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

Ciò spiegherà la possibilità di un anno legato, così come se un anno di morte di qualcuno non corrisponde alla nascita di qualcuno.


Questa risposta non fa alcun tentativo di fornire la spiegazione accademica di Big O richiesta dall'OP.
Mickmackusa,

0

La memoria è saggia da mantenere currentPopulatione currentYearcalcolata. Iniziare ordinando entrambi $birthse gli $deathsarray è un ottimo punto, perché l'ordinamento delle bolle non è un compito così pesante, ma consente di tagliare alcuni angoli:

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

non mi piace molto tuffarmi Big O cosa di , te l'ha lasciato.

Inoltre, se riscopri currentYearComputingogni ciclo, puoi cambiare i cicli in ifistruzioni e lasciarli con un solo ciclo.

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

l'array shift è una buona opzione per la memoria ma non per le prestazioni, controlla questo cmljnelson.blog/2018/10/16/phps-array_shift-performance
Emiliano

È sempre possibile ordinare in ordine decrescente, procedere con la decrementazione anziché con l'incremento e con il pop anziché lo shift.
yergo

0

Riempo molto bene di questa soluzione, la complessità di 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));
?>

Non dovrebbe $tmpArray--essere $tmpArray[$death]--? Prova anche con $births=[1997,1997,1998]; $deaths=[];- Ritorna 1998come dovrebbe?
Paul Spiegel,

si hai ragione.
Emiliano

Questo codice non solo fallisce nei casi limite complessi, ma fallisce anche nei casi più semplici come dati gli array di input $births = [3,1,2,1,3,3,2]e $deaths = [2,3,2,3,3,3]mi aspetterei di tornare 2come anno di popolazione più elevato, ma il codice ritorna 1. In effetti il tuo codice ha fallito 9 dei 15 dei miei test unitari . Non solo non posso accettare questa come la risposta più efficiente, ma non posso nemmeno accettarla come una risposta efficiente dal momento che non funziona affatto.
Sceriffo

Non hai letto attentamente la domanda e quindi non hai fornito una buona risposta. Fai qui il presupposto che ti avevo detto di non fare ( che gli array sono ordinati ). Quindi per favore rimuovi il tuo commento offensivo nella domanda su come ho assegnato la generosità a una risposta non efficiente e questa è in qualche modo una " soluzione ".
Sherif

0

Uno degli approcci più semplici e chiari per il tuo problema.

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

uscita :

1909

complessità :

O(m + log(n))

per 1 milione di record il tempo di esecuzione è appena29.64 milliseconds
Ronak Dhoot

Come indicato nella domanda, non sto cercando ottimizzazioni di runtime, ma va notato che il tuo calcolo Big O è leggermente fuori qui. Inoltre, il tuo codice è leggermente rotto. Non riesce in un numero di casi limite.
Sceriffo
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.