牛客周赛 Round 96
A题
题意:小歪正在点外卖,他可以选择使用八折优惠券或者立减5元优惠券,仅可使用其中一个,现在要购买\(x\)元的外卖,问使用哪一张优惠券会使得最终支付的金额最小。显然我们只需要判断求出相等时的\(x\)即可,即\(x\times 0.8=x-5,解得x=25\).所以如果\(x==25\),输出0;\(x<25\),输出5;\(x>25\),输出8.
B题
知识点:贪心+排序

解法:先对\(a\)数组做\(a_i=|a_i-k|\)的操作。因为后面是可以不按顺序随便选择,所以我们贪心的选择\(m\)名最小的\(a_i\)即可,对数组\(a\)升序排序,输出a[m]即可。
C题
知识点:模拟

根据题意,对于一个确定的字符\(cc\),不会不出现他后面和前面同时能和它匹配,因此直接使用栈模拟得到的最后一定就是最短的,不会出现\(cc\)和后面的匹配使得最后的字符串长度最短,因此直接模拟。注意小写和大写的的反过来的,我当时有点怀疑,所以按照一直消除相邻的,直到不能消除,最后还是过了。
int check(char c){
if(c>='a'&&c<='z') return 1;
else return 0;
}
bool contains(string&s){
int n=s.size();
string s1;
for(int i=0;i+1<n;){
if((check(s[i])^check(s[i+1]))==1){
s1+=s[i];
i++;
}else if(check(s[i])){
if(s[i]-'a'+s[i+1]-'a'==25&&s[i]<s[i+1]){
i+=2;
}else{
s1+=s[i];
i++;
}
}else{
if(s[i]-'A'+s[i+1]-'A'==25&&s[i]>s[i+1]){
i+=2;
}else{
s1+=s[i];
i++;
}
}
if(i==n-1) s1+=s[i];
}
if(s1==s) return false;
else{
s=s1;
return true;
}
}
void solve(){
int n;cin >> n;
string s;cin >> s;
while(contains(s));
cout << s.size() << endl;
}
时间复杂度在最坏情况下可以到\(O(n^2)\)的,所以还是直接用栈模拟是\(O(n)\)。
D题
知识点:打表(找规律)

可以发现增加的数有\(3,8,15,24,\dots\),但是我们用3和8打表发现:

只有\(2,3,4,6,8,11\)不能组成,所以直接特判这几个数就可以了。其实这好像是叫一个麦乐鸡块定理,就是表达式\(3x+8y(x和y非负)\)不能表示的最大正整数为\(3*8-3-8=13\),这是增加的,所以最大不能表示的数为14,所以这就证明了后面的数都可以由3和8生成。当然打表也可以。
E题
知识点:后缀和,贪心

