【信息学竞赛真题! ! !】「USACO2016」Subsequences Summing to Sevens题解(C++版)

朋友们好!今天要和大家分享的是一个有趣的题,这是2016年美国信息学奥赛USACO一月赛季的题目,中文名叫“与7无关的数”,你将在本文中了解到前缀和的知识,并且了解到一道难题是怎么做出来的。(注,这是一个困难的过程)

(本文适合学习C++有那么一点点基础,但基础又不是特别高的朋友)

A.题目描述

题目描述

给你n个数,分别是a[1],a[2],…,a[n]。求一个最长的区间[x,y],使得区间中的数(a[x],a[x+1],a[x+2],…,a[y-1],a[y])的和能被7整除。输出区间长度。若没有符合要求的区间,输出0。

输入

第一行一个数n,接下来为n个数,每个数在0~1000000范围内,1<=n<=50000

输出

输出最大区间长度

样例输入
7
3 5 1 6 2 14 10

样例输出
5

提示

来源
USACO2016JAN

B.初步分析

一开始你看到这道题是什么感受呢?你会不会说:这道题不就是道枚举题嘛,so easy!

1.0版方法

for(int i=1;i<=n;i++){
	for(int j=i;j<=n;j++){
		for(int k=i;k<=j;k++){
			s+=a[k];
		}
		if(s%7==0)
			m=max(j-i,m);
		s=0;
	}
}

可是,注意一下数据范围,三重循环,明晃晃的超时呀!
于是,你是否人有了改进枚举的想法——

1.5版方法

for(int i=1;i<=n;i++){
	int s=0;
	for(int j=i;j<=n;j++){
		s+=a[j];
		if(s%7==0)
			m=max(j-i,m);
	}
}

看上去好些了,那么让我来告诉自信的你,时间超限了……

等等,换种思维试试——

1.7版方法

for(int i=1;i<=n;i++){
	int s=0;
	for(int j=n;j>=i;j--){
		s+=a[j];
		if(s%7==0){
			m=max(j-i,m);
			break;
		}
	}
}

哈哈,从今以后请记住:不狡猾的出题人是不聪明的。单纯地用枚举,无论怎样都会时间超限……

C.算法改进

这时,总算有人想起前缀和与差分。
前缀和直接就求出了1~i之间的所有数值和,不是很好用吗?

(不懂得前缀和的朋友,请直接把文档拉到底部E.3处。)

2.0版方法

for(int i=1;i<=n;i++){
	scanf("%lld",&a[i]);//注意,a[i]太大,要用long long定义,也就得用lld输入
 	b[i]=b[i-1]+a[i];
}

这里不就求出数组a的前缀和数组b了吗,我们继续——

for(int i=0;i<=n;i++){
	for(int j=n;j>=0;j--){
		if((a[j]-a[i])%7==0){
			printf("%d",j-i);
			return 0;
		}
	}
}

这里我竟然犯了个如此 ZhiZhang 的错误。(网警提醒您,拒绝粗鲁脏话)
时间没有超,但是 j - i 不一定最优啊~~

于是我们又得艰难的改变算法——

2.2版方法

for(int i=0;i<=n;i++){
	int s=0;
	for(int j=n;j>=1;j--){
		if((a[j]-a[i])%7==0){
			s=j-i;
			break;
		}
	}
	m=max(m,s);
}
printf("%d",m);

不瞒你说,出题人还是蛮有恒心与方法的,他真的给你创造了50000个不满足要求数!
那么,我们的答案肯定超时的。

怎么办?顺着灵感继续吧,不获全胜誓不收兵!

2.4版方法

for(int i=0;i<=n;i++){
	int s=0,fl=0;
	for(int j=n;j>=1;j--){
		if(j-i<=fl)
			break;
		if((a[j]-a[i])%7==0){
			s=j-i;
			fl=s;
			break;
		}
	}
	m=max(m,s);
}
printf("%d",m);

把不可能的都排除,很不错的方法,可是就像我刚才说的那样,万一都不满足,你的时间复杂度就有点高咯。
相信你一定有些恨我了,可是有什么办法呢?无论学什么学科都是这样,在无数次失败中找到方法,慢慢抬起头来

努力的想吧,只有来之不易的东西才会让人记忆深刻。

2.8版方法

