acm.DFS深度搜索专题

DFS深搜应用

 

搜索被称为“通用的解题法”,在算法和人工智能方面占有非常重要的地位,特别是在各类ACM程序设计比赛中非常常见,在题目中一般位于中间位置,作为中等难度的题目出现。因此我们需要着重掌握各类的搜索算法,才能面对各类即将到来的ACM大赛。“只要功夫深,铁棒磨成针”,“冰冻三尺,非一日之寒”,要能熟练的掌握和AC比赛中的题目,必须在熟练掌握各类搜索算法的基础上勤加练题,也是唯一的好方法。

本次讲解中,首先给出有关搜索的基本概念,然后针对各类专题,讲解具体的几个例题并加于分析(注:题目全部选自poj中的题目)。

 

 

一 概念介绍

状态(state) 问题在某一时刻的进展情况的数学描述。

 

算符(operator)/ 状态转移 问题从一种状态变换成另一种(或几种)状态的操作。

 

解答树 搜索的过程实际是在遍历一个图,它的结点就是所有的状态,有向边对应于算符,而一个可行解就是一条从起始节点出发到目标状态集中任意一个结点的路径。特个图称为状态空间(state space),这样的搜索称为 状态空间搜索(Single-AgentSearch) ,得到的遍历树称为解答数。

 

基本搜索法:

 

枚举 枚举法的基本思想是根据提出的问题枚举所有可能状态,并用问题给定的条件检验哪些是需要的,哪些是不需要的。能使命题成立,即为其解。枚举算法的特点比较单纯,往往容易写出程序,也容易证明算法的正确性和分析算法的时间复杂度,可以解决一些很小的问题。它的缺点是效率特别低,往往很多题目不能用枚举方法,用了只会超时。所以它适应于容易找到状态并且状态较少的题目。没有信心确定其状态较少,请勿立即动手写程序!

 

 

深度优先搜索DFS(有时称为 回溯法) 遵循的搜索策略是尽可能深地搜索图,在深度优先搜索中,对于最新发现的顶点,如果它还有以此为起点而未探索到的边,就沿此边继续搜索下去。当结点V的所有边都已被探寻过时,搜索将回溯到发现结点V有那条边的始结点。这一过程一直进行到已发现从源结点可达的所有结点为止。如果还存在未被发现的结点,则选择另一个未发现的结点作为新的源结点重新上面的过程,直至所有的结点都搜索到。

 

 

 

 

 

宽度优先搜索 BFS 遵循的搜索策略是从源结点开始,把所有该结点的子结点都搜索完,在搜索每个结点的时候都把其子结点都放入一个队列的后面等待以后搜索,当把此层结点全部搜索完时,所有的下层结点也就进入了队列,继续这样的过程。当仍然没有找到解并且还有没有搜索到的结点时,以没有搜索的结点作为源结点继续上述的搜索过程,直至找到解。

 

剪枝 在搜索的过程中,在还没到达叶结点之前就可以判断以此结点为根的子树不可能包含可行解或者最优解,因此不需要扩展这颗树,就像拿一把剪刀把这颗子树剪去,称为剪枝。

 

还有一些搜索算法的概念没有给出 如分支限界搜索,迭代加深搜索,迭代加宽搜索,A*算法等。他们都各种有适应的范围,也是较为复杂的搜索。

 

专题1深度优先搜索DFS

深度优先搜索有很多呈现形式,一般用递归解决,但也可循环等解决,不管用什么实现,其都会遵循深度优先思想。下面是递归实现的程序框架,供初始学习者参考:

 

ProcedureDepthFirstSearch(State:Statetype;depth:integer);

Begin

    ForOperand:=1 to Operandcount(State) do

    Begin

       NewState:=DoOperand(State,Operand);

       IfAnswer then

           PrintAnswer

       Elseif depth

           Search(NewState,depth);

    End;

End;

 

说明:函数名为DepthFirstSearch。有两个形参Statedepth,分别代表初始状态和搜索的层数。for语句从1开始循环所有能用的算符,循环体里意义是:初始状态上作用一种算符得到新的状态,随后就判断此状态是否为问题的解,如果是的话就输出之,不是的话向下递归,以新的状态作为初始状态,层数加1做为新的层数开始新的判断。这个框架只是最简单的,它有很多可以变形。另外,还有一些地方需要注意。

另给出一个C的深搜框架:(引自张邦佐老师的课件)

void Backtrack(int t)

{    if(t>n)Output(x);

       else

              for(inti=f(n,t);i<=g(n,t);i++)

              {

                     x[t]=h[i];

                     if((Constraint(t)&& Bound(t)) Backtrack(t+1);

              }

}

(1)  循环解决的深搜(此题又像枚举)

1166The Clocks

Description

 
  

There are nine clocks in a 3*3 array (figure 1). The goal is to return all thedials to 12 o'clock with as few moves as possible. There are nine differentallowed ways to turn the dials on the clocks. Each such way is called a move.Select for each move a number 1 to 9. That number will turn the dials 90'(degrees) clockwise on those clocks which are affected according to figure 2below.

Input

Your program is to read from standardinput. Nine numbers give the start positions of the dials. 0=12 o'clock, 1=3o'clock, 2=6 o'clock, 3=9 o'clock.

Output

Your program is to write to standardoutput. Output a shortest sorted sequence of moves (numbers), which returns allthe dials to 12 o'clock. You are convinced that the answer is unique.

Sample Input

3 3 0
2 2 2
2 1 2

Sample Output

4 5 8 9
 
大概题意
有九个时钟,每个时钟只有4种状态:12:00,3:00,6:00,9:00。分别用0,1,2,3代替。现在有9种操作,如表2所示,每种操作能使对应的钟顺时针旋转90度。
现在给出9个时钟的初始状态,问怎么用最少的操作使得这9个时钟都指向12:00。
输入给出9个数字,用1,2,3,4代表每个时钟的初始状态。输出要求给出操作代号,此操作代号按升序排列。
 
解题思想:
可以观察到这九个时钟都是按顺时针旋转,由于求最少的操作数,我们知道一个时钟如果旋转4次,那么它就回到了初始位置,相当于没有操作,那么上面的4次操作是多余的,不可能成为解,因此可以得出结论,每个操作,如操作1 ABDE 最多只需要操作3次,最少是不操作。那么9个时钟的有4^9种方法,此数字并不是很大,完全可以在有限的1000ms运行出结果。因此可以用最笨的方法解决。本题如果先不对程序加以分析和解剖,洞察问题的本质和寻找规律,那么将是非常严重的问题,不但效率低,甚至严重超时。
 
 
 
 
 
参考代码
#include 
using namespace std;
int temp[9];//存储每一列时钟的状态
int main()
{
    int move[9][9]={     //操作表,对对应有操作为1
    {1,1,0,1,1,0,0,0,0},//1操作
    {1,1,1,0,0,0,0,0,0},//2操作
    {0,1,1,0,1,1,0,0,0},//3操作
    {1,0,0,1,0,0,1,0,0},//4操作
    {0,1,0,1,1,1,0,1,0},//5操作
    {0,0,1,0,0,1,0,0,1},//6操作
    {0,0,0,1,1,0,1,1,0},//7操作
    {0,0,0,0,0,0,1,1,1},//8操作
    {0,0,0,0,1,1,0,1,1} //9操作
    };
    int ways[9];          //各操作的遍数
    bool find;
    int clock[9];         //时钟的初始状态
    int i,j;
    for (i=0;i<9;i++)
    {
          cin>>clock[i];
    }
    //下面9层循环遍历了所有可能的出现的情况
    for (ways[0]=0;ways[0]<4;ways[0]++)
    for (ways[1]=0;ways[1]<4;ways[1]++)
    for (ways[2]=0;ways[2]<4;ways[2]++)
    for (ways[3]=0;ways[3]<4;ways[3]++)
    for (ways[4]=0;ways[4]<4;ways[4]++)
    for (ways[5]=0;ways[5]<4;ways[5]++)
    for (ways[6]=0;ways[6]<4;ways[6]++)
    for (ways[7]=0;ways[7]<4;ways[7]++)
    for (ways[8]=0;ways[8]<4;ways[8]++)
    {
          find=true;
          for (i=0;i<9;i++)      //对每个时钟旋转
          {
                temp[i]=clock[i];
                for (j=0;j<9;j++)
                {
                      temp[i]+=ways[j]*move[j][i];
                }
                temp[i]%=4;
                if(temp[i]!=0)//判断i号钟是否指向12点
                {
                      find=false;
                      break;
                }
          }
          if(find)
          {
                for (i=0;i<9;i++)
                {
                      for (j=0;j 
  
                      {
                            cout< 
  
                      }
                }
                cout< 
  
                return 0;
          }
    }
    return 0;
}
 
 
 
 
 
(2) 递归解决的简单搜索
1562 Oil Deposits

Description

The GeoSurvComp geologic survey company isresponsible for detecting underground oil deposits. GeoSurvComp works with onelarge rectangular region of land at a time, and creates a grid that divides theland into numerous square plots. It then analyzes each plot separately, usingsensing equipment to determine whether or not the plot contains oil. A plotcontaining oil is called a pocket. If two pockets are adjacent, then they arepart of the same oil deposit. Oil deposits can be quite large and may containnumerous pockets. Your job is to determine how many different oil deposits arecontained in a grid.

Input

The inputcontains one or more grids. Each grid begins with a line containing m and n,the number of rows and columns in the grid, separated by a single space. If m =0 it signals the end of the input; otherwise 1 <= m <= 100 and 1 <= n<= 100. Following this are m lines of n characters each (not counting theend-of-line characters). Each character corresponds to one plot, and is either`*', representing the absence of oil, or `@', representing an oil pocket.

Output

are adjacent horizontally, vertically, ordiagonally. An oil deposit will not contain more than 100 pockets.

Sample Input

1 1
*
3 5
*@*@*
**@**
*@*@*
1 8
@@****@*
5 5 
****@
*@@*@
*@**@
@@@*@
@@**@
0 0

SampleOutput

0
1
2
2
 
大概题意

有一个GeoSurvComp地质勘探公司正在负责探测地底下的石油块。这个公司在一个时刻调查一个矩形区域,并且创建成一个个的格子用来表示众多正方形块。它然后使用测定设备单个的分析每块区域,决定是否有石油。一块有石油小区域被称为一个pocket,假设两个pocket是相邻的,然后他们就是相同石油块的一部分,石油块可能非常的大并且包涵很多的pocket。你的任务就是在一个网格中存在多少个石油块。输入首先给出图的大小,然后给出这个图。*代表没有石油,@代表存在石油。输出每种情况下石油块的个数。

解题思想:

对于个给出的图,用一个整型的二维数组代表有无石油,然后循环这个二维数组,如发现存在石油,则以这个方格为起始结点进行深搜,如发现石油就标记为-1,然后继续遍历这个二维数组,直至找到下一个有石油的方格,找到一个计数加1。最后的计数即为所求的石油的方块数。

参考代码
·         #include
·         using namespace std;
·         void seach(int x,int y);
·         char oil[100][100];
·         bool visit[100][100];
·         int m,n;
·         int go[8][2]={{-1,-1},{0,-1},{1,-1},{1,0},{1,1},{0,1},{-1,1},{-1,0}};//算符存入一个二维数组里
·         void seach(int x,int y)
·         {
·          int i;
·          visit[x][y]=true;
·          for (i=0;i<8;i++)
·          {
·           if((x+go[i][0])>=0&&(x+go[i][0])=0&&(y+go[i][1]) 
  
·                {
·                       seach(x+go[i][0],y+go[i][1]);
·                }
·          }
·          return;
·          
·         }
·         int main()
·         {
·          int j;
·          while (cin>>m>>n&&(m||n))
·          {
·          
·                for (int i=0;i 
  
·                {
·                       for (j=0;j 
  
·                       {
·                             cin>>oil[i][j];
·                             visit[i][j]=false;
·                       }
·                }
·                int count=0;//计数值
·                for (i=0;i 
  
·                {
·                       for (j=0;j 
  
·                       {
·                                if(oil[i][j]=='@'&&!visit[i][j])
·                             {
·                                   seach(i,j);
·                                   count++;
·                             }
·                       }
·                }
·                cout< 
  
·          }
·          return 0;
·         }
·         //做poj 1175 starry night
(3) 递归解决的带剪枝的深搜
2248 Addition Chains

Description

An addition chain for n is an integersequence with the following four properties:

  • a0 = 1
  • am = n
  • a0 < a1 < a2 < ... < am-1 < am
  • For each k (1<=k<=m) there exist two (not necessarily different) integers i and j (0<=i, j<=k-1) with ak=ai+aj


You are given an integer n. Your job is to construct an addition chain for nwith minimal length. If there is more than one such sequence, any one isacceptable.
For example, <1,2,3,5> and <1,2,4,5> are both valid solutions whenyou are asked for an addition chain for 5.

Input

The input will contain one or more testcases. Each test case consists of one line containing one integer n(1<=n<=100). Input is terminated by a value of zero (0) for n.

Output

For each test case, print one linecontaining the required integer sequence. Separate the numbers by one blank.
Hint: The problem is a little time-critical, so use proper break conditionswhere necessary to reduce the search space.

Sample Input

5
7
12
15
77
0

Sample Output

1 2 4 5
1 2 4 6 7
1 2 4 8 12
1 2 4 5 10 15
1 2 4 8 9 17 34 68 77
 
大概题意
一个整数n的加法链就是一个满足下面性质的序列:
  • a0 = 1
  • am = n
  • a0 < a1 < a2 < ... < am-1 < am
  • 对于每个k (1<=k<=m) 存在两个整数 (不一定不同) i 和 j (0<=i, j<=k-1) 有ak=ai+aj

你被给定一个n,你的工作是构造一个最短长度的加法链,假设这样的序列超过一个,任何一个合适的序列均可。输入包括很多种测试实例,每个测试实例由一行组成,即就是n,然后输出这个加法链的数字序列。

解题思想:

由于每个数字都是由前面的两个数字之和。并且要使这个数字链最短。那么我们可以深搜一下,假设后面的数是由前面的两个数字的和,前面的两个数是我们可以逐渐往前扫描,那么算符就是前面的两个数相加,得到后面的数,这样深搜下去,并且记录找到解后这个数据链的长度。当数据长度最短时即为所求的解,这里为了加快寻找速度,对于一些不必要搜索的我们不需要去搜索,于是就把它剪去。

参考代码

#include

#include

usingnamespace std;

int n,page,ans, a[100], r[100], d[202];

 

voiddfs(int);

voidprint();

void init();

int main()

{

        while(scanf("%d", &n) &&n)

        {

              init();

              page=0;

              dfs(0);

              print();

        }

              return 0;

}

void init()

{

        int i;

        ans = n;

        a[0] = 1;

        for(i = n; i <= n + n; i ++)

               d[i] = 0;

        for(i = n - 1; i > 0; i --) 

               d[i] = d[i + i] + 1;          //d[i]中存放到给定数字的最短的距离数

}

 

void print()

{

        int i;

        for(i = 0; i < ans; i ++)

        {

              printf("%d ", r[i]);

        }

        printf("%d\n", n);

}

void dfs(intl)

{

        int i, k;

        if(l + d[a[l]] >= ans)            //剪枝,当搜索到的长度加上从此数到给定的数最短 //的距离的值比已搜到的最短的距离还要长时,则不需要深搜下去,直接跳出,继续相邻结//点的搜素

        {

               return; 

        }

        if(a[l] == n)

        {

              ans = l;

              for(i = 0; i < l; i ++)

              {

                     r[i]= a[i];

              }

              return;

        }

        for(i = l; i >= 0; i --)           //此两层循环即为循环所有算符,以找到新的状态

        {

              for(k = i; k >= 0; k --)

              {

                     a[l + 1] = a[i] + a[k];   //后一个数由前两个个值之和得到

                     if(a[l + 1] > a[l]&& a[l + 1] <= n)   //排除不符合的情况

                            dfs(l + 1);

              }

        }

}

专题2宽度优先搜索BFS

(1)从结点V开始,给V标上已到达(或访问)标记——此时称结点V还没有被检测,而当算法访问了邻接于某结点的所有结点时,称该结点被检测了。
(2)访问邻接于V且尚未被访问的所有结点——这些结点是新的未被访问的结点。将这些结点依次放置到一个未检测结点表(队列Q)中(末端插入)。
(3)标记V已被检测。
(4)若未检测结点表为空,则算法终止;否则从未检查结点表的表头取一结点作为下一个待检测结点。
按照广度优先的顺序遍历状态空间,一般用open,close表来实现。不要用循环队列,因为需要保存已出列的结点。算法框架如下:
Procedure BreadthFirstSearch(InitialState:StateType);
Begin
        Enqueue(InitialState);
        While Not EmptyQueue do
        Begin
               Dequeue(State);
               For Operand:=1 to OperandCount(State) do
               Begin
                       NewState:=DoOperand(State,Operand);
                       If Answer(NewState) then PrintAnswer;
                       Else Enqueue(NewState);
               End;
        End;
End;
说明:函数名为BreadthFirstSearch,只有一个形参InitialState表示初始状态。
函数体内开始是一个进队列函数,把初始状态进队列,然后就是一个循环语句,如果队列不为空则循环,循环体内是首先把队列的头结点取出,然后循环算符,产生此结点的所有后继结点,若出现解,者打印输出,若还没出现解则入队列。
(1) 常见求最少步数宽搜
2243 Knight Moves

Description

A friend of you is doing research on theTraveling Knight Problem (TKP) where you are to find the shortest closed tourof knight moves that visits each square of a given set of n squares on achessboard exactly once. He thinks that the most difficult part of the problemis determining the smallest number of knight moves between two given squaresand that, once you have accomplished this, finding the tour would be easy.
Of course you know that it is vice versa. So you offer him to write a programthat solves the "difficult" part.

Your job is to write a program that takes two squares a and b as input and thendetermines the number of knight moves on a shortest route from a to b.

Input

The input will contain one or more testcases. Each test case consists of one line containing two squares separated byone space. A square is a string consisting of a letter (a-h) representing thecolumn and a digit (1-8) representing the row on the chessboard.

Output

For each test case, print one line saying"To get from xx to yy takes n knight moves.".

Sample Input

e2 e4
a1 b2
b2 c3
a1 h8
a1 h7
h8 a1
b1 c3
f6 f6

Sample Output

To get from e2 to e4 takes 2 knight moves.
To get from a1 to b2 takes 4 knight moves.
To get from b2 to c3 takes 2 knight moves.
To get from a1 to h8 takes 6 knight moves.
To get from a1 to h7 takes 5 knight moves.
To get from h8 to a1 takes 6 knight moves.
To get from b1 to c3 takes 1 knight moves.
To get from f6 to f6 takes 0 knight moves.
 
大概题意:
一个8行8列的棋谱,任意给出knight的两个位子,问按照knight的走法,从其中的一个位子出发,问至少需要经过几步到达另外一个位子。 行用1-8表示,列用a-h表示。
输出格式见上。
 
解题思想:
起始位子作为初始结点进入队列,然后取出第一个结点,把knight能走的位子依次全部进入队列,在进入队列前判断是否为终结点。开辟一个二维数组记录步数。

参考代码:
·         #include 
·         #include
·         using namespace std;
·         struct point
·         {
·          int x,y;
·         };
·         int chessboard[8][8];//记录步数
·         vector v;
·         point p;
·         bool find;
·         int index;
·         int start_x,start_y,end_x,end_y;
·         void deal(int x,int y,int time)
·         {
·          if(x==end_x&&y==end_y)
·          {
·                find=true;
·                return;
·          }
·          if(x-2>=0&&y-1>=0&&chessboard[x-2][y-1]==-1)
·          {
·                chessboard[x-2][y-1]=time+1;
·                p.x=x-2;
·                p.y=y-1;
·                v.push_back(p);
·          }
·          if(x-2>=0&&y+1<8&&chessboard[x-2][y+1]==-1)
·          {
·                chessboard[x-2][y+1]=time+1;
·                p.x=x-2;
·                p.y=y+1;
·                v.push_back(p);
·          }
·          if(x+2<8&&y+1<8&&chessboard[x+2][y+1]==-1)
·          {
·                chessboard[x+2][y+1]=time+1;
·                p.x=x+2;
·                p.y=y+1;
·                v.push_back(p);
·          }
·          if(x+2<8&&y-1>=0&&chessboard[x+2][y-1]==-1)
·          {
·                chessboard[x+2][y-1]=time+1;
·                p.x=x+2;
·                p.y=y-1;
·                v.push_back(p);
·          }
·          if(x-1>=0&&y+2<8&&chessboard[x-1][y+2]==-1)
·          {
·                chessboard[x-1][y+2]=time+1;
·                p.x=x-1;
·                p.y=y+2;
·                v.push_back(p);
·          }
·          if(x-1>=0&&y-2>=0&&chessboard[x-1][y-2]==-1)
·          {
·                chessboard[x-1][y-2]=time+1;
·                p.x=x-1;
·                p.y=y-2;
·                v.push_back(p);
·          }
·          if(x+1<8&&y+2<8&&chessboard[x+1][y+2]==-1)
·          {
·                chessboard[x+1][y+2]=time+1;
·                p.x=x+1;
·                p.y=y+2;
·                v.push_back(p);
·          }
·          if(x+1<8&&y-2>=0&&chessboard[x+1][y-2]==-1)
·          {
·                chessboard[x+1][y-2]=time+1;
·                p.x=x+1;
·                p.y=y-2;
·                v.push_back(p);
·          }
·          
·         }
·         int main()
·         {
·          
·          char _s,_e;
·          int s,e;
·          while (cin>>_s>>s>>_e>>e)
·          {
·                start_x=(int)(_s-'a');
·                start_y=s-1;
·                end_x=(int)(_e-'a');
·                end_y=e-1;
·                memset(chessboard,-1,sizeof(chessboard));
·                chessboard[start_x][start_y]=0;
·                p.x=start_x;
·                p.y=start_y;
·                v.clear();//注意:别忘了v的清空
·                v.push_back(p);
·                find=false;
·                index=0;
·                while (!find)
·                {
·                 deal(v[index].x,v[index].y,chessboard[v[index].x][v[index].y]);
·                       index++;
·                }
·                cout<<"To get from "<<_s< 
  
·          }
·          return 0;
·         }
(2) 宽搜经典
3278 Catch That Cow

Description

Farmer Johnhas been informed of the location of a fugitive cow and wants to catch herimmediately. He starts at a point N (0 ≤ N ≤ 100,000) on a numberline and the cow is at a point K (0 ≤ K ≤ 100,000) on the samenumber line. Farmer John has two modes of transportation: walking andteleporting.

* Walking: FJcan move from any point X to the points X - 1 or X + 1 in a single minute
* Teleporting: FJ can move from any point X to the point 2 × X ina single minute.

If the cow,unaware of its pursuit, does not move at all, how long does it take for FarmerJohn to retrieve it?

Input

Line 1: Two space-separated integers: Nand K

Output

Line 1: The least amount of time, inminutes, it takes for Farmer John to catch the fugitive cow.

Sample Input

5 17

Sample Output

4

Hint

The fastest way for Farmer John to reachthe fugitive cow is to move along the following path: 5-10-9-18-17,which takes 4 minutes.

 

大概题意:
 
Farmer John想抓到一头牛,Farmer John在一个数轴的一个位子,牛在数轴的另一个位子。但Farmer John这个人在一个单位时间内可以有三种走的方式,一是走到数轴的下一格,或是前一格,或是他的数轴的两倍的格子上,为此人最快抓到牛的时间。
 
解题思想:
其实是上面的一个变形,棋子变成了一个人和一头牛,然后走的规则变了一下,问的步数变成了时间。所以模板还是基本和上面的一样。
参考代码:
·         #include 
·         #include 
·         using namespace std;
·         int digit[200001];
·         int start,end;
·         int index;
·         bool find;
·         vectorvc;
·         void deal(int x,int time)
·         {
·          if (x==end) 
·          {
·                find=true;
·                return;
·          }
·          if (x+1<=100000&&digit[x+1]==-1)
·          {
·                digit[x+1]=time+1;
·                vc.push_back(x+1);
·          }
·          if (x-1>=0&&digit[x-1]==-1)
·          {
·                digit[x-1]=time+1;
·                vc.push_back(x-1);
·          }
·          if (x*2<=200000&&digit[x*2]==-1)
·          {
·                digit[x*2]=time+1;
·                vc.push_back(x*2);
·          }
·         }
·         int main()
·         {
·          cin>>start>>end;
·          memset(digit,-1,sizeof(digit));
·          vc.clear();
·          vc.push_back(start);
·          digit[start]=0;
·          index=0;
·          find=false;
·          while (index 
  
·          {
·                deal(vc[index],digit[vc[index]]);
·                index++;
·          }
·          cout< 
  
·          return 0;
·         }
 
 

专题3剪枝问题(摘自《算法艺术与信息学竞赛》)

 

剪枝是一个非常有趣的课题,往往需要发挥自己的创造性与想象力,同时需要有敏感的观察力和一定的经验。

主要的剪枝思想有三种:极端法调整法数学方法

极端法 极端法广泛地应用在各种搜索算法的剪枝中。它的基本思想是通过对当前结点进行理想式扩展,通过否认这样的“理想情况”来避免对当前结点的扩展。

调准法 调准法的基本思想是通过对子树的比较剪掉重复子树和明显不是最有“前途”的子树。

数学方法 上面的两种方法为一般方法,然而对于一些具体问题,也可以应用一些专门的知识进行剪枝。例如在图论中借助连通分量,数论中借助模方程的分析等。

 

极端法在上面的2248 Addition Chains有很好的体现,对于搜索中到达的数,如果按照理想方式(即最少步数)到达所给定的数的总和比当前找到的最少的步数还大,那么就不需要扩展这个子树。
 
调准法举例:

1011 Sticks

Description
乔治拿来一组等长的木棒,将它们随机地裁断,使得每一节木棍的长度都不超过50个长度单位。然后他又想把这些木棍恢复到为裁截前的状态,但忘记了初始时有多少木棒以及木棒的初始长度。请你设计一个程序,帮助乔治计算木棒的可能最小长度。每一节木棍的长度都用大于零的整数表示。

Input
输入包含多组数据,每组数据包括两行。第一行是一个不超过64的整数,表示砍断之后共有多少节木棍。第二行是截断以后,所得到的各节木棍的长度。在最后一组数据之后,是一个零。

Output
为每组数据,分别输出原始木棒的可能最小长度,每组数据占一行。

Sample Input

9
5 2 1 5 2 1 5 2 1
4
1 2 3 4
0

Sample Output

6
5

 

解题思想:
此题是一个经典的深度优先搜索和剪枝的题目,对程序设计的技巧性要求非常大。一堆杂乱长度不等的木头,怎样才能把他们分组使得每一组长度之和相等呢?首先,每组的长度一定是所有木头长度和的约数,即所有木头长度之和能被每组木头长度之和整除。问题要我们找原始木头可能最小的长度,那么我们可以编制一个每组木头长度之和的循环,循环的初始值为木头中最大的长度,循环的边界是所有木头长度之和,这样就保证了得到可能最小长度的木头。当循环中的数不能整除所有木头的长度时我们就跳过,直至找到下一个可以整除的数。
当我们找到一个约数时,此时就需要尝试着去拼接了。我们把木头按大到小进行排序。这样做一是减少运行时间,二是方便于剪枝。我们编写这样一个函数             deal(int n,int m,int left,int len)
n是木头的数目,m是剩下还没有拼接的木头的数目,left是剩下的需要去找拼接的木头的长度,需要拼接的木头的长度,初始值为n,n,0,i。当找到一段木头时,m就减1。Left就减去找到的木头的长度,当left为0并且还有木头等待去找是就把left赋len。
显然只按上面去编写的程序效率仍然不是很高,程序中还执行一些不必要去做的工作,此时我们就需要剪枝了,怎么剪呢?可以找到两种可以剪去的枝。1:当我们按顺序找到一段刚好可以满足要求使得长度为要求的长度时,我们就没有必要再去拼接那些更小的长度的木头使得满足要求,即使找到,那么此种能拼接成的组合没有大木头更有希望获得可行解。所以当用大木头去填充而获不到可行解时,那么就没有必要去考虑更小的木头了。2:当我们拼接好一段木头时突然发现接下来以另一根最大的木头为底去拼接而不可行时,那么就没有必要再去考察后面小的木头做底去拼接了,因为所有木头都必须参与拼接,那个拼不了的最大的木头始终要参与拼接,所以最后还是会失败!考虑完了深搜和剪枝就可以编写程序了。
 
参考代码:
#include 
using namespace std;
int start;
int stricks[64];
bool isuse[64];
int compare(const void *ele1,const void *ele2)
{
        return *(int*)ele2-*(int*)ele1;
}
bool deal(int n,int m,int left,int len)
{
        int i;
        if(m==0&&left==0) return true;
        if(left==0)
        {
               i=0;
               left=len;
        }
        else
        {
               i=start+1;
        }
        for (;i 
  
        {
               if(isuse[i]||stricks[i]>left) continue;
               isuse[i]=true;
               start=i;
               if(deal(n,m-1,left-stricks[i],len)) return true;
               else
                       isuse[i]=false;
              if(left==stricks[i]||left==len) break;//两种情况下的剪枝
        }
        return false;
}
int main()
{
        int i,n,sum;
        while (cin>>n)
        {
               if(n==0) break;
               sum=0;
               for (i=0;i 
  
               {
                       cin>>stricks[i];
                       sum+=stricks[i];
                       isuse[i]=false;
               }
               qsort(stricks,n,sizeof(int),compare);
               for (i=stricks[0];i<=sum;i++)
               {
                       if(sum%i!=0) continue;
                       if(deal(n,n,0,i))
                       {
                               cout< 
  
                               break;
                       }
               }
        }
        return 0;
}
 
数学方法举例:
 
 

1190 生日蛋糕

Description

7月17日是Mr.W的生日,ACM-THU为此要制作一个体积为Nπ的M层生日蛋糕,每层都是一个圆柱体。
设从下往上数第i(1<= i <= M)层蛋糕是半径为Ri, 高度为Hi的圆柱。当i < M时,要求Ri > Ri+1且Hi > Hi+1。
由于要在蛋糕上抹奶油,为尽可能节约经费,我们希望蛋糕外表面(最下一层的下底面除外)的面积Q最小。
令Q = Sπ
请编程对给出的N和M,找出蛋糕的制作方案(适当的Ri和Hi的值),使S最小。
(除Q外,以上所有数据皆为正整数)

Input

有两行,第一行为N(N <= 10000),表示待制作的蛋糕的体积为Nπ;第二行为M(M <= 20),表示蛋糕的层数为M。

Output

仅一行,是一个正整数S(若无解则S = 0)。

Sample Input

100
2

Sample Output

68

Hint

圆柱公式
体积V = πR2H
侧面积A' = 2πRH
底面积A = πR2

 
解题思想:
这是一道很好的深搜加剪枝问题。难度较大!我们分两步来解决这个问题:1.怎么进行深搜。2.在深搜的基础上怎么剪枝使得效率变高。
首先解决深搜的问题。我们可以考虑用递归解决!即问题的结果先要子问题得到解决,问题在递归中越来越小,直至子问题可以直接解决。我们先构造一个递归函数,函数的参数应该是层数n,剩下的体积v,上一层的半径r(以后的层的半径都必须小于这个半径),上一层的高度h,此时的表面积s,可以这样生成一个递归函数deal(n,,r,h,s)。初始值
deal(M,N,-1,-1,0)。首先考虑第一层蛋糕的可能半径和高度。由于半径和高度都必须是整数,那么最高一层应该就是1,下面是2,3....,这是边界的情况。那么第n层的半径最小是n,最大是只有一层且高度最小的半径。那么高度如何定界呢?同样最小值是n,n层上面的蛋糕的总体积有最小值,h最大值为(v-上面蛋糕体积最小值)/(r*r).故构成两层循环。当不是初始值稍微有点变化,半径的最大是上一层的半径减1.高度的最大是(v-上面蛋糕体积最小值)/(r*r)和h-1的最小值。
     我们再来考虑一下剪枝的问题。剪枝有三种方法,极端法,调准法,数学法。我们先来看看极端法。递归参数中当v的体积小于和大于上层蛋糕体积最小值和最大值时将是不可能出现的,故可以剪掉。再看看数学方法:
     假设我们确定了前i层的体积为T,表面积为W,那么剩下的M-i层体积为:
N-T=
而剩余的表面积满足:(注意下面式中少了一个2)
LeftS=
显然,如果PS-W,根据不等式的传递性LeftSS-W,即LeftS+WS,显然应该剪枝。
 
参考代码(网上参见的代码):
 
#include 
#include 
#include 
using namespace std;
int N,V,goodr,MinV[30],maxv[30][101][101];
int MaxV(int n,int r,int h){
    int s;
    s=0;
    if (maxv[n][r][h]>-1)
                 return maxv[n][r][h];
    for (int i=0;i 
  
                 s+=(r-i)*(r-i)*(h-i);
    return maxv[n][r][h]=s;
}
int Min(int a,int b){return a>b?b:a;}
int f(int n,int v,int r,int h,int now)//递归函数
{
    if (n==0){if (v==0)return now;return 100000000;}//递归出口
    if (v<0 || now>goodr)return 100000000;//剪枝1---当前面积已大于最优解面积
    int m;
    if (r==-1)//初始状态的递归
    {
       for (int i=n;i*i<=v;i++)
                 for (int j=(v-MinV[n-1])/i/i;j>=n;j--)
                 {
                          m=f(n-1,v-i*i*j,i,j,i*i+2*i*j);
                          if (m 
  
                  }
       return goodr;
    }
    if (v 
  
    if (v>MaxV(n,r-1,h-1))return 100000000;//剪枝3(体积向上极端)
    if (r>1)if (now+2*v/(r-1)>goodr)return 100000000;//剪枝4 上面的数学方法
    m=100000000;
    for (int i=r-1;i>=n;i--)//一般状态的递归
                 for (int j=Min(((v-MinV[n-1])/i)/i,h-1);j>=n;j--)
                 {
                          int w;
                          w=f(n-1,v-i*i*j,i,j,2*i*j+now);
                          if (w 
  
                 }
    return m;
}     
int main()
{
    memset(maxv,0xff,sizeof(maxv));
    MinV[0]=0;
    for (int i=1;i<30;i++)MinV[i]=MinV[i-1]+i*i*i;
    goodr=100000000;
    scanf("%d %d",&V,&N);
    int s=f(N,V,-1,-1,0);
    if(s==100000000)
         cout<<"0"< 
  
else cout< 
  
    return 0;
}
 
 
题目推荐:
 
1979 Red and Black(灵活掌握的简单深搜题)
1118 Lining Up(经常可以碰到这类题的变形)
1176 Party Lamps(考察对题目实质的探索)
2362 Square(剪枝)
1326 prime path(数的一个简单枚举)
 
 
 
 
 
 
 
 好久不写博,今天又学快排,想想自己也只是知道思想,不曾真正写过。找了个ACM题练手Hdu1106,主要是ACM的有数据,方便知道自己的对不对。
    写的时间虽然久了点,但是弄出来了,还是有成就感的,没看书什么的,只凭指导思想自己思考的那种方式写的,比较喜欢这种Coding的方式。好了,废话少说,上菜。
/**
@r:要排序的数组
@s:排的起点,从0开始
@e:排的终点,从n-1开始
*/
void quicksort(int r[1001],int s,int e)
{
   int t = r[s];//哨兵,为开头的那个
   int f = s+1;
   int b = e;//f为前向指针,从s+1开始,b为反向指针,从e开始
   int m = 0;
   if(s>=e)return;//退出条件
   
   while(f<=b)
   {
       while(f<=b&&r[f]<=t) f++;//在前面找比哨兵大的元素
       while(f<=b&&r[b]>=t) b--;//在后面找比哨兵小的元素
       //交换这两个元素
       if(f
            m = r[f];
            r[f] = r[b];
            r[b] = m;
            f++;    b--;
       }
   }
   //交换哨兵和r[b],r[b]肯定要比哨兵小
   r[s] = r[b];
   r[b] = t;
   //排两边的
   quicksort(r,s,b-1);
   quicksort(r,b+1,e);
}
注意有重复键的这种情况,比如要排的数据是1,1,1,1。这个bug调了好久。
写的时候,最好在纸上画画,结合调试(我有点太依赖调试了,所以费时间),最佳实践啦~~
作者“青禾”

 

#include

int partions(int l[],intlow,int high)

{

  int prvotkey=l[low];

  l[0]=l[low];

  while (low

  {

     while(low=prvotkey)

     --high;

     l[low]=l[high];

     while(low

     ++low;

     l[high]=l[low];

  }

  l[low]=l[0];

  return low;

}

void qsort(int l[],intlow,int high)

{

  int prvotloc;

  if(low

{

     prvotloc=partions(l,low,high);    //将第一次排序的结果作为枢轴

     qsort(l,low,prvotloc-1); //递归调用排序 由low 到prvotloc-1

     qsort(l,prvotloc+1,high); //递归调用排序 由 prvotloc+1到high

}

}

void quicksort(int l[],intn)

{

qsort(l,1,n); //第一个作为枢轴 ,从第一个排到第n个

}

void main()

{

  int a[200],n,i;

  printf("n=");

  scanf("%d",&n);

  for (i=1;i<=n;i++)

    scanf("%d",&a[i]);

  quicksort(a,n);

  printf("right number:");

  for(i=1;i<=n;i++)

    printf("%d ",a[i]);

  printf("\n");

}

你可能感兴趣的:(acm)