递归与分治

Download Report

Transcript 递归与分治

分治算法教案
长沙市雅礼中学 朱全民
问题1:找出伪币
给你一个装有1 6枚硬币的袋子。1 6枚硬币中有一
个是伪造的,并且那个伪造的硬币比真的硬币要
轻一些。你的任务是找出这枚伪造的硬币。
 为了帮助你完成这一任务,将提供一台可用来比
较两组硬币重量的仪器,比如天平。利用这台仪
器,可以知道两组硬币的重量是否相同。

方法1

任意取1枚硬币,与其他硬币进行比较,若发现
轻者,这那枚为伪币。最多可能有15次比较。
方法2

将硬币分为8组,每组2个,每组比较一次,若发现
轻的,则为伪币。最多可能有8次比较。
方法3
分析
 上述三种方法,分别需要比较15次,8次,4次,那
么形成比较次数差异的根本原因在哪里?
 方法1:每枚硬币都至少进行了一次比较,而有
一枚硬币进行了15次比较
 方法2:每一枚硬币只进行了一次比较
 方法3:将硬币分为两组后一次比较可以将硬
币的范围缩小到了原来的一半,这样充分地利
用了只有1枚伪币的基本性质。
问题2:金块问题

有一个老板有一袋金块。每个月将有两名雇员会因
其优异的表现分别被奖励一个金块。按规矩,排名
第一的雇员将得到袋中最重的金块,排名第二的雇
员将得到袋中最轻的金块。根据这种方式,除非有
新的金块加入袋中,否则第一名雇员所得到的金块
总是比第二名雇员所得到的金块重。如果有新的金
块周期性的加入袋中,则每个月都必须找出最轻和
最重的金块。假设有一台比较重量的仪器,我们希
望用最少的比较次数找出最轻和最重的金块。
方法1
假设袋中有n 个金块。可以用函数M a x通过n-1次
比较找到最重的金块。找到最重的金块后,可以从
余下的n-1个金块中用类似的方法通过n-2次比较找
出最轻的金块。这样,比较的总次数为2n-3。
 算法如下:

max:=a[1];min:=a[1];
for i:=2 to n do
{2n-2次比较}
begin
if a[i]>max then max:=a[i];
if a[i]<min then min:=a[i];
end ;
可对上述改进少1次
max:=a[1];
for i:=2 to n do
{n-1次比较,从n个元素中找到最大的}
if a[i]>max then begin
max:=a[i];
j:=i
end;
for i:=j+1 to n do a[i-1]:=a[i]; {去掉最大的数a[j]}
min:=a[1];
for i:=2 to n-1 do
{n-2次比较,从剩下的元素中找最小的}
if a[i]>max then min:=a[i];
找金块的示例图
方法2:
n≤2,识别出最重和最轻的金块,一次比较就足够
了。
 n>2,第一步,把这袋金块平分成两个小袋A和B。
第二步,分别找出在A和B中最重和最轻的金块。
设A中最重和最轻的金块分别为HA 与LA,以此类
推,B中最重和最轻的金块分别为HB 和LB。第
三步,通过比较HA 和HB,可以找到所有金块中
最重的;通过比较LA 和LB,可以找到所有金块
中最轻的。在第二步中,若n>2,则递归地应用
分而治之方法。

分治过程
比较过程
分析
从图例可以看出,当有8个金块的时候,方法1需
要比较15~16次,方法2只需要比较10次,那么形成
比较次数差异的根本原因在哪里?
 其原因在于方法2对金块实行了分组比较。
 对于N枚金块,我们可以推出比较次数的公式:
假设n是2的次幂,c(n)为所需要的比较次数。
方法1: c(n)=2n-1
方法2:c(n) = 2c(n/2 ) + 2。
由c(2)=1, 使用迭代方法可知c(n) = 3n/2 - 2。在本
例中,使用方法2比方法1少用了2 5%的比较次数。

