[アルゴリズム] ツリー配列データ構造

Part.I 予備知識

参考:
ツリー配列のシンプルで分かりやすい詳細解説

第I章 いくつかの前提と概念

  • コンピューターにおける負の数の 2 進数表現
  • 接頭辞の合計: 接頭辞の合計は、配列 (それ自体を含む) の添え字の前にあるすべての配列要素の合計を指します。プレフィックス合計は、1 次元のプレフィックス合計と 2 次元のプレフィックス合計に分けられます。プレフィックスの合計は、アルゴリズムの時間の複雑さを軽減できる重要な前処理です。たとえば、1 次元のプレフィックス合計の式は、sum[i] = sum[i-1] + arr[i] ; sumプレフィックス合計配列であり、arrは内容配列です。プレフィックスと配列を使用すると、O(1)の時間計算量で区間の合計を求めることができます。
  • サフィックスと:
  • 離散化: アルゴリズムの時空間効率を向上させるために、無限空間内の有限の個人を有限空間にマッピングします。平たく言えば、離散化とは、データの相対的なサイズを変更せずに、それに応じてデータを削減することです。離散化は、データがそれらの間の相対的なサイズにのみ関連しており、特定の数値とは関係がない場合に実行できます。4 つの数値があるため1234567, 123456789, 12345678, 123456、最初にそれらを並べ替えます123456<1234567<12345678<123456789 → 1<2<3<4。そのため、元のデータは次のようにマッピングできます2, 4, 3, 1

第Ⅱ章 ロービット関数

目的はさておき、まずこの関数がどのように計算されるかを理解してください。名前が示すように、lowbitこの関数の機能は、特定の数値のバイナリ表現で最下位ビットを見つけることです1。たとえば、x = 6バイナリ値が の場合110最後のビット 1 は 2 を意味するため、lowbit(x)を返します。2

どのように尋ねますlowbitか?一般に次の 2 つの方法があります。

  • まず最後の桁を削除し1(x & (x - 1)左側にx-1は影響しません)、次に元の数値から最後の桁を引きますたとえば、 8 ビット コンピューターのバイナリ表現はバイナリ表現はバイナリ表現は なのでそのバイナリ表現が必要なものになりますlowbit11x - (x & (x - 1))x = 2400001100x - 100001011x & (x - 1)00001000x - (x & (x - 1))00000100lowbit
  • 「コンピュータで負の数を表現する方法」(2の補数)によると、数値そのものとその数値の逆の論理積( )x & -xたとえば、 8 ビット コンピューターのx = 24バイナリ表現はであり00001100-xバイナリ表現は であり11110100x & -xバイナリ表現00000100は私たちが望むものですlowbit

Part.II ツリー配列

ツリー配列はデータ構造ですが、なぜそのようなデータ構造を構築するのでしょうか? これは、特定の問題を解決する上で独自の利点があるためです。このような問題を考えてみましょう。n長さ の配列がa[n]あり、それに対して「クエリ」(特定の間隔内のすべての要素の合計をクエリ)、「更新」(値を変更する)などの操作を実行したいとします。要素)。q次に、更新とクエリを実行したいと思います。この更新とクエリは散在しています。qqq


元のデータ構造を使用する場合、各「更新」の時間計算量は(値を直接O(1)変更したいため)、各「クエリ」の時間計算量は(数値の合計が必要なので、長さのループを実行するために必要です);ia[i]=valueO(n)nn


ツリー配列を使用すると、各「クエリ」の時間計算量を最小限に抑えることができますO(log(n))が、各「更新」の時間計算量も最小限に抑えることができますO(log(n))なぜ?理由については当面公表せず、今後詳細に分析する。


Chapter.I ツリー配列の考え方

最初に次の写真 ( Zhihu @orangebirdから)
ここに画像の説明を挿入
うまくいかない場合は、次の写真 ( CSDN@FlushHipから)

ここに画像の説明を挿入

