找出人口最多的年份(最有效的解决方案)

12

给定两个数组; $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个人出生,1个人死亡,意味着那一年人口增加了1。

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

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

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

要求

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

例如:

$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(大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),则上述算法应该在多项式时间内工作。但是,我不确定是否存在更有效的解决方案。

我的兴趣纯粹在于改进现有算法的计算复杂性的大O表示法。内存复杂性不是问题。运行时优化也不是主要关注点。至少这不是关键因素。任何次要/主要运行时优化都可以接受,但这不是关键因素。


2
由于您已经有了一个可行的解决方案,是否更适合将其提交到https://codereview.stackexchange.com/? - Nigel Ren
2
这个问题是在寻求最有效的解决方案,而不一定是任何可行的解决方案。我认为这在SO上完全合理。 - Sherif
1
我并不是说这个问题在SO上无效(如果是的话,我会投票关闭),我只是想知道你是否可以在CR上获得更多的回应。 - Nigel Ren
1
如果您搜索出生和死亡关键字,SO本身就有很多与您问题相关的问题。一个简单的改进是改善排序:创建一个长度为出生/死亡跨度的数组(每个单元格默认为0),并根据出生和死亡情况添加1或减去1。然后累加求和并保留找到的最大总和。 - grodzi
你标记了 PHP,但我不会用 PHP 发布。我也给了你排序的方法。让我用一个更清晰的例子来坚持说明。拿 24310 举例,你想将其排序为 01234。那么就创建一个大小为 5 的数组,并将数字 i 分配给 v[i]。2 被分配给 v[2],4 被分配给 v[4]……有点像计数排序。这是 O(n) 的时间复杂度。 - grodzi
显示剩余10条评论
8个回答

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

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


1
@Sherif 实现留给读者作为练习... 不过这很简单。有什么不清楚的吗? - Dave
1
如果我们需要解析一个“大小为max_yr-min_yr + 1的数组”,那么这怎么可能是线性时间呢?(@Sherif) - גלעד ברקן
@גלעדברקן 我提出了一个假设,即年份范围相对于出生和死亡人数来说很小。即使它们相等,每年平均有一次出生或死亡,也是线性的。我们需要非常稀疏的年份才能使其次线性。 - Dave
@Dave 没错,这就是我们想要的。唯一的要求是年末总人口的最大值。 - Sherif
1
@Dave:对于点1和点2,复杂度不是O(2n)吗?1. 遍历所有出生和死亡事件一次:O(n): 找到跨越出生和死亡的最小和最大年份 2. 再次遍历所有出生和死亡事件:O(n): 解析出生和死亡数组,增加相应索引的值然后你做:O(m): 解析你的数组,跟踪累积总和及其最大值。*(你不需要解析这个数组 - 你可以在增加第2个索引时跟踪MAX)* - Antony
显示剩余10条评论

3
我认为我们可以通过先排序,然后在迭代过程中维护当前人口和全局最大值,以 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));

如果年份范围mn数量级相当,我们可以在该范围内存储每个年份的计数,并具有O(n)时间复杂度。如果我们想要更高级的方法,我们还可以使用Y-fast trie,使后继查找时间复杂度为O(log log m),从而实现O(n * log log m)时间复杂度。


使用桶排序以实现线性时间。 - Dave
我在我的回答中已经提出了这个想法:“如果年份范围m与n的数量级相同,我们可以存储该范围内每年的计数,并具有O(n)的时间复杂度。” - גלעד ברקן
@Emiliano,我的回答包含三种回答问题的方法描述,只有其中一种提供了代码。您认为哪种方法最有效,并且此页面上是否有描述它的答案? - גלעד ברקן
@Emiliano 首先,这是我的悬赏并且我有权将其授予任何我选择的人。第二,我选择了我认为对我所有要求都回答得最好的答案。第三,在其他任何答案中,我没有看到任何改进已授予答案(它们都是相同的想法或杂乱无章的实现,没有解决全局算法问题)。任何类似但是在选择的答案后发布的答案我都会称赞,但如果授予这些答案的话似乎不公平,因为这个是第一个被选中的。 - Sherif
@Emiliano 最后,我特意给你的回答点了个踩,因为它并不起作用。我在你的回答下面的评论中详细地指出了它的问题所在。它无法通过超过一半的单元测试。一个甚至不能正常工作的答案肯定是低效的,而且你还没有解决算法问题,Big O 的计算也有误。我曾因为更小的问题而给别人的回答点过踩。 - Sherif
显示剩余3条评论

3
首先将出生和死亡率汇总到一个映射表中(年份 => 人口变化),按键排序,然后计算其人口数量。
这大约需要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))就变得无关紧要了。 - Paul Spiegel
你可以使用 $indexed = array_count_values($births); 处理出生数据。 - Nigel Ren

3
我使用了内存需求为O(n+m)(最坏情况下,最好情况为O(n)),时间复杂度为O(n logn)的方法解决了这个问题。
这里的n和mbirthsdeaths数组的长度。
我不懂PHP或JavaScript。我用Java实现了它,逻辑非常简单。但我相信我的想法也可以在这些语言中实现。 技术细节: 我使用了Java的TreeMap结构来存储出生和死亡记录。 TreeMap将数据按键排序(基于键)插入为(key, value)对,这里的键是年份,值是出生和死亡的累计总数(死亡为负数)。
我们不需要插入发生在最高出生年份之后的死亡值。
一旦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年及以后)在最后一个出生年1913年之后没有被计算,这避免了不必要的计算。
针对总共1000年时间范围内的1000万条数据(包括出生和死亡),该程序需要大约3秒钟才能完成。
如果是同样大小但只有100年时间范围的数据,则仅需1.3秒。
所有输入都是随机选择的。

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

这将考虑到年份相同的可能性,以及某人死亡的年份与某人的出生不对应的情况。

该回答没有试图提供OP所要求的学术Big O解释。 - mickmackusa

0

我对这个解决方案感到非常满意,时间复杂度为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 - Paul Spiegel
这段代码不仅在复杂的边缘情况下失败,甚至在最简单的情况下也会失败,例如给定输入数组 $births = [3,1,2,1,3,3,2]$deaths = [2,3,2,3,3,3],我期望得到 2 作为最高人口年份,但是你的代码返回了 1。事实上,你的代码在我的15个单元测试中有9个失败了。我不仅不能接受这个作为有效的答案,而且我甚至不能接受它作为一个有效的答案,因为它根本不起作用。 - Sherif
你没有仔细阅读问题,因此未能提供一个好的答案。你在这里做出了一个假设,即我告诉你不要做(数组已排序)。所以请删除你在问题中关于我将赏金授予一个非高效答案并且这是某种“修复”的冒犯性评论。 - Sherif

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

对于100万条记录,执行时间仅为“29.64毫秒”。 - Ronak Dhoot
正如问题所述,我不是追求运行时优化,但应该注意到你的大 O 计算略有偏差。此外,你的代码还存在一些小问题。它在一些边缘情况下会失败。 - Sherif

0
内存方面,要保持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;
        }

数组移位对于内存来说是一个不错的选择,但对于性能来说则不太好。请参考此链接:https://cmljnelson.blog/2018/10/16/phps-array_shift-performance/ - Emiliano
你可以始终采用降序排序,而不是升序排序,使用弹出操作而不是移除操作。 - yergo

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接