1. 程式人生 > >樹狀陣列和逆序對

樹狀陣列和逆序對

## 逆序對的概念   在一個有 $n$ 個元素的陣列 $A$ 中,如果存在 $1 \leqslant i A_j$ ,則稱 $$ 為 $A$ 的一個逆序對。我們熟知的排序其實就是一個消滅逆序對的過程。求一個數組的逆序對數目,我們可以用歸併排序,或者用我們今天的主角樹狀陣列,還不會樹狀陣列的同學可以看我之前的一篇學習筆記的部落格([戳這裡](https://www.cnblogs.com/ailanxier/p/13419109.html)),很快就可以理解了。 ## 樹狀陣列打逆序對 + 洛谷 [P1908 逆序對](https://www.luogu.com.cn/problem/P1908) + 牛客 [NC15163 逆序數](https://ac.nowcoder.com/acm/problem/15163)   兩題都是求逆序對的模板題,洛谷資料被加強過,而且數字範圍更大,如果用**樹狀陣列做需要進行離散化操作**,而歸併排序不用,這也是歸併排序更快的原因。但是有的時候歸併排序不能維護一些區間資訊(見下一題)。   先來分析一下逆序對應該怎麼數。把逆序對的概念換一種更通俗的說法,逆序對其實就是由**一個數和之前比它大的數**組成。樸素的做法就是在每一個數插入的時候,遍歷它前面的每一個數,如果比它大答案就加一。這種做法顯然是 $O(n^2)$ 的,而 $n$ 的範圍是 $10^5$ 級別的,肯定衝不過去。為了把時間複雜度降到 $O(nlogn)$ 級別,我們需要使用線段樹和樹狀陣列來維護。線段樹在這裡就有點麻煩了,我們用更簡潔的樹狀陣列就可以了。每插入一個數,是單點修改;每查詢一個數之前比它大的數目,是區間查詢,可行!   樹狀陣列傳統程式碼,修改和查詢完全不變: ```cpp #include
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>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<$ ,只有 $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$ 的,要自己手寫輸入輸出,這裡不用輸入,我寫了一個輸出。 ```cpp #include
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]; vectora; 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<>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; } ``` ## 逆序對和排序問題 + [P1966 火柴排隊](https://www.luogu.com.cn/problem/P1966) + [NC16526 火柴排隊](https://ac.nowcoder.com/acm/problem/16526) ### 題意概括   有兩列都是 $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:$** ```cpp #include 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<