蓝桥杯 试题 算法训练 礼物 C++ 详解

题目:

JiaoShou在爱琳大陆的旅行完毕,即将回家,为了纪念这次旅行,他决定带回一些礼物给好朋友。
  在走出了怪物森林以后,JiaoShou看到了排成一排的N个石子。
  这些石子很漂亮,JiaoShou决定以此为礼物。
  但是这N个石子被施加了一种特殊的魔法。
  如果要取走石子,必须按照以下的规则去取。
  每次必须取连续的2*K个石子,并且满足前K个石子的重量和小于等于S,后K个石子的重量和小于等于S。
  由于时间紧迫,Jiaoshou只能取一次。
  现在JiaoShou找到了聪明的你,问他最多可以带走多少个石子。

输入格式

  第一行两个整数N、S。
  第二行N个整数,用空格隔开,表示每个石子的重量。

输出格式

  第一行输出一个数表示JiaoShou最多能取走多少个石子。

样列输入

  8 3
  1 1 1 1 1 1 1 1

样列输出

  6

数据规模和约定

  对于20%的数据:N<=1000
  对于70%的数据:N<=100,000
  对于100%的数据:N<=1000,000,S<=10^12,每个石子的重量小于等于10^9,且非负


前言:

还是一样,网上的方法(二分查找),又是不怎么解释过程。但是数据量太大,没空去慢慢测试数据,干脆自己想一个方法,也方便详细讲解解题过程。


开始:(已明确题意)

(也不知道是积累的做题经验,还是灵光一现,我想到:)

既然是要满足:前 K 个 <= S , 后 K 个也 <= S . 那不就类似分区嘛。按照S进行分区,找出两个相邻(或者部分重叠)的两个区间,其长度之和即为最多可拿的石子数量(其实这句话不太对,后面会慢慢改进的)

还是我最喜欢的举例(例子):7 4 1 0 8 5 2 9 6 3,那么显然对它分区的结果为:(图解)

分区方式:7 对应 1 代表 若从 7 开始选,至多选 1 个(满足 <= S) 7 <= 10

                  对应 3 代表 若从开始选,至多选 3 个(满足 <= S) 4 + 1 + 0 <= 10

                  以此类推……

【分区数组所存放的是:对应原始数组(相同下标),从当前位置往后选,不超过 S 的情况下,最多可以选多少个石子】

得到所有的分区情况,那就开始遍历,找最长相邻区间即可。

1.1 先挑最简单的情况:0开始选,分区结果为2,意味着前S能选2个、选0之后,"去0到的后2位",到5,分区结果为2,意味着(后S)能选2个。综合得:从0开始选,前KK不超过S情况下,可以最多选 2 + 2 = 4个。(满足:前KK不超过S,前K == 后K)

【看到这里,停下来,理解上面的操作:(关键)"去0到的后2位",之后再往下看,这只是第一步】

然后遍历过程不断更新最大值(当然奇数的话,要-1为偶数)……

咔咔一顿敲,提交上去:正确率10%,用时1.0XXs(超时)。

超时的原因,时间复杂度:输入O(N),分区O(N^2),遍历O(N)。综合:O(N^2)这在蓝桥杯这样的比赛显然是无法忍受的,效率太低了,于是我又优化了一下,可以参考代码对比一下:

	//输入操作 + 优化前的分区操作
    for (int i = 1; i <= N; i++) cin >> ii[i];
	
	for (int i = 1; i <= N; i++)
	{
		int sum = 1; int j = i + 1; long long add = ii[i];

		while (add + ii[j] <= S && j <= N)
		{
			add += ii[j]; sum++; j++;
		}
		jj[i] = sum;
	}

    //输入操作 + 优化后的分区操作
    int left = 1;
	long long add = 0;
	for (int i = 1; i <= N; i++)
	{
		cin >> aa[i]; add += aa[i];
		while (add > S)
		{
			bb[left] = i - left; add -= aa[left++];
		}
	}
	while (left <= N)
	{
		bb[left] = N - left + 1; left++;
	}

这下把分区优化到O(N)的时间复杂度。

时间缩短了,但正确率还是10%,看来还是逻辑有疏漏。

1.2 接下来看其他情况:

7开始,分区为1前S1个、选7之后,"去7到的后1位",到4,分区为3,(后S)能选3个。综合得:从7开始选,前KK不超过S情况下,可以最多选 1 + 3 = 4个。(满足:前KK不超过S,但是不满足:K == 后K)

这时候就要改进了。如果选7,为了满足前K不超过S,那么只能选1个,为了前K == 后K,后面也只能选1个,总共2个。那如果不选7,哪怕只在(4 1 0)中选,也最多能选2个。(看着我个人都觉得好绕,但其实并不复杂,应该是我个人表述能力不行……)

