涨姿势题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
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吧。注意不要提交错题目了。
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;
}
View Code

 

 

于是从暴力开始,想一步一步的改善。
一开始想到能不能就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;
}
View Code


于是想到了 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;
}
View Code


 

经历了上面的所有劫难还是不行。我就搁置了几天。( 自己还是没头绪 )
然后 经高人指点( 就是 小白 ), 有两种算法。
从一个情况的不同维度出发得到的。

由于题意是 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;
}
View Code

 

第二种, 假设 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;
}
View Code

 

上述两种 AC 的算法中, 自然是第二种的性能高。 但第一种算法的 适用范围 比较大。




以上是关于涨姿势题2_水题_两种解法的主要内容,如果未能解决你的问题,请参考以下文章

16级第一周寒假作业

FJUT16级第一周寒假作业题解I题

涨姿势了!delete后加 limit是个好习惯么?

涨姿势了!delete后加 limit是个好习惯么?

吃透这份Java高级工程师面试497题解析,涨姿势!

2021腾讯Java面试题精选,涨姿势!