牛客周赛 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,所以这就证明了后面的数都可以由38生成。当然打表也可以。


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})\)

posted @ 2025-06-16 11:29  alij  阅读(54)  评论(0)    收藏  举报