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.


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.


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.


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

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.


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.


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

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

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

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
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.