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

比赛链接

官方讲解不分P不分段直接两小时怼上来是坏文明

这场的题很棒,思维有难度,考察的知识点广泛,有深度,很透彻。感觉学到了很多。建议补题。


A 小红的正整数自增

思路:

签到。

可以模拟一下,看加上几次,个位就变成 0 0 0 了。

或者可以用 10 10 10 减一下个位数,看看离进位差几。

code:

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

int n;

int main(){
	cin>>n;
	cout<<(10-n%10)%10;
	return 0;
}

B 小红的抛弃后缀

思路:

有一个初等数论的知识点可以秒掉此题: 一个数是 9 的倍数 ⇔ 这个数十进制数码的和能被 9 整除 一个数是 9 的倍数\Leftrightarrow这个数十进制数码的和能被9整除 一个数是9的倍数这个数十进制数码的和能被9整除

比如 1233 1233 1233 的各个数位上的数分别为 1 , 2 , 3 , 3 1,2,3,3 1,2,3,3,它们的和 1 + 2 + 3 + 3 = 9 1+2+3+3=9 1+2+3+3=9 可以被 9 9 9 整除,那么 1233 1233 1233 这个数就可以被 9 9 9 整除,反之也成立。

所以我们删掉一个后缀使得剩余的数能被 9 9 9 整除,其实就相当于去验证剩余的前缀的数字之和能被 9 9 9 整除即可。

我们跑一遍前缀和,并检查每个前缀和能否被 9 9 9 整除。能被整除的前缀和的个数就是答案。

code:

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

int n,s[maxn],ans;
string str;

int main(){
	cin>>str;
	n=str.length();
	str=" "+str;
	
	for(int i=1;i<=n;i++){
		s[i]=(s[i-1]+str[i]-'0')%9;
		if(!s[i])ans++;
	}
	cout<<ans<<endl;
	return 0;
}

不止 9 9 9 有这种性质,其他很多数也有各种不同的性质。它们基本都是通过同余的性质来证明的,证明可以看:初等数论-同余性质的应用


C 小红的字符串构造

思路:

如果我们全部填入一个相同的字母,那么最多可以出现 1 + 2 + 3 + ⋯ + n − 1 = n ∗ ( n − 1 ) 2 1+2+3+\dots+n-1=\dfrac{n*(n-1)}{2} 1+2+3++n1=2n(n1) 个回文串,但是题目却保证 k ≤ n 2 k\le \dfrac{n}2 k2n,这就很奇怪。

考虑到我们两个相同的字母放在一起就可以用两个字符组成一个回文串,这就正好和题目的 k ≤ n 2 k\le \dfrac{n}2 k2n 对应上了。所以我们用 a a b b c c a a b b c c … aabbccaabbcc\dots aabbccaabbcc 这种方式来组回文串——两个字符组成一个回文串,为了防止出现其他回文串,需要至少三种字符来进行循环。

k k k 个回文串,那么前面需要 2 k 2k 2k 个字符来凑。后面多余的位置就可以用 d e f d e f d e f … defdefdef\dots defdefdef 这种方式来填充,同样的为了防止出现回文串,需要至少三种字符进行循环。

code:

"abc"[i%3] 相当于

char str[]="abc";

str[i%3];

前面的字符串相当于一个匿名的字符数组。并且占有空间(应该?)。

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

int n,k;
string s;

int main(){
	cin>>n>>k;
	for(int i=1;i<=k;i++){
		s+="abc"[i%3];
		s+="abc"[i%3];
	}
	for(int i=2*k+1;i<=n;i++)
		s+="def"[i%3];
	cout<<s<<endl;
	return 0;
}

D 小红的平滑值插值

思路:

发现如果相邻两项的差值超过了 k k k,我们就需要向中间插入数字,将这个差值降下来。

而降差值的时候可以贪心地来进行,即每差 k k k 才插一个,差值要是还大就继续插,够了就往下看。这样如果差值为 d d d 的话,我们需要插 ⌈ d k ⌉ − 1 = ⌊ d − 1 k ⌋ \left\lceil\dfrac{d}{k}\right\rceil-1=\left\lfloor\dfrac{d-1}{k}\right\rfloor kd1=kd1个数。

