1. 程式人生 > >luogu P4769 [NOI2018] 氣泡排序

luogu P4769 [NOI2018] 氣泡排序

題目大意:考慮用氣泡排序的過程求一個排列的逆序對數,由於將pi從i位置移動到pi位置至少需要移動|i-pi|次,而每次交換最多實現2次正向移動,所以逆序對數的下界是1/2 sigma |i-pi|,當然這個下界並不總能達到(比如3 2 1有3個逆序對而計算得到的下界是2),求:給定n和排列p,滿足字典序嚴格大於p且能達到下界的排列有多少個,模998244353。

一道差點把我送退役的題……算了說多了都是淚。

考慮什麼時候才能達到下界?顯然是所有元素都只會正向移動的時候。

這就意味著:如果pi>i,那麼pi左邊的數都必須<pi;否則,所有<pi的數都必須出現在pi左邊(之所以全部考慮左邊,是為了下一步設計dp狀態方便)。

於是我們立刻得到了一個狀壓dp:設dp[s][0/1]表示現在選了s集合的數,字典序是否貼下界的合法方案數。轉移時列舉下一個選的數字,討論一下是否合法即可。

於是我們已經有了一個能解決n<=18,期望得分44的優秀演算法,然後呢?

當然是用它來打表了。

進一步分析:

題目可以轉化為:要求排列中不存在長度>=3的下降子序列。

因為如果出現的話,那麼這個下降子序列中間的元素需要既與左邊比它大的元素交換又與右邊比它小的元素交換,需要折返一下,顯然就不合法了。

這又等價於可以將序列劃分為2個上升子序列。

首先我們先不看那個字典序的性質,相信大家打一下表就能發現答案是卡特蘭數。

然後我們再想想它的本質是什麼:

假設前i個位置中,最大的數是j,那麼我們會發現,>j的數當前是可以隨便填的,然而<j的數只能限制從小到大按順序填入(因為這些元素一定被歸入同一個上升子序列)。

於是我們就可以設f(i,j)表示還剩餘i個數沒填,其中後j個是大於當前最大值的“非限制元素”的答案。

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

如果填限制元素,非限制元素的數量不變;否則假設填入第k個非限制元素,非限制元素的數量就會減少k個。

f(i,j) = sigma k=0~j f(i-1,j-k)

邊界是f(0,0) = 1

這其實就是個字首和:

f(i,j) = f(i,j-1) + f(i-1,j)

擅長打表熟悉組合數的人會很快發現,這東西就是兩個組合數相減:

f(i,j) = c(i+j-1,j) - c(i+j-1,j-2)

它的正確性容易驗證:

f(i,j-1) + f(i-1,j) = c(i+j-2,j-1) + c(i+j-2,j) - c(i+j-2,j-3) - c(i+j-2,j-2) = c(i+j-1,j) - c(i+j-1,j-2) = f(i,j)

特別地,f(i,0) = 1,f(i,1) = i

這也解釋了為什麼沒有限制時答案是卡特蘭數:只需注意到此時的所求是f(n,n)

f(n,n) = c(2×n-1,n) - c(2×n-1,n-2) = C(n)

我們再考慮限制,假設當前做到第i位,給定的排列中這一位是ai,後面有bi個數比它大,前面有ci個數比它小,這兩個陣列可以用樹狀陣列方便地計算出來,並且現在的“非限制元素”還有nw個。

我們先計算填入的數字pi>ai的情況,然後再令pi=ai繼續計算下一位。

首先nw可以與bi取個min,因為填完這一位後非限制元素一定不超過bi個。

然後此時如果nw=0(最大的數被填進去了),則意味著我們後面將別無選擇地只能按順序填入,而這個排列的字典序是嚴格不大於給定排列的,因此就可以退出了。

否則,我們相當於要求sigma j=0~nw-1 f(n-i,j),根據字首和它等於f(n-i+1,nw-1),可以O(1)計算。

然後,我們考慮填入pi=ai是否合法:

如果剛剛bi更新了nw,說明ai本身就是一個“非限制元素”,當然合法;

否則,如果ai是當前未填入的元素中最小的(對應ci==ai-1),相當於填了一個最小的“限制元素”,也是合法的;

否則,就是亂序填入“限制元素”,不合法,就可以退出了。

總複雜度O(n log n),瓶頸其實在於樹狀陣列求bi和ci的部分。

總結:一道絕好的打表找規律題。

程式碼如下:

#include<bits/stdc++.h>
using namespace std;
int t,n,a[600010];
#define li long long
#define gc getchar()
#define pc putchar
int read(){
    int x = 0,c = gc;
    while(!isdigit(c)) c = gc;
    while(isdigit(c)){
        x = (x << 1) + (x << 3) + (c ^ '0');
        c = gc;
    }
    return x;
}
void print(int x){
    if(x >= 10) print(x / 10);
    pc(x % 10 + '0');
}
int tt[1200010];
void xg(int q){
    for(int i = q;i <= n;i += i & (-i)) ++tt[i];
}
int cx(int q){
    int as = 0;
    for(int i = q;i;i -= i & (-i)) as += tt[i];
    return as;
}
int b[600010],c[600010];
li jc[1200010],nj[1200010];
const int mo = 998244353;
li ksm(li q,li w){
    li as = 1;
    while(w){
        if(w & 1) as = as * q % mo;
        q = q * q % mo;
        w >>= 1;
    }
    return as;
}
int mx1 = 1200005;
inline li zh(li q,li w){
    return jc[q] * nj[w] % mo * nj[q - w] % mo;
}
inline li wk(li q,li w){
    if(w == 0) return 1;
    if(w == 1) return q;
    return (zh(q + w - 1,w) - zh(q + w - 1,w - 2) + mo) % mo;
}
int main(){
    int i,j;
    jc[0] = 1;
    for(i = 1;i <= mx1;++i) jc[i] = jc[i - 1] * i % mo;
    nj[mx1] = ksm(jc[mx1],mo - 2);
    for(i = mx1 - 1;i >= 0;--i) nj[i] = nj[i + 1] * (i + 1) % mo;
    t = read();
    while(t--){
        n = read();
        for(i = 1;i <= n;++i) a[i] = read(),tt[i] = 0;
        for(i = n;i;--i){
            b[i] = n - i - cx(a[i]);
            xg(a[i]);
            c[i] = i - 1 - (n - a[i] - b[i]);

        }
        li as = 0;int nw = n; 
        for(i = 1;i <= n;++i){
            bool fg = b[i] < nw;
            nw = min(nw,b[i]);
            if(nw <= 0) break;
            (as += wk(n - i + 1,nw - 1)) %= mo;
            if(!fg && c[i] != a[i] - 1) break;
        } 
        print(as);pc('\n');
    } 
    return 0;
}