最も人口の多い年を見つける(最も効率的なソリューション)


9

2つの配列があるとします。$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より大きい正の整数です)。

私たちはできません配列がソートされることまたはただし、重複する値がないことを前提としています。

必要条件

これらの2つの配列を入力として、最大人口が発生した年を返す関数を作成する必要があります。この関数は返すことがあり0false""、またはNULL任意falsey値が許容される)入力配列が空の場合、または集団全体で0に常にあった場合。最高の人口が複数年に発生した場合、関数は最高の人口に達した最初の年またはそれ以降の年を返すことがあります。

例えば:

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

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

さらに、ソリューションのビッグ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)で、誕生年の数(常にそれわかっているため)が死年の数である場合、多項式時間で機能するはずです。ただし、より効率的な解決策があるかどうかはわかりません。nkkk >= yy

私の興味は、純粋に既存のアルゴリズムの計算の複雑さの改善されたBig Oにあります。メモリの複雑さは問題ではありません。ランタイムの最適化も同様です。少なくともそれは主要な問題ではありません。マイナー/メジャーランタイムの最適化は歓迎されますが、ここでの重要な要素ではありません。


2
実用的なソリューションがあるので、これはcodereview.stackexchange.comに適していますか?
Nigel Ren

1
質問は、最も効率的なソリューション、必ずしも求めているすべての作業溶液を。それはSOでは完全に有効だと思います。
シェリフ

1
SOでは有効ではないと言っているわけではありません(その場合は終了するように投票したでしょう)。
Nigel Ren

@NigelRen試しても害はありません。私はこれを数日間開いたままにしたいと思いますが。回答が得られない場合は、賞金を差し上げます。
シェリフ

1
SO自体には、出生死亡のキーワードを検索する場合、多くの問題の質問があります。安価な改善は、ソートを改善することです:長さの配列を誕生/死のスパンにします(各セルはデフォルトで値0を保持する日付です)。生と死に関するセルに1を加えるか1を
引いて

回答:


4

最初に並べ替えてから、現在の人口とグローバルな最大値を維持することで、追加のスペースで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高速トライO(n * log log m)使用して、時間を複雑にすることもできます。O(log log m)


1. Yファーストトライの存在を教えてくれたthx。アルゴに関して:減少した後に最大値をチェックする必要はありません。増分後のみ。最後に、ブロックは不要です。2つのソート済みリストをソートすることを検討してください。両方(i、j)のヘッドが必要で、それぞれのヘッドを選択して、小さい方を進めます。if(birth_i < death_j){//increment stuff + check max} else{//decrement}; birth_i||=infty; death_j||=infty。また、最大まで繰り返すことができますmin(birthSize, deathSize)。分が出産なら、やめなさい。分は死がある場合(不審な...)、停止およびチェック(max + birth.length-i)
grodzi

@grodziマージソートを検討することから始めましたが、重複と、誕生と死亡の順序がカウントにどのように影響するかにより、これには追加の処理が必要であると結論付けました。最後のwhileループは、誕生年に匹敵しない死年があるとき、私には必要なようです。あなたはそのループの最大値が不必要であることは正しいです。
גלעדברקן

@גלעדברקן線形時間にバケットソートを使用します。
デイブ

「年の範囲mがnのオーダーである場合、範囲内の各年のカウントを格納でき、O(n)の時間の複雑さを持つ可能性があります。」
גלעדברקן

ハハハあなたに報酬を与える理由は、これは効率ではない、私は知らない
エミリアーノ

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
動作する実装を含めていただけますか?
シェリフ

1
@Sherifの実装は読者の練習問題として残されています...とにかく簡単です。不明な点はありますか?
デイブ

粒度が年であるため、あいまいさがいくつかあることに注意します。年末の時点で人口を効果的に測定しているという点で、出生と死亡のタイミングのために人口が多い年の途中の別の時点があるかもしれません。
デイブ

1
「サイズがmax_yr-min_yr + 1の配列」を解析する必要がある場合、この線形時間はどうですか?(cc @Sherif)
ברקןברקן

1
@Dave:ポイント1および2の複雑度はO(2n)ではありませんか? 1.すべての出産+死を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):配列を解析し、累積合計とその最大値を追跡します。(この配列を解析する必要はありません-2のインデックスをインクリメントしながらMAXを追跡できます)
アントニー

3

最初に出生と死亡をマップ(year => 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);
Nigel Ren

3

私はこの問題をO(n+m)[最悪の場合、最良の場合O(n)]のメモリ要件で解決しました

そして、時間の複雑さO(n logn)

ここで、およびn & mの長さはです。birthsdeaths

PHPやJavaScriptがわかりません。私はそれをJavaで実装しており、ロジックは非常に簡単です。しかし、私の考えはそれらの言語でも実装できると思います。

テクニックの詳細:

Java TreeMap構造を使用して、出生および死亡の記録を保存しました。

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

ここでは、死は(1914 & later)最後の誕生年の後に発生しましたが1913、まったく数えられなかったため、不要な計算が回避されています。

合計10 millionデータ(出生と死亡を合わせたもの)以上の1000 years range場合、プログラムは3 sec.終了するまでかかりました。

と同じサイズのデータ​​なら100 years range、かかりました1.3 sec

すべての入力がランダムに取得されます。


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

メモリに関しては、それを維持currentPopulationしてcurrentYear計算することです。バブルのソートはそれほど重い作業ではありませんが、いくつかのコーナーをカットすることができるため、両方$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ステートメントに変更して、1つのループだけで終了できます。

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

配列シフトはメモリにとっては良いオプションですが、パフォーマンスについてはそうではありません。このcmljnelson.blog/2018/10/16/phps-array_shift-performanceを
Emiliano

常に降順で並べ替えたり、増分ではなく減分を使用したり、シフトではなくポップを使用したりできます。
yergo

0

私はこのソリューションに非常に満足しています。複雑さの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ますか?
Paul Spiegel

そうそう。
エミリアーノ

このコードは、複雑なエッジケースでは失敗していないだけではなく、入力配列を与えられたようにそれも例最も単純に失敗$births = [3,1,2,1,3,3,2]し、$deaths = [2,3,2,3,3,3]私は戻って取得することが期待される2最も高い人口の年として、まだあなたのコードを返します1。実際、あなたのコードは私のユニットテストの15回のうち9回は失敗しました。私はこれを最も効率的な答えとして受け入れることができないだけでなく、まったく機能しないため効率的な答えを受け入れることさえできません。
シェリフ

質問を注意深く読むことができなかったため、適切な回答を提供できませんでした。ここでは、私がしないように言った(配列はソートされている)と仮定します。それで、非効率的な答えに賞金をどのように与えたかについての質問の中のあなたの不快なコメントを削除してください、これはどういうわけか「修正」です。
シェリフ

0

問題に対する最も単純で明確なアプローチの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];

/* 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
Ronak Dhoot

質問で述べたように、私はランタイムの最適化の後ではありませんが、Big Oの計算はここでは少しずれていることに注意してください。また、コードがわずかに壊れています。これは、多くのエッジケースで失敗します。
シェリフ
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.