算法笔记 第4章 入门篇(2) --算法初步 学习笔记
4.1 排序
4.1.1 选择排序
简单选择排序是指,对一个序列A中的元素A[1] ~ A[n],令i从1到n枚举,进行n趟操作,每趟从待排序部分[i,n]中选择最小的元素,令其与待排序部分的第一个元素A[i]进行交换,这样元素A[i]就会与当前有序区间[1,i-1]形成新的有序区间[1,i]。
4.1.2 插入排序
直接插入排序是指,对序列A的n个元素A[1] ~ A[n],令i从2到n枚举,进行n-1趟操作。假设某一趟时,序列A的前i-1个元素A[1] ~ A[i-1]已经有序,而范围[i,n]还未有序,那么该趟从范围[1,i-1]中寻找某个位置j,使得将A[i]插入位置j后,范围[1,i]有序。
4.1.3 排序题与sort函数的应用
直接使用C++中的sort()函数,效率较高
1.如何使用sort()排序
sort函数的使用必须加上头文件#include<algorithm>和using namespace std;
使用方式如下:
sort(首元素地址(必填),尾元素地址的下一个地址(必填),比较函数(非必填))
示例:
#include<cstdio> #include<algorithm> using namespace std; int main(){ int a[6] = {9,4,2,5,6,-1}; sort(a,a+4); for(int i=0;i<6;i++){ printf("%d ",a[i]); } printf("\n"); sort(a,a+6); for(int i=0;i<6;i++){ printf("%d ",a[i]); } return 0; }
sort的第三个可选参数是compare函数,一般写作cmp函数,用来制定排序规则来建立可比性
2.如何实现比较函数cmp
(1)基本数据类型数组的排序
若比较函数不填,则默认按照从小到大的顺序排序。如果想要从大到小来排序,则要使用比较函数cmp来“告诉”sort何时要交换元素。
#include<stdio.h> #include<algorithm> using namespace std; bool cmp(int a,int b){ return a > b; } int main(){ int a[] = {3,1,4,2}; sort(a,a+4,cmp); for(int i=0;i<4;i++){ printf("%d ",a[i]); } return 0; }
PAT A1025 PAT Ranking (25分)
Programming Ability Test (PAT) is organized by the College of Computer Science and Technology of Zhejiang University. Each test is supposed to run simultaneously in several places, and the ranklists will be merged immediately after the test. Now it is your job to write a program to correctly merge all the ranklists and generate the final rank.
Input Specification:
Each input file contains one test case. For each case, the first line contains a positive number N (≤), the number of test locations. Then N ranklists follow, each starts with a line containing a positive integer K (≤), the number of testees, and then K lines containing the registration number (a 13-digit number) and the total score of each testee. All the numbers in a line are separated by a space.
Output Specification:
For each test case, first print in one line the total number of testees. Then print the final ranklist in the following format:
registration_number final_rank location_number local_rank
The locations are numbered from 1 to N. The output must be sorted in nondecreasing order of the final ranks. The testees with the same score must have the same rank, and the output must be sorted in nondecreasing order of their registration numbers.
Sample Input:
2
5
1234567890001 95
1234567890005 100
1234567890003 95
1234567890002 77
1234567890004 85
4
1234567890013 65
1234567890011 25
1234567890014 100
1234567890012 85
Sample Output:
9
1234567890005 1 1 1
1234567890014 1 2 1
1234567890001 3 1 2
1234567890003 3 1 2
1234567890004 5 1 4
1234567890012 5 2 2
1234567890002 7 1 5
1234567890013 8 2 3
1234567890011 9 2 4
#include<cstdio> #include<cstring> #include<algorithm> using namespace std; struct student{ char id[20]; int final_rank; //最终排名 int location_number; //考场号 int local_rank; //考场排名 int score; }stu[30010]; bool cmp(student a,student b){ if(a.score != b.score) return a.score > b.score; else return strcmp(a.id,b.id)<0; } int main(){ int n; scanf("%d",&n); int count = 0; for(int i=1;i<=n;i++){ int num; scanf("%d",&num); for(int j=0;j<num;j++){ scanf("%s %d",stu[count].id,&stu[count].score); stu[count].location_number = i; count++; } sort(stu+count-num,stu+count,cmp); int r=1; stu[count-num].local_rank = r; for(int j=count-num+1;j<count;j++){ if(stu[j].score == stu[j-1].score){ stu[j].local_rank = stu[j-1].local_rank; r++; }else{ r++; stu[j].local_rank = r; } } } sort(stu,stu+count,cmp); int rank = 1; stu[0].final_rank = 1; for(int i=1;i<count;i++){ if(stu[i].score == stu[i-1].score){ stu[i].final_rank = stu[i-1].final_rank; rank++; }else{ rank++; stu[i].final_rank = rank; } } printf("%d\n",count); for(int i=0;i<count;i++){ printf("%s %d %d %d\n",stu[i].id,stu[i].final_rank,stu[i].location_number,stu[i].local_rank); } return 0; }
4.2 散列
一般来说,散列可以浓缩成一句话“将元素通过一个函数转换为整数,使得该整数可以尽量唯一地代表这个元素”。其中把这个转换函数称为散列函数H,也就是说,如果元素在转换前为key,那么转换后就是一个整数H(key)。
4.3 递归
4.3.1 分治
分治的全称为”分而治之“,也就是说,分治法将原问题划分成若干个规模较小而结构与原问题相同或相似的子问题,然后分别解决这些子问题,最后合并子问题的解,即可得到为原问题的解。
分治法的三个步骤:
①分解:将原问题分解为若干和原问题拥有相同或相似结构的子问题。
②解决:递归求解所有子问题。如果存在子问题的规模小到可以直接解决,就直接解决它。
③合并:将子问题的解合并为原问题的解。
4.3.2 递归
递归就在于反复调用自身函数,但是每次把问题范围缩小,直到范围缩小到可以直接得到边界数据的结果,然后再在返回的路上求出对应的解。
经典例子:使用递归求解n的阶乘。
n! = 1*2*...*n,这个式子写成递推的形式就是n! = (n-1)! * n
如果用F(n)表示n!,就可以写成F(n) = F(n-1) * n,由于0! = 1,因此不妨以F(0) = 1作为递归边界,即当规模减小至n = 0的时候开始”回头“。
代码如下:
#include<stdio.h> int F(int n){ if(n==0) return 1; else return F(n-1) *n; } int main(){ int n; scanf("%d",&n); printf("%d\n",F(n)); return 0; }
4.4贪心
4.4.1 简单贪心
贪心法是求解一类最优化问题的方法,它总是考虑在当前状态下局部最优(或较优)的策略,来使全局的结果达到最优(或较优)。
月饼是中国人在中秋佳节时吃的一种传统食品,不同地区有许多不同风味的月饼。现给定所有种类月饼的库存量、总售价、以及市场的最大需求量,请你计算可以获得的最大收益是多少。
注意:销售时允许取出一部分库存。样例给出的情形是这样的:假如我们有 3 种月饼,其库存量分别为 18、15、10 万吨,总售价分别为 75、72、45 亿元。如果市场的最大需求量只有 20 万吨,那么我们最大收益策略应该是卖出全部 15 万吨第 2 种月饼、以及 5 万吨第 3 种月饼,获得 72 + 45/2 = 94.5(亿元)。
输入格式:
每个输入包含一个测试用例。每个测试用例先给出一个不超过 1000 的正整数 N 表示月饼的种类数、以及不超过 500(以万吨为单位)的正整数 D 表示市场最大需求量。随后一行给出 N 个正数表示每种月饼的库存量(以万吨为单位);最后一行给出 N 个正数表示每种月饼的总售价(以亿元为单位)。数字间以空格分隔。
输出格式:
对每组测试用例,在一行中输出最大收益,以亿元为单位并精确到小数点后 2 位。
输入样例:
3 20
18 15 10
75 72 45
输出样例:
94.50
#include<cstdio> #include<algorithm> using namespace std; struct mooncake{ double store; double sell; double price; }cake[1010]; bool cmp(mooncake a,mooncake b) { return a.price>b.price; } int main() { int N,D; scanf("%d%d",&N,&D); for(int i=0;i<N;i++) { scanf("%lf",&cake[i].store); } for(int i=0;i<N;i++) { scanf("%lf",&cake[i].sell); cake[i].price = cake[i].sell/cake[i].store; } sort(cake,cake+N,cmp); double ans=0; for(int i=0;i<N;i++) { if(cake[i].store>=D) { ans = ans + D*cake[i].price; break; } else { D=D-cake[i].store; ans = ans +cake[i].sell; } } printf("%.2f",ans); return 0; }
给定数字 0-9 各若干个。你可以以任意顺序排列这些数字,但必须全部使用。目标是使得最后得到的数尽可能小(注意 0 不能做首位)。例如:给定两个 0,两个 1,三个 5,一个 8,我们得到的最小的数就是 10015558。
现给定数字,请编写程序输出能够组成的最小的数。
输入格式:
输入在一行中给出 10 个非负整数,顺序表示我们拥有数字 0、数字 1、……数字 9 的个数。整数间用一个空格分隔。10 个数字的总个数不超过 50,且至少拥有 1 个非 0 的数字。
输出格式:
在一行中输出能够组成的最小的数。
输入样例:
2 2 0 0 0 3 0 0 1 0
输出样例:
10015558
#include<cstdio> int count[10]; int main(){ for(int i=0;i<10;i++){ scanf("%d",&count[i]); } for(int i=1;i<10;i++){ if(count[i]>0){ printf("%d",i); count[i]--; break; } } for(int i=0;i<10;i++){ while(count[i]!=0){ printf("%d",i); count[i]--; } } return 0; }
4.5二分
4.5.1 二分查找
#include<stdio.h> int binarySearch(int A[],int left,int right,int x){ int mid; while(left <= right){ mid = (left + right) / 2; if(A[mid]==x) return mid; else if(A[mid] > x){ right = mid -1; } else{ left = mid + 1; } } return -1; } int main(){ const int n = 10; int A[n] = {1,3,4,6,7,8,10,11,12,15}; printf("%d %d\n",binarySearch(A,0,n-1,6),binarySearch(A,0,n-1,9)); return 0; }
问题:求序列中的第一个大于等于x的元素的位置
int lower_bound(int A[],int left,int right,int x){ int mid; while(left < right){ mid = (left + right) / 2; if(A[mid] >= x){ right = mid; }else{ left = mid + 1; } } return left; }
问题:求序列中第一个大于x的元素的位置
int upper_bound(int A[],int left,int right,int x){ int mid; while(left < right){ mid = (left + right) / 2; if(A[mid] > x){ right = mid; }else{ left = mid + 1; } } return left; }
4.5.2 二分法拓展
计算根号2的近似值:
const double eps = 1e-5; double f(double x){ return x * x; } double calSqrt(){ double left = 1,right = 2,mid; while(right - left > eps){ mid = (left + right) / 2; if(f(mid) > 2){ right = mid; }else{ left = mid; } } return mid; }
4.6 two pointers
4.6.1 什么是two pointers
以一个例子引入:给定一个递增的正整数序列和一个正整数M,求序列中的两个不同位置的数a和b,使得它们的和恰好为M,输出所有满足条件的方案。例如给定序列{1,2,3,4,5,6}和正整数M=8,就存在2+6=8和3+5=8成立。
本题的一个最直观的想法是,使用二重循环枚举序列中的整数a和b,判断它们的和是否为M,如果是,输出方案;如果不是,继续枚举,代码如下:
for(int i=0;i<n;i++){ for(int j=i;j<n;j++){ if(a[i] + a[j] ==M){ printf("%d %d\n",a[i],a[j]); } } }
但这种做法的时间复杂度为O(n^2)
可以通过如下方法进行降低复杂度,令下标i的初值为0,下标j的初值为n-1,即令i、j分别指向序列的第一个元素和最后一个元素,接下来根据a[i] + a[j]与M的大小进行下面三种选择,使i不断向右移动、使j不断向左移动,直到 i >= j 成立。
①如果满足a[i] + a[j] == M,说明找到了一组方案。由于序列递增,不等式a[i+1] + a[j] > M与a[i] + a[j-1] <M 均成立,但是a[i+1] + a[j-1]与M的大小未知,因此剩余的方案只可能在[i+1,j-1]区间内产生,令i = i +1,j = j-1。
②如果满足a[i] + a[j] > M,由于序列递增,不等式a[i+1] + a[j] > M成立,但是a[i] + a[j-1]与M的大小未知,因此剩余的方案只可能在[i,j-1]区间内产生,令j = j-1。
③如果满足a[i] + a[j] < M,由于序列递增,不等式a[i] + a[j-1] < M成立,但是a[i+1] + a[j]与M的大小未知,因此剩余的方案只可能在[i+1,j-]区间内产生,令i = i+1。
while(i < j){ if(a[i] + a[j] == m){ printf("%d %d\n",i,j); i++; j--; }else if(a[i] + a[j] < m){ i++; }else{ j--; } }
这时,时间复杂度下降到了O(n)
广义上的two pointers则是利用问题本身与序列的特性,使用两个下标I,j对序列进行扫描(可以同向扫描,也可以反向扫描),以较低的复杂度解决问题。
4.6.2 归并排序
例子,要将{66,12,33,57,64,27,18}进行2-路归并排序。
①第一趟,两两分组,得到四组:{66,12}、{33,57}、{64,27}、{18},组内单独排序,得到新序列{{12,66},{33,57},{27,64},{18}}
②第二趟,将四个组继续两两分组,得到两组:{12,66,33,57} 、{27,64,18},组内单独排序,得到新序列{{12,33,57,66},{18,27,64}}
③第三趟,将两个组继续两两分组,得到一组:{12,33,57,66,18,27,64},组内单独排序,得到新序列{12,18,27,33,57,64,66}。算法结束。
4.6.3 快速排序
快速排序是排序算法中平均时间复杂度为O(nlogn)的一种算法。对一个序列A[1]、A[2]、A[3]....、A[n],调整序列中的元素的位置,使得A[1]的左侧所有元素都不超过A[1]、右侧所有元素都大于A[1]。例如对序列{5,3,9,6,4,1}来说,可以调整序列中元素的位置,形成序列{3,1,4,5,9,6}
int Partition(int A[],int left,int right){ int temp = A[left]; while(left < right){ while(left < right && A[right] > temp) right--; A[left] = A[right]; while(left < right && A[left] <= temp) left++; A[right] = A[left]; } A[left] = temp; return left; } void quickSort(int A[],int left,int right){ if(left < right){ int pos = Partition(A,left,right); quickSort(A,left,pos-1); quickSort(A,pos+1,right); } }
4.7 其他高效技巧与算法
4.7.1 打表
打表是一种典型的用空间换时间的技巧,一般指将所有可能需要用到的结果事先计算出来,这样后面需要用到时就可以直接查表获得。
①在程序中一次性计算出所有需要用到的结果,之后的查询直接取这些结果。
②在程序B中分一次或多次计算出所有需要用到的结果,手工把结果写在程序A的数组中,然后在程序A中就可以直接使用这些结果。
③对一些感觉不会做的题目,先用暴力程序计算小范围数据的结果,然后找规律,或许就能发现一些“蛛丝马迹”。
4.7.2 活用递推
The string APPAPT
contains two PAT
's as substrings. The first one is formed by the 2nd, the 4th, and the 6th characters, and the second one is formed by the 3rd, the 4th, and the 6th characters.
Now given any string, you are supposed to tell the number of PAT
's contained in the string.
Input Specification:
Each input file contains one test case. For each case, there is only one line giving a string of no more than 1 characters containing only P
, A
, or T
.
Output Specification:
For each test case, print in one line the number of PAT
's contained in the string. Since the result may be a huge number, you only have to output the result moded by 1000000007.
Sample Input:
APPAPT
Sample Output:
2
#include<cstdio> #include<cstring> const int MAXN = 100010; const int MOD = 1000000007; char str[MAXN]; int main(){ scanf("%s",str); int len = strlen(str); int leftnum[MAXN],rightnum = 0; for(int i=0;i<len;i++){ if(i>0){ leftnum[i] = leftnum[i-1]; } if(str[i]=='P'){ leftnum[i]++; } } int ans = 0; for(int i=len-1;i>=0;i--){ if(str[i]=='T'){ rightnum++; } if(str[i]=='A'){ ans = (ans + leftnum[i]*rightnum) %MOD; } } printf("%d\n",ans); return 0; }