第二次训练题解

第二次训练题解

第二次相比第一次难度有一点的提升加入了一些数据结构的内容

A 兔子的逆序对

知识点归并排序、数学知识

首先我们采用归并排序求出逆序对的数量,这个看了大家写的代码大家都会求就不多讲了这里放出我写的归并求逆序对的代码可以供大家参考下

大家特别要注意对于逆序对的数量记得开long long

ll merge_sort(int l,int r) {
	if(l==r) return 0;
	int mid=l+r>>1;
	ll ans=merge_sort(l,mid)+merge_sort(mid+1,r);
	int k=0,i=l,j=mid+1;
	while(i<=mid&&j<=r) {
		if(q[i]<=q[j]) tmp[k++]=q[i++];
		else {
			tmp[k++]=q[j++];
			ans+=mid-i+1;
		}
	}
	while(i<=mid) tmp[k++]=q[i++];
	while(j<=r) tmp[k++]=q[j++];
	for(int i=l,j=0;i<=r;i++,j++){
		q[i]=tmp[j];
	}
	return ans;
}

接下来就是这个题的关键之处

对于一个序列我们有个常识可知:正序对的数量加上逆序对的数量会等于任意两个数(下标i>j)组成序对的数量

并且区间内的数字反转并不会影响序列总的逆序对数量因为相对位置并没有改变。

就比如 1、3 、2

任意两两组合可以得到的数量为n*(n-1)/2

也就是3*2/2=3

正序对为(1,3),(1,2)

逆序对为(3,2)

于是我们就先设一个序列中总的逆序对数量为ans

需要修改区间中的逆序对数量为x

修改区间中总的序对数目为(r-l+1)(r-l)/2

修改区间中的正序对数量为cnt

于是有等式:x+cnt=(r-l+1)*(r-l)/2

并且修改区间外的逆序对数量:ans-x

当修改过后区间中的正序对变成逆序对

所以整个序列中的逆序对数量为ans-x+cnt

于是化简可以得到最后序列逆序数为

ans-x+(r-l+1) * (r-l)/2-x=ans-2*x+(r-l+1) * (r-l)/2

通过这个式子可知2x不影响奇偶可以直接去掉那么影响奇偶的就只有ans和(r-l+1)*(r-l)/2了也就是总共区间中的逆序对数量和区间内的总序对数量。

std

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn=2e5+10;
int n,m,k,l,r,x,t,y;
int a[maxn],sum[maxn],q[maxn],tmp[maxn];
ll merge_sort(int l,int r) {
	if(l==r) return 0;
	int mid=l+r>>1;
	ll ans=merge_sort(l,mid)+merge_sort(mid+1,r);
	int k=0,i=l,j=mid+1;
	while(i<=mid&&j<=r) {
		if(q[i]<=q[j]) tmp[k++]=q[i++];
		else {
			tmp[k++]=q[j++];
			ans+=mid-i+1;
		}
	}
	while(i<=mid) tmp[k++]=q[i++];
	while(j<=r) tmp[k++]=q[j++];
	for(int i=l,j=0;i<=r;i++,j++){
		q[i]=tmp[j];
	}
	return ans;
}

int main() {
	//1 4 2 3
	int flag=1;
	n=read();
	for(int i=0;i<n;i++){
		q[i]=read();
	}
	t=read();
	int ans=merge_sort(0,n-1);
	if(ans%2==0){
		flag=0;
	}
	while(t--){
		l=read();
		r=read();
		int pp=(r-l+1)*(r-l)>>1;
		if(pp%2!=0) flag^=1;
		if(flag==0){
			printf("like\n");
		}
		else{
			printf("dislike\n");
		}
	}
	
}

B、C都是模拟水题就不讲了


D 完全平方数

题意就是要我们找出区间中可以开平方的数有多少个

于是那我们就对区间两个端点就行开根号处理

因为左右端点开完根号后形成的区间中的数平方一定能在原区间中找到

但是要注意的是因为sqrt是会自动四舍五入的所以如果左端点开的实际根号数会大于整数,就比如5开根号是2.几左右会自动四舍五入为2这样区间就会多算一个1个数所以我们需要判定一下然后减一。对于右端点是不影响的

