web-dev-qa-db-ja.com

O(n)で、すべてのメンバーがリストに含まれている最大の区間を見つけます

面接で聞いた。整数のリストが与えられた場合、与えられたリストにすべてのメンバーが含まれる最大の区間をどのように見つけることができますか?

例えば。リスト1、3、5、7、4、6、10が与えられた場合、答えは[3、7]になります。それは3と7の間のすべての要素を持っているからです。

私は答えようとしましたが、説得力がありませんでした。私が採用したアプローチは、最初にリストを並べ替えてから、最大の間隔でリストをチェックすることでした。しかし私はO(n)でそうするように頼まれました。

40
Jayram

私はハッシュと動的計画法に基づく解決策を知っています。 f(x)をハッシュ関数とします。秘訣はハッシュテーブルの値です。リストに含まれる最長の間隔で、xで開始または終了することを考慮してください。次にh [f(x)] = y、ここでyその間隔のもう一方の端。その間隔の長さはabs(x --y)+1。アルゴリズムの説明により、その値を格納する理由が明確になります。

リストの上に移動します。 iを現在のインデックス、x:= list [i]-現在の番号。今

1。h [f(x)]が空でない場合は、以前に番号xに会いました。何もすることはありません、続けてください。

2。チェックh [f(x-1)]およびh [f(x + 1)]

2.1。両方が空でない場合、それはすでに会ったことを意味しますx-1およびx + 1、そしていくつかの間隔を知っています[a..x-1]および[x + 1..b ]これはすでにリストで出会っています。 a= h [f(x-1)]およびb= h [f(x + 1) ]hの定義による。 xを取得すると、間隔全体が満たされたことを意味します[a、b]したがって、値を次のように更新します。h [f(a)]:=bおよびh [f( b)]:=a
また、h [f(x)]をある値に設定します(たとえばx、答えに影響を与えない)、次回会うときにx リストでは、無視します。 xはすでに彼の仕事をしています。

2.2。 1つだけが設定されている場合、たとえばh [f(x-1)] =a、つまり、すでに一定の間隔に達していることを意味します[a。 x-1]、そして今ではxで拡張されています。更新はh [f(a)]:=xおよびh [f(x)]:=a

2.3。いずれも設定されていない場合は、どちらにも会っていないことを意味しますx-1 =、またはx + 1、およびを含む最大の間隔xすでに会ったのはシングルです[x]それ自体。したがって、h [f(x)]:=x

最後に、答えを得るには、リスト全体を渡して、maximumabs(x-h [f(x)])+1すべてのx

35

並べ替えが望ましくない場合は、ハッシュマップと 素集合データ構造 の組み合わせを使用できます。

リスト内の要素ごとにノードを作成し、key =要素の値を使用してハッシュマップに挿入します。次に、ハッシュマップでvalue +1とvalue-1をクエリします。何かが見つかった場合は、現在のノードを隣接ノードが属するセットと組み合わせます。リストが終了すると、最大のセットが最大の間隔に対応します。

時間計算量はO(N *α(N))です。ここで、α(N)は逆アッカーマン関数です。

編集:実際には素集合はこの単純なタスクには強力すぎます。 GrigorGevorgyanによるソリューションはそれを使用しません。したがって、よりシンプルで効率的です。

8
Evgeny Kluev

スペースをトレードオフして、線形時間でこれを取得できます。

  1. リストをスキャンして、最小値と最大値SおよびLを探します。
  2. (L --S + 1)エントリを保持するのに十分な大きさのブール値の配列またはビットベクトルAを使用します。
  3. リストをもう一度確認し、Aの適切な要素が表示されたらtrueに設定します。
  4. ここで、Aがソートされます。 Aを調べて、真の値の最大の連続セットを見つけます。

最初のステップはリスト内で直線的です。最後の値はAのサイズが線形であり、遠く離れた値がいくつかある場合は、リストに比べて大きくなる可能性があります。しかし、intを扱っているので、Aは有界です。

5
Dave

