3.5 分治算法策略

3.5 分治算法策略

 

所谓分治法就是将问题分而治之。将问题一分为二、一分为三或一分为N等份。对每一等份分别进行解决后,原问题就可以很快得以解决。因此一个问题能否用分治法解决,关键是看该问题是否能将原问题分成n个规模较小而结构与原问题相似的子问题。递归的解决这些子问题,然后合并其结果就得到原问题的解。当n=2时的分治法又称二分法。

分治法解题的一般步骤:

(1)分解,将要解决的问题划分成若干规模较小的同类问题;

(2)求解,当子问题划分得足够小时,用较简单的方法解决;

(3)合并,按原问题的要求,将子问题的解逐层合并构成原问题的解。

当我们求解某些问题时,由于这些问题要处理的数据相当多,或求解过程相当复杂,使得直接求解法在时间上相当长,或者根本无法直接求出。对于这类问题,我们往往先把它分解成几个子问题,找到求出这几个子问题的解法后,再找到合适的方法,把它们组合成求整个问题的解法。如果这些子问题还较大,难以解决,可以再把它们分成几个更小的子问题,以此类推,直至可以直接求出解为止。这就是分治策略的基本思想。

使用分治策略的问题常常要借助递归的结构,逐层求解,当问题规模达到某个简单情况时,解容易直接得出,而不必继续分解。其过程大致如下:

  if 问题不可分then

begin

       直接求解;

       返回问题的解;

      end 

   else begin

        从原问题中划出含1/n运算对象的子问题1;

        递归调用分治法过程,求出解1;

        从原问题中划出含1/n运算对象的子问题2;

        递归调用分治法过程,求出解2;

          …………

        从原问题中划出含1/n运算对象的子问题n;

        递归调用分治法过程,求出解n;

        将解1、解2、……、解n组合成整个问题的解;

       end;

根据分治法的分割原则,原问题应该分为多少个子问题才较适宜?大量实践发现:在用分治法设计算法时,最好是子问题的规模大致相同。通常可以采取二分法,因为这么划分简单而且均匀。

 

例21:赛程问题。

有n个编号为1到n的运动员参加某项运动的单循环比赛,即每个运动员要和所有其他运动员进行一次比赛。试为这n个运动员安排一个比赛日程,使得每个运动员每天只进行一场比赛,且整个比赛在n-1天内结束。输入运动员人数n(n<=10000),输出一个n阶方阵A[1..N,0..N-1],当J>0时,A[1,J]表示第1名运动员在第J天的比赛对手。
  【分析提示】由于N个运动员要进行单循环比赛,且在N-1天内要结束全部比赛,经过分析,当且仅当N为2的整次幂时,问题才有解,当然解是不惟一的。这样可以将运动员分成两组:1,2,…,N/2和N/2+1,N/2+2,…,N。给第一组运动员安排一个比赛日程,得到一个N/2阶的方阵A1;同时给第二组的运动员安排一个比赛日程,同样会得到一个N/2阶的一个方阵A2。考虑到比赛的性质,设定第1个运动员在某一天的比赛对手为第K个运动员,则第K个运动员在同一天的比赛对手必然是第1个运动员,即若有A[1,J]=K,则A[K,J]=1。因此原问题的 解(一个N阶方阵)可以由分解后的两个子问题的解,按下图所示形式合并起来。同时每一个子问题又可以按照上述的二分法分解下去,直至每个组中仅有2个运动员时为止。

{$r+}

const num=8;

var i,j:integer; a:array [1..num,0..num-1] of integer;

procedure arrange(k,n:integer);

var i,j:integer;

begin

 if n=2 then begin a[k,0]:=k;a[k,1]:=k+1;a[k+1,0]:=k+1;a[k+1,1]:=k;end else

  begin

   arrange(k,n div 2);

   arrange(k+n div 2,n div 2);

   for i:=k to k+n div 2 -1 do

    for j:=n div 2 to n-1 do

     a[i,j]:=a[i+n div 2,j-n div 2];

   for i:=k+n div 2 to k+n-1 do

    for j:=n div 2 to n-1 do

     a[i,j]:=a[i-n div 2,j-n div 2];

  end;

end;

begin

 arrange(1,num);

 for i:=1 to num do

  begin for j:=0 to num-1 do write(a[i,j],' '); writeln; end;

end.

 

例22:求一组数中的最大值和最小值。

  我们可以把这n个元素放在一个数组中,用直接比较法求出。算法如下:

 BEGIN

  MIN:=A[1]:MAX:=A[1];

  FOR I:=2 TO N DO

    BEGIN

    IF A[I] > MAX THEN MAX:=A[I];

    IF A[I] < MIN THEN MIN:=A[I];

 END.

上面这个算法需比较2(N-1)次,即时间复杂度是2(N-1)。能否找到更好的算法呢?我们用分治策略来讨论。
   我们把n个元素分成:

A1={A[1],...,A[int(n/2)]}和A2={A[INT(N/2)+1],...,A[N]}两组,分别求这两组的最大值和最小值,然后分别将这两组的最大值和最小值相比较,求出全部元素的最大值和最小值。
    如果A1和A2中的元素多于两个,则再用上述方法各分为两个子集。直至子集中元素至多两个元素为止。
    例如有下面一组元素:
    -13,13,9,-5,7,23,0,5。用分治策略比较的过程如下: 

 

图中每个方框中,左边是最小值,右边是最大值。从图中看出,用这种方法一共比较了10次,比直接比较法的14次减少4次,即约减少了1/3。
    算法如下: 

procedure maxmin(i,j,max,min);

 begin

  case j-i of

    0:max:=a[i];min:=a[i];

    1:if a[i]<a[j] then begin min:=a[i];max:=a[j];end else begin max:=a[i];min:=a[j];end;

    else 

begin

mid:=(i+j) div 2;

    maxmin(i,mid,max1,min1);

        maxmin(mid+1,j,max2,min2);

     IF max1>max2 THEN MAX:=max1 else max:=max2;

     IF min1<min2 THEN MIN:=min1 else min:=min2;

end;

 end;

 

例:归并排序

某数列存储在对序列A[1],A[2],……,A[n],现采用归并思想进行排序。

分析:

这里我们采用二分法。先将n个元素分成两个各含(表示“不大于x的最大整数”)个元素的子序列;再用归并排序法对两个子序列递归的排序;最后合并两个已排序的子序列以得到排序结果。在对子序列排序时,当其长度为1时递归结束。单个元素被视为是已经排好的序列。

下面我们来分析一下对两个已排好序的子序列A[P..Q]和A[Q+1..R],将它们合并成一个已排好的子序列[P..R]。

引入一个辅助过程merge(A,P,Q,R)来完成这一合并工作,其中A是数组,P,Q,R是下标。其方法是:每次选两个子序列中较小的一个元素加入到目标序列中,直到某一个子序列为空,最后把另一子序列中剩下的元素加入到目标序列中。

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

    I := P; J := Q + 1;

    for T:=P to R do begin

      {若左序列剩有元素并且右序列元素全部合并或

      左序列的首元素小于等于右序列的首元素,则左序列的首元素进入合并序列}

      if (J>R) or ((I<Q) and (A[I]<=A[J])) then begin

        Lt[t]:=A[I]; Inc(I); end

      else

        begin {否则右序列的首元素进入合并序列}

         Lt[t]:=A[J]; Inc(J);

        end;

    end; {for}

    A:=Lt; {合并后的序列赋给A}

  end;

 

下面我们来看看分治过程。利用mergesort(A,P,R)对数组A[P..R]进行排序。若P=R, 则子序列只有一个元素,分解完毕。否则,计算出中间下标Q,将A[P..R]分成A[P..Q]和A[Q+1..R]。

procedure MergeSort(var A: ListType; P, R: Integer);

 var

   Q: Integer;

 begin

   if P <> R then begin {若子序列A中不止一个元素}

     Q:=(P+R) div 2;          {计算中间下标Q}

     MergeSort(A, P, Q);     {继续对左子序列A[P..Q]递归排序}

     MergeSort(A, Q + 1, R);   {继续对左子序列A[Q+1..R]递归排序}

     Merge(A, P, Q, R) {对左子序列和右子序列归并排序}

   end;

end;

用Mergesort(A,1,N)便可对整个序列进行归并排序。如果我们自底向上来看这个过程的操作时,算法将两个长度为1的序列合并成排好序的长度为2的序列,继而合并成长度为4的序列……,依次类推。随着算法自底向上执行,被合并的排序序列长度逐渐增加,一直进行到将两个长度为n/2的序列合并成最终排好序的长度为n的序列。图25列出了对序列(5,1,4,6,2,3,2,6)进行归并排序的过程。

 

例30:剔除多余括号

键盘输入一个含有括号的四则运算表达式,可能含有多余的括号,编程整理该表达式,去掉所有多余的括号,原表达式中所有变量和运算符相对位置保持不变,并保持与原表达式等价。

例如:

输入表达式

应输出表达式

A+(b+c)

A+b+c

(a*b)+c/(d*e)

A*b+a/(d*e)

A+b/(c-d)

A+b/(c-d)

注意输入a+b时不能输出b+a。

表达式以字符串输入,长度不超过255,输入不需要判错。

所有变量为单个小写字母。只是要求去掉所有多余括号,不要求对表达式简化。

