二.初出茅庐——初级篇
2.1 穷竭搜索
穷竭搜索是将所有的可能性罗列出来,在其中寻找答案的方法。主要介绍深度优先搜索和广度优先搜索
1.递归函数
在一个函数中再次调用函数自身的行为叫做递归,这样的函数被称为递归函数。例如下面例子中求阶乘。
#include
using namespace std;
int fact( int n)
{
if( n == 0) return 1;
return n * fact(n-1);
}
int main()
{
int result = fact(10);
cout <<"fact(10): " << result << endl;
return 0;
}
递归函数中,比较重要的一点是函数的停止条件是必须存在的。
斐波那契数列的定义A0=0,A1=1以及An = An-1 + An-2 (n > 1)。初项的条件就对应了递归的终止条件。
int fib(int n)
{
if(n <= 1) return n;
return fib(n-1) +fib(n-2);
}
但是由于上述函数需要依次递归展开,即使求fib(40)这样的n较小的结果时,也花费很长时间。由于fib(n)的n是一定的,无论调用多少次都会得到同样的结果,因此在计算依次之后,可以将数列结果保存,再次使用到时不需要重新计算,便可以优化结果。这种方法出于记忆搜索或者动态规划的思想。
#define MAX_N 40
int memo[MAX_N+1];
int fib( int n)
{
if( n <= 1) return n;
if(memo[n] != 0) returnmemo[n];
return memo[n] = fib(n-1)+ fib(n-2);
}
上述的代码减少了计算的次数,节省时间。
2.栈
栈(Stack)是支持push和pop两种操作的数据结构。最后进入栈的一组数据可以先被取出,也即LIFO:Last In FirstOut,即后进先出
函数调用的过程通常是通过使用栈来实现的,因此递归函数的递归过程也可以改用栈上的操作来实现。
Stack使用例子
#include
#include
using namespace std;
int main()
{
stack s;
s.push(1);
s.push(2);
s.push(3);
cout << s.top()<< endl; // 打印栈顶元素 3
s.pop(); // 弹出栈顶元素
cout << s.top()<< endl;
s.pop();
cout << s.top()<< endl;
s.pop();
return 0;
}
3.队列
队列(Queue)与栈元素一样支持push和pop两个操作,与栈不同的是,pop完成的不是取出最顶端的元素,而是取出最底端的元素。最初放入的元素能够被最先取出,FIFO:First InFirst Out,即先进先出
队列使用例子:
#include
#include
using namespace std;
int main()
{
queue que;
que.push(1);
que.push(2);
que.push(3);
cout << que.front()<< endl;
que.pop();
cout << que.front()<< endl;
que.pop();
cout << que.front()<< endl;
que.pop();
return 0;
}
4.深度优先搜索
深度优先搜索(DFS, Depth-First Search)是搜索的手段之一。从某个状态开始,不断地转移状态直到无法转移,然后回退到前一步的状态,继续转移到其他的状态,如此不断反复,直到找到最终的解。
根据深度优先搜索的特点,采用递归函数实现比较简单。
题目1:求部分和问题
给定整数a1,a2,… ,an,判断是否可以从中选出若干个数字,试他们的和恰好为k。
其中:1 <= n <= 20,-10^8 <= ai <= 10^8,-10^8 <= k <= 10^8
输入:
n=4
a={1,2, 4, 7}
k=13
输出
Yes(13= 2 + 4 + 7)
输入:
n=4
a={1,2, 4, 7}
k=15
输出
No
方法:从a1开始按顺序决定每个数加或者是不加两种情况,在全部n个数都决定后再判断他们的和是否是k即可。
此时的状态数为2^( n+1)个,所以复杂度就为O(2^n)
#include
using namespace std;
#define MAX_N 4
int a[MAX_N]={1, 2, 4, 7};
int n, k;
bool dfs(int i, int sum)
{
if( i == n)
{
return sum == k;
}
if(dfs(i+1, sum)) // 不加第i个数字的情况
{
return true;
}
if(dfs(i+1,sum+a[i])) // 加上第i个数字的情况
{
return true;
}
return false;
}
int main()
{
n = MAX_N;
k = 13;
int sum = 0;
if(dfs( 0, sum))
{
cout <<"Yes"<< endl;
}
return 0;
}
深度优先搜索从最开始的状态出发,遍历所有可以到达的状态。因此对所有的状态也可以进行操作或者列举出所有的状态。
题目2:Lake Counting
有一个大小为N*M的院子,雨后积起了水。八连通的积水被认为是链接在一起的,请求出院子里总共有多少水洼?(八连通指的是下图中相对W的*的部分)
***
*w*
***
限制条件: N,M <= 100
N=10,M=20,图如下,W代表积水,’.’代表没有积水
w. . . . . . . . . . . . . . . . ww .
.www . . . . . . . . . . . . . www
.. . . ww . . . . . . . . . . . ww .
.. . . . . . . . . . . . . . . . ww .
.. . . . . . . . . . . . . . . . w. .
.. w . . . . . . . . . . . . . . w. .
.w . w . . . . . . . . . . . . . ww .
w. w . w . . . . . . . . . . . . . w.
.w . w . . . . . . . . . . . . . . w.
.. w . . . . . . . . . . . . . . . w.
输出:
3
方法:从任意的w开始,不停地把相连接的部分用’.’代替,依次DFS后与初始的这个w链接的所有的W就被替换成了’,’,因此直到途中不再存在w为止,总供进行的次数就是答案。
8个方向共八个状态转移,每个格子的DFS的参数至多被调用依次,所以复杂度为O(8*N*M),即O(N*M)
#include
using namespace std;
int N = 10;
int M = 12;
char field[10][12] =
{
{'w', '.', '.', '.', '.','.', '.', '.', '.', 'w', 'w', '.'},
{'.', 'w', 'w', 'w', '.','.', '.', '.', '.', 'w', 'w', 'w'},
{'.', '.', '.', '.', 'w','w', '.', '.', '.', 'w', 'w', '.'},
{'.', '.', '.', '.', '.','.', '.', '.', '.', 'w', 'w', '.'},
{'.', '.', '.', '.', '.','.', '.', '.', '.', 'w', '.', '.'},
{'.', '.', 'w', '.', '.','.', '.', '.', '.', 'w', '.', '.'},
{'.', 'w', '.', 'w', '.','.', '.', '.', '.', 'w', 'w', '.'},
{'w', '.', 'w', '.', 'w','.', '.', '.', '.', '.', 'w', '.'},
{'.', 'w', '.', 'w', '.','.', '.', '.', '.', '.', 'w', '.'},
{'.', '.', 'w', '.', '.','.', '.', '.', '.', '.', 'w', '.'},
};
void dfs(int x, int y)
{
// 将当前的w设置为 .
field[x][y] = '.';
// 遍历所有的可能方向
for(int dx = -1; dx <=1; dx++)
{
for(int dy = -1; dy<= 1; dy++)
{
int nx = x +dx; // 分别移动dx 和 dy,得到相邻的位置判断是否有水
int ny = y + dy;
// 如果相邻位置有 w,则进行深度遍历
if( nx >= 0&& nx < N && ny >=0 && ny < M &&field[nx][ny] == 'w')
{
dfs(nx, ny);
}
}
}
}
int main()
{
int res = 0;
for( int i = 0; i < N;i++)
{
for( int j = 0; j
5. 宽度优先搜索
宽度优先搜索(BFS,Breadth-FirstSearch)也是搜索的手段之一。与深度搜索类似,从某个状态出发搜索所有可以到达的状态。
宽度优先搜索总是先搜索距离初始状态近的状态,也就是说按照开始状态->只需要一次转移就可以到达的所有状态->只需要2次转移就可以到达的状态-> … 以这样的方式进行搜索。宽度优先搜索只经过一次,因此复杂度为O(状态数*转移方式)
深度优先搜索隐式地利用栈进行计算,而宽度优先搜索则利用队列。所有状态都是按照初始状态,由近及远的顺序被遍历。
题目1:迷宫的最短路径
给定一个大小为N*M 的迷宫,迷宫由通道和墙壁组成,每一步可以向邻接的上下左右四个格的通道移动。请求出从起点到终点所需要的最小步数,请注意,本体假设从起点一定可以移动到终点。
限制条件:N,M <= 100
输入:
#S######. #
.. . . . . # . . #
.# . ## . ##.#
.# . . . . . . . .
##.## . ####
.. . . # . . . .#
.#######. #
.. . . # . . . . .
.#### . ### .
.. . . # . . .G#
输出
22
方法:宽度优先搜索按照距离开始状态由近及远的顺序进行搜索,因此可以很容易地求出最短路径,最少操作之类问题的答案。本问题中,状态仅仅是目前所在位置的坐标,因此可以构成pair或者编码为int来表达。
转移的方向分为四个方向,状态数与迷宫的大小相等,复杂度为O(4*M*N)
宽度优先搜索中,只要将已经访问过的标记管理起来,就可以很好地做由近及远的搜索。这个问题中要求最短距离,不妨用d[N][M]数组来保存最短距离
#include
#include
using namespace std;
#define MAX_N 10
#define MAX_M 10
const int INF = 100000000;
typedef pair P;
char maze[MAX_N][MAX_M+1] =
{
{ '#', 'S', '#' , '#','#', '#', '#', '#', '.', '#'},
{ '.', '.', '.' , '.','.', '.', '#', '.', '.', '#'},
{ '.', '#', '.' , '#','#', '.', '#', '#', '.', '#'},
{ '.', '#', '.' , '.','.', '.', '.', '.', '.', '.'},
{ '#', '#', '.' , '#','#', '.', '#', '#', '#', '#'},
{ '.', '.', '.' , '.','#', '.', '.', '.', '.', '#'},
{ '.', '#', '#' , '#','#', '#', '#', '#', '.', '#'},
{ '.', '.', '.' , '.','#', '.', '.', '.', '.', '.'},
{ '.', '#', '#' , '#','#', '.', '#', '#', '#', '.'},
{ '.', '.', '.' , '.','#', '.', '.', '.', 'G', '#'},
};
int N = 10, M = 10;
int sx = 0;
int sy = 1;
int gx = 9;
int gy = 8;
int d[MAX_N][MAX_M];
int dx[4] = {1, 0, -1, 0}, dy[] = { 0, 1, 0, -1};
int bfs()
{
queue que;
// 初始化所有距离为INF
for( int i = 0; i < N;i++)
{
for( int j = 0; j
宽度优先搜索和深度优先搜索是一样的,都会生成所有能够遍历到的状态,因此需要所有状态进行处理时也可以选择宽度优先。由于递归函数可以写得很简短,而且状态的管理业更加简单,所以大多数情况下还是用深度优先算法。
求最短路径时深度优先搜索需要反复经过同样的状态,所以使用宽度优先搜索为好。
另外一种类似宽度优先搜索的状态转移顺序,并且注重节约内存的迭代加深深度优先搜索(IDDFS,IterativeDeepening Depth-First Search)
6.特殊状态枚举
生成可行解空间多数采用深度优先,但是在状态空间比较特殊时,可以很简短地实现。
C++的标准库提供了next_permutation()方法,用于把n个元素共n!种不同的排列生成出来。
或者通过使用位运算,可以枚举从n个元素中取出k个的共C(k,n)中状态,或是某个集合的全部子集等。
#include
#include
using namespace std;
#define MAX_N 4
bool used[MAX_N];
int perm[MAX_N];
// 生成0,1,2,3,4.... n-1的n!中排列
void permutation1( int pos, int n)
{
if(pos == n)
{
for( int i = 0; i
7.剪枝
穷竭搜索会把所有的可能都检查一遍,当空间非常大时,复杂度会相应地变大。深度优先时,如果已经明确了当前状态无论如何转移都不会存在解,在这种情况下就不再继续搜素,而是直接跳过,这种方法称为剪枝。
如部分和中,如果某一个节点处,sum > k,则跳过本节点以后的搜索,再搜索也不会出现sum == k的情况了。
专栏:
栈内存在程序启动时统一分配,此后不再扩大,这一区域是有上限的,因此函数的递归深度也有上限。不过一般情况下,C和C++中进行上万次递归是可以实现的。
By Andy @ 2013-7-28