2024年6月赛丙组


本文总阅读量

丙组6月卷涉及了:枚举、位运算、简单数论、数学或宽搜、单调队列优化。
按CCF考纲来看,已经超出入门组范畴,属于入门组+或提高组-难度。

T1 布置会场(二)

可以用选择结构解决,但是漏情况会错。
数据并不大,不妨用枚举遍历所有情况来寻找答案。
一束花用c朵,不妨枚举束的数量,剩余的单买。

#include<bits/stdc++.h>  
using namespace std;  
int n,a,b,c,ans=2e9;   
int main()  
{  
	ios::sync_with_stdio(0);  
	cin.tie(0);  
	cout.tie(0);  
	cin>>n>>a>>b>>c;  
	for(int i = 0; i <= n/c+1; i++){  
		int m = n - i*c;  
		int tmp = i*b;  
		if(m>0) tmp += m*a;  
		ans = min(ans,tmp);  
	}  
	cout<<ans;  
	return 0;  
}  
  

为什么枚举的范围是n/c+1,而不是n/c?这是因为有可能剩余的买一束比单买便宜,题目也没说刚好凑到n朵,只要价格便宜就行(多出来的可以扔……)。

T2 位运算

还没接触过位运算的会无从下手。
来看下需要满足的三个条件:
n&m0
nm0
nm0
题目要求找出最小的正整数m,第一个条件,只要找出n最后一个二进制位1,这样数字足够小,也满足条件。
第二个条件,用第一个条件找出来的m符合要求。因为1|1=1
第三个条件是异或,异或是当二进制位不同的时候结果为1
共有两种情况:

#include<bits/stdc++.h>  
using namespace std;  
int n,m;  
int main()  
{  
	ios::sync_with_stdio(0);  
	cin.tie(0);  
	cout.tie(0);  
	cin>>n;  
	m = n & -n;  
	if(__builtin_popcount(n)>1) cout<<m;  
	else{  
		if(m==1) cout<<3;  
		else cout<<(m|1)<<endl;  //或者输出m+1
	}  
	return 0;  
}  
  

学过树状数组的知道n&-n可以算出n最后一个二进制1,例如5,二进制(101)2,那么5&5=(001)2
__builtin_popcount()能够快速计算出二进制1的个数,会位运算的也可以手动计算二进制位1的个数(移位判断)

T3 完全平方数对

分解质因数+思维
应该是这份试题中最优价值的一题。
题意:两个因数相乘,因数不超过n的前提下,乘积为完全平方数的数对有多少个(两个不同的因数交换位置属于不同的数对)。
对整数i(1in),符合要求乘积最大的完全平方数数对为(i,i),现在要计算(i,j)是平方数的数对有几个(1ji)。
如果直接枚举,n最大为105,显然会超时。
完全平方数的中不同的质因数必须偶数个才能形成完全平方数。
例如223=12,质数3只有一个,如果在配一个3就能形成完全平方数:2233=36
根据这个思路,我们可以先对i分解质因数,然后质因数如果是奇数个,则补上一个质因数,求出需要补的因数jj由若干个需要补的质因数相乘得到),这样可以找出符合要求最小完全平方数的数对(i,j)
那么i的其他数对如何找呢?
175为例:557=175
显然最小的j=7
最大的数对为(i,i)=(175,175)
也就是557755是符合要求的最大数对。
从因数角度入手,只要因数成双的,它们的乘积必然是完全平方数。

完全平方数 i j 补的因数 最终算式 数对
1225 557 7 11 557711 (175,7)
4900 557 7 22 557722 (175,28)
11025 557 7 33 557733 (175,63)
19600 557 7 44 557744 (175,112)
30625 557 7 55 557755 (175,175)

通过上面的例子可以发现规律,找到最小的j后,和i能形成符合要求的数对为:j11j22j33j44j55
那么在整数i=175中,去掉配对的因数7后,剩下的为55,那么之后补因数的时候是不能超过55的,显然符合要求的数对数量为sqrt(5)2,因为交换位置算一种方案,所以要cheng乘2,但是(i,i)只能算一种方案,所以最后方案数要减去n,一共有n个两个数相同的数对:(1,1)(2,2)(3,3)...(n,n)

#include<bits/stdc++.h>  
using namespace std;  
typedef long long ll;  
ll n,ans;  
void divid(int x)  
{  
	int tx = x, res = 1;  //res用来求最小的数对(i,res)  
	for(int i = 2; i <= sqrt(x); i++){  
		int cnt = 0;       //cnt用来统计相同质因数个数  
		while(x % i==0) x /= i,cnt++;  
		if(cnt % 2==1) res *= i;  
	}  
	if(x > 1) res *= x;  
	ans += int(sqrt(tx / res)) * 2;  //最终符合要求的数对  
}  
int main()  
{  
	ios::sync_with_stdio(0);  
	cin.tie(0);  
	cout.tie(0);  
	cin>>n;  
	for(int i = 1; i <= n; i++) divid(i);  
	cout<<ans - n<<endl;  //扣除n对(i,i)  
	return 0;  
}  
  

T4 超级奇数

数学规律或宽搜
这里讲一下宽搜做法。
超级奇数只有1、3、5、7、9组成,最大为9位数,所以超级奇数一共有:5+52+53+54+55+56+57+58+59=2441405
可以发现并不多,所以直接用宽搜扩展寻找即可。

#include<bits/stdc++.h>  
using namespace std;  
typedef long long ll;  
ll n,ans,b[]={1,3,5,7,9},cnt;  
queue<ll> q;   
int main()  
{  
	ios::sync_with_stdio(0);  
	cin.tie(0);  
	cout.tie(0);  
	cin>>n;  
	for(int i = 0; i < 5; i++) q.push(b[i]);  
	while(q.size()){  
		ll x = q.front();  
		q.pop();  
		cnt++;  
		if(x == n){  
			cout<<cnt<<endl;  
			return 0;  
		}  
		for(int i = 0; i < 5; i++){  
			ll t = x * 10 + b[i];  
			q.push(t);  
		}  
	}  
	return 0;  
}  
  

T5 环岛旅行

单调队列优化
这题是一本通旅行问题的简化版。
环形问题,先破环成链,即用2倍长度的线性序列代替环形。
例如,假设n=5,环形序列为1,2,3,4,52倍长度序列为1,2,3,4,5,1,2,3,4,5
序列编号为110,则求从编号1的点顺时针出发能否回到编号1的点,就是求从编号1的点出发能否到达编号6的点。
能否到达取决于油量是否够,所以途中的任意一个加油站,存储的油量必须大于等于消耗的油量,才能到达下一个加油站。
p[i]表示第i个加油站可以加的油量,d[i]表示从第i个点走到第i+1个点消耗的油量,则p[i]d[i]就表示从第i个点到第i+1个点油量的增减情况。
考虑顺时针情况,s[i]=s[i1]+p[i]d[i]表示剩余油量的前缀和,也就是说s[i]表示刚走到第i+1个点还未加油时汽车的剩余油量,如果油是负数,则说明无法到达。
那么在区间[i,n+i]n个加油站,每个站点的剩余油量都不能为负数,否则无法到达。所以会想到枚举这些区间和,如果油负数,则无法到达,但是这样时间复杂度很高。其实我们的目标只要找最小的前缀和,找到最小的前缀和不就可以计算最小的区间和吗?那么在一个区间中寻找最小值,就可以使用单调队列优化
破环成链和前缀和

	for(int i = 1; i <= n; i++){  
		cin>>p[i]>>c[i];  
		p[n+i] = p[i];  
		c[n+i] = c[i];  
	}  
	for(int i = 1; i < 2 * n; i++) sum[i] = sum[i-1] + p[i] - c[i];  

单调队列维护

	int head = 0, tail = 0;  
	for(int i = 1; i <= 2 * n; i++){  
		while(head <= tail && i - q[head] >= n) head++;  
		while(head <= tail && sum[q[tail]] >= sum[i]) tail--;  
		q[++tail] = i;  
		if(i >= n && sum[q[head]] - sum[i - n] >= 0){ //i为1的时候表示走向2,所以i为n的时候表示走向1  
			cout<<i - n + 1;  
			return 0;  
		}   
	}  
	cout<<0<<endl;  //没有符合要求的起点  

因为是顺时针,所以当i=n的时候,表示回到原点。需要注意的是,第i=n的时候,表示走回加油站1
sum[q[head]]sum[in]计算的是区间和,如果最小的区间和大于等于0,那么其他区间和必然大于0,就不会出现半路没油的情况,也就能返回起点。


本站总访问量