嘘~ 正在从服务器偷取页面 . . .

模拟赛题讲解[13]


模拟赛题讲解[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{a\dots b\dots c \dots c \dots b \dots c \dots} \] 我们强制做如下匹配 \[ \tt{\color{red}{a} \,\color{black}{\dots} \, \color{red}{b} \,\color{black}{\dots} \, \color{blue}{c} \,\color{black}{\dots}\, \color{red}{c} \,\color{black}{\dots}\, \color{blue}{b}\, \color{black}{\dots} \, \color{blue}{c}\, \color{black}{\dots}}\, \] 这样的好处在于,对于合法的方案,一定存在一个分界线,满足

  • 分界线左侧的所有 \(\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);
}

文章作者: q779
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-ND 4.0 许可协议。转载请注明来源 q779 !
评论
  目录