Codeforces Round 1079 (Div. 2)A,B,C,D,E1,E2,F个人题解

A. 友好数字

  • #数学 #枚举
    每个测试时间限制1秒
    每个测试内存限制256兆字节

对于一个整数 x x x,如果另一个整数 y y y 满足以下条件,我们称 y y y 是友好的:

y − d ( y ) = x y - d(y) = x y−d(y)=x,其中 d ( y ) d(y) d(y) 是 y y y 的各位数字之和。

对于给定的整数 x x x,确定它有多少个友好数字。

输入

每个测试包含多个测试用例。第一行包含测试用例的数量 t t t( 1 ≤ t ≤ 500 1 \leq t \leq 500 1≤t≤500)。接下来是测试用例的描述。

每个测试用例由一行组成,包含一个整数 x x x( 1 ≤ x ≤ 10 9 1 \leq x \leq 10^9 1≤x≤109)。

输出

对于每个测试用例,输出一个整数------问题的答案。

思路

本题上来在草稿纸上列举,很容易想到一个错误的答案:所有 % 9 = = 0 \% 9==0 %9==0的数

因为枚举 0 ∼ 70 0\sim 70 0∼70作为 y y y来打表,发现所有满足的 x x x都为9的倍数,所以便交了一发。但是实际上这仅仅是必要条件,也就是说,所有 x x x必然是9的倍数,但是9的倍数不一定是合法的 x x x

正确的思路应当是尝试枚举接近 x x x的100个数,因为 y > x + 100 y>x+100 y>x+100时 y − d ( y ) > x y-d(y)>x y−d(y)>x

为了更节省时间,只需要枚举接近 x x x的10个个位为0的数字即可,因为你会发现在 y y y的个位由0变化到9的过程中 y − d ( y ) y-d(y) y−d(y)的值是不变的

代码

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define int ll 
#define rep(i,a,b) for(int i=(a);i<=(b);i++)
#define per(i,a,b) for(int i=(a);i>=(b);i--)
 
int d(int x) {
    int res = 0;
    while (x) {
        res += x % 10;
        x /= 10;
    }
    return res;
}
 
void solve() {
    int x;cin >> x;
    rep(k, x / 10, x / 10 + 10) {
        if (10 * k - d(k) == x) {
            cout << 10 << '\n';return;
        }
    }
    cout << 0 << '\n';
}
 
signed main() {
    ios::sync_with_stdio(0);
    cin.tie(0), cout.tie(0);
    int t = 1;
    cin >> t;
    while (t--)solve();
}

B. 数组与排列

  • #模拟
    每个测试的时间限制为1.5秒
    每个测试的内存限制为256 MB

给定一个长度为 n n n 的排列 p p p 和一个长度为 n n n 的数组 a a a。

如果数组 a a a 可以通过对排列 p p p 进行若干次(可能为零次)以下类型的操作得到,那么我们称排列 p p p 是数组 a a a 的生成排列:

选择一个下标 i i i ( 1 ≤ i < n 1 \le i < n 1≤i<n)并执行以下两种替换之一:

  • p i : = p i + 1 p_i := p_{i+1} pi:=pi+1;
  • p i + 1 : = p i p_{i+1} := p_i pi+1:=pi。

换句话说,在一次操作中,你可以选择数组中的两个相邻元素,并将其中一个的值复制到另一个。

你需要报告排列 p p p 是否是数组 a a a 的生成排列。

*长度为 n n n 的排列是一个由 1 1 1 到 n n n 之间的 n n n 个不同整数以任意顺序组成的数组。例如, [ 2 , 3 , 1 , 5 , 4 ] [2,3,1,5,4] [2,3,1,5,4] 是一个排列,但 [ 1 , 2 , 2 ] [1,2,2] [1,2,2] 不是排列(2 在数组中出现了两次), [ 1 , 3 , 4 ] [1,3,4] [1,3,4] 也不是排列( n = 3 n=3 n=3 但数组中出现了 4)。

