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[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 列。
于是,就可以将答案拆分乘两部分。
- 前 \(row - 1\) 行 所有元素的和。
其中的每一行所有元素的和都是一个 三角形数,而 三角形数的求和公式 为 \(S_{n} = \frac{n (n + 1) (n + 2)}{6}\),可以快速计算。
(有关三角形数的详情,见文章 三角形数) - 第 \(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\) 的括号序列,要求支持两种操作:
将 \(\left[L_{i}, R_{i}\right]\) 区间内(序列中的第 \(L_{i}\) 个字符到第 \(R_{i}\) 个字符)的括号全部翻转(左括号变成右括号,右括号变成左括号)。
求出以 \(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\) 的括号序列,支持两种操作:
- 区间翻转:将区间 \([L, R]\) 内的每个括号翻转(
(变为),)变为()。 - 查询最长合法括号区间:给定左端点 \(L\),求最大的 \(R\) 使得子串 \([L, R]\) 是一个合法的括号序列(即括号匹配正确)。若不存在这样的 \(R\),输出 0。
题解
定义: '(' 为 \(1\),')' 为 '-1'。
那么一个合法的括号序列就转化为 累加和为 \(0\) 的区间。
显然这题可以利用线段树来快速操作。
构造一个线段树,其中维护三个属性:
- 累加和。
- 最小前缀和。
- 最大前缀和。
最小、最大前缀和 的作用后面会提。
这里简单介绍下最小前缀和。
\[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 \leq a, b, c \leq n_{i}\);
\(a \oplus b \oplus c=0\) ,其中 \(\oplus\) 表示二进制按位异或;
长度为 \(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 \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\) 个状态,同时对其进行记忆化:
idx: 从低位到高位,当前考虑到哪一位二进制位。limit: 考虑过的位数中,a 是否和 n 相等。_b: 考虑过的二进制位中 b 是否比 a 小。_c: 考虑过的二进制位中 c 是否比 a 小。_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})\)

浙公网安备 33010602011771号