std

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn=2e5+10;
const int INF=0x3f3f3f;
//int n,m,k,l,r,x,t,y;
inline int read() {
	int X=0;
	bool flag=1;
	char ch=getchar();
	while(ch<'0'||ch>'9') {
		if(ch=='-') flag=0;
		ch=getchar();
	}
	while(ch>='0'&&ch<='9') {
		X=(X<<1)+(X<<3)+ch-'0';
		ch=getchar();
	}
	if(flag) return X;
	return ~(X-1);
}//快读
inline void write(int X) {
	if(X<0) {
		X=~(X-1);
		putchar('-');
	}
	if(X>9) write(X/10);
	putchar(X%10+'0');
}//快输
//函数预定义区

int main() {
	//IOS;
	ll n,l,r;
	ll p,q;
	cin>>n;
	while(n--) {
		cin>>p>>q;
		ll i=sqrt(p);
		ll ans=0;
		if(i*i!=p){
			ans-=1;
		}
		ll j=sqrt(q);
		 ans+=j-i+1;
		cout<<ans<<endl;
	}
}

E 栈和排序

知识点:模拟、贪心

这题大家的通过率都还可以那我就简单讲下

这题就是想让输出来的数的字典序尽可能大,如果能完全从大到小输出来那当然最好

于是我们从数组后面开始统计后缀最大值,接着使用栈来判定我们的栈顶是否比后缀最大值更大如果更大则该出栈了,否则会让结果字典序变小

std

#include <bits/stdc++.h>
using namespace std;
const int N=1e6+2;
int n,a[N], maxn[N],sta[N];

int main()
{
    int top=0;
    scanf("%d",&n);
    int i;
    for(i=0;i<n;i++)
        scanf("%d",&a[i]);
    for(i=n-1;i;i--)
        maxn[i]=max(maxn[i+1],a[i]);
    for(int i=0;i<n;++i)
    {
        sta[++top]=a[i];
        while(top&&sta[top]>maxn[i+1])
        {
            printf("%d ",sta[top--]);
        }
    }
}

F wyh的物品

知识点:二分

这个题是经典的二分答案类型的题,我们通过猜答案来一步步逼近正确的答案并使正确的答案更符合题意

对于这个题我们直接二分单位价值然后让每个物品的总价值减去总重量*单位价值,最后排序选取前k个相加判断是否大于等于0

对于这个大于0就等于想知道这个单位价值我们是否能得到如果全都前k个加起来小于0说明这个单位价值我们取的太大了需要减小

若大于等于0则符合条件我们使左边的端点变为中点使答案更大

若小于0则不符合使右边的端点选取更小的单位价值

时间复杂度(nlog(n))

std

#include <bits/stdc++.h>
using namespace std;
const int maxn=10e5;
const int INF=1e9;
typedef long long ll;
int c[maxn],v[maxn];
double sum[maxn];
int t,n,m,k;
bool cmp(double a,double b)
{
	return a>b;
}
bool check(double avg)
{
	for(int i=0;i<n;i++)
	{
		sum[i]=v[i]-c[i]*avg;
	}
	sort(sum,sum+n,cmp);
	double sumv=0;
	for(int i=0;i<k;i++)
	{
		sumv+=sum[i];
	}
	return sumv>0;
 }
 int main()
 {
 	scanf("%d",&t);
 	while(t--)
 	{
 		scanf("%d %d",&n,&k);
 		for(int i=0;i<n;i++)
 		{
 			scanf("%d %d",&c[i],&v[i]);
		 }
		 double l=0,r=INF;
		 for(int i=0;i<100;i++)
		 {
		 	double mid=(l+r)/2;
		 	if(check(mid))
		 	{
		 		l=mid;
			 }
			 else
			 {
			 	r=mid;
			 }
		 }
		 printf("%.2lf\n",r);
	 }
  } 

G 图的遍历

知识点:图的建立、连通块、染色法、判环、dfs深搜,

这题应该算是这里面最难的了牵扯到的知识点也非常的多大家现阶段做不出来也很正常o( ̄▽ ̄)ブ。

关于图的定义我稍微提一下本质上就是一些点和边的集合,点通过边连接了起来。

边呢有无向边和有向边像这个题就是无向边,顾名思义就是两个点可以互通。

