樹狀陣列和逆序對

ailanxier發表於2020-08-05

逆序對的概念

  在一個有 \(n\) 個元素的陣列 \(A\) 中,如果存在 \(1 \leqslant i <j \leqslant n\) ,使得 \(A_i > A_j\) ,則稱 \(<A_i,A_j>\)\(A\) 的一個逆序對。我們熟知的排序其實就是一個消滅逆序對的過程。求一個陣列的逆序對數目,我們可以用歸併排序,或者用我們今天的主角樹狀陣列,還不會樹狀陣列的同學可以看我之前的一篇學習筆記的部落格(戳這裡),很快就可以理解了。

樹狀陣列打逆序對

  兩題都是求逆序對的模板題,洛谷資料被加強過,而且數字範圍更大,如果用樹狀陣列做需要進行離散化操作,而歸併排序不用,這也是歸併排序更快的原因。但是有的時候歸併排序不能維護一些區間資訊(見下一題)。
  先來分析一下逆序對應該怎麼數。把逆序對的概念換一種更通俗的說法,逆序對其實就是由一個數和之前比它大的數組成。樸素的做法就是在每一個數插入的時候,遍歷它前面的每一個數,如果比它大答案就加一。這種做法顯然是 \(O(n^2)\) 的,而 \(n\) 的範圍是 \(10^5\) 級別的,肯定衝不過去。為了把時間複雜度降到 \(O(nlogn)\) 級別,我們需要使用線段樹和樹狀陣列來維護。線段樹在這裡就有點麻煩了,我們用更簡潔的樹狀陣列就可以了。每插入一個數,是單點修改;每查詢一個數之前比它大的數目,是區間查詢,可行!
  樹狀陣列傳統程式碼,修改和查詢完全不變:

#include<bits/stdc++.h>
using namespace std;
#define For(i,sta,en) for(int i = sta;i <= en;i++)
#define lowbit(x) x&(-x)
#define speedUp_cin_cout ios::sync_with_stdio(false);cin.tie(0); cout.tie(0);
typedef long long ll;
typedef __int128 lll;
const int maxn = 5e6+9;
ll  t[maxn],ans;
int n,m,num[maxn];

void update(int now){
    while(now<=m){
        t[now] ++;
        now += lowbit(now);
    }
}

ll query(int now){
    ll an = 0;
    while(now){
        an += t[now];
        now -= lowbit(now);
    }
    return an;
}

int main(){
    speedUp_cin_cout//加速讀寫
    cin>>n;
    For(i,1,n) cin>>num[i],m = max(m,num[i]);
    For(i,1,n) {
        //先查詢比它大的數的數目,query(m)是總數,query(num[i])是小於等於num[i]的數的數目,相減可得
        ans += query(m)-query(num[i]);
        //再加入樹狀陣列
        update(num[i]);
    }
    cout<<ans;
    return 0;
}

  然而這個程式碼只能過牛客那題,交到洛谷是全紫 \(RE\) 的 (ノへ ̄、) 。原因其實就是 \(num[i]\) 的上限是 \(10^9\) ,導致樹狀陣列越界了。解決方法很簡單,因為我們只關心數字之間的關係,並不關心他們的實際大小,所以我們對輸入資料進行一下離散化,把大數對映成小數,如果你沒接觸過離散化也沒關係,看一遍就懂了,不懂可以看一下其他部落格哦。
  修改主函式程式碼,即可解決洛谷模板題:

//離散化陣列,可以用vector
vector<int>a;
int main(){
    speedUp_cin_cout//加速讀寫
    cin>>n;
    For(i,1,n) cin>>num[i],a.push_back(num[i]);
    //先排序
    sort(a.begin(),a.end());
    //再去重,固定寫法
    a.erase( unique( a.begin(),a.end() ) ,a.end());
    //獲得去重後的陣列大小,即不同的數有多少個
    m = a.size();
    For(i,1,n) {
        //確定num[i]在去重後升序排列的陣列中的位置,這裡注意要加1,因為樹狀陣列從1開始存
        num[i] = lower_bound(a.begin(),a.end(), num[i] ) - a.begin()+1;
        //先查詢比它大的數的數目,query(m)是總數,query(num[i])是小於等於num[i]的數的數目,相減可得
        ans += query(m)-query(num[i]);
        //再加入樹狀陣列
        update(num[i]);
    }
    cout<<ans;
    return 0;
}

