题目:
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
4 对应 3 代表 若从 4 开始选,至多选 3 个(满足 <= S) 4 + 1 + 0 <= 10
以此类推……
【分区数组所存放的是:对应原始数组(相同下标),从当前位置往后选,不超过 S 的情况下,最多可以选多少个石子】
得到所有的分区情况,那就开始遍历,找最长相邻区间即可。
1.1 先挑最简单的情况:从0开始选,分区结果为2,意味着前S能选2个、选0之后,"去0到的后2位",到5,分区结果为2,意味着(后S)能选2个。综合得:从0开始选,前K后K不超过S情况下,可以最多选 2 + 2 = 4个。(满足:前K后K不超过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,前S选1个、选7之后,"去7到的后1位",到4,分区为3,(后S)能选3个。综合得:从7开始选,前K后K不超过S情况下,可以最多选 1 + 3 = 4个。(满足:前K后K不超过S,但是不满足:前K == 后K)
这时候就要改进了。如果选7,为了满足前K不超过S,那么只能选1个,为了前K == 后K,后面也只能选1个,总共2个。那如果不选7,哪怕只在(4 1 0)中选,也最多能选2个。(看着我个人都觉得好绕,但其实并不复杂,应该是我个人表述能力不行……)
所以,当前S的K小于后S的K,比较(前S的K * 2)和(后S的K)哪个更大,大的即为结果。
同理,当前S的K大于后S的K,比较(前S的K)和(后S的K * 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;
}
结束:
并不是很复杂,多在纸上推导几遍即可。感觉表述得不好,希望理解吧。
(文章写得很急,哪里不懂的,私信或者评论问我,有空都会回复)