证明
令n=2k
C(2K)=2C(2K-1)+2
=2[2C(2K-2)+2]+2=22+2+22C(2K-2)
=22+2+2[2C(2K-3)+2]=23+22+2+23C(2K-3)
……
= 2K-1+2K-2+…+2+2K-1C(2)
=2K-1+2K-2+…+2+2K-1
=2K-2+2K-1
C(n)=3n/2 -2
分治思想


1.
2.
3.
分治(divide-and-conquer)就是“分而治之”的意
思,其实质就是将原问题分成n个规模较小而结构
与原问题相似的子问题;然后递归地解这些子问
题,最后合并其结果就得到原问题的解。当n=2时
的分治法又称二分法。
其三个步骤如下;
分解(Divide):将原问题分成一系列子问题。
解决(Conquer):递归地解各子问题。若子问题足
够小,则可直接求解。
合并(combine);将子问题的结果合并成原问题的
解。
分治思想
问题S
问题的分解
问题S1
问题S2
……
问题Si
……
问题Sn
……
Sn的解
子问题求
解
S1的解
子集解的合并
S2的解
……
Si的解
问题S
S的解
分治思想



由分治法所得到的子问题与原问题具有相同的类型。如
果得到的子问题相对来说还太大,则可反复使用分治策
略将这些子问题分成更小的同类型子问题,直至产生出
不用进一步细分就可求解的子问题。
分治求解可用一个递归过程来表示。
要使分治算法效率高,关键在于如何分割?一般地,出
于一种平衡原则,总是把大问题分成K个规模尽可能相
等的子问题,但也有例外,如求表的最大最小元问题的
算法,当n=6时,等分定量成两个规模为3的子表L1和
L2不是最佳分割。
分治策略的解题思路
if 问题不可分then begin
直接求解;
返回问题的解;
end
else begin
对原问题进行分治;
递归对每一个分治的部分求解
归并整个问题,得出全问题的解;
end;
问题3:一元三次方程求解





有形如:ax3+bx2+cx+d=0这样的一个一元三次方程。
给出该方程中各项的系数(a,b,c,d均为实数),并
约定该方程存在三个不同实根(根的范围在-100至100
之间),且根与根之差的绝对值>=1。要求由小到大依
次在同一行输出这三个实根(根与根之间留有空格),并
精确到小数点后4位。
提示:记方程f(x)=ax3+bx2+cx+d,若存在2个数x1和
x2,且x1<x2,f(x1)*f(x2)<0,则在(x1,x2)之间一定有
一个根。
样例
输入:1 -5 -4 20
输出:-2.00 2.00 5.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.0001>b或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中所述的二分法迅
速出找出解。
问题4: 快速排序





最有效的一般目的的排序,O(n lg n)
基本策略
- 把要排序的数据数组(或向量)分成两个子数组这样:
在第一个子数组中的数据比一个已知值要小
在第二个数组中的数据比那个值要大
- 技术上来说称为“分块”
已知值称为‘枢元素’
- 一旦我们已经分好块,枢元素将处于它最终的位置
- 然后我们继续把子数组分为更小的数组,直到剩余部分
只有一个元素(递归!)
快速排序算法
1. 选择一个元素作为枢元素。我们使用右边的元素
2. 在左边和(右-1)元素开始索引
3. 移除左边的索引直到我们得到一个元素>枢元素
4. 移除右边的索引直到我们得到一个元素<枢元素
5. 若索引不相交,则交换值并重复步骤3和4
6. 若索引相交,则交换枢元素值和左边索引值
7. 在子数组调用快速排序得到枢元素左右的值
实例
原始值
第一次交换
第二次交换
分块

1.
2.
3.
4.
5.
分块是快速排序的关键步骤:
在我们的快速排序的说法里,pivot选为要排序的(子)数
组的最后一个元素
使用index low从左边扫描(子)数组寻找>=pivot的元素
当我们得到一个元素>=pivot时,使用index high从右边扫
描寻找<=pivot的元素
若low<high,则交换它们的值,然后开始扫描另一对可交
换的元素
若low>=high,则完成我们需要做的,交换low和pivot的值,
该值位于两个分块之间
另一个分块的实例
分块交换
pivot值交换
当前pivot值
原先的pivot值
较好的快速排序




