算法笔记 8.高效算法设计

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

黑色格子:*

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值