蓝桥学院ACM入门基础--第四节 枚举+优化套路(4)#蓝桥杯2016年省赛题:四平方和#hihoCoder-1505题:小Hi和小Ho的礼物

第四节 枚举+优化套路(4)

1、蓝桥杯2016年的一道省赛题目:四平方和

四平方和定理,又称为拉格朗日定理:
每个正整数都可以表示为至多4个正整数的平方和。
如果把0包括进去,就正好可以表示为4个数的平方和。


比如:
5 = 0^2 + 0^2 + 1^2 + 2^2
7 = 1^2 + 1^2 + 1^2 + 2^2
(^符号表示乘方的意思)

对于一个给定的正整数,可能存在多种平方和的表示法。
要求你对4个数排序:
0 <= a <= b <= c <= d
并对所有的可能表示法按 a,b,c,d 为联合主键升序排列,最后输出第一个表示法

程序输入为一个正整数N (N<5000000)
要求输出4个非负整数,按从小到大排序,中间用空格分开

例如,输入:
5
则程序应该输出:
0 0 1 2

再例如,输入:
12
则程序应该输出:
0 2 2 2

再例如,输入:
773535
则程序应该输出:
1 1 267 838

资源约定:
峰值内存消耗 < 256M

CPU消耗  < 3000ms

第一个思路就是枚举abcd的值,然后判断它们的平方和是不是等于N。我们可以分析一下abcd的枚举范围:

因为a是四个数里最小的,所以a的最小值是0,最大值是500万除以4之后开根号;b最小值也是0,最大值是当a=0的时候,因为b是bcd三个数中最小的,所以b的最大值是500万除以3之后再开根号。同理可以得到c的枚举范围和d的枚举范围

这样abcd的需要枚举的范围大约都是1000~2000,总枚举量是10^12(1000*1000*1000*1000)这个量级。我们之前提到过10^8这个量级的计算量大约用时1秒。所以10^12这个计算量肯定会超时。

超时我们就要想办法优化。我想经过上一节课,大家应该都能想到一个优化的办法,就是减少枚举的变量,只枚举abc三个变量;然后用计算代替枚举,把d算出来,看看d是不是合法。换句话说就是枚举abc,然后判断N-a^2-b^2-c^2是不是完全平方数。

思路2:

思路2对于CPU消耗  < 3000ms已经不超时了。

若继续优化:

于是问题来了:我们能不能只枚举a和b呢?也就是想办法能不能c也不枚举了。这个问题我们需要分析一下,假设我们只枚举a和b,那么我们就知道余下的部分是N-a^2-b^2,不妨把这部分记作R。于是我们的问题变成:c^2+d^2=R有没有解?如果有解,其中c最小的解是什么?

这里大家要注意一点,由于题目要求是abcd联合主键升序第一的解。所以这里要快速求出来c最小的解。

比如这时R=25,那么得到的解应该是c=0, d=5,而不是c=3,d=4。


这里哈希表就派上用场了。我们可以预先求出来R=c^2+d^2的解,用下面这样的unordered_map来保存一个R对应的c。unordered_map<int, int> f;


比如

f[5]=1,表示R=5的解是c=1,(d=2可以由R和c算出来);

f[25]=0,表示R=25的解是c=0,(d=5可以由R和c算出来)。

如果我们能求出来f[0], f[1], … f[5000000]的值,那么我们就可以查哈希表用O(1)复杂度找到R=c^2+d^2的解。
求出f[0], f[1], … f[5000000]的值,也可以用枚举来求解。就是枚举c的值和d的值,代码如下:

#include <iostream>
#include<map>
#include<cmath> 
using namespace std;
int n;
map<int,int>f;