有向边就是有方向的只能一个点到另一个点,他们两总有一方不能通过这个边到达另一个点。

对于图的存储方式像这题不带边权所以我建议使用vector来存每个点与点之间的关系相当于邻接表来存储了因为这样比较好dfs。

解决完图的存储问题,要做这个题之前还得知道连通块的概念,如果有一部分点相互联通那么他们就是联通块,用题目中的样例来解释

5 4
1 2
2 3
3 4
4 5

5个点4条边

非联通

像这种的就不是连通块因为5跟1并不联通

但如果你将1跟4,5任意相连他就是个连通块了

接着来说下奇数环和偶数环

奇数环就是有奇数个点的环

偶数环同理

这题要我们每次只能走两步

所以这题就是要判断是否有奇数环,因为有奇数环我们可以通过这个环走到任何一个地方

举个栗子:

联通图

1可以通过这个奇数环将所有点遍历掉

于是这题就转变为求连通块数量和是否有奇数环的问题了😊

如果没有奇数环我们需要人工加一条,已经含有则不需要,对于n个联通块需要n-1条边

std

#include<bits/stdc++.h>
#include<algorithm>
#include<iostream>
#include<cstdio>
#include<cmath>
#include<cstring>
#define R __int128
using namespace std;
typedef long long ll;
typedef pair<int,int> PII;
const ll INF=0x7f7f7f7f;
const int maxn=1e6+10;
const ll mod=1e9+7;
const int N=9;
ll read() {
	ll f=0,x=0;
	char ch=getchar();
	while(!isdigit(ch)) {
		if(ch=='-') f=1;
		ch=getchar();
	}
	while(isdigit(ch)) {
		x=x*10+ch-'0';
		ch=getchar();
	}
	return !f?x:-x;
}
ll qpow(ll a,ll b,ll mod) {
	ll ans=1;
	while(b) {
		if(b&1) ans=ans*a%mod;
		a=a*a%mod;
		b>>=1;
	}
	return ans;
}
ll inv(ll x,ll mod) {
	return qpow(x,mod-2,mod);
}
int vis[maxn],s[maxn],flag=1;
ll n,t,k,m,d;
vector<int> g[maxn];
void dfs(int x){
	//染色法判断是否有奇环
	for(int i=0;i<g[x].size();i++){
		int j=g[x][i];
		if(vis[j]==0){
			//如果没有遍历过则染成与邻接点不同颜色
			s[j]=!s[x];
			vis[j]=1; //遍历过了
			dfs(j); 
		}
		else if(s[j]==s[x]) flag=0; //代表出现了奇环
	}
}
void solve(){
	int ans=0;
	cin>>n>>m;
	while(m--){
		int a,b;
		cin>>a>>b;
		g[a].push_back(b);
		g[b].push_back(a);
	}
	for(int i=1;i<=n;i++){
		if(vis[i]==0){ //判连通块
			vis[i]=1;
			ans++;
			dfs(i);
		}
	}
	cout<<ans+flag-1<<endl;
}
int main() {
    solve();
}
/*
 *  ┌───┐   ┌───┬───┬───┬───┐ ┌───┬───┬───┬───┐ ┌───┬───┬───┬───┐ ┌───┬───┬───┐
 *  │Esc│   │ F1│ F2│ F3│ F4│ │ F5│ F6│ F7│ F8│ │ F9│F10│F11│F12│ │P/S│S L│P/B│  ┌┐    ┌┐    ┌┐
 *  └───┘   └───┴───┴───┴───┘ └───┴───┴───┴───┘ └───┴───┴───┴───┘ └───┴───┴───┘  └┘    └┘    └┘
 *  ┌───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───────┐ ┌───┬───┬───┐ ┌───┬───┬───┬───┐
 *  │~ `│! 1│@ 2│# 3│$ 4│% 5│^ 6│& 7│* 8│( 9│) 0│_ -│+ =│ BacSp │ │Ins│Hom│PUp│ │N L│ / │ * │ - │
 *  ├───┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─────┤ ├───┼───┼───┤ ├───┼───┼───┼───┤
 *  │ Tab │ Q │ W │ E │ R │ T │ Y │ U │ I │ O │ P │{ [│} ]│ | \ │ │Del│End│PDn│ │ 7 │ 8 │ 9 │   │
 *  ├─────┴┬──┴┬──┴┬──┴┬──┴┬──┴┬──┴┬──┴┬──┴┬──┴┬──┴┬──┴┬──┴─────┤ └───┴───┴───┘ ├───┼───┼───┤ + │
 *  │ Caps │ A │ S │ D │ F │ G │ H │ J │ K │ L │: ;│" '│ Enter  │               │ 4 │ 5 │ 6 │   │
 *  ├──────┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴────────┤     ┌───┐     ├───┼───┼───┼───┤
 *  │ Shift  │ Z │ X │ C │ V │ B │ N │ M │< ,│> .│? /│  Shift   │     │ ↑ │     │ 1 │ 2 │ 3 │   │
 *  ├─────┬──┴─┬─┴──┬┴───┴───┴───┴───┴───┴──┬┴───┼───┴┬────┬────┤ ┌───┼───┼───┐ ├───┴───┼───┤ E││
 *  │ Ctrl│    │Alt │         Space         │ Alt│    │    │Ctrl│ │ ← │ ↓ │ → │ │   0   │ . │←─┘│
 *  └─────┴────┴────┴───────────────────────┴────┴────┴────┴────┘ └───┴───┴───┘ └───────┴───┴───┘
 */

