NOIP基础算法——分治与贪心巴蜀中学黄新军第四部分分治策略一、分治思想•分治(divide-and-conquer)就是“分而治之”的意思,其实质就是将原问题分成n个规模较小而结构与原问题相似的子问题;然后递归地解这些子问题,最后合并其结果就得到原问题的解。二、分治法的适用条件•能使用分治法解决的问题,它们一般具备以下几个特征:•①该问题可以分解成若干相互独立、规模较小的相同子问题;•②子问题缩小到一定的程度就能轻易得到解;•③子问题的解合并后,能得到原问题的解;•分治法在信息学竞赛中应用非常广泛,使用分治策略能生成一些常用的算法和数据结构,如快排、最优二叉树、线段树等;还可以直接使用分治策略,解决一些规模很大、无法直接下手的问题。三、分治的三步骤•①分解:将要解决的问题分解成若干个规模较小的同类子问题;•②解决:当子问题划分得足够小时,求解出子问题的解。•③合并:将子问题的解逐层合并成原问题的解。分治算法设计过程图•由分治法所得到的子问题与原问题具有相同的类型。如果得到的子问题相对来说还太大,则可反复使用分治策略将这些子问题分成更小的同类型子问题,直至产生出不用进一步细分就可求解的子问题。分治求解可用一个递归过程来表示。•要使分治算法效率高,关键在于如何分割?一般地,出于一种平衡原则,总是把大问题分成K个规模尽可能相等的子问题,但也有例外,如求表的最大最小元问题的算法,当n=6时,等分定量成两个规模为3的子表L1和L2不是最佳分割。一般来讲,都是2分为主。四、分治的框架结构procedureDIVIDE()beginif(问题不可分)then//解决begin直接求解;返回问题的解;endelsebegin对原问题进行分治;//分解递归对每一个分治的部分求解;归并整个问题,得出全问题的解;//合并endend;五、分治的典型应用•1、求最大值和最小值•2、非线性方程求根•3、二分查找•4、归并排序•5、快速幂•6、求解线性递推关系•7、棋盘覆盖问题•8、循环日程表问题•9、寻找最近点对1、求最大值和最小值•例题1:给n个实数,求它们之中最大值和最小值,要求比较次数尽量小。分析:假设数据个数为n,存放在数组a[1..n]中。可以直接进行比较:minn:=a[1];maxx:=a[1];fori:=2tondoifa[i]maxxthenmaxx:=a[i];elseifa[i]minnthenminn:=a[i];使用这一算法,比较次数为2(n-1)。若n=10,则比较18次。•用分治法解决这个问题就是把集合a分成a1,a2两个子集,每个子集有n/2个元素,应用递归结构找出两个子集的最大元和最小元,比较得到的两个最大元和最小元即可得到整个集合a中的最大元和最小元。•划分:把n个数均分为两半。即:划分点为d=(r1+r2)/2,两个区间为[r1,d]和[d+1,r2]。•递归求解:求左半的最小值min1和最大值max1以及右半最小值min2和最大值max2。•合并:所有数的最大值为maxx,最小值为minn。procedurepd(r1,r2:integer;varmaxx,minn:integer)beginvarmax1,min1,max2,min2,d:integer;ifr1=r2thenbeginmaxx:=x[r1];minn:=x[r1];endelseifr2=r1+1thenbeginifx[r2]x[r1]thenbeginmaxx:=x[r2];minn:=x[r1];endelsebeginmaxx:=x[r1];minn:=x[r2];endendelsebegind:=(r1+r2)/2;pd(r1,d,max1,min1);pd(d+1,r2,max2,min2);ifmax1max2thenmaxx:=max1;elsemaxx:=max2;ifmin1min2thenminn:=min1;elseminn:=min2;endend【思考试题】最大值最小化•【问题描述】把一个包含n个正整数的序列划分成m个连续的子序列(每个正整数恰好属于一个序列)。设第i个序列的各数之和为S(i),你的任务是让所有的S(i)的最大值尽量小。例如序列123254划分成3个序列的最优方案为123|25|4,其中S(1)=6,S(2)=7,S(3)=4,最大值为7;如果划分成12|32|54,则最大值为9;不如刚才的好。n=10^6,所有数字之和不超过10^9。2、非线性方程求根例题2:一元三次方程的解【题目描述】有形如:ax3+bx2+cx+d=0这样的一个一元三次方程。给出该方程中各项的系数(a,b,c,d均为实数),并约定该方程存在三个不同实根(根的范围在-100至100之间),且根与根之差的绝对值=1。要求由小到大依次在同一行输出这三个实根(根与根之间留有空格),并精确到小数点后4位。【文件输入】输入仅一行,有四个数,依次为a、b、c、d【文件输出】输出也只有一行,即三个根(从小到大输出)【样例输入】1-5-420【样例输入】-2.002.005.00分析•如果精确到小数点后两位,可用简单枚举法:将x从-100.00到100.00(步长0.01)逐一枚举,得到20000个f(x),取其值与0最接近的三个f(x),对应的x即为答案。而题目已改成精度为小数点后4位,枚举算法时间复杂度将达不到要求。•直接使用求根公式,极为复杂。加上本题的提示给我们以启迪:采用二分法逐渐缩小根的范围,从而得到根的某精度的数值分析A.当已知区间(a,b)内有一个根时;•用二分法求根,若区间(a,b)内有根,则必有f(a)*f(b)0。重复执行如下的过程:•(1).若a+0.0001b或f((a+b)/2)=0,则可确定根为(a+b)/2并退出过程;•(2).若f(a)*f((a+b)/2)0,则由题目给出的定理可知根在区间(a,(a+b)/2)中,故对区间重复该过程;•(3).若f(a)*f((a+b)/2)0,则必然有f((a+b)/2)*f(b)0,根在((a+b)/2,b)中,对此区间重复该过程。•执行完毕,就可以得到精确到0.0001的根。分析•B、求方程的所有三个实根•所有的根的范围都在-100至100之间,且根与根之差的绝对值=1。因此可知:在[-100,-99]、[-99,-98]、……、[99,100]、[100,100]这201个区间内,每个区间内至多只能有一个根。即:除区间[100,100]外,其余区间[a,a+1],只有当f(a)=0或f(a)·f(a+1)0时,方程在此区间内才有解。若f(a)=0,解即为a;若f(a)·f(a+1)0,则可以利用A中所述的二分法迅速出找出解。如此可求出方程的所有的解。核心参考代码proceduredivide(x1,x2:double)Beginvarx0,y0,y1,y2:double;x0:=(x1+x2)div2;y1:=cal(x1);y2:=cal(x2);y0:=cal(x0);if(x2-x10.00001andy1*y20)thenbeginwrite((x2+x1)div2:0:4);exit;endif(y1*y00orx0-x11)thendivide(x1,x0);if(y0*y20orx2-x01)thendivide(x0,x2);End;3、归并排序•归并排序的基本思想:归并排序充分应用分治算法的策略,通过二分的思想,将n个数最终分成n个单独的有序数列,每个数列中仅有一个数字;再将相邻的两列数据合并成一个有序数列;再重复上面的合并操作,直到合成一个有序数列。按照分治三步法来说,•归并过程为:(1)划分:把序列分成元素个数相等的两半;(2)递归求解:把两半分别排序;(3)合并:把两个有序表合成一个有序表;分析•显然,前两部分是很容易完成的,关键在于如何把两个有序表合成一个。每次只需要把两个序列中当前的最小元素加以比较,删除较小元素并加入合并后的新表。核心参考代码temp[maxn];//辅助空间procedureMergeSort(left,right:integer)//归并排序beginifleft=rightthenexit;//只有一个元素mid:=(left+right)div2;//找中间位MergeSort(left,mid);//对左边归并MergeSort(mid+1,right);//对右边归并i:=left;j:=mid+1,p:=left;//合并左右while(i=midandj=right)doif(a[i]a[j])thenbegintemp[p]:=a[j];inc(p);inc(j);endelsebegintemp[p]:=a[i];inc(p);inc(i);endwhile(i=mid)dobegintemp[p]:=a[i];inc(p);inc(i);endwhile(j=right)dobegintemp[p]:=a[j];inc(p);inc(i);endfori:=lefttorightdoa[i]:=temp[i];End;【变形1】逆序对数目•例题3:求“逆序对”。•给定一整数数组A=(A1,A2,…An),若ij且AiAj,则i,j就为一个逆序对。例如数组(3,1,4,5,2)的逆序对有3,1,3,2,4,2,5,2。问题是,输入n和A数组,统计逆序对数目。•数据范围:1=n=30000。朴素算法•原始的解决方案(双重循环)•C:=0;•fori:=1ton-1do•forj:=i+1tondo•ifa[i]a[j]thenc:=c+1;•时间复杂度为O(n2)分治算法:采用二分法求解:记数列a[st,ed]的逆序对数目为d(st,ed);mid=[(st+ed)/2],则有:d(st,ed)=d(st,mid)+d(mid+1,ed)+F(st,mid,ed)其中F(st,mid,ed)表示一个数取自a[st,mid],令一个数取自a[mid+1,ed]的逆序对数目。•和归并排序一样,划分和递归求解都好办,关键在于合并:如何求出i在左边,而j在右边的逆序对数目呢?统计的常见技巧是“分类”。我们按照j的不同把这些“跨越两边”的逆序对进行分类:只要对于右边的每个j,统计左边比它大的元素个数f(j),则所有f(j)之和便是答案。•幸运的是,归并排序可以帮助我们“顺便”完成f(j)的计算:由于合并操作是从小到大进行排序的,当右边的a[j]复制到T中时,左边还没有来得及复制到T的那些数就是左边所有比a[j]大的数。此时累加器中加上左边元素个数mid-i+1即可。•即把“if(a[i]a[j])thenbegintemp[p]:=a[j];inc(p);inc(j);end•改为“if(a[i]a[j])then•begintot:=tot+mid-i+1;temp[p]:=a[j];inc(p);inc(j);end4、二分查找•【问题描述】给出从小到大排列的n个不同数a[1]~a[n],试判断元素x是否出现在表中。•方法1:顺序查找。方法是一个个寻找,时间复杂度为O(n)。这个方法并没有用到“n个数从小到大排列”这一个关键条件,因而时间效率低下。方法2:二分查找•只需要比较log2n个元素。假设需要在a[L]~a[r]中查找元素x。•划分:检查某个元素a[m](L=m=r),如果a[m]=x则查找成功,返回m。•递归求解:如果a[m]x,那么元素只可能在a[L]~a[m-1]中;如果a[m]x,那么元素只可能在a[m+1]~a[r]中。•合并:不需要合并。方法1:二分查找的递归实现functionbsh(L,r,x:integer):integer;Beginvarm:integer;ifLrexit(-1);m:=(L+r)div