输入

每个测试包含多个测试用例。第一行包含测试用例的数量 t t t ( 1 ≤ t ≤ 10 4 1 \le t \le 10^4 1≤t≤104)。接下来是测试用例的描述。

每个测试用例的第一行包含一个整数 n n n ( 2 ≤ n ≤ 2 ⋅ 10 5 2 \le n \le 2 \cdot 10^5 2≤n≤2⋅105)------数组和排列的长度。

每个测试用例的第二行包含 n n n 个整数 p 1 , p 2 , ... , p n p_1, p_2, \dots, p_n p1,p2,...,pn ( 1 ≤ p i ≤ n 1 \le p_i \le n 1≤pi≤n)------排列的元素。

每个测试用例的第三行包含 n n n 个整数 a 1 , a 2 , ... , a n a_1, a_2, \dots, a_n a1,a2,...,an ( 1 ≤ a i ≤ n 1 \le a_i \le n 1≤ai≤n)------数组的元素。

保证所有测试用例的 n n n 的总和不超过 2 ⋅ 10 5 2 \cdot 10^5 2⋅105。

输出

对于每个测试用例,如果排列 p p p 是数组 a a a 的生成排列,则输出"YES",否则输出"NO"。

你可以以任何大小写形式输出每个字母(小写或大写)。例如,字符串"yEs"、"yes"、"Yes"和"YES"都将被视为肯定回答。

思路

本题在进行一定地分析后,可以转化为序列a与p的元素出现顺序问题

假设序列a中存在形如 { a , a , ... , a , b , b , ... , b , c , c , ... , c } \{ a,a, \dots,a,b,b, \dots,b,c,c, \dots,c \} {a,a,...,a,b,b,...,b,c,c,...,c}的部分,那么就要求在排列p中, a , b , c a,b,c a,b,c的相对顺序为 { a , b , c } \{ a,b,c \} {a,b,c},这样就可以通过倒序遍历排列p的方式,逐步构造出序列a

因此,我们只需要遍历序列a,在前后元素不相同的时候,记录记录元素的相对顺序,再遍历一边排列p进行验证即可

为了实现这个功能,我们可以创建一个二维数组, c n t [ i ] cnt[i] cnt[i]中存储着所有相对位置小于数字 i i i的值

在遍历排列p的过程中,遍历过的打上 v i s vis vis标记,那么当前数字 p [ i ] p[i] p[i]而言, c n t [ p [ i ] ] cnt[p[i]] cnt[p[i]]内的元素必定要全部 v i s vis vis过才合法

代码

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define int ll 
#define rep(i,a,b) for(int i=(a);i<=(b);i++)
#define per(i,a,b) for(int i=(a);i>=(b);i--)
 
 
void solve() {
    int n;cin >> n;
    vector<int>a(n + 1);
    vector<int>p(n + 1);
    vector<vector<int>>cnt(n + 1);
    rep(i, 1, n)cin >> p[i];
    rep(i, 1, n) {
        cin >> a[i];
        if (i >= 2 && a[i] != a[i - 1]) {
            cnt[a[i]].push_back(a[i - 1]);
        }
    }
    vector<bool>vis(n + 1);
    rep(i, 1, n) {
        vis[p[i]] = 1;
        for (auto son : cnt[p[i]]) {
            if (!vis[son]) {
                cout << "NO\n";
                return;
            }
        }
    }
    cout << "YES\n";
}
 
signed main() {
    ios::sync_with_stdio(0);
    cin.tie(0), cout.tie(0);
    int t = 1;
    cin >> t;
    while (t--)solve();
}

C. 分数游戏

  • #博弈论 #数学
    每道测试时间限制:2 秒
    每道测试内存限制:256 兆字节

爱丽丝和鲍勃有两个整数 p p p 和 q q q,他们正在用这两个数字玩游戏。玩家轮流行动,爱丽丝先手。在轮到某位玩家时,他可以执行以下两种操作之一:

  • 将 p p p 减少一(此操作仅在 p > 0 p>0 p>0 时可行);
  • 将 q q q 减少一(此操作仅在 q > 1 q>1 q>1 时可行)。