直接看我的分析:
E题应该考虑答案的集合有哪些,分析可得答案集合A={全0,全1,000...000111..111},我们取答案集合从原字符串变为答案集合中代价最小的那一个即可。
朴素的做法是枚举每一个分界位置i,即[1,i-1]变成全0,代价令为pre_cost,我们一边枚举一边维护pre_cost的值
同时[i,n]变为全1,我们可以提前预处理出每一个i的后缀要修改的代价,当然这样就需要知道将[1,i-1]变为全0操作的次数,因为如果将[1,i-1]变为全0操作次数为偶数次,那么s[i]就是原字符串的s[i],否则现在应该是被翻转了,即s[i]=((s[i]-'0')^1)+'0'。但是我们不用去修改它,只用知道在将前面[1,i-1]变为全0时,当前s[i]实际是'0'还是'1'
这样是求去算s[i]是否要操作,因为[i,n]它们变化的次数一致,对于变为全1没有影响
000...011001->从省略号后面我们算第一位,那么会改(1,2,4,7)
000...100110->我们会改(2,4,7)
可以看出主要是对当前第i位的影响,所以枚举第i位的答案应该为pre_{i-1}+第i位实际是否为1的花费+suf[i+1]
所以先预处理出每一个后缀的花费,之后枚举每一个i,同时维护pre_cost,同时统计答案即可
但是上述枚举的答案都是000...111...这样形式的,答案还应该包括全0和全1,可以对s进行填充'0'+s+'1',这样我们实际枚举[1,n+1]的所有位置这样就将答案全0和全1考虑进最终答案集合了。
注意预处理我们是找到前面与自己不同的才计算进答案,同时后缀数组记录的是[i+1,n]要修改的代价,如果s[i]实际是'1',那么后面的就一定是变为1的,如果当前是'0',那么后面的也全是0,这时候再操作一次i,后面全变为'1',这样就更能证明先全变为0的正确性了。
从上述暴力做法我们总结还可以先变为全0,之后再撤销操作,这也是一个很好的办法。
现在证明一下其正确性,答案集合为{000000...,111111...,000...1111...}。朴素做法容易看出将s变为答案集合中的一个
令为ans1,所做的操作都是必要的,不会有更少的代价将其变为ans1.
令全0串为s0,现在怀疑的是将s->s0->ans1,会不会比s->ans1所需要的代价更多呢?考虑下面例子
01000111010100101
变为全0需要操作(2,3,6,9,10,11,12,13,15,16,17)
现在令ans1=000111111111111111,考虑朴素做法,需要在(2,3,4,6,9,10,11,12,13,15,16,17),发现确实只要撤销操作4
和朴素做法一样。再考虑一般性,将[1,i-1]变为全0,和朴素做法一致,唯一不同在于将[i,n]变为全1和全0。
两者不同之处在于变为全0:如果s[i]实际为1,那么要变为0,后续和朴素做法变为全1一致,而朴素做法如果s[i]实际为1
那么不会去操作,变为全0多了这一个操作,所以撤销之后和朴素做法是等价的;那么如果s[i]实际为0,那么这里不操作
而朴素做法要操作一次,所以再操作一次就撤销了,所以可以证明先变为全0再撤销是正确的。
代码包含两种做法:
#include<bits/stdc++.h>
using namespace std;
#define inf 1e18
#define endl '\n'
#define int long long
typedef long long ll;
typedef pair<int,int> pii;
int dx[4] = {1, 0, -1, 0}, dy[4] = {0, 1, 0, -1};
const int maxn=2e5+9,mod=1e9+7;
void solve(){
int n;cin >> n;
string s;cin >> s;
s='0'+s+'1';
vector<int> c(n+2,0);//拓展了s
for(int i=1;i<=n;i++) cin >> c[i];
//预处理后缀
//后缀记录的就是那些s[i]!=s[i+1]的地方
//s[0]固定为0,不修改
vector<int> suf(n+3,0);
for(int i=n+1;i>=1;i--){
suf[i]=suf[i+1];
//第一个与自己不相同的地方才修改这个位置
if(s[i]!=s[i-1]){
suf[i]+=c[i];
}
}
int pre=0;//记录[0,i-1]变为全0的修改次数的奇偶性
int pre_cost=0;
int ans=inf,cur=0;
for(int i=1;i<=n+1;i++){
char ch=s[i];
ch=((s[i]-'0')^pre)+'0';
cur=suf[i+1];
if(ch=='0'){
cur+=c[i];
}
ans=min(ans,cur+pre_cost);
if(s[i]!=s[i-1]){
pre^=1;
pre_cost+=c[i];
}
//01000111010100101
}
cout << ans << endl;
}
void solve1(){
int n;cin >> n;
string s;cin >> s;
s='0'+s;
vector<int> c(n+1,0);
for(int i=1;i<=n;i++) cin >> c[i];
int sum=0,mx=0;
for(int i=1;i<=n;i++){
if(s[i]!=s[i-1]){
sum+=c[i];
mx=max(mx,c[i]);
}
}
cout << sum-mx << endl;
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
int t=1;
cin >> t;
while(t--){
solve1();
}
return 0;
}
时间复杂度为\(O(n)\)。
F题
知识点:线性基
前置知识:线性基。由于我没学过,所以下面先复述一遍,加深理解。可能说的不对,大佬勿喷。
给定一个序列\(a\),那么我们一定可以找到一个序列\(b\),使得\(a\)中的每一个元素都可以由\(b\)的一个子序列通过定义的操作得到,这道题操作就是异或。这和高中学的平面上的一组基向量是一致的,还有一个正整数的质数分解式,可以找到这个正整数的所有质数以及质数的幂,这样这个正整数的所有因数都可以由质数和幂构成,就是\(N=p_1^{\phi_1}p_2^{\phi_2}p_3^{\phi_3}p_4^{\phi_4}\dots p_n^{\phi_n}\),那么它的每一个因数都可以根据这个构造出来。
得到序列\(a\)的线性基序列有什么好处呢?因为通过\(a\)线性变换得到的数,都可以通过序列\(b\)得到,且\(b\)的大小为\(log(A)\),\(A为a的值域\)。一下小很多,可以优化\(check\)一个数是否能被\(a\)构造的时间。
所以我们学习了线性基是什么,如何构造线性基,如何\(check\)一个数能否由线性基序列构成。
由于本题是异或操作,会推出一个结论,\(mex\)一定是\(2^k\)这种形式的,即为2的幂。证明:
假设最后mex是17:10001
那么0到16就都可以得到,我们挑选这几个数
00000 00001 00010 00100 01000 10000
可以知道通过异或操作可以得到00000到11111的所有数,与假设矛盾。
我们总是可以从[0,mex-1]中挑出上述那样的数,假设mex-1表示成2进制的最高位为t,那么一定可以得到t个0到t个1这样的所有数,那么不能表示的数一定是2^t,即t个1再加1不能表示。
构造线性基:
定义一个base,大小为log(A),这道题就是vector<int> base(60,0);
每次插入一个数,从高位向低位枚举,如果这一位j为1,考虑插入操作,如果base[j]已经插入过,那么x异或上base[j],否则插入x,即base[j]=x,同时要break掉;
contains:
从高位枚举x,遇到为1的且base[j]存在的和base[j]做异或操作
枚举完之后判断x是否为0,为0可以构造,否则不能构造
#include<bits/stdc++.h>
using namespace std;
#define inf 1e18
#define endl '\n'
#define int long long
typedef long long ll;
typedef pair<int,int> pii;
int dx[4] = {1, 0, -1, 0}, dy[4] = {0, 1, 0, -1};
const int maxn=2e5+9,mod=1e9+7;
vector<int> base(60);
void insert(int x){
//从高位枚举x的1位
for(int j=59;j>=0;j--){
if(x >> j &1){
if(base[j]){
x^=base[j];
}else{
base[j]=x;
break;
}
}
}
}
bool contains(int x){
for(int j=59;j>=0;j--){
if(x >> j & 1){
if(base[j]){
x^=base[j];
}
}
}
return x==0;
}
void solve(){
for(int i=0;i<60;i++) base[i]=0;
int n;cin >> n;
for(int i=1;i<=n;i++){
int t;cin >> t;
insert(t);
}
//枚举2的幂
for(int i=0;i<=60;i++){
if(!contains(1LL << i)){
cout << (1LL << i) << endl;
break;
}
}
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
int t=1;
cin >> t;
while(t--){
solve();
}
return 0;
}
时间复杂度为\(O(n\times\log{A}+\log^2{A})\)。

浙公网安备 33010602011771号