ACM常用解题技巧方法

转自:http://blog.csdn.net/consciousman/article/details/51407514

参加acm也一段时间了,决定重新总结下自己学到的一些东西,这样才会收获得更多。

比赛考验的不仅仅是知识面,思维能力,编码能力,更有团队合作的意识以及心态。平时接触的题目会涵盖各个方面的内容,我们都需要有一些适当的解题技巧,在这里总结谈一些比较重要的技巧或者方法的运用:1、预处理 2、算法优化 3、STL的运用。

我只是根据自己的学习到的东西来粗略地总结总结,还远没有达到那种真正会的水平,如果有任何错误或者说得不好的地方请见谅(路过的大牛、大神请无视我.....)

 

编程的技巧着实很多,小到一些基本的简单操作比如说位运算的运用大到一些好的思想的应用。任何一个技巧的应用都可能给你的程序带来不一般的在速度或其他方面的变化,就比如说这个位运算符的应用,充分利用的话时间开销大大减小。

我也曾在一本叫做算法心得的书上看到了这个技巧,老外写的,非常经典,书没有说一些我们现在的算法,而是说怎么利用一些简单却非常高效的技巧,只是那本书我实在没有看完…感觉写的不错,但我还没有用过。

 

下面也有一些小技巧(在此引用,感谢这位博主):

http://blog.sina.com.cn/s/blog_a46ca35201013c4n.html

 

下面进入正题:

1、预处理

所谓预处理,顾名思义,就是事先计算好需要的值或事先处理某些东西,有时候你会发现你做一个题目出现了TLE,原因就是重复的计算会导致效率不高(或者说你的预处理不够“优雅”)。

一、直接把结果预处理:

举几个比较简单的例子:


1、Xtuoj1052
Description
某一个数字集合定义如下:
1. 0属于这个集合;
2.如果x属于这个集合,那么2x+1,3x+1也属于这个集合;
3.集合只包含按增序排列的前100000个元素。
集合按增序排列,根据输入的元素序号,输出对应的元素值。
输入
每行一个整数n(n<100000),表示元素的序号(从0开始记数),如果是-1,则输入结束。
输出
每行输出对应元素的值。
Sample Input
0
1
2
3
4
5
-1
Sample Output
0
1
3
4
7
9

分析:很明显,不能也不好直接判断是否存在于这个集合中,只需要把所有存在于这个集合中标记,并且预处理这些元素的序号,之后输出就行了,那么一次预处理便可以知道所有序号对应的元素了。

[cpp] view plain copy
  1. #include <iostream>  
  2. #define MAX 2000001  
  3. using namespace std;  
  4.   
  5. int a[100010], b[3*MAX];  
  6. int main() {  
  7.    int n, i, j;  
  8.    b[0] = 1;  
  9.    for (i = 0; i < MAX; i++)  
  10.        if (b[i] == 1) b[2*i+1] = b[3*i+1] = 1;  
  11.    for (i = 0, j = 0; i < 100000; j++)  
  12.        if (b[j] == 1) a[i++] = j;  
  13.    while (cin >> n, n != 1) cout << a[n] << endl;  
  14.    return 0;  
  15. }  

2、POJ1426

点击打开链接

题意:有k个坏人k个好人坐成一圈,前k个为好人(编号1~k),后k个为坏人(编号k+1~2k),给定m,从编号为1的人开始报数,报到m的人就要自动死去,之后从下一个人继续 开始新一轮的报数。问当m为什么值时,k个坏人会在好人死亡之前全部死掉?

分析:遇到存在环的题目的时候,可以直接直线化处理。当然也可以直接利用循环链表或者数组进行环的模拟,不过这样的程序写起来有点复杂。

这个题目直接暴力模拟求解必定TLE,需要一点数学的知识,这在里就不详细说了,即使这样,还是会超时,正确的方法便是预处理出仅有的14个答案,但既然已经知道了所有答案,而且又只有14个,那么直接把答案交上去就行了。

[cpp] view plain copy
  1. #include <cstdio>  
  2.   
  3. int ans[15] = {0, 2, 7, 5, 30, 169, 441, 1872, 7632, 1740, 93313, 459901, 1358657, 2504881, 13482720};  
  4. int main() {  
  5.      int n;  
  6.      while (scanf("%d", &n), n) printf("%d\n", ans[n]);  
  7.      return 0;  
  8. }  

3、uva12716

点击打开链接

题意:给定一个整数n,求出有多少对整数a,b满足1<=b<=a<=n且gcd(a,b)=a XOR b.

分析:最容易想到的方法是枚举a,b,双重循环加上求gcd,总复杂度为O(n*n*logn),绝对无法承受。如何减少枚举呢?注意到亦或运算的性质,如果a^b=c,那么a^c=b,既然c为a,b的最大公约数的话,那么我们可以从枚举a和c出发,那么就是枚举所有因子c及其可能的倍数a,和素数筛法一样,这样复杂度为O(nlogn*logn),n最大为30000000,复杂度还是有点高,怎么减少复杂度呢?这就要通过一点数学知识或者找规律发现了,通过打印出所有满足条件的a,b,c可以发现a+b=c,所以可以将复杂度降为O(n*logn),但是题目是多样例输入,如果每次都需要O(n*logn)计算答案的话,还是会超时,观察便可得知其实在计算n以内满足条件的a,b对数时比n小的数以内的对数都已经计算出来了,也就是说不需要重复计算了,那么我们可以通过一次预处理,在计算的过程中统计每个a组合时的对数,之后循环遍历一次全部加起来就可以知道每个n以内的答案了。


代码:

[cpp] view plain copy
  1. #include <cstdio>  
  2. #include <algorithm>  
  3. #include <cstring>  
  4. #include <cmath>  
  5. using namespace std;  
  6.   
  7. const int N = 30000000;  
  8. int a[N+5];  
  9. void pretreat() {  
  10.     for (int i = 1; i <= 15000000; i++) {  
  11.         for (int j = i<<1; j <= N; j += i) {  
  12.             if ((j ^ (j-i)) == i) a[j]++;  
  13.         }  
  14.     }  
  15.     for (int i = 2; i <= N; i++) a[i] += a[i-1];  
  16. }  
  17.   
  18. int main() {  
  19.     pretreat();  
  20.     int t, ca = 0;  
  21.     scanf("%d", &t);  
  22.     while (t--) {  
  23.         int n;  
  24.         scanf("%d", &n);  
  25.         printf("Case %d: %d\n", ++ca, a[n]);  
  26.     }  
  27.     return 0;  
  28. }  

二、把需要用的预处理

比较常见的基本就是三个:预处理素数、预处理组合数、预处理前缀和。


首先举个比较经典的例子:素数打表

