2021第十二届蓝桥杯c++B组国赛题解

P8759 [蓝桥杯 2021 国 B] 带宽

题目描述:

小蓝家的网络带宽是 \(200 \mathrm{Mbps}\),请问,使用小蓝家的网络理论上每秒钟最多可以从网上下载多少 \(\mathrm{MB}\) 的内容。

输入格式:

无

输出格式:

这是一道结果填空的题,你只需要算出结果后提交即可。本题的结果为一个整数,在提交答案时只填写这个整数,填写多余的内容将无法得分。

题目大意

\(200\text{Mbps}\) 的网络贷款,每秒可以下载多少 \(\text{MB}\) 的内容。

题解

\(200\text{Mbps}\) 就是每秒下载 \(200Mb\), \(1 \text{B} = 8 \text{b}\),所以答案就是 \(\frac{200}{8} = 25\)。

\(\text{b}\) 是指 比特(bit, 一个 \(0\) 或者 \(1\))。\(\text{B}\) 是指 字节(Byte)。

时间复杂度: \(O(1)\)

P8759 [蓝桥杯 2021 国 B] 纯质数

题目描述:

如果一个正整数只有 \(1\) 和它本身两个约数,则称为一个质数(又称素数)。

前几个质数是:\(2,3,5,7,11,13,17,19,23,29,31,37,\cdots\)。

如果一个质数的所有十进制数位都是质数,我们称它为纯质数。例如:2,\(3,5,7,23,37\) 都是纯质数,而 \(11,13,17,19,29,31\) 不是纯质数。当然 \(1,4,35\) 也不是纯质数。

请问,在 \(1\) 到 \(20210605\) 中,有多少个纯质数?

输入格式:

无

输出格式:

这是一道结果填空的题,你只需要算出结果后提交即可。本题的结果为一个整数,在提交答案时只填写这个整数,填写多余的内容将无法得分。

题目大意

找出 \(1\) 到 \(20210605\) 中有几个纯质数。

纯质数: 数本身是质数的同时,数中各位数字也为质数。例如, \(23\)、\(37\) 就是纯质数,而 \(31\) 不是。

题解

通过 线性筛 快速找出 \(1 \sim 20210605\) 中哪些是质数,哪些不是,之后将所有筛出的质数逐位的检查是否是质数并计数。

代码:

const int N = 20210605 + 10;

bool judge[N]; // judge[i]: true 表示 非质数, false 表示 质数
// prime: 所有的质数
// cnt: 质数的个数
int prime[N], cnt = 0; 
int getPrimes(int n){ // 线性筛
   judge[0] = judge[1] = true;
   for(int i = 2; i <= n; i ++){
       if(!judge[i]){
           prime[cnt ++] = i;
       }
       for(int j = 0; prime[j] * i <= n; j ++){
           judge[prime[j] * i] = true;
           if(i % prime[j] == 0){
               break;
           }
       }
   }
   return cnt;
}

void solve(void){
   int n = 20210605;
   int m = getPrimes(n);
   int ans = 0;
   for(int i = 0; i < m; i ++){ // 枚举所有的质数
       string s = to_string(prime[i]); 
       bool flag = true;
       for(auto c : s){ // 逐位检查是否 都是素数
           int cc = c - '0';
           if(judge[cc]){ // judge[i] = true 表示 非质数
               flag = false;
               break;
           }
       }
       if(flag){ // 计数
           ans ++;
       }
   }
   cout << ans << endl;
}

时间复杂度: \(O(n)\)

P8759 [蓝桥杯 2021 国 B] 完全日期

题目描述:

如果一个日期中年月日的各位数字之和是完全平方数,则称为一个完全日期。

例如:\(2021\) 年 \(6\) 月 \(5\) 日的各位数字之和为 \(2+0+2+1+6+5=16\),而 \(16\) 是一个完全平方数,它是 \(4\) 的平方。所以 \(2021\) 年 \(6\) 月 \(5\) 日是一个完全日期。

例如:\(2021\) 年 \(6\) 月 \(23\) 日的各位数字之和为 \(2+0+2+1+6+2+3=16\),是一个完全平方数。所以 \(2021\) 年 \(6\) 月 \(23\) 日也是一个完全日期。

请问,从 \(2001\) 年 \(1\) 月 \(1\) 日到 \(2021\) 年 \(12\) 月 \(31\) 日中,一共有多少个完全日期?

输入格式:

无

输出格式:

这是一道结果填空的题,你只需要算出结果后提交即可。本题的结果为一个整数,在提交答案时只填写这个整数,填写多余的内容将无法得分。

题目大意

计算 \(2001\) 年 \(1\) 月 \(1\) 日到 \(2021\) 年 \(12\) 月 \(31\) 日之间有多少个完全日期。

完全日期: 日期的各位数字加起来是一个完全平方数。

例如: \(2021\) 年 \(6\) 月 \(23\) 日就是一个完全日期。 \(2 + 0 + 2 + 1 + 6 + 2 + 3 = 16 = 4^{2}\)。

题解

直接模拟其中的每一天并检查其每一位的和是否是完全平方数,是则累加即可。

代码:

int day[] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; // 每个月份的天数
set<int> st = {4, 9, 16, 25, 36, 49}; // 因数值较小,直接列举出几个完全平方数,方便查询

