G2. Light Bulbs (Hard Version)

G2. Light Bulbs (Hard Version)

The easy and hard versions of this problem differ only in the constraints on n. In the hard version, the sum of values of n over all test cases does not exceed 2105. Furthermore, there are no additional constraints on the value of n in a single test case.

There are 2n light bulbs arranged in a row. Each light bulb has a color from 1 to n (exactly two light bulbs for each color).

Initially, all light bulbs are turned off. You choose a set of light bulbs S that you initially turn on. After that, you can perform the following operations in any order any number of times:

  • choose two light bulbs i and j of the same color, exactly one of which is on, and turn on the second one;
  • choose three light bulbs i,j,k, such that both light bulbs i and k are on and have the same color, and the light bulb j is between them (i<j<k), and turn on the light bulb j.

You want to choose a set of light bulbs S that you initially turn on in such a way that by performing the described operations, you can ensure that all light bulbs are turned on.

Calculate two numbers:

  • the minimum size of the set S that you initially turn on;
  • the number of sets S of minimum size (taken modulo 998244353).

Input

The first line of the input contains a single integer t (1t104) — the number of test cases. Then follow the descriptions of the test cases.

The first line of each test case contains a single integer n (2n2105) — the number of pairs of light bulbs.

The second line of each test case contains 2n integers c1,c2,,c2n (1cin), where ci is the color of the i-th light bulb. For each color from 1 to n, exactly two light bulbs have this color.

Additional constraint on the input: the sum of values of n over all test cases does not exceed 2105.

Output

For each test case, output two integers:

the minimum size of the set S that you initially turn on;

the number of sets S of minimum size (taken modulo 998244353).

Example

input

4
2
2 2 1 1
2
1 2 2 1
2
1 2 1 2
5
3 4 4 5 3 1 1 5 2 2

output

2 4
1 2
1 4
2 8

 

解题思路

  前置知识:有向图的强连通分量,线段树优化建图。

  官方的题解看了几遍都看不懂,索性用思路相对简单但实现很复杂的做法。

  假设第 i 种颜色的灯在序列中出现的两个位置分别是 liri,当选择先打开 li 位置的灯时,根据第 1 个操作 ri 位置上的灯也会被打开。同理先打开 ri 位置的灯。因此对于同一种颜色的灯我们只选择其中一个位置的打开。另外根据第 2 个操作,当打开第 i 种颜色的灯后,区间 [li,ri] 中的灯都会被打开。

  容易发现这个过程具有传递性,因此考虑建图。把 2n 个位置看作是点,对于每种颜色 i,分别从 liri 向区间 [li,ri] 中的每个点都连一条有向边,表示打开 liri 位置上的灯后,边所指向的这些位置的灯也会被打开。

  最后因为要考虑选哪些点作为起始点,使得图中所有节点都可以被遍历到,因此考虑对这个图求强连通分量,把那些可以相互到达的点进行缩点,缩点后的图就会变成拓扑图(不一定连通)。此时所有入度为 0 的点(缩点后)必然是要选择的点,因为其入度为 0,即没有节点能访问到该节点,因此必须要选入度为 0 的节点。当把所有入度为 0 的点选择后,根据拓扑排序知道一定能遍历图中的所有点。

  因此要选择的点的最小值就是缩点后入度为 0 的点的数量,只需在所有该节点所表示的强连通分量中任意选择一个节点即可(因为强连通分量内的点可以互相到达)。因此选择的方案数也知道了,就是每个入度为 0 的点所表示的强连通分量的大小的乘积。

  现在最大的问题就是如何建图,先考虑 G1. Light Bulbs (Easy Version),由于 n 最大只有 1000,因此可以考虑暴力连边建图,即枚举每种颜色 i,然后遍历区间 [li,ri] 进行连边。这样整个图边的数量是 O(n2) 级别的。

  Easy Version 的 AC 代码如下,时间复杂度为 O(n2)

Code
#include <bits/stdc++.h>
using namespace std;

typedef long long LL;

const int N = 2010, M = N * N, mod = 998244353;

int l[N], r[N];
int head[N], e[M], ne[M], idx;
int dfn[N], low[N], sz;
int stk[N], tp, vis[N];
int id[N], sum[N], cnt;
int deg[N];

void add(int u, int v) {
    e[idx] = v, ne[idx] = head[u], head[u] = idx++;
}