ツリー状の配列構造(構造が数値に似ていて配列であるため、ツリー状配列と呼ばれます)はバイナリに基づいています。上の図を見ると、その概念がよくわかりますが、なぜこのように分けるべきでしょうか?これは上記の を使用します。長さ の配列lowbitを考えてみましょう。新しい配列は と呼ばれ、新しい配列は上の図の構成を通じて古い配列から取得されます。8ac

  • クエリ: たとえば、 を尋ねたい場合、sum(1:7)最初の72 進表現は です111∑ i = 1 n = 7 ai = ( a 1 + a 2 + a 3 + a 4 ) + ( a 5 + a 6 ) + a 7 \sum\limits_ {i=1}^{n=7}{a_i}=(a_1+a_2+a_3+a_4)+(a_5+a_6)+a_7i = 1n = 7ある私は=( _1+ある2+ある3+ある4+( _5+ある6+ある7、擬似コード (配列の添字はバイナリ) の書き方は次のとおりですsum(001:111)=c[111]+c[110]+c[100]。つまりsum(1:7)=c[7]+c[7-lowbit(7)]+c[6-lowbit(6)]、時間計算量は⌈ log 2 ( n ) ⌉ \lceil log_2(n) \rceilログ_ _2( n )⌉、つまりO(log n)
  • 更新: たとえばa[3]、変更したい値、最初の3バイナリ表現は0011、次に8長さの配列の場合、更新する必要がありますc[3], c[4], c[8]; 言い換えれば、 (バイナリ添字) を更新する必要がありますa[0011], a[0100], a[1000]; つまり、更新する必要がありますa[3], a[3+lowbit(3)], a[4+lowbit(4)]どうやら、時間の複雑さも同様ですO(log n)

上記は、ツリー配列の「クエリ」および「更新」操作の時間計算量が非常に高い理由を説明していますO(log n)
以前に質問がありました。では、なぜ同時に保存しないのですac? 更新操作を実行する場合は、 で直接実行しa、時間計算量は ですO(1)。クエリ操作を実行する場合は、cで実行し、時間計算量は ですO(log n)なお、「クエリ」と「更新」の操作は交互​​に行われ、aインターネット上で「更新」を行うと、cリファクタリング後に次の「クエリ」を実行しないと「新しい情報」が反映されませんが、cその処理時間は複雑になります。リファクタリングとはO(n)、こうすれば最適化によって孤独が最適化されます。

第II章 ツリー配列の構築

上記の説明に従って、次の関数を含むクラスを構築します。

  • lowbit: 整数を取得しますlowbit
  • BIT: コンストラクター、に従ってvector<int>初期化されます
  • update: 関数を更新し、i>0最初の数値を追加します。val
  • query: クエリ関数、m前の数値の合計を返します。
  • print: 出力tree
class BIT {
    
    
private:
    int n;              // the length of the tree
    vector<int> tree;   // the data tree

public:
    int lowbit(int x) {
    
     return x & -x; }

    BIT(vector<int> a)
    {
    
    
        n=a.size();
        vector<int>  temp(n,0);
        tree=temp;
        for(int i=0;i<n;i++)
        {
    
    
            update(i+1,a[i]);
        }
    }

    /**
     * @brief  updata the tree array
     * @param[in] i         the index, >=1
     * @param[in] val       the value of the update, =now-origin
     * @return              none
     */
    void update(int i, int val)
    {
    
    
        for(;i<=n;tree[i-1]+=val,i+=lowbit(i));
    }

    /**
     * @brief  query the summary of the first m terms
     * @param[in] m         the index, >=1
     * @param[out] sum      the sum
     * @return              int
     */
    int query(int m)
    {
    
    
        int sum=0;
        for(;m>0;sum+=tree[m-1],m-=lowbit(m));
        return sum;
    }

    void print()
    {
    
    
        for (int i = 0; i < n; cout << tree[i] << "    ", i++);
        cout << endl;
    }
};

呼び出し例:

int main()
{
    
    
    int test[7]={
    
    1,2,3,4,5,6,7};
    vector<int> origin(test, test + 7);
    BIT bt(origin);
    bt.print();                 // 打印 tree 的内容
    cout<<bt.query(5)<<endl;    // 输出前5项和
    bt.update(3,6);             // 第3项加6
    bt.print();                 // 打印更新后的 tree 的内容
    cout<<bt.query(5)<<endl;    // 输出更新后的前5项和
    getchar();
    return 0;
}
// ----------------- output ------------------
1    3    3    10    5    11    7
15
1    3    9    16    5    11    7
21

上記のコードは無料でダウンロードできます:ダウンロード アドレス

Part.III ツリー配列の応用

Chap.I LeetCode: 2426. 不等式を満たす数のペアの数

そう、疑問を練っていた時にこの疑問に出会ったから2426このnoteを書き、ついに牙を剥いたのです(RUA!!)。


セクション I トピックの説明と分析

まず、トピックの説明は次のとおりです。

添字が 0 から始まりnums1nums2両方の配列のサイズが である 2 つの整数配列が与えられ、n同時に整数が与えられdiff、次の条件を満たす数のペアを数えます(i, j)

  • 0 <= i < j <= n - 1
  • nums1[i] - nums1[j] <= nums2[i] - nums2[j] + diff

条件を満たすペアの数を返してください。

問題解決ビデオ: bilibili@林茶山艾府


トピック分析 ( に基づくpython):

  • まず、transpose: を実行しますnums1[i] - nums2[i] <= nums1[j] - nums2[j] + diff。これにより、その時点で満たされるすべてのデータ ペアnums[i] = nums1[i] - nums2[i] を見つけるだけで済みます0 <= i < j <= n - 1nums[i] <= nums[j] + diff(i, j)
  • nums[i]同じ値を持つ要素が存在することは避けられないため、それらを一意set性のために使用してから並べ替えることができますb
  • 離散化: ツリー状の配列を構築しますbt(すべての要素が 0 に初期化されます)。ツリー状の配列の長さは、numさまざまな要素の数に等しくなります (データのサイズに関係なく、非常に多くのグレードに分割するlen(set(nums))のと同等です) nums、データの相対的なサイズのみを考慮します。これは離散化です]、ツリー配列の各要素には、このレベルのデータの数が格納されます)。ツリー配列には 2 つの主な関数があります。1 つはadd(x)(xインデックスの値に 1 を追加します。ここでの値は上記の値ですAが、ツリー配列には が格納されるCため、複数の要素を変更する必要があります)、もう 1 つはquery(x)(findインデックス 以下のxすべてのデータの合計)。
  • ポインタiを使用して トラバース しnums、トラバース プロセス中にツリー配列を埋めますbt。ツリー配列x=nums[i]には、左側に各「グレード」要素の番号が格納されます。最初にそれを使用して、index=bisect_right(b, x + diff)要素の最小インデックス値以上をb見つけます。 x+diffto を使用し、それ以上の左側の要素の数の合計query(index)を Count します(つまり、と を満たすすべての数値の合計を見つけます)。nums[i]x+diffnums[m] <= nums[i] + diffm<im
  • 次に、要素が以下であるすべての要素の最大インデックス値を使用し(つまり、index2=bisect_left(b, x)対応する「グレード」インデックスを見つけます)、関数を使用してそれをツリー配列に追加し、次のエントリの準備をします。bxxadd(index2)query(i+1)
  • それらをすべて合計すると、query(index)必要なものが得られます