int main() {
	/*预处理出f的值*/
	cin>>n;
	for(int c=0;c*c<=n/2;c++){
		for(int d=c;c*c+d*d<=n;d++){
			if(f.find(c*c+d*d)==f.end()){//只有在c^2+d^2的值没有在f中的时候,才进行下一行的赋值
				f[c*c+d*d]=c;
			}
		}
	}
	/*通过预处理得到f之后,我们就可以只枚举a和b,然后查表得到c,再算出d*/
	for(int a=0;a*a<=n/4;a++){
		for(int b=a;a*a+b*b<=n/2;b++){
			if(f.find(n-a*a-b*b)!=f.end()){
				int c2=f[n-a*a-b*b];
				int d2=sqrt(n-a*a-b*b-c2*c2);
				cout<<a<<' '<<b<<' '<<c2<<' '<<d2<<endl;
				return 0;//找到一个解就停止 
			}
		}
	}
	return 0;
} 

有些同学可能会有疑问,就是f里保存的是c最小的解,会不会这个c比b小,不满足题目要求。比如N=30,我们枚举到a=1,b=2,这时f[25]=0,我们找到的解会是a=1, b=2, c=0, d=5。
实际上不用担心这个问题。因为如果a=1, b=2, c=0, d=5是一个解,那么换一下顺序a=0, b=1, c=2, d=5也一定是一个解。并且a=0, b=1一定比a=1, b=2先枚举到(参考第17和18行),在这时就会求出a=0, b=1, c=2, d=5的解,然后程序结束了。

其中用sqrt函数需引用#include<cmath> 

-----------------------------------------------------------------------------------------------------

2、hihoCoder #1505题:小Hi和小Ho的礼物

https://hihocoder.com/problemset/problem/1505

时间限制:10000ms

单点时限:1000ms

内存限制:256MB

描述

某人有N袋金币,其中第i袋内金币的数量是Ai。现在他决定选出2袋金币送给小Hi,再选2袋金币送给小Ho,同时使得小Hi和小Ho得到的金币总数相等。他想知道一共有多少种不同的选择方法。

具体来说,有多少种下标四元组(i, j, p, q)满足i, j, p, q两两不同,并且i < j, p < q, Ai + Aj = Ap + Aq。  

例如对于数组A=[1, 1, 2, 2, 2],一共有12种选法:

i j p q//是下标啊
1 3 2 4
1 3 2 5
1 4 2 3
1 4 2 5
1 5 2 3
1 5 2 4
2 3 1 4
2 3 1 5
2 4 1 3
2 4 1 5
2 5 1 3
2 5 1 4

输入

第一行包含一个整数N。  

第二行包含N个整数,A1, A2, A3 ... AN。

对于70%的数据,1 <= N <= 100  

对于100%的数据,1 <= N <= 1000, 1 <= Ai <= 1000000

输出

不同选择的数目。

样例输入

5  
1 1 2 2 2

样例输出

12

这道题有70%的数据,N小于等于100。直接枚举ijpq的4重循环复杂度是O(N^4),大概能过70%的数据。
但是最大规模的数据N=1000,4重循环的枚举肯定是会超时的。

有了之前四平方和的基础。
这道题我们很快就会想到一个类似的优化思路:首先预处理出来2袋金币数目和是某个值X一共有多少种选法。把预处理的结果存在哈希表里,记作cnt2[X],表示选出2袋金币和是X有几种选法。
然后只枚举i和j,也就是给小Hi的两袋金币。这样我们就知道金币的和应该是多少。通过查哈希表得到小Ho的两袋金币一共有多少种选法。

这个思路大致方向是对的,但是有个小问题。就是一袋金币不能既给小Hi又给小Ho。我们用样例来说明一下。样例是5袋金币,分别有1,1,2,2,2枚。那么一共有几种选法能选出总数是3枚的2袋金币呢?

从上图我们可以看到一共有6种选法。(注意ij是下标)现在假设我给小Hi的金币是第1袋和第3袋,(金币总数之和是3)那么这时给小Ho的2袋有几种选法呢?注意上面6种和是3的选法并不是都成立,因为第1袋和第3袋金币已经分给小Hi了,所以(1, 3)(1, 4)(1, 5)(2, 3)这四种组合都不能选,只剩下2种组合可选。

于是我们又有了新的问题:我现在选了第1袋和第3袋给小Hi,我知道金币和是3。我们怎么从金币和是3的6种选法里把包含第1袋和第3袋的组合去掉。注意我们不能枚举,我们得想办法把这个结果“算”出来:

