1. 程式人生 > >樹狀陣列求逆序對的應用

樹狀陣列求逆序對的應用

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\)取模的結果。

輸入樣例#1: 複製
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;
}