牛客小白月赛87(A,B,C,D,E,F,G)

文章讲述了在一系列编程题目中,涉及贪心算法、数组操作、排序技巧、逆序对计算等技术,如石子游戏中的策略、区间选择问题、IDE操作模拟及数组构造等,通过代码示例展示了如何解决这些问题。
摘要由CSDN通过智能技术生成

冷知识,“小白”月赛的Rating还是蛮高的。比赛链接

A-F主要是推结论,找到结论之后就简单了,不难。G题很难,做法精妙。


A 小苯的石子游戏

思路:

两人肯定贪心的选取当前最大的石子堆,对石子堆的大小从大到小排序,那么Alice肯定会拿到第 1 , 3 , 5 , … , 2 k − 1 1,3,5,\dots,2k-1 1,3,5,,2k1 堆石子堆,Bob肯定会拿到第 2 , 4 , 6 , … , 2 k 2,4,6,\dots,2k 2,4,6,,2k 堆石子堆,累计一下然后比较就行了。

code:

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

int T,n;
int a[maxn];

int main(){
	cin>>T;
	while(T--){
		cin>>n;
		int cnt1=0,cnt2=0;
		for(int i=1;i<=n;i++)
			cin>>a[i];
		for(int i=n;i>=1;i-=2)
			cnt1+=a[i];
		for(int i=n-1;i>=1;i-=2)
			cnt2+=a[i];
		puts((cnt1>cnt2)?"Alice":"Bob");
	}
	return 0;
}

B 小苯的排序疑惑

思路:

首先这个区间不能全选,其次最多选一次。既然不管怎么选都只算作一次操作,那么我们肯定选 [ 2 , n ] [2,n] [2,n] 或者 [ 1 , n − 1 ] [1,n-1] [1,n1] 这两者之一。

如果要保证选出区间排序后整段区间都有序,如果我们选的 [ 2 , n ] [2,n] [2,n] ,那么第一个元素就必须本来就在正确的位置上,同理 [ 1 , n − 1 ] [1,n-1] [1,n1]

所以我们看一下第一个元素是不是最小的,或者最后一个元素是不是最大的,两者满足一个就是YES

code:

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

int T,n;
int a[maxn];

int main(){
	cin>>T;
	while(T--){
		cin>>n;
		int maxx=-inf,minn=inf;
		for(int i=1;i<=n;i++){
			cin>>a[i];
			maxx=max(maxx,a[i]);
			minn=min(minn,a[i]);
		}
		if(a[1]==minn || a[n]==maxx)puts("YES");
		else puts("NO");
	}
	return 0;
}

C,D 小苯的IDE括号问题

思路:

只涉及添加删除一个元素,以及光标向前后移动一位,显然链表题。光标 I 可以看作是链表上的一个特殊指针。模拟一下操作过程,然后顺序输出链表内容就可以了。没什么思维难度,就是难写。

code:

这里链表头节点默认是 0 0 0 号节点,维护的是双向循环链表,每次在末尾插入元素相当于向头节点之前插入元素。it 指针表示光标i

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

int n,k;
int pre[maxn],nxt[maxn],val[maxn],cnt,it,p;
string str;

void del(int p){//删除p节点 
	nxt[pre[p]]=nxt[p];
	pre[nxt[p]]=pre[p];
}

int main(){
	cin>>n>>k>>str;
	for(int i=0;i<n;i++){
		if(str[i]=='(' || str[i]==')'){
			val[++cnt]=(str[i]==')');
			pre[cnt]=pre[0];
			nxt[cnt]=nxt[pre[0]];
			nxt[pre[0]]=cnt;
			pre[0]=cnt;
		}
		else it=pre[0];
	}
	
	for(int i=1;i<=k;i++){
		cin>>str;
		if(str=="backspace"){
			if(it){
				p=pre[it];
				if(val[it]==0 && nxt[it] && val[nxt[it]]==1){
					del(it);
					it=nxt[it];
				}
				del(it);
				it=p;
			}
		}
		else if(str=="delete"){
			if(nxt[it]){
				p=nxt[it];
				del(p);
			}
		}
		else if(str=="<-"){
			if(it)it=pre[it];
		}
		else if(str=="->"){
			if(nxt[it])it=nxt[it];
		}
	}
	if(it==0)printf("I");
	for(int p=nxt[0];p;p=nxt[p]){
		printf("%c",(val[p])?')':'(');
		if(p==it)printf("I");
	}
	return 0;
}

