[经验总结]浅谈DFS(深度优先搜索)剪枝技巧.

    DFS(深度优先搜索)常用于答案的穷举.

在我们遇到的一些问题当中,有些问题我们不能够确切的找出数学模型,即找不出一种直接求解的方法,解决这一类问题,我们一般采用搜索的方法解决。搜索就是用问题的所有可能去试探,按照一定的顺序、规则,不断去试探,直到找到问题的解,试完了也没有找到解,那就是无解,试探时一定要试探完所有的情况(实际上就是穷举)

                                                                                                                                                                                                    ——百度百科

    但是由于深搜需要大量的递归,导致了OIer写出一个高效率的搜索算法是非常困难的,这就需要加入一些算法设计技巧,也就是剪枝。

    什么是剪枝?

    深搜的过程形象化就像一棵不断延伸分叉的树,这些枝干的末端可能就是我们所求的答案,但有些枝干从一开始长出来就注定是做无用功,这时候我们就需要通过一些条件判断,设计技巧等工具来“剪断”这些枝干,减少时间开销。

    剪枝的原则

1、 正确性

        正如上文所述,枝条不是爱剪就能剪的. 如果随便剪枝,把带有最优解的那一分支也剪掉了的话,剪枝也就失去了意义. 所以,剪枝的前提是一定要保证不丢失正确的结果.

2、 准确性

        在保证了正确性的基础上,我们应该根据具体问题具体分析,采用合适的判断手段,使不包含最优解的枝条尽可能多的被剪去,以达到程序“最优化”的目的. 可以说,剪枝的准确性,是衡量一个优化算法好坏的标准.

3、 高效性

         设计优化程序的根本目的,是要减少搜索的次数,使程序运行的时间减少. 但为了使搜索次数尽可能的减少,我们又必须花工夫设计出一个准确性较高的优化算法,而当算法的准确性升高,其判断的次数必定增多,从而又导致耗时的增多,这便引出了矛盾. 因此,如何在优化与效率之间寻找一个平衡点,使得程序的时间复杂度尽可能降低,同样是非常重要的. 倘若一个剪枝的判断效果非常好,但是它却需要耗费大量的时间来判断、比较,结果整个程序运行起来也跟没有优化过的没什么区别,这样就太得不偿失了.

综上所述,我们可以把剪枝优化的主要原则归结为六个字: 正确、准确、高效.

 

相关技巧

  • 变量标记

使用标记数组来给搜索过程设置条件,“究竟这条路可不可以走,答案取决于你的条件设置。”

例子1:P1219 八皇后问题 

回溯法求解,用变量标记每行每列每一对角线是否存在一个“皇后”。

#include <iostream>

using namespace std;

int hang[100];
int b[100];
int c[100];
int d[100];

int N;
int ans = 0;

void Search(int);

int main(void)
{
    cin >> N;
    Search(1);
    cout << ans << endl;
    return 0;
}

void Search(int cur)
{
    if(cur > N)
    {
        if(ans < 3)
        {
            for(int i = 1;i <= N;i++)
            {
                cout << hang[i] << ' ';
            }
            cout << endl;
        }
        ans++;
        return;
    }
    else
    {
    
        for(int i = 1; i <= N;i++)
        {
            if(!(b[i]) && (!c[i+cur]) && (!d[i-cur+N]))
            {
                hang[cur] = i;	//第cur行的i列被占领 
                b[i] = 1;	//第i列被占领 
                c[i+cur] = 1;	//某对角线占领,只是某种表示法 
                d[i-cur+N] = 1;	//同上 
                Search(cur+1);
                b[i] = 0;	
                c[i+cur] = 0;
                d[i-cur+N] = 0;
                //回溯 
            }
        }
    }
}

例子2:P1019 单词接龙

除了必要的字符串对比算法,还有用标记数组Visited表示访问次数,同一个单词的使用次数不能超过2.

#include <iostream>
#include <string>
#include <algorithm>

using namespace std;

string Word[20];
int Mt[20][20];
int Visited[20] = {0}; 

int N;

int Overlay(string,string);

int ans = 0;
int Num = 0;

void DFS(string,int);

int main(void)
{
    cin >> N;
    
    for(int i = 0;i <= N;i++)
    {
        cin >> Word[i];
    }
    DFS(' '+Word[N],1);
    cout << ans << endl;
    return 0;
}

void DFS(string now,int lengthNow)
{
    ans = max(lengthNow,ans);
    
    for(int i = 0;i < N;i++)
    {
        if(Visited[i] < 2)
        {
            Visited[i]++;
            int c = Overlay(Word[i],now);
            if(c > 0)
            {
                DFS(Word[i],lengthNow+Word[i].length()-c);
            }
            Visited[i]--;
        }
    }
}

int Overlay(string a,string b)
{
    /*检测字符串前后的重复部分*/
    /*优先取最短*/
    for(int i = 1;i < min(a.length(),b.length());i++)
    {
        int flag =1;
        for(int j=0;j < i;j++)
        {
            if(a[j] != b[b.length()-i+j])
            {
                flag = 0;
            }
        }
        if(flag)
        {
            return i;
        }
    }
    
    return 0;
}
  • 搜索前的数据整理

例子1:P1074 靶形数独

