前言
題目連結:Hydro & bzoj;黑暗爆炸。
題意簡述
給你一張 \(n\) 個點 \(m\) 條邊的有向圖。有 \(p\) 種括號,每條邊的邊權可以是這 \(p\) 種括號中某一種的左括號或者右括號,也可以為空。問你有多少條從 \(1\) 開始到 \(n\) 的長度小於等於 \(k\) 的路徑,滿足括號匹配,或者剩餘若干未配對的左括號。答案對 \(10007\) 取模。
\(2 \leq n \leq 50\),\(1 \leq m \leq n(n-1)\),\(1 \leq k \leq 50\),\(p = 26\)。
題目分析
一眼 DP。考慮狀態,顯然不可以把棧壓到狀態裡,那我們乾脆不記棧了,設 \(f_{i, u, v}\) 表示 \(u\) 到 \(v\),經過了恰好 \(i\) 條邊,且此時棧為空的方案數。但是答案可以剩餘左括號,所以不妨再記 \(h_{i, u, v}\) 表示棧中剩餘若干未匹配的左括號,並且這些括號會一直留到最後。
這看起來十分正確,我們可以列舉中轉點 \(w\),再列舉 \(u \rightarrow w\) 和 \(w \rightarrow v\) 分別經過了多少條邊,兩個子問題方案數累乘後做一遍累加即可:\(f_{i, u, v} = \sum \limits _ {w = 1} ^ n \sum \limits _ {t = 1} ^ {i - 1} f_{t, u, w} f_{i - t, w, v}\)。而 \(h\) 只不過是多了一種無用左括號的轉移:\(g_{i, u, v} = \sum \limits _ {w \in \operatorname{out}(u)} g_{i - 1, w, v}\)。
事實上,這種做法存在「重複統計」的問題。具體來講,如果一條路徑形如 \(u {\tt ()()()} v\),會被 \(u {\color{red} \tt ()} w {\color{magenta} \tt ()()} v\)、\(u {\color{red} \tt ()()} w {\color{magenta} \tt ()} v\) 統計到。怎麼解決呢?
事實上,回顧我們序列上的括號匹配問題,即「區間 DP」,同樣存在這種重複統計。那我們是怎麼去除呢?發現,只要保證轉移的時候,是「第一次」進行某一個操作,例如,「第一次」放置一個矩形([CEOI2009] photo),就能夠保證每一種情況只會被統計到一次(因為「第一次」是唯一的)。括號匹配的「第一次」便是「第一次」成為一個匹配的括號串。
區間上如此,圖上亦如此。我們考慮列舉的中轉點是「第一次」成為一個匹配的括號串的位置,也即,保證 \(u \rightarrow w\) 的路徑兩端是一對匹配上的括號(\(u\ \texttt{(...)}\ v\))。我們可以記一個 \(g_{i, u, v}\),和 \(f\) 定義類似,只不過保證 \(u \rightarrow v\) 的路徑兩端是一對匹配上的括號。這樣,我們的 \(f\) 就能夠透過 \(g\) 和 \(f\) 的路徑拼接而來:\(f_{i, u, v} = \sum \limits _ {w = 1} ^ n \sum \limits _ {t = 1} ^ {\color{red} \mathbf{i}} g_{t, u, w} f_{i - t, w, v}\)。注意,此時 \(t\) 的上界是 \(i\),因為對於 \(u\ \texttt{(...)}\ v\) 本身就合法。當然,\(h\) 同樣如此。
我們把目光聚焦在求出 \(g\) 身上。我們不難發現,如果把最外層的那對括號扒掉,不就剩下一個形如 \(f\) 的括號串了嗎?所以,我們只需要在 \(f_{i - 2}\) 的基礎上,外層包裹一對括號即可:\(g_{i, u, v} = \sum \limits _ {tp = 1} ^ {p} \sum \limits _ {(u', tp) \in \operatorname{out}(u)} \sum \limits _ {(v', tp) \in \operatorname{in}(v)} f_{i - 2, u', v'}\)。
答案沒什麼好說的,就是 \(\sum \limits _ {i = 0} ^ k h_{i, 1, n}\)。
時間複雜度 \(\Theta(kn^4 + k^2n^3) = \Theta(kn^3(n + k))\),空間複雜度 \(\Theta(kn^2)\)。
程式碼
#include <cstdio>
const int N = 51;
using mint = unsigned short;
const mint mod = 10007;
inline mint add(mint a, mint b) { return a >= mod - b ? a + b - mod : a + b; }
inline mint mul(mint a, mint b) { return int(a) * b % mod; }
inline void toadd(mint &a, mint b) { a = add(a, b); }
int n, m, k;
char out[N][N], in[N][N], op;
bool emp[N][N];
mint f[N][N][N], h[N][N][N], g[N][N][N];
signed main() {
scanf("%d%d%d", &n, &m, &k);
for (int u, v; m--; ) {
scanf("%d%d", &u, &v);
while ((op = getchar()) == ' ');
if ('A' <= op && op <= 'Z') out[u][v] = op - 'A' + 'a';
else if ('a' <= op && op <= 'z') in[u][v] = op;
else emp[u][v] = true;
}
for (int i = 1; i <= n; ++i) f[0][i][i] = h[0][i][i] = 1;
for (int i = 1; i <= k; ++i) {
if (i >= 2) {
for (int u = 1; u <= n; ++u)
for (int $u = 1; $u <= n; ++$u) if (out[u][$u])
for (int v = 1; v <= n; ++v)
for (int $v = 1; $v <= n; ++$v) if (out[u][$u] == in[$v][v])
toadd(g[i][u][v], f[i - 2][$u][$v]);
}
for (int w = 1; w <= n; ++w)
for (int u = 1; u <= n; ++u)
for (int v = 1; v <= n; ++v) {
if (emp[u][w]) toadd(f[i][u][v], f[i - 1][w][v]);
if (emp[u][w] || out[u][w]) toadd(h[i][u][v], h[i - 1][w][v]);
for (int l = 1; l <= i; ++l) {
toadd(f[i][u][v], mul(g[l][u][w], f[i - l][w][v]));
toadd(h[i][u][v], mul(g[l][u][w], h[i - l][w][v]));
}
}
}
mint ans = 0;
for (int i = 0; i <= k; ++i) toadd(ans, h[i][1][n]);
printf("%hu", ans);
return 0;
}