牛客周赛 Round 36(A,B,C,D,E,F)

本文解析了几道编程竞赛题目,涉及字符串操作(A小红)、矩阵构造(B小红)、字符替换(C小红)、路径搜索(D小红和E小红走矩阵)以及回文子串查询(F小红好子串询问),主要运用了贪心算法、BFS搜索、线段树等技术。
摘要由CSDN通过智能技术生成

比赛链接

这场简单,只有F题比较值得一做,C是贪心,D是个BFS,E是构造,F是不太明显的线段树。


A 小红的数位删除

思路:

string 的提取字串函数 substr() 即可。

code:

#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;

string s;

int main(){
	cin>>s;
	cout<<s.substr(0,s.length()-3);
	return 0;
}

B 小红的小红矩阵构造

思路:

数据很小 n , m ≤ 100 n,m\le 100 n,m100 ,直接暴力,统计一下总和,算出每一行每一列的异或值,看一下是否满足条件即可。

code:

#include <iostream>
#include <cstdio>
#include <set>
using namespace std;
const int maxn=105;

int n,m,x,a[maxn][maxn];

long long tot;

int main(){
	cin>>n>>m>>x;
	for(int i=1;i<=n;i++){
		for(int j=1;j<=m;j++){
			cin>>a[i][j];
			a[i][0]^=a[i][j];
			a[0][j]^=a[i][j];
			tot+=a[i][j];
		}
	}
	if(tot!=x){
		puts("wrong answer");
		return 0;
	}
	set<int> S;
	for(int i=1;i<=n;i++)S.insert(a[i][0]);
	for(int j=1;j<=m;j++)S.insert(a[0][j]);
	puts((S.size()==1)?"accepted":"wrong answer");
	return 0;
}

C 小红的白色字符串

思路1:

这题做到身败名裂。

其实就是很简单的贪心策略,如果某个字符是个不满足条件的字符就直接涂白掉就行了,注意涂白不是删除。

为什么这么贪心是对的:我们每次看到的某个位置的字符不满足条件,说明这个字符是个大写字母,并且前面那个字符没有被涂白,这时候我们可以涂白前面的字符,也可以涂白这个字符,显然涂白这个字符能给后面的字符留出更多操作空间,因此这样贪心就是对的。

code1:

#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;

int cnt;
string s;

bool isu(char ch){
	return ch>='A' && ch<='Z';
}

int main(){
	cin>>s;
	for(int i=1;i<s.length();i++){
		if(isu(s[i]) && s[i-1]!='!'){
			s[i]='!';
			cnt++;
		}
	}
	cout<<cnt;
	return 0;
} 

思路2:

模拟一下这个过程,其实就是遇到一段长为 l e n len len 的连续的大写字母时,把这一段上第 1 , 3 , 5 , … 1,3,5,\dots 1,3,5, 位置上的字符涂白,这就需要涂白 ⌈ l e n 2 ⌉ \left\lceil\dfrac{len}2\right\rceil 2len 个位置。所以找一下有多少个大写字母段,对每段数一下贡献也是可以的。

code2:

#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;

string s;
int cnt;

inline bool isu(char x){
	return x>='A' && x<='Z'; 
}

int main(){
	cin>>s;
	for(int l=1,r=0;l<s.length();l++){
		if(isu(s[l])){
			r=l+1;
			while(r<s.length() && isu(s[r]))r++;
			cnt+=(r-l+1)/2;
			l=r;
		}
	}
	cout<<cnt;
	return 0;
}

D 小红走矩阵

思路:

就是个很板的BFS,无非就是下一次移动多一个 不能走到和当前位置字母相同的位置上 的限制条件而已。

code:

#include <iostream>
#include <cstdio>
#include <queue>
#include <cstring>
#define mk make_pair
using namespace std;
const int maxn=1005;
const int inf=1e9;

int n,m;
string mp[maxn];
bool vis[maxn][maxn];
int d[maxn][maxn];

int fx[]={1,-1,0,0},fy[]={0,0,1,-1};

int main(){
	cin>>n>>m;
	for(int i=1;i<=n;i++){
		cin>>mp[i];
		mp[i]=" "+mp[i];
	}
	
	for(int i=1;i<=n;i++)
		for(int j=1;j<=m;j++)
			d[i][j]=inf;
	queue<pair<int,int> > q;
	q.push(mk(1,1));
	d[1][1]=0;
	while(!q.empty()){
		int ux=q.front().first,uy=q.front().second;
		q.pop();
		if(ux==n && uy==m){
			cout<<d[n][m];
			return 0;
		}
		if(vis[ux][uy])continue;
		else vis[ux][uy]=true;
		for(int i=0,x,y;i<4;i++){
			x=ux+fx[i];
			y=uy+fy[i];
			if(x<1 || x>n || y<1 || y>m || mp[x][y]==mp[ux][uy])continue;
			if(d[x][y]>d[ux][uy]+1){
				q.push(mk(x,y));
				d[x][y]=d[ux][uy]+1;
			}
		}
	}
	cout<<-1;
	return 0;
}

