10月3日补题报告

算法竞赛补题总结

题目分析

第一题-数字对应

题目描述

给定一个长度为 n 的序列 A,给序列 A 中每个数字找出一个对应的正整数,构成一个长度为 n 的序列 B。

序列 B 中数字不能在序列 A 中出现过,并且序列 A 中第 i 个正整数与 序列 B 中的第 i 个正整数对应。对应关系可以随意指定,但是必须唯一。例如序列 A 中的数字 3 对应序列 B 中的数字 4,那么序列 A 中所有的 3 都必须对应序列 B 中的 4,并且序列 A 中其余数字不能再对应序列B中的 4。

请你输出字典序最小的序列 B。

思考过程

一开始我进行了暴力枚举,尽量将前面的数字对应成小的数字。即假设当前数字没有对应的数字,那么就从小到大寻找第一 个未出现过的数字,对应到当前数字上,但是这种方法不仅效率低,而且还容易时间超限。
所以我们首先引入bool类型的vis数组来标记数字是否被使用
(表示每一个数字是否出现过),因为随后,使用 map 进行映射,最后与原集合一一对应,输出我们已经映射好的值就行.

期望得分

期望得分60,实际得分40,出现偏差的原因为:

对于做题的方法掌握得不够透彻,尤其是没有想到map,并且暴力方法虽然好打,但代价就是时间复杂度过高(时间复杂度是O(n²),甚至更高),所以没有拿到预计得分和全分

完整代码

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll N=1e5+20;
map<ll,ll>vis,mp;
ll n,cnt=1,a[2*N];
int main()
{
	//freopen("digit.in","r",stdin);
	//freopen("digit.out","w",stdout);
	scanf("%lld",&n);
	for(ll i=1;i<=n;i++)
	{
		scanf("%lld",a+i);
		vis[a[i]]=true;
	}
	for(ll i=1;i<=n;i++)
	{
		if(mp[a[i]]==0)
		{
			while(vis[cnt]) cnt++;
			vis[cnt]=1,mp[a[i]]=cnt;
		}
		printf("%lld ",mp[a[i]]);
	}
	return 0;
}

第二题-技能学习

题目描述

有 n 个同学正在学习新技能,技能的学习与小可老师给的学习资料数量有关系。

已知小可老师有 m 份学习资料,并且小可老师可以将学习资料随意分给每位同学。但是某位同学如果学习资料数量不足 k 份,将掌握不了新技能。

同学学习新技能与学习资料的数量成正比,比如一位同学拿到了 p 份学习资料,那么每单位时间(分钟)会增长 p 点技能点,但是同学技能点最多到 Q,就表示已经完全学会了此技能,并且后续时间内也不会再继续学习,技能点也不再增长。

现在总共有 t 分钟,小可老师希望同学们的技能点数之和最多,请问技能点最多是多少?

注意:小可老师会在第0分钟时,将所有学习资料发放完毕(学习资料必须按照整份发放),之后不能再调整。

思考过程

首先,我考虑的是可以枚举挨个发放学习资料,暴力求出答案,结果因为时间超限卡掉了一半的分。
其实我们可以贪心考虑一下,应该尽可能多得让同学提升技能点,如果将学习资料全部发放给少数几位同学,那么也很容易到达上限。而学习资料发放的越分散,就越不容易到达上限。所以我们将学习资料尽可能平均分给能提升技能点的同学.特判时,尤其要注意m<k这种情况,否则会影响后面答案的求解.

期望得分

期望得分70,实际得分40,出现偏差的原因为:

1.没有考虑到m<k的情况,可能会导致丢掉一部分分.

2.计算ans的方法不仅繁琐,而且占用了大量的时间和空间,导致一部分数据时间超限.

完整代码

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll N=1e5+20;
ll n,m,k,q,p,t,x,pos,cnt,ans,num[N];
int main()
{
	//freopen("skill.in","r",stdin);
	//freopen("skill.out","w",stdout);
	scanf("%lld%lld%lld%lld%lld",&n,&m,&k,&q,&t);
	if(m<k)
	{
		printf("0");
		return 0;
	}
	else if(n*k>m) n=m/k;
	x=m/n;
	ans=min(x*t,q)*(n-m%n);
	ans+=min((x+1)*t,q)*(m%n);
	printf("%lld",ans);
	return 0;
}

第三题-等于

题目描述

给定一个长度为 n 的序列,并且序列中每个元素属于 -2,-1,1,2中的一个。

请问多少个 子数组 满足最大值的绝对值等于最小值的绝对值。

思考过程

我一开始的想法就是打暴力去尽可能多的拿分,虽然最后的成果可能有一些不尽人意.

区间最大值和最小值的绝对值相等时,存在两种情形:

第一种情形:区间内所有元素相同。这种情况较为简单,通过遍历数组并统计连续相同元素即可处理。

第二种情形:区间最大值与最小值为相反数。具体表现为两种情况:

  1. 最大值为1,最小值为-1
  2. 最大值为2,最小值为-2

针对第二种情形的求解方法如下: 当固定左端点时,寻找合法右端点的规则:

  1. 对于最大值为1、最小值为-1的情况:

    • 右端点需满足:区间包含1和-1,但不含2和-2
    • 可通过维护左端点右侧1、-1、2、-2首次出现的位置来确定
    • 右端点起始位置取1和-1首次出现位置的最大值
    • 结束位置取2和-2首次出现位置的最小值
  2. 对于最大值为2、最小值为-2的情况:类似上述方法处理,需保证区间包含2和-2.该算法的时间复杂度为O(n)。

期望得分

期望得分40,实际得分10,出现偏差的原因为:

1.思考的方式有所偏差,只有全局思维,应该可以先尝试拿到部分分,在考虑所有数据.

2.对于题目的理解有出入(很大出入,比如没有看见最小值这件事),导致丢掉了许多本来拿到的分.

完整代码

注:第一组是40分的暴力枚举.

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll N=1e5+20;
ll n,ans,len=1,a[N];
int main()
{
	//freopen("equal.in","r",stdin);
	//freopen("equal.out","w",stdout);
	scanf("%d",&n);
	for(ll i=1;i<=n;i++) scanf("%d",a+i);
	a[n+1]=3;
	for(ll i=1;i<=n;i++)
	{
		if(abs(a[i])!=abs(a[i+1]))
			ans+=(len+len*(len-1)/2),len=1;
		else len++;
	}
	printf("%lld",ans);
	return 0;
}

注:下面附80分代码(我也不知道哪错了)

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll N=1e5+20,INF=0x3f3f3f3f;
ll n,ans,pos,r,len=1,a[N],p1[N],p2[N];
int main()
{
	//freopen("equal.in","r",stdin);
	//freopen("equal.out","w",stdout);
	scanf("%lld",&n);
	for(ll i=1;i<=n;i++)
	{
	    scanf("%lld",a+i);
	    if(a[i]==-1) a[i]=1;
	}
	a[n+1]=3;
	for(ll i=1;i<=n;i++)
	{
		if(a[i]!=a[i+1]) ans+=(len+len*(len-1)/2),len=1;
		else len++;
	}
	pos=INF;
	for(ll i=n;i>=1;i--)
	{
	    if(a[i]==2) pos=i;
	    p1[i]=pos;
	}
	pos=INF;
	for(ll i=n;i>=1;i--)
	{
	    if(a[i]==-2) pos=i;
	    p2[i]=pos;
	}
	for(ll l=1;l<=n;l++)
	{
	    r=max(p1[l],p2[l]);
	    if(r==INF) break;
	    ans+=(n-r+1);
	}
	printf("%lld",ans);
	return 0;
}

注:AC代码摘抄于题目解析-STD3:

#include <bits/stdc++.h>
#define ll long long
using namespace std;
const int maxn=5e5+10;const int inf=0x3f3f3f3f;
ll n,ans,num[maxn];
int nxt[maxn][5];
int pos1,pos2;
int main() {
	ios::sync_with_stdio(false);
	cin.tie(0);
	cout.tie(0);
	cin>>n;
	memset(nxt,0x3f,sizeof nxt);
	for (int i=1;i<=n;++i) cin>>num[i];
	ll ret=1,lst=num[1];
	for (int i=2;i<=n;++i)
	{
		if (num[i]==lst) ++ret;
		else ans+=ret*(ret+1)/2,ret=1,lst=num[i];
	}
	ans+=ret*(ret+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][num[i]+2]=i;
			int maxpos1=nxt[i][1+2], maxpos2=nxt[i][2+2];
			int minpos1=nxt[i][-1+2],minpos2=nxt[i][-2+2];
			pos1=max(maxpos2,minpos2);
			pos2=n+1;
			if (pos1!=inf&&pos1<pos2) ans+=pos2-pos1;
			pos1=max(maxpos1,minpos1),pos2=min(min(maxpos2,minpos2),(int)n+1);
			if (pos1!=inf&&pos1<pos2) ans+=pos2-pos1;
	}
	cout<<ans<<'\n';
	return 0;
}

第四题-最小方差

题目描述

给定一颗无根树 T。

已知 T 包含 n 个点,n−1 条边,且边权全部为 1,请在 T 中寻找一个树根 root,当树根确定后计算出树上每个点到 root 的距离,得到一个长度为 n 的序列 a。请让序列 a 的方差最小。

为了方便输出,输出的方差值乘以 n​2​​。

提示:设序列 a 的平均值为 x,那么乘以 n​2​​ 后的方差为 n×∑i=1n(ai−x)2

思考过程

注:另一种更方便的方差公式:n × ∑ ( a² ) - ( ∑ a )².

注:思路摘抄于题目解析-STD4:


30%数据下:可以直接暴力枚举每个点作为根的情况,然后dfs出每个点到根的距离,计算方差。另外的20%数据下:为一条链表的情况,那么根设置在中间的位置是优的。
100%数据下:考虑换根DP
首先考虑方差的计算式,假设选择点 为根,各点距离为 ,则此时有
说明我们需要维护各距离的平方和,以及各距离的和。假设 1 为整棵树的根,考虑怎么向上计算出结果。
令 sum1[u] 表示以 为根的子树上各点到 的距离和, sum2[u] 表示以 为根的子树上各点到 的距离平方和。假设已有儿子 的答案,考虑如何转移到父亲 上:显然,儿子上所有店到父亲的距离会在 原来的基础上加一。因此发现需要维护子树上的点数量,记为 sz[u]
此时可以写出转移式:
这样我们就可以算出以 为根时整棵树的方差,这也是第一次 dfs 做的事。接下来考虑如果换成其它点作为根,答案会有怎么样的变化。
某一个节点为根时,方差的答案有两个来源:一个是该结点所在的子树的贡献(这里的子树指的是假设为根时的子树),还有一个来源是当前点上方的祖先结点对的贡献。(祖先也是指 为根时这个点的祖先)
前者我们已经在第一次 dfs 中求得。我们来考虑第二个问题如何计算。首先,将子树的影响从父节点中去掉:
这个点的子树对父亲结点的贡献为:1. 对 sz 的贡献:
2. 对 sum1 的贡献:
3. 对 sum2 的贡献:
不妨将它们记为 szu,ret1,ret2
从根向下传时,
然后从根出发进行 dfs,向下的过程中记录维护 , 的变化,来获得之后的点作为根时整棵树的方差。

期望得分

期望得分0,实际得分0,主要是对于这一块的知识点掌握不熟练

完整代码

注:AC代码摘抄于题目解析-STD4:

#include <bits/stdc++.h>
#define ll unsigned long long
using namespace std;
const int maxn=1e5+10;
ll sum2[maxn],sum1[maxn],sz[maxn],n,res;
vector<int> G[maxn];
void dfs1(int u,int f)
{
	for (int i=0;i<G[u].size();i++)
	{
		int v=G[u][i];
		if (v==f) continue;
		dfs1(v,u);
		sz[u]+=sz[v],sum1[u]+=sum1[v],sum2[u]+=sum2[v];
	}
	sum2[u]+=sz[u]+2*sum1[u],sum1[u]+=sz[u],sz[u]+=1;
	return;
}
void dfs2(int u,int f,ll s1,ll s2)
{
	res = min(res,n*(s2+sum2[u])-(sum1[u]+s1)*(sum1[u]+s1));
	for (int i =0;i<G[u].size();i++)
	{
		int v=G[u][i];
		if (v==f) continue;
		ll ret1=sum1[u]-(sum1[v]+sz[v])+s1,ret2=sum2[u]-(sum2[v]+2*sum1[v]+sz[v])+s2,szu=n-sz[v];
		dfs2(v,u,ret1+szu,ret2+2*ret1+szu);
	}
	return;
}
int main() {
	int t;
	cin>>t;
	while(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)
		{
			int u,v;
			cin>>u>>v;
			G[u].push_back(v);
			G[v].push_back(u);
		}
		res=LONG_LONG_MAX;
		dfs1(1,0);
		dfs2(1,0,0,0);
		cout<<res<<endl;
	}
}

比赛分析

做题顺序与时间分配

这次对于时间的规划和做题熟练度于昨天有一定的进步,前两题分配到的时间算是比较合理,对于后两道题所分配到的时间也相对来说比较合理.可能对于第四题来说,45分钟的时间比较短,但是也是足够用的(因为第四题根本不会),所以对于时间分配这一块,考试中的把握已经趋于成熟.

考试心态

大家应该培养在考试中更强的抗压能力用以在考试中发挥正常.——马老师

这次考试的发挥和得分相较于昨天有了显著的进步(虽然还是丢了很多不该丢的分),保持住这种做题状态,争取在考试中能有更好的发挥!

### 题目描述 CCF CSP 2017年9月17补考的第一题是“打酱油”。 小明带着 **N元** 钱去买酱油。酱油 **10元一瓶**。商家进行促销活动,促销规则如下: - 每买 **3瓶送1瓶** - 或者每买 **5瓶送2瓶** 任务是计算小明最多可以得到多少瓶酱油。 输入保证 **N是10的整数倍**,且 **N不超过300**。 --- ### 输入输出格式 - **输入格式**:一个整数N,表示小明用于买酱油的钱数。 - **输出格式**:一个整数,表示小明最多可以得到的酱油瓶数。 --- ### 解题思路 由于每瓶酱油价格为10元,所以小明最多能买 `N // 10` 瓶酱油。然后根据促销规则计算送的瓶数。 促销规则是: - 每买 **3瓶送1瓶**:即每购买3瓶可以获得4瓶。 - 每买 **5瓶送2瓶**:即每购买5瓶可以获得7瓶。 为了最大化获得的酱油瓶数,应优先使用“买5送2”的优惠,因为它的单位收益更高(每5瓶获得7瓶,相当于每瓶的“送”量更高),然后再使用“买3送1”的优惠。 --- ### 算法实现 以下是该题目的Python实现代码: ```python def max_soy_sauce_bottles(n): total_bottles = n // 10 # 初始购买的瓶数 # 使用“买5送2”的优惠 five_sets = total_bottles // 5 total_bottles += five_sets * 2 # 剩下的部分使用“买3送1”的优惠 remaining = total_bottles - five_sets * 5 three_sets = remaining // 3 total_bottles += three_sets return total_bottles ``` #### 示例运行 - 输入:`40` - 初始购买:4瓶 - 可以使用“买3送1”:3瓶送1瓶 - 总共:4 + 1 = 5瓶 - 输出:`5` - 输入:`80` - 初始购买:8瓶 - 使用“买5送2”:5瓶送2瓶,剩余3瓶 - 剩余3瓶使用“买3送1” - 总共:8 + 2 + 1 = 11瓶 - 输出:`11` --- ### 复杂度分析 由于输入金额 **N** 最大为300元,最多只能买30瓶酱油,因此算法的时间复杂度为 **O(1)**,无需考虑优化。 --- ### 总结 本题是一个典型的贪心问题,通过优先选择优惠力度更大的策略,可以最大化获得的酱油瓶数。由于数据规模较小,直接模拟即可解决问题。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值