// 获取每一位数字的和
int get(int x){ 
    int sum = 0;
    while(x){
        sum += x % 10;
        x /= 10;
    }
    return sum;
}

// 检查年份是否是 闰年
int chk(int x){
    return x % 4 == 0 && x % 100 != 0 || x % 400 == 0;
}

void solve(void){
    int ans = 0;
    for(int y = 2001; y <= 2021; y ++){
        for(int m = 1; m <= 12; m ++){
            int dd = day[m]; 
            // 闰年 2 月多一天
            if(m == 2 && chk(y)) dd ++;
            for(int d = 1; d <= dd; d ++){
                int sum = get(y) + get(m) + get(d);
                if(st.count(sum)) ans ++;
            }
        }
    }
    cout << ans << endl;
}

时间复杂度: \(O(n)\)

P8759 [蓝桥杯 2021 国 B] 最小权值

题目描述:

对于一棵有根二叉树 \(T\),小蓝定义这棵树中结点的权值 \(W(T)\) 如下:

空子树的权值为 \(0\) 。

如果一个结点 \(v\) 有左子树 \(L\),右子树 \(R\),分别有 \(C(L)\) 和 \(C(R)\) 个结点,则

\(W(v)=1+2 W(L)+3 W(R)+(C(L))^{2} C(R)\)。

树的权值定义为树的根结点的权值。

小蓝想知道,对于一棵有 \(2021\) 个结点的二叉树,树的权值最小可能是多少?

输入格式:

无

输出格式:

这是一道结果填空的题,你只需要算出结果后提交即可。本题的结果为一个整数,在提交答案时只填写这个整数,填写多余的内容将无法得分。

题目大意

给定一棵有 \(2021\) 个结点的有根二叉树,定义每个结点的权值为:若结点有左子树 \(L\) 和右子树 \(R\),则权值 \(W(v)=1+2 W(L)+3 W(R)+(C(L))^{2} C(R)\)。\(C(u)\) 表示二叉树 \(u\) 的节点个数。

空子树权值为 \(0\)。

求有 \(2021\) 个结点的二叉树的权值的最小可能值。

题解

这题可以通过 线性dp 来解决。

定义 dp[i] 表示节点数为 \(i\) 的二叉树的最小权值。

题目给的公式中还需要左右子树的节点数量,故假设左子树的节点数为lf,右子树的节点数为rt = i - lf - 1。(\(0 \le lf , rt \lt i\))

rt = i - lf - 1这里的 \(-1\) 是因为,子树的节点数为 \(i\),这颗子树的根也占用一个节点,所以 \(lf + rt + 1 = i\)。

那么递推公式就为:

\[dp[i] = \begin{cases} 1 + 2 * dp[lf] + 3 * dp[rt] + lf * lf * rt &, dp[i] = 0 \\ min(dp[i], 1 + 2 * dp[lf] + 3 * dp[rt] + lf * lf * rt) &, dp[i] \not = 0 \end{cases} \]

答案就是 \(dp[2021]\)。

代码:

void solve(void){
    int n = 2021;
    // dp[i] := 节点数为 i 的二叉树的最小权值
    vector<ll> dp(n + 1, 0);
    for(int i = 1; i <= n; i ++){ // 从小到大枚举二叉树的节点数
        for(ll lf = 0; lf < i; lf ++){ // 枚举二叉树的左子树的节点数
            ll rt = i - lf - 1; // 右子树的节点数(这颗子树的根也算一个节点)
            if(!dp[i]){ // 如果当前节点数的二叉树的最小权值未被计算过
                dp[i] = 1ll + 2ll * dp[lf] + 3ll * dp[rt] + lf * lf * rt;
                continue;
            }
            dp[i] = min(dp[i], 1ll + 2ll * dp[lf] + 3ll * dp[rt] + lf * lf * rt);
        }
    }
    // 输出结果
    cout << dp[n] << endl;
}

时间复杂度: \(O(2021) \to O(1)\)

P8761 [蓝桥杯 2021 国 BC] 大写

题目描述:

给定一个只包含大写字母和小写字母的字符串,请将其中所有的小写字母转换成大写字母后将字符串输出。

输入格式:

输入一行包含一个字符串。

输出格式:

输出转换成大写后的字符串。

样例:

输入:
LanQiao

输出:
LANQIAO

数据规模:

对于所有评测用例, 字符串的长度不超过 \(100\)。

题目大意

输入一串只有小写字母和大写字母的字符串,将所有小写字母转大写后,将字符串输出。

题解

遍历整个字符串,将所有的小写字母 \(- 32\) 后输出。

代码:

void solve(void){
    string s; cin >> s;
    for(auto &c : s){
        if(c >= 'a' && c <= 'z'){
            c -= 32;
        }
    }
    cout << s << endl;
}

时间复杂度: \(O(n)\)

P8762 [蓝桥杯 2021 国 ABC] 123

题目描述:

小蓝发现了一个有趣的数列, 这个数列的前几项如下:

\(1,1,2,1,2,3,1,2,3,4, \ldots\)