pivot值的选择对快速排序具有决定性的作用。
理想的pivot值是子数组的中间值,即是排序数组的中间组
成。但是在排序之前我们无法得到中间值。
事实证明每个子数组的第一个,中间的和最后一个元素的
中间值是上面所说的中间值的很好的替代。它保证分块的每
部分至少有两个元素,提供数组至少有4个,但是它的执行
方式通常更好。并且没有自然的情况会产生最坏的情况。
其他改进:
- 从递归到叠代的转化,更加有效率。总是先处理最短的
子数组(有限的栈大小,pop,push)
- 当子数组足够小(5-25个元素)使用插入排序,在小问
题上它更快
快速排序算法(分块)
问题5: 归并排序
已知某数列存储在序列A[1],A[2],……,A[n],现
需要采用分治策略对它们进行从大到小(从小到大)
排序。
 例如:
52462326
 排序后为
66543222

归并排序的整个过程
归并过程
procedure Merge(var A: ListType; P, Q, R: Integer);
{将A[P..Q]和A[Q+1..R],合并到序列A[P..R]}
var
I, J,
{左右子序列指针}
T: Integer;
{合并后的序列的指针}
Lt: ListType; {暂存合并的序列}
begin
T:= P; I := P; J := Q + 1;
while T <= R do begin{合并未完成}
if (I <= Q) and ((J > R) or (A[I] <= A[J])) then begin
Lt[t] := A[I]; Inc(I);
end else begin
{否则右序列的首元素进入合并序列}
Lt[t] := A[J]; Inc(J);
end;
Inc(T);
{合并后的序列的指针右移}
end;
A := Lt;
{合并后的序列赋给A}
end;
二分过程
procedure Merge_Sort(var A: ListType; P, R: Integer);
var
Q: Integer;
begin
if P <> R then begin
{若子序列A中不止一个元素}
Q := (P + R - 1) div 2;
{计算中间下标Q}
Merge_Sort(A, P, Q);
{继续对左子序列递归排序}
Merge_Sort(A, Q + 1, R);
{继续对右子序列递归排序}
Merge(A, P, Q, R)
{对左右子序列归并}
end;
end;
问题6:求逆序对个数
有一实数序列A[1]、A[2] 、A[3] 、……A[n-1] 、A[n],若i<j,
并且A[i]>A[j],则称A[i]与A[j]构成了一个逆序对,求数列A中
逆序对的个数。
 n≤10000。
 例如,5 2 4 6 2 3 2 6,可以组成的逆序对有
(5 2),(5 4),(5 2),(5 3),(5 2),
(4 2),(4 3),(4 2),
(6 2),(6 3),(6 2),
(3 2)
 共:12个

分析
在看完试题以后,我们不难想到一个非常简单的算
法——穷举算法,即对数组中任意的两个元素进行
判断,看它们是不是构成“逆序对”,因此这种算
法的时间复杂度为O(N2)。
 时间效率不尽如人意…..
 问题出现在哪里呢??

转换思维

用分治怎么样?

首先将这一序列A一分为二,分成两个不同的序列B、C
如果求出了B,C的逆序对,那么可由B,C求出A的逆序对.
如何来统计序列B和序列C之间的“逆序对”呢?
如果还按照穷举的思想来统计的话,那么我们采用分治法
就没有什么意义!!!



提示

在递归的求解B、C两个序列中的逆序对的个数以后,如
果对B、C两个序列当中的元素进行排序的话,要统计B、
C两个序列之间的“逆序对”是非常容易的.
如图
排序前

排序后



在B数组当中,首先,B中的6,5,4都与C数组当中的3,
2,2都构成了“逆序对”,而2不会构成逆序对,因此B、
C两个数组之间构成的逆序对的个数为3+3+3=9。
结论


由于B、C两个数组已经进行了排序,因此可以设置两个
指针进行操作,有效的统计B、C两个数组之间的“逆序
对”的个数。而且这一统计步骤是能够在线性时间内完
成的。考虑到我们设计的二分模型本身是递归定义的,
所以可以将我们所建立的二分模型完全与归并排序联系
起来。因为排序的过程是可以在递归求解子问题时就能
够完成的,算法的时间复杂度就降到了O(nlogn)。
在这里,虽然对B、C两个序列当中的元素进行了排序,
使得序列A当中某一部分元素的顺序被打乱了,但由于
求解过程是递归定义的,在排序之前B、C两个序列各自
其中的元素之间的“逆序对”个数已经被统计过了,并
且排不排序对统计B、C两个数组之间的“逆序对”的个
数不会产生影响。所以这个排序过程对整个问题的求解
的正确性是没有任何影响的。
总结归纳





分治是一种解题的策略,它的基本思想是:“如果整个问题
比较复杂,可以将问题分化,各个击破。”
分治包含“分”和“治”两层含义,如何分,分后如何“治”
成为解决问题的关键所在
不是所有的问题都可以采用分治,只有那些能将问题分成与
原问题类似的子问题,并且归并后符合原问题的性质的问题,
才能进行分治
分治可进行二分,三分等等,具体怎么分,需看问题的性质
和分治后的效果。
只有深刻地领会分治的思想,认真分析分治后可能产生的预
期效率,才能灵活地运用分治思想解决实际问题。
思考题1: 剔除多余括号




输入一个含有括号的四则运算表达式,可能含有多余的括
号,编程整理该表达式,去掉所有多余的括号,原表达式
中所有变量和运算符相对位置保持不变,并保持与原表达
式等价。
表达式以字符串输入,长度不超过255,输入不需要判错。
所有变量为单个小写字母。只是要求去掉所有多余括号,
不要求对表达式简化。
样例:
输入表达式
a+b(+c)
应输出表达式
a+b+c
(a*b)+c/(d*e)
a*b+a/(d*e)
a+b/(c-d)
a+b/(c-d)
分析
对于四则运算表达式,我们分析一下哪些括号可以去掉。
1. 设待整理的表达式为(s1 op s2);op为括号内优先级最
低的运算符(“+”,“-”或“*”,“/”);
2. 左邻括号的运算符为“/”,则括号必须保留,即 … /(s1
op s2)… 形式。
3. 左邻括号的预算符为“*”或“-”。而op为“+”或“-”,则
保留括号,即…*(s1+s2)… 或 … -(s1+s2)… 或 …
*(s1-s2)… 或 … -(s1-s2)…
4. 右邻括号的运算符为“*”或“/”,而op为“+”或“-”,原
式中的op运算必须优先进行,因此括号不去除,即(s1+s2)
* …
5. 除上述情况外,可以括号去除,即… s1 op s2… 等价
于…(s1 op s2)…
 我们从最里层嵌套的括号开始,依据上述规律逐步向外进
行括号整理,直至最外层的括号保留或去除为止。这个整
理过程可以用一个递归过程来实现。
剔除“((a+b)*f)-(i/j)”中多余的括号
思考题2:导线和开关


如上图是一个具有3根导线的电缆把A区和B区连接起来。
在A区3根导线标以1,2,3;在B区导线1和3被连到开关3,
导线2连到开关1。
一般说来,电缆含m(1≤m ≤90)根导线,在A区标以
1,2,…,m。在B 区有m个开关,标为1,2,… ,m。每一
根导线都被严格地连到这些开关中的某一个上; 每一个开
关上可以连有0根或多根导线。
问题描述
测量
 你的程序应作某些测量来确定,导线和开关怎样连。 每个开关或处
于接通或处于断开状态,开关的初始状态为断开。我们可用一个探头
P在A区进行测试:如果探头点到某根导线上,当且仅当该导线连到处
接通状态的开关时,灯L才会点亮。
 你的程序从标准输入读入一行以得到数字m;然后可以通过向标准输
出写入一行以发出命令(共3种命令)。每种命令的开头是一个大写字
母:
 测试导线命令T:T后面跟一个导线标号;
 改变开关状态命令C:C后面跟一个开关标号;
 完成命令D:D后面跟的是一个表列(LIST),该表列中的第i个元素代表