この質問ではツリー配列が使用されていますが、配列には要素の値ではなく要素の数が格納されることに注意してください。さらに、ツリー配列は一度に構築されるのではなく、クエリを走査して要素を追加するプロセス中に徐々に確立されます。この2点を知っておけば、動画の解説を見ると理解しやすいはずです。著者はこの考えを可能な限り整理しようとしましたが、振り返ってみると、まだ一口ですorz


セクション II コードの実装

以下は C++ コードの実装です。

class BIT {
    
    
private:
    int length=0;
    vector<int> tree;
public:
    BIT(int n)
    {
    
    
        length=n;
        vector<int> temp(n,0);
        tree=temp;
    }
    int lowbit(int x){
    
     return x & -x; }
    void add(int i)
    {
    
       // i=index+1,>=1
        while(i<=length){
    
     tree[i-1]++; i=i+lowbit(i); }
    }
    int query(int i)
    {
    
       // i=index+1,>=1
        int sum=0;
        while(i>0){
    
    
            sum+=tree[i-1];
            i-=lowbit(i);
        }
        return sum;
    }
};

class Solution {
    
    
public:
    long long numberOfPairs(vector<int>& nums1, vector<int>& nums2, int diff) {
    
    
        int n=nums1.size();
        vector<int> nums(n,0);
        for(int i=0;i<n;i++) {
    
     nums[i]=nums1[i]-nums2[i]; }
        vector<int> b(nums);
        sort(b.begin(),b.end());
        b.erase(unique(b.begin(),b.end()),b.end());
        BIT bt(b.size());
        long ans=0;
        for(int i=0;i<n;i++)
        {
    
    
            ans+=bt.query(upper_bound(b.begin(),b.end(),nums[i]+diff)-b.begin());
            bt.add(lower_bound(b.begin(),b.end(),nums[i])-b.begin()+1);
        }
        return ans;
    }
};