H kmp模板题

这个大家看csdn的博客讲解可能比我讲的更为清晰这个放在这里只是让大家能有个印象代码可以参考我的

时间复杂度O(n+m)

std

#include <bits/stdc++.h>
using namespace std;
const int maxn=1e6+5;
int ne[maxn],n,m;
char s[maxn],p[maxn]; 
int main() {
//     ios::sync_with_stdio(false);
//     cin.tie(0); cout.tie(0);
// 	cin>>s+1;
    scanf("%s",s+1);
//	getchar();
// 	cin>>p+1;
    scanf("%s",p+1);
	int n=strlen(s+1);
	int m=strlen(p+1);
	for(int i=2,j=0;i<=m;i++){
		while(j&&p[i]!=p[j+1]) j=ne[j];
		if(p[j+1]==p[i]) j++;
		ne[i]=j;
	} 
	for(int i=1,j=0;i<=n;i++){
		while(j&&s[i]!=p[j+1]) j=ne[j];
		if(s[i]==p[j+1]) j++;
		if(j==m){
            printf("%d\n",i-j+1);
// 			cout<<i-j+1<<endl;
		}
	}
	for(int i=1;i<=m;i++){
// 		cout<<ne[i]<<" ";
        printf("%d ",ne[i]);
	}
}

I Big Water Problem

其实这题我没想到牛客的数据这么水能让O(nm)的复杂度过

让有些同学前缀和过掉了,实际上在这个数据范围内O(nm)使必不可能过的,但是这也是敢于尝试的奖励吧哈哈

这里题目想要考察的知识点使线段树的区间查询和单点修改

这里直接给出我的代码代码中都有注释

std

#include<iostream>
#include<cstring>
#include<cstdio>
#include<algorithm>

using namespace std;

const int N=100010;

int n,m;

int w[N];//记录一下权重

struct node{
    int l,r;//左右区间

    int sum;//总和
}tr[N*4];//记得开 4 倍空间

void push_up(int u)//利用它的两个儿子来算一下它的当前节点信息
{
    tr[u].sum=tr[u<<1].sum+tr[u<<1|1].sum;//左儿子 u<<1 ,右儿子 u<<1|1  
}

void build(int u,int l,int r)/*第一个参数,当前节点编号,第二个参数,左边界,第三个参数,右边界*/
{
    if(l==r)tr[u]={l,r,w[r]};//如果当前已经是叶节点了,那我们就直接赋值就可以了
    else//否则的话,说明当前区间长度至少是 2 对吧,那么我们需要把当前区间分为左右两个区间,那先要找边界点
    {
        tr[u]={l,r};//这里记得赋值一下左右边界的初值

        int mid=l+r>>1;//边界的话直接去计算一下 l + r 的下取整

        build(u<<1,l,mid);//先递归一下左儿子

        build(u<<1|1,mid+1,r);//然后递归一下右儿子

        push_up(u);//做完两个儿子之后的话呢 push_up 一遍u 啊,更新一下当前节点信息
    }
}