不过如果中间不存在一个差值大于等于 k k k 的位置的话,我们就需要人为造一个差值为 k k k 的相邻的数。比如在最后补一个数,使得它和前面那个数差值为 k k k。所以我们需要看一下中间有没有一个差值大于等于 k k k 的位置。

code:

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

int n,k,a[maxn];
long long cnt=0;

int main(){
	cin>>n>>k;
	for(int i=1;i<=n;i++)cin>>a[i];
	
	bool flag=false;
	for(int i=1,d;i<n;i++){
		d=abs(a[i]-a[i+1]);
		if(d>=k){
			cnt+=(d-1)/k;
			flag=true;
		}
	}
	if(!flag)cnt++;
	cout<<cnt<<endl;
	return 0;
}

E 小苯的等比数列

思路:

没想到被一个 1799 1799 1799 rate 的比赛的 E E E 题单防了。

这个题的思路其实很简单:就是枚举每一个数,让它作为等比数列的首项,并枚举公比,暴力进行验证即可。

当然需要一点点优化,因为每个数的值域很小,所以我们直接可以把每个数桶排序一下,把它扔进桶里,这样查找一个数的时间复杂度就可以降为 O ( 1 ) O(1) O(1) 的。不过公比为 1 1 1 的情况会不停验证下去,所以需要特判解决,我们之前用桶可以直接记录每个数的个数,就可以解决掉公比为 1 1 1 的情况。这时候再暴力就可以做到接近 O ( n ∗ l o g   n ) O(n*log\,n) O(nlogn) 的逆天时间复杂度。