注目すべき点:

  • upper_bound(b.begin(),b.end(),val)この関数の機能は、bコンテナ内で要素値が以上である (データはすでに順序付けされている)val最小のインデックス イテレータ (ポインタとして理解できる) を見つけ*upper_bound(xx)、インデックスの要素値を返すことです。これはupper_bound(xx)-b.begin()インデックス値です
  • upper_bound(b.begin(),b.end(),val)この関数の機能は、b要素値がコンテナ内の要素値より小さい (データはすでに順序付けされているval) 最大のインデックス反復子 (ポインターとして理解できます) を見つけることです。その他の用途は同じです。upper_bound

以下は Python のコード実装です。

class BIT:
    def __init__(self,n: int):
        self.length=n
        self.tree=[0]*n
    def add(self, i: int):
        while(i<=self.length):
            self.tree[i-1]+=1
            i+=(i & -i)
    def query(self, i: int) -> int:
        sum=0
        while(i>0):
            sum+=self.tree[i-1]
            i-=(i & -i)
        return sum
 

class Solution:
    def numberOfPairs(self, nums1: List[int], nums2: List[int], diff: int) -> int:
        n=len(nums1)
        nums=[0]*n
        for i in range(n):
            nums[i]=nums1[i]-nums2[i]
        b=sorted(set(nums))
        bt=BIT(len(b))
        ans=0
        for i in range(n):
            ans+=bt.query(bisect_right(b,nums[i]+diff))
            bt.add(bisect_left(b,nums[i])+1)
        return ans

第II章 LeetCode: 51. 配列内の逆ペア

この質問は、すでに「Jianzhi Offer」に含まれているため、古典的な質問になるはずです。実際には上記のものと非常に似ていますが、それよりも単純です。したがって、以下では分析しません。解決策を投稿するだけです


Python ベースのコードは次のとおりです。

class Solution:
    def reversePairs(self, nums: List[int]) -> int:
        b = sorted(set(nums))
        ans = 0
        n = len(b)
        bt = BIT(n)
        for x in nums:
            temp=n-bisect_left(b, x)
            ans += bt.query(temp-1)
            bt.add(temp)
        return ans

class BIT:
    def __init__(self,n: int):
        self.length=n
        self.tree=[0]*n
    def add(self, i: int):
        while(i<=self.length):
            self.tree[i-1]+=1
            i+=(i & -i)
    def query(self, i: int) -> int:
        sum=0
        while(i>0):
            sum+=self.tree[i-1]
            i-=(i & -i)
        return sum

おすすめ

転載: blog.csdn.net/Gou_Hailong/article/details/127717773