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

題意

給你一個長爲 \(n\) 的排列 \(p\) ,問你有多少個等長的排列知足c++

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

題解

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

參考了 liuzhangfeiabc 大佬的博客spa

首先咱們觀察一下最後的序列有什麼性質:debug

考試 打表 觀察的:對於每一個數來講,它後面全部小於它的數都是單調遞增的。code

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

整個序列最長降低子序列長度不超過 \(3\) ,或者說整個序列能劃分紅兩個最長上升子序列。排序

這看上去頗有道理,但並非那麼顯然?get

證實:博客

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

反證法:那麼若是存在一個長度 \(\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\) 個數須要填入,咱們對於當前這一位任意選取一個 \(> p_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;
}
相關文章
相關標籤/搜索