游戏在 p = 0 p=0 p=0 且 q = 1 q=1 q=1 时结束。

如果在游戏过程中的任意时刻,分数 p q \frac{p}{q} qp 的值等于分数 2 3 \frac{2}{3} 32,则鲍勃获胜。否则,爱丽丝获胜。

给定 p p p 和 q q q 的初始值,假设双方都采取最优策略,请确定游戏的获胜者。

输入

每个测试包含多个测试用例。第一行包含测试用例的数量 t t t( 1 ≤ t ≤ 10 4 1 \le t \le 10^4 1≤t≤104)。接下来是每个测试用例的描述。

每个输入用例由一行组成,包含两个整数 p p p 和 q q q( 1 ≤ p , q ≤ 10 18 1 \le p, q \le 10^{18} 1≤p,q≤1018)。

输出

对于每个输入用例,输出:

  • 如果爱丽丝获胜,输出 "Alice";
  • 如果鲍勃获胜,输出 "Bob"。

思路

首先感性理解一下游戏过程,Bob能赢的情况只有双方不得不p-1,q-1,p-1,q-1,...的时候才会出现,其他情况都由Alice控制输赢
令 x − z y − z = 2 3    ⟹    3 x − 2 y = z \begin{align} &令 \frac{x-z}{y-z}=\frac{2}{3}\\ \\ \implies &3x-2y=z \end{align} ⟹令y−zx−z=323x−2y=z

显然当 z = x z=x z=x或者 z = y z=y z=y的时候, x − z y − z \frac{x-z}{y-z} y−zx−z要么为0要么非法, z > min ⁡ { x , y } z>\min\{ x,y \} z>min{x,y}的时候分子或者分母将率先出现负数,因此 z ≥ min ⁡ { x , y } z\geq \min\{ x,y \} z≥min{x,y}的时候Alice赢

其次,如果算出来 z < 0 z<0 z<0,那么说明是需要进行p+1,q+1的操作的,题干说只能减,所以也不合法,Alice赢

剩下的就是Bob赢

代码

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define int ll 
#define rep(i,a,b) for(int i=(a);i<=(b);i++)
#define per(i,a,b) for(int i=(a);i>=(b);i--)
 
 
void solve() {
    int x, y;cin >> x >> y;
    int z = 3 * x - 2 * y;
    if (z >= min(x, y) || z < 0)cout << "Alice\n";
    else cout << "Bob\n";
}
 
signed main() {
    ios::sync_with_stdio(0);
    cin.tie(0), cout.tie(0);
    int t = 1;
    cin >> t;
    while (t--)solve();
}

D. 关于美丽数对的另一道题

  • #根号分治
    每个测试的时间限制为 2 秒
    每个测试的内存限制为 256 兆字节

在数组 a a a 中,如果下标对 i i i、 j j j 满足以下条件,我们称之为美丽的:

a i ⋅ a j = j − i a_i \cdot a_j = j - i ai⋅aj=j−i

计算数组 a a a 中美丽数对的数量。

输入格式

每个测试包含多个测试用例。第一行包含测试用例的数量 t t t( 1 ≤ t ≤ 10 4 1 \le t \le 10^4 1≤t≤104)。接下来是测试用例的描述。

每个测试用例的第一行包含一个整数 n n n( 2 ≤ n ≤ 2 ⋅ 10 5 2 \le n \le 2 \cdot 10^5 2≤n≤2⋅105)。

每个测试用例的第二行包含 n n n 个整数 a i a_i ai( 1 ≤ a i ≤ 10 9 1 \le a_i \le 10^9 1≤ai≤109)。

输入的附加约束:

所有测试用例的 n n n 之和不超过 2 ⋅ 10 5 2 \cdot 10^5 2⋅105。

输出格式

对于每个测试用例,输出一个整数------问题的答案。

思路

a i ⋅ a j = j − i ≤ n a_{i}\cdot a_{j}=j-i\leq n ai⋅aj=j−i≤n

根据这一放缩,我们发现有许多的组合是不合法的,仅有相乘后小于等于n的组合才被我们考虑在内

又因为 n ≤ 2 e 5 n\leq 2e 5 n≤2e5,因此很容易想到猜测本题复杂度与 O ( n ) O(\sqrt{ n }) O(n )相关,那么便是根号分治了

我们设 1 ≤ i ≤ j ≤ n 1\leq i\leq j\leq n 1≤i≤j≤n, u = a i , v = a j u=a_{i}\ ,\ v=a_{j} u=ai , v=aj

合法条件变为: u ⋅ v = j − i u\cdot v=j-i u⋅v=j−i

先考虑 a i ≤ n a_{i}\leq \sqrt{ n } ai≤n 的情况:

  • 由于 O ( n n ) O(n\sqrt{ n }) O(nn )的复杂度是可以接受的,我们可以暂时将所有 O ( n ) O(\sqrt{ n }) O(n )复杂度的操作想成 O ( 1 ) O(1) O(1),再去思考一个 O ( n ) O(n) O(n)的解法
  • 在固定一个 j j j的情况下,可以 O ( n ) O(\sqrt{ n }) O(n )的时间内找出所有合法的 i i i
  • 枚举 1 ≤ j ≤ n 1\leq j\leq n 1≤j≤n,那么对于一个 j j j,可以在 O ( n ) O(\sqrt{ n }) O(n )的时间内枚举所有的 u ≤ n u\leq \sqrt{ n } u≤n , u ⋅ a j = j − i    ⟹    i = j − u ⋅ a j u\cdot a_{j}=j-i\implies i=j-u\cdot a_{j} u⋅aj=j−i⟹i=j−u⋅aj,再判断是否有 a i = = u a_{i}==u ai==u即可
  • 当然,要注意下标 i i i的合法性

再考虑 a i > n a_{i}>\sqrt{ n } ai>n 的情况:

  • 由于 a i > n a_{i}>\sqrt{ n } ai>n ,那么必然要有 a j < n a_{j}<\sqrt{ n } aj<n ,因此对于每一个 i i i,我们可以 O ( n ) O(\sqrt{ n }) O(n )找出所有合法的 j j j
  • 枚举 1 ≤ i ≤ n 1\leq i\leq n 1≤i≤n,在 a i > n a_{i}>\sqrt{ n } ai>n 的前提下, O ( n ) O(\sqrt{ n }) O(n )枚举所有的 v ≤ n v\leq \sqrt{ n } v≤n , a i ⋅ v = j − i    ⟹    j = i + a i ⋅ v a_{i}\cdot v=j-i\implies j=i+a_{i}\cdot v ai⋅v=j−i⟹j=i+ai⋅v,再判断是否有 a j = = v a_{j}==v aj==v即可
  • 当然,也要注意下标 j j j的合法性

代码

cpp 复制代码
#pragma GCC optimize(3, "Ofast", "inline")
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define int ll 
#define rep(i,a,b) for(int i=(a);i<=(b);i++)
#define per(i,a,b) for(int i=(a);i>=(b);i--)
 
 
void solve() {
    int n;cin >> n;
    vector<int>a(n + 1);
    rep(i, 1, n) cin >> a[i];
    int B = sqrt(n), ans = 0;
    rep(j, 1, n) {
        int v = a[j];
        rep(u, 1, B) {
            if (j - u * v < 1)break;
            if (a[j - u * v] == u)ans++;
        }
    }
    rep(i, 1, n) {
        if (a[i] <= B)continue;
        int u = a[i];
        rep(v, 1, B) {
            if (i + u * v > n)break;
            if (a[i + u * v] == v)ans++;
        }
    }
    cout << ans << '\n';
}
 
signed main() {
    ios::sync_with_stdio(0);
    cin.tie(0), cout.tie(0);
    int t = 1;
    cin >> t;
    while (t--)solve();
}

E1&E2. 交互式图

  • #交互 #图
    单点时间限制2秒
    内存限制256MB

这是本题的困难版本。不同版本之间的区别是,在此版本中,你最多可以询问 n + m n+m n+m 个问题,并且 n ≤ 30 n \leq 30 n≤30。只有在你解决了此问题的所有版本后,才能进行hack。

这是一个交互式问题。

裁判想好了一个无环、无自环、无重边的有向无环图,该图有 n n n 个顶点和 m m m 条边。

你的任务是确定图中包含哪些边。为此,你可以提出以下形式的问题:在图的所有路径按字典序 ∗ ^* ∗排序的列表中,第 k k k 条路径是什么。

图中的路径是一个顶点序列 u 1 , u 2 , ... , u l u_1, u_2, \dots, u_l u1,u2,...,ul,使得对于任意 i < l i < l i<l,图中存在边 ( u i , u i + 1 ) (u_i, u_{i+1}) (ui,ui+1)。

你的任务是通过不超过 n + m n+m n+m 次询问来完成此目标。

∗ ^* ∗序列 a a a 字典序小于序列 b b b 当且仅当以下条件之一成立:

  • a a a 是 b b b 的前缀,但 a ≠ b a \ne b a=b;或者
  • 在 a a a 和 b b b 第一个不同的位置上,序列 a a a 的元素小于 b b b 中对应的元素。

输入

每个测试包含多个测试用例。第一行包含测试用例的数量 t t t ( 1 ≤ t ≤ 10 ) (1 \le t \le 10) (1≤t≤10)。测试用例描述如下。

每个测试用例由一行一个整数 n n n ( 1 ≤ n ≤ 30 ) (1 \le n \le 30) (1≤n≤30) 组成 ------ 表示图中的顶点数。

裁判保证给定的图不包含环或多重边。

注意 m m m 对你来说是未知的!

交互

每个测试用例的交互从读取整数 n n n 开始。

之后你可以提出最多 n + m n+m n+m 个问题。

要提出问题,请以 "? k" 格式(不带引号) ( 1 ≤ k ≤ 2 30 ) (1 \le k \le 2^{30}) (1≤k≤230) 输出一个字符串。每次询问后,读取一个整数 q q q ------ 第 k k k 条路径的顶点数。如果 q = 0 q = 0 q=0,则这样的路径不存在;否则,读取 q q q 个整数 ------ 组成此路径的顶点编号。

要报告你的答案,首先输出一个格式为 "! m" 的字符串,然后输出 m m m 行描述边,格式为 "u v",表示存在一条从 u u u 指向 v v v 的边。你可以以任意顺序输出边。输出答案不计入询问次数。

可以证明,在给定的约束条件下,图中不同路径的数量不超过 2 30 2^{30} 230。

每次打印查询后,不要忘记输出换行符并刷新 ∗ ^* ∗输出。否则,你将得到超出闲置限制的判定结果。

如果在任何交互步骤中,你读取到 − 1 -1 −1 而不是有效数据,你的程序必须立即退出。这意味着你的解决方案将因无效查询或任何其他错误而收到错误答案。未能退出可能会导致任意判决,因为你的解决方案将继续从已关闭的流中读取。

思路

本题的询问次数卡得非常死, n n n次单点询问, m m m次边询问,所以我们对于每一次询问,都必须得到一个有效的答案。

我们以下面的路径作为例子:
1 2 2 → 1 2 → 3 3 4 5 5 → 2 5 → 2 → 1 5 → 2 → 3 5 → 3 \begin{align} &1 \\ &2 \\ &2\to 1 \\ &2\to 3 \\ &3 \\ &4 \\ &5 \\ &5\to 2 \\ &5\to 2\to 1 \\ &5\to 2\to 3 \\ &5\to 3 \end{align} 122→12→33455→25→2→15→2→35→3