实际上这个结果也不难算,包含第1袋的选法数目等于有几个袋子的金币与第3袋一样(包含2个金币的袋子数目)。你看(1, 3)(1, 4)(1, 5)实际上第3袋第4袋第5袋都是装着2个金币,与第3袋相同。同理包含第3袋的选法数目等于包含1个金币的袋子数目。(1, 3)(2, 3)实际上第1袋和第2袋都包含1个金币。

于是我们需要多预处理一个结果:cnt1[X]表示包含X枚金币的袋子数量。

有了cnt2和cnt1,我们就可以进行计算了。当我们枚举分给小Hi的袋子是i=1和j=3时,分给小Ho的选法一共有:cnt2[A[i] + A[j]]cnt1[A[i]] cnt1[A[j]] + 1

 

注意这里+1是因为容斥原理,(1, 3)这个组合被减了2次。另外上面容斥原理算式还有个特例,就是A[i]等于A[j]的时候,这个时候小Ho的选法一共有:cnt2[A[i] + A[j]] – cnt1[A[i]] – cnt1[A[j]] + 3

最后我们看一下代码:

        //注意j=i+1是为了避免重复 

#include <iostream>
#include<map>
using namespace std;
int n,a[1000];
map<int,int>cnt1,cnt2;
long long ans=0;

int main() {
	/*预处理cnt1,cnt1[X]也就是有几个袋子包含X枚金币*/
	cin>>n;
	for(int i=0;i<n;i++){
		cin>>a[i];
		cnt1[a[i]]++;// 
	}
	/*预处理cnt2,cnt2[X]表示选出2个袋子,金币之和是X有几种选法*/
	for(int i=0;i<n;i++){
		for(int j=i+1;j<n;j++){//注意j=i+1是为了避免重复 
			cnt2[a[i]+a[j]]++;
		}
	}
	for(int i=0;i<n;i++){
		for(int j=i+1;j<n;j++){//枚举i和j,也就是分给小Hi的袋子是哪两个
		//注意j=i+1是为了避免重复 
			if(a[i]!=a[j])
				ans=ans+cnt2[a[i]+a[j]]-cnt1[a[i]]-cnt1[a[j]]+1;
			else
				ans=ans+cnt2[a[i]+a[j]]-cnt1[a[i]]-cnt1[a[j]]+3;
		}
	}
	cout<<ans<<endl;
	return 0;
} 

-----------------------------------------------------------------------------------------------------------------------------

一道作业题

题目 : 互补二元组

时间限制:10000ms

单点时限:1000ms

内存限制:256MB

描述

给定N个整数二元组(X1, Y1), (X2, Y2), ... (XN, YN)。  

请你计算其中有多少对二元组(Xi, Yi)和(Xj, Yj)满足Xi + Xj = Yi + Yj且i < j。

输入

第一行包含一个整数N。  

以下N行每行两个整数Xi和Yi。  

对于70%的数据,1 ≤ N ≤ 1000    

对于100%的数据,1 ≤ N ≤ 100000  -1000000 ≤ Xi, Yi ≤ 1000000

输出

一个整数表示答案。

样例输入

5  
9 10  
1 3  
5 5  
5 4    
8 6

样例输出

2

Xi + Xj = Yi + Yj

Xi -Yi =  Yj-Xj

Xi -Yi =  -(Xj-Yj)

计算所有(X,Y)对的差值并保存,然后查找差值互为相反数的(X,Y)对

将输入的数对作差值存储起来即可。考虑到X、Y的取值范围,可用map直接查找。

嗯,以上的都能想到。大神的方法妙在存储X-Y时,直接对Y-X做加操作,省去了重复遍历和查找,很巧妙。(注意X,Y的取值范围,longlong)

#include <iostream>
#include<map>
using namespace std;
int n;
int x,y;
map<long,long>s1;
long long ans=0;

int main() {
	cin>>n;
	while(n--) {
        cin >> x >> y;
        ans=ans+s1[x-y];
        s1[y-x]++;
    }
	cout<<ans<<endl;
	return 0;
} 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

夏普通

谢谢打赏~普通在此谢过

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

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

打赏作者

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

抵扣说明:

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

余额充值