涨姿势题2_水题_两种解法
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了涨姿势题2_水题_两种解法相关的知识,希望对你有一定的参考价值。
Problem Description
涨姿势题就是所谓的优化题,在组队赛中,队伍发现了一题水题,那么应该交给谁去处理?作为处理水题的代码手,应该具备什么样的素养?
1,要快,水题拼的就是速度!
2,不能卡水题!水题都卡,绝对不是一个代码手的风范!
3,不能出错,错一次即罚时20分钟,对于水题来讲是致命的!
4,要能看出来一题是水题!没有这条,上面三条都是没有意义的!
如果你希望你成团队中一个合格的代码手,那么这套题是你最好的选择,快AC吧!
本系列即是为了提高水题代码手的素养而准备的!水题经常需要用到简单的优化,中难题的解题过程中也经常需要各种优化,优化是处理超时的首要选择,目的是降低时间复杂度。
涨姿势题为3题,题面完全相同,仅数据范围不同,请根据不同的数据范围选择合适的算法。
题目描述:
给定数列a[1] a[2] ... a[n]
多次询问
每次询问 有一个数字 qi
求有多少组(l,r)满足 f(l,r)=a[l]+a[l+1]+...+a[r]=qi
1,要快,水题拼的就是速度!
2,不能卡水题!水题都卡,绝对不是一个代码手的风范!
3,不能出错,错一次即罚时20分钟,对于水题来讲是致命的!
4,要能看出来一题是水题!没有这条,上面三条都是没有意义的!
如果你希望你成团队中一个合格的代码手,那么这套题是你最好的选择,快AC吧!
本系列即是为了提高水题代码手的素养而准备的!水题经常需要用到简单的优化,中难题的解题过程中也经常需要各种优化,优化是处理超时的首要选择,目的是降低时间复杂度。
涨姿势题为3题,题面完全相同,仅数据范围不同,请根据不同的数据范围选择合适的算法。
题目描述:
给定数列a[1] a[2] ... a[n]
多次询问
每次询问 有一个数字 qi
求有多少组(l,r)满足 f(l,r)=a[l]+a[l+1]+...+a[r]=qi
Input
第一行是一个t表示测试数据的组数。
每组数据的第一行是两个整数n、q,分别表示数组长度和询问的次数
第二行是n个整数a[1],a[2],...,a[n]。
接下来是q行,每行一个整数qi表示第i次询问。
数据范围:
第1题:t<=130,1<=n<=1000,-10^9<=ai<=10^9,-10^12<=qi<=10^12,大数据不超过一半,每组大数据的q为1或2。
第2题:t<=130,1<=n<=10000,q<=50,1<=ai<=10^9,1<=qi<=10^12,大数据不超过一半。
第3题:t<=30,1<=n<=1000,q<=1000000,-10^6<=ai<=10^6,-10^6<=qi<=10^6,大数据不超过5组。
注意认真比较每题的每个数的数据范围,然后选择合适的算法AC吧。注意不要提交错题目了。
每组数据的第一行是两个整数n、q,分别表示数组长度和询问的次数
第二行是n个整数a[1],a[2],...,a[n]。
接下来是q行,每行一个整数qi表示第i次询问。
数据范围:
第1题:t<=130,1<=n<=1000,-10^9<=ai<=10^9,-10^12<=qi<=10^12,大数据不超过一半,每组大数据的q为1或2。
第2题:t<=130,1<=n<=10000,q<=50,1<=ai<=10^9,1<=qi<=10^12,大数据不超过一半。
第3题:t<=30,1<=n<=1000,q<=1000000,-10^6<=ai<=10^6,-10^6<=qi<=10^6,大数据不超过5组。
注意认真比较每题的每个数的数据范围,然后选择合适的算法AC吧。注意不要提交错题目了。
Output
对于每个询问,输出一个整数表示答案
SampleInput
1 5 6 4 5 6 5 4 4 11 1 20 6 10
SampleOutput
2 2 0 2 1 0
下面就以AC的心路历程开说吧。(想看最终结果-->代码或是做法,请直接拉到较后面。)(代码想看就点开,主要是为了版面,虽然说本来就是 ugly。 o(╯□╰)o )
此题一开始自然是想到暴力。也自然是超时。题意说明是t<=130,1<=n<=10000,q<=50,1<=ai<=10^9,1<=qi<=10^12。就是限暴力,提效率而言。
直接暴力代码:
#include <cstdio> using namespace std; const int all = 10006; typedef long long ll; ll sum, num, cnt; int nnum[all], n, t, q; int main(void) { scanf( "%d", &t ); while( t -- ){ scanf("%d%d", &n, &q ); for( int i=0; i < n; ++ i ){ scanf( "%d", nnum+i ); } // 获取每个qi, 进行n*n枚举。复杂度为O(q*n*n),我也是醉了 for( int i=0; i < q; ++ i ){ scanf( "%lld", &num ); cnt = 0; // n*n枚举 for( int j=0; j < n; ++ j ){ sum = 0; for( int k=j; k < n; ++ k ){ sum += nnum[ k ]; if( sum > num ) break; // 符合,结果cnt +1 if( sum == num ) ++ cnt; } } // 打印结果 printf( "%lld\n", cnt ); } } return 0; }
于是从暴力开始,想一步一步的改善。
一开始想到能不能就q值范围而言,做个一次全历遍,过程结果与qi的值匹配。为了能得到快速匹配,就用了Hash表(具体为Hash表开放址法)。
由于n值大,n*n次历遍也就大,如果用求余自然效率低。就想到了位操作的&运算。用过程值和sum&63,之所以选63,是因为63的二进制是 111111。通过与运算快速匹配获得效率。还是超时了。
代码:
// 用了Hash表开放址法。 #include <cstdio> using namespace std; const int allq = 63; const int alln = 10000; typedef struct node Node; typedef long long ll; // 声明Hash表的节点。 struct node { ll x; int cnt; Node *next; }; // 为删除所生成的节点。 void del( Node *p ) { if( p != NULL ){ del( p->next ); delete p; } } 定义Hash表 Node* Hash[allq+1]; int numn[ alln ]; ll numq[ allq ]; int main(void) { int n, q, t; ll tmp, sum; Node *p; scanf("%d", &t ); while( t -- ){ scanf( "%d%d", &n, &q ); // 扫入所有ai for( int i=0; i < n; ++ i ){ scanf( "%d", numn+i ); } // 扫入所有 qi,并建立 qi 的 node 并初始化每个节点的cnt值,即初始化答案。 // 若存在有 qi 相同, 也有处理办法。在下面有办法 for( int i=0; i < q; ++ i ){ scanf("%lld", &tmp ); p = new Node; numq[i] = p->x = tmp; p->cnt = 0; tmp &= allq; p->next = Hash[ tmp ]; Hash[ tmp ] = p; } // 想通过一次全枚举n*n, 获得结果 for( int i=0; i < n; ++ i ){ sum = 0; for( int j=i; j < n; ++ j ){ sum += numn[j]; p = Hash[ sum&allq ]; while( p != NULL ){ // 若匹配, 该 node 的 cnt + 1, 之前若有存在同 qi 值的, // 匹配完之后, 就break, 存在在较后面的相同 qi 会 一直为0 if( p->x == sum ){ ++ p->cnt; break; } p = p->next; } } } // 通过qi存表, 在查Hash表获得 qi 的 cnt, 即每个 qi 的结果 for( int i=0; i < q; ++ i ){ p = Hash[ numq[i]&allq ]; while( p != NULL ){ if( p->x == numq[i] ){ printf("%d\n", p->cnt ); break; } p = p->next; } } for( int i=0; i < allq; ++ i ){ p = Hash[i]; Hash[i] = NULL; del( p ); } } return 0; }
于是想到了 qi 的一些值可能是超过了 ai 的总和。应该舍弃。在这方面做了个取舍。
当然,还是超时了。原因在于 q 值不够大( 即问题个数不够多 )。总体不符合这个题意,自然效率低。如果 q 值够大。也是可以 AC 的。
失败的终结改良代码(还是失败):
// 仅仅加了个 选取符合小于 ai 总和sum 的 最大 qi // 若有疑问, 看上板。 #include <cstdio> using namespace std; typedef long long ll; typedef struct node Node; struct node { ll x; Node *next; int times; }; const int all = 63; Node *Hash[ all+1 ]; ll ans[ all+1 ]; int nnum[ 10005 ]; ll sum, Max, tmpnum, allnum; int group, num, row, tmp; Node *p; void Free( Node *a ) { if( a != NULL ){ Free( a->next ); delete a; } } int main(void) { scanf("%d", &group ); while( group -- ){ scanf("%d%d", &num, &row ); // 扫所有 ai, 算 ai 总和 为 allnum allnum = 0; for( int i=0; i < num; ++ i ){ scanf( "%d", nnum+i ); allnum += nnum[i]; } // 建表, 算最大符合情况的 Max Max = 0; for( int i=0; i < row; ++ i ){ scanf ("%lld", &tmpnum ); ans[ i ] = tmpnum; if( tmpnum > Max && allnum > tmpnum ) Max = tmpnum; tmp = tmpnum&all; p = Hash[ tmp ]; Hash[ tmp ] = new Node; Hash[ tmp ]->x = tmpnum; Hash[ tmp ]->next = p; if( tmpnum == allnum ) Hash[ tmp ]->times = 1; else Hash[ tmp ]->times = 0; } for( int i=0; i < num; ++ i ){ sum = 0; for( int j=i; j < num; ++ j ){ sum += nnum[ j ]; if( sum > Max ) break; p = Hash[ sum&all ]; while( p != NULL ){ if( p->x == sum ){ p->times += 1; break; } p = p->next; } } } for( int i=0; i < row; ++ i ){ p = Hash[ all & ans[i] ]; while( p != NULL ){ if( p->x == ans[i] ){ printf("%d\n", p->times ); break; } p = p->next; } } for( int i=0; i < row; ++ i ){ Free( Hash[i] ); Hash[i] = NULL; } } return 0; }
经历了上面的所有劫难还是不行。我就搁置了几天。( 自己还是没头绪 )
然后 经高人指点( 就是 小白 ), 有两种算法。
从一个情况的不同维度出发得到的。
由于题意是 1 <= ai <= 10e9; 即是不包括0, 所以顶多存在 n 个等于 qi 的情况。 而且过程值累加起来,
即 sum1 = a1;
sum2 = sum1+a2;
sum3 = sum2+a3;
...
sumi = sum(i-1)+ai。
此过程中, sumi越来越大。(即 sumi 序列是 升序序列 )
基于这个过程, 有两种角度解决问题。
第一种便是通过 sumi-qi 的值是否等于 sum(i-m), m <= i, 即是 qi + sum(i-m) == sumi, m <= i.
此过程便是利用 sumi 恰好构成升序序列, 因为是升序, 所以在 0 < m <= i中寻找 sum(i-m) 的情况,
运用 二叉寻找。算法复杂度为 O( q*nlogn )。
代码如下:
#include <cstdio> using namespace std; typedef long long ll; const int all = 10005; ll num[ all ]; int t, n, q, cnt; ll tmp, tmpq; // 二叉寻找 bool binfind( int l, int r ) { if( l <= r ){ int mid = (l+r)/2; if( tmp == num[mid] ) return true; if( tmp < num[mid] ) return binfind( l, mid-1 ); else return binfind( mid+1, r ); } return false; } int main(void) { scanf( "%d", &t ); while( t -- ){ scanf( "%d%d", &n, &q ); // 直接做累加到 num数组 中 for( int i=1; i <= n; i ++ ){ scanf( "%lld", num+i ); num[i] += num[i-1]; } for( int i=0; i < q; ++ i ){ scanf( "%lld", &tmpq ); cnt = 0; // 历遍整个数组num, 寻找符合情况, cnt + 1 for( int j=1; j <= n; ++ j ){ tmp = num[j]-tmpq; if( binfind( 0, j ) ) ++ cnt; } printf( "%d\n", cnt ); } } return 0; }
第二种, 假设 sum(i-j) 一开始小于 qi, 随着 sum 值累加变大, 假定 sum(i-j) 值最终 大于等于 qi, 若有这种情况,则需要去掉 aj 值, 即 变成 sum(i-j+1)。 此时变成 sum(i-j+1) 与 qi 的比较。 共存在 二 种情况, 即有 sum(i-j+1) >= qi( 同假定一样, 自然做法也一样 ),sum(i-j+1) < qi, 即sum继续累加即可。 算法复杂度为 O( q*n )。
代码如下:
#include <cstdio> using namespace std; typedef long long ll; const int all = 10005; int num[ all ]; ll sum, tmp; int n, q, t, i, j, ans; bool Isplus; int main(void) { scanf( "%d", &t ); while( t -- ){ scanf( "%d%d", &n, &q ); // 保存 ai for( i=0; i < n; ++ i ){ scanf( "%d", num+i ); } while( q -- ){ scanf( "%lld", &tmp ); // 初始化 sum = ans = 0; Isplus = true; for( i=j=0; i < n; ){ // 若是 Isplus == true, sum 累加 if( Isplus ) sum += num[i]; if( sum == tmp ){ // 去尾 sum -= num[j]; ++ j; ++ i; Isplus = true; ++ ans; } else if( sum < tmp ){ ++ i; // 确定 sum 累加 Isplus = true; } else{ sum -= num[j]; ++ j; // sum 值进行判断 Isplus = false; } } printf( "%d\n", ans ); } } return 0; }
上述两种 AC 的算法中, 自然是第二种的性能高。 但第一种算法的 适用范围 比较大。
以上是关于涨姿势题2_水题_两种解法的主要内容,如果未能解决你的问题,请参考以下文章