记 s i z [ u ] siz[u] siz[u]为以 u u u作为起点的路径数,那么不难发现,从第一条路径开始向下遍历,在第二次遇见结尾为 u u u的时候,接下来的 s i z [ u ] − 1 siz[u]-1 siz[u]−1条路的后缀必定和之前遇见 u u u的一样

比如例子中的2号点,在出现 5 → 2 5\to 2 5→2的时候,我们知道接下来必定有两条路径是可以跳过的,因为 s i z [ 2 ] = 3 siz[2]=3 siz[2]=3,即 { 2 , 2 → 1 , 2 → 3 } \{ 2,2\to 1,2\to 3 \} {2,2→1,2→3}

所以我们可以开一个标记数组, v i s [ u ] = 1 vis[u]=1 vis[u]=1表示 u u u点已经访问过一次了

每次读入路径的时候,都将结尾元素连到前一个上面,这一定是一条新边

如果发现结尾元素已经 v i s vis vis过了,那么就跳过接下来的 s i z [ b a c k ] siz[back] siz[back]条路径继续向下遍历,这样就可以保证每次询问都必定得到一个有效信息

总共询问次数为 n + m + 1 n+m+1 n+m+1,因为在询问完所有的路径之后,我们无法判断什么时候询问应该停止,多问一次 n + 1 n+1 n+1这个点,如果返回了0那么就退出

比如,如果在上述例子的结尾多加一个 5 → 4 5\to 4 5→4,那么也是一个合法的样例,但是如果不询问是否停止,程序将无法区分这两个样例的区别

当然,这样的策略只可以通过 E 1 E_{1} E1,但是通过 E 2 E_{2} E2其实只需要再稍加一点点修改即可:

注意到,无论如何,第一个访问的路径必然是 { 1 } \{ 1 \} {1},因此,我们在开始询问之前令 v i s [ 1 ] = 1 vis[1]=1 vis[1]=1,跳过第一条询问,这样就可以节省出一个询问用于查询是否结束

代码

cpp 复制代码
#pragma GCC optimize(3, "Ofast", "inline")
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define int ll 
#define rep(i,a,b) for(int i=(a);i<=(b);i++)
#define per(i,a,b) for(int i=(a);i>=(b);i--)

void ask(int k) {
    cout << "? " << k << endl;
}

vector<vector<int>>e;

void solve() {
    int n;cin >> n;
    if (n == 1) {
        cout << "! 0\n";return;
    }
    int id = 2;
    vector<int>tmp;
    e.assign(n + 1, tmp);
    vector<int>siz(n + 1);
    vector<bool>vis(n + 1);
    siz[1] = 1;
    vis[1] = 1;
    while (1) {
        ask(id);
        int cnt;cin >> cnt;
        if (cnt == 0)break;
        vector<int>now(cnt + 1);
        rep(i, 1, cnt)cin >> now[i];
        if (cnt >= 2) {
            e[now[now.size() - 2]].push_back(now.back());
        }
        if (vis[now.back()]) {
            rep(i, 1, cnt - 1) {
                siz[now[i]] += siz[now.back()];
            }
            id += siz[now.back()];
            continue;
        }
        vis[now.back()] = 1;
        rep(i, 1, cnt) {
            siz[now[i]]++;
        }
        id += siz[now.back()];
    }
    int m = 0;
    rep(i, 1, n) {
        m += e[i].size();
    }
    cout << "! " << m << endl;
    rep(i, 1, n) {
        for (auto son : e[i]) {
            cout << i << " " << son << endl;
        }
    }
}

signed main() {
    // ios::sync_with_stdio(0);
    // cin.tie(0), cout.tie(0);
    int t = 1;
    cin >> t;
    while (t--)solve();
}

F. 双括号序列

  • #贪心 #字符串
    每个测试的时间限制为2秒
    每个测试的内存限制为256 MB

给定一个长度为偶数 n n n 的字符串 s s s,由字符"("、")"、"[" 和 "]" 组成,这些字符是两种类型的括号(圆括号和方括号)。

我们称一个字符串 t t t 是美丽的,如果它同时满足两个条件:

  • 所有圆括号构成的子序列形成一个正确的括号序列*;
  • 所有方括号构成的子序列形成一个正确的括号序列

例如,字符串 "[(])[]" 是美丽的,因为其中所有圆括号构成的子序列 "()()" 形成一个正确的括号序列,而所有方括号构成的子序列 "[][[]]" 也形成一个正确的括号序列。

你希望将 s s s 变成任意一个美丽的字符串;为此,你可以更改字符:在每次操作中,你可以选择一个位置 i i i,满足 1 ≤ i ≤ n 1 \le i \le n 1≤i≤n,并将字符 s i s_i si 更改为任意一个圆括号或方括号。

将 s s s 变换为任意一个美丽的字符串所需的最少操作次数是多少?

  • 如果一个括号序列可以通过插入符号 "+" 和 "1" 得到一个有效的数学表达式,则该序列被称为正确的括号序列 。例如,序列 "(())()"、"[]" 和 "(()(()))" 是正确的,而 ")("、"[[]" 和 "(()))(" 则不是。

输入

每个测试包含多个测试用例。第一行包含测试用例的数量 t t t ( 1 ≤ t ≤ 10 4 ) (1 \le t \le 10^4) (1≤t≤104)。接下来是测试用例的描述。

每个测试用例的第一行包含一个整数 n n n ( 2 ≤ n ≤ 2 ⋅ 10 5 ) (2 \le n \le 2 \cdot 10^5) (2≤n≤2⋅105) ------ 字符串 s s s 的长度。保证 n n n 是偶数。

每个测试用例的第二行包含一个长度为 n n n 的字符串 s s s,仅由字符"("、")"、"[" 和 "]" 组成。

保证所有测试用例的 n n n 之和不超过 2 ⋅ 10 5 2 \cdot 10^5 2⋅105。

输出

对于每个测试用例,输出一个整数 ------ 问题的答案。

思路

本题和以前遇到的括号匹配的定义略有不同,只需要子序列能匹配就合法,因此可以先单独分离出来观察 [ ] [] []与 ( ) () ()的序列

对于原本就已经匹配的位置 ( i , j ) (i,j) (i,j),直接保留他们,不做任何修改是最优的。因为这样不会消耗操作次数,并且减少了需要处理的字符

