【NOI 2018】冒泡排序(組合數學)


題意大概是給定一個長度為$n$的排列$p$,求有多少長度為$n$的排列滿足冒泡排序的交換次數為$\frac{1}{2} \sum\limits_{i = 1}^{n}|i - p_{i}|$。

可以發現,該式子是冒泡排序復雜度的下界,任意一個數想要回到規定的位置至少要被交換$|i - p_{i}|$次,即在排序過程中不浪費任何一次交換,每一個數都只能向它歸回原位的方向走。

稍加思索,可以得出一個結論:

  1. 任何一個最長下降子序列長度超過$2$的排列一定是不合法的。
  2. 任何一個最長下降子序列不超過$2$的排列一定合法。

考慮第一句話的正確性:

試想處於中間的那個數,左邊存在比它大的數,右邊存在比它小的數,那么該數一定會被向左交換一次,也一定會被向右交換一次,然后這兩次交換是對該數無意義的,因為它向兩個方向走了,導致位置沒有改變。所以這兩次的交換被浪費,故一定不合法。

考慮第二句話的正確性:

試想一個逆序對,處於右邊小的數一定會向左走,如果要使它不合法,必定要令它向右至少走一步,那就意味着右邊存在更小的數,與原條件矛盾,對於左邊的數同理。

 有了這個結論,不考慮起始字典序的限制,我們就可以寫出一個$O(n^{2})$的dp,考慮$f_{i,j}$表示前$i$數,記$i$個數中最大的數為$mx$,剩余數中有$j$個數比$mx$小,此時的方案數。

此處有兩種轉移:

  1. 選一個比$mx$大的數,不會破壞上述結論。
  2. 選剩余幾個比$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 }
View Code

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM