P153
学习目标:
“基本操作”、渐进时间复杂度、O !“最大连续和”问题的各种算法及其时间复杂度分析 算法分析的优点和局限性,使用分析结果 !归并排序和逆序对统计的分治算法 快速选择和快速选择算法 !!二分查找算法,包括找上下界的算法 能用递归的方式思考和求解问题 !!二分法求解非线性方程 !!二分法把优化问题转化为判定问题 !贪心法的各类经典问题 |
枚举、回溯等暴力方法:较低效。越通用,越不能挖掘问题的特殊性
8.1算法分析初步
在编程之前估计时空开销(如果又复杂又慢,就不要急着写出来)
渐进时间复杂度
例8-1:长度n序列,求最大连续和。
解:
#include<stdio.h>
//#define LOCAL
#define N 3
int main(){
#ifdef LOCAL
freopen("data.in","r",stdin);
freopen("data.out","w",stdout);
#endif
int A[N]={-1,1,2};
int max=A[0];
int sum=0;
int i,j;
for(i=0;i<N;i++){
//sum清零,从i加到j,比较
sum=0;
for(j=i;j<N;j++){
sum+=A[j];//也是递推思想
if(sum>max)max=sum;
}
}
printf("max:%d",max);
return 0;
}
Max初值为A[0]最保险,初值为0在所有值为负时可能出现问题
基本操作的数量,排除机器影响,衡量算法操作的次数。
渐进时间复杂度:基本操作的数量用输入规模表达式,保留最大项忽略系数。衡量操作数随规模的增长情况
输入规模为n时:
上界分析
上界分析:所有最坏情况同时取到
渐进时间复杂度推导:3重循环,每层最坏n次,总运算次数<=n^3(数量级一致,紧上界)
分治法
步骤:划分子问题,递归解决子问题,合并子问题得到解
把元素分成两半,分别求最佳序列,求出起点位于左半,终点位于右半的最大序列,并和子问题的最优解比较
关键在于合并,先寻找最佳起点,在寻找最佳终点
区间分割、取平均数技巧
取最大值符号>?经过尝试,编译错误,这里用函数greater代替
#include<stdio.h>
//#define LOCAL
#define N 3
int greater(int a,int b){
return a>b?a:b;
}
int maxsum(int* A,int x,int y){//返回[x,y)区间的最大序列和
int max; //分段后局部最大值
int m;//分界点
int v,i;
int L,R;//从分界点外扩左右两段全局最大值
if(y-x<=1)return A[x];//只有一个元素时直接返回
m=x+(y-x)/2;//确定分界点
// max=maxsum(A,x,m)>?maxsum(A,m,y);//递归,分段后局部最大值
max=greater(maxsum(A,x,m),maxsum(A,m,y)) ;
//求拼接和
v=0;L=A[m-1];
// for(i=m-1;i>=x;i--) L>?=v+=A[i];//分界点往外全局最大值
for(i=m-1;i>=x;i--){
// L=greater(L,v+=A[i]);
v+=A[i];
L=greater(L,v);
}
v=0;R=A[m];
// for(i=m;i<y;i++) R>?=v+=A[i];//分界点往外全局最大值
for(i=m;i<y;i++) R=greater(R,v+=A[i]);
// return max >?(L+R);//局部最优与全局最优比较
return greater(max,(L+R));
}
int main(){
#ifdef LOCAL
freopen("data.in","r",stdin);
freopen("data.out","w",stdout);
#endif
int A[N]={-1,1,2};
printf("max:%d",maxsum(A,0,3));
return 0;
}
另一个细节-计算分界点:
计算机是朝0取整(只取整数部分),而不是向下取整(负数时不同)
用x+(y-x)/2代替(x+y)/2可以达到向下取整的效果
法四:
N、nlog2N规模大,随速度增长快
有效算法:渐进时间复杂度为多项式(多项式时间算法)
指数时间算法:n!、2^n
8.2再谈排序与检索
现成整数排序方法:C标准库qsort、C++STL中sort或stable_sort
归并排序merge sort(分治思想)
划分:序列分成两半
递归:两半分别排序
合并:两个有序表合并
关键在于合并有序表:比较两个序列的最小元素,得全局最小,出列,进新表
合并过程需要附加空间(新表),时间是线性的
复杂度和分治法一样:O(nlogn)
程序8-4归并排序
几个条件是关键,利用||为短路运算符,如果序列为空,则不会访问非法内存
#include<stdio.h> //#define LOCAL #define N 3 void merge_sort(int* A,int x,int y,int* T){//左闭右开? if(y-x>1){//有元素才排序 int m=x+(y-x)/2;//靠近0位分界点 int p=x,q=m,i=x;//指向当前操作位,pq为两序列,i 为辅助序列位 merge_sort(A,x,m,T); merge_sort(A,m,y,T);//递归 while(p<m||q<y) {//当前两序列中有未处理元素 if(q>=y||(p<m&&A[p]<=A[q])){//4种情况。右序列空或均非空左小,则左入列 T[i++]=A[p++] ; } else{ T[i++]=A[q++];//右入列 } } for(i=x;i<y;i++){ A[i]=T[i];//将排序完毕的复制回 } } } int main(){ #ifdef LOCAL freopen("data.in","r",stdin); freopen("data.out","w",stdout); #endif int A[N]={2,1,-1},T[N]; int i; merge_sort(A,0,3,T); for(i=0;i<N;i++){ printf("%d\t",A[i]); } return 0; } |
例8-2逆序对数
求有多少个有序对(i,j),i<j但Ai>Aj。n可以高达10^6。
O(n^2)枚举将超时,使用分治三步法
划分:分成左右两半
递归:分别统计两边局部逆序对个数(ij在同一边)
合并:统计i左列,j右列的逆序对个数
合并时通常分类,分别求每个j的i个数f(j),求和
归并排序可以顺便完成每个f(j)计算,当右列A[j]入辅助T时,左边剩下没复制的都是比j小的(两边相等的情况左先入了)(个数m-p,左边剩余区间为[p,m))
!调用之前给cnt清零
#include<stdio.h> //#define LOCAL #define N 3 int merge_sort(int* A,int x,int y,int* T,int* cnt){//左闭右开? if(y-x>1){//有元素才排序 int m=x+(y-x)/2;//靠近0位分界点 int p=x,q=m,i=x;//指向当前操作位,pq为两序列,i 为辅助序列位 // int cnt=0;//计算逆序对数 merge_sort(A,x,m,T,cnt); merge_sort(A,m,y,T,cnt);//递归 while(p<m||q<y) {//当前两序列中有未处理元素
if(q>=y||(p<m&&A[p]<=A[q])){//4种情况。右序列空或均非空左小,则左入列 T[i++]=A[p++] ; } else{ // printf("A[%d]:%d\tm-p:%d\tA[%d]:%d\n",q,A[q],m-p,p,A[p]); T[i++]=A[q++];//右入列 *cnt+=m-p; } } for(i=x;i<y;i++){ A[i]=T[i];//将排序完毕的复制回 } return *cnt; } return 0; } int main(){ #ifdef LOCAL freopen("data.in","r",stdin); freopen("data.out","w",stdout); #endif int A[N]={2,1,-1},T[N]; int i,cnt=0;//计算逆序对数 merge_sort(A,0,3,T,&cnt); for(i=0;i<N;i++){ printf("%d\t",A[i]); } printf("\n逆序对:%d",cnt); return 0; } |
快速排序
最快的通用内部排序算法,且不需要辅助空间
划分:各个元素重排后分成左右两部分,左边任意元素都<=右边
递归:左右分别排序
合并:不用合并,已完全有序
快排的划分有多个版本,网上找
算法:
把第一个元素的值作为分界标记,分别同时从两端找(右找小(这时标记位在左半边),左找大,和标记交换),比它小的放左,大的放右。应该要等右边找完一个才找左边,左右指针自增需要另外控制
左右分别交换一次为一个循环,左右寻找的指针碰头为一趟排序
一趟排序结束,分界标记位置定下来后,两边分别递归(此题为一边递归),递归区间为0则返回
例8-3第k小的数
输入n个整数和一个正整数k(1<=k<=n),输入这些整数从小到大排序后的第k个。n<=10^7
分析:
可先排序,直接输出A[k-1]。但10^7对归并O(nlogn)也稍大,用快排+剪枝
划分后A[p…r]-->A[p…q]+A[q+1…r],根据左边元素个数q-p+1和k的大小关系只在左(右)边递归求解。时间复杂度O(n)
#include<stdio.h> int k=2; void swap(int * a,int * b){ printf("swap:%d %d\n",*a,*b); int temp=*a; *a=*b; *b=temp; } void quick_sort(int * A,int x,int y){//[x,y] // printf("-->sort x:%d y:%d\n",x,y); if(y-x<1)return; int key=x;//标记值位 int i=x,j=y;//从两端找 // printf("key:%d\n",A[key]); while(i<j){ while(i<j){//检查语法 ,此时自增与for循环步长不同,循环体内操作会受影响 if(A[j]<A[key]){ swap(&A[j],&A[key]); key=j--; break; }else j--; } while(j>i){ if(A[i]>A[key]){ swap(&A[i],&A[key]); key=i++; break; }else i++; } } if(k-1<=key) quick_sort(A,x,key);//k在数组中实际位置为k-1,k-1<=key else quick_sort(A,key+1,y);//key+1可能溢出数组 ,此处有k约束 } int main(){ int A[5]={5,4,3,2,1} ; quick_sort(A,0,4); printf("%d",A[k-1]); return 0; } |
二分查找(有序序列)
排序的重要意义就是方便检索,单次少数次查找可以遍历,
但查找次数多,有序表可用二分查找。
每次选择可行区间的中点,每次把范围缩小一半,查找次数log2N
分治:
原序列分成两半,递归查找
二分查找一般写成非递归(迭代)。循环体常直接写在程序中
程序8-5二分查找:查到相同值,直接返回(若有多个则返回的是中间的,不存在返回-1)
#include<stdio.h> //参数x,y,v分别是查找起始点和查找值,找到则返回下标 int bsearch(int* A,int x,int y,int v){//区间[x,y) ,则循环条件为<,此时xy相等为空区间;若为闭区间(含y)则循环条件需改为<=,此时xy相等区间仍有一点 int m;//middle while(x<y){ m=x+(y-x)/2;//向下取整 //printf("x:%d y:%d m:%d\n",x,y,m) ; if(A[m]==v)return m; else if(A[m]>v)y=m;//在左半区间寻找 else x=m+1; } return -1; } int main(){ int A[5]={0,1,2,3,4} ; printf("%d",bsearch(A,0,5,4)); return 0; } |
程序8-6二分查找下界:相同值继续查找(若有多个返回第一个,不存在返回可插入下标-插入前需把原元素后移)有序表中相同值总是连续的
A[m]=v,找到一个,但左边可能还有,区间变为[x,m]
A[m]>v,左边可能,m也可能(插入位在此),区间变为[x,m]
A[m]<v,右边可能,m不可能(不能把比它小的位挤到后面),区间变为[m+1,y]
危险注意:[x,m]或[m+1,y]与原区间相同可能发生死循环(此例不会,在x.y不等的情况下,向下取整m!=y,有可能=x,但m+1!=x)
查找区间是[x,y)但返回值区间是[x,y](如果v大于所有数,则插在序列尾部)
查找上界类似,只需改变重设区间范围时的条件
#include<stdio.h> //参数x,y,v分别是查找起始点和查找值,找到则返回下标 int lower_bound(int* A,int x,int y,int v){//区间[x,y) ,则循环条件为<,此时xy相等为空区间;若为闭区间(含y)则循环条件需改为<=,此时xy相等区间仍有一点 int m;//middle while(x<y){ m=x+(y-x)/2;//向下取整 printf("x:%d y:%d m:%d\n",x,y,m) ; if(A[m]>=v)y=m;//在左半区间寻找 // if(A[m]>v)y=m;//upper_bound else x=m+1; } return x;//最后结束条件是x.y,返回m可能不在区域内 } int main(){ int A[5]={0,1,1,3,4} ; printf("%d",lower_bound(A,0,5,1)); return 0; } |
最后可得出范围[L,R),若R-L=0,则区间为空元素不存在
!左闭右开区间的使用方法、上下界函数的实现细节
例8-4范围统计
给出n个整数xi和m个询问,对每个询问(a,b),输出闭区间[a,b]内的整数xi的个数
分析:
预处理先排序
大于等于a的第一个元素(等于lower_bound值),如果所有元素都小于a则下标为n(不存在数)
小于等于b的最后一个元素的下一个位置(等于upper_bound值),如果所有都大于b,则下标为0
答案即[L,R]的长度R-L
STL中已经包含sort、lower_bound、upper_bound函数,可以直接使用
#include<cstdio> #include<algorithm>//包含sort、lower_bound、upper_bound函数 using namespace std; int v[10000]; int main(){ int n,m,a,b;//分别是序列中数字个数,询问数,和询问的上下界 scanf("%d%d",&n,&m);//连续读入数字,占位符间不加分隔符能适应更多情况 for(int i=0;i<n;i++)scanf("%d",&v[i]);//读入数组 sort(v,v+n);//预处理 for(int i=0;i<m;i++){ scanf("%d%d",&a,&b);//依次读入询问 printf("%lld\n",upper_bound(v,v+n,b)-lower_bound(v,v+n,a)); //lld这样输出? } return 0; } |
8.3递归和分治
递归作用(排序、检索+此节各典型)
这节很多题目描述简略又抽象(连输入输出样例或描述都没有),第一道还有点像Uva322那道船的题目
棋盘覆盖问题
有一个2^k*2^k的方格棋盘,恰有一个方格是黑色,其他为白色。你需要用包含3个方格的L型牌覆盖所有的白色方格,黑色方格不能被覆盖,且任意一个白色方格不能同时被多牌覆盖。如图为L型牌的4四种旋转方式
XX X | XX X | X XX | X XX |
分析:
因为棋盘是2^k*2^k(2的幂,可以分治,两个方向各递归分两半)
把棋盘切4块,每一块都是2^(k-1)*2^(k-1)
有黑格子的那一块可以递归解决,其他3块没有黑格子构造黑格子
递归边界:k=1时1块牌即可
思路:
没覆盖的白色格子表示为.
L型牌:X
黑色格子:*