试题 算法训练 礼物
资源限制
时间限制:1.0s 内存限制:256.0MB
问题描述
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
样列解释
任意选择连续的6个1即可。
数据规模和约定
对于20%的数据:N<=1000
对于70%的数据:N<=100,000
对于100%的数据:N<=1000,000, S < = 1 0 12 S<=10^{12} S<=1012,每个石子的重量小于等于 1 0 9 10^9 109,且非负
解法
思路
第一眼看到这个题目,就抓住了关键的地方-区间和,这让我第一反应就想到了前缀和,赶紧看一下数据范围:每个石子的重量<= 1 0 9 10^9 109,并且N<= 1 0 6 10^6 106,所以最大的区间和就是 1 0 9 ∗ 1 0 6 = 1 0 15 10^9 * 10^6=10^{15} 109∗106=1015
!!!
这明显是不超过long型数据范围的,那就可以搞起来了!
什么是前缀和?
这个知识可以跟我们高中的知识挂钩了,数列还记得吧?
假设有一个数列 { a n } \{a_n\} {an},那他的前 n n n项和就是 S n S_n Sn,那么假设我们要求他第 a a a项到第 b b b项的和该怎么办呢?这也 s o e a s y so\ easy so easy啦
我们只要用他的前 b b b项和减去他的前 a − 1 a-1 a−1项和就行了。
也就是:
∑ i = a b a i = S b − S a − 1 \sum_{i=a}^ba_i=S_b-S_{a-1} i=a∑bai=Sb−Sa−1
所以我们在存储数据的过程中,不存储他们的每一个石子的重量,我们只要存储前 i i i个石子的重量和,这也就可以将计算区间重量的时间复杂度从 O ( N ) O(N) O(N)降低到 O ( 1 ) O(1) O(1)了!
所以接下来,我们要做的就是模拟了,从1到n枚举每一个石子作为七点的最大能获取的满足条件的石子数就行了
java代码
import java.io.BufferedReader;
import java.io.InputStreamReader;
/**
* @author 王宇哲
*/
public class Main {
static int n;
static long s;
/**
* 前x个石子的总重量
*/
static long[] weights;
public static void main(String[] args) throws Exception {
//使用缓存流加速读取
BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
String[] tmp = reader.readLine().split(" ");
n = Integer.parseInt(tmp[0]);
s = Long.parseLong(tmp[1]);
weights = new long[n+1];
int cnt = 1;
//遍历求出对于每个i,前i个石子的总重量
while(cnt<=n){
tmp = reader.readLine().split(" ");
for(int i=0;i<tmp.length;i++){
weights[cnt] = weights[cnt-1] + Integer.parseInt(tmp[i]);
cnt++;
}
}
int maxNum=-1;
//石子的数量
int num=0;
//左边的区间的重量
long lWeight;
//右边区间的重量
long rWeight;
for (int i=1;i<=n;i++){
//k表示区间一半的长度
for(int k=1 ;i + 2 * k -1 <= n;k++){
lWeight = weights[i-1+k]-weights[i-1];
rWeight = weights[i-1+2*k]-weights[i+k-1];
if(lWeight > s || rWeight > s){
break;
}
num = 2*k;
}
maxNum = Math.max(maxNum,num);
}
System.out.println(maxNum);
}
}
然后。。。。然后你发现你超时了。。。。。哈哈哈哈
那我们开始思考超时的地方在哪?
首先,我们每次对i进行循环的时候,都会更新一遍maxNum,也就是最多能取的石头数量,那么我们发现,当 2 k < m a x N u m 2k<maxNum 2k<maxNum时,出现了很多的重复操作,时间复杂度会达到 O ( n 2 ) O(n^2) O(n2)
那么我们完全可以将枚举的区间从 k = m a x N u m k=maxNum k=maxNum开始,这样我们就可以大大减少时间复杂度。
真正的java代码
package com.lanqiao;
import java.io.BufferedReader;
import java.io.InputStreamReader;
/**
* @author 王宇哲
*/
public class 算法训练_礼物 {
static int n;
static long s;
/**
* 前x个石子的总重量
*/
static long[] weights;
public static void main(String[] args) throws Exception {
//缓存流加快读取速度
BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
String[] tmp = reader.readLine().split(" ");
n = Integer.parseInt(tmp[0]);
s = Long.parseLong(tmp[1]);
weights = new long[n+1];
int cnt = 1;
//遍历求出对于每个i,前i个石子的总重量
while(cnt<=n){
tmp = reader.readLine().split(" ");
for(int i=0;i<tmp.length;i++){
weights[cnt] = weights[cnt-1] + Integer.parseInt(tmp[i]);
cnt++;
}
}
int maxNum=1;
//石子的数量
int num=0;
//左边的区间的重量
long lWeight;
//右边区间的重量
long rWeight;
//将每一个石子作为最开始选择的石子开始枚举
for (int i=1;i<=n;i++){
//在这里让k从maxNum/2开始枚举区间,可以减少时间复杂度
//同时为了防止越界,要判断 i-1+k>=0(其实也有点多余,可以去掉)
for(int k=maxNum/2+1 ;i + 2 * k -1 <= n && i-1+k>=0;k++){
lWeight = weights[i-1+k]-weights[i-1];
rWeight = weights[i-1+2*k]-weights[i+k-1];
if(lWeight > s || rWeight > s){
break;
}
//这边的2*k应该是一定>=maxNum的,所以理论上可以改为:
//maxNum = 2*k;
num = 2*k;
}
maxNum = Math.max(maxNum,num);
}
System.out.println(maxNum);
}
}
终于,过了!!!
题外话
对于这道题,我看到大家在csdn上的解法都是使用的二分查找,但是实际测试下来我的这种优化好像效果跟二分查找时间复杂度以及空间复杂度都一样,而且思路要更加简单一些。
当然,对于这道题,我想应该是可以使用动态规划来进行求解的,但是实在不够聪明,如果有解法的话希望能教我一下