Codeforces Round 916 (Div. 3) G2. Light Bulbs (Hard Version) (强连通分量)

原题链接: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 12 2 → 3 2 \rightarrow 3 23 3 → 1 3 \rightarrow 1 31,我们会缩成一个点,设为 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;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

柠檬味的橙汁

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值