void tarjan(int u) {
    dfn[u] = low[u] = ++sz;
    stk[++tp] = u, vis[u] = 1;
    for (int i = head[u]; i != -1; i = ne[i]) {
        if (!dfn[e[i]]) {
            tarjan(e[i]);
            low[u] = min(low[u], low[e[i]]);
        }
        else if (vis[e[i]]) {
            low[u] = min(low[u], dfn[e[i]]);
        }
    }
    if (dfn[u] == low[u]) {
        cnt++;
        sum[cnt] = 0;
        int t;
        do {
            t = stk[tp--];
            vis[t] = 0;
            id[t] = cnt;
            sum[cnt]++;
        } while (t != u);
    }
}

void solve() {
    int n;
    scanf("%d", &n);
    memset(l, 0, n + 10 << 2);
    for (int i = 1; i <= n << 1; i++) {
        int x;
        scanf("%d", &x);
        if (!l[x]) l[x] = i;
        else r[x] = i;
    }
    idx = 0;
    memset(head, -1, 2 * n + 10 << 2);
    for (int i = 1; i <= n; i++) {
        for (int j = l[i]; j <= r[i]; j++) {
            add(l[i], j);
            add(r[i], j);
        }
    }
    sz = cnt = 0;
    memset(dfn, 0, 2 * n + 10 << 2);
    for (int i = 1; i <= n << 1; i++) {
        if (!dfn[i]) tarjan(i);
    }
    memset(deg, 0, cnt + 10 << 2);
    for (int i = 1; i <= n << 1; i++) {
        for (int j = head[i]; j != -1; j = ne[j]) {
            int x = id[i], y = id[e[j]];
            if (x != y) deg[y]++;
        }
    }
    int ret = 0, s = 1;
    for (int i = 1; i <= cnt; i++) {
        if (!deg[i]) ret++, s = (LL)s * sum[i] % mod;
    }
    printf("%d %d\n", ret, s);
}

int main() {
    int t;
    scanf("%d", &t);
    while (t--) {
        solve();
    }
    
    return 0;
}

  现在的 n 变成了 2105,显然不可以像上面那样暴力建图。由于每次都是将单个点连向某个区间,因此可以考虑用线段树来优化建图。这是因为线段树可以用节点来表示一个区间,因此我们只需把边连向这些节点,这样就可以大大减少边的数量了。

  其中对于线段树中的节点,大区间所表示的节点向小区间所表示的节点连边,即每个父节点都会向其两个儿子连一条边,这是因为必然可以从大区间走到小区间。当然每个儿子也可以向其父节点连一条边,不过由于本题中只需考虑从单点向区间连边,因此为了方便这里仅从父节点向儿子连边。如下图维护区间 [1,4] 的线段树:

  考虑节点 4 向区间 [1,3] 中的点连边:

  另外我们把序列的每个位置单独成一个点来考虑(如上图中的 4 号节点),并让线段树中的叶子节点指向与其相应的点,如图:

  规定线段树用到的节点编号为 14n,序列节点用到的编号为 4n+15n。建树的代码如下:

void build(int u, int l, int r) {
    tr[u] = {l, r};
    if (l == r) {
        add(u, l + 4 * n);
    }
    else {
        int mid = l + r >> 1;
        build(u << 1, l, mid);
        build(u << 1 | 1, mid + 1, r);
        add(u, u << 1);
        add(u, u << 1 | 1);
    }
}

  单点连向区间的代码如下(x 表示映射后的序列节点编号):

void modify(int u, int l, int r, int x) {
    if (tr[u].l >= l && tr[u].r <= r) {
        add(x, u);
    }
    else {
        int mid = tr[u].l + tr[u].r >> 1;
        if (l <= mid) modify(u << 1, l, r, x);
        if (r >= mid + 1) modify(u << 1 | 1, l, r, x);
    }
}

  最后对这个图求强连通分量进行缩点。不过与上面说过的“选择的点的最小值是缩点后入度为 0 的点的数量”不同,此时强连通分量内只有序列节点(即编号为 4n+15n 的节点)才有贡献,线段树的节点没有贡献,意味着入度为 0 的点所表示的强连通分量可能都是由线段树的节点构成的。因此我们应该把这些点从拓扑图中删去。

  由于在求完强连通分量后,所得到的强连通分量的倒序就是缩点后的图的拓扑序,假设一共有 cnt 个强连通分量,只需要从第 cnt 个强连通分量倒序遍历,把入度为 0sumi0 的点(表示第 i 个强连通分量中序列节点的个数)删去,即枚举第 i 个强连通分量中的所有点,并把与这些点相邻的(不在同一个强连通分量内的)点的度数减 1

  最后再统计所有入度为 0,且 sumi 不为 0 的点即可。

  AC 代码如下,时间复杂度为 O(nlogn)

