Encontre o ano com a população mais alta (solução mais eficiente)

9

Dada duas matrizes; $birthscontendo uma lista de anos de nascimento indicando quando alguém nasceu e $deathsuma lista de anos de morte indicando quando alguém morreu, como podemos encontrar o ano em que a população foi mais alta?

Por exemplo, dadas as seguintes matrizes:

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

O ano em que a população era mais alta deveria ser 1996, porque as 3pessoas estavam vivas durante esse ano, que era a maior contagem de população de todos esses anos.

Aqui está a matemática em execução nisso:

| Nascimento Morte População |
| ------- | ------- | ------------ |
| 1981 | 1 |
| 1984 | 2
| 1984 1984 2
| 1991 1991 2
| 1996 | 3

Premissas

Podemos assumir com segurança que o ano em que alguém nasce, a população pode aumentar em um e o ano em que alguém morreu, a população pode diminuir em um. Portanto, neste exemplo, 2 pessoas nasceram em 1984 e 1 pessoa morreu em 1984, significando que a população aumentou 1 naquele ano.

Também podemos assumir com segurança que o número de mortes nunca excederá o número de nascimentos e que nenhuma morte poderá ocorrer quando a população estiver em 0.

Também podemos assumir com segurança que os anos em ambos $deathse $birthsnunca serão valores negativos ou de ponto flutuante ( eles sempre são números inteiros positivos maiores que 0 ).

Não podemos assumir que as matrizes serão classificadas ou que não haverá valores duplicados.

Exigências

Devemos escrever uma função para retornar o ano em que a população mais alta ocorreu, considerando essas duas matrizes como entrada. A função pode retornar 0, false, "", ou NULL( qualquer valor Falsey é aceitável ) se os arrays de entrada estão vazias ou se a população foi sempre a 0 durante todo. Se a população mais alta ocorreu em vários anos, a função pode retornar o primeiro ano em que a população mais alta foi atingida ou em qualquer ano subsequente.

Por exemplo:

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

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

Além disso, incluir o Big O da solução seria útil.


Minha melhor tentativa de fazer isso seria a seguinte:

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 algoritmo acima deve funcionar em tempo polinomial, dado que, na pior das hipóteses, O(((n log n) * 2) + k)onde nestá o número de elementos a serem classificados de cada matriz e o knúmero de anos de nascimento ( já que sabemos que ké semprek >= y ) onde yestá o número de anos de morte. No entanto, não tenho certeza se existe uma solução mais eficiente.

Meus interesses são puramente em um Big O aprimorado de complexidade computacional sobre o algoritmo existente. A complexidade da memória não é preocupante. Nem é a otimização de tempo de execução. Pelo menos não é uma preocupação principal . Quaisquer otimizações de tempo de execução menores / maiores são bem-vindas, mas não são o fator chave aqui.

xerife
fonte
2
Como você tem uma solução funcional, isso seria mais adequado para codereview.stackexchange.com ?
Nigel Ren
11
A questão é buscar a solução mais eficiente, não necessariamente qualquer solução funcional. Eu acho que isso é perfeitamente válido no SO.
Sherif
11
Eu não estou dizendo que não é válido no SO (eu teria votado para fechar nesse caso); estou apenas pensando se você pode obter mais respostas sobre o RC.
Nigel Ren
@NigelRen Não vejo mal em tentar. Embora eu queira deixar isso em aberto por alguns dias. Se não receber uma resposta, colocarei uma recompensa.
Sherif
11
O SO em si tem muitas perguntas sobre o problema, se você procurar por palavras-chave de morte por nascimento. Uma melhoria barata seria melhorar o tipo: faça com que uma matriz de comprimento seja o período de nascimento / morte (cada célula é uma data guardada para o valor 0 por padrão). adicione 1 ou substrato 1 à célula em relação ao nascimento e à morte e, em seguida, some cumulativamente e mantenha a soma máxima encontrada
grodzi

Respostas:

4

Acho que podemos ter O(n log n)tempo com O(1)espaço adicional classificando primeiro e depois mantendo a população atual e o máximo global à medida que iteramos. Tentei usar o ano atual como ponto de referência, mas a lógica ainda parecia um pouco complicada, por isso não tenho certeza se está completamente elaborado. Felizmente, ele pode dar uma idéia da abordagem.