判断是否素数有3种方式:O(sqrt(n)的简单素性测试、埃氏筛法,以及Miller_Rabin 算法进行素数测试。

如果需要进行大量的用到素数或者判断素数,则可以埃氏筛法打表出所有的素数。


1、xtuoj1237

Description
题目描述
如果n和n+2都是素数,我们称其为孪生素数,比如3和5,5和7都是孪生素数。给你一个区间[a,b],请问期间有多少对孪生素数?

输入
第一行是一个整数K(K≤ 10000),表示样例的个数。以后每行一个样例,为两个整数,a和b,1≤a≤b≤5000000。
输出
每行输出一个样例的结果。
样例输入
5
1 3
1 10
1 100
1 1000
1 5000000
样例输出
0
2
8
35
32463


分析:计算区间内个数的题目一般满足区间减法性质,但是不能一概而论,具体题目具体分析,就像这题一对孪生素数是跨越了3个数,要分情况考虑。

首先直接标记出所有的素数,令g[x]为1到x+2这个区间内孪生素数的对数,要统计出数量,遍历一次即可,只需要一次预处理就可以计算出所有的g[x],之后便可以O(1)计算出所有1到x+2这个区间内孪生素数的对数了。

如果输入的区间长度小于2,那么必定没有,如果长度大于2,稍加思考便可以得知答案即为g[b-2]-g[a-1]。

[cpp] view plain copy
  1. #include <cstdio>  
  2. #include <cmath>  
  3.    
  4. const int N = 5000001;  
  5. int f[N], g[N];  
  6. int main() {  
  7.    int up = sqrt(N);  
  8.    for (int i = 2; i <= up; i++)  
  9.        if(!f[i]) for (int j = i*i; j <= N; j += i) f[j] = 1;  
  10.    for (int i = 3; i < N-1; i += 2)  
  11.        g[i+1] = g[i] = g[i-1] + !(f[i]|| f[i+2]);  
  12.    int t;  
  13.    scanf("%d", &t);  
  14.    while (t--) {  
  15.        int a, b;  
  16.        scanf("%d %d", &a, &b);  
  17.        b-a < 2 ? puts("0") : printf("%d\n", g[b-2] -g[a-1]);  
  18.     }  
  19.    return 0;  
  20. }  

2、CF231CTo Add or Not to Add

点击打开链接

题意:给定一个数组,每次可以给任意元素加1,这样的操作不能超过k次,求操作不超过k次后数组中一个数能够出现的最多次数,以及能够以这个次数出现的最小的数。

分析:这个题目明显具有单调性,这样的话就可以进行二分搜索求取最大次数了。怎么判断假定的解是否可行呢?既然只能是加1,而且又不超过k次,那么首先将数组排序,假设搜索的次数为m,那么i从第m个数循环到最后一个数,只要满足了次数不小于k就立即退出循环,这样找到的便一定是出现m次的最小的数,但是这个判断的过程就是第m个数与其之前的m-1个数的差之和要不大于k,如果每次都直接加上差进行判断必定超时,因为二分搜索加循环判断的时间复杂度太高,那么最好的优化就是直接之前预处理,求出第1个数到第m个数区间的和,后面判断的时候直接就是o(1)计算区间的和了。

 

代码如下:

[cpp] view plain copy
  1. #include <cstdio>  
  2. #include <algorithm>  
  3. #include <cstring>  
  4. using namespace std;  
  5.    
  6. typedef long long LL;  
  7. const int INF = 0x3f3f3f3f;  
  8. const int N = 100010;  
  9. LL a[N], sum[N];  
  10.    
  11. int main() {  
  12.    int n; LL k;  
  13.    while (~scanf("%d %I64d", &n, &k)) {  
  14.        for (int i = 1; i <= n; i++) scanf("%I64d", &a[i]);  
  15.        sort(a + 1, a + 1+n);  
  16.        int r = INF, l = 0;  
  17.        sum[1] = a[1];  
  18.        for (int i = 2; i <= n; i++) sum[i] = a[i] + sum[i-1];  
  19.        LL ans;  
  20.        while (r - l > 1) {  
  21.            int m = (r+l) / 2;  
  22.            if (m > n) { r = m; continue; }  
  23.            int flag = 0;  
  24.             for (int i = m; i <= n; i++) {  
  25.                 if ((m-1)*a[i] - sum[i-1] +sum[i-m] <= k){  
  26.                     flag = 1; ans = a[i];  
  27.                     break;  
  28.                 }  
  29.            }  
  30.            flag ? l = m : r = m;  
  31.        }  
  32.        printf("%d %I64d\n", l, ans);  
  33.     }  
  34.    return 0;  
  35. }  


三、关于预处理的总结:

预处理的目的是为了减少重复的计算从而减少时间复杂度,有时一个简单的预处理能使得算法性能显著提高。首先我们可以按思路直接写一个程序,如果复杂度太大,那么算法的优化可以从这个过程出发,思考可以对哪个算法的部分进行改进,而预处理便是一种对应的非常重要的技巧。像预处理区间和便是非常常见的,而且正如上面所示的几个题一样,一次预处理出全部的答案也是很常见的,要注意思考每个部分的联系。


2、算法优化:

其实不知道该怎么给这个技巧取名字,算法优化涵盖的范围很广,优化的地方可以有很多,所以总结也不可能全面,就针对自己遇到的一些题来总结下吧,像上面的预处理技巧其实也算是算法优化的一部分,只不过觉得很重要,所以特别分开了。

分为3个部分:

1、减少冗余计算。2、减少计算量。3、优化计算过程。

(1)、减少冗余计算

1、Codeforces659F-Polycarp and Hay

点击打开链接

题意:给定一个矩阵,每个格子有一个数字,操作是改变格子中的数字,使其变小,操作后的矩阵必须满足以下要求:所有格子中的数字和为k,非零格子中的数字要相同,至少一个格子要保留为原来的数字不变化,所有格子必须联通,联通是指所有非零格子都可以通过非零格子到达。


分析:很容易想到搜索来解决。刚开始的思路是,对于k的某一个约数d,如果格子中存在数字d且大于等于d的数字的个数如果大于等于k/di个的话,那么开始进行搜索并判断是否满足条件,满足条件就把上一次走过的的格子全部变为d,其他全部变为0即可。具体是从正反开始枚举k的每一个因子然后重复上述过程,但各种优化之后最终还是超时了,跑到了第57个样例,发现方法选取有问题,首先需要枚举每一个可行的因子,每次枚举都需要遍历所有格子,这样时间复杂度过高。

其实可以换种方式:直接一次遍历原图即可,对于格子中的某一数字d,如果它是k的因子的话,那么直接从此开始搜索,判断是否满足条件,这样优化之后复杂度明显改善,但最后还是超时了,到了第95个test,但是这只是一个最直接的处理,算法的优化是无尽的,很重要的一点就是减少冗余,这样便除去了大量的重复过程,如果每个处理的过程不是常数时间的话,那么这个优化对于时间复杂度的改善是明显的!!!回到此题,明显对于某一个数字d,如果搜索完所有可以到达的格子仍然后不符合条件的话,那么这个过程所有与d相等的格子无需在进行搜索处理,标记即可,因为这些格子也必定不符合条件。

 

[cpp] view plain copy
  1. #include <cmath>  
  2. #include <cstring>  
  3. #include <algorithm>  
  4. #include <cstdio>  
  5. using namespace std;  
  6.   
  7. typedef long long LL;  
  8. const int N = 1010;  
  9. LL k, di, tab[N][N];  
  10. int n, m, flag = 1, tot, cnt, sum;  
  11. int vis[N][N], v[N][N];  
  12. int dx[] = {1, -1, 0, 0}, dy[] = {0, 0, 1, -1};  
  13.   
  14. void DFS(int x, int y) {  
  15.     if (tab[x][y] == di) v[x][y] = 1;//标记数字相等的格子  
  16.     vis[x][y] = sum;  
  17.     if (++cnt == tot) { flag = 0; return ; }  
  18.     if (flag) {  
  19.         for (int i = 0; i < 4 && flag; i++) {  
  20.             int nx = x + dx[i], ny = y + dy[i];  
  21.             if (nx < 1 || nx > n || ny < 1 || ny > m || vis[nx][ny] == sum) continue;  
  22.             if (tab[nx][ny] >= di) DFS(nx, ny);  
  23.         }  
  24.     }  
  25. }  
  26.   
  27. int main() {  
  28.     scanf("%d %d %I64d", &n, &m, &k);  
  29.     for (int i = 1; i <= n; i++)  
  30.         for (int j = 1; j <= m; j++) scanf("%I64d", &tab[i][j]);  
  31.     LL t = k/n/m;  
  32.     for (int i = 1; i <= n && flag; i++) {  
  33.         for (int j = 1; j <= m && flag; j++) {  
  34.             di = tab[i][j];  
  35.             if (k % di == 0 && di >= t && !v[i][j]) {  
  36.                 cnt = 0; sum++;  
  37.                 tot = k/tab[i][j];  
  38.                 DFS(i, j);  
  39.             }  
  40.         }  
  41.     }  
  42.     if (flag) puts("NO");  
  43.     else {  
  44.         puts("YES");  
  45.         for (int i = 1; i <= n; i++) {  
  46.             for (int j = 1; j <= m; j++) printf("%I64d ", vis[i][j] == sum ? di : 0);  
  47.             puts("");  
  48.         }  
  49.     }  
  50.     return 0;  
  51. }  

2、HDU5510-Bazinga

点击打开链接

题意:给定n个字符串,分别s1、s2…..sn,找出最大的i,使得存在一个j(1<=j<i)使得sj不是si的子串。

分析:题目数据量很大,最多500个字符串,而且长度可以达到2000,然后最后50个样例,估算下可知就算是使用kmp算法,直接暴力也必定超时,但是这种题没有什么其他的高效解法,只能暴力,那么我们需要从优化算法的角度入手。其实只要稍加优化就能过了。

如果逆着枚举所有i的话,那么无法优化,最坏情况下还是会枚举完所有的i,肯定也会超时,顺着枚举i然后减少重复的判断就可以了。可以知道,如果一个串i是另一个串j的子串的话,那么在枚举时对于串i便无需在进行匹配了,只需要与j匹配即可,这题出题人肯定也是考察这一点,想想看,对于串的匹配,通常的算法是o(n^2),对于串i,如果前面的串中很多都是i的子串的如果不用判断的话会减少大量的时间消耗大的重复判断过程。

 不过值得一提的是使用strstr,或者search居然会比kmp快了近一倍,虽说一般这两个函数是平方的,但在实际应用中一般比kmp快,但是这种大量的匹配过程为何还会快且快这么多呢?

[cpp] view plain copy
  1. #include <cstdio>  
  2. #include <cstring>  
  3. #include <algorithm>  
  4. using namespace std;  
  5.   
  6. char s[510][2010];  
  7. int main() {  
  8.     int t, k = 0;  
  9.     int vis[510];  
  10.     scanf("%d", &t);  
  11.     while (t--) {  
  12.         int n;  
  13.         scanf("%d", &n);  
  14.         for (int i = 1; i <= n; i++) scanf("%s", s[i]);  
  15.         int ans = -1;  
  16.         memset(vis, 0, sizeof(vis));  
  17.         for (int i = 2; i <= n; i++) {  
  18.             for (int j = 1; j < i; j++) {  
  19.                 if (!vis[j]) {  
  20.                     int leni = strlen(s[i]);  
  21.                     if (search(s[i], s[i]+leni, s[j], s[j]+strlen(s[j])) == s[i]+leni) { ans = i; break; }  
  22.                     else vis[j] = 1;  
  23.                 }  
  24.             }  
  25.         }  
  26.         printf("Case #%d: %d\n", ++k, ans);  
  27.     }  
  28.     return 0;  
  29. }  

(2)、减少计算量

1、uva10976- Fractions Again?!

点击打开链接

题意:给定正整数k,找到所有的正整数x>=y使得1/k=1/x+1/y,并且输出所有的式子。

分析:暴力枚举即可,但是枚举量多大呢?如果枚举x,推导可知最优也要从k*(k+1)枚举到2*k,太大了,明显不可取,但是如果从枚举y出发,可以知道y<=2*k,枚举量大大减小,效率足够高了。

[cpp] view plain copy
  1. #include <cstdio>  
  2. #include <algorithm>  
  3. #include <cstring>  
  4. #include <utility>  
  5. using namespace std;  
  6.   
  7. typedef pair<intint> p;  
  8. p ans[10000];  
  9.   
  10. int main()  
  11. {  
  12.     int n;  
  13.     while (~scanf("%d", &n)){  
  14.         int k = 0;  
  15.         for (int i = 2*n; i >= n+1; i--){  
  16.             int j = n*i/(i-n);  
  17.             if (j*(i-n) == n*i && j >= 2*n && j <= n*(n+1)) ans[k++] = p(j, i);  
  18.         }  
  19.         printf("%d\n", k);  
  20.         for (int i = k-1; i >= 0; i--) printf("1/%d = 1/%d + 1/%d\n", n, ans[i].first, ans[i].second);  
  21.     }  
  22.     return 0;  
  23. }  

 

2、Hdu5128 - The E-pangPalace

点击打开链接

题意:给定n个点,要求从这n个点当中选择8个点作为顶点来构成2个不相交(不能有任何一个点公共)的矩形,如果无法选出两个这样的矩形的话输出imp,否则输出这两个矩形的最大的面积和。

分析:最容易想到的就是预处理出所有的矩形,假设有n个矩形,之后n*n跑一遍,但这样可能达到C(30,4)*C(30,4)的复杂度,会超时。其实没必要,类似于折半枚举(双向搜索)的思想,只要枚举矩形的两个对角点即两个点就行了,然后判断另外两个点是否存在即可。所以枚举量最多只有C(30,4)了。而且判断两个矩形是否相交的话我们不需要分类出所有的情况,从反面考虑,如果一个矩形在另一个矩形的上方或者左方或者下方或者右方的话就说明不相交了。而且另外一点是,对于一个矩形,我们只需要枚举一次对角顶点的组合,另外一个组合没必要枚举,这样减少了很多不必要的枚举。而且对于选出的4个点,按照x从左从左至右排列假设为a,b,c,d四个点的话,只有3种组合方式(a,b)与(c,d),(a,c)与(b,d),(a,d)与(b,c),全部枚举一下就好了。为方便判断,可以事先给坐标排序,之后枚举所有大小为4的子集,并且每次对3种组合方式判断一下取最大值。

这题的一个坑点:一个矩形可能完全包含在另一个矩形中,这种情况下面积取较大者,否则取和。

暴力枚举不失为一种好办法,但是过多的枚举必然会导致效率低下了。所以下手之前可以分析下,看是否可以减少重复的没必要的枚举,而且有时候减少这些枚举还可以保证正确率,就像这题如果枚举矩形的两个对角的组合的话可能会导致判断不全面导致计算错误,之前做的时候就是这样,发现答案变大了,原因是因为考虑不全面,但是其实是没必要的。而且折半枚举也是一种重要的思想,因为很多情况下只需要枚举一部分量,然后另一部分量直接判断或者进行查找就好了。 

  

[cpp] view plain copy
  1. #include <cstdio>    
  2. #include <algorithm>    
  3. #include <cstring>    
  4. #define LL long long    
  5. #define INF 0x3f3f3f3f    
  6. using namespace std;    
  7.     
  8. struct po{    
  9.     int x, y;    
  10.     bool operator < (const po& a) const{    
  11.         return x == a.x ? y < a.y : x < a.x;    
  12.     }    
  13. }p[50];    
  14. int vis[210][210];    
  15.     
  16. int check(int i, int j, int k, int l)    
  17. {    
  18.     int ix = p[i].x, iy = p[i].y, jx = p[j].x, jy = p[j].y;    
  19.     int kx = p[k].x, ky = p[k].y, lx = p[l].x, ly = p[l].y;    
  20.     if (ix == jx || iy >= jy || kx == lx || ky >= ly) return 0;    
  21.     if (!vis[ix][jy] || !vis[jx][iy] || !vis[kx][ly] || !vis[lx][ky]) return 0;    
  22.     if (jx < kx || ix > lx || jy < ky || iy > ly) return (jx - ix) * (jy - iy) + (lx - kx) * (ly - ky);    
  23.     if (ix < kx && kx < jx && ix < lx && lx < jx && iy < ky && ky < jy && iy < ly && ly < jy) return (jx - ix) * (jy - iy);    
  24.     return 0;    
  25. }    
  26. int solve(int i, int j, int k, int l)    
  27. {    
  28.     return max(check(i, j, k, l), max(check(i, k, j, l), check(i, l, j, k)));    
  29. }    
  30. int main()    
  31. {    
  32.     int n;    
  33.     while (scanf("%d", &n), n){    
  34.         memset(vis, 0, sizeof(vis));    
  35.         for (int i = 0; i < n; i++) scanf("%d %d", &p[i].x, &p[i].y), vis[p[i].x][p[i].y] = 1;    
  36.         if (n < 8) { puts("imp"); continue; }    
  37.         sort(p, p + n);    
  38.         int k = (1 << 4) - 1;    
  39.         int ans = 0;    
  40.         while (k < 1 << n){    
  41.             int t[4];    
  42.             for (int i = 0, j = 0; i < n; i++)    
  43.                 if (k & (1 << i)) t[j++] = i;    
  44.             ans = max(ans, solve(t[0], t[1], t[2], t[3]));    
  45.             int x = k & -k, y = k + x;    
  46.             k = ((k & ~y) / x >> 1) | y;    
  47.         }    
  48.         ans ? printf("%d\n", ans) : puts("imp");    
  49.     }    
  50.     return 0;    
  51. }    

(3)、优化计算过程

在此举一个数据结构进行算法的优化的例子:

1、codeforces675E  Trains and Statistic

 题意:一个城市有n个车站,编号从1到n,在第i号车站只能买到第i+1号到ai号车站的票(ai>=i+1),令P(i,j)为从第i号车站去第j号车站所需要买的最少的票的数量,给定ai,求所有的P(i,j)之和(1<=i<j<=n).

分析:看起来像图论题,但是无法存储,考虑其他方法。无后效性,明显的动态规划,令dp[i]为从i号车站去之后i+1到n这些车站的总票数最小数量,可以知道需要逆推,状态转移即为dp[i] = dp[pos]+n-i-(a[i]-pos),pos为i+1到a[i]这些车站中a[pos]最大的一个编号,知道了状态转移还不行,因为需要有大量计算区间最大值的过程,使用线段树优化即可。

[cpp] view plain copy
  1. #include <cstdio>  
  2. #include <algorithm>  
  3. #include <cstring>  
  4. using namespace std;  
  5.   
  6. typedef long long LL;  
  7. const int N = 100010;  
  8. int id[N<<2], a[N];  
  9. LL dp[N], ans;  
  10.   
  11. void update(int root) {  
  12.     if (a[id[root<<1]] > a[id[root<<1|1]]) id[root] = id[root<<1];  
  13.     else id[root] = id[root<<1|1];  
  14. }  
  15. void build(int root, int l, int r) {  
  16.     if (l == r) { id[root] = l; return ; }  
  17.     int t = (r-l) / 2;  
  18.     build(root<<1, l, l + t);  
  19.     build(root<<1|1, l + t+1, r);  
  20.     update(root);  
  21. }  
  22. int query(int l, int r, int L, int R, int root) {  
  23.     if (l > R || r < L) return -1;  
  24.     if (l <= L && R <= r) return id[root];  
  25.     int idls, idrs, t = (R-L) / 2;  
  26.     idls = query(l, r, L, L + t, root<<1);  
  27.     idrs = query(l, r, L + t+1, R, root<<1|1);  
  28.     if (idls == -1) return idrs;  
  29.     if (idrs == -1) return idls;  
  30.     if (a[idls] > a[idrs]) return idls;  
  31.     return idrs;  
  32. }  
  33. int main() {  
  34.     int n;  
  35.     scanf("%d", &n);  
  36.     for (int i = 1; i < n; i++) scanf("%d", a+i);  
  37.     a[n] = n; ans = dp[n-1] = 1;  
  38.     build(1, 1, n);  
  39.     for (int i = n-2; i >= 1; i--) {  
  40.         int pos = query(i+1, a[i], 1, n, 1);  
  41.         dp[i] = dp[pos]  + n-i - a[i]-pos;  
  42.         ans += dp[i];  
  43.     }  
  44.     printf("%I64d\n", ans);  
  45.     return 0;  
  46. }  


算法优化的总结:

程序时间复杂度过高,除了换更优的算法之外,优化当前的算法也是很重要的。就像上面说明的那样:减少当前计算过程中的冗余计算,比如说标记,删除、从减少计算量出发优化,比如说部分枚举,折半枚举、或者优化某个 计算的过程,比如说数据结构优化。算法优化思想或者技巧需要知识的积累和对算法过程进行的仔细分析,难以总结全面。要想熟练运用,还得不断地锻炼和总结。

3、 STL的高效运用:

不管你是一个ACM竞赛者还是一个开发程序员,STL都会是一个非常好的工具。如果能够充分地利用STL,可以在代码空间、执行时间和编码效率上获得极大的好处,代码不仅高效简洁而且看起来特别舒服。


首先粗略介绍下STL:

STL 大致可以分为三大类:算法(algorithm)、容器(Container)、迭代器(iterator)。

STL 容器是一些模板类,提供了多种组织数据的常用方法,例如vector(向量,类似于数组)、list(列表,类似于链表)、deque(双向队列)、set(集合)、map(映射)、stack(栈)queue(队列)、priority_queue(优先队列)等,

通过模板的参数我们可以指定容器中的元素类型。

STL 算法是一些模板函数,提供了相当多的有用算法和操作,从简单如for_each(遍历)到复杂如stable_sort(稳定排序)。STL 迭代器是对C 中的指针的一般化,用来将算法和容器联系起来。几乎所有的STL 算法都是通过迭代器来存取元素序列进行工作的,而STL 中的每一个容器也都定义了其本身所专有的迭代器,用以存取容器中的元素。有趣的是,普通的指针也可以像迭代器一样工作。熟悉了STL 后,你会发现,很多功能只需要用短短的几行就可以实现了。通过STL,我们可以构造出优雅而且高效的代码,甚至比你自己手工实现的代码效果还要好。

STL 的另外一个特点是,它是以源码方式免费提供的,程序员不仅可以自由地使用这些代码,也可以学习其源码,甚至按照自己的需要去修改它。

在这里首先例举ACM经常需要用到的STL容器或算法:

STl容器或适配器等:pair、list、vector、priority_queue、set、map、stack

常用STL 算法库:sort快速排序算法、二分查找算法、枚举排列算法 .


在这里只介绍下应用,如果想知道这个容器的具体操作可以去看书或者找度娘,这里不详细介绍操作了.

①、容器或适配器部分:

1、  pair

相当于一个Struct,访问方式举个例子:pair<int,int> p; 那么第一个成员便是p.first、第二个p.second,pair使用起来很方便,简单快速高效,可以当做一个二元struct使用,而且它定义了比较的方法,先根据第一个成员比较,在根据第二个,所以如果你的比较运算符是这样,那么你就不需要定义比较函数了,而struct是不能直接进行比较的,构造pair的方法:make_pair。

下面是一个简单的例子:

[cpp] view plain copy
  1. #include <cstdio>  
  2. #include <algorithm>  
  3. #include <cstring>  
  4. #include <utility>  
  5. #include <functional>  
  6. using namespace std;  
  7.   
  8. const int N = 1010;  
  9. typedef pair<intint> p;  
  10. p a[N];  
  11.   
  12. int main() {  
  13.     int k = 0;  
  14.     a[k++] = p(3, 4);  
  15.     a[k++] = p(3, 100);  
  16.     a[k++] = p(1, 2);  
  17.     a[k++] = p(4, 10);  
  18.     //首先按第一个成员从大到小排列,当第一个成员相等时,按第二个成员second从大到小排列  
  19.     sort(a, a+k, greater<p>());  
  20.     for (int i = 0; i < k; i++) printf("%d %d\n", a[i].first, a[i].second);  
  21.     return 0;  
  22. }  

2、List

list是一个循环链表,其实我并不经常使用。关键在于这个容器的特点:快速插入和删除。作用和vector差不多,但内部是用链表实现。

这个容器不支持随机访问,你不能[]或者利用通用算法操作,比如说要排序的话你只能利用成员函数比如list.sort(),而且很重要的一点,list的size()函数是线性的,因为是以遍历函数distance实现的。


下面的这个题便可以用list的O(1)插入和删除特性暴力解决,简直是神技般的存在!

Hdu5127-Dogs' Candies

点击打开链接

题意:一个以两个数字p,q作为单个元素的序列,支持三种操作:

1、 从序列中去掉包含p,q的这两个数字的元素。

2、向序列中添加包含p,q的这两个数字的元素。

3、对于给定的x和y,找到一对p,q,使得px+qy最大,输出这个最大值。

 

分析:这题虽然数据量比较大,但是却可以暴力解决,据说现场赛时只有10个队伍过了此题,正解很麻烦,cdq分治+凸包,然而我全都不会…..首先抓住这题的一个特性,题目可能存在大量的插入和删除操作,而且对于寻找最大值的操作,只能循环遍历一次,不存在贪心的选取策略能使得枚举量减少。如果采取不删除,而直接进行标记的策略便会超时,因为这样会使得遍历的时间增加。

根据以上的分析,抓住题目的特征,选取合适的方法,选用list作为容器来存储。运行时间不到时限的一半,简直神奇,如果选取其他的容器也都会超时。也许这就是传说中的暴力姿势比较好吧….

[cpp] view plain copy
  1. #include <cstdio>  
  2. #include <algorithm>  
  3. #include <cstring>  
  4. #include <list>  
  5. #include <utility>  
  6. using namespace std;  
  7.   
  8. typedef long long LL;  
  9. typedef pair<LL, LL> p;  
  10. list<p> l;  
  11.   
  12. int main() {  
  13.     int n;  
  14.     while (scanf("%d", &n), n) {  
  15.         l.clear();  
  16.         for (int i = 0; i < n; i++) {  
  17.             LL a, b;  
  18.             int t;  
  19.             scanf("%d %I64d %I64d", &t, &a, &b);  
  20.             if (t == 1) l.push_back(p(a, b));  
  21.             else if (t == -1) l.erase(find(l.begin(), l.end(), p(a, b)));  
  22.             else {  
  23.                 list<p>::iterator i = l.begin();  
  24.                 LL ans = i->first * a + i->second * b;  
  25.                 for (++i; i != l.end(); i++) ans = max(ans, i->first * a + i->second * b);  
  26.                 printf("%I64d\n", ans);  
  27.             }  
  28.         }  
  29.     }  
  30.     return 0;  
  31. }  


3、vector

可以说这是一个STL的神器,相当于一个不定长数组,利用这个你可以添加任意多的元素,容器以连续数组的方式存储元素序列,可以将vector 看作是以顺序结构实现的线性表。当我们在程序中需要使用动态数组时,vector将是一个理想的选择。这个完全相当于把一个数组当成一个元素来进行使用了,可以直接相等,赋值操作等。

在这里介绍几个比较经典的使用:

1、利用vector防止递归爆栈。2、利用vector来实现邻接表(通常是vector<int>G[N],但这种方式可能会因为存储量巨大然后vector的容量扩展导致TLE)3、利用vector存储空间占用比较大的矩阵。

vector高效的原因在于配置了比其所容纳的元素更多的内存,但其内存重新配置会花很多时间,(在这里顺便说下,像new和malloc等函数要慎用,这属于用时间开销代价换取空间的举措,非常容易TLE),vector确实非常好用,合理使用实则帮助很大。

Vector算是应用得最多的一个stl容器了,例子太多了,在此就不举了.....

 

4、priority_queue:

这个也可以说是一个神器,优先队列其实就是堆,在优先队列中,元素被赋予优先级。当访问元素时,具有最高优先级的元素最先被删除。优先队列具有最高级先出(first in, largest out)的行为特征。在这里说下这个怎么进行重载的问题。

两种方式:

一、直接在struct或者class内部定义,那么你定义时直接指明元素类型就好了,因为你是定义了比较函数的。二、定义比较结构。

[cpp] view plain copy
  1. //内部定义:  
  2. struct node{  
  3.    int x, y;  
  4.    node(int x = 0, int y = 0) : x(x), y(y) {}  
  5.    bool operator < (const node &a) const { return x > a.x; }  
  6. };  
  7. priority_queue<node> q;  
  8. //定义比较结构  
  9. struct node{  
  10.    int x, y;  
  11.    node(int x = 0, int y = 0) : x(x), y(y) {}  
  12. };  
  13. struct cmp {  
  14.    bool operator () (const node &a, const node &b) { return a.x< b.x; }  
  15. };  
  16. priority_queue<node, vector<node>,cmp> q;  

具体点就是可以用来贪心或者加速搜索,下面是对应的两个题:

(1)、贪心


优先队列最常用的就是贪心,举个例子。

poj2010-Moo University - Financial Aid     

 点击打开链接

题意:有c只奶牛,现在要招募n只进动物学校,第i只奶牛的分数为si,需要的资金帮助为ai,现在有总共为f的资金,问招募到的n只奶牛中中位数最大是多少?

分析:采取贪心策略,尽量选择大的,首先按分数从小到大排个序,之后从大到小枚举中位数,所以我们要知道当前分数作为中位数下,需要的资金总和最小是多少。令l[i]表示选取第i+1号奶牛作为中位数时分数比它小的n/2头奶牛需要的资金帮助的最小值,令r[i]表示选取第i-1号奶牛作为中位数时分数比它大的n/2头奶牛需要的资金帮助的最小值。遍历时用优先队列存储当前的分数,贪心地换掉需要的资金多的奶牛,一次预处理r[i]和l[i]即可,总的时间复杂度为O(nlogn)

[cpp] view plain copy
  1. #include <cstdio>  
  2. #include <algorithm>  
  3. #include <cstring>  
  4. #include <queue>  
  5. using namespace std;  
  6.   
  7. const int N = 100010;  
  8. int l[N], r[N];  
  9. struct calf {  
  10.     int s, a;  
  11. }ca[N];  
  12. bool cmp(calf x, calf y) { return x.s < y.s; }  
  13.   
  14. int main() {  
  15.     int n, c, f;  
  16.     scanf("%d %d %d", &n, &c, &f);  
  17.     for (int i = 1; i <= c; i++) scanf("%d %d", &ca[i].s, &ca[i].a);  
  18.     sort(ca+1, ca+1+c, cmp);  
  19.     n >>= 1;  
  20.     priority_queue <int> q;  
  21.     int sum = 0;  
  22.     for (int i = 1; i <= n; i++) q.push(ca[i].a), sum += ca[i].a;  
  23.     l[n] = sum;  
  24.     for (int i = n+1; i <= c-n-1; i++) {  
  25.         if (ca[i].a >= q.top()) l[i] = sum;  
  26.         else {  
  27.             sum -= q.top() - ca[i].a;  
  28.             q.pop(); q.push(ca[i].a);  
  29.             l[i] = sum;  
  30.         }  
  31.     }  
  32.     sum = 0;  
  33.     while (!q.empty()) q.pop();  
  34.     for (int i = c; i >= c-n+1; i--) q.push(ca[i].a), sum += ca[i].a;  
  35.     r[c-n+1] = sum;  
  36.     for (int i = c-n; i >= n+2; i--) {  
  37.         if (ca[i].a >= q.top()) r[i] = sum;  
  38.         else {  
  39.             sum -= q.top() - ca[i].a;  
  40.             q.pop(); q.push(ca[i].a);  
  41.             r[i] = sum;  
  42.         }  
  43.     }  
  44.     int ans = -1;  
  45.     for (int i = c-n; i >= n+1; i--) {  
  46.         if (r[i+1] + l[i-1] + ca[i].a <= f) {  
  47.             ans = ca[i].s;  
  48.             break;  
  49.         }  
  50.     }  
  51.     printf("%d\n", ans);  
  52.     return 0;  
  53. }  

(2)、加速搜索

csu1780 - 简单的图论问题

点击打开链接

这题很明显属于搜索题,bfs广搜即可,但是需要找到权值最小的路径的话,那么我们需要使用优先队列来加速搜索和求解答案。每一步优先选择权值小的结点来拓展状态,且结点中记录上一次扩展状态时的方向,而且给一个点标记去重的需要考虑四个方向的状态。

[cpp] view plain copy
  1. #include <cstdio>    
  2. #include <cstring>    
  3. #include <algorithm>    
  4. #include <vector>    
  5. #include <utility>    
  6. #include <queue>    
  7. #define INF 0x3f3f3f3f    
  8. #define LL long long    
  9.     
  10. using namespace std;    
  11. struct po{    
  12.     int x, y, w, di;    
  13.     bool operator > (const po &a)const {return w > a.w;}    
  14. };    
  15. int n, m, vis[505][505], v[505][505][4];    
  16. int maze[510][510], r1, c1, r2, c2, t;    
  17. char st[5];    
  18. int dx[] = {1, -1, 0, 0}, dy[] = {0, 0, 1, -1};    
  19. int bfs()    
  20. {    
  21.     priority_queue <po, vector<po>, greater<po> > q;    
  22.     q.push((po){r1, c1, maze[r1][c1], 0});    
  23.     memset(vis, 0, sizeof(vis));    
  24.     vis[r1][c1] = 1;    
  25.     while (!q.empty()){    
  26.         po t = q.top(); q.pop();    
  27.         if (t.x==r2 && t.y==c2) return t.w;    
  28.         for (int i = 0; i < 4; i++){    
  29.             int nx = t.x+dx[i], ny = t.y+dy[i];    
  30.             if (nx<1 || nx>n || ny<1 || ny>m || vis[nx][ny] || maze[nx][ny]==-1) continue;    
  31.             q.push((po){nx, ny, t.w+maze[nx][ny], 0}); vis[nx][ny] = 1;    
  32.         }    
  33.     }    
  34.     return -1;    
  35. }    
  36.     
  37. int bfs1()    
  38. {    
  39.     memset(v, 0, sizeof(v));    
  40.     priority_queue <po, vector<po>, greater<po> > q;    
  41.     q.push((po){r1, c1, maze[r1][c1], -1});    
  42.     v[r1][c1][0] = v[r1][c1][1] = v[r1][c1][2] = v[r1][c1][3] = 1;    
  43.     while(!q.empty()){    
  44.         po t = q.top(); q.pop();    
  45.         if (t.x==r2 && t.y==c2) return t.w;    
  46.         for(int i = 0;i < 4;i ++){    
  47.             if(i == t.di) continue;    
  48.             int nx = t.x+dx[i], ny = t.y+dy[i];    
  49.             if(nx<1 || nx>n || ny<1 || ny>m || v[nx][ny][i] || maze[nx][ny]==-1) continue;    
  50.             q.push((po){nx, ny, t.w+maze[nx][ny], i}); v[nx][ny][i] = 1;    
  51.         }    
  52.     }    
  53.     return -1;    
  54. }    
  55.     
  56. int main()    
  57. {    
  58.     while (~scanf("%d %d %d %d %d %d", &n, &m, &r1, &c1, &r2, &c2)){    
  59.         memset(maze, -1, sizeof(maze));    
  60.         for (int i = 1; i <= n; ++i)    
  61.             for (int j = 1; j <= m; ++j){    
  62.                 scanf("%s", st);    
  63.                 if (st[0] != '*') sscanf(st, "%d", &maze[i][j]);    
  64.             }    
  65.         printf("Case %d: %d %d\n", ++t, bfs(), bfs1());    
  66.     }    
  67.     return 0;    
  68. }    

5、set:

set,顾名思义,集合,无重复元素,插入的元素自动按增序排列。
内部实现: 红黑树,一种平衡的二叉排序树,其Compare 函数,类似于qsort 函数里的那个Compare 函数,作为红黑树在内部实现的比较方式。
在这里说下一些成员函数的时间复杂度:
insert() O(logn)、erase()O(logn)、find()O(logn)
lower_bound()O(logn) 查找第一个不小于k 的元素
upper_bound()O(logn) 查找第一个大于k 的元素
equal_range()O(logn) 以pair形式返回lower_bound和upper_bound,在需要确定一个增序序列中某一段都是某一值的情况下是非常有效的,这比通用算法更有效。
容器最主要的功能就是判重,也可以进行二分查找。
要允许重复元素,选用multiset即可。
容器性能:set与map的查找、删除、插入性能都是对数复杂度。

没有定义比较方式的元素需要进行重载,重载方式和优先队列一样。


判重是set的一个最常见的使用。

Uva11572- Unique Snowflakes

点击打开链接

题意:给定一个长度为n序列A,找到一个最长的连续子序列,使得这个序列中没有相同的元素。

分析:经典的O(n)尺取法,实现过程中需要判断当前的右端点元素是否存在,利用set判断即可。s.find(x) != s.end()和s.count(x)都可以。

[cpp] view plain copy
  1. #include <cstdio>  
  2. #include <algorithm>  
  3. #include <cstring>  
  4. #include <set>  
  5.   
  6. using namespace std;  
  7. int a[1000001];  
  8. set<int> s;  
  9.   
  10. int main() {  
  11.     int t, n;  
  12.     scanf("%d", &t);  
  13.     while (t--) {  
  14.         scanf("%d", &n);  
  15.         for (int i = 0; i < n; i++) scanf("%d", a+i);  
  16.         s.clear();  
  17.         int st = 0, en = 0, ma = 0;  
  18.         while (en < n) {  
  19.             while (en < n && !s.count(a[en])) s.insert(a[en++]);  
  20.             ma = max(ma, en-st);  
  21.             s.erase(a[st++]);  
  22.         }  
  23.         printf("%d\n", ma);  
  24.     }  
  25.     return 0;  
  26. }  

6、map

这个容器也非常好,和set差不多,pair 组成的红黑树(所以比较方式和pair一样),成员函数复杂度和前者一样,在这里就不赘述了,容器适用于那些需要进行映射(可以根据Key找到对应的Value,作为hash还是不错的),因此map是关联数组。

要允许重复元素,使用multimap。

在这里指出:在C++11里,有unoder和hash的map和set,里面的元素没有顺序.速度比普通的set和map快了很多,但是需要自己重载hash

map最常见的应用就是进行映射。

hdu4460 - Friend Chains

点击打开链接

题意:给定n个人的名字和这n个人之间的朋友关系,间接相连也算是朋友,如果两个人之间通过n个人相连,那么这条朋友链的长度即为n-1.求一个最小的k,使得对任意两个人,他们之间的朋友链的长度不会超过k。

分析:明显是求任意两点之间的最短路,把那个最大的距离找到即可,但是给定的是名字,需要进行映射,用map映射为标号即可。而且这题的边的权值为1,不需要用优先队列,用普通队列即可。值得注意的是,不需要进行n次bfs求取最短路,只需要进行两次。首先选取一个点x进行最短路的求解,之后找到离x距离最大的那个点y在进行一次最短路的求解即可。因为第一次最短路肯定会求得整个图中离图的重心点最远的那个点,也就是说处于整个图的最端点。那么之后从这个点出发求得的最短路距离必定是整个图中任意两点之间距离的最大值。

[cpp] view plain copy
  1. #include <cstdio>  
  2. #include <cstring>  
  3. #include <string>  
  4. #include <vector>  
  5. #include <algorithm>  
  6. #include <map>  
  7. #include <queue>  
  8. using namespace std;  
  9.   
  10. const int N = 1010;  
  11. int vis[N], d[N];  
  12. map <string, int> mp;  
  13. vector<int> G[N];  
  14.   
  15. int solve(int x, int n) {  
  16.     int ma = 0, res, cnt = 1;  
  17.     queue<int> q; q.push(x);  
  18.     memset(vis+1, 0, sizeof(int) * (n+1));  
  19.     memset(d+1, 0, sizeof(int) * (n+1));  
  20.     vis[x] = 1;  
  21.     while (!q.empty()) {  
  22.         int t = q.front(); q.pop();  
  23.         for (int i = 0; i < G[t].size(); i++) {  
  24.             int y = G[t][i];  
  25.             if (vis[y]) continue;  
  26.             vis[y] = 1;  
  27.             d[y] = d[t] + 1;  
  28.             if (ma < d[y]) res = y, ma = d[y];  
  29.             q.push(y); cnt++;  
  30.         }  
  31.     }  
  32.     return cnt != n ? -1 : x == 1 ? res: ma;  
  33. }  
  34.   
  35. int main() {  
  36.     int n;  
  37.     while (scanf("%d", &n), n) {  
  38.         mp.clear();  
  39.         for (int i = 1; i <= n; i++) G[i].clear();  
  40.         char s[15], str[15];  
  41.         for (int i = 1; i <= n; i++) scanf("%s", s), mp[s] = i;  
  42.         int m;  
  43.         scanf("%d", &m);  
  44.         for (int i = 1; i <= m; i++) {  
  45.             scanf("%s %s", s, str);  
  46.             G[mp[s]].push_back(mp[str]);  
  47.             G[mp[str]].push_back(mp[s]);  
  48.         }  
  49.         int ans = solve(1, n);  
  50.         ans == -1 ? puts("-1") : printf("%d\n", solve(ans, n));  
  51.     }  
  52.     return 0;  
  53. }  

7、stack:

stack,栈在STL里面它属于容器适配器,对容器的重新包装,后进先出结构。

经典使用:单调栈的实现。

poj2559 - Largest Rectangle in a Histogram

点击打开链接

题意:给定一个柱状图,由n个宽为1,长为hi的矩形从左至右依次排列组成,求里面包含的长方形的最大面积是多少?

分析:如果预处理一次区间最小值,暴力也要O(n^2),无法承受,换一种方式,我们可以枚举每个h[i],求以当前高度作为长度的矩形的最大面积,所以需要做的就是对于固定的h[i],求出左端点L和右端点R,必定有h[L-1]<h[i]和h[R]<h[i],否则总可以更新左右端点,令l[i]为(j<=i且h[j-1]<h[i]的最大的j),r[i]为(j>i且h[j]<h[i]的最小的j). 怎么计算l[i]和r[i]. 思考下就可以知道可以利用栈解决。

考虑计算l[i]的情况,栈中维护的是高度依次减小的标号,也就是如果栈中从上到下的值依次为i,那么i>i+1且h[i]>h[i+1]。对于当前的高度h[i],如果有栈顶的元素h[k]>=h[i],证明左端点可以一直往左移动,不断地取出栈顶元素,如果栈为空了证明可以移动到最左边,也就是l[i]=0,否则h[i]=j+1。之所以利用栈求解,是因为每次都要取得左边第一个高度小的端点和右边第一个高度小的端点。这样符合后进先出的性质,模拟下栈的工作情况便可以明白了。正反两次预处理就可以计算出l和r了,之后遍历一次高度求取最大值。

这样的栈称为单调栈,和单调队列是类似的。

[cpp] view plain copy
  1. #include <cstdio>  
  2. #include <algorithm>  
  3. #include <cstring>  
  4. #include <stack>  
  5. #include <cctype>  
  6. using namespace std;  
  7.   
  8. typedef long long LL;  
  9. const int N = 100010;  
  10. stack<int> s;  
  11. template <class T>  
  12. inline void read(T &res) {  
  13.     char c; res = 0;  
  14.     while (!isdigit(c = getchar()));  
  15.     while (isdigit(c)) res = res * 10 + c - '0', c = getchar();  
  16. }  
  17. int l[N], r[N];  
  18. LL h[N];  
  19.   
  20. int main() {  
  21.     int n;  
  22.     while (read(n), n) {  
  23.         for (int i = 0; i < n; i++) read(h[i]);  
  24.         while (!s.empty()) s.pop();  
  25.         for (int i = 0; i < n; i++) {  
  26.             while (!s.empty() && h[s.top()] >= h[i]) s.pop();  
  27.             l[i] = s.empty() ? 0 : s.top()+1;  
  28.             s.push(i);  
  29.         }  
  30.         while (!s.empty()) s.pop();  
  31.         for (int i = n-1; i >= 0; i--) {  
  32.             while (!s.empty() && h[s.top()] >= h[i]) s.pop();  
  33.             r[i] = s.empty() ? n : s.top();  
  34.             s.push(i);  
  35.         }  
  36.         LL ans = 0;  
  37.         for (int i = 0; i < n; i++) ans = max(ans, (LL)h[i] * (r[i] - l[i]));  
  38.         printf("%I64d\n", ans);  
  39.     }  
  40.     return 0;  
  41. }  

②  、算法库

主要的头文件就是#include <algorithm>

1、 sort排序系列:

sort:对给定区间所有元素进行排序(全排)
stable_sort :对给定区间所有元素进行稳定排序,就是相等的元素位置不变,原来在前面的还在前面。
partial_sort :对给定区间所有元素部分排序,就是找出你指定的数目最小或最大的值放在最前面或最后面,比如说我只要找到1000000个数中最大的五个数,那你用这个函数是最好的,排序后最大的五个数就在最前面的五个位置,其他的元素位置分布不确定。
partial_sort_copy:对给定区间复制并排序,和上面的一样,只是这是指定区间进行复制然后排序的。
nth_element :找出给定区间的某个位置对应的元素,根据比较函数找到第n个最大(小)元素,适用于寻找“第n个元素”。
is_sorted :判断一个区间是否已经排好序(返回bool值判断是否已排序)
partition :使得符合某个条件的元素放在前面,划分区间函数,将 [first, last)中所有满足的元素置于不满足的元素前面,这个函数会返回迭代器,设返回的迭代器为 i,则对 [first, i)中的任意迭代器 j,*j满足给定的判断,对 [i, last) 中的任意迭代器 k,*k不满足。
stable_partition :相对稳定的使得符合某个条件的元素放在前面(和上面的一样,只是位置不变)
这个函数真是绝对的神助,相比qsort没这么麻烦了。使用时根据需要选择合理的排序函数即可,所有的排序函数默认从小到大排序,可以定义自己的比较方式。


sort排序十分常见,在此举个其他排序的例子:

xtu1050-第K个数

Description
给你一个整数序列和若干个问题,问题是这个序列的第i个元素到第j个元素的片断中的第k大数是什么?比如说给你的序列为(1, 5, 2, 6, 3, 7, 4),问题是(2,5,3),则从第2个元素到第5个元素为(5,2,6,3),排序以后是(2,3,5,6),则第三个数是5。

输入:
第一行为两个整数n,m(1 <= n <= 100 000, 1 <= m <= 5 000),表示序列的元素个数和问题的个数,第二行为n个正整数的序列,每个整数为一个32位整数,两个数之间有一个空格隔开。以后m行为问题,每个问题由三个整数表示i,j,k,第i个元素到第j个元素的片断中的第k大数是什么?(元素序号从1开始计数,i<=j,1<=k<=j-i+1)

输出:
每行输出对应问题的结果。

Sample Input
7 3
1 5 2 6 3 7 4
2 5 3
4 4 1
1 7 3

Sample Output
5
6
3

分析:只需要选择第k个数,直接nth_element,线性复杂度暴力水过………而且这题解法很多,比如用线段树等数据结构进行求解会是更优的。

[cpp] view plain copy
  1. #include <cstdio>  
  2. #include <algorithm>  
  3.   
  4. using namespace std;  
  5. int a[100010];  
  6. int b[100010];  
  7.   
  8. int main()  
  9. {  
  10.     int n, t, i, j, k;  
  11.     scanf("%d %d", &n, &t);  
  12.     for (i = 1; i <= n; i++) scanf("%d", &a[i]);  
  13.     while (t--)  
  14.     {  
  15.         copy(a+1, a+n+1, b+1);  
  16.         scanf("%d %d %d", &i, &j, &k);  
  17.         nth_element(b+i, b+i+k-1, b+j+1);  
  18.         printf("%d\n", b[i+k-1]);  
  19.     }  
  20.     return 0;  
  21. }  

2、 二分系列:

二分检索,复杂度O(log(last-first))
itr =upper_bound(first, last, value, cmp);
//itr 指向大于value 的第一个值(或容器末尾)
itr = lower_bound(first, last, value, cmp);
//itr 指向不小于valude 的第一个值(或容器末尾)
pairequal_range(first, last, value, cmp);
//找出等于value 的值的范围O(2*log(last– first))
Binary_search(first,last, value)返回bool值,找到则true,否则false。


二分经常会与其他算法结合,二分的应用是也是十分常见的,在这举个例子说明下找一个数或等于一个数的一个范围的二分应用:

hdu1496-Equations

点击打开链接

题意:给定a,b,c,d四个数,求有多少对x1,x2,x3,x4满a*x1^2+b*x2^2+c*x3^2+d*x4^2=0

分析:暴力会超时,把ax1*x1+bx2*x2打表,利用二分查找,排序之后equal_range寻找等于这个值的范围就行了。

[cpp] view plain copy
  1. #include <iostream>  
  2. #include <algorithm>  
  3. #include <cstring>  
  4.   
  5. using namespace std;  
  6. int val[40010];  
  7.   
  8. int main() {  
  9.     pair <int*, int*> p;  
  10.     int a, b, c, d;  
  11.     while (cin >> a >> b >> c >> d) {  
  12.         if( (a > 0 && b > 0 && c > 0 && d > 0) || (a < 0 && b < 0 && c < 0 && d < 0)){  
  13.             cout << 0 << endl;  
  14.             continue;  
  15.         }  
  16.         memset(val, 0, sizeof(val));  
  17.         int k = 0;  
  18.         for (int i = -100; i <= 100; i++){  
  19.             if (i == 0) continue;  
  20.             for (int j = -100; j <= 100; j++) {  
  21.                 if (j == 0) continue;  
  22.                 val[k++] = a*i*i + b*j*j;  
  23.             }  
  24.         }  
  25.         sort(val, val+k);  
  26.         int cnt = 0;  
  27.         for (int j = -100; j <= 100; j++) {  
  28.             if (j == 0) continue;  
  29.             for (int i = -100; i <= 100; i++) {  
  30.                 if (i == 0) continue;  
  31.                 int sum = c*j*j + d*i*i;  
  32.                 p = equal_range(val, val+k, -sum);  
  33.                 cnt += p.second - p.first;  
  34.             }  
  35.         }  
  36.         cout << cnt << endl;  
  37.     }  
  38.     return 0;  
  39. }  

利用二分查找时间复杂度降到了o(n*nlog(n))。当然这个题目可以hash,这是最好的办法,O(1)查找,复杂度更小了。


3、 排列系列:

这两个函数都可以用来枚举字典序排列,那种水题直接用这个就解决了。

在这里贴个网址,看了下,说的挺好的;        

http://blog.sina.com.cn/s/blog_9f7ea4390101101u.html

 

4、 常用函数:

最后补充下常用函数,//其实是我常用的函数...

copy函数,直接拷贝,比for循环高效,最坏为线性复杂度,而且这个可以说是一个copy族函数,还有类似的满足一定条件的copy函数如copy_if等。

find、find_i:查找第一个匹配的值或第一个满足函数使其为true的值位置,没有返回指定区间的末尾,线性复杂度,还有一些不怎么常用的find族函数就不多介绍了。

count、count_if: 返回匹配或使函数为true的值的个数,线性复杂度。

search,这是寻找序列是否存在于另一个序列中的函数,挺好用的,某些简单的寻找公共子串的题就可以这样写,时间复杂度二次。

reverse,翻转一个区间的值,我经常遇到需要这种题,直接reverse了,不需要for循环了,主要是方便。

for_each:直接对一个区间内的每个元素执行后面的函数操作,写起来简单。

max、min、max_element、min_element:寻找两个数或者一个区间的最大最小值,都可以添加比较函数参数。

集合操作函数:includes、set_union、set_difference、set_intersection、set_symmetric_difference、前面这些函数的最差复杂度为线性,另外附加一个序列的操作函数merge,相当于归并排序中的合并函数,时间复杂度为线性,注意这些函数的操作对象都必须是升序的。

这里举个例子:

[cpp] view plain copy
  1. #include<cstdio>   
  2. #include<algorithm>   
  3. using namespace std;   
  4.   
  5. void out(int a) { if (a != -1) printf("%d ",a); }   
  6. int main() {   
  7.     int a[5] = {1, 8, 10, 52, 100};   
  8.     int b[5] = {6, 8, 9, 10, 1000};   
  9.     int c[20];   
  10.     printf("a集合为:");   
  11.     for_each(a, a+5, out);   
  12.     puts("");   
  13.     printf("b集合为:");   
  14.     for_each(b, b+5, out);   
  15.     puts("");   
  16.       
  17.     //判断b是否是a的子集。   
  18.     if(includes(a, a+5, b, b+5)) printf("bis a sub set of a\n");   
  19.       
  20.     //合并两个有序序列,必须为合并后的序列分配空间,否则程序会崩溃。   
  21.     printf("两个集合的合并序列为:");   
  22.     merge(a, a+5, b, b+5, c);   
  23.     for_each(c, c+10, out);   
  24.     puts("");   
  25.   
  26.     //求两个集合的并集。   
  27.     fill(c, c+20, -1);   
  28.     set_union(a, a+5, b, b+5, c);   
  29.     printf("两个集合的并集为:");   
  30.     for_each(c, c+10, out);   
  31.     puts("");   
  32.   
  33.     //求两个集合的交集。   
  34.     fill(c, c+20, -1);   
  35.     set_intersection(a, a+5, b, b+5, c);   
  36.     printf("两个集合的交集为:");   
  37.     for_each(c, c+10, out);   
  38.     puts("");   
  39.   
  40.     //求两个集合的差集,这里为a-b。   
  41.     fill(c, c+20, -1);   
  42.     set_difference(a, a+5, b, b+5, c);   
  43.     printf("a-b的差集为:");   
  44.     for_each(c, c+10, out);   
  45.     puts("");   
  46.     //求两个集合的差集,这里为b-a。   
  47.     fill(c, c+20, -1);   
  48.     set_difference(b, b+5, a, a+5, c);   
  49.     printf("b-a的差集为:");   
  50.     for_each(c, c+10, out);   
  51.     puts("");   
  52.       
  53.     //求两个集合的对称差   
  54.     set_symmetric_difference(a, a+5, b, b+5,c);   
  55.     printf("两个集合的对称差为:");   
  56.     for_each(c, c+10, out);   
  57.     puts("");   
  58.     return 0;   
  59. }   

下面的这个题便说明了STL的方便与实用:

codeforces730A - Toda2

点击打开链接

题意:有n个人,每个人有一个rating,目的是使得每个人的rating相同而且尽可能的大,每次可以选取2到5个人,使得每个人的rating减1,如果rating已经为0了,那么不减少,输出你的操作过程的每一步,每一步为一个01序列,如果选择了第i个人,那么序列中第i位为1。

分析:模拟题,不过比较麻烦,为方便起见,利用STL实现,思路是每次选取两到三个人进行rating的减小,然后每次选取rating较大的几个人减少就行了,如果r全部相等就可以了。

[cpp] view plain copy
  1. #include <cstdio>  
  2. #include <algorithm>  
  3. #include <cstring>  
  4. #include <set>  
  5. #include <vector>  
  6. #include <string>  
  7. #include <iostream>  
  8. using namespace std;  
  9.   
  10. struct p{  
  11.     int r, id;  
  12.     p(int r = 0, int id = 0) : r(r), id(id) {}  
  13.     bool operator <(const p &x)const { return r > x.r; }  
  14. };  
  15. vector<string> ans;  
  16. multiset<p> s;  
  17.   
  18. int main() {  
  19.     ios::sync_with_stdio(0);  
  20.     int n;  
  21.     cin >> n;  
  22.     for (int i = 0; i < n; i++) {  
  23.         int r;  
  24.         cin >> r;  
  25.         s.insert(p(r, i));  
  26.     }  
  27.     while (s.begin()->r != s.rbegin()->r){  
  28.         string t(n, '0');  
  29.         int num = 2;  
  30.         if(s.count(*s.begin()) == 3) num++;  
  31.         vector<p> tmp;  
  32.         for (int i = 1; i <= num; i++) {  
  33.             p a = *s.begin();  
  34.             s.erase(s.begin());  
  35.             t[a.id] = '1';  
  36.             if (a.r) a.r--;  
  37.             tmp.push_back(a);  
  38.         }  
  39.         s.insert(tmp.begin(), tmp.end());  
  40.         ans.push_back(t);  
  41.     }  
  42.     cout << s.begin()->r << "\n" << ans.size() << "\n";  
  43.     for (int i = 0; i < ans.size(); i++) cout << ans[i] << "\n";  
  44.     return 0;  
  45. }  

③  、STL使用的总结:

STl不失为一种绝佳工具,合理的使用是一种很好的技巧,但是需要指出不要过分依赖于STL,STL里面函数确实多的你几乎每个操作都有函数代替,写起来又简单。但凡是都有利弊,STl不好的一个地方就是如果碰到某些卡时间、卡空间、各种卡的题目,那些函数的效率问题就大打折扣了,一般手写的效率会高。
1、STL的容器如queue还有很多函数都比较慢,在碰到那些数据量大的题目时效率确实不高,很容易超时,poj1426 BFS水题吧,用STl容器queue各种超时,一直想不明白,后面自己写个queue,直接就32ms AC了,通过这个也更加体会到这一点(最致命的一点)
2、STL封装了大量的数据结构和算法,使用起来一定需要注意,STl容器的操作都是建立在迭代器之上的,迭代器的使用也需要注意,不然各种问题,程序崩溃… 
3、STL算法极其多,大致分为搜索、修改、排序、集合与工具等,每一种类中的算法都比较多,有助于我们快速地完成程序设计。
4、使用STL容器时注意使用自带的成员函数要比通用算法高效很多。 
5、STL的算法思想是非常值得借鉴的,你可以去查看以学习。
6、STL也是存在缺点的,没有提供任何泛型的图或树结构,只能自己实现。


终于总结完了~

最后给自己一句话吧:Godhelp those who help themselves.

posted @ 2017-08-10 21:13  Archger  阅读(537)  评论(0编辑  收藏  举报