由分治法产生的子问题往往是原问题的较小模式,这就为使用递归技术提供了方便。
在这种情况下,反复应用分治手段,可以使子问题与原问题类型一致而其规模却不断缩小,最终使子问题缩小到很容易直接求出其解。
这自然导致递归过程的产生。
分治与递归像一对孪生兄弟,经常同时应用在算法设计之中,并由此产生许多高效算法。
分治法的适用条件:
(1)该问题的规模缩小到一定的程度就可以容易地解决;
(2)该问题可以分解为若干个规模较小的相同问题,即该问题具有最优子结构性质。
(3)利用该问题分解出的子问题的解可以合并为该问题的解;
(4)该问题所分解出的各个子问题是相互独立的,即子问题之间不包含公共的子问题。
上述的第一条特征是绝大多数问题都可以满足的,因为问题的计算复杂性一般是随着问题规模的增加而增加;第二条特征是应用分治法的前提,它也是大多数问题可以满足的,此特征反映了递归思想的应用;第三条特征是关键,能否利用分治法完全取决于问题是否具有第三条特征,如果具备了第一条和第二条特征,而不具备第三条特征,则可以考虑贪心法或动态规划法。
第四条特征涉及到分治法的效率,如果各子问题是不独立的,则分治法要做许多不必要的工作,重复地解公共的子问题,此时虽然可用分治法,但一般用动态规划法较好。
分治法的基本步骤:
分治法在每一层递归上都有三个步骤:
分解:
将原问题分解为若干个规模较小,相互独立,与原问题形式相同的子问题;
解决:
若子问题规模较小而容易被解决则直接解,否则递归地解各个子问题;
合并:
将各个子问题的解合并为原问题的解。
它的一般的算法设计模式如下:
Divide-and-Conquer(P)
1. if|P|≤n0
2. thenreturn(ADHOC(P))
3. 将P分解为较小的子问题P1,P2,...,Pk
4. fori←1tok
5. doyi←Divide-and-Conquer(Pi) △递归解决Pi
6. T←MERGE(y1,y2,...,yk) △合并子问题
7. return(T)
其中|P|表示问题P的规模;n0为一阈值,表示当问题P的规模不超过n0时,问题已容易直接解出,不必再继续分解。
ADHOC(P)是该分治法中的基本子算法,用于直接解小规模的问题P。
因此,当P的规模不超过n0时,直接用算法ADHOC(P)求解。
算法MERGE(y1,y2,...,yk)是该分治法中的合并子算法,用于将P的子问题P1,P2,...,Pk的相应的解y1,y2,...,yk合并为P的解。
根据分治法的分割原则,原问题应该分为多少个子问题才较适宜?
各个子问题的规模应该怎样才为适当?
这些问题很难予以肯定的回答。
但人们从大量实践中发现,在用分治法设计算法时,最好使子问题的规模大致相同。
换句话说,将一个问题分成大小相等的k个子问题的处理方法是行之有效的。
许多问题可以取k=2。
这种使子问题规模大致相等的做法是出自一种平衡(balancing)子问题的思想,它几乎总是比子问题规模不等的做法要好。
分治法的合并步骤是算法的关键所在。
有些问题的合并方法比较明显,有些问题合并方法比较复杂,或者是有多种合并方案;或者是合并方案不明显。
究竟应该怎样合并,没有统一的模式,需要具体问题具体分析。
四、实验内容
1、编程实现归并排序算法和快速排序算法,程序中加入比较次数的计数功能,输出排序结果和比较次数。
输入10组相同的数据,验证排序结果和完成排序的比较次数。
用表格列出比较结果。
给出文字分析。
2、汉诺塔(hanoi)问题。
3、棋盘覆盖问题。
4、循环赛日程安排问题。
五、算法设计
1、归并排序算法
procedureMERGESORT(low,high)
//A(low;high)是一个全程数组,它含
有high-low+1≥0个待排序的元素//
integerlow,high;
iflowthenmid←,//求这个集合的分割点//
callMERGESORT(low,mid)//将一个子集合排序//
callMERGESORT(mid+1,high)//将另一个子集合排序
callMERGE(low,mid,high)//归并两个已排序的子集合//
endif
endMERGESORT
归并两个已排序的集合
procedureMERGE(low,mid,high)
//A(low:
high)是一个全程数组//
//辅助数组B(low;high)//
integerh,i,j,k;
h←low;i←low;j←mid+1;
whileh≤midandj≤highdo//当两个集合都没取尽时//
ifA(h)≤A(j)thenB(i)←A(h);h←h+1
elseB(i)←A(j);j←j+1
endif
i←i+1
repeat
ifh>midthen
fork←jtohighdo//处理剩余的元素//
B(i)←A(k);i←i+1
repeat
elsefork←htomiddo
B(i)←A(k);i←i+1
repeat
endif
将已归并的集合复制到A
endMERGE
2、快速排序算法
我们已经知道,在决策树计算模型下,任何一个基于比较来确定两个元素相对位置的排序算法需要Ω(nlogn)计算时间。
如果我们能设计一个需要O(n1ogn)时间的排序算法,则在渐近的意义上,这个排序算法就是最优的。
许多排序算法都是追求这个目标。
下面介绍快速排序算法,它在平均情况下需要O(nlogn)时间。
这个算法是由C.A.R.Hoare发明的。
算法的基本思想:
快速排序的基本思想是基于分治策略的。
对于输入的子序列L[p..r],如果规模足够小则直接进行排序,否则分三步处理:
分解(Divide):
将输入的序列L[p..r]划分成两个非空子序列L[p..q]和L[q+1..r],使L[p..q]中任一元素的值不大于L[q+1..r]中任一元素的值。
递归求解(Conquer):
通过递归调用快速排序算法分别对L[p..q]和L[q+1..r]进行排序。
合并(Merge):
由于对分解出的两个子序列的排序是就地进行的,所以在L[p..q]和L[q+1..r]都排好序后不需要执行任何计算L[p..r]就已排好序。
这个解决流程是符合分治法的基本步骤的。
因此,快速排序法是分治法的经典应用实例之一。
QuickSort(p,q)
//将数组A[1:
n]中的元素
A[p],A[p+1],,A[q]按不降次序排列,
并假定A[n+1]是一个确定的、且大于
A[1:
n]中所有的数。
//
intp,q;globaln,A[1:
n];
ifpj=Partition(p,q+1);//划分后j成为划分元素的位置
QuickSort(p,j-1);
QuickSort(j+1,q);
endif
endQuickSort
procedurePARTITION(m,p)
//退出过程时,p带着划分元素所在的下标位置。
//
integerm,p,i;globalA(m:
p-1)
v←A(m);i←m//A(m)是划分元素//
loop
loopi←i+1untilA(i)≥vrepeat//i由左向右移//
loopp←p-1untilA(p)≤vrepeat//p由右向左移//
ifi
thencallINTERCHANGE(A(i),A(p))//A(i)和A(p)换位//
elseexit
endif
repeat
A(m)←A(p);A(p)←v//划分元素在位置p//
EndPARTITION
3、汉诺塔(hanoi)问题。
设有A、B、C共3根塔座,在塔座A上堆叠n个金盘,每个盘大小不同,只允许小盘在大盘之上,最底层的盘最大,如下图所示。
现在要求将A上的盘全都移到C上,在移的过程中要遵循以下原则:
每次只能移动一个盘;圆盘可以插在A、B和C任一个塔座上;在任何时刻,大盘不能放在小盘的上面。
hanoi问题递归求解思想:
我们把一个规模为n的hanoi问题:
1到n号盘按照移动规则从A上借助B移到C上表示为H(A,B,C,n);原问题划分成如下三个子问题:
(1)将1到n-1号盘按照移动规则从A上借助C移到B上H(A,C,B,n-1);
(2)将n号盘从A上直接移到C上;
(3)将1到n-1号盘按照移动规则从B上借助A移到C上H(B,A,C,n-1);
经过三个子问题求解,原问题的也即求解完成。
4、盘覆盖问题。
在一个2k×2k个方格组成的棋盘中,恰有一个方格与其它方格不同,称该方格为一特殊方格,且称该棋盘为一特殊棋盘。
在棋盘覆盖问题中,要用图示的4种不同形态的L型骨牌覆盖给定的特殊棋盘上除特殊方格以外的所有方格,且任何2个L型骨牌不得重叠覆盖。
六、参考程序代码
1、归并排序
#include
#include
#include
#include
#defineM11
typedefintKeyType;
typedefintElemType;
structrec{
KeyTypekey;
ElemTypedata;
};
typedefrecsqlist[M];
classguibing{
public:
guibing(sqlistb)
{
for(inti=0;ir[i]=b[i];
}
voidoutput(sqlistr,intn)
{
for(inti=0;icout<cout<}
voidxuanze(sqlistb,intm,intn)
{
inti,j,k;
for(i=m;i{
k=i;
for(j=i;jif(b[k].key>b[j].key)k=j;
if(k!
=i)
{
rectemp=b[k];
b[k]=b[i];
b[i]=temp;
}
}
}
voidmerge(intl,intm,inth,sqlistr2)
{
xuanze(r,l,m);
xuanze(r,m,h);
output(r,M);
inti,j,k;
k=i=l;
for(j=m;i{
if(r[i].key<=r[j].key)
{
r2[k]=r[i];
i++;
}
else
{
r2[k]=r[j];
j++;
}
output(r2,M);
}
while(j{
r2[k]=r[j];
j++;
k++;
}
while(i<=m)
{
r2[k]=r[i];
i++;
k++;
}
output(r2,M);
}
private:
sqlistr;
};
voidmain()
{
cout<<"guibingfa1运行结果:
\n";
sqlista,b;
inti,j=0,k=M/2,n=M;
srand(time(0));
for(i=0;i{
a[i].key=rand()%80;b[i].key=0;
}
guibinggx(a);
cout<<"排序前数组:
\n";
gx.output(a,M);
cout<<"数组排序过程演示:
\n";
gx.merge(j,k,n,b);
cout<<"排序后数组:
\n";
gx.output(b,M);
cin.get();
}
2、快速排序
#include
#include
#include
#include
#defineMAXI10
typedefintKeyType;
typedefintElemType;
structrec{
KeyTypekey;
ElemTypedata;
};
typedefrecsqlist[MAXI];
classkuaisu
{
public:
kuaisu(sqlista,intm):
n(m)
{
for(inti=0;i}
voidquicksort(ints,intt)
{
inti;
if(si=part(s,t);
quicksort(s,i-1);
quicksort(i+1,t);
}
elsereturn;
}
intpart(ints,intt)
{
inti,j;
recp;
i=s;j=t;p=b[s];
while(i{
while(i=p.key)j--;
b[i]=b[j];
while(ib[j]=b[i];
}
b[i]=p;
output();
returni;
}
voidoutput()
{
for(inti=0;icout<cout<}
private:
sqlistb;
intn;
};
voidmain()
{
cout<<"kuaisu1.cpp运行结果:
\n";
sqlista1;
inti,n=MAXI,low=0,high=9;
srand(time(0));
for(i=0;ia1[i].key=rand()%80;
kuaisupx(a1,n);
cout<<"数组排序过程演示:
\n";
px.quicksort(low,high);
cout<<"排序后数组:
\n";
px.output();
cin.get();
}
3、hanoi问题递归求解代码:
voidH(charA,charB,charC,intn)
{
if(n>0)
{
H(A,C,B,n-1);
printf(“%dfrom%cto%c”,n,A,C);
H(B,A,C,n-1);
}
}
4、棋盘覆盖问题。
voidchessBoard(inttr,inttc,intdr,intdc,intsize)
{
if(size==1)return;
intt=tile++, //L型骨牌号
s=size/2; //分割棋盘
//覆盖左上角子棋盘
if(dr
//特殊方格在此棋盘中
chessBoard(tr,tc,dr,dc,s);
else{//此棋盘中无特殊方格
//用t号L型骨牌覆盖右下角
board[tr+s-1][tc+s-1]=t;
//覆盖其余方格
chessBoard(tr,tc,tr+s-1,tc+s-1,s);}
//覆盖右上角子棋盘
if(dr
|
=tc+s) //特殊方格在此棋盘中
chessBoard(tr,tc+s,dr,dc,s);
else{//此棋盘中无特殊方格
//用t号L型骨牌覆盖左下角
board[tr+s-1][tc+s]=t;
//覆盖其余方格
chessBoard(tr,tc+s,tr+s-1,tc+s,s);}
//覆盖左下角子棋盘
if(dr>=tr+s&&dc //特殊方格在此棋盘中
chessBoard(tr+s,tc,dr,dc,s);
else{//用t号L型骨牌覆盖右上角
board[tr+s][tc+s-1]=t;
//覆盖其余方格
chessBoard(tr+s,tc,tr+s,tc+s-1,s);}
//覆盖右下角子棋盘
if(dr>=tr+s&&dc>=tc+s)
//特殊方格在此棋盘中
chessBoard(tr+s,tc+s,dr,dc,s);
else{//用t号L型骨牌覆盖左上角
board[tr+s][tc+s]=t;
//覆盖其余方格
chessBoard(tr+s,tc+s,tr+s,tc+s,s);}
}
5、循环赛日程安排问题
#include"stdio.h"
voidTable(intk,inta[][9])
{
intn=1;
for(inti=1;i<=k;i++)n*=2;
for(i=1;i<=n;i++)a[1][i]=i;
intm=1;
for(ints=1;s<=k;s++)
{
n/=2;
for(intt=1;t<=n;t++)
for(i=m+1;i<=2*m;i++)
for(intj=m+1;j<=2*m;j++)
{
a[i][j+(t-1)*m*2]=a[i-m][j+(t-1)*m*2-m];
a[i][j+(t-1)*m*2-m]=a[i-m][j+(t-1)*m*2];}
m*=2;
}
}
main()
{
intk=3;
inta[9][9]={0};
Table(k,a);
for(inti=1;i<=8;i++)
{for(intj=1;j<=8;j++)
printf("%3d",a[i][j]);
printf("\n");
}
}
思考问题:
1、递归的关键问题在哪里?
2、递归与非递归之间程序的转换?
展开阅读全文
相关搜索