2023CSP-J复赛冲刺模拟赛三赛后补题报告


1.比赛概况:  

          比赛共4题,110(240)/400,T1~T4:0/100/0/10(100/100/30/0)

          气死我了,题目名称写错了,坑了我130分。n~ha~ha~a~~~~~


2.比赛过程:

        T1是一道简单的用桶的题,但数据过大要用map.(花50min却死于文件名)

        T2是一道数学题,我花了40min来推,终于写完了(这几天第一个AC)

        T3以为是个DP(500000的DP?),就按DP来写,发现不行。就开始花式骗分,几乎骗了60%,但因为某些原因暴零了。

        T4本以为很简单,没想到很难。只能打暴力+骗分。(因为时间不够还没调出来)


3.题解报告:


                                    【T1:人员借调】


1.    情况:  0分,已补题(其实能AC)

2.    题意:有一个长度为n的序列A,构成一个长度为n的序列B。序列B中数字不能在序列A中出现过,并且序列A中第i个正整数与序列B中的第i个正整数一一对应。求字典序最小的序列B。

3.   数据范围:

        对于20%数据:1\leqslant n,ai\leqslant 1000

        对于40%数据:1\leqslant n\leqslant 10^5,1\leqslant ai\leqslant 1000

        对于60%数据:1\leqslant n,ai\leqslant 10^5

        对于另外的20%数据:11\leqslant n\leqslant 1000,1\leqslant ai\leqslant 10^9

        对于100%数据:1\leqslant n\leqslant 10^5,1\leqslant ai\leqslant 10^9

4.    赛时本题做题想法/题解:用桶,但数据过大要用map。先吧每个数放桶里,再遍历桶,找n个里面没有的数。再根据A依次输出。

5.     AC代码:

#include<iostream>
#include<cstdio>
#include<map>
using namespace std;
int n,a[100005],j=1;
map<int,int> mp,m;
int main(){
	scanf("%d",&n);
	for(int i=1;i<=n;i++){
		scanf("%d",&a[i]);
		if(mp[a[i]]==0){
			mp[a[i]]=1;
		}
	}
	for(int i=1;i<=n;i++){
		if(m[a[i]]==0){
			while(mp[j]!=0) j++;
			m[a[i]]=j++;
			cout<<m[a[i]]<<" ";
		}
		else cout<<m[a[i]]<<" ";
	}
	return 0;
}

                                        【T2:技能学习】


1.    情况: 100分
2.    题意:有n个同学有m份学习资料。学习资料可以随意分给每位同学。但是某位同学如果学习资料数量不足k份,将掌握不了新技能。某位同学拿到了p份学习资料,每分钟会增长p点技能点,最多到Q。总共有t分钟,求技能点之和最多是多少?注意:第0分钟将所有学习资料发放完(按照整份发放),不再调整。

3.    数据范围:      

        在 30%数据下:1\leqslant n\leqslant 10,1\leqslant m\leqslant 100

        在70%数据下:1\leqslant n,m,k,Q,t\leqslant10^5

        在 100%数据下:1\leqslant n,m,k,Q,t \leqslant10^9,k\leqslant m

4.    赛时本题做题想法/题解:一道数学题,就要用数学来解决。(阳寿算法:O(1))

        分类讨论:如果n位同学分m分资料不够分,就舍弃n-n/m位同学。剩下的同学分两类,一类有x人每人p份,另一类是n-x人p+1份。x=min(n,m%n),p=(m/n);

5.    AC代码:

        这样,加上一些压行,一份很有逼格的代码就出来了。(比正解还有逼格!)

#include<iostream>
#include<cstdio>
using namespace std;
long long n,m,k,q,t;
int main(){
	cin>>n>>m>>k>>q>>t;
	if(n*k>m) n=m/k; 
	if(k*t>=q) cout<<q*n;
	else cout<<(n-min(n,m%n))*min((m/n)*t,q)+min(n,m%n)*min((m/n+1)*t,q);
	return 0;
}


                                        【T3:等于】

1.    情况: 0分,已补题。

2.    题意:一个长度为n的序列,序列中每个元素属于 -2,-1,1,2 中的一个。求多少个子数组满足最大值的绝对值等于最小值的绝对值。

3.    数据范围:

        在 20%数据下:n \leqslant 5000

        另有10%数据:n\leqslant50000

        另有10%数据:所有数字的绝对值相等,1\leqslant n\leqslant50000

        另有10%数据:所有数字均为正整数,1\leqslant n\leqslant 100000

        另有20%数据:不存在数字 -2,1\leqslant n\leqslant 100000

        另有10%数据:1\leqslant n\leqslant 100000

        在100%数据下:1\leqslant n\leqslant 500000

4.    赛时本题做题想法:以为是个DP(500000的DP?),就按DP来写,发现不行。就开始花式骗分,几乎骗了60%,但因为某些原因暴零了。(请欣赏花式骗分代码)

#include<iostream>
#include<cstdio>
#include<cmath>
#include<cstring>
using namespace std;
int n,a[500005],mx[500005][35],mn[500005][35],po[30],ans=0,p2=1,p3=1,p4=1;
int main(){
	scanf("%d",&n);
	for(int i=1;i<=n;i++){
		scanf("%d",&a[i]);
		if(i!=1&&abs(a[i])!=abs(a[i-1])) p2=0;
		if(a[i]<0) p3=0;
		if(a[i]==-2) p4=0;
	}
	memset(mx,-0x3f,sizeof(mx));
	memset(mn,0x3f,sizeof(mn));
	for(int i=1;i<=n;i++){
		mx[i][1]=mn[i][1]=a[i];
	}
	for(int i=1;i<30;i++) po[i]=(po[i-1]<<1);
	for(int j=1;po[j]<=n;j++){
		for(int i=1;i+j<=n;i++){
			mx[i][j]=max(mx[i][j-1],mx[i+po[j-1]][j-1]);
			mn[i][j]=min(mn[i][j-1],mn[i+po[j-1]][j-1]);
		}
	}
	if(n<=10000){
		for(int i=1;i<=n;i++){
			for(int j=i;j<=n;j++){
				int k=log2(j-i+1);
				if(abs(max(mx[i][k],mx[j-po[k]+1][k]))==abs(min(mn[i][k],mn[j-po[k]+1][k]))) ans++;
			}
		}
		cout<<ans+n;
	}
	else if(p2) cout<<(1+n)*n/2;
	else if(p3){
		int i=1;
		while(i<=n){
			int p=1;
			while(a[i]==a[i+1]){
				i++;
				p++;
			}
			i++;
			ans+=(p+1)*p/2;
		}
		cout<<ans;
	}
	else if(p4){
		int i=1;
		while(i<=n){
			int p=1;
			while(a[i]==a[i+1]||a[i]==-a[i+1]){
				i++;
				p++;
			}
			i++;
			ans+=(p+1)*p/2;
		}
		cout<<ans;
	}
	else{
		cout<<(1+n)*n/4;
	} 
	return 0;
}

5.    题解: 分类讨论。分为1.全一样的和2.全不一样的。全一样的用for+求和公式就行。不一样的又分(1).最大值1最小值 -1,和(2)最大值2,最小值-2 。考虑固定左端点时,如何找合法的右端点。对于(1)右端点需要保证:区间中有1和-1,区间中没有2和-2。用一个nxt数组来标记,nxt[i][j]表示i~n中出现j的最小的下标。对于(2)右端点需要保证:区间中有2和-2,同理。转移方程为nxt[i][j]=nxt[i+1][j]; nxt[i][a[i]+2]=i;(加2是因为最小是-2)

6.    AC代码:

#include<bits/stdc++.h>
using namespace std;
int n,a[500005],nxt[500005][5];
long long ans=0,s,e;
int main(){
	scanf("%d",&n);
	memset(nxt,0x3f,sizeof(nxt));
	for(int i=1;i<=n;i++) scanf("%d",&a[i]);
	long long p=a[1],cnt=1;
	for(int i=2;i<=n;i++){
		if(a[i]==p) cnt++;
		else{
			ans+=cnt*(cnt+1)/2;
			cnt=1;
			p=a[i];
		}
	}
	ans+=cnt*(cnt+1)/2;
	for(int i=n;i>=1;i--){
		for(int j=0;j<=4;j++) nxt[i][j]=nxt[i+1][j];
		nxt[i][a[i]+2]=i;
		int mx1=nxt[i][3],mx2=nxt[i][4],mn1=nxt[i][1],mn2=nxt[i][0];
		s=max(mx2,mn2);
		e=n+1;
		if(s!=0x3f3f3f3f&&s<e) ans+=e-s;
		s=max(mx1,mn1);
		e=min(min(mx2,mn2),n+1);
		if(s!=0x3f3f3f3f&&s<e) ans+=e-s;
	}
	cout<<ans;
	return 0;
}


                                        【T4:最小方差】

1.    情况:10分,已补题

2.    题意:给定一颗无根树n个点n-1条边,边权=1 。在n个点中寻找一个树根 ,确定后计算出树上每个点到根的距离,得到一个长度为n的序列 。求序列最小的方差。
注意:输出的方差值乘以n^2 。提示:设序列n的平均值为x,那么乘以n^2后的方差为n*\sum_{i=1}^{n}(a_{i}-x)^2。(t组数据)

3.    数据范围:

        对于10%数据:1\leqslant n\leqslant 100

        另有20%数据:1\leqslant n\leqslant 1000

        另有20%数据:给定的数据为一条链

        对于 100%数据:1\leqslant n\leqslant 40000,1\leqslant t\leqslant 5

4.     赛时本题做题想法:一看是个图论,立马精神了。一看题,又懵了。不甘心的打了个暴力,又开始骗分(可惜时间不够了,就40min了),代码还没调出来。

5.    题解:这题是个换根DP(这东西能出在J组?)设p为根,则n^2*d=n*\sum(dis_{i}^{2})-(\sum dis_{i})^2说明我们需要维护各距离的平方和,以及各距离的和。状态转移时需要维护子树上的点数量。某一个节点为根时,方差的答案有两个来源:一个是该结点所在的子树的贡献,还有一个来源是当前点上方的祖先结点对的贡献。

6.    AC代码:

这题已超出本人极限,只能贴个正解了。

#include <bits/stdc++.h>
using namespace std;
#define ll unsigned long long
const int maxn = 1e5 + 10;
//sum1表示以i为根的子树上的各点到i的距离和 
//sum2表示以i为根的子树上的各点到i的距离平方和
//sz表示以i为根的子树的节点个数 
ll sum2[maxn], sum1[maxn], sz[maxn], n, res;
vector<int> G[maxn];//存树的vector 
void dfs1(int u, int f) {//u是当前搜的点,子树的根,f是其父节点 
	//如果已知了孩子的sum1,那么转移到其父亲的sum1中时,所有孩子的孩子的距离+1 
	for (int i=0;i<G[u].size();i++) {//遍历u的孩子 
		int v=G[u][i];
		if (v == f) continue;//双向存储,如果当前遍历到其父亲则跳过 
		dfs1(v, u);//继续以v为起点深搜,u是其父亲 
		sz[u] += sz[v];//u为根的树的结点个数+其孩子v为根的结点个数 
		sum1[u] += sum1[v];//距离和转移 
		sum2[u] += sum2[v];//距离平方和转移 
	}
	//公式转换 u是v的父亲,
	//如果v的距离平方和是(x1)^2+(x2)^2+(x3)^2
	//这些点到u的距离统一+1,则平方和变化(x1+1)^2+(x2+1)^2+(x3+1)^2
	//拆:x1^2+2*x1+1  +x2^2+2*x2+1  +x3^2+2*x3+1
	//即 x1^2+x2^2+x3^2 +2(x1+x2+x3) +3
	//即 sum2[v] +2*sum1[v]+子节点个数 
	sum2[u] += sz[u] + 2 * sum1[u];//25行和26行顺序不能颠倒:因为25行中用的sum1是没有额外距离的sum1 
	sum1[u] += sz[u];//距离和中,所有子树转移时,每个结点到根u的距离应该+1 
	sz[u] += 1;//以u为根的树节点个数+1,算上了u本身 
	return;
}
void dfs2(int u, int f, ll s1, ll s2) {
	//u作为根,f是其父亲
	//s1是u这个子树对其父亲距离和的贡献
	//s2是u这个子树对其父亲的距离平方和的贡献
	//以u为根的公式 序列方差*n方公式:n*距离平方和-距离和的平方 
	res = min(res, n * (s2 + sum2[u]) - (sum1[u] + s1) * (sum1[u] + s1));
	for (int i=0;i<G[u].size();i++) {//遍历u的孩子 
		int v=G[u][i];//遍历u的邻接点 
		if (v == f) continue;//v恰好是父亲,越过 
		//其父节点的那一半的贡献,总量减去v子树的贡献+父亲的父亲的贡献 
		ll ret1 = sum1[u] - (sum1[v] + sz[v]) + s1;//和版本 
		ll ret2 = sum2[u] - (sum2[v] + 2 * sum1[v] + sz[v]) + s2;//平方和版本 
		ll szu = n - sz[v];//其父节点那边的子树的节点个数 
		//v作为孩子,u作为父亲
		//其父亲给其和贡献s1+每个点距离+1
		//其父亲给其平方和贡献 父亲那边子树的平方和+2倍的和+节点个数 
		dfs2(v, u, ret1 + szu, ret2 + 2 * ret1 + szu);
	}
	return;
}
int main() {
	int t;
	cin >> t;
	while (t--) {//t组数据 
		cin >> n;
		for (int i = 1; i <= n; i++) {//先初始化,所有点清空 
			G[i].clear();
			sum1[i] = sum2[i] = sz[i] = 0;
		}
		for (int i = 1; i <= n - 1; ++i) {//n-1条边 
			int u, v;
			cin >> u >> v;//u和v之间有一条边 
			G[u].push_back(v);//u的邻接点加入一个v 
			G[v].push_back(u);//v的邻接点加入一个u 
		}
		res = LONG_LONG_MAX;//res初始化极大值 
		//一开始认为1是初始根,其没有父亲 
		dfs1(1, 0);//1作为初始根,其父亲是0,进行第一轮深搜 
		//1作为初始根,其父亲是0,
		//s1是其父亲对应的子树(另一半)给他的距离和的贡献值
		//s2是其父亲对应的子树(另一半)给他的距离平方和的贡献值 
		dfs2(1, 0, 0, 0); 
		cout << res << endl;
	}
}


4. 赛后总结:

        今天的得分很可惜,丢了130分。以后要注意文件操作,(一个字母100分),合理安排时间。(实在离谱的题就弃了吧)。

        (听说明天题很简单)


                                                 日期:2023年10月03日星期二
                                                              姓名:董峻熙

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值