分析:

对于四则运算表达式,我们分析一下哪些括号可以去掉。

设待整理的表达式为(s1 op s2);op为括号内优先级最低的运算符(“+”,“-”或“*”,“/”);

① 左邻括号的运算符为“/”,则括号必须保留,即…/(s1 op s2)…形式。

② 左邻括号的运算符为“*”或“-”。而op为“+”或“-”,则保留括号,即… *(s1+s2)…或

… -(s1+s2)…或…*(s1-s2)…或…-(s1-s2)…。

③ 右邻括号的运算符为“*”或“/”,而op为“+”或“-”,原式中的op运算必须优先进行,因此括号不去除,即(s1+s2)*…

除上述情况外,可以括号去除,即…s1 op s2…等价于…(s1 op s2)…

我们从最里层嵌套的括号开始,依据上述规律逐步向外进行括号整理,直至最外层的括号保留或去除为止。这个整理过程可以用一个递归过程来实现。

例如,剔除表达式“((a+b)*f)-(i/j)”中多余的括号。依据上述算法进行整理的过程如图所示。

最后,自底向上得到整理结果:(a+b)*f-i/j。

 

 

 

 
   

 

程序如下:

 

program CTSC94_1;

 const

   Inp = 'input.txt';

   Outp = 'output.txt';

 var

   Ch: Char;

   Expr: string;

 

function RightBracket(S:string;I:Byte):Byte;

{在S串中找到下一个运算符的位置}

   var

     Q: Byte; {Q用来记录括号层数}

   begin

     Q := 1;

     repeat

       Inc(I);

       if S[I] = '(' then

         Inc(Q)

       else

         if S[I] = ')' then Dec(Q);

     until Q = 0;

     RightBracket := I;

   end;

 

 function Find(S: string): Byte;

{找到优先级别最低的运算符的位置}

   var

     I, K: Byte;

   begin

     I := 1; K:= 0;

     while I <= Length(S) do

begin

       if (S[I] = '+') or (S[I] = '-') then 

begin Find := I; Exit; end;

       if (K = 0) and ((S[I] = '*') or (S[I] = '/')) then K := I;

       if S[I] = '(' then I := RightBracket(S, I);

       Inc(I);

      end;

     Find := K;

   end;

 

 function DeleteBracket(S: string; var P: Char): string;

{剔除多余括号,S表示要处理的表达式;P表示表达式中最后一个运算符}

   var

     I: Byte;

     Ch1, Ch2: Char;

     Left, Right: string;

   begin

     if Length(S) = 1 then begin  {当表达式中无运算符}

            DeleteBracket := S; P := ' ';

       Exit;

     end;

     if (S[1] = '(') and (RightBracket(S, 1) = Length(S)) then begin

      {当表达式最外层有括号}

       DeleteBracket := DeleteBracket(Copy(S, 2,Length(S)- 2), P);

       Exit; end;

     I := Find(S); {找到最低运算符}

     P := S[I]; 

     Left := DeleteBracket(Copy(S,1,I-1), Ch1);

 {递归处理运算左边}

     Right := DeleteBracket(Copy(S,I+1,Length(S)-I),Ch2); 

     {递归处理运算右边}

     if (P in ['*', '/']) and (Ch1 in ['+', '-']) then

        Left := '(' + Left + ')';     

     if (P in ['*','-']) and (Ch2 in ['+','-']) or (P ='/')and(Ch2 <>' ') then 

        Right := '(' + Right + ')';

     DeleteBracket := Left + P + Right;

 end;

 

 Begin

   Assign(Input, Inp); Reset(Input);

   Readln(Expr);

   Close(Input);

   Assign(Output, Outp); Rewrite(Output);

   Writeln(DeleteBracket(Expr, Ch));

   Close(Output);

 End.

 

分治策略在计算机算法中经常应用,而且大多数分为2个子问题,因此也叫做二分法。例如二分法检索、求方程的根等。
    从上述的分治思想来看,运用分治策略解决的问题一般来说具有以下特点:
    1、原问题可以分解为多个子问题,这些子问题与原问题相比,只是问题的规模有所降低,其结构和求解方法与原问题相同或相似。
    2、原问题在分解过程中,递归地求解子问题,由于递归都必须有一个终止条件,因此,当分解后的子问题规模足够小时,应能够直接求解。
    3、在求解并得到各个子问题的解后,应能够采用某种方式、方法合并或构造出原问题的解。
    利于分治策略求解时,所需时间取决于分解后子问题的个数、子问题的规模大小等因素,而二分法,由于其划分的简单和均匀的特点,是经常采用的一种有效的方法。

posted @ 2010-10-11 21:15  lj_cherish  阅读(586)  评论(0编辑  收藏  举报