#include <bits/stdc++.h>
using namespace std;

typedef long long LL;

const int N = 2e6 + 10, M = 1.8e7 + 10, mod = 998244353;

int n;
int l[N], r[N];
int head[N], e[M], ne[M], idx;
int dfn[N], low[N], sz;
int stk[N], tp, vis[N];
int id[N], sum[N], cnt;
vector<int> scc[N];
int deg[N];
struct Node {
    int l, r;
}tr[N];

void add(int u, int v) {
    e[idx] = v, ne[idx] = head[u], head[u] = idx++;
}

void build(int u, int l, int r) {
    tr[u] = {l, r};
    if (l == r) {
        add(u, l + 4 * n);
    }
    else {
        int mid = l + r >> 1;
        build(u << 1, l, mid);
        build(u << 1 | 1, mid + 1, r);
        add(u, u << 1);
        add(u, u << 1 | 1);
    }
}

void modify(int u, int l, int r, int x) {
    if (tr[u].l >= l && tr[u].r <= r) {
        add(x, u);
    }
    else {
        int mid = tr[u].l + tr[u].r >> 1;
        if (l <= mid) modify(u << 1, l, r, x);
        if (r >= mid + 1) modify(u << 1 | 1, l, r, x);
    }
}

void tarjan(int u) {
    dfn[u] = low[u] = ++sz;
    stk[++tp] = u, vis[u] = 1;
    for (int i = head[u]; i != -1; i = ne[i]) {
        if (!dfn[e[i]]) {
            tarjan(e[i]);
            low[u] = min(low[u], low[e[i]]);
        }
        else if (vis[e[i]]) {
            low[u] = min(low[u], dfn[e[i]]);
        }
    }
    if (dfn[u] == low[u]) {
        cnt++;
        scc[cnt].clear();
        sum[cnt] = 0;
        int t;
        do {
            t = stk[tp--];
            vis[t] = 0;
            id[t] = cnt;
            scc[cnt].push_back(t);
            if (t > n << 2) sum[cnt]++;
        } while (t != u);
    }
}

void solve() {
    scanf("%d", &n);
    n <<= 1;
    memset(l, 0, n + 10 << 2);
    for (int i = 1; i <= n; i++) {
        int x;
        scanf("%d", &x);
        if (!l[x]) l[x] = i;
        else r[x] = i;
    }
    idx = 0;
    memset(head, -1, 5 * n + 10 << 2);
    build(1, 1, n);
    for (int i = 1; i <= n >> 1; i++) {
        modify(1, l[i], r[i], l[i] + 4 * n);
        modify(1, l[i], r[i], r[i] + 4 * n);
    }
    sz = cnt = 0;
    memset(dfn, 0, 5 * n + 10 << 2);
    for (int i = 1; i <= 5 * n; i++) {
        if (!dfn[i]) tarjan(i);
    }
    memset(deg, 0, cnt + 10 << 2);
    for (int i = 1; i <= 5 * n; i++) {
        for (int j = head[i]; j != -1; j = ne[j]) {
            int x = id[i], y = id[e[j]];
            if (x != y) deg[y]++;
        }
    }
    for (int i = cnt; i; i--) {
        if (!deg[i] && !sum[i]) {
            for (auto &x : scc[i]) {
                for (int j = head[x]; j != -1; j = ne[j]) {
                    if (i != id[e[j]]) deg[id[e[j]]]--;
                }
            }
        }
    }
    int ret = 0, s = 1;
    for (int i = 1; i <= cnt; i++) {
        if (!deg[i] && sum[i]) {
            ret++;
            s = (LL)s * sum[i] % mod;
        }
    }
    printf("%d %d\n", ret, s);
}

int main() {
    int t;
    scanf("%d", &t);
    while (t--) {
        solve();
    }
    
    return 0;
}

 

参考资料

  Codeforces Round 916 (Div. 3) A - G:https://zhuanlan.zhihu.com/p/673159169

  DS 优化建图:https://www.luogu.com.cn/blog/forever-captain/DS-optimize-graph

  「算法笔记」线段树优化建图 - maoyiting - 博客园:https://www.cnblogs.com/maoyiting/p/13764109.html

posted @   onlyblues  阅读(111)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
· 为什么 退出登录 或 修改密码 无法使 token 失效
Web Analytics
点击右上角即可分享
微信分享提示