1. 程式人生 > >LOJ #2719. 「NOI2018」冒泡排序(組合數學 + 樹狀數組)

LOJ #2719. 「NOI2018」冒泡排序(組合數學 + 樹狀數組)

git stderr 好的 sizeof 序列 下界 deb efi 如果

題意

給你一個長為 \(n\) 的排列 \(p\) ,問你有多少個等長的排列滿足

  1. 字典序比 \(p\) 大 ;
  2. 它進行冒泡排序所需要交換的次數可以取到下界,也就是令第 \(i\) 個數為 \(a_i\) ,下界為 \(\displaystyle \sum_{i=1}^{n} |i - a_i|\)

題解

一道特別好的題,理解後做完是真的舒暢~

參考了 liuzhangfeiabc 大佬的博客 。

首先我們觀察一下最後的序列有什麽性質:

考試 打表 觀察的:對於每個數來說,它後面所有小於它的數都是單調遞增的。

然後問了問肖大佬,肖大佬說這不就等價於

整個序列最長下降子序列長度不超過 \(3\)

,或者說整個序列能劃分成兩個最長上升子序列。

這看上去很有道理,但並不是那麽顯然?

證明:

考慮整個交換次數取到下限,那麽對於任意一個數都需要取到下界。

反證法:那麽如果存在一個長度 \(\ge 3\) 的最長下降子序列的話,那麽這個元素首先會被右邊小於它的數動一次位置,然後自己需要折返一次才能換到原位,那麽就多了次數,不滿足條件。

這個性質有什麽用呢?我們發現這個上升子序列與最大值是有關系的。

也就是說我們填到第 \(i\) 個位置,假設當前最大值為 \(j\) ,我們可以隨意填一個 \(> j\) 的數。但如果要填 \(< j\) 的數,要滿足當前這個數到結尾全都是從小到大去填。

那麽我們可以根據這個進行一個顯然的 \(dp\)

我們令大於當前最大值的數為 非限制元素 ,小於當前的數為 限制元素

\(f_{i,j}\) 表示還剩余 \(i\) 個數沒填,其中後 \(j\) 個是大於當前最大值的 非限制元素 的方案數。

轉移就是枚舉下一個位置填一個 限制元素 或某一個 非限制元素

如果填限制元素,非限制元素的數量不變;

否則假設填入從小到大第 \(k\) 個非限制元素,非限制元素的數量就會減少 \(k\) 個。

考慮逆推,那麽顯然有一個轉移方程了:
\[ f_{i,j} = \sum_{k=0}^{j} f_{i-1, j - k} \]
邊界有
\[ f_{i, 0} = 1 \\]

我們可以把這個看成一個二維矩陣。

那麽對於 \((i, j)\) 這個點就是上一行前 \(j\) 個數的和,也就等價於
\[ f_{i,j} = f_{i - 1, j} + f_{i, j - 1} \]
這個矩陣其中一部分如下(不難發現要滿足 \(j \le i\) 才能有取值):
\[ \begin{bmatrix} 1 & 0 &0 & 0 & 0 & 0 \1 & 1 & 0 & 0 & 0 & 0 \1 & 2 & 2 & 0 & 0 & 0 \1 & 3 & 5 & 5 & 0 & 0 \1 & 4 & 9 & 14 & 14 & 0 \1 & 5 & 14 & 28 & 42 & 42 \\end{bmatrix} \]
對角線上的數就是卡特蘭數,但對於其中任意一個數可以由如下組合數導出:
\[ \binom {i + j - 1} {j} - \binom {i + j - 1}{j - 2} \]
它對於 \((i, j)\) 這個點的實際意義為從 \((0, 0)\) 一直向下和向右走,對於每一步要滿足向下走的步數不少於向右走的步數,且最後走到 \((i, j)\) 的方案數。

對於這個組合數實際的組合意義,我並不知道。。。(有知道大佬快來告訴我啊)

但我們可以證明這個組合數是正確的:

類似與數學歸納,我們進行二維歸納
\[ \begin{align} f_{i, j} &= f_{i,j-1}+ f_{i - 1, j} \&= (\binom {i + j - 2}{j - 1} + \binom {i + j - 2}{j}) - (\binom{i + j - 2}{j - 3} + \binom{i + j - 2}{j - 2}) \& = \binom {i + j - 1} {j} - \binom {i + j - 1}{j - 2} \end{align} \]

然後我們繼續考慮它的限制。

對於字典序限制,我們可以這樣考慮。

枚舉最終得到的序列和原序列不同的第一位(前面的都相同)然後對於這個分開計數。

假設當前做到第 \(i\) 位,給定排列中的這一位為 \(p_i\) ,後面有 \(big\) 個數比他大,\(small\) 個數比它小。

且當前的 非限制元素\(lim\) 個(也就是後面大於前面出現過的最大值的數的個數)。