与导线i相连的开关号。


在命令T和C之后,你的程序应从标准输入(standard input)读入一
行。 若开关状态能使灯亮,则命令T的回答应是Y;反之,回答应是N。
命令C的作用是改变开关的状态(若原来是接通则变为断开;若原来是
断开则变为接通)。对C命令的回答是作为一种反馈信号。
你的程序可以给出一系列命令,将T命令与C命令以任意顺序混合使用。
最后给出命令D,并结束。你的程序给出的命令总数应不大于900。
样例
Standard
Output
Standard Input
C3
T1
T2
T3
C3
C2
T2
D313
3
Y
Y
N
Y
N
Y
N
分析
为了使导线和开关的连接工作有规律地进行,我们不妨采用二分法。
1.分解
 设当前待接的开关为head..tail,初始时为1..m,则

左区间head..[(head+tail-1)/2], 开关集合为p1={1..m}
右区间 [(head+tail-1)/2]+1..tail, 开关集合为p2={}
导线的连接状态state=(0,1),分别表示断开和连接
 对区间进行检测,对p1中的每根导线发出T命令,若开关状态为闭合,
且回答N,或者开关状态为断开,且回答Y,则移到p2
2. 递归过程

 check(p1,左区间,1-state)
 check(p2,右区间,state)
3. 合并
当区间近近剩下一个开关(head=tail)且与之相连的导线集合p1非空,
则p1中所有的导线与head相连,并使得ANS[i]=head,i属于p1
算法框架
Procedure check(p1,head,tail,state);
Begin
if p1<>[] then
if head =tail then begin
计算左区间p1;
通过c命令和用户应答,将左区间开关状态取反;
右区间p2=[];
对p1中的每根导线发出T命令,若开关状态为闭合,
且回答N,或者开关状态为断开,且回答Y,则移到p2;
end;
i:=trunc((head+tail)/2);
check(p1,head,i,state);
check(p2,i+1,tail,state);
End;
思考题3:BTP职业网球赛
 [问题描述]
N(N≤65536)—
有N头奶牛(N是2P)参加网球淘汰赛。即N
头奶牛分成N/2组,每组两头奶牛比赛,决出
N/2位胜者;所有胜者继而分成N/4组比
赛……直至剩下一头牛是冠军。
K (1≤K≤N-1)—
比赛既要讲求实力,又要考虑到运气。每头
奶牛都有一个BTP排名,恰为1-N。如果两
头奶牛的排名相差大于给定整数K,则排名靠
前的奶牛总是赢排名靠后的奶牛;否则,双
方都有可能获胜。
Answer—
现在观众们想知道,哪头奶牛是所有可能成
为冠军的牛中排名最靠后的。并要求你列举
出一个可能的比赛安排使该奶牛获胜。
初步分析

由于N很大,猜想可以通过贪心方法解决。
K+11
K+22
……
2KK
3K+12K+1
……
……
初步分析
 但我们很容易找到反例,例如:N=8,
K=2
31 42 75 86
43
87
48
67 53 48 21
65
42
64
图BTP-1
图BTP-2
 但最优解为6。
 解决方法:枚举!
性质:隐含的有序性
 如果排名为X的选手最终获胜,那么排名在X
前的选手Y也可以获胜。
 证明:
情况一:Y被Z≠X击败
Z? … Y? … X?
ZX
Y … Y
X?
… Y
X? …
X?
Y
性质:隐含的有序性
 如果排名为X的选手最终获胜,那么排名在X
前的选手Y也可以获胜。
 证明:
情况二:Y被X击败
Y? … X?
… Y
X X
Y …
… Y
X? …
X?
Y
问题的解决
 于是,我们可以
二分枚举获胜的X。
可以!
 知道了X,能否很快构造出对战方式?
例如 N=8,K=2,X=6
67 53 48 21
65
42
64
 可以证明这样贪心是正确的。
 如果利用静态排序二叉树,整个问题可以在
O(Nlog2N)时间完成。