所以,当前SK小于后SK,比较(前SK * 2)和(后SK)哪个更大大的即为结果

同理,当前SK大于后SK,比较(前SK)和(后SK * 2)哪个更大大的即为结果

接下来,还是举几个例子吧。

(石子数量)N =  7,(限制重量)S = 10,(石子重量)2 2 2 2 2 5 5,分区为:5 4 3 3 2 2 1 

(在纸上推导正确)结果为:4,改进前:(5 + 2 = 7, 7 - 1 = )6,改进后:(5 > 2 * 2, 5 - 1 = )4 

(石子数量)N =  7,(限制重量)S = 10,(石子重量)5 5 2 2 2 2 2,分区为:2 3 5 4 3 2 1 

(在纸上推导正确)结果为:4,改进前:(2 + 5 = 7, 7 - 1 = )6,改进后:(2 * 2 < 5, 5 - 1 = )4 

N =  10,S = 12,2 2 2 2 2 2 3 3 3 3 ,分区为:6 5 5 5 4 4 4 3 2 1 

(在纸上推导正确)结果为:8,改进前:(6 + 4 = )10,改进后:(6 < 2 * 4, 2 * 4 = )8 

……

好了,完善到这里,已经有90%的正确率了。

还剩最后10%,请看这个案例:N = 11 , S = 8 , 1 1 1 1 1 1 1 1 2 3 3 , (分区)8 7 7 6 5 5 4 3 3 2 1

(在纸上推导正确)结果为:10,改进后:(8 > 2 * 3)8,错误。在纸上推导的我们很容易得出,答案10是由第二个(分区中的)5得出的。因为它前5位是8,意味着它前面第五个数,如果选了,则可以选8个,自然囊括了它在内。而选了它,分区为5,最多可选5个。所以它本身5个,加上前面8内的5个,加起来10个也就是最优解。(说起来挺复杂的,但其实动手在纸上写一写,其实很容易理解)

所以,综合得:(伪代码?)

1、定义数组 aa[1000001], 数组 bb[1000001];2、输入数据(到 aa),同时进行分区(到 bb);3、遍历( bb),从 bb[i], bb[i + bb[i]], bb[i - bb[i]] 中找出最大的……

【遍历到数组的某个元素,看三个:元素本身,该元素前"元素值"个,该元素后"元素值"个。当中最大的为结果】

还有分区,优化后的分区(自行理解)还是比较有趣的,差不多,就这样吧。

附上代码:

#include<iostream>
using namespace std;

//温馨提示:过大的数据要放在函数体外
int aa[1000001] = { 0 };
int bb[1000001] = { 0 };

//取最大
int Max(int a, int b, int c = -1)
{
	int max = (a > b) ? a : b;
	return (max > c) ? max : c;
}

//取最小
int Min(int a, int b)
{
	return (a < b) ? a : b;
}

int main()
{
	int N; cin >> N;
	long long S; cin >> S;

	int left = 1;
	long long add = 0;
	for (int i = 1; i <= N; i++)
	{
		cin >> aa[i];
		add += aa[i];

		//while循环就是执行"分区"的操作了。
		while (add > S)
		{
			bb[left] = i - left;
			add -= aa[left++];
		}
	}
	//也是"分区"的操作(收尾)
	while (left <= N)
	{
		bb[left] = N - left + 1;
		left++;
	}

	//测试:检查分区情况
	//for (int i = 1; i <= N; i++)
	//{
	//	cout << bb[i] << " ";
	//}
	//cout << endl;

    /*注意:下面这段for循环内的注释,写得不好,尽量自行理解,不要被我的注释限制思路*/

	int MaxSum = 0;
	for (int i = 1; i <= N; i++)
	{
		//当前遍历到的长度
		int Len01 = bb[i];

		//选了Len01之后,进到下一S分区的长度
		int Len02 = 0;
		if (i + bb[i] <= N) Len02 = bb[i + bb[i]];

		//以Len01长度分区的上一段
		int Len03 = 0;
		if (i - bb[i] >= 0 && bb[i] <= bb[i - bb[i]]) Len03 = bb[i] * 2;

		int MaxLen = Max(Len01, Len02, Len03);
		int MinLen = Min(Len01, Len02);

		MaxLen = Max(MaxLen, MinLen * 2);

        //更新最优解
		MaxSum = Max(MaxSum, MaxLen);
	}
	//奇数则化为偶数,位运算妙用
	cout << (((MaxSum & 1) == 1) ? MaxSum - 1 : MaxSum);

	return 0;
}

结束:

并不是很复杂,多在纸上推导几遍即可。感觉表述得不好,希望理解吧。

(文章写得很急,哪里不懂的,私信或者评论问我,有空都会回复)

  • 10
    点赞
  • 33
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 9
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

_Lyz_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值