中间复杂且失败的2.5 2.6 2.7版方法先不说,我把我最神奇的想法说给你听。

#include<bits/stdc++.h> 
using namespace std;

long long a[1000000],b[1000000];

int main(){
	int n,x,y,s=0,m=0,fl=0;
 	scanf("%d",&n);
 	
 	for(int i=1;i<=n;i++){//O(50000)
 		scanf("%lld",&a[i]);
 		b[i]=b[i-1]+a[i];
	}
	//求前缀和b数组;一个有意思的想法便开始了
	
	int k,l; 
	for(int i=0;i<=n;i++){//O(50001)
		k=(n-m)/3+i;
		k+=(n-m)%3;
		if(k<n-1)
			l=k+1;
		else
			l=1;//如果k<i,则循环会跳出 
		if(n-i<m){
			break;
		}
		fl=0;
		for(int j=n;j>=l&&j>=fl;j--,l++,k--){//O(17000)
			//若仅一个j来算,时间复杂度是O(50000)整段最坏复杂度为O(2500000000) ,所以需要k和l来帮忙分段计算 
			//分三段,k第一段,l第二段,j最大一段
			
			if(k==0)
				k++;
			//printf("%d  %d %d %d %d\n",m,i,j,l,k);
			if(j-i<=m)
				break;
			//如果连最大的j减去i都不大于m,则没有继续算的必要
			 
			if((b[j]-b[i])%7==0&&b[j]-b[i]>=7){
				m=j-i;
				break;//因为被减数越大,差越大,所以如果 b[j]-b[i])%7==0成立,m会最大,然后可以跳出 
			}
			if((b[l]-b[i])%7==0&&b[l]-b[i]>=7){
				if(l-i>m&&b[l]-b[i]>=7){
					m=l-i;
					fl=l;
				}
			}
			if((b[k]-b[i])%7==0&&k>=i&&b[k]-b[i]>=7){
				if(k-i>=m&&k>i&&b[k]-b[i]>=7){
					m=k-i;
					fl=k;
				} 
			}
		}
	}
	//别着急,慢慢看。 
	//O(50001*17000)=O(850017000)
	
	printf("%d",m);
	//时间复杂度最坏情况:O(50000+850017000)=O(850067000)
	
	return 0;
 }

神奇的分段算法:

不
把从i到n这一段距离分给k,l,j来计算,j最优先,其次是了,最后是k。

虽然这种算法还是超时了,但这种方法还是挺值得借鉴的。
那么能取得答案正确的算法是什么呢,我们马上揭晓——

D.答案揭晓

一条神秘的不为人知的数学定理

朋友们,如果你与我一样百思不得其解,那么只能说明我们是一类人——数学没学好。
其实数学中有这么一条定理——一个区间中所有数的和取余7等于0,则这个区间两头的数除以7后余数相等。
长知识了?

有朋友问:你怎么知道的?

下面这一张图,看得懂才看,看不懂算了
在这里插入图片描述
来吧,看看代码如何实现吧——

3.0版方法

#include<bits/stdc++.h>
using namespace std;

long long a[1000000],b[1000000];

int main(){
	int n,x,y,s=0,m=-1,fl=0;
 	scanf("%d",&n);
 	
 	for(int i=1;i<=n;i++){//O(50000)
 		scanf("%lld",&a[i]);
 		b[i]=b[i-1]+a[i];
 		//看好,事情就要发生转机了!
		 
		b[i]=b[i]%7; 
	} 
	
	int c[7][3]={0};//现在要做的,就是找出同一个余数最早出现和最晚出现的地方
	
	for(int i=0;i<=6;i++){//逆转,时间复杂度从O(50001)一下子降到了O(7)
		for(int j=1;j<=n;j++){//<=O(50001),且总会有达到O(1)的 
			if(b[j]%7==i){
				c[i][1]=j;
				//记录i最早出现的位置
				
				break; 
			}
		}
		for(int j=n;j>=1;j--){
			if(b[j]%7==i){
				c[i][2]=j;
				//记录i最后出现的位置 
				
				break; 
			}
		}
		//这时适合顺道 
		//printf("%d %d %d %d\n",i,c[i][1],c[i][2],c[i][2]-c[i][1]);
		if(c[i][2]-c[i][1]>m)
			m=c[i][2]-c[i][1];
	} 
	cout<<m;
	return 0;
}

