模拟赛题讲解[13]
来自 yukuai26 2022-08-06 noi.ac #2757
据说是从JOI搬过来的,赛时有位巨佬18min就A了 Orz
题目描述:
橙 —— 作为幽幽子大人的朋友的使者, 非常贪玩, 也喜欢思考有趣 的问题。
她在冥界思考一个有趣的问题, 希望赶到的灵梦能够解答。
一个字符串由 $\tt{a,b,c}$三种字母组成, 它最多能分成多少个 $\tt{abc}$ 和 $\tt{cbc}$ 的子序列呢?
具体的, 一个字符串 $str$ 可以由选择 $3$ 个位置 $i < j < k$ 使得 $s_i,s_j,s_k$ 依次连接组成 $\tt{abc}$ 或 $\tt{cbc}$ ,
这样算分出一个字符串, 接着把 $3$ 个位置上的字母删掉, 剩下的字母重新组成新的字符串并接着提取。
输入格式:
第一行一个正整数 $n$ 表示字符串的长度
接下来一个长度为 $n$ 的字符串, 保证由 $\tt{a,b,c}$ 组成
输出格式:
一行一个数,表示最大能分成的个数
输入1:
6
abcbcc
输出1:
2
输入2:
5
abcbb
输出2:
1
数据范围:
打包测试
对于测试包 $1$,分值 $40$ 分,$n \leq 15$
对于测试包 $2$,分值 $30$ 分,$n \leq 50$
对于测试包 $3$,分值 $20$ 分,$n \leq 3000$
对于测试包 $4$,分值 $10$ 分,$n \leq 10^6$
题解:
Subtask 1 40pts
注意到 $\tt{c}$ 可以作为结尾,也可以作为开头
考虑对于所有的 $\tt{c}$ 暴搜他作为第一个还是第三个
如果是第一个就变成 $\tt{a}$ ,计算即可。
Subtask 2 30pts
考虑从后往前做
设 $f_{i,j,k}$ 表示最后 $i$ 位,有 $j$ 个 $\tt{bc}$ ,$k$ 个 $\tt{c}$ ,最多能有多少 $\tt{abc}$ 或 $\tt{cbc}$ ,转移 $O(1)$。
Subtask 3 20pts
把所有作为 $\tt{abc}$ 和 $\tt{cbc}$ 末尾的 $\tt{c}$ 放在原串的最后面
具体地,如果串里面出现了
我们强制做如下匹配
这样的好处在于,对于合法的方案,一定存在一个分界线,满足:
- 分界线左侧的所有 $\tt{c}$ 均可以看作 $\tt{a}$
- 分界线右侧仅由 $\tt{bc}$ 构成,并且与分界线左侧每个 $\tt{a/c}$ 两两配对
考虑枚举这个分界线,时间复杂度 $O(n^2)$
Subtask 4 10pts
注意到如果右侧的 $\tt{bc}$ 不够了,分界线理应往左移
如果左侧的 $\tt{c}$ 过多了,分界线理应往右移
实际上是存在单调性的。考虑二分分界线。
时间复杂度 $O(n \log n)$ ,可以通过此题
代码:
#include <iostream>
#include <string>
#include <vector>
#include <algorithm>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <cmath>
#include <iomanip>
#include <random>
using namespace std;
#define int long long
#define INF 0x3f3f3f3f3f3f3f3f
#define N (int)(1e6+15)
int n;
bool vis[N];
char s[N];
bool check(int mid)
{
for(int i=1; i<=n; i++) vis[i]=0;
int cnt_a=0,cnt_b=0,cnt_c=0;
for(int i=n; i>=1; i--)
{
if(s[i]=='c'&&cnt_c<mid) vis[i]=1,++cnt_c;
else if(s[i]=='b'&&cnt_b<cnt_c) vis[i]=1,++cnt_b;
}
if(cnt_b < mid)return 0;
cnt_a=cnt_b=0;
for(int i=n; i>=1; i--)
{
if(s[i]=='b'){if(vis[i]) ++cnt_b; else continue;}
else if(!vis[i] && cnt_a < cnt_b) ++cnt_a;
}
return cnt_a==mid;
}
signed main()
{
ios::sync_with_stdio(0);
cin.tie(0);cout.tie(0);
// freopen("check.in","r",stdin);
// freopen("check.out","w",stdout);
cin >> n >> (s+1);
int l=0,r=n/3,mid;
while(l<r)
{
mid=(l+r+1) >> 1;
if(check(mid)) l=mid;
else r=mid-1;
}
cout << l << '\n';
return 0;
}
Extra 更快的解法
时间复杂度 $O(n)$ ,目前q779还不会,因此把老师的题解搬过来了
O(n)
考虑从后往前做,记录bc和c的个数
对于已经有bc时出现c的情况,先匹配成cbc,并记录这个c的位置
对于没有c并且出现b的情况,也记录这个b的位置
这时出现a并且没有bc,我们考虑反悔操作,把前面的cbc拆开变成abc,这时候会多出一个c,根据前面记录的b的位置与c的位置视情况会多出一个c或者bc
老师的代码:
#include<bits/stdc++.h>
#define For(i,j,k) for (int i=(int)(j);i<=(int)(k);i++)
#define Rep(i,j,k) for (int i=(int)(j);i>=(int)(k);i--)
#define ll long long
using namespace std;
const int N=1000005;
int n,ans;
char s[N];
int f[N][3];
int g[N][3];
int main(){
scanf("%d%s",&n,s+1);
For(i,1,n){
For(j,0,2) f[i][j]=f[i-1][j];
if (s[i]=='a'||s[i]=='c') ++f[i][0];
else if (f[i][0]) --f[i][0],++f[i][1];
}
Rep(i,n,1){
For(j,0,2) g[i][j]=g[i+1][j];
if (s[i]=='c') ++g[i][0];
else if (s[i]=='b'){
if (g[i][0]) --g[i][0],++g[i][1];
}
else{
if (g[i][1]) --g[i][1],++g[i][2];
}
}
For(i,0,n){
int sum=g[i+1][2],v;
v=min(f[i][0],g[i+1][1]);
sum+=v; f[i][0]-=v; g[i+1][1]-=v;
v=min(f[i][1],g[i+1][0]);
sum+=v; f[i][1]-=v; g[i+1][0]-=v;
v=min(f[i][1],g[i+1][1]);
sum+=v; f[i][1]-=v; g[i+1][1]-=v;
ans=max(ans,sum);
}
printf("%d\n",ans);
}