E 小苯的数组构造

思路:

如果我们取出数组 a a a 的最大元素,把所有元素都变成它,最多加 2 ∗ 1 0 9 2*10^9 2109,不会超过数组 b b b 的数据范围,所以这个题是一定有解的。

考虑怎么把一对逆序对变成非逆的。只给大数减,给大数减小数加 其实和 只给小数加 得到的极差是一样的,所以我们可以只考虑给小数加到和大数相同,这个时候数组 b b b 就一定会有大数-小数的差值存在,极差至少会是这个值。

那么问题就变成了找到差值最大的逆序对,它的差值其实就是数组 b b b 的极差。而第 i i i 个元素如果小于前 i − 1 i-1 i1 个最大的那个元素,就变成了那个元素, b b b 数组就是增加值。跑一遍就可以得到 b b b 数组了。

code:

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

int n;

int main(){
	cin>>n;
	int mx=-inf;
	for(int i=1,t;i<=n;i++){
		cin>>t;
		if(t>=mx){
			mx=t;
			printf("0 ");
		}
		else printf("%d ",mx-t);
	}
	return 0;
}

F 小苯的数组切分

思路:

乍一看没什么思路,但是手玩一下会发现一个规律。那就是最后一段一定只包含最后一个元素

因为与是越与越小的,而或是越或越大的。所以你与其给与,不如给旁边的或。我们把最后一个元素直接给与,然后枚举前面两个区间的断点,记录最大值。区间异或的结果可以用前缀和处理,区间或的结果可以用后缀和处理,最后枚举断点记录最大值。就做完了。

code:

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

ll n,a[maxn],ans;
ll suf[maxn],pre[maxn];//后缀或,前缀异或 

int main(){
	cin>>n;
	for(int i=1;i<=n;i++)
		cin>>a[i];
	n--;
	
	pre[1]=a[1]; 
	suf[n]=a[n];
	for(int i=2;i<=n;i++)
		pre[i]=pre[i-1]^a[i];
	for(int i=n-1;i>=1;i--)
		suf[i]=suf[i+1]|a[i];
	
	for(int i=1;i<n;i++)
		ans=max(ans,pre[i]+suf[i+1]);
	
	cout<<ans+a[n+1]<<endl;
	return 0;
}

G 小苯的逆序对

思路:

这题好啊,官方视频讲解

我们假设 d p [ d ] dp[d] dp[d] 表示 g c d ( a i , a j ) = d gcd(a_i,a_j)=d gcd(ai,aj)=d 的逆序对 ( i , j ) (i,j) (i,j) 对的对数。直接求不好求,但是我们大概好求 g c d ( a i , a j ) = k d gcd(a_i,a_j)=kd gcd(ai,aj)=kd 的逆序对 ( i , j ) (i,j) (i,j) 对的对数。我们把所有 d d d 的倍数的 a i a_i ai 都提取出来,单独求逆序对,得到的就是既是逆序对,又都至少包含因子 d d d 的逆序对 ( i , j ) (i,j) (i,j) 对的对数。算出来的就是 g c d ( a i , a j ) = k d gcd(a_i,a_j)=kd gcd(ai,aj)=kd 的逆序对 ( i , j ) (i,j) (i,j) 对的对数。

考虑怎么把 g c d ( a i , a j ) = k d gcd(a_i,a_j)=kd gcd(ai,aj)=kd 的逆序对 ( i , j ) (i,j) (i,j) 对的对数 转化成 g c d ( a i , a j ) = d gcd(a_i,a_j)=d gcd(ai,aj)=d 的逆序对 ( i , j ) (i,j) (i,j) 对的对数。实际上假设 d ∼ n d\sim n dn d p dp dp 值我们都已经算出来了,那么其实
[ g c d ( a i , a j ) = d ] 的逆序对对数 = [ g c d ( a i , a j ) = k d ] 的逆序对对数 − ∑ k > 1 k d ≤ n d p [ k d ] [gcd(a_i,a_j)=d] 的逆序对对数 \\= [gcd(a_i,a_j)=kd] 的逆序对对数 - \sum_{k>1}^{kd\le n}dp[kd] [gcd(ai,aj)=d]的逆序对对数=[gcd(ai,aj)=kd]的逆序对对数k>1kdndp[kd]
就是答案了。含义就是把等于 k d kd kd 里的 2 d , 3 d , 4 d , … 2d,3d,4d,\dots 2d,3d,4d, 都减掉,剩下的不就是 d d d 的么。

