1 条题解
-
0
题意理解 我们有一个有向图:
n n 个点, 2 m 2m 条边(因为每条“左括号边”都对应一条反向的“右括号边”)
每条边标记为某种括号类型的左括号 ( 或右括号 )
括号类型有 k k 种
要求统计点对 ( x , y ) (x,y)( x < y x<y)的数量,使得存在一条从 x x 到 y y 的路径,路径上边的标记按顺序组成一个合法的括号序列。
合法括号序列条件 一个合法括号序列需要满足:
左右括号数量相等
任意前缀中,左括号数量 ≥ 右括号数量
初步分析 直接枚举点对和路径不可行( n n 可达 3 × 10 5 3×10 5 )。
我们需要利用括号序列的性质来设计高效算法。
括号序列与栈 一个常见的技巧是:遇到左括号压栈,遇到右括号时,如果栈顶是匹配的左括号则弹出,否则非法。
在图上,我们可以把“栈”的状态设计进图搜索中。
状态表示 设状态为 ( u , S ) (u,S),其中:
u u 是当前节点
S S 是当前栈的内容(表示还未匹配的左括号序列)
但栈可能很长,直接存储不可行。
关键观察 由于括号类型有限( k ≤ n k≤n),我们可以用一个 k k 元组表示栈: 栈中只关心每种括号未匹配的左括号数量。
因为匹配必须是同种括号的左右括号匹配,不同种类括号互不影响。
所以栈状态可以表示为一个长度 k k 的数组 c n t [ 1.. k ] cnt[1..k], c n t [ w ] cnt[w] 表示第 w w 种括号未匹配的左括号数量。
状态转移 从状态 ( u , c n t ) (u,cnt) 出发:
如果走一条左括号边 u → v u→v,类型 w w,则 c n t ′ [ w ]
c n t [ w ] + 1 cnt ′ [w]=cnt[w]+1,其他不变。
如果走一条右括号边 u → v u→v,类型 w w,则:
如果 c n t [ w ]
0 cnt[w]>0,则 c n t ′ [ w ]
c n t [ w ] − 1 cnt ′ [w]=cnt[w]−1,其他不变(合法匹配)
如果 c n t [ w ]
0 cnt[w]=0,则此转移非法(右括号无法匹配)
初始状态: ( u , z e r o ) (u,zero),其中 z e r o zero 是全 0 数组。
目标状态: ( v , z e r o ) (v,zero) 表示栈为空,即完整匹配的括号序列。
问题转化 我们要求的是:对于所有 x < y x<y,是否存在从 x x 到 y y 的路径,使得从空栈开始,结束时空栈。
即:从 ( x , z e r o ) (x,zero) 能否到达 ( y , z e r o ) (y,zero)。
图规模问题 状态数是 n × ( L + 1 ) k n×(L+1) k ,其中 L L 是栈的最大深度。但 L L 可能很大,直接 BFS 状态爆炸。
进一步优化 注意到我们只关心栈为空的状态,因此我们可以考虑双向搜索:
从起点 x x 出发,记录能到达的状态 ( u , c n t ) (u,cnt) 从终点 y y 出发(在反向图上),记录能到达的状态 ( u , c n t ) (u,cnt) 如果存在某个中间节点 u u,使得正向能到达 ( u , S ) (u,S),反向能到达 ( u , S ) (u,S),那么 x → y x→y 存在合法路径。
但这样还是需要枚举点对,不行。
官方解法思路(分治 + 最短路径) 这是一个已知题,常用做法是分治 + Dijkstra:
我们给每个节点 u u 分配一个 k k 维向量 d i s t [ u ] dist[u],表示从某个起点出发到达 u u 时的最小“栈状态”(用某种度量表示)。
利用分治,每次处理起点在某一半,终点在另一半的点对。
在分治过程中,对起点集合进行多源 Dijkstra,状态为 ( u , c n t ) (u,cnt),但通过巧妙的状态表示,可以避免指数级状态数。
实际上,因为不同括号类型独立,我们可以把问题分解为 k k 个独立的括号匹配问题吗? 不行,因为路径上的括号顺序是混合的。
更可行的思路:BFS 状态压缩 由于 k ≤ n k≤n,但 n n 很大,不能直接指数枚举。 但数据范围中 k k 可能较小(测试点里有的 k
1 , 2 k=1,2),对于 k k 小的情况可以直接 BFS 状态 ( u , c n t ) (u,cnt)。
对于 k k 大的情况,需要利用图的性质。
另一种思路:转化为括号树 把括号序列看作树结构:
左括号:进入新子树
右括号:返回父节点
在图上,我们可以建立“括号自动机”来跟踪栈状态,然后用可达性分析。
实现方案(针对中小 k k) 对于 k ≤ 2 k≤2 的情况,可以直接 BFS 状态 ( u , c 1 , c 2 ) (u,c 1 ,c 2 )。
算法步骤:
枚举起点 x x
BFS 从 ( x , 0 , 0 , . . . , 0 ) (x,0,0,...,0) 开始
如果到达 ( y , 0 , 0 , . . . , 0 ) (y,0,0,...,0) 且 y
x y>x,则计数
用 visited 数组避免重复搜索同一状态
复杂度: O ( n ⋅ ( L + 1 ) k ⋅ degree ) O(n⋅(L+1) k ⋅degree),对于 k k 小且图稀疏时可接受。
代码框架( k
1 k=1 情况示例) cpp #include <bits/stdc++.h> using namespace std;
int n, m, k; vector<pair<int, int>> adj[300005]; // to, type (0=left, 1=right)
int main() { cin >> n >> m >> k; for (int i = 0; i < m; i++) { int u, v, w; cin >> u >> v >> w; // 左括号边 u->v adj[u].push_back({v, w}); // 正数表示左括号 // 右括号边 v->u
adj[v].push_back({u, -w}); // 负数表示右括号 }long long ans = 0; // 枚举起点 for (int start = 1; start <= n; start++) { // BFS 状态 (node, balance) queue<pair<int, int>> q; map<pair<int, int>, bool> vis; q.push({start, 0}); vis[{start, 0}] = true; while (!q.empty()) { auto [u, bal] = q.front(); q.pop(); if (bal == 0 && u > start) { ans++; // 找到合法点对 (start, u) } for (auto [v, type] : adj[u]) { int new_bal = bal; if (type > 0) { // 左括号 new_bal++; } else { // 右括号 if (new_bal > 0) { new_bal--; } else { continue; // 不合法 } } if (!vis.count({v, new_bal})) { vis[{v, new_bal}] = true; q.push({v, new_bal}); } } } } cout << ans << endl; return 0;} 大规模数据优化 对于 n n 大的情况,需要更高效的方法:
利用强连通分量缩点
动态规划计算可达性
分治处理点对
这类题目的正解通常比较复杂,需要结合括号自动机和图论优化。
总结 这道题的核心是:
将括号匹配的栈状态融入图搜索
状态表示为 ( u , c n t 1 , . . . , c n t k ) (u,cnt 1 ,...,cnt k ),利用 k k 较小的特点
通过 BFS/DFS 统计合法点对
对于大数据需要更复杂的优化(缩点、分治、DP)
由于时间和篇幅限制,这里给出了 k k 较小时的可行方案,大规模数据需要进一步优化。
- 1
信息
- ID
- 4940
- 时间
- 1000ms
- 内存
- 256MiB
- 难度
- 10
- 标签
- 递交数
- 1
- 已通过
- 1
- 上传者