【问题标题】:Find the year with the highest population (most efficient solution)查找人口最多的年份(最有效的解决方案)
【发布时间】:2020-06-07 10:42:15
【问题描述】:

给定两个数组; $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 |

假设

我们可以有把握地假设,某人出生的那一年人口可以增加 1,而某人死亡的那一年,人口可以减少 1。所以在这个例子中,1984 年有 2 人出生,1984 年有 1 人死亡,这意味着该年人口增加了 1。

我们还可以安全地假设死亡人数永远不会超过出生人数,并且当人口为 0 时不会发生死亡。

我们还可以安全地假设 $deaths$births 中的年份永远不会是负数或浮点值(它们总是大于 0 的正整数)。

但是,我们不能假设数组会被排序或者不会有重复的值。

要求

给定这两个数组作为输入,我们必须编写一个函数来返回人口最多的年份。如果输入数组为空或人口始终为 0,则该函数可能返回 0false""NULL任何错误值都是可接受的)。如果人口最多的年份出现在多个年份,则该函数可能会返回达到最高人口的第一年或任何后续年份。

例如:

$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 是死亡年数。但是,我不确定是否有更有效的解决方案。

我的兴趣纯粹是在现有算法的基础上改进计算复杂度的大 O。内存复杂度无关紧要。运行时优化也不是。 至少这不是主要问题。欢迎任何次要/主要的运行时优化,但这不是这里的关键因素。

【问题讨论】:

  • 既然您有一个可行的解决方案,这是否更适合codereview.stackexchange.com
  • 问题是寻求最有效的解决方案,不一定是任何可行的解决方案。我认为这在 SO 上是完全有效的。
  • 我并不是说它在 SO 上无效(在这种情况下我会投票关闭),我只是想知道您是否会在 CR 上得到更多回应。
  • @NigelRen 我不认为尝试有什么害处。虽然我想让这个开放几天。如果没有得到答案,我会悬赏。
  • 如果您搜索生死关键词,SO 本身就有很多问题。一个廉价的改进是改进排序:使长度数组成为出生/死亡的跨度(每个单元格都是默认值 0 的日期)。关于生死的单元格加1或减1,然后累积求和并保持找到的最大和

标签: php arrays algorithm language-agnostic


【解决方案1】:

我们可以使用桶排序在线性时间内解决这个问题。假设输入的大小是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)。

如果 m 为 O(n),则这是 n 中的线性解;即,如果年份范围的增长速度不快于出生和死亡人数。对于现实世界的数据,这几乎可以肯定是正确的。

【讨论】:

  • @Sherif 实现留给读者作为练习......无论如何它都是微不足道的。有什么不清楚的吗?
  • 我会注意到,因为您的粒度是年份,所以存在一些歧义。因为我们有效地测量了截至年底的人口,并且由于出生和死亡的时间,可能在年中的某个其他时间点人口更高。
  • 这里没有歧义。正如问题中明确指出的那样,您可以放心地假设某人出生或死亡的年份是那一年。
  • 如果我们必须解析“大小为 max_yr - min_yr + 1 的数组”,这个线性时间是多少? (抄送@Sherif)
  • @Dave:第 1 点和第 2 点的复杂度不是 O(2n) 吗? 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): Parse您的数组,跟踪累积总和及其最大值。 (你不需要解析这个数组——你可以在增加 2 中的索引时跟踪 MAX)
【解决方案2】:

我认为我们可以通过首先排序来获得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) 时间复杂度。如果我们想变得花哨,我们还可以使用Y-fast trie 来获得O(n * log log m) 时间复杂度,该Y-fast trie 允许在O(log log m) 时间中查找后继。

【讨论】:

  • 1.谢谢你教我存在 Y-fast trie。关于算法:减少后无需检查最大值。只有在增加之后。最后一个 while 块是不必要的:考虑对两个排序列表进行排序:您只需要两个 (i,j) 的头部,选择每个的头部,然后推进较小的一个。 if(birth_i &lt; death_j){//increment stuff + check max} else{//decrement}; birth_i||=infty; death_j||=infty。你也可以迭代到min(birthSize, deathSize)。如果 min 是出生,停止。如果 min 是死亡(可疑..),请停止并检查 (max + birth.length-i)
  • @grodzi 我确实开始考虑合并排序,但得出的结论是这需要额外处理,因为重复以及出生与死亡的顺序如何影响计数。当死亡年份与出生年份不匹配时,最后一个 while 循环对我来说似乎是必要的。您是正确的,该循环中的最大值是不必要的。
  • @גלעדברקן 对线性时间使用桶排序。
  • 我已经在我的回答中说明了这个想法,“如果年份范围 m 大约为 n,我们可以将每年的计数存储在该范围内并有 O(n) 时间复杂性。”
  • 这不是效率,不知道为什么给你打赏哈哈哈
【解决方案3】:

首先将出生和死亡汇总到一张地图 (year =&gt; 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); 处理出生。
【解决方案4】:

我用O(n+m)的内存需求解决了这个问题[在最坏的情况下,最好的情况下O(n)]

并且,O(n logn) 的时间复杂度。

这里,n &amp; mbirthsdeaths 数组的长度。

我不知道 PHP 或 javascript。我用Java实现了,逻辑很简单。但我相信我的想法也可以用这些语言实现。

技术细节:

我使用javaTreeMap结构来存储出生和死亡记录。

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

这里,上一个出生年份 1913 之后发生的死亡 (1914 &amp; later) 根本不计算在内,这样可以避免不必要的计算。

对于总共10 million 数据(出生和死亡合并)和超过1000 years range,该程序大约需要3 sec. 才能完成。

如果与100 years range 相同大小的数据,则使用1.3 sec

所有输入都是随机取的。

【讨论】:

    【解决方案5】:
    $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);
    

    这将考虑到平年的可能性,以及如果某人死亡的年份与某人的出生不对应。

    【讨论】:

    • 此答案未尝试提供 OP 要求的学术大 O 解释。
    【解决方案6】:

    在内存方面保持currentPopulationcurrentYear 的计算是明智的。从对$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;
            }
    

    【讨论】:

    【解决方案7】:

    我对这个解决方案很满意,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。事实上你的代码在我的 15 个单元测试中有 9 个失败。我不仅不能接受这是有效的答案,而且我什至不能接受它一个有效的答案,因为它根本不起作用。跨度>
    • 您没有仔细阅读问题,因此未能提供好的答案。你在这里做了我告诉你不要做的假设(数组是排序的)。因此,请删除您在关于我如何将赏金授予无效答案的问题中的冒犯性评论,这在某种程度上是“修复”。
    【解决方案8】:

    解决您的问题的最简单明了的方法之一。

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

    【讨论】:

    • 100 万条记录的执行时间只是29.64 milliseconds
    • 如问题中所述,我不在运行时优化之后,但应注意您的 Big O 计算在这里略有偏差。此外,您的代码略有损坏。它在许多边缘情况下都失败了。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多