子區間逆序對

  同樣是求逆序對,這題卻要求我們求所有子區間的逆序對個數,肯定是不能用剛剛的方法直接暴力 \(O(n^3logn)\)。我們考慮一次遍歷陣列就可以把每個逆序對的貢獻算出來,讓時間複雜度還是 \(O(nlogn)\) 的。
  對於一個逆序對 \(<A_i,A_j>\) ,只有 $l \in [1,i] ,r \in [j,n] $ 構成的子區間 \([l,r]\) 才能包含這個逆序對,也就是話說一個逆序對對總答案的貢獻就是 \(i * (n-j+1)\),即包含它的子區間數目。由上一題我們可以知道,當我們遍歷到 \(A_j\) 的時候,可以用樹狀陣列查詢大於 \(A_j\) 的數的數目。而在這裡我們僅僅維護數目是不行的,因為每個數的貢獻還和它的位置有關。而分析上面那個逆序對貢獻的式子,只要知道 \(A_i\) 的下標 \(i\) 即可。那麼我們就用樹狀陣列來維護每個數的下標和,這樣就可以一次遍歷求出答案了。
  還要注意這道題爆 \(long~ long\) 了(最近總是遇到剛好爆 \(long~ long\) 的題,有心理陰影了),我又不捨得打高精度,只好用奇技淫巧 $__int128 $了,但是要注意 $__int128 $ 型別是不能用 \(cin、cout、scanf、printf\) 的,要自己手寫輸入輸出,這裡不用輸入,我寫了一個輸出。

#include<bits/stdc++.h>
using namespace std;
#define For(i,sta,en) for(int i = sta;i <= en;i++)
#define lowbit(x) x&(-x)
#define speedUp_cin_cout ios::sync_with_stdio(false);cin.tie(0); cout.tie(0);
typedef long long ll;
typedef __int128 lll;
const int maxn = 5e6+9;
lll  t[maxn];
int n,m,num[maxn];
vector<int>a;

void update(int now,int value){
    while(now<=m){
        t[now] += value;
        now += lowbit(now);
    }
}

ll query(int now){
    ll an = 0;
    while(now){
        an += t[now];
        now -= lowbit(now);
    }
    return an;
}

//__int128輸出
void print(lll x){
    if(x == 0) return;
    print(x/10);
    int tem = x%10;
    cout<<tem;
}

int main(){
    speedUp_cin_cout
    cin>>n;
    For(i,1,n) cin>>num[i],a.push_back(num[i]);
    sort(a.begin(),a.end());
    a.erase(unique(a.begin(),a.end()),a.end());
    m = a.size();
    lll ans = 0;
    For(i,1,n) {
        num[i] = lower_bound(a.begin(),a.end(),num[i])-a.begin()+1;
        //計算比num[i]大的數的座標和
        lll l = (query(m)-query(num[i]));
        //右邊部分的區間長度
        lll r = n-i+1;
        ans += l*r;
        //加入座標
        update(num[i],i);  
    }
    if(ans) print(ans);
    else cout<<0;
    return 0;
}

逆序對和排序問題

題意概括

  有兩列都是 \(n\) 根的火柴,同一列高度互不相同,將兩列火柴直接的距離定義為 \(\sum\left(a_{i}-b_{i}\right)^{2}\) 。其中 \(a_i\) 表示第一列火柴中第 \(i\) 個火柴的高度,\(b_i\) 表示第二列火柴中第 \(i\) 個火柴的高度。僅可以交換相鄰兩根火柴的位置,求要讓兩列火柴距離最小的最小交換次數,並對 \(10^8-3\) 取模。資料滿足 \(1 \leq n \leq 10^{5}, 0 \leq\) 火柴高度 \(<2^{31}\)

分析

  這道題看起來和逆序對好像沒有什麼關係,需要一些分析後才可以和逆序對聯絡起來。
  首先我們要分析出什麼時候火柴距離最小。展開火柴距離的式子:

\[\begin{array}{c} \sum_{i=1}^{n}\left(a_{i}-b_{i}\right)^{2} \\\\ =\sum_{i=1}^{n}\left(a_{i}^{2}-2 a_{i} b_{i}+b_{i}^{2}\right) \\\\ =\sum_{i=1}^{n}\left(a_{i}^{2}+b_{i}^{2}\right)-\sum_{i=1}^{n}\left(2 a_{i} b_{i}\right) \end{array}\]

  因為所有火柴高度已經定下來了,即 \(\sum_{i=1}^{n}\left(a_{i}^{2}+b_{i}^{2}\right)\) 大小不會隨著交換而改變。所以我們要最大化 \(\sum_{i=1}^{n}\left(2 a_{i} b_{i}\right)\) 才能使這個式子最小。根據排序不等式,我們知道同序和 \(\geqslant\) 亂序和 \(\geqslant\) 逆序和(證明可以自行百度,會用就行了)。所以我們要讓火柴排成“同序和”的順序即可。換句話說,假如我們僅交換 \(b\) 列火柴(交換 \(a\)\(b\) 是等效的,我們選一列交換,讓另一列不動就行)就是讓 \(b\) 的第 \(i\) 小與 \(a\) 的第 \(i\) 小懟齊。
  這其實是一種排序,認識到這一點很重要。我們原來平時的排序,以升序為例,其實是把下標當做一個標準序列 \(standard[~]=\{1,2,3,···,n\}\) ,然後把要排序的陣列 \(num\) 按照 \(standard\) 從小到大懟齊,也就是 \(num\) 的第 \(i\) 小與 \(standard\) 的第 \(i\) 小懟齊。而在只能進行相鄰交換的前提下,最小的交換次數就是 \(num\) 的逆序對數目(可以自己感性證明一下)。現在我們把標準序列的定義換成一個指示 \(a\) 的第 \(i\) 小的位置的陣列,即 \(a[~standard[i]~]\)\(a\) 的第 \(i\) 小,要排序的陣列定義改為指示 \(b\) 的第 \(i\) 小的位置的陣列 ,即 \(b[~num[i]~]\)\(b\) 的第 \(i\) 小。然後新建一個序列 \(q\),讓 \(q[~standard[i]~] = num[i]\) ,即讓 \(num[~]\) 按照 \(standard[~]\) 進行“排序”,最終答案就是 \(q\) 的逆序對數目。
  這裡是比較難理解的,需要自己列幾個例子輔助思考。剩下部分其實就是求逆序對的模板。雖然資料範圍很大,但是我們用了一種特殊的離散化方式,將離散化陣列 \(p_i\) 定義為 \(a\)\(b\) 的第 \(i\) 小的位置(也就是上文中的 \(standard\)\(num\))。

\(Code:\)

#include<bits/stdc++.h>
using namespace std;
#define For(i,sta,en) for(int i = sta;i <= en;i++)
#define lowbit(x) x&(-x)
#define speedUp_cin_cout ios::sync_with_stdio(false);cin.tie(0); cout.tie(0);
typedef long long ll;
const int maxn = 2e5+9;
const int mod = 1e8-3;
int a[maxn],b[maxn],q[maxn],pa[maxn],pb[maxn];
ll t[maxn],n;

void update(int now){
    while(now <= n){
        t[now]++;
        now += lowbit(now);
    }
}

ll query(int now){
    ll an = 0;
    while(now){
        an =  (an + t[now])%mod;
        now -= lowbit(now);
    }return an;
}
bool cmp1(int &x,int &y){
    return a[x] < a[y];
}
bool cmp2(int &x,int &y){
    return b[x] < b[y];
}
int main(){
    speedUp_cin_cout  //讀寫優化
    cin>>n;
    For(i,1,n) cin>>a[i],pa[i] = i;  //pa,pb為離散化陣列
    For(i,1,n) cin>>b[i],pb[i] = i;
    sort(pa+1,pa+1+n,cmp1);
    sort(pb+1,pb+1+n,cmp2);
    For(i,1,n)  q[pa[i]] = pb[i];      //新建序列
    ll ans = 0;
    //求q的逆序對
    For(i,1,n){
        ans = (((query(n) - query(q[i]))%mod + ans)%mod+mod)%mod;
        update(q[i]);
    }
    cout<<ans<<endl;
    return 0;
}

總結

  逆序對和樹狀陣列的聯絡還是挺大的,很多涉及逆序對的題目都可以嘗試用樹狀陣列衝一下,當然歸併排序也是一定要掌握的啦。

相關文章