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;
}
杨辉三角形(递推式: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;
}
}
}