HashSetを使用して非常に簡単なソリューションを作成しました。 containsremoveはO(1)操作であるため、ランダムなセット項目から新しい間隔を作成し、間隔を「拡張」するだけです。あなたはそのフルサイズを発見し、あなたが進むにつれてセットからアイテムを削除します。これがあなたがどんな間隔でも「繰り返す」ことを妨げるので、削除は重要です。

このように考えると役立つ場合があります。リストにはK個の間隔があり、そのサイズは合計でNになります。次に、間隔や項目を繰り返さずに、これらの間隔が何であるかを検出することがタスクです。これが、HashSetが仕事に最適な理由です。間隔を広げると、セットからアイテムを効率的に削除できます。次に、あなたがする必要があるのは、あなたが進むにつれて最大の間隔を追跡することです。

  1. リストをHashSetに入れます
  2. セットが空でない間:
    1. セットからランダムにアイテムを削除します
    2. そのアイテムから新しい間隔を定義します
    3. 次のように間隔を拡大します:
      1. i = interval.start-1を定義します
      2. セットにiが含まれている間に、セットからiを削除し、iinterval.startの両方をデクリメントします。
      3. 手順2を反対方向に繰り返します(interval.endから展開します)
    4. 拡張された間隔が以前の最大間隔よりも大きい場合は、新しい間隔を最大間隔として記録します
  3. 最大間隔を返す

Javaでの解決策は次のとおりです。

public class BiggestInterval {

    static class Interval {
        int start;
        int end;

        public Interval(int base) {
            this(base,base);
        }

        public Interval(int start, int end) {
            this.start = start;
            this.end = end;
        }

        public int size() {
            return 1 + end - start;
        }

        @Override
        public String toString() {
            return "[" + start + "," + end + "]";
        }
    }

    /**
     * @param args
     */
    public static void main(String[] args) {
        System.out.println(biggestInterval(Arrays.asList(1,3,5,7,4,6,10)));
    }

    public static Interval biggestInterval(List<Integer> list) {
        HashSet<Integer> set = new HashSet<Integer>(list);
        Interval largest = null;

        while(set.size() > 0) {
            Integer item = set.iterator().next();
            set.remove(item);

            Interval interval = new Interval(item);
            while(set.remove(interval.start-1)) {
                interval.start--;
            }
            while(set.remove(interval.end+1)) {
                interval.end++;
            }

            if (largest == null || interval.size() > largest.size()) {
                largest = interval;
            }
        }

        return largest;
    }
}
2
Kevin K

これがGrigorのソリューションに似たソリューションです。 2つの主な違いは、このソリューションが他のインデックスの代わりにシーケンシャルセットの長さを格納することと、これにより最後のハッシュセットの反復が不要になることです。

  1. 配列を反復処理します

    • 隣接するセットエンドポイントを探して更新することにより、ハッシュマップを作成します。

      Key-配列値

      Value-キーがシーケンシャルセットのエンドポイントである場合、そのセットの長さを格納します。それ以外の場合は、物事を一度だけ検討するように、それを真実に保ちます。

    • 現在のセットサイズが最も長い場合は、最長のセットサイズと最長のセット開始を更新します。

わかりやすくするためのJavaScriptの実装と、実際の動作を確認するための fiddle を次に示します。

var array = [1,3,5,7,4,6,10];

//Make a hash of the numbers - O(n) assuming O(1) insertion
var longestSetStart;
var longestSetSize = 0;

var objArray = {};
for(var i = 0; i < array.length; i++){
    var num = array[i];

    if(!objArray[num]){//Only consider numbers once
        objArray[num] = 1;//Initialize to 1 item in the set by default

        //Get the updated start and end of the current set
        var currentSetStart = num;//Starting index of the current set
        var currentSetEnd = num;//Ending index of the current set

        //Get the updated start of the set
        var leftSetSize = objArray[num - 1];
        if(leftSetSize){
            currentSetStart = num - leftSetSize;
        }

        //Get the updated end of the set
        var rightSetSize = objArray[num + 1];
        if(rightSetSize){
            currentSetEnd = num + rightSetSize;
        }

        //Update the endpoints
        var currentSetSize = currentSetEnd - currentSetStart + 1;
        objArray[currentSetStart] = currentSetSize;
        objArray[currentSetEnd] = currentSetSize;

        //Update if longest set
        if(currentSetSize > longestSetSize){
            longestSetSize = currentSetSize;
            longestSetStart = currentSetStart;
        }
    }
}