小蓝发现, 这个数列前 \(1\) 项是整数 \(1\) , 接下来 \(2\) 项是整数 \(1\) 至 \(2\) , 接下来 \(3\) 项是整数 \(1\) 至 \(3\) , 接下来 \(4\) 项是整数 \(1\) 至 \(4\) , 依次类推。

小蓝想知道, 这个数列中, 连续一段的和是多少。

输入格式:

输入的第一行包含一个整数 \(T\), 表示询问的个数。

接下来 \(T\) 行, 每行包含一组询问, 其中第 \(i\) 行包含两个整数 \(l_{i}\) 和 \(r_{i}\), 表示 询问数列中第 \(l_{i}\) 个数到第 \(r_{i}\) 个数的和。

输出格式:

输出 \(T\) 行, 每行包含一个整数表示对应询问的答案。
样例:

输入:
3
1 1
1 3
5 8

输出:
1
4
8

数据规模:

对于 \(10 \%\) 的评测用例, \(1 \leq T \leq 30,1 \leq l_{i} \leq r_{i} \leq 100\) 。

对于 \(20 \%\) 的评测用例, \(1 \leq T \leq 100,1 \leq l_{i} \leq r_{i} \leq 1000\) 。

对于 \(40 \%\) 的评测用例, \(1 \leq T \leq 1000,1 \leq l_{i} \leq r_{i} \leq 10^{6}\) 。

对于 \(70 \%\) 的评测用例, \(1 \leq T \leq 10000,1 \leq l_{i} \leq r_{i} \leq 10^{9}\) 。

对于 \(80 \%\) 的评测用例, \(1 \leq T \leq 1000,1 \leq l_{i} \leq r_{i} \leq 10^{12}\) 。

对于 \(90 \%\) 的评测用例, \(1 \leq T \leq 10000,1 \leq l_{i} \leq r_{i} \leq 10^{12}\) 。

对于所有评测用例, \(1 \leq T \leq 100000,1 \leq l_{i} \leq r_{i} \leq 10^{12}\) 。

题目大意

有一个数列,按如下方式构造:先写1,然后写1,2,然后写1,2,3,然后写1,2,3,4,……即第 \(k\) 组包含 \(k\) 个整数 \(1,2,\dots,k\)。给定一个区间 \([l, r]\)(表示数列的第 \(l\) 项到第 \(r\) 项),求这段区间内所有数的和。

题解

注: 本题题解索引从 \(1\) 开始。

多个查询,每个查询需要返回一个区间内的和,显然是要用 前缀和 的思想。

假设 \(pre[i]\) 表示前 \(i\) 个元素的和,那么区间 \([lf, rt]\) 中的和显然就为 \(pre[rt] - pre[lf - 1]\)。

按照题目中给出的数列构造方法,我们将这个数列进行拆分并分组,如下:

1: 1
2: 1 2
3: 1 2 3
4: 1 2 3 4
5: 1 2 3 4 5
......

假设我们需要求出第 idx 位元素及之前所有元素的和,且第 idx 数位于 row 行 col 列。

于是,就可以将答案拆分乘两部分。

  1. 前 \(row - 1\) 行 所有元素的和。
    其中的每一行所有元素的和都是一个 三角形数,而 三角形数的求和公式 为 \(S_{n} = \frac{n (n + 1) (n + 2)}{6}\),可以快速计算。
    (有关三角形数的详情,见文章 三角形数)
  2. 第 \(row\) 行中 前 \(col\) 个元素的和。
    可以发现,前 \(col\) 个元素其实就是公差为 \(1\) 的等差数列,便可以用 等差数列求和公式 \(S_{n} = \frac{n (n + 1)}{2}\) 快速计算。

接下来的问题就是如何确定第 idx 个数位于具体的哪一行哪一列。

行数是一个具有单调性的序列(\(1, 2, 3, \cdots\)),于是就可以考虑使用 二分查找 来快速确定。

每一行的个数是和其行数一致的,那么前 \(i\) 行有多少个数可以通过 等差数列求和公式 快速得到。于是,找到第一个前 \(i\) 行元素个数大于等于 idx 的这行就是 idx 所在的行,假设其为 row。

将 idx 减去前 \(row - 1\) 行元素个数,剩下的就是其所在列 col。

代码:

// 等差数列前 n 项和
ll pre_sum(ll n){
    return n * (n + 1) / 2;
}

// 二分查找当前位处于哪一层
int get_row(ll x){ 
    int lf = 1, rt = 2e6;
    int mid;
    while(lf + 1 < rt){
        mid = lf + ((rt - lf) >> 1);
        if(pre_sum(mid) < x){
            lf = mid;
        }else{
            rt = mid;
        }
    }
    if(pre_sum(lf) >= x) return lf;
    return rt;
}

// 前 n 个三角形数的和
ll get_pre_triangle(ll n){
    return n * (n + 1) * (n + 2) / 6;
}

// 获取该位置前所有数的前缀和
ll get_pre_sum(ll x){
    int row = get_row(x); // 第几行
    int col = x - pre_sum(row - 1); // 第几列
    // cout << "row:" << row << "  col:" << col << endl;
    return get_pre_triangle(row - 1) + pre_sum(col);
}