通过打表的方式方便求Score的值,以及通过对每一行的0空格个数为标准进行排序,从0少的单元格进行搜索尝试,减少了可能的无效搜索规模,减少时间开销.

#include <iostream>
#include <algorithm>
#include <cstdio>

using namespace std;


struct Row_Struct
{
    int Rank;
    int Zero_Cnt;
}RowStruct[10] = {{0}};

bool CMP(Row_Struct a,Row_Struct b)
{
    return a.Zero_Cnt < b.Zero_Cnt;
}

int ans = -1;

int Point[10][10] =
{
    {0,0,0,0,0,0,0,0,0,0},
    {0,6,6,6,6,6,6,6,6,6},
    {0,6,7,7,7,7,7,7,7,6},
    {0,6,7,8,8,8,8,8,7,6},
    {0,6,7,8,9,9,9,8,7,6},
    {0,6,7,8,9,10,9,8,7,6},
    {0,6,7,8,9,9,9,8,7,6},
    {0,6,7,8,8,8,8,8,7,6},
    {0,6,7,7,7,7,7,7,7,6},
    {0,6,6,6,6,6,6,6,6,6},
};

int u = 0;

int Map[10][10];

bool SmallNine[10][10] = {0};
bool Line[10][10] = {0};
bool Row[10][10] = {0};

int Sp[82][3] = {{0}};	/*储存要填写的数字的单元格信息*/

int GetSmallNineIndex(int,int);

void DFS(int,int);

int main(void)
{
    int Score = 0;
    for(register int i = 1;i <= 9;i++)
    {
        for(register int j = 1;j <= 9;j++)
        {
            scanf("%d",&Map[i][j]);
            if(Map[i][j])
            {
                SmallNine[GetSmallNineIndex(i,j)][Map[i][j]] = true;
                Line[i][Map[i][j]] = true;
                Row[j][Map[i][j]] = true;
                Score += Map[i][j]*Point[i][j];
            }
            else
            {
                RowStruct[i].Zero_Cnt++;
            }
        }
        RowStruct[i].Rank = i;
    }
    
    /*DFS剪枝(减少搜索层数),先搜索0少的行,提高效率*/
    sort(RowStruct+1,RowStruct+10,CMP);
    
    for(register int i = 1;i <= 9;i++)
    {
        for(register int j = 1;j <= 9;j++)
        {
            if(!Map[RowStruct[i].Rank][j])
            {
                Sp[u][0] = RowStruct[i].Rank;
                Sp[u][1] = j;
                Sp[u][2] = GetSmallNineIndex(RowStruct[i].Rank,j);
                u++;	//u代表要填写的空格数量. 
            }
        }
    }

    DFS(Score,0);

    printf("%d\n",ans);
    return 0;
}

void DFS(int Score,int Step)
{
    if(Step == u)
    {
        ans = max(ans,Score);
        return;
    }
    
    for(register int Num = 1;Num <= 9;Num++)
    {
        if(!SmallNine[Sp[Step][2]][Num] && !Line[Sp[Step][0]][Num] && !Row[Sp[Step][1]][Num])
        {		
            SmallNine[Sp[Step][2]][Num] = true;
            Line[Sp[Step][0]][Num] = true;
            Row[Sp[Step][1]][Num] = true;
            
            DFS(Score+Num*Point[Sp[Step][0]][Sp[Step][1]],Step+1);
                    
            SmallNine[Sp[Step][2]][Num] = false;
            Line[Sp[Step][0]][Num] = false;
            Row[Sp[Step][1]][Num] = false;
        }
    }
    
}

/*通过座标获取小九宫格的编号*/
int GetSmallNineIndex(int x,int y)
{
    int a = (x-1)/3 + 1;
    int b = (y-1)/3 + 1;
    
    return a + b*3 - 3;
}

例子2:P1118 [USACO06FEB]数字三角形

杨辉三角形(递推式:F[i][j] = F[i-1][j] + F[i-1][j-1] 第i层第j个数字的值)的引入,大大减少求和的时间。所求数列可用对应的杨辉三角的第N层各自相乘,得到总和。

#include <iostream>

using namespace std;

int N,Sum;
int Number[13] = {0};
bool IsUsed[13] = {0};

bool Done = false;

int PT[13][13];

void DFS(int,int);

int main(void)
{
    cin >> N >> Sum;
    for(int i = 1;i <= N;i++)
    {
        PT[i][1] = 1;
    }
    for(int i = 2;i <= N;i++)
    {
        for(int j = 2;j <= i;j++)
        {
            PT[i][j] = PT[i-1][j-1]+PT[i-1][j];
        }
    }
    DFS(1,0);
    return 0;
}

void DFS(int Num,int SumNow)
{
    if(SumNow > Sum)
    {
        return;
    }
    
    if(Done)
    {
        return;
    }
    if(Num == N+1)
    {
        if((SumNow != Sum))
        {
            return;
        }
        for(int i = 1;i <= N;i++)
        {
            cout << Number[i] << ' ';
        }
        Done = true;
        return;
    }
    for(int i = 1;i <= N;i++)
    {
        if(!IsUsed[i])
        {
            IsUsed[i] = true;
            Number[Num] = i;
            
            /*通过杨辉三角形求出和值,降低时间复杂度*/
            DFS(Num+1,SumNow+i*PT[N][Num]);
            IsUsed[i] = false;
        }
    }
}

 

点赞