1. 程式人生 > 其它 >CSP-S 2021 括號序列

CSP-S 2021 括號序列

CSP-S 2021 括號序列

這道題考場殺我 \(2.5h\),寫了兩個錯誤演算法,最後寫了一個 \(O(n^4)\),然後優化成 \(O(n^3)\) 了。

題意

一開始讀錯題了,寫了一個多小時的錯解。當時以為只要括號都匹配,* 在哪裡無所謂,只要連續的不超過 \(k\) 就可以。

所以請務必好好讀題,接下來解釋一下題意,並且引出解題所需的定義:

如果有一個合法序列,這個序列左右兩個端點是一對互相匹配的括號,將整個序列括起來,我們稱之為 "完全合法序列"。也就是題目中,第三種情況的合法序列。

那麼本題中合法情況可以這樣描述:

由若干完全合法序列組成,相鄰的完全合法序列之間可以有不超過 \(k\)

* 分隔。

那麼根據本題的要求,合法的序列的左端或右端加上不超過 \(k\)*,然後在外面加一對括號,就得到了一個完全合法序列。(特別注意,左右不能同時加,但是可以都不加)

特別地,在不超過 \(k\)* 外直接加括號也是完全合法序列。

\(O(n^4)\) DP

我們設計狀態 \(f_{Len, i}\) 表示以 \(i\) 為左端點,長度為 \(Len\) 的區間為完全合法序列的方案數,\(g_{Len, i}\) 表示以 \(i\) 為左端點,長度為 \(Len\) 的區間為合法序列的方案數。

首先發現任何合法的序列,其左右兩端一定分別是 (),所以當輸入序列左端點是 *

) 或右端點是 *( 時,兩個值都為 \(0\)

兩端點合法後,考慮轉移:

\[f_{Len, i} = g_{Len - 2, i + 1} + \sum_{j = 1}^{min(Len - 2, m)} g_{Len - 2 - j, i + 1 + j} + \sum_{k = 1}^{min(Len - 3, m)}g_{Len - 2 - k, i + 1} \]

第一個轉移表示直接在合法序列外加括號,第二個和第三個表示在合法序列的左邊或右邊加若干 * 後再加括號。

其中 \(j\) 列舉過程中需要考慮區間 \([i + 1, i + j]\) 必須都是 *?,否則直接 break

\(k\) 也是一樣的,列舉過程中判斷當前位置是否可以變成 *

至於為什麼 \(k \leq min(Len - 3, m)\),因為對於 \(j = Len - 2\) 的情況,表示的是直接在連續的 * 外面加括號,如果 \(k = Len - 2\) 再統計一次,就會重複統計。

接下來是 \(g\) 的轉移:

\[g_{Len, i} = \sum_{j = 0}^{Len - 2} (g_{j, i} * \sum_{k = 0}^{min(Len - j - 2, m)} f_{Len - j - k, i + j + k}) \]

這裡的轉移表示先選一個長度為 \(j\) 的合法序列 \([i, i + j - 1]\),然後在它後面加 \(k\)*,後面再接一個長度 \(Len - j - k\) 的完全合法序列,組成一個新的合法序列。

當然,這裡列舉 \(k\) 的時候也要判斷對應位置是否能賦成 *,否則 break

列舉邊界兩個 \(-2\) 是因為完全合法序列最短也要 \(2\),所以要提前留至少兩個長度給最後的完全合法序列。

答案便是 \(g_{n, 1}\)

兩個 DP 狀態都是 \(O(n^2)\)\(f\) 的轉移是 \(O(n)\)\(g\) 的轉移是 \(O(n^2)\),總複雜度 \(O(n^4)\)

複雜度裡面有一個 \(n\) 是和連續的 * 的數量有關的,所以隨機資料下 \(O(n^4)\)\(500\) 飛快。

優化到 \(O(n^3)\)

發現演算法的瓶頸在於轉移 \(g\) 的時候,列舉中間的 * 的數量。

這時候,增加一個狀態 \(g_{Len, i, 0}\) 表示的資訊和之前的 \(g\) 相同,加一個 \(g_{Len, i, 1}\) 表示這個區間內,除去右端的 \((0, m]\)*,左邊留下的是非空的合法序列的方案數。

這個狀態不是一般的好求

\[g_{Len, i, 1} = \sum_{j = 1}^{min(Len - 2, m)} g_{Len - j, i, 0} \]

轉移意義為在合法序列後加 \(j\)*

這個 \(j\) 列舉時仍要判斷對應位置是否能取 *,否則 break

然後就可以把 \(g_{Len, i, 0}\) 的轉移優化到 \(O(n)\)

\[g_{Len, i, 0} = \sum_{j = 0}^{Len - 2} ((g_{j, i, 0} + g_{j, i, 1}) * f_{Len - j, i + j})) \]

轉移表示合法序列還是合法序列後加 \((0, m]\)*,這些情況後面加完全和法序列,一定能得到合法序列。

總複雜度優化到了 \(O(n^3)\)

而且這時 \(f\) 的轉移也簡化了。

\[f_{Len, i} = g_{Len - 2, i + 1, 0} + g_{Len - 2, i + 1, 1} + \sum_{j = 1}^{min(Len - 2, m)} g_{Len - 2 - j, i + 1 + j, 0} \]

接下來是去掉預設源的考場程式碼:

unsigned long long Mod(1000000007);
unsigned long long f[505][505], g[505][505][2];
unsigned n, m;
unsigned A, B, C, D;
unsigned Cnt(0), Ans(0), Tmp(0);
char a[505];
signed main() {
  n = RD(), m = RD();
  scanf("%s", a + 1);
  for (unsigned i(n); i; --i) g[0][i][0] = f[0][i] = 1;
  for (unsigned Len(2); Len <= n; ++Len) {
    for (unsigned i(n - Len + 1); i; --i) {
      for (unsigned len(1); len < Len && len <= m; ++len) {
        if((a[i + Len - len] == '(') || (a[i + Len - len] == ')')) break;
        g[Len][i][1] += g[Len - len][i][0];
        if(g[Len][i][1] >= Mod) g[Len][i][1] -= Mod;
      }
      if((a[i] == '*') || (a[i] == ')') || (a[i + Len - 1] == '*') || (a[i + Len - 1] == '(')) {
        continue;
      }
      unsigned Tol(min(Len - 2, m));
      f[Len][i] = g[Len - 2][i + 1][0] + g[Len - 2][i + 1][1];
      if(f[Len][i] >= Mod) f[Len][i] -= Mod; 
      for (unsigned len(1); len <= m && len + 2 <= Len; ++len) {
        if((a[i + len] == '(') || (a[i + len] == ')')) break;
        f[Len][i] += g[Len - 2 - len][i + len + 1][0]; 
        if(f[Len][i] >= Mod) f[Len][i] -= Mod; 
      }
      g[Len][i][0] = f[Len][i];
      for (unsigned len(2); len + 2 <= Len; ++len) {
        g[Len][i][0] = (g[Len][i][0] + g[len][i][1] * f[Len - len][i + len]) % Mod;
      }
      for (unsigned len(2); len + 2 <= Len; ++len) {
        g[Len][i][0] = (g[Len][i][0] + g[len][i][0] * f[Len - len][i + len]) % Mod;
      }
    }
  }
  printf("%llu\n", g[n][1][0]);
	return 0;
}