void solve(void){
    int q; cin >> q;
    while(q --){
        int lf, rt;
        cin >> lf >> rt;
        ll ans = get_pre_sum(rt) - get_pre_sum(lf - 1);
        cout << ans << endl;
    }
}

时间复杂度: \(O(q \log{n})\)

P8763 [蓝桥杯 2021 国 ABC] 异或变换

题目描述:

小蓝有一个 01 串 \(s=s_{1} s_{2} s_{3} \cdots s_{n}\)。

以后每个时刻, 小蓝要对这个 01 串进行一次变换。每次变换的规则相同。 对于 01 串 \(s=s_{1} s_{2} s_{3} \cdots s_{n}\), 变换后的 01 串 \(s^{\prime}=s_{1}^{\prime} s_{2}^{\prime} s_{3}^{\prime} \cdots s_{n}^{\prime}\) 为:

\[\begin{aligned} &s_{1}^{\prime}=s_{1} \\ &s_{i}^{\prime}=s_{i-1} \oplus s_{i} \end{aligned}\]

其中 \(a \oplus b\) 表示两个二进制的异或, 当 \(a\) 和 \(b\) 相同时结果为 \(0\) , 当 \(a\) 和 \(b\) 不同时结果为 \(1\)。

请问, 经过 \(t\) 次变换后的 01 串是什么?

输入格式:

输入的第一行包含两个整数 \(n, t\), 分别表示 01 串的长度和变换的次数。

第二行包含一个长度为 \(n\) 的 01 串。

输出格式:

输出一行包含一个 01 串, 为变换后的串。

样例:

输入:
5 3
10110

输出:
11010

样例说明:

初始时为 10110 , 变换 1 次后变为 11101 , 变换 2 次后变为 10011 , 变换 3 次后变为 11010。

数据规模:

对于 \(40 \%\) 的评测用例, \(1 \leq n \leq 100,1 \leq t \leq 1000\)。

对于 \(80 \%\) 的评测用例, \(1 \leq n \leq 1000,1 \leq t \leq 10^{9}\)。

对于所有评测用例, \(1 \leq n \leq 10000,1 \leq t \leq 10^{18}\)。

题目大意

给定一个长度为 \(n\) 的 01 字符串,按如下规则反复变换:第一个字符不变,第 \(i\)(\(i\ge 2\))个字符变为原串中第 \(i-1\) 位与第 \(i\) 位的异或值。求经过 \(t\) 次变换后得到的字符串。其中 \(n\le 10^4\),\(t\le 10^{18}\)。

题解

由于数字很大 \(t\le 10^{18}\),所以大概率有规律可循。

打表找规律:

代码:

void solve(void){
    int n, m; cin >> n >> m;
    string s; cin >> s;
    cout << "0: " << s << endl;
	for(int cnt = 1; cnt <= m; cnt ++){
        cout << cnt << ": ";
        for(int i = n - 1; i > 0; i --){
            s[i] = (s[i - 1] - '0') ^ (s[i] - '0') + '0';
        }
        cout << s << endl;
    }
}
输入:
2 4
10

输出:
0: 10 <-
1: 11
2: 10 <-
3: 11
4: 10

--------------

输入:
3 6
101

输出:
0: 101 <-
1: 111
2: 100
3: 110
4: 101 <-
5: 111
6: 100

--------------

输入:
4 6
1011

输出:
0: 1011 <-
1: 1110
2: 1001
3: 1101
4: 1011 <-
5: 1110
6: 1001

--------------

输入:
5 10
10110

输出:
0: 10110 <-
1: 11101
2: 10011
3: 11010
4: 10111
5: 11100
6: 10010
7: 11011
8: 10110 <-
9: 11101
10: 10011

从中就能发现,其实是有循环的,且循环节为最小的大于等于 \(n\) 的 2 次幂。
于是我们就可以对 \(n\) 进行取模操作,减少大量的循环操作,剩下的暴力模拟便可。

代码:

void solve(void){
    int n, m; cin >> n >> m;
    string s; cin >> s;
    ll e = 1;
    while(e < n) e<<= 1ll;
    m %= e;
    // cout << "0: " << s << endl;
    for(int cnt = 1; cnt <= m; cnt ++){
        // cout << cnt << ": ";
        for(int i = n - 1; i > 0; i --){
            s[i] = (s[i - 1] - '0') ^ (s[i] - '0') + '0';
        }
    }
    cout << s << endl;
}

时间复杂度: \(O(n \log{n})\)

P8764 [蓝桥杯 2021 国 BC] 二进制问题

题目描述:

小蓝最近在学习二进制。他想知道 \(1\) 到 \(N\) 中有多少个数满足其二进制表示中恰好有 \(K\) 个 1。你能帮助他吗?

输入格式:

输入一行包含两个整数 \(N\) 和 \(K\)。

输出格式:

输出一个整数表示答案。

样例:

输入:
7 2

输出:
3

数据规模:

对于 \(30 \%\) 的评测用例,\(1 \leq N \leq 10^{6}, 1 \leq K \leq 10\)。

对于 \(60 \%\) 的评测用例,\(1 \leq N \leq 2 \times 10^{9}, 1 \leq K \leq 30\)。

对于所有评测用例,\(1 \leq N \leq 10^{18}, 1 \leq K \leq 50\)。

题目大意

