数列 解题报告(尺取法)

数列

提交文件:bound.exe

输入文件:bound.in

输出文件:bound.out

问题描述:

给定n(1<=n<=100000)个绝对值不超过10000的整数,在这个数列中找一个连续的非空子序列,使其和的绝对值距离整数t(0<=t<=1000000000)最近。

 

输入格式:

输入包含多组数据。

每组数据先输入n和k。n=k=0表示输入结束。

接下来n个用空格分隔的整数,表示数列中的每一个元素。

接下来k个用空格分隔的整数,每个整数表示一次询问的t值。

 

输出格式:

       对于每组数据的每次询问,输出用空格分隔的三个整数:最接近t的连续子序列的和的绝对值,连续子序列的左边界,连续子序列的右边界。子序列可能从1到n。如果存在多个最接近的解,输出任意解都可行。

输入样例:

输出样例:

5 1

-10 -5 0 5 10

3

10 2

-9 8 -7 6 -5 4 -3 2 -1 0

5 11

15 2

-1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1

15 100

0 0

5 4 4

5 2 8

9 1 1

15 1 15

15 1 15


这一题最朴素的方法是暴力枚举,时间复杂度为O(tn^2),显然会超时。

利用尺取法去做,可以把每个询问的时间复杂度由O(n)降到O(1)。能用尺取法的问题满足一定的单调性,我们后面再提。

对本题而言,尺取法是对于在求区间时,不一一枚举所有区间的和,而是定义l为区间左端点、r为区间右端点,扫描过程中持续推进左端点和右端点并更新记录最优解,直到r>n;

前面说到,可以用尺取法的题目必须满足一定的单调性;对比一下与二分的过程,可以发现,无论在二分还是尺取法中,单调性可以帮助我们判断什么时候推进l,什么时候推进r。
把前缀和从小到大排一下序,那么对于每段区间[l,r],若sum[r]-sum[l]<t,那么我们希望区间和更大以接近t;而因为前缀和已经从小到大排过一次序,r++,则sum[r]增大,区间和将更大;同理,sum[r]-sum[l]>t,我们希望区间和更小以接近t,l++,则sum[l]增大,区间和减小;而sum[r]-sum[l]==t时,此时必定最接近t了嘛(都相等了那当然是最接近的了!)。

但是,因为答案要求我们输出最接近t的区间和时对应的区间端点,那么在读入时可以对每个前缀和的下标记录一下,排序的时候也顺带交换即可(记得每组数据处理前要初始化数组)。

代码如下:

#include<cstdio>
#include<cstring>
using namespace std;
	int a[100001];
	int label[100001];
	int n,m;
void qs(int l,int r)
{
	int i=l;
	int j=r;
	int m=a[(l+r)/2];
	while (i<=j)
	{
		while (a[i]<m) i++;
		while (a[j]>m) j--;
		if (i<=j)
		{
			int k=a[i];
			a[i]=a[j];
			a[j]=k;
			k=label[i];
			label[i]=label[j];
			label[j]=k;
			i++;
			j--;
		}
	}
	if (i<r) qs(i,r);
	if (j>l) qs(l,j);
}
int abs(int x)
{
	if (x<0) return (-x); else return x;
}
int main()
{
	freopen("bound.in","r",stdin);
	freopen("bound.out","w",stdout);
	scanf("%d%d",&n,&m);
	while (n!=0&&m!=0)
	{
		memset(a,0,sizeof(a));
		label[0]=0;
		for (int i=1;i<=n;i++)
		{
			int x;
			scanf("%d",&x);
			a[i]=a[i-1]+x;
			label[i]=i; //记录下标
		}
		qs(0,n);
		for (int i=1;i<=m;i++)
		{
			int t;
			scanf("%d",&t);
			int l=0;
			int r=1;
			int ans=2147483647;
			int left_most;
			int right_most;
			while (r<=n) //尺取法过程
			{
				if (abs(a[r]-a[l]-t)<abs(ans-t))
				{
					ans=abs(a[r]-a[l]);
					left_most=label[l];
					right_most=label[r];
				}
				if (a[r]-a[l]<t) r++;
					else if (a[r]-a[l]>t) l++;
						else if (a[r]-a[l]==t) break;
				if (l==r) r++;
			}
			if (left_most>right_most)
			{
				int k=left_most;
				left_most=right_most;
				right_most=k;
			}
			printf("%d %d %d\n",ans,left_most+1,right_most);
		}
		scanf("%d%d",&n,&m);
	}
	return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值