Código JavaScript (contra-exemplos / bugs são bem-vindos)

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 o intervalo do ano,, mestiver na ordem de n, poderíamos armazenar as contagens para cada ano no intervalo e ter O(n)complexidade de tempo. Se quiséssemos ser extravagantes, também poderíamos ter O(n * log log m)complexidade de tempo, usando um teste Y-fast que permite a pesquisa sucessora a O(log log m)tempo.

גלעד ברקן
fonte
1. thx por me ensinar a existência de Y-fast trie. Em relação a algo: não é necessário verificar o máximo depois de diminuir. Somente após o incremento. Por último, enquanto o bloco é desnecessário: considere classificar duas listas classificadas: você só precisa da cabeça de ambos (i, j), escolha a cabeça de cada uma e avance a menor. if(birth_i < death_j){//increment stuff + check max} else{//decrement}; birth_i||=infty; death_j||=infty. Além disso, você pode iterar até min(birthSize, deathSize). se min é nascimento, pare. se min é morte (suspeito ..), pare e verifique(max + birth.length-i)
grodzi 25/02
@grodzi Eu comecei a considerar a classificação por mesclagem, mas concluí que isso precisa de tratamento extra por causa de como duplicatas, bem como a ordem de nascimento e morte, afeta a contagem. O último loop while parece necessário para mim quando há anos de morte inigualáveis ​​por anos de nascimento. Você está certo de que o máximo nesse loop é desnecessário.
גלעד ברקן 25/02
@ קןלעדברקן Use a classificação do intervalo por tempo linear.
Dave
Eu já afirmei essa idéia na minha resposta: "Se o intervalo do ano, m, estiver na ordem de n, poderíamos armazenar as contagens de cada ano no intervalo e ter O (n) complexidade de tempo".
גלעד ברקן 3/03
isso não é eficiência, não sei por que te dar a recompensa hahaha
Emiliano
4

Podemos resolver isso em tempo linear com a classificação de bucket. Digamos que o tamanho da entrada seja n e o intervalo de anos seja 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 maior máximo cumulativo é a sua resposta.

O tempo de execução é O (n + m) e o espaço adicional necessário é O (m).

Esta é uma solução linear em n se m for O (n); ou seja, se o intervalo de anos não estiver crescendo mais rapidamente do que o número de nascimentos e mortes. Isso é quase certamente verdade para dados do mundo real.

Dave
fonte
11
Você pode incluir uma implementação funcional, por favor?
Sherif
11
A implementação @Sherif é deixada como um exercício para o leitor ... De qualquer maneira, é trivial. Alguma coisa não está clara?
Dave
Observarei que, como sua granularidade é ano, há alguma ambiguidade. na medida em que estamos efetivamente medindo a população a partir do final do ano, e pode haver algum outro ponto no meio do ano em que a população seja maior devido ao tempo de nascimentos e mortes.
Dave
11
Como é esse tempo linear se tivermos que analisar uma "matriz de tamanho max_yr - min_yr + 1"? (cc @Sherif)
קןלעד ברקן
11
@ Dave: a complexidade não é O (2n) para os pontos 1 e 2? 1. itere uma vez por todos os nascimentos + morte: O(n): Find the min and max year across births and deaths 2. itere novamente por todos os nascimentos + morte: O(n): Parse the births+death array, incrementing the appropriate index of the array e faça: O (m): Analise sua matriz, mantendo o controle da soma acumulada e seu valor máximo. (você não precisa analisar essa matriz - pode acompanhar o MAX enquanto aumenta os índices em 2)
Antony
3

Primeiro agregue os nascimentos e mortes em um mapa ( year => population change), classifique-o por chave e calcule a população em execução sobre ele.

Isso deve ser aproximadamente O(2n + n log n), onde né o número de nascimentos.

$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));
Richard van Velzen
fonte
Como vejo: Com n = número de eventos (nascimentos + mortes) e m = número de anos de eventos (anos com nascimentos ou mortes), esse seria realmente O (n + m log m) . Se n >> m - pode ser considerado como O (n) . Se você tiver bilhões de nascimentos e mortes em um período de (digamos) 100 anos - classificar uma matriz com 100 elementos ( ksort($indexed)) torna-se irrelevante.
Paul Spiegel
Você pode processar os nascimentos com $indexed = array_count_values($births);.
Nigel Ren
3