为啥时间复杂度这么低?枚举每个数当然是 O ( n ) O(n) O(n) 的,而对每个公比 d d d,我们最多只需要验证到 n d \dfrac{n}{d} dn 个公比就可以直接返回了(因为后面的数,数列第二项就会直接超出 2 ∗ 1 0 5 2*10^5 2105),而每个公比只需要验证 l o g d ( 2 ∗ 1 0 5 ) log_d(2*10^5) logd(2105) 次就会超出 2 ∗ 1 0 5 2*10^5 2105 的值域范围。可以写个程序打个表:(第一行是公比 d d d,第二行是执行次数 l o g d ( 2 ∗ 1 0 5 ) log_d(2*10^5) logd(2105)
在这里插入图片描述
其实平均每个公比也就跑了 2.00278 次。所以总的时间复杂度大概就是 O ( n 2 + n 3 + ⋯ + n n ≈ n ∗ l o g   n ) O(\dfrac n2+\dfrac n3+\dots+\dfrac nn\approx n*log\,n) O(2n+3n++nnnlogn),然后带上一点常数,所以这样暴力跑是不会超时的。逆天。

code:

#include <iostream>
#include <cstdio> 
#include <vector>
#include <set>
using namespace std;
typedef long long ll;
const int maxn=2e5+5;

int n,vis[maxn],ans;

int main(){
	cin>>n;
	for(int i=1,t;i<=n;i++){
		cin>>t;
		vis[t]++;
	}
	for(ll a0=1;a0<=2e5;a0++){
		if(!vis[a0])continue;
		ans=max(ans,vis[a0]);
		for(ll d=2;d<=2e5;d++){
			if(a0*d>2e5)break;
			int cnt=1;
			for(ll a=a0*d;;a*=d){
				if(a>2e5 || !vis[a]){
					ans=max(ans,cnt);
					break;
				}
				else cnt++;
			}
		}
	}
	cout<<ans<<endl;
	
	return 0;
}

F 小苯的回文询问

思路1(莫队):

发现是离线的区间询问,可以尝试使用莫队!借鉴这位大佬的

考虑到是子序列长度 > 2 >2 >2 的回文串。所以我们只要保证有 有两个相同的数中间隔了一些数 这种情况出现就行。但是想用莫队就需要通过一些参数来描述上面这种情况,并且 [ l , r ] [l,r] [l,r] 区间的参数可以 O ( 1 ) O(1) O(1) 拓展到相邻的 [ l − 1 , r ] , [ l + 1 , r ] , [ l , r − 1 ] , [ l , r + 1 ] [l-1,r],[l+1,r],[l,r-1],[l,r+1] [l1,r],[l+1,r],[l,r1],[l,r+1] 区间。

两个相同的数中间隔了一些数的话,相当于需要两个相同的数不相邻,或者有三个及以上的相同的数。所以我们用 f 1 f1 f1 变量记录三个及以上的相同的数的个数,用 f 2 f2 f2 变量记录两个相同的数的个数。

如果 f 1 > 0 f1>0 f1>0 那么一定是有回文串的。如果 f 1 = = 0 f1==0 f1==0,这时候我们需要保证 两个相同的数 至少有一对不相邻,所以我们可以设置一个变量 s u m sum sum,记录相邻的相同的数有几对,如果 f 2 ! = s u m f2 !=sum f2!=sum,就说明至少有一对相同的数不相邻,这时也有回文串。

不过每个数的值域太大了,如果使用 m a p map map,时间会多出一个 l o g log log,总的时间复杂度为 O ( n ∗ l o g   n + q n ∗ l o g   n ) O(n*log\,n+q\sqrt n*log\,n) O(nlogn+qn logn),会超时。所以我们对每个数离散化预处理一下,这样时间复杂度就变成了 O ( n ∗ l o g   n + q n ) O(n*log\,n+q\sqrt n) O(nlogn+qn ),可以通过。

code1:

上面那个大佬写的更加优美。

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

int n,q;

int len,tot;
int a[maxn],L[maxn],R[maxn],belong[maxn];

void init(){
	len=log2(n);
	tot=(n+len-1)/len;
	for(int i=1;i<=tot;i++){
		L[i]=R[i-1]+1;
		R[i]=i*len;
	}
	R[tot]=n;
	for(int i=1;i<=tot;i++)
		for(int j=L[i];j<=R[i];j++)
			belong[j]=i;
}

struct itv{
	int l,r,id;
	bool ans;
}qy[maxn<<1];

bool cmp1(const itv a,const itv b){return (belong[a.l]^belong[b.l])?a.l<b.l:((belong[a.l]&1)?a.r<b.r:a.r>b.r);}
bool cmp2(const itv a,const itv b){return a.id<b.id;}

int f1,f2,sum;
//map<int,int> mp;
int mp[maxn];
void add(int x){
	mp[x]++;
	if(mp[x]==2)f1++;
	else if(mp[x]==3)f2++;
}
void del(int x){
	if(mp[x]==2)f1--;
	else if(mp[x]==3)f2--;
	mp[x]--;
}

vector<int> ttt;
int find(int x){
	return lower_bound(ttt.begin(),ttt.end(),x)-ttt.begin()+1;
}

int main(){
	cin>>n>>q;
	init();
	for(int i=1;i<=n;i++)cin>>a[i],ttt.push_back(a[i]);
	sort(ttt.begin(),ttt.end());
	ttt.erase(unique(ttt.begin(),ttt.end()),ttt.end());
	for(int i=1;i<=n;i++)a[i]=find(a[i]);
//	for(int i=1;i<=n;i++)cout<<a[i]<<" \n"[i==n];
	
	for(int i=1;i<=q;i++)cin>>qy[i].l>>qy[i].r,qy[i].id=i;
	sort(qy+1,qy+q+1,cmp1);
	
	int tl=1,tr=0,cnt=0;
	for(int i=1,l,r,t;i<=q;i++){
		l=qy[i].l;
		r=qy[i].r;
		while(tr<r){
			add(a[++tr]);
			if(a[tr]==a[tr-1])sum++;
		}
		while(tr>r){
			if(a[tr]==a[tr-1])sum--;
			del(a[tr--]);
		}
		while(tl<l){
			if(a[tl]==a[tl+1])sum--;
			del(a[tl++]);
		}
		while(tl>l){
			add(a[--tl]);
			if(a[tl]==a[tl+1])sum++;
		}
		
		if(f2 || (f1 && sum!=f1))qy[i].ans=true;
	}
	
	sort(qy+1,qy+q+1,cmp2);
	for(int i=1;i<=q;i++)
		puts((qy[i].ans)?"YES":"NO");
	return 0;
}

思路2:

同样的,我们想要两个相同的数中间隔了一些数的话,这时就是有回文子序列的。那么我们可以处理出每个位置的数的前一个不相邻的相同数的位置,假设是 l s t [ i ] lst[i] lst[i]。这样,对一个区间,我们只要保证 [ l , r ] [l,r] [l,r] 区间里有一个 l s t [ i ] ≥ l lst[i]\ge l lst[i]l 即可。

因为只要存在一个这样的 l s t [ i ] lst[i] lst[i] 就行,所以我们查询 l s t lst lst 的区间最大值,只要这个最大值满足条件就行了。而区间离线询问,可以跑线段树,ST表啊之类的都可以。

不过发现一个性质,就是 l s t [ i ] < i lst[i]<i lst[i]<i。所以在 i < l i<l i<l 的位置,都满足 l s t [ i ] < l lst[i]<l lst[i]<l,这意味着 l s t [ i ] > l lst[i]>l lst[i]>l 的情况一定发生在 i ∈ [ l , r ] i\in [l,r] i[l,r] 的区间内,查询最大值的时候加入前缀不会影响结果大于等于l。因此我们直接处理出 l s t lst lst 的前缀 m a x max max,然后查询是否满足 l s t [ r ] ≥ l lst[r]\ge l lst[r]l 就行了。

code2:

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

int n,q;
int a[maxn],lst[maxn];
map<int,int> mp;

int main(){
	cin>>n>>q;
	for(int i=1;i<=n;i++){
		cin>>a[i];
		lst[i]=mp[a[i]];
		mp[a[i]]=i;
	}
	for(int i=n;i>=1;i--)
		if(lst[i]==i-1)
			lst[i]=lst[lst[i]];
	for(int i=2;i<=n;i++)
		lst[i]=max(lst[i-1],lst[i]);
	for(int i=1,l,r;i<=q;i++){
		cin>>l>>r;
		puts((lst[r]>=l)?"YES":"NO");
	}
	return 0;
}

G 小红的区间删除

思路:

区间删除很难想,不如先看看单点删除。一个数放在某个位置上可以产生多少逆序对,就说明被删掉后会失去多少逆序对。而在某个位置插入一个数的话,它会和这个位置之前的每个比它大的数产生一对逆序对,以及和这个位置之后的每个比它小的数产生一对逆序对。

因此我们删掉一个点的剩余逆序对对数,相当于总的逆序对个数要减去 序列中在这个位置之前的比它大的数的个数和这个位置之后的比它小的数的个数。而区间删除就是一个点一个点地删掉。

我们要删掉中间一段连续的区间,然后剩余的部分的逆序对个数 ≥ k \ge k k,所以我们可以通过上面的单点修改一个一个删掉中间的部分,得到剩余部分的逆序对个数。

因为我们删掉的数越多,逆序对数就越少,它是单调的。所以我们只要对每个右端点 r r r 找到满足条件的最小的 l l l,那么中间所有的位置都可以作为满足条件的左端点。而也因为是单调的,我们可以使用双指针来移动左右端点。这样移动指针的复杂度就降为了 O ( n ) O(n) O(n),移动一次的复杂度为 O ( l o g   n ) O(log\,n) O(logn),可以通过。

不过因为什么都不删也是一种方法,所以我们需要特判一下,如果什么都不删都不满足条件,就直接输出 0 0 0。否则答案额外加上这个特殊情况。

值域给的很贴心,不用离散化了。

code:

#include <iostream>
#include <cstdio>
using namespace std;
typedef long long ll;
const int maxn=2e5+5;
const int maxf=1e6+5;

int n;
ll k;
int a[maxn];

ll cnt=0,ans;//剩余部分逆序对个数 答案 
struct tree_array{
	const int _n=1e6;
	int tr[maxf];
	
	int lowbit(int x){return x&-x;}
	void add(int idx,int x){
		for(int i=idx;i<=_n;i+=lowbit(i))
			tr[i]+=x;
	}
	int query(int idx){
		int ans=0;
		for(int i=idx;i;i-=lowbit(i))
			ans+=tr[i];
		return ans;
	}
	int qpre(int idx){//<idx的数个数 
		return query(idx-1);
	}
	int qsuf(int idx){//>idx的数个数 
		return query(_n)-query(idx);
	}
}t1,t2;



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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值