原题链接:G2. Light Bulbs (Hard Version)
题目大意:
有 n n n 种颜色,每种颜色都有两个灯泡,灯泡都排成一行。
最初所有灯都是关闭的,你可以选择任意几个灯泡使它们打开,然后你可以做以下操作。
- 选择两个灯泡 i , j i,j i,j ,它们 同色,且其中一个灯泡亮了,你就可以打开另一个。
- 选择三个灯泡 i , j , k i,j,k i,j,k,且 i , k i,k i,k 同色且都亮了,并且 i < j < k i < j < k i<j<k ,则可以点亮灯泡 j j j 。
现在希望你选择一些灯泡,并且在一定的顺序操作后,可以使得所有灯泡都被点亮。
现在询问你:
- 最少需要点亮多少个灯泡,才能使得所有的灯泡都被点亮。
- 在满足最少数量的前提下,有多少种选择灯泡的方法,使得所有的灯泡都被点亮,对 998 998 998 244 244 244 353 353 353 取模后输出。
解题思路:
我们手玩一下,发现几个情况,无论怎么给出序列,都是下面几种情况的拼接:
假设序列是: [ 1 , 2 , 3 , 1 , 2 , 3 ] [1,2,3,1,2,3] [1,2,3,1,2,3] 。
我们选择 1 1 1 ,那么区间 [ 1 , 4 ] [1,4] [1,4] 所有的灯泡都能被点亮,而颜色 2 , 3 2,3 2,3 有一个端点被点亮了,所以它们的另一个端点也能被点亮。同理,选择 2 , 3 2,3 2,3 作为开始的灯泡也是一样的,我们这样操作能点亮的区间是整个 [ 1 , 6 ] [1,6] [1,6] ,而且是最小操作。
假设序列是: [ 1 , 2 , 3 , 3 , 1 , 2 ] [1,2,3,3,1,2] [1,2,3,3,1,2] 。
同样的,选择 1 , 2 1,2 1,2 ,就能点亮所有的灯泡,但我们如果选 3 3 3 ,它只会点亮 [ 3 , 4 ] [3,4] [3,4] 这个区间,还要额外再选 1 , 2 1,2 1,2 才能把整个序列都点亮,显然不是最小方案。
注意到,我们只需要开局选择点亮一个灯泡,之后就可以操作 2 2 2 ,操作 1 1 1 ,操作 2 2 2 , . . . ... ... ,这样循环下去,最重要的还是最初怎么选点来执行操作 2 2 2 。
我们把题意转化一下,把每个颜色 i i i 看成是一个节点 i i i ,当点 i i i 被选了,就可以同时去选择一些在区间 [ l i , r i ] [l_{i}, r_{i}] [li,ri] 之内的点 j j j ,但点 j j j 不一定能选回点 i i i ,这是一个有向图的形式。
如果按照这样的思路的话,我们就可以按照这样的情况画出上面两种情况的图:
对于第一个情况:
对于这种情况来说,我们无论最开始先选 1 , 2 , 3 1,2,3 1,2,3 的任意一个,都能把 1 , 2 , 3 1,2,3 1,2,3 点亮,因为他们同属于一个强连通分量。
对于第二个情况:
对于这种情况来说,我们无论最开始先选 1 1 1 ,还是 2 2 2 都能把 1 , 2 , 3 1,2,3 1,2,3 点亮,因为 1 , 2 1,2 1,2 属于同一个强连通分量,并且 3 3 3 是 1 , 2 1,2 1,2 能到达的点。
这启发我们做一个事情,跑强连通分量,然后缩点,同时记录缩点后的每个强连通分量里有多少个点,缩点完后的图一定是一张 D A G DAG DAG 图。
考虑缩点后的 D A G DAG DAG 图,我们想要选择最少的点使得整张图都被点亮,就只用选择那些入度为 0 0 0 的点,因为除了开始就点亮以外,没有其他办法能点亮它们。
入度为 0 0 0 的点的个数,就是我们要的最小操作数。
那么方案数呢?
也很简单,因为入度为 0 0 0 的点必选,且每个点要么是一个强连通分量要么是原图上的点,如果是强连通的点,比如上面的第一个样例 1 → 2 1 \rightarrow 2 1→2, 2 → 3 2 \rightarrow 3 2→3, 3 → 1 3 \rightarrow 1 3→1,我们会缩成一个点,设为 X X X,且点 X X X 包含原图的点的数量为 3 3 3 ,所以我们有三种方案。
同理上图的第二个样例的方案数就是 2 2 2 。
那么答案按乘法原理,就是所有入度为 0 0 0 的点的方案的乘积。
那么一个点向区间连边呢?这是个很典型的 t r i c k trick trick ,我们用 线段树优化建图 就好了。
要注意的是,如果用线段树优化建图,我们首先会造出一个虚拟图出来。
虚拟图上有很多不属于统计的范围内,但是入度为 0 0 0 的点,我们要把它们先删去,否则会影响答案,这里用拓扑排序删点就好了。
每个点建最多 O ( log n ) O(\log n) O(logn) 条边,一共有 n n n 个点,而跑 T a r j a n Tarjan Tarjan 是 O ( V + E ) O(V+E) O(V+E) 的。
所以时间复杂度为: O ( n log n ) O(n \log n) O(nlogn)
AC代码:
#include <bits/stdc++.h>
using namespace std;using PII = pair<int, int>;
using i64 = long long;//强连通分量板子
struct SCC {int n, c_scc, idx;vector<int> stk;vector<int> dfn, low, scc, siz;vector<vector<int>> g;SCC() {};SCC(int _) { init(_); }void init(int _) {this->n = _;g.assign(_ + 1, {});dfn.resize(_ + 1);low.resize(_ + 1);scc.resize(_ + 1);siz.resize(1);stk.clear();idx = c_scc = 0;}void addEdge(int u, int v) {g[u].emplace_back(v);}void DFS(int u) {dfn[u] = low[u] = ++idx;stk.emplace_back(u);for (auto& v : g[u]) {if (!dfn[v]) {DFS(v);low[u] = min(low[u], low[v]);} else if (!scc[v]) {low[u] = min(low[u], dfn[v]);}}if (dfn[u] == low[u]) {int top = -1, cnt = 0; ++c_scc;while (top != u) {top = stk.back(); stk.pop_back();scc[top] = c_scc; ++cnt;}siz.emplace_back(cnt);}}void work() {for (int i = 1; i <= n; ++i) {if (!dfn[i]) DFS(i);}}
};const int mod = 998244353;void solve() {int n;cin >> n;n <<= 1;SCC g(n * 4);vector<PII> line(n >> 1);for (int i = 1; i <= n; ++i) {int x;cin >> x;auto& [l, r] = line[--x];if (!l) {l = i;} else {r = i;}}//build建虚拟图 那么叶子节点的节点编号就对应我们的 1,2,...,n 号点了vector<int> leaf(n + 1);auto build = [&](auto self, int k, int l, int r) -> void {if (l == r) {leaf[l] = k;return;}int mid = l + r >> 1;self(self, k << 1, l, mid);self(self, k << 1 | 1, mid + 1, r);g.addEdge(k, k << 1);g.addEdge(k, k << 1 | 1);};build(build, 1, 1, n);//线段树优化建图auto connect = [&](auto self, int k, int l, int r, int x, int y, int node) -> void {if (l >= x && r <= y) {g.addEdge(node, k);return;}int mid = l + r >> 1;if (x <= mid) self(self, k << 1, l, mid, x, y, node);if (y > mid) self(self, k << 1 | 1, mid + 1, r, x, y, node);};//每个点 i 向区间 [l,r] 用线段树优化建图for (auto& [l, r] : line) {connect(connect, 1, 1, n, l, r - 1, leaf[r]);connect(connect, 1, 1, n, l + 1, r, leaf[l]);}g.work();//记录每个强连通分量有多少个原图内的点vector<int> cnt(g.c_scc + 1);for (int i = 1; i <= n; ++i) {++cnt[g.scc[leaf[i]]];}vector<int> in(g.c_scc + 1);//建缩点后的图:vector<vector<int>> G(g.c_scc + 1);for (int i = 1; i <= n * 4; ++i) {for (auto& v : g.g[i]) {if (g.scc[i] != g.scc[v]) {G[g.scc[i]].emplace_back(g.scc[v]);++in[g.scc[v]];}}}//由于我们是在线段树上的虚拟图跑的Tarjan 所以我们要除去那些没用的点//只保留 缩点后点内至少有一个原图的点 的那些点queue<int> que;for (int i = 1; i <= g.c_scc; ++i) {if (!in[i]) {que.push(i);}}while (que.size()) {int u = que.front(); que.pop();for (auto& v : G[u]) {if (--in[v] == 0) {if (!cnt[v]) {que.push(v);}}}}//如果入度为0且是合法点 即我们缩点点内至少有一个原图的点才是符合统计范围内的点i64 ans1 = 0, ans2 = 1;for (int i = 1; i <= g.c_scc; ++i) {if (!in[i] && cnt[i]) {++ans1;ans2 = ans2 * cnt[i] % mod;}}cout << ans1 << " " << ans2 << '\n';
}signed main() {ios::sync_with_stdio(0);cin.tie(0), cout.tie(0);int t = 1; cin >> t;while (t--) solve();return 0;
}