因此,我们先创建两个栈,将原本就已经匹配的位置消去,那么这两个类剩下来必定形如 ) ) ...   ) ) ( ( ... ( ( ))\dots))((\dots(( ))...))((...((与 ] ] ...   ] ] [ [ ... [ [ ]]\dots]][[\dots[[ ]]...]][[...[[,我们将他们放入数组 p d pd pd中,将 ( , [ (,[ (,[记为0, ) , ] ),] ),]记为1

对于 p d pd pd中的两个位置 ( i , j ) (i,j) (i,j),他们所构成的01串为 p d [ i ] p d [ j ] pd[i]pd[j] pd[i]pd[j],如果想要通过修改把他们凑成一对:

  • 代价为1的情况:
    • 01 01 01实际上对应着 ( ] (] (]或者 [ ) [) [),代价为1
    • 00 00 00或 11 11 11实际上对应着 ( ( , ) ) , [ [ , ] ] ((\ ,\ ))\ ,\ [[\ ,\ ]] (( , )) , [[ , ]] 四种情况,代价为1
  • 代价为2的情况:
    • 实际上只剩下了 10 10 10的情况,也就是 ) ( , ) [ , ] ( , ] [ )(\ ,\ )[\ ,\ ](\ ,\ ][ )( , )[ , ]( , ][这四种情况,必须修改两次,代价为2

为了尽可能让代价变小,我们希望将情况2转变为情况1

发现:只有在 p d pd pd所构成的01串形如 11 ... 100 ... 0 11\dots 100\dots 0 11...100...0并且 0 0 0的个数为奇数的时候,会出现情况2,也就是所有的1都在0的左边,除了 00 00 00、 11 11 11配对,还需要多花费一次代价为2的操作来修改 10 10 10,答案为 p d . s i z e ( ) 2 + 1 \frac{pd.size()}{2}+1 2pd.size()+1

否则,只要任一个0出现在了1的右边:

  • 如果0的个数为偶数,那么一定可以通过 00 00 00、 11 11 11将所有的偶数个0与1各自配对,答案为 p d . s i z e ( ) 2 \frac{pd.size()}{2} 2pd.size()
  • 如果0的个数为奇数,那么将这个最左端的0与一个1组成 01 01 01,剩余的01串中0的个数便变回了偶数,可以用上述方式匹配,答案为 p d . s i z e ( ) 2 \frac{pd.size()}{2} 2pd.size()

代码

cpp 复制代码
#pragma GCC optimize(3, "Ofast", "inline")
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define int ll 
#define rep(i,a,b) for(int i=(a);i<=(b);i++)
#define per(i,a,b) for(int i=(a);i>=(b);i--)


void solve() {
    int n;cin >> n;
    string s;cin >> s;
    deque<pair<char, int>>dq1, dq2;
    vector<bool>vis(n + 1);
    rep(i, 1, n) {
        if (s[i - 1] == '(') {
            dq1.push_back({ s[i - 1],i });
        }
        if (s[i - 1] == ')') {
            if (dq1.size() && dq1.back().first == '(') {
                vis[i] = vis[dq1.back().second] = 1;
                dq1.pop_back();
            }
        }
        if (s[i - 1] == '[') {
            dq2.push_back({ s[i - 1],i });
        }
        if (s[i - 1] == ']') {
            if (dq2.size() && dq2.back().first == '[') {
                vis[i] = vis[dq2.back().second] = 1;
                dq2.pop_back();
            }
        }
    }
    vector<int>pd;
    map<char, int>mp = { {'(',0},{')',1},{'[',0},{']',1} };
    int cnt[2] = { 0,0 };
    rep(i, 1, n) {
        if (!vis[i]) {
            pd.push_back(mp[s[i - 1]]);
            cnt[mp[s[i - 1]]]++;
        }
    }
    // cout << "pd:";for (auto x : pd)cout << x << " ";cout<<'\n';
    if (pd.size() == 0) {
        cout << 0 << '\n';return;
    }
    int ans = pd.size() / 2;
    if (cnt[0] & 1) {
        int f1 = 0, f2 = 0;
        rep(i, 1, pd.size() - 1) {
            if (pd[i] && !pd[i - 1])f1++;
            if (!pd[i] && pd[i - 1])f2++;
        }
        if (!f1 && f2 == 1)ans++;
    }
    cout << ans << '\n';
}

signed main() {
    ios::sync_with_stdio(0);
    cin.tie(0), cout.tie(0);
    int t = 1;
    cin >> t;
    while (t--)solve();
}
相关推荐
阿里嘎多学长2 小时前
2026-02-07 GitHub 热点项目精选
开发语言·程序员·github·代码托管
666HZ6662 小时前
数据结构4.0 串
c语言·数据结构·算法
Anastasiozzzz2 小时前
Java异步编程:CompletableFuture从入门到底层实现
java·开发语言
weixin_421585012 小时前
常微分方程
算法
九.九2 小时前
高性能算子库 ops-nn 的底层架构:从调度到指令的极致优化
开发语言
比奇堡派星星2 小时前
sed命令
linux·运维·服务器·开发语言
船神丿男人3 小时前
C++:STL string(一)
开发语言·c++
文艺倾年3 小时前
【免训练&测试时扩展】通过任务算术转移思维链能力
人工智能·分布式·算法
程序员zgh3 小时前
Linux 内存管理单元 MMU
linux·运维·服务器·c语言·开发语言·c++