代码改变世界

回溯深搜与剪枝初步

2015-04-19 23:31  星星之火✨🔥  阅读(...)  评论(...编辑  收藏

回溯算法也称试探法,一种系统的搜索问题的解的方法,是暴力搜寻法中的一种。回溯算法的基本思想是:从一条路往前走,能进则进。回溯算法解决问题的一般步骤:

  • 根据问题定义一个解空间,它包含问题的解
  • 利用适于搜索的方法组织解空间
  • 利用深度优先法搜索解空间,并且在搜索过程中用剪枝函数避免无效搜索

回溯法采用试错的思想,它尝试分步的去解决一个问题。在分步解决问题的过程中,当它通过尝试发现现有的分步答案不能得到有效的正确的解答的时候,它将取消上一步甚至是上几步的计算,再通过其它的可能的分步解答再次尝试寻找问题的答案。回溯法通常用最简单的递归方法来实现,在反复重复上述的步骤后可能出现两种情况:

  找到一个可能存在的正确的答案

  在尝试了所有可能的分步方法后宣告该问题没有答案

在最坏的情况下,回溯法会导致一次复杂度为指数时间的计算,因此通常只有对小规模输入问题求解的时候采用回溯法,并且用剪枝函数来提速。

使用了剪枝技术的回溯法是一个即带有系统性又带有跳跃性的搜索算法。回溯法在用来求问题的所有解时,要回溯到根,且根节点的所有子树都已被搜索遍才结束。而如果用来求问题的任一解时,只要搜索到问题的一个解就可以结束。简单的介绍过后,还是看一道暴经典的习题吧,题目取自HDU_1010:Tempter of the Bone

Problem Description
The doggie found a bone in an ancient maze, which fascinated him a lot. However, when he picked it up, the maze began to shake, and the doggie could feel the ground sinking. He realized that the bone was a trap, and he tried desperately to get out of this maze.

The maze was a rectangle with sizes N by M. There was a door in the maze. At the beginning, the door was closed and it would open at the T-th second for a short period of time (less than 1 second). Therefore the doggie had to arrive at the door on exactly the T-th second. In every second, he could move one block to one of the upper, lower, left and right neighboring blocks. Once he entered a block, the ground of this block would start to sink and disappear in the next second. He could not stay at one block for more than one second, nor could he move into a visited block. Can the poor doggie survive? Please help him.
 
Input
The input consists of multiple test cases. The first line of each test case contains three integers N, M, and T (1 < N, M < 7; 0 < T < 50), which denote the sizes of the maze and the time at which the door will open, respectively. The next N lines give the maze layout, with each line containing M characters. A character is one of the following:

'X': a block of wall, which the doggie cannot enter; 
'S': the start point of the doggie; 
'D': the Door; or
'.': an empty block.

The input is terminated with three 0's. This test case is not to be processed.
 
Output
For each test case, print in one line "YES" if the doggie can survive, or "NO" otherwise.
 
Sample Input
4 4 5
S.X.
..X.
..XD
....
3 4 5
S.X.
..X.
...D
0 0 0

Sample Output
NO
YES

一看是迷宫问题,一般就提示我们回溯+DFS:

#include<stdio.h> // 62MS
#include<stdlib.h>
int des_row, des_col;
int m, n, t;
bool escape;
int dir[4][2] = {{0, -1}, {-1, 0}, {0, 1}, {1, 0}}; // dirction:left, up, right, down
int map[7][7];
void dfs(int row, int col, int step);
int main(void)
{
    int start_row, start_col;

    int wall;
    while(scanf("%d%d%d", &n, &m, &t))
    {
        wall = 0;
        escape = false; // 每组测试数据都要重置escape的值,否则肯定WA
        if(n == 0 && m == 0 && t == 0)
            break;
        for(int row = 1; row <= n; row++)
            for(int col = 1; col <= m; col++)
            {
                scanf(" %c", &map[row][col]); // 注意%c前面的空格
                if(map[row][col] == 'S')
                {
                    start_row = row;
                    start_col = col;
                }
                if(map[row][col] == 'D')
                {
                    des_row = row;
                    des_col = col;
                }
                if(map[row][col] == 'X')
                    wall++;
            }
        if(n*m - wall - 1 < t) // 定界剪枝,该枝不剪耗时546MS
        {
            printf("NO\n");
            continue;
        }
        map[start_row][start_col] = 'X'; 
        dfs(start_row, start_col, 0);
        if(escape)
            printf("YES\n");
        else
            printf("NO\n");
        
    }

    return 0;
}

void dfs(int row, int col, int step) // 因为回溯需要修改step所以它不能定义成全局变量,本函数的三个变量都需要保存在栈中
{
    if(row == des_row && col == des_col && step == t)
    {
        escape = true;
        return;
    }
    if(escape) // 提高效率,缺少的话超时,根据题意只需要确定有无解即可,不必找全解
        return;
    if(row<1 || row>n || col<1 || col > m) // 出界
        return;
    int temp = (t-step) - ( abs(des_row-row) + abs(des_col-col) );
    if(temp < 0 || (temp&1)) // 定界剪枝+奇偶剪枝(缺少超时),&的优先级大于||(奇偶剪枝可放在main函数中,只需一次判断即可)
        return;
    for(int i = 0; i < 4; i++)
    {
        if(map[row+dir[i][0]][col+dir[i][1]] != 'X')
        {
            map[row+dir[i][0]][col+dir[i][1]] = 'X';
            dfs(row+dir[i][0], col+dir[i][1], step+1); // 不要写成step++
            map[row+dir[i][0]][col+dir[i][1]] = '.';
        }
    }
    return; 
}
// 一点心得&牢骚: 
// 全不全局是个问题,一方面传参数目要尽量少,另一方面又要避免全局变量过多引起混乱
// 考虑不周的话,DFS很容易就栈溢出或者数组下标越界,因此要考虑好深搜到什么时候终止
// 该代码C++过了,G++ WA,害我反复修改提交了几个小时.不得不喷一下,HDOJ怎么评测的:(

我们结合上述程序中定义的数据进行分析,最多max_step = n*m- wall - 1 步到达终点,如果t > max_step 问题显然无解。最少min_step = abs(des_row-row) + abs(des_col-col) 步到达终点,如果t < min_step,问题同样无解。

这样我们限定了min_step ≤ t ≤ max_step。据此可以进行定界剪枝。

至于上述程序中的奇偶剪枝,意思就是偶数步才能到达的点,奇数步绝不可能到达,反之亦然。而偶数 - 奇数 = 奇数; 偶数 - 偶数 = 偶数; 奇数 - 奇数 = 偶数。再根据整数底层的二进制表示形式可知,奇数的最后一位必为1,这就是程序中temp&1 的由来

另外在说一点,关于出界判断if(row<1 || row>n || col<1 || col > m) 也可以不用,方法就是在迷宫的最外围全部设置成虚拟的墙。

All Rights Reserved.
Author:海峰:)
Copyright © xp_jiang. 
转载请标明出处:http://www.cnblogs.com/xpjiang/p/4438300.html
以上.