E 小红的小红走矩阵

思路:

构造题,这几个限制条件说的其实就是:

  1. 答案路径不能遍历整个图。
  2. 答案路径必须拐个弯,不能只靠向左向下就行。
  3. 某个字母不能出现超过一半的位置。
  4. 必须存在答案路径。

那我们的构造可以分成两种思路,一种是先让答案路径必须遍历整个图,然后再开辟一条近路,一种是先让答案路径可以简单的走到最下面,然后再走到最右边,然后再在必经之路上卡住,让答案路径绕一下远路。直觉上第二种更简单,因此尝试第二种。

让路径一直向下走到头而不从中间跑出去,我们可以让相邻两行不同,然后第二列和第一列保持一致。走到最下边后,想要不能只向右走,可以在某个相邻两个位置放上一对相同的字符,这样答案就必须向上绕路。这样构造出来了,其他位置无所谓,为了不出现某个字符很多的情况,可以在没必要的位置用 26 26 26 个字母轮着铺。

code:

#include <iostream>
#include <cstdio>
using namespace std;
const int maxn=1e3+5;

int n,m;
char mp[maxn][maxn];

int main(){
	cin>>n>>m;
	for(int i=1;i<=n;i++)
		for(int j=1;j<=m;j++)
			mp[i][j]=(i+j)%26+'a';

	for(int i=1;i<n;i++)mp[i][2]=mp[i][1];
	mp[n][m-1]=mp[n][m];
	
	for(int i=1;i<=n;i++)cout<<mp[i]+1<<endl;
	return 0;
}

F 小红的好子串询问

思路:

很棒的线段树题,这个问法就很线段树。其实一开始读假题了,想成是不允许有长度大于2的回文串,不然赛时应该能A掉的。

考虑如何满足不允许有长度不小于2的回文串的限制条件。显然所有回文串都是由更小的回文串两边加相同字符得到的,因此我们只要保证序列中没有长为 2 2 2 3 3 3 的回文串,就可以保证不存在更长的回文串。

没有长为 2 2 2 的回文串,意味着相邻字符不同,没有长为 3 3 3 的回文串,意味着隔一个字符的两个字符也不同。这其实就是规定了连续的三个字符一定各不同,加上只能用这三种字符的限制条件,那么这个字符串的字符一定是以 3 3 3 为周期在循环的。也就说,只要确定了最前面三个字符的排列方法,我们就知道了整个字符串是什么

而最前面三个字符的排列只有 A 3 2 = 6 A_3^2=6 A32=6 种情况。即 rededrdrerdedererd。如果用 012 012 012 分别代表 r e d red red,那么就是 012120201021210102。把一个字符串修改为满足条件的串,就相当于把这个串修改成上述的任意一种的串。那么这个题的操作 2 2 2 就变成了统计把一段子串修改为上述六种其一的串的最少修改次数。

因为要在 l o g log log 时间内对某个区间的信息进行统计,考虑线段树维护。但是两个区间也就是串的合并规则并不明确。

考虑如何将两个串合并。发现 012120201 相当于一个串的循环,同理 021210102。如果一个 012 类型的串后面接上另外一个串仍然保持 012 类型,那么就需要看前面那个串的长度。如果前面的串模 3 3 3 有余数的话,这个多出来的一小部分相当于给后面的串补了一点点,比如余 1 1 1,前面的串相当于给后面的串补了一个最前面的 0 0 0,后续只需要循环补上 120 120 120 就行了。于是有:

  1. 如果长度能被 3 3 3 整除,那么后面就接 012 012 012 类型。
  2. 如果长度能除 3 3 3 1 1 1,那么后面就接 120 120 120 类型。
  3. 如果长度能除 3 3 3 2 2 2,那么后面就接 201 201 201 类型。

同理,对其他情况进行一下讨论,就可以得到合并规则。这样就可以使用线段树进行维护了。

code:

#include <iostream>
#include <cstdio>
using namespace std;
const int maxn=1e5+5;

int n,Q;

struct segment_tree{
	#define ls p<<1
	#define rs p<<1|1
	
	struct Node{
		//r-0 e-1 d-2
		//修改为012 120 201 021 102 210序列的修改次数 
		int x[6],len;
		Node(int a=0,int b=0,int c=0,int d=0,int e=0,int f=0){
			x[0]=a;
			x[1]=b;
			x[2]=c;
			x[3]=d;
			x[4]=e;
			x[5]=f;
		}
	}tr[maxn<<2];
	Node merge_Node(Node a,Node b){
		Node t;
		t.len=a.len+b.len;
		if(a.len%3==0){
			t.x[0]=a.x[0]+b.x[0];
			t.x[1]=a.x[1]+b.x[1];
			t.x[2]=a.x[2]+b.x[2];
			t.x[3]=a.x[3]+b.x[3];
			t.x[4]=a.x[4]+b.x[4];
			t.x[5]=a.x[5]+b.x[5];
		}
		else if(a.len%3==1){
			t.x[0]=a.x[0]+b.x[1];
			t.x[1]=a.x[1]+b.x[2];
			t.x[2]=a.x[2]+b.x[0];
			t.x[3]=a.x[3]+b.x[5];
			t.x[4]=a.x[4]+b.x[3];
			t.x[5]=a.x[5]+b.x[4];
		}
		else {
			t.x[0]=a.x[0]+b.x[2];
			t.x[1]=a.x[1]+b.x[0];
			t.x[2]=a.x[2]+b.x[1];
			t.x[3]=a.x[3]+b.x[4];
			t.x[4]=a.x[4]+b.x[5];
			t.x[5]=a.x[5]+b.x[3];
		}
		return t;
	}
	