时间复杂度计算:我们从 n n n 1 1 1 枚举 d d d,去计算 g c d ( a i , a j ) = k d gcd(a_i,a_j)=kd gcd(ai,aj)=kd 的逆序对 ( i , j ) (i,j) (i,j) 对的对数。对一个 d d d,数组中一共有 n / d n/d n/d 个元素,这 n / d n/d n/d 个元素使用树状数组计算逆序对的时间复杂度是 O ( ⌊ n d ⌋ ∗ l o g ( ⌊ n d ⌋ ) ) O(\left\lfloor\dfrac n d \right\rfloor*log(\left\lfloor\dfrac n d \right\rfloor)) O(dnlog(dn)) ,算所有 d d d g c d ( a i , a j ) = k d gcd(a_i,a_j)=kd gcd(ai,aj)=kd 的逆序对对数的运算次数是 ∑ d = 1 n ⌊ n d ⌋ ∗ l o g ( ⌊ n d ⌋ ) ≤ ∑ d = 1 n ⌊ n d ⌋ ∗ l o g   n ≈ n ∗ l o g   n   ∗ ∑ d = 1 n 1 d = n ∗ l o g   n   ∗ l o g   n   \sum_{d=1}^n\left\lfloor\dfrac n d \right\rfloor*log(\left\lfloor\dfrac n d \right\rfloor)\le \sum_{d=1}^n\left\lfloor\dfrac n d \right\rfloor*log\,n\approx n*log\,n\ * \sum_{d=1}^n\dfrac 1d=n*log\,n\ *log\,n\ d=1ndnlog(dn)d=1ndnlognnlogn d=1nd1=nlogn logn 

∑ k > 1 k d ≤ n d p [ k d ] \sum_{k>1}^{kd\le n}dp[kd] k>1kdndp[kd] 的枚举次数是 ⌊ n d ⌋ − 1 \left\lfloor\dfrac n d \right\rfloor-1 dn1 ,总的枚举次数是 ∑ d = 1 n ( ⌊ n d ⌋ − 1 ) ≈ n ∗ ∑ d = 1 n 1 d − n = n ∗ l o g   n   − n \sum_{d=1}^n(\left\lfloor\dfrac n d \right\rfloor-1)\approx n*\sum_{d=1}^n \dfrac 1 d -n=n*log\,n\ -n d=1n(dn1)nd=1nd1n=nlogn n。综上,总的时间复杂度是 O ( n l o g 2 n ) O(nlog^2n) O(nlog2n)

code:

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

int n,a[maxn],pos[maxn];

struct tree_arry{
	int n,tr[maxn];
	int lowbit(int x){return x&-x;};
	void add(int x,int y){
		for(int i=x;i<=n;i+=lowbit(i))
			tr[i]+=y;
	}
	int qry(int x){
		int ans=0;
		for(int i=x;i;i-=lowbit(i))
			ans+=tr[i];
		return ans;
	}
	int qry(int x,int y){
		return qry(y)-qry(x-1);
	}
	void print(){
		for(int i=1;i<=n;i++)
			printf("%d ",tr[i]);
		puts("");
	}
	
	ll f(int d){//求gcd(ai,aj)=kd的逆序对 
		vector<pair<int,int> > t;
		for(int k=d;k<=n;k+=d)
			t.push_back(make_pair(pos[k],k));
		sort(t.begin(),t.end());
		
		ll cnt=0;
		for(auto x:t){
			add(x.second,1);
			cnt+=qry(x.second+1,n);
		}
		for(auto x:t){
			add(x.second,-1);
		}
		return cnt;
	}
}ta;
ll dp[maxn];

int main(){
	cin>>n;
	ta.n=n;
	for(int i=1;i<=n;i++){
		cin>>a[i];
		pos[a[i]]=i;
	}
	
	for(int i=n;i>=1;i--){
		dp[i]=ta.f(i);
		for(int k=2*i;k<=n;k+=i)
			dp[i]-=dp[k];
	}
	
	cout<<dp[1]<<endl;
	
	return 0;
} 
  • 23
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值