给定整数 \(N\) 和 \(K\),求区间 \([1, N]\) 内有多少个数的二进制表示中恰好有 \(K\) 个 \(1\)。

题解

这里给出两种解法。

组合数:

举两个例子。

假设 \(N\) 的二进制为 \(10000\),\(K = 2\)。

首先,考虑在 \(0 \sim 1111\) 中寻找有 \(2\) 个 \(1\) 的数字个数,其实就是在 \(4\) 个位置中挑选 \(2\) 个为 \(1\),即有为 \(C_{4}^{2}\),最后考虑自身,并不满足条件,于是答案为 \(C_{4}^{2}\)。

假设 \(N\) 的二进制为 \(101011\),\(K = 3\)。

从左往右,首先考虑 \(0 \sim 11111\) 中满足条件的个数,累加 \(C_{5}^{3}\);考虑 \(100000 \sim 100111\) 中满足条件的个数,也就是 \(0 \sim 111\) 中有 \(2\) 个 \(1\) 的个数,累加 \(C_{3}^{2}\);考虑 \(101000 \sim 101001\) 中满足条件的个数,也就是 \(0 \sim 1\) 中有 \(1\) 个 \(1\) 的个数,累加 \(C_{1}^{1}\);考虑 \(101010 \sim 101010\) 中满足条件的个数,也就是 \(0 \sim 0\) 中有 \(0\) 个 \(1\) 的个数,累加 \(C_{0}^{0}\);考虑自身,并不满足条件。最终答案为 \(C_{5}^{3} + C_{3}^{2} + C_{1}^{1} + C_{0}^{0}\) 。

应该发现其中的规律了,从左往右遍历每一位,如果这一位为 \(1\),其右侧还有 \(m\) 位,目前已有 \(cnt\) 个 \(1\),则满足条件的个数为 \(C_{m}^{k - cnt + 1}\) 个。

代码:

ll C(ll n, ll m){
    if(n < m) return 0;
    ll ans = 1;
    for(int i = 1; i <= m; i ++){
        ans = ans * (n - i + 1) / i;
    }
    return ans;
}

void solve(void){
    ll n, k; cin >> n >> k;
    int ans = 0;
    ll cnt = 0; // 1 的个数
    for(int i = 62; i >= 0; i --){
        if(k - cnt + 1 == 0) break;
        if(n >> i & 1){
            cnt ++;
            ans += C(i, k - cnt + 1);
        }
    }
    if(cnt == k) ans ++;
    cout << ans << endl;
}

时间复杂度: \(O(k \log{N})\)

数位DP:

从高位到低位尝试,假设目前在第 idx 位,已经有 cnt 个 \(1\)。

  • 如果 idx 左侧都和原数相等,则剩下的数位不能随意取。假设第 idx 位为 \(a\),那么该位的取值范围为 \([0, a]\)。
  • 如果 idx 左侧有数位小于了原数,则接下来的所有位都可以任意取。取值范围为 \([0, 1]\)。

为加速计算,添加记忆化。

代码:

const int INF = 70;

ll n, m;
ll dp[INF][INF][2];

// 考虑到第 idx 位,有 cnt 个 1,是否和原数相等
ll dfs(int idx, int cnt, bool limit){
    if(cnt > m) return 0;
    if(idx == -1) return cnt == m;
    if(dp[idx][cnt][limit] != -1) return dp[idx][cnt][limit];
    // 当前位的取值上限
    int top = limit ? (n >> idx) & 1ll : 1;
    ll sum = 0;
    for(int i = 0; i <= top; i ++){
        sum += dfs(idx - 1, cnt + i, limit && i == top);
    }
    dp[idx][cnt][limit] = sum;
    return sum;
}

void solve(void){
    memset(dp, -1, sizeof dp);
    cin >> n >> m;
    ll ans = dfs(62, 0, true);
    cout << ans << endl;
}

时间复杂度: \(O(K \log{n})\)

P8765 [蓝桥杯 2021 国 AB] 翻转括号序列

题目描述:

给定一个长度为 \(n\) 的括号序列,要求支持两种操作:

  1. 将 \(\left[L_{i}, R_{i}\right]\) 区间内(序列中的第 \(L_{i}\) 个字符到第 \(R_{i}\) 个字符)的括号全部翻转(左括号变成右括号,右括号变成左括号)。

  2. 求出以 \(L_{i}\) 为左端点时,最长的合法括号序列对应的 \(R_{i}\) (即找出最大的 \(R_{i}\) 使 \(\left[L_{i}, R_{i}\right]\) 是一个合法括号序列)。

输入格式:

输入的第一行包含两个整数 \(n, m\),分别表示括号序列长度和操作次数。

第二行包含给定的括号序列,括号序列中只包含左括号和右括号。

接下来 \(m\) 行,每行描述一个操作。如果该行为 1 L R, 表示第一种操作,区间为 \(\left[L, R\right]\);如果该行为 2 L 表示第二种操作,左端点为 \(L\) 。

输出格式:

对于每个第二种操作,输出一行,表示对应的 \(R_{i}\)。如果不存在这样的 \(R_{i}\),请输出 \(0\) 。

样例:

输入:
7 5
((())()
2 3
2 2
1 3 5
2 3
2 1

输出:
4
7
0
0

数据规模:

对于 \(20 \%\) 的评测用例,\(n, m \leq 5000\);

对于 \(40 \%\) 的评测用例,\(n, m \leq 30000\);

对于 \(60 \%\) 的评测用例,\(n, m \leq 100000\);

对于所有评测用例,\(1 \leq n \leq 10^{6}, 1 \leq m \leq 2 \times 10^{5}\) 。

题目大意

维护一个长度为 \(n\) 的括号序列,支持两种操作:

  1. 区间翻转:将区间 \([L, R]\) 内的每个括号翻转(( 变为 ),) 变为 ()。
  2. 查询最长合法括号区间:给定左端点 \(L\),求最大的 \(R\) 使得子串 \([L, R]\) 是一个合法的括号序列(即括号匹配正确)。若不存在这样的 \(R\),输出 0。

题解

定义: '(' 为 \(1\),')' 为 '-1'。

那么一个合法的括号序列就转化为 累加和为 \(0\) 的区间。

显然这题可以利用线段树来快速操作。

构造一个线段树,其中维护三个属性:

  1. 累加和。
  2. 最小前缀和。
  3. 最大前缀和。

最小、最大前缀和 的作用后面会提。

这里简单介绍下最小前缀和。

\[pre\_mn[i] = \min_{j = 1}^{i}\{pre\_sum[j]\} \]

举个例子:

注: 索引从 1 开始。
(())()())()
arr[] = {1, 1, -1, -1, 1, -1, 1, -1, -1, 1, -1}
pre_sum[] = {1, 2, 1, 0, 1, 0, 1, 0, -1, 0, -1}
pre_mn[] = {1, 1, 1, 0, 0, 0, 0, 0, -1, -1, -1}

最大前缀和与之对应。

操作一:

翻转区间 \([L,R]\) 中的所有括号,也就是对区间内的所有数字乘 \(-1\)。

主要关注对于 最小、最大前缀和 的影响。

以 list[] = {4, 2, 5, -1, -3} 这个序列为例子,\(max = 5, min = -3\)。

对其中所有数字乘 \(-1\),- list[] = {-4, -2, -5, 1, 3},\(max = 3,min = -5\)。

应该发现了,就是最大和最小值交换后取负。

操作二:

在前面介绍最小前缀和的时候举了个这样的例子:

(())()())()

arr[] = {1, 1, -1, -1, 1, -1, 1, -1, -1, 1, -1}

pre_sum[] = {1, 2, 1, 0, 1, 0, 1, 0, -1, 0, -1}

pre_mn[] = {1, 1, 1, 0, 0, 0, 0, 0, -1, -1, -1}

发现 最小前缀和 是有单调性的(单调不增),尝试利用这一点。

当确定了一个左端点 \(lf\) 后,我们可以对最小前缀和数组 二分,寻找最后一个最小前缀和为 \(0\) 的点,假设为 \(rt\),显然 \([rt + 1, n]\) 区间并不是 合法括号序列。

当确定了最后一个最小前缀和为 \(0\) 的点后,还需要通过不断的向左移动右边界,找到的第一个区间累加和为 \(0\) 的索引位置才是最终的答案。

例如:

(())(()

arr[] = {1, 1, -1, -1, 1, 1, -1}

pre_sum[] = {1, 2, 1, 0, 1, 2, 1}

pre_mn[] = {1, 1, 1, 0, 0, 0, 0}

在这个例子中,最右侧点就不并是答案所求的点。

可以发现,假设当前右边界索引为 rt,那么 pre_sum[rt] 的值就是多出的 '(' 的数量,可以通过减去 pre_sum[rt] 来快速定位答案。

代码:

// 线段树的节点
struct Node{ 
    int sum = 0; // 区间和  
    int pre_mn = 0; // 区间内的最小前缀和
    int pre_mx = 0; // 区间内的最大前缀和
    // 构造函数
    Node() = default;
    Node(int _sum, int _mn, int _mx): sum(_sum), pre_mn(_mn), pre_mx(_mx){} 
};

// 线段树
struct Segment_tree{
    int n; // 区间个数
    vector<Node> tree; // 树
    vector<bool> lazy; // 懒惰标记
    
    // 初始化线段树大小
    Segment_tree(int _n): n(_n){
        tree.assign(n << 2, Node());
        lazy.assign(n << 2, false);
    }

    // 合并左右子节点,更新当前节点
    void push_up(int idx, int l, int r){
        tree[idx].sum = tree[idx << 1].sum + tree[idx << 1 | 1].sum;
        tree[idx].pre_mn = min(tree[idx << 1].pre_mn, tree[idx << 1].sum + tree[idx << 1 | 1].pre_mn);
        tree[idx].pre_mx = max(tree[idx << 1].pre_mx, tree[idx << 1].sum + tree[idx << 1 | 1].pre_mx);
    }

    // 构建线段树
    void build(int idx, int l, int r, const vector<int> &arr){
        if(l == r){
            tree[idx].sum = arr[l];
            tree[idx].pre_mn = arr[l];
            tree[idx].pre_mx = arr[l];
            return;
        }
        int mid = l + ((r - l ) >> 1);
        build(idx << 1, l, mid, arr);
        build(idx << 1 | 1, mid + 1, r, arr);
        push_up(idx, l, r);
    }

    // 添加懒惰标记
    void add_tag(int idx, int l, int r){
        tree[idx].sum *= -1;
        // 交换最大最小前缀和
        swap(tree[idx].pre_mn, tree[idx].pre_mx);
        tree[idx].pre_mn *= -1;
        tree[idx].pre_mx *= -1;
        lazy[idx] = !lazy[idx];
    }

    // 向下传递懒惰标记
    void push_down(int idx, int l, int r){
        if(!lazy[idx]) return;
        int mid = l + ((r - l) >> 1);
        add_tag(idx << 1, l, mid);
        add_tag(idx << 1 | 1, mid + 1, r);
        lazy[idx] = false;
    }

    // [lf, rt] 区间内所有内容所有括号翻转
    void update(int idx, int l, int r, int lf, int rt){
        if(lf <= l && r <= rt){
            add_tag(idx, l, r);
            return;
        }
        push_down(idx, l, r);
        int mid = l + ((r - l) >> 1);
        if(lf <= mid) update(idx << 1, l, mid, lf, rt);
        if(mid < rt) update(idx << 1 | 1, mid + 1, r, lf, rt);
        push_up(idx, l, r);
    }

    // 获得 [lf, rt] 区间内所有节点累加和
    int query_sum(int idx, int l, int r, int lf, int rt){
        if(lf <= l && r <= rt) return tree[idx].sum;
        push_down(idx, l, r);
        int mid = l + ((r - l) >> 1);
        int ans = 0;
        if(lf <= mid) ans += query_sum(idx << 1, l, mid, lf, rt);
        if(mid < rt) ans += query_sum(idx << 1 | 1, mid + 1, r, lf, rt);
        return ans;
    }

    // 将区间 [lf, rt] 所有值结合为一个 Node 节点
    Node query_node(int idx, int l, int r, int lf, int rt){
        if(lf <= l && r <= rt) return tree[idx];
        push_down(idx, l, r);
        int mid = l + ((r - l) >> 1);
        if(rt <= mid) return query_node(idx << 1, l, mid, lf, rt);
        if(mid < lf) return query_node(idx << 1 | 1, mid + 1, r, lf, rt);
        Node a = query_node(idx << 1, l, mid, lf, rt);
        Node b = query_node(idx << 1 | 1, mid + 1, r, lf, rt);
        Node ans;
        ans.sum = a.sum + b.sum;
        ans.pre_mn = min(a.pre_mn, a.sum + b.pre_mn);
        ans.pre_mx = max(a.pre_mx, a.sum + b.pre_mx);
        return ans;
    }

    // 获得以 front 为左端点时,最长合法括号序列对应的 rt 索引 
    int query_rt(int front){
        // 左端点为 '(' 则不存在合法括号序列
        if(query_sum(1, 1, n, front, front) == -1) return -1;
        // 二分找到最右侧的 最小前缀和为0 的索引
        int lf = front, rt = n;
        int mid;
        while(lf + 1 < rt){
            mid = lf + ((rt - lf) >> 1);
            Node cur = query_node(1, 1, n, front, mid);
            if(cur.pre_mn >= 0){
                lf = mid;
            }else{
                rt = mid;
            }
        }
        int ans;
        if(query_node(1, 1, n, front, rt).pre_mn == 0){
            ans = rt;
        }else if(query_node(1, 1, n, front, lf).pre_mn == 0){
            ans = lf;
        }else{
            // 不存在
            return -1;
        }
        // 从最右侧向前找到第一个 区间和为0 的索引
        while(ans >= front){
            int flag = query_sum(1, 1, n, front, ans);
            if(flag == 0){
                return ans;
            }
            ans -= flag;
        }
        return -1;
    }
};

void solve(){
    int n, q;
    cin >> n >> q;
    string s; cin >> s;
    vector<int> arr(n + 1, 0);
    for(int i = 0; i < n; i ++){
        arr[i + 1] = s[i] == '(' ? 1 : -1;
    }

    // 初始化线段树
    Segment_tree tree = Segment_tree(n);
    tree.build(1, 1, n, arr);

    while(q --){
        int op; cin >> op;
        if(op == 1){
            int lf, rt;
            cin >> lf >> rt;
            tree.update(1, 1, n, lf, rt);
        }else if(op == 2){
            int lf; cin >> lf;
            int rt = tree.query_rt(lf);
            cout << (rt == -1 ? 0 : rt) << endl;
        }else{
            cout << "None" << endl;
        }
    }
}

时间复杂度: \(O(q \cdot n\log^{2}{n})\)

P8766 [蓝桥杯 2021 国 AB] 异或三角

题目描述:

给定 \(T\) 个数 \(n_{1}, n_{2}, \cdots, n_{T}\), 对每个 \(n_{i}\) 请求出有多少组 \(a, b, c\) 满足:

  1. \(1 \leq a, b, c \leq n_{i}\);

  2. \(a \oplus b \oplus c=0\) ,其中 \(\oplus\) 表示二进制按位异或;

  3. 长度为 \(a, b, c\) 的三条边能组成一个三角形。

输入格式:

输入的第一行包含一个整数 \(T\) 。

接下来 \(T\) 行每行一个整数, 分别表示 \(n_{1}, n_{2}, \cdots, n_{T}\) 。

输出格式:

输出 \(T\) 行, 每行包含一个整数, 表示对应的答案。

样例:

输入:
2
6
114514

输出:
6
11223848130

数据规模:

对于 \(10 \%\) 的评测用例, \(T=1,1 \leq n_{i} \leq 200\); 对于 \(20 \%\) 的评测用例, \(T=1,1 \leq n_{i} \leq 2000\) ;

对于 \(50 \%\) 的评测用例, \(T=1,1 \leq n_{i} \leq 2^{20}\);

对于 \(60 \%\) 的评测用例, \(1 \leq T \leq 100000,1 \leq n_{i} \leq 2^{20}\);

对于所有评测用例, \(1 \leq T \leq 100000,1 \leq n_{i} \leq 2^{30}\) 。

题目大意

给定 \(n\),求有多少个有序三元组 \((a, b, c)\) 满足:

  • \(1 \le a, b, c \le n\)
  • \(a \oplus b \oplus c = 0\)
  • \(a, b, c\) 能构成三角形。

题解

有三个数,找个特殊的数作为着手点,假设 \(a\) 最大。

条件二说明 没有两条边相等。假设 \(a = b\) 则 \(c = 0\),显然是不成立的。

在条件二两边同时 \(\oplus ~ a\),则 \(a = b \oplus c\)。

条件三表示, 任意两条边只和大于第 \(3\) 边,由于 \(a\) 最大,所以只需满足 \(a < b + c\) 即可。

由于 \(a = b \oplus c\),所以 \(b \oplus c < b + c\)。

我们知道 \(b \oplus c \le b + c\) 这条定理,所以只要让 \(b \oplus c \not = b + c\) 就好了。

异或操作其实可以视为 不进位的加操作。

\[b + c = b \oplus c + (b ~\&~ c) * 2 \]

那么,若想 \(b \oplus c \not = b + c\),只需 \(b ~\&~ c\) 不等于 \(0\) 即可,也就是让 \(b\) 和 \(c\) 有至少 \(1\) 位二进制位同时为 \(1\)。

综上,就可到了一下几条:

  • \(a > b,c \text{且} b \not = c\)
  • \(b\) 和 \(c\) 至少有一位二进制位同时为 \(1\) (\(b ~\&~ c \not = 0\))。

同时,由于题目中 \((a, b, c)\) 是有序的,这里假设的 \(a\) 最大,同时还有 \(b\) 最大、\(c\) 最大,所以最终得到的结果需要 乘 \(3\)。

由于数据量很大,那么我们尝试使用 数位DP。

在搜索过程中,我们定义 \(5\) 个状态,同时对其进行记忆化:

  1. idx: 从低位到高位,当前考虑到哪一位二进制位。
  2. limit: 考虑过的位数中,a 是否和 n 相等。
  3. _b: 考虑过的二进制位中 b 是否比 a 小。
  4. _c: 考虑过的二进制位中 c 是否比 a 小。
  5. _flag: 考虑过的二进制位中 b 和 c 是否存在同为 1 的二进制位。

具体实现参考代码。

/*
记忆化
dp[idx][limit][_b][_c][flag]
idx := 考虑到第 idx 位置
limit := 数字 a 是否受 n 限制
_b := 数字 b 是否小于 a
_c := 数字 c 是否小于 a
flag := 数字 b 和 c 是否用同为 1 的位
*/
ll dp[32][2][2][2][2]; 

int n;

/*
idx :=  从低位到高位,当前考虑到哪一位二进制位
limit := 考虑过的位数中,a 是否和 n 相等
_b := 考虑过的二进制位中 b 是否比 a 小
_c := 考虑过的二进制位中 c 是否比 a 小
_flag := 考虑过的二进制位中 b 和 c 是否存在同为 1 的二进制位
*/
ll dfs(int idx, bool limit, bool _b, bool _c, bool _flag){
    if(idx < 0) return _b && _c && _flag;
    if(dp[idx][limit][_b][_c][_flag] != -1) return dp[idx][limit][_b][_c][_flag]; // 记忆化
    // 数字 a 当前位能取的上限
    int top = limit ? (n >> idx) & 1 : 1;
    ll ans = 0;
    for(int a = 0; a <= top; a ++){
        for(int b = 0; b <= 1; b ++){
            int c = a ^ b;
            bool nxt_b = _b, nxt_c = _c;
            if(!_b){
                if(b > a) continue;
                if(b < a) nxt_b = true;
            }
            if(!_c){
                if(c > a) continue;
                if(c < a) nxt_c = true;
            }
            ans += dfs(idx - 1, limit && a == top, nxt_b, nxt_c, _flag || (b && c));
        }
    }
    dp[idx][limit][_b][_c][_flag] = ans;
    return ans;
}

void solve(void){
    memset(dp, -1, sizeof dp);
    cin >> n;
    cout << dfs(30, true, false, false, false) * 3ll << endl;
}

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

时间复杂度: \(O(T \cdot 31 \cdot 2^{4})\)

posted @ 2026-05-30 18:22  x_yeyue  阅读(62)  评论(0)    收藏  举报