	void build(int p,int l,int r){
		if(l==r){
			char c;
			cin>>c;
			if(c=='r'){
				tr[p].x[0]=tr[p].x[3]=0;
				tr[p].x[1]=tr[p].x[2]=tr[p].x[4]=tr[p].x[5]=1;
			}
			else if(c=='e'){
				tr[p].x[1]=tr[p].x[4]=0;
				tr[p].x[0]=tr[p].x[2]=tr[p].x[3]=tr[p].x[5]=1;
			}
			else {
				tr[p].x[2]=tr[p].x[5]=0;
				tr[p].x[0]=tr[p].x[1]=tr[p].x[3]=tr[p].x[4]=1;
			}
			tr[p].len=1;
			return;
		}
		int mid=(l+r)>>1;
		build(ls,l,mid);
		build(rs,mid+1,r);
		tr[p]=merge_Node(tr[ls],tr[rs]);
	}
	
	void print(int p,int l,int r){
		printf("%d[%d,%d] ",p,l,r);
		for(int i=0;i<6;i++)printf("%d ",tr[p].x[i]);
		printf("\n");
		if(l==r)return;
		int mid=(l+r)>>1;
		print(ls,l,mid);
		print(rs,mid+1,r);
	}
	void print(){print(1,1,n);}
	
	void mdy(int p,int l,int r,int idx,int v){
		if(l==r){
			tr[p].x[0]=tr[p].x[1]=tr[p].x[2]=tr[p].x[3]=tr[p].x[4]=tr[p].x[5]=1;
			tr[p].x[v]=tr[p].x[v+3]=0;
			return;
		}
		int mid=(l+r)>>1;
		if(idx<=mid)mdy(ls,l,mid,idx,v);
		else mdy(rs,mid+1,r,idx,v);
		tr[p]=merge_Node(tr[ls],tr[rs]);
	}
	void mdy(int idx,char ch){
		int t;
		if(ch=='r')t=0;
		else if(ch=='e')t=1;
		else t=2;
		mdy(1,1,n,idx,t);
	}
	
	Node qry(int p,int l,int r,int L,int R){
		if(L<=l && r<=R){
			return tr[p];
		}
		int mid=(l+r)>>1;
		if(R<=mid)return qry(ls,l,mid,L,R);
		else if(L>mid)return qry(rs,mid+1,r,L,R);
		else return merge_Node(qry(ls,l,mid,L,R),qry(rs,mid+1,r,L,R));
	}
	int qry(int l,int r){
		Node t=qry(1,1,n,l,r);
		int minn=1e9;
		for(int i=0;i<6;i++)minn=min(minn,t.x[i]);
		return minn;
	}
	
	#undef ls
	#undef rs
}tr;

int main(){
	cin>>n>>Q;
	tr.build(1,1,n);
	
	while(Q--){
		int op;
		cin>>op;
		if(op==1){
			char ch;
			int idx;
			cin>>idx>>ch;
			tr.mdy(idx,ch);
		}
		else {
			int l,r;
			cin>>l>>r;
			cout<<tr.qry(l,r)<<endl;
		}
	}
	return 0;
} 
  • 15
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
牛客 a卷2022年第四季度的华为题目中,要求考生设计一种高效的数据结构,能够支持以下几种操作: 1. 添加一个元素 2. 删除一个元素 3. 查找是否存在某个元素 4. 返回元素的总数 该数据结构要求满足空间复杂度较小、时间复杂度较低、能够快速地进行查找和修改等多种操作。 想要编写这样一种数据结构,我们可以参考许多已有的经典算法与数据结构,如二叉树、哈希表、红黑树等,通过综合利用它们的优点来实现这个问题的解决。 例如,我们可以通过哈希表来存储所有元素的值,并在每个哈希链表的元素中再使用红黑树来进行排序与查找。这样,我们既能够轻松地进行元素的添加和删除操作,也能够在查找较大数据范围和数量时保持较高的速度与效率。同时,由于使用了多个数据结构来协同完成这个问题,我们也能够在空间复杂度上适度地进行优化。 当然,在具体设计这个数据结构的过程中,我们还需要考虑一些实践中的细节问题,例如如何避免哈希冲突、如何处理数据丢失与被删除元素所占用的空间等问题,这都需要相应的算法与流程来进行处理。 总体来看,设计这种支持多种操作的高效数据结构,需要我们具备丰富的算法知识和编程实践能力,同时需要我们在具体处理问题时能够将多种算法和数据结构进行有效地结合。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值