var longestSetEnd = longestSetStart + longestSetSize - 1;
1
Briguy37

これは、平均O(1)ハッシュテーブルで作成された辞書を考慮すると線形になります。

L = [1,3,5,7,4,6,10]

a_to_b = {}
b_to_a = {}

for i in L:
    if i+1 in a_to_b and i-1 in b_to_a:
        new_a = b_to_a[i-1]
        new_b = a_to_b[i+1]
        a_to_b[new_a] = new_b
        b_to_a[new_b] = new_a
        continue
    if i+1 in a_to_b:
        a_to_b[i] = a_to_b[i+1]
        b_to_a[a_to_b[i]] = i
    if i-1 in b_to_a:
        b_to_a[i] = b_to_a[i-1]
        a_to_b[b_to_a[i]] = i
    if not (i+1 in a_to_b or i-1 in b_to_a):
        a_to_b[i] = i
        b_to_a[i] = i

max_a_b = max_a = max_b = 0
for a,b in a_to_b.iteritems():
    if b-a > max_a_b:
        max_a = a
        max_b = b
        max_a_b = b-a

print max_a, max_b  
1
akalenuk

秘訣は、アイテムをリストではなくセットとして考えることです。セットを使用すると、item-1またはitem + 1が存在するかどうかを確認できるため、これにより、連続する範囲の開始または終了にあるアイテムを識別できます。これにより、線形時間と空間で問題を解決できます。

擬似コード:

  • セット内のアイテムを列挙し、範囲の先頭にあるアイテムを探します(x-1がセットにない場合、xは範囲を開始します)。
  • 範囲の開始である各値について、対応する範囲の終了値が見つかるまで上方向にスキャンします(x + 1がセットにない場合、xは範囲を終了します)。これにより、関連するすべての連続した範囲が得られます。
  • 終了が最初から最も遠い連続範囲を返します。

C#コード:

static Tuple<int, int> FindLargestContiguousRange(this IEnumerable<int> items) {
    var itemSet = new HashSet<int>(items);

    // find contiguous ranges by identifying their starts and scanning for ends
    var ranges = from item in itemSet

                 // is the item at the start of a contiguous range?
                 where !itemSet.Contains(item-1)

                 // find the end by scanning upward as long as we stay in the set
                 let end = Enumerable.Range(item, itemSet.Count)
                           .TakeWhile(itemSet.Contains)
                           .Last()

                 // represent the contiguous range as a Tuple
                 select Tuple.Create(item, end);

     // return the widest contiguous range that was found
     return ranges.MaxBy(e => e.Item2 - e.Item1);
}

注:MaxByは MoreLinq からのものです

テスト

小さなサニティチェック:

new[] {3,6,4,1,8,5}.FindLargestContiguousRange().Dump();
// prints (3, 6)

大きな連続リスト:

var zeroToTenMillion = Enumerable.Range(0, (int)Math.Pow(10, 7)+1);
zeroToTenMillion.FindLargestContiguousRange().Dump();
// prints (0, 10000000) after ~1 seconds

大きな断片化されたリスト:

var tenMillionEvens = Enumerable.Range(0, (int)Math.Pow(10, 7)).Select(e => e*2);
var evensWithAFewOdds = tenMillionEvens.Concat(new[] {501, 503, 505});
evensWithAFewOdds.FindLargestContiguousRange().Dump();
// prints (500, 506) after ~3 seconds

複雑さ

このアルゴリズムには、O(N)時間とO(N)スペースが必要です。ここで、Nはリスト内のアイテムの数です。ただし、セット操作は次のようになります。一定の時間。

セットが入力として指定された場合、アルゴリズムによって構築されるのではなく、O(1)スペースのみが必要になることに注意してください。

(一部のコメントでは、これは2次時間であると言われています。範囲の先頭にあるアイテムだけでなく、すべてのアイテムがスキャンをトリガーすると想定していました。アルゴリズムがそのように機能した場合、実際には2次になります。)

0
Craig Gidney