这是一个优秀的时间复杂度。
我们也由此知道,一道题是怎样在艰难险阻中完成的。
你也许还会说:这真是一道 简单 的题。
没关系,还有许多简单的题呢——

E.补充模块

1.USACO简介

USACO(United States of America Computing Olympiad, 美国计算机奥林匹克竞赛) 是一项针对全世界所有的高中信息学竞赛选手的一项竞赛。这项赛事不仅可以培养学生的算法和编程思维,好的竞赛成绩还能给孩子大学申请加分。由于有些编程题跟谷歌,脸书等顶级科技公司面试题类似,好的USACO竞赛成绩对孩子以后申请实习也大有裨益。

编程语言:
C, C++, Java, Pascal, and Python任选一种

编程训练:
http://usaco.org 本身有提供一些免费的训练材料,包括100多道样题,以及一些编程语言基础和经典算法的介绍。训练题优秀的同学会被推荐参加usaco训练营,从中会产生参加IOI(国际信息学奥林匹克)的国家队选手。

赛程赛事:

月赛:一年4~6次。一般在每年的1(JAN),2(FEB),3(MAR),10(OCT),11(NOV),12(DEC)月举行。
公开赛 (US Open):每年4月举行,题目比月赛要难。成绩优异者可获得参加USACO训练营的机会。

赛程:
一次比赛的时间为34小时,选手需要在时间内完成34道题目。选手可以在该次月赛指定的时间范围(4天)中的任何一个时间打开题目,并在规定的时间内完成比赛并提交。

评分:
代码运行正确性,算法时间效率,内存使用效率等。

难度分级:
分别是金组、银组、铜组,难度依次递减。铜组为入门级。必须在上一级中取得极好的成绩或者是通过所有试题才能进入下一级。

参加要求:
基础编程语言,数据结构,算法。

2.USACO注册方法

1.登陆网站
http://usaco.org/index.php

2.了解界面
你会看到一个全英文的界面——
不要太过惊慌
不要太过惊慌,我们只需要了解几个地方。
在这里插入图片描述
注册界面是这样的:

在这里插入图片描述
接着,进入你的邮箱:
在这里插入图片描述
于是你就可以登录了。
在这里插入图片描述
修改密码:
在这里插入图片描述
如果您正巧遇上月赛,那么这个位置会有一个按钮:
在这里插入图片描述
因为题目是全英文的,所以准备好百度翻译哟。

3.前缀和

题目描述:「模板」前缀和

题目描述
输入n个数,给出m个询问,询问区间 [x,y] 的和。

输入
第一行为n和m,1<=n,m<=100000
接下来一行为n个数,范围在0~100000之间
接下来m行,每行两个数x,y,输出第x个数到第y个数之间所以数的和。保证x<=y

输出
m个输出

样例输入
5 3
1 2 0 7 6
1 3
2 2
4 5
样例输出
3 2 13

初步分析

这很简单呀!

for(int i=1;i<=m;i++){
	scanf("%d%d",&x,&y);
	int s=0;
	for(int j=x;j<=y;j++){
		s+=a[j];
	}
	printf("%d\n",s);
}

这也是对的,不过时间容易超限……所以我们今天要用一种神奇的方法。

神奇的方法

让我们来仔细观察,如果我们在输入的时候,将a[i]前所有数求一个和呢?如图:
在这里插入图片描述

然后——
b[3]-b[1-1]=3 正确。
b[2]-b[2-1]=2 正确。
b[5]-b[4-1]=13 正确

是不是很神奇?!

那么我们就把b数组叫做a数组的前缀和。

for(int i=1;i<=n;i++){
	scanf("%d",&a[i]);//输入a
	b[i]=b[i-1]+a[i];//聪明人都明白这样做更好
}

for(int i=1;i<=m;i++);{
	scanf("%d%d",&x,&y);
	printf("%d\n",b[y]-b[x-1]);
}

时间复杂度=O(n+m),很不错!

好了,今天的题目分享就到此结束了,如果您喜欢这篇文章,别忘了点个赞哟!

F.同主题文章链接

Sorry,这才是我第一篇博客呢~~

发布了1 篇原创文章 · 获赞 0 · 访问量 19

猜你喜欢

转载自blog.csdn.net/Devrt/article/details/104534340
今日推荐