首先需要把 \(lim\)\(big\) 取個 \(min\) ,這個是我們當前非限制元素的下界。

如果 \(lim = 0\) 那就意味著最大的數已經被我們填入,後面所有數只能從小到大填入,但這並不能滿足字典序比原序列大的情況,直接退出即可。

否則我們需要計算的就是
\[ \sum_{j=0}^{lim - 1} f_{n - i, j} = f_{n - i + 1, lim - 1} \]
也就是後面有 \(n - i\) 個數需要填入,我們任意選取 \(0 \sim lim - 1\) 個非限制元素,填在後面序列的開頭的方案數。

然後我們需要繼續考慮能否繼續向後填,也就是當前填入的數 \(a_i = p_i\) 是否合法

  1. 如果當前 \(big\) 更新了 \(lim\) ,那麽說明 \(a_i\) 本身是一個非限制元素(也就是當前的最大值),合法;
  2. 否則,如果 \(a_i\) 是填入的最小數,那麽是合法的;
  3. 其他情況顯然都是不合法的。

復雜度是 \(O(n \log n)\)

總結

對於一類 \(dp\) 我們考慮忽略它們的具體取值,只考慮他們所屬的種類。

以及一些 \(dp\) 可以用組合數進行表達。

然後字典序計數考慮按位去做(似乎可以容斥?)

代碼

#include <bits/stdc++.h>

#define For(i, l, r) for(register int i = (l), i##end = (int)(r); i <= i##end; ++i)
#define Fordown(i, r, l) for(register int i = (r), i##end = (int)(l); i >= i##end; --i)
#define Set(a, v) memset(a, v, sizeof(a))
#define Cpy(a, b) memcpy(a, b, sizeof(a))
#define debug(x) cout << #x << ": " << x << endl
#define DEBUG(...) fprintf(stderr, __VA_ARGS__)

using namespace std;

inline bool chkmin(int &a, int b) {return b < a ? a = b, 1 : 0;}
inline bool chkmax(int &a, int b) {return b > a ? a = b, 1 : 0;}

inline int read() {
    int x = 0, fh = 1; char ch = getchar();
    for (; !isdigit(ch); ch = getchar()) if (ch == '-') fh = -1;
    for (; isdigit(ch); ch = getchar()) x = (x << 1) + (x << 3) + (ch ^ 48);
    return x * fh;
}

void File() {
    freopen ("inverse.in", "r", stdin);
    freopen ("inverse.out", "w", stdout);
}

const int N = 2e6 + 1e3, Mod = 998244353;
int fac[N], ifac[N];

int n, p[N], maxsta;

int fpm(int x, int power) {
    int res = 1;
    for (; power; power >>= 1, x = 1ll * x * x % Mod)
        if (power & 1) res = 1ll * res * x % Mod;
    return res;
}

void Math_Init(int maxn) {
    fac[0] = ifac[0] = 1;
    For (i, 1, maxn)
        fac[i] = 1ll * fac[i - 1] * i % Mod;
    ifac[maxn] = fpm(fac[maxn], Mod - 2);
    Fordown (i, maxn - 1, 1)
        ifac[i] = 1ll * ifac[i + 1] * (i + 1) % Mod;
}

inline int C(int n, int m) {
    if (n < 0 || m < 0 || n < m) return false;
    return 1ll * fac[n] * ifac[m] % Mod * ifac[n - m] % Mod;
}

#define lowbit(x) (x & -x)
namespace Fenwick_Tree {

    int sumv[N];

    void Init() { For (i, 1, n) sumv[i] = 0; }

    void Update(int pos, int uv) {
        for (; pos <= n; pos += lowbit(pos))
            sumv[pos] += uv;
    }

    int Query(int pos) {
        int res = 0;
        for (; pos; pos -= lowbit(pos))
            res += sumv[pos];
        return res;
    }

}

inline int f(int i, int j) {
    if (j > i) return 0;
    return (C(i + j - 1, j) - C(i + j - 1, j - 2) + Mod) % Mod;
}

int main () {

    File();
    int cases = read();

    Math_Init(2e6);

    while (cases --) {

        Fenwick_Tree :: Init();
        n = read();
        For (i, 1, n)
            Fenwick_Tree :: Update((p[i] = read()), 1);

        int lim = n, ans = 0;
        For (i, 1, n) {
            Fenwick_Tree :: Update(p[i], -1);
            int small = Fenwick_Tree :: Query(p[i]), big = (n - i) - small;

            if (!big) break ;
            bool flag = !chkmin(lim, big);


            (ans += f(n - i + 1, lim - 1)) %= Mod;
            if (flag && small) break;
        }

        printf ("%d\n", ans);

    }

    return 0;
}

LOJ #2719. 「NOI2018」冒泡排序(組合數學 + 樹狀數組)