樹狀陣列求逆序對的應用
BIT-逆序對應用
逆序對的定義:
對於一個數列
\[
a_1, a_2, a_3, \cdots, a_n
\]
逆序對的個數是:
\[
\sum_{i < j, a_i > a_j} 1
\]
P1966 火柴排隊
題目描述
涵涵有兩盒火柴,每盒裝有n根火柴,每根火柴都有一個高度。 現在將每盒中的火柴各自排成一列, 同一列火柴的高度互不相同, 兩列火柴之間的距離定義為:\(∑(a_i−b_i)^2\)
其中\(a_i\)表示第一列火柴中第i個火柴的高度,\(b_i\)表示第二列火柴中第i個火柴的高度。
每列火柴中相鄰兩根火柴的位置都可以交換,請你通過交換使得兩列火柴之間的距離最小。請問得到這個最小的距離,最少需要交換多少次?如果這個數字太大,請輸出這個最小交換次數對 \(99,999,997\)取模的結果。
4 2 3 1 4 3 2 1 4輸出樣例#1: 複製
1輸入樣例#2: 複製
4 1 3 4 2 1 7 2 4輸出樣例#2: 複製
2
題解
可以證明當兩個數列都是經過排序之後的數列可以使得\(\sum(a_i-b_i)^2\)取到最小,
根據上面的結論,就是說如果對於數列\(\{a_n\}, \{b_n\}\):
\[
a_1,~a_2,~a_3,~\cdots,~a_{n-1},~a_n \\
b_1,~b_2,~b_3,~\cdots,~b_{n-1},~b_n
\]
對它們的\(id\)排序之後,原陣列排序的過程中相當於消除逆序對,但是本來的\(id\)是正序的,對於這個過程是對\(id\)增加逆序對的數量。可以說就因該是當\(a\)陣列對於\(b\)陣列想要移動到一模一樣所需要花的時間。但是如何求出這個步驟是一個值得討論的問題:
首先定於\(q[a[i]] = b[i]\),最終的目標是\(a[i]=b[i] = t\),即\(q[t] = t\)。可以發現終極目標就是將\(q\)陣列進行排序所需要的次數是多少,即求\(q\)的逆序對的個數。
程式碼:
#include <iostream> #include <algorithm> using namespace std; const int maxn = 100005; const int P = 99999997; int n; struct node { int id, val; } a[maxn], b[maxn]; int T[maxn], m[maxn]; int lb(int i) { return i & (-i); } bool cmp(node x, node y) { return x.val < y.val; } void add(int i, int delta) { while (i <= n) { T[i] += delta; i += lb(i); } } int sum(int i) { int rtn = 0; while (i > 0) { rtn += T[i]; i -= lb(i); } return rtn; } int main() { cin >> n; for (int i = 1; i <= n; i ++) cin >> a[i].val; for (int i = 1; i <= n; i ++) { cin >> b[i].val; a[i].id = b[i].id = i; } sort(a + 1, a + 1 + n, cmp); sort(b + 1, b + 1 + n, cmp); for (int i = 1; i <= n; i ++) { m[a[i].id] = b[i].id; } int ans = 0; for (int i = 1; i <= n; i ++) { add(m[i], 1); ans = (ans + i - sum(m[i])) % P; } cout << ans << endl; return 0; }
P2344 奶牛抗議
題目描述
約翰家的N 頭奶牛正在排隊遊行抗議。一些奶牛情緒激動,約翰測算下來,排在第i 位的奶牛的理智度為Ai,數字可正可負。
約翰希望奶牛在抗議時保持理性,為此,他打算將這條隊伍分割成幾個小組,每個抗議小組的理智度之和必須大於或等於零。奶牛的隊伍已經固定了前後順序,所以不能交換它們的位置,所以分在一個小組裡的奶牛必須是連續位置的。除此之外,分組多少組,每組分多少奶牛,都沒有限制。
約翰想知道有多少種分組的方案,由於答案可能很大,只要輸出答案除以1000000009 的餘數即可。
題解:
方程:
\[
f(i) = \sum_{0≤j<i,~\sum_{t = j + 1}^i a[t] ≥0}f(j), ~f(0)=1
\]
時間複雜度:\(O(n^3)\),使用字首和優化:
\[
f(i) = \sum_{0≤j<i,~s[i]-s[j] ≥0}f(j), ~f(0)=1
\]
時間複雜度:\(O(n^2)\),使用樹狀陣列存\(f\),其中下標為\(s\)。
對於\(s\)陣列需要使用離散化,並且\(0\)要加入離散化的過程,因為\(f(0)=1\)。
程式碼:
#include <iostream>
#include <algorithm>
using namespace std;
const int maxn = 100005;
const int P = 1000000009;
int n, a[maxn],s[maxn];
int T[maxn];
int lb(int i) { return i & (-i); }
void add_sum(int i, int d) {
// 由於陣列的大小加了1,所以要n + 1
while (i <= n + 1) {
T[i] = (T[i] + d) % P;
i += lb(i);
}
}
int query_sum(int i) {
int ans = 0;
while (i > 0) {
ans = (ans + T[i]) % P;
i -= lb(i);
}
return ans;
}
int main() {
cin >> n;
for (int i = 1; i <= n; i ++) {
cin >> a[i];
s[i] = s[i-1] + a[i];
a[i] = s[i];
}
// 對s進行離散化
sort(a, a + n + 1);
for(int i = 0; i <= n; i ++)
s[i]=lower_bound(a, a + n + 1, s[i]) - a + 1;
// 設定f[0] = 1, 此處的s[0]表示在原陣列中第0號元素在離散化過的陣列中的位置
add_sum(s[0], 1);
int ans = 0;
for (int i = 1; i <= n; i ++) {
ans = query_sum(s[i]);
add_sum(s[i], ans);
}
cout << ans << endl;
return 0;
}
BZOJ-1782 : 奶牛散步/P2982 [USACO10FEB]
題目描述
約翰有n個牧場,編號為1到n。它們之間有n−1條道路,每條道路連線兩個牧場,通過這些道路,所有牧場都是連通的。
1號牧場裡有個大牛棚,裡面有n頭奶牛。約翰會把它們放出來散步。奶牛按編號順序出發,首先出發的是第一頭奶牛,等它到達了目的地後,第二頭奶牛才會出發,之後也以此類推。每頭奶牛的目的地都不同,其中第iii頭奶牛的目的地是\(t_i\)號牧場。假如編號較大的奶牛,在經過一座牧場的時候,遇到了一頭編號較小的奶牛停在那裡散步,就要和它打個招呼。請你統計一下,每頭奶牛要和多少編號比它小的奶牛打招呼。
題解
首先這道題可以這樣理解:
所有的奶牛從\(1\)號到\(n\)號依次離開出發到各自的牧場\(t_i\),在經過的道路上如果遇到編號比自己小的牛打招呼,統計總共打多少次招呼。由於編號從小到大,所以只要路徑上有牛就肯定會打招呼。
至此,題目的要求的就是對於每頭牛,統計在去的路徑上有多少頭牛。
定義一個農場編號與牛編號的對映關係:\(cow[t[i]] = i\)。
在\(dfs\)整張圖的過程中,我們會發現
- 從根節點開始,對樹進行深度優先遍歷。
- 當進行到節點 i 時,有:
- i 的祖先們 Father[i] 已經被訪問過了,但還沒有退出。
- 其他節點或者已經被訪問過並退出了,或者還沒有被訪問。
- 那麼需要一個數據結構,維護那些已經被訪問過了,但還沒有退出的點權,支援查詢小於特定值的元素的數量 。
- 可以使用樹狀陣列。(使用奶牛編號的下標標記)
所以有以下的程式碼:
#include <iostream>
using namespace std;
const int maxn = 100005;
int n, p[maxn], head[maxn], cow[maxn], T[maxn], ans[maxn];
int lb(int i) { return i & (-i); }
void modify(int i, int delta) {
while (i <= n) {
T[i] += delta;
i += lb(i);
}
}
int query(int i) {
int ret = 0;
while (i > 0) {
ret += T[i];
i -= lb(i);
}
return ret;
}
struct edge {
int to, next;
} g[maxn * 2];
int ecnt = 2;
void add_edge(int u, int v) {
g[ecnt] = (edge) {v, head[u]};
head[u] = ecnt ++;
}
void dfs(int u, int fa) {
int current = cow[u];
// 這個頭牛的答案就是查詢:樹狀陣列當中編號比它小的,在路徑上的個數和
ans[current] = query(current);
// 剛剛進入這個節點(及其子樹),所以把這個節點加入樹狀陣列
modify(current, 1);
for (int e = head[u]; e != 0; e = g[e].next)
if (g[e].to != fa)
dfs(g[e].to, u);
// 即將退出該節點,再也不會訪問到,所以將其從樹狀陣列中刪除
modify(current, -1);
}
int main() {
cin >> n;
for (int i = 1; i < n; i ++) {
int a, b; cin >> a >> b;
add_edge(a, b); add_edge(b, a);
}
for (int i = 1; i <= n; i ++) {
cin >> p[i];
cow[p[i]] = i;
}
dfs(1, 0);
for (int i = 1; i <= n; i ++) {
cout << ans[i] << endl;
}
return 0;
}
P3988 [SHOI2013]發牌
題目描述
在一些撲克遊戲裡,如德州撲克,發牌是有講究的。一般稱呼專業的發牌手為荷官。荷官在發牌前,先要銷牌(burn card)。所謂銷牌,就是把當前在牌庫頂的那一張牌移動到牌庫底,它用來防止玩家猜牌而影響遊戲。
假設一開始,荷官拿出了一副新牌,這副牌有N 張不同的牌,編號依次為1到N。由於是新牌,所以牌是按照順序排好的,從牌庫頂開始,依次為1, 2,……直到N,N 號牌在牌庫底。為了發完所有的牌,荷官會進行N 次發牌操作,在第i 次發牌之前,他會連續進行Ri次銷牌操作, Ri由輸入給定。請問最後玩家拿到這副牌的順序是什麼樣的?
舉個例子,假設N = 4,則一開始的時候,牌庫中牌的構成順序為{1, 2, 3, 4}。
假設R1=2,則荷官應該連銷兩次牌,將1 和2 放入牌庫底,再將3 發給玩家。目前牌庫中的牌順序為{4, 1, 2}。
假設R2=0,荷官不需要銷牌,直接將4 發給玩家,目前牌庫中的牌順序為{1,2}。
假設R3=3,則荷官依次銷去了1, 2, 1,再將2 發給了玩家。目前牌庫僅剩下一張牌1。
假設R4=2,荷官在重複銷去兩次1 之後,還是將1 發給了玩家,這是因為1 是牌庫中唯一的一張牌。
輸入輸出格式
輸入格式:
第1 行,一個整數N,表示牌的數量。
第2 行到第N + 1 行,在第i + 1 行,有一個整數Ri,0<=Ri<N
輸出格式:
第1 行到第N行:第i 行只有一個整數,表示玩家收到的第i 張牌的編號。
輸入樣例#1: 複製4 2 0 3 2輸出樣例#1: 複製
3 4 2 1
題解
維護一個數組,其中存的是每張牌是否還在牌庫當中,若在,則值為\(1\),反之,值為\(0\)。
每次摸牌,先銷牌\(s\)張就是在剩下的\(m\)張牌中往後尋找\(s\)張牌就是了,如果還未找到\(s\)就已經為原狀態的最後一張了,其實只需要進行對\(m\)的牌數進行取模,其實這個想法非常好理解,因為銷牌的這個過程是滾動的。
所以我們定義\(r_0\)為原來的找牌的“指標”,\(r_t\)為找到牌的指標,會有下式:
\[
r_t = (s + r_0) \mod m
\]
現在,我們來思考一下\(r_t\)的意義,其實它就是說剩下的牌中(牌的先後位置關係始終未變,變的是找牌的指標)第\(r_t\)張,也就是說我們要找到要維護的陣列當中字首和為\(r_t\)的那個位置就是第\(i\)張牌的位置,即第\(i\)個答案。
從上述的表述可以理解:我們需要維護一個樹狀陣列,並二分答案。
但是其實有一個比二分答案更為簡單的做法,就是模擬\(lb\)通過二分的方法訪問\(T[]\)來得到位置,時間複雜度僅為\(O(\log n)\),而不是\(O(\log^2n)\)。
程式碼:
#include <iostream>
#include <cstdio>
#include <stdio.h>
using namespace std;
const int maxn = 700005;
const int maxx = 1 << 20;
int n, T[maxn];
inline int lb(int i) { return i & (-i); }
int query(int x) {
int pos = 0;
// 第一次查詢的區間最大,其後每次減半,相當於二分,但這裡訪問T陣列的時間複雜度為O(1),故時間複雜度為O(log n)而不是O(log^2 n)
for (int i = maxx; i > 0; i >>= 1) {
// 更新位置
int j = i + pos;
// 整個過程相當於在進行lb,所以x代表直到pos的字首和與原來所求的差值,即距query目標還差的一部分
if (j <= n && T[j] <= x) {
pos = j;
x -= T[j];
}
}
return pos + 1;
}
void modify(int i, int d) {
while (i <= n) {
T[i] += d;
i += lb(i);
}
}
int main() {
scanf("%d", &n);
// O(n)時間建樹狀陣列,原因是a[i]=1
for (int i = 1; i <= n; i ++)
T[i] = lb(i);
int r = 0;
for (int m = n; m > 0; m --) {
int s; scanf("%d", &s);
r = (r + s) % m;
// 在樹狀陣列當中查詢值為r的位置,時間複雜度為O(n)
int pos = query(r);
// 由於這張牌被髮掉了,所以應該將這張牌從樹狀陣列當中移除
modify(pos, -1);
// 答案就為這個位置編號
printf("%d\n", pos);
}
return 0;
}