題意大概是給定一個長度為$n$的排列$p$,求有多少長度為$n$的排列滿足冒泡排序的交換次數為$\frac{1}{2} \sum\limits_{i = 1}^{n}|i - p_{i}|$。
可以發現,該式子是冒泡排序復雜度的下界,任意一個數想要回到規定的位置至少要被交換$|i - p_{i}|$次,即在排序過程中不浪費任何一次交換,每一個數都只能向它歸回原位的方向走。
稍加思索,可以得出一個結論:
- 任何一個最長下降子序列長度超過$2$的排列一定是不合法的。
- 任何一個最長下降子序列不超過$2$的排列一定合法。
考慮第一句話的正確性:
試想處於中間的那個數,左邊存在比它大的數,右邊存在比它小的數,那么該數一定會被向左交換一次,也一定會被向右交換一次,然后這兩次交換是對該數無意義的,因為它向兩個方向走了,導致位置沒有改變。所以這兩次的交換被浪費,故一定不合法。
考慮第二句話的正確性:
試想一個逆序對,處於右邊小的數一定會向左走,如果要使它不合法,必定要令它向右至少走一步,那就意味着右邊存在更小的數,與原條件矛盾,對於左邊的數同理。
有了這個結論,不考慮起始字典序的限制,我們就可以寫出一個$O(n^{2})$的dp,考慮$f_{i,j}$表示前$i$數,記$i$個數中最大的數為$mx$,剩余數中有$j$個數比$mx$小,此時的方案數。
此處有兩種轉移:
- 選一個比$mx$大的數,不會破壞上述結論。
- 選剩余幾個比$mx$小的數中最小的那個。如果出現了長度為$3$的最長下降子序列,那在它第二個數的時候就不會被選了,因為它不是最小的數。在$j = 0$時不能用此轉移。
這樣就能$O(n^{2})$做一個dp了。我們把dp的兩維看作二維坐標,看看它的實際意義。
我們每次橫坐標$i$加$1$時,縱坐標$j$每次加一個非負整數,或者減一。其中$j$總是非負的,那由$(0,0)$走到$(n,0)$的一條合法路徑就是一個合法答案。
由於每次增加的是任意非負整數,不好計算,我們甚至可以把它轉化成括號序列。每次橫坐標增加$1$個單位時,相當於加了一個右括號,在這之前可以加任意多個左括號,對應了縱坐標增加量加一,即將$j-1$對應了不加左括號。顯然這個模型和上一個完全一樣,而且你可以快速的用組合數算出括號序列的總數。
最后我們來考慮初始字典序的限制,通過上述論證,我們知道一個字典序對應了一個括號序列,我們考慮在哪一個位置突破了字典序的限制,那么顯然以后就可以隨機游走了。突破字典序的限制,意味着選一個比當前更大的數(一定會更大,如果選擇了一個比$mx$小但比當前數大的一個數就會違反只能選最小的數的規則),我們只要比原先多加一個左括號就可以了(不需要多加,因為那會被隨機游走枚舉到)。
此處有一個小trick:我們在計算棧中還有$x$個左括號,還有$y$個右括號將來匹配的括號序列方案數時,組合數算出來的是隨機游走的方案數,事實上我們必須保證縱坐標非負,我們只需要容斥掉不合法的就可以了。具體來講就是將我們要算的起點按直線$y=-1$鏡像,再計算沒有右括號數限制的隨機游走的方案數,因為每一個算出來的方案一定會經過$y=-1$這條直線,我們把這個方案表示的路徑在第一次觸碰直線$y=-1$之前的那段向上翻回去,就能對應了原起點在游走時被計算進去的一條不合法路徑,把這個減掉就可以了。
所以總的時間復雜度是$O(n)$的。
$\bigodot$技巧&套路:
- 冒泡排序的復雜度(交換次數)分析。
- 隨機游走和括號序列計數的聯系。
- 有限制的括號序列計數的容斥轉化技巧。
1 #include <cstdio> 2 #include <cstring> 3 #include <algorithm> 4 5 typedef long long LL; 6 const int N = 1200005, MOD = 998244353; 7 8 int tc, n; 9 int fac[N], ifac[N], p[N >> 1], vis[N >> 1]; 10 11 inline void Read(int &x) { 12 x = 0; static char c; 13 for (c = getchar(); c < '0' || c > '9'; c = getchar()); 14 for (; c >= '0' && c <= '9'; x = (x << 3) + (x << 1) + c - '0', c = getchar()); 15 } 16 17 inline int Pow(int x, int b) { 18 static int re; 19 for (re = 1; b; b >>= 1, x = (LL) x * x % MOD) 20 if (b & 1) re = (LL) re * x % MOD; 21 return re; 22 } 23 24 inline int C(int x, int y) { 25 if (x < y) return 0; 26 return (LL) fac[x] * ifac[y] % MOD * ifac[x - y] % MOD; 27 } 28 inline int Cal(int x, int y) { 29 if (x < y) return 0; 30 int dis = 2 * x - y, re1 = C(dis, x); 31 if (y + 2 > dis) return re1; 32 return (re1 - C(dis, (dis + y) / 2 + 1)) % MOD; 33 } 34 35 int main() { 36 freopen("inverse.in", "r", stdin); 37 freopen("inverse.out", "w", stdout); 38 39 fac[0] = 1; 40 for (int i = 1; i < N; ++i) { 41 fac[i] = (LL) fac[i - 1] * i % MOD; 42 } 43 ifac[N - 1] = Pow(fac[N - 1], MOD - 2); 44 for (int i = N - 1; i >= 1; --i) { 45 ifac[i - 1] = (LL) ifac[i] * i % MOD; 46 } 47 48 scanf("%d", &tc); 49 for (; tc; --tc) { 50 int re = 0, mx = 0, low = 1, he = 0; 51 memset(vis, 0, sizeof vis); 52 scanf("%d", &n); 53 for (int i = 1; i <= n; ++i) { 54 Read(p[i]); 55 } 56 for (int i = 1; i < n; ++i, --he) { 57 if (mx < p[i]) he += p[i] - mx; 58 mx = std::max(mx, p[i]); 59 for (vis[p[i]] = 1; vis[low]; ++low); 60 re = (re + Cal(n - i + 1, he + 1)) % MOD; 61 if (mx > p[i] && p[i] > low) { 62 break; 63 } 64 } 65 printf("%d\n", (re + MOD) % MOD); 66 } 67 68 return 0; 69 }