Resolvi esse problema com um requisito de memória de O(n+m)[no pior dos casos, no melhor caso O(n)]

e, complexidade do tempo de O(n logn).

Aqui, n & mestão o comprimento birthse as deathsmatrizes.

Eu não sei PHP ou javascript. Eu o implementei com Java e a lógica é muito simples. Mas acredito que minha ideia também possa ser implementada nesses idiomas.

Detalhes da técnica:

Usei a TreeMapestrutura java para armazenar registros de nascimentos e óbitos.

TreeMapinsere dados classificados (com base em chave ) como par (chave, valor), aqui a chave é o ano e o valor é a soma cumulativa de nascimentos e mortes (negativa para mortes).

Não precisamos inserir o valor das mortes que ocorreram após o ano de nascimento mais alto .

Depois que o TreeMap é preenchido com os registros de nascimentos e óbitos, todas as somas acumuladas são atualizadas e armazenam a população máxima com o ano à medida que avançava.

Entrada e saída de amostra: 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

Entrada e saída de amostra: 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

Aqui, as mortes ocorreram ( 1914 & later) após o último ano de nascimento 1913, não foram contabilizadas, o que evita cálculos desnecessários.

Para um total de 10 milliondados (nascimentos e mortes combinados) e mais 1000 years range, o programa demorou 3 sec.para terminar.

Se os dados do mesmo tamanho com 100 years range, levou 1.3 sec.

Todas as entradas são obtidas aleatoriamente.

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

Isso explicará a possibilidade de um ano vinculado, bem como se um ano da morte de alguém não corresponder ao nascimento de alguém.

kmuenkel
fonte
Essa resposta não tenta fornecer a explicação acadêmica do Big O solicitada pelo OP.
mickmackusa 7/03
0

Memória é para manter currentPopulatione currentYearcalculado. Começar por classificar as duas $birthse as $deathsmatrizes é um ponto muito bom, porque a classificação de bolhas não é uma tarefa pesada, mas permite cortar alguns cantos:

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

realmente interessado em mergulhar na coisa do Big O , deixou isso para você.

Além disso, se você redescobrir currentYearComputingcada loop, poderá alterar os loops em ifinstruções e sair com apenas um loop.

    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
fonte
o deslocamento de matriz é uma boa opção para a memória, mas não para o desempenho, verifique este cmljnelson.blog/2018/10/16/phps-array_shift-performance
Emiliano
Você sempre pode classificar em ordem decrescente, decrementar em vez de incrementar e pop em vez de shift.
yergo 7/03
0

Encho muito confortável essa solução, a complexidade 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));
?>
Emiliano
fonte
Não deveria $tmpArray--ser $tmpArray[$death]--? Teste também com $births=[1997,1997,1998]; $deaths=[];- Ele retorna 1998como deveria?
Paul Spiegel
Sim, você está certo.
Emiliano
Esse código não apenas falha nos casos complexos, mas também falha nos casos mais simples, como as matrizes de entrada, $births = [3,1,2,1,3,3,2]e $deaths = [2,3,2,3,3,3]eu esperaria voltar 2como o ano com maior população, mas seu código retornará 1. Na verdade, seu código falhou em 9 de 15 dos meus testes de unidade . Não só não posso aceitar isso como a resposta mais eficiente, mas também não posso aceitá-la como uma resposta eficiente, pois não funciona de todo.
Sherif
Você não leu a pergunta com atenção e, portanto, não forneceu uma boa resposta. Você assume aqui que eu disse para não fazer ( que as matrizes são classificadas ). Portanto, remova seu comentário ofensivo na pergunta sobre como eu concedi a recompensa a uma resposta não eficiente e isso é de alguma forma uma " correção ".
Sherif
0

Uma das abordagens mais simples e claras para o seu 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);

saída :

1909

complexidade :

O(m + log(n))
Ronak Dhoot
fonte
para 1 milhão de registros, o tempo de execução é apenas29.64 milliseconds
Ronak Dhoot
Conforme indicado na pergunta, não estou buscando otimizações de tempo de execução, mas deve-se notar que o cálculo do Big O está um pouco desatualizado aqui. Além disso, seu código está um pouco quebrado. Ele falha em vários casos extremos.
Sherif