int query(int u,int l,int r)//查询的过程是从根结点开始往下找对应的一个区间
{
    if(l<=tr[u].l&&tr[u].r<=r)return tr[u].sum;//如果当前区间已经完全被包含了,那么我们直接返回它的值就可以了
    //否则的话我们需要去递归来算
    int mid=tr[u].l+tr[u].r>>1;//计算一下我们 当前 区间的中点是多少
    //先判断一下和左边有没有交集

    int sum=0;//用 sum 来表示一下我们的总和

    if(mid>=l)sum+=query(u<<1,l,r);//看一下我们当前区间的中点和左边有没有交集
    if(r>=mid+1)//看一下我们当前区间的中点和右边有没有交集
    sum+=query(u<<1|1,l,r);

    return sum;

}

void modify(int u,int x,int v)//第一个参数也就是当前节点的编号,第二个参数是要修改的位置,第三个参数是要修改的值
{
    if(tr[u].l==tr[u].r)tr[u].sum+=v; //如果当前已经是叶节点了,那我们就直接让他的总和加上 v 就可以了

    //否则
    else
    {

      int mid=tr[u].l+tr[u].r>>1;
      //看一下 x 是在左半边还是在右半边
      if(x<=mid)modify(u<<1,x,v);//如果是在左半边,那就找左儿子
      else modify(u<<1|1,x,v);//如果在右半边,那就找右儿子

      //更新完之后当前节点的信息就要发生变化对吧,那么我们就需要 pushup 一遍

      push_up(u);
    }

}

int main()
{
    scanf("%d%d",&n,&m);

    for(int i=1;i<=n;i++)scanf("%d",&w[i]);

    build(1,1,n);/*第一个参数是根节点的下标,根节点是一号点,然后初始区间是 1 到 n */

    //后面的话就是一些修改操作了

    while(m--)
    {
        int k,a,b;

        scanf("%d%d%d",&k,&a,&b);

        if(k==2)printf("%d\n",query(1,a,b));//求和的时候,也是传三个参数,第一个的话是根节点的编号 ,第二个的话是我们查询的区间 
        //第一个参数也就是当前节点的编号
        else
        modify(1,a,b);//第一个参数是根节点的下标,第二个参数是要修改的位置,第三个参数是要修改的值

    }


    return 0;
}


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
CSP-S2提高组是中国计算机学会(CCF)主办的一项全国性计算机竞赛,旨在挑选出优秀的高中生并为他们提供提高计算机科学和编程能力的平台。2021年第二轮题解分为以下几个部分。 第一题是关于石头游戏的思考。题目给出了一堆石头的数量,两位玩家轮流选择石头进行取走,每次可以取走1个或者2个石头,最后无法继续取走者输掉游戏。通过观察可以发现,如果一开始给定的石头数量是3的倍数,那么第一个选手必胜;否则,第一个选手无法必胜。这是因为无论第一个选手怎么选取,第二个选手总可以使得每一轮选取后的石头数量保持在3的倍数。因此,只需要判断起始时石头数量是否为3的倍数即可。 第二题是关于好书的购买。题目给出了若干种书的价格和折扣情况,要求在有限的预算下买到尽可能多的书籍。这是一个经典的背包问题。使用动态规划算法可以解决,按照价格从小到大的顺序遍历书籍,设置一个二维数组dp[i][j]表示在前i本书中,花费j的预算能够买到的最多书籍数量。状态转移方程为:dp[i][j]=max(dp[i-1][j], dp[i-1][j-price[i]]+1)。最终的结果即为dp[n][budget],其中n为书籍总数,budget为预算。 第三题是关于均匀生成所有正整数的问题。题目给出了一个区间[L, R],要求输出在该区间内存在的所有正整数。首先通过观察可以发现,对于任意的正整数x,若2x在区间[L, R]内,那么x也在该区间内;若2x+1在区间[L, R]内,那么x也在该区间内。基于这个思路,可以使用递归的方式实现。若L<=R,则输出L,然后递归输出从2*L到R的所有整数。若L>R,则结束递归。 以上就是CSP-S2提高组2021第二轮题解的简要概述。希望这些解题思路对参与竞赛的同学有所帮助!

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值