1181:给定一个无权有向图,求起点到终点是否可达.
题目地址:http://acm.hdu.edu.cn/showproblem.php?pid=1181
一.广度优先搜索
先用BFS解一下看看:
char map[26][26], letters[26];
int discover[26];
/*
* 广度优先搜索,求B到M的最短路径
*
* @return 1:可以从b->m, 0:不可以从b->m
*
* @param x - 字母的序号,a:0, b:1, ..., z:25
*
*/
char bfs(void)
{
/* 返回值,初始化为0 */
char ret = 0;
char *line, i, *lp;
struct queue *q;
/*
* 图上最多26个顶点,每个顶点仅仅入队一次,
* 因此队列大小只要26
*/
q = queue_new(26);
memset(discover, 0, sizeof(discover));
/* 将B插入到队列中 */
queue_append(letters + 1, q);
check:
queue_append(NULL, q);
while ((lp = (char*)queue_pop(q)) != NULL) {
line = map[*lp];
for (i = 0; i < 26; ++i) {
if (line[i] && !discover[i]) {
if (letters[i] == 'm' - 97) {
ret = 1;
goto exit;
}
discover[i] = 1;
queue_append(letters + i, q);
}
}
}
/*
* 队列变空是广度优先搜索基本的终止条件.
* 当取出一个NULL后,如果队列空了,
* 说明B到M不可达,因此直接退出并返回0
*/
if (!queue_isempty(q))
goto check;
exit:
queue_destroy(q);
return ret;
}
在上述过程中,每个节点最多入队一次,因此while循环最多执行25次(M不会入队).
二.深度优先搜索
再试试DFS.
char map[26][26], block[26];
/*
* 深度优先搜索,
* 路径从B开始,并且无论何时,只要M出现在从B开始的路径上,算法就结束
* 否则,算法会遍历完所有路径
*
* @return - 当前字母到M的路径长度
* INT_MAX: 不可达
*
* @param deepth - 当前深度
* @param current - 当前字母
*
*/
int dfs(int deepth, char current)
{
int best = INT_MAX;
int len, i, ret;
/*
* 将测试条件放在dfs()开始的地方还是
* 将条件放在for循化内部?
* 这是个让人头疼的问题.
* 但是,对1180而言,后者明显好于前者(因为你不能进入到
* 下一层递归以后才想起要实现"滑动").
*/
if (current == 'm' - 97)
return 0;
if (block[current])
return INT_MAX;
/* 阻塞节点也在for循环外部执行 */
block[current] = 1;
for (i = 0; i < 26; ++i) {
if (map[current][i] == 1 && !block[i]) {
len = dfs(deepth + 1, i);
if (len < best)
best = len;
}
}
block[current] = 0;
ret = best + 1;
/* 从Linux内核学来的判断溢出的方法 */
return ret < best ? best : ret;
}
三.比较
对这个题目而言似乎BFS和DFS不分上下.
可是思考后发现,如果不存在起点到终点的路径,BFS只要遍历完所有节点就可以,但是DFS却要遍历完所有路径.从这个意义上看,似乎BFS还是优于DFS.
回过头再跟1181比较一下,在1180和1181中:
- BFS表现出的最直观优势就是它绝不会将一个节点入队两次;
- BFS都被用来寻找从起点到终点的最短路径;
经过一番思考,再联想到就连教科书常用BFS解决无权最短路径问题,那么,寻找最短路径应该正是BFS的长处所在.
在使用BFS寻找最短路径的过程中,节点第一次被发现的同时,其最短路径也被发现,因此每个节点只会入队一次,所以外层的while循环只会执行O(|V|)次(整个算法的复杂度是O(|V|*|E|)).
如果使用DFS寻找最短路径,那么DFS必须把图中的所有路径统统遍历一遍,虽然可以采用剪枝来优化这个过程,但是其最坏情形(两点不通)的复杂度依然与图中简单路径(无环的路径)的数目成正比,这意味着DFS寻找最短路径通常是指数级的算法.
再思考后,BFS和DFS的区别更加明确:
BFS的长处在于它可以很快找到最短路径;而DFS的长处在于它可以遍历图中所有简单路径.
BFS和DFS是不可交换的.如果用DFS寻找最短路径,会使算法从|V|*|E|级上升到指数级;反过来,如果强行用BFS遍历所有简单路径,估计我的内存和大脑都会马上冒烟.
这样,BFS和DFS的应用领域就很明确了:
- 如果找到最短路径就能解决问题,那么选择BFS;
- 如果找到最短路径不一定能解决问题, 那么选择DFS;
1181和1180都是找到最短路径就能解决问题的题目,那么有没有找到最短路径也不能解决的题目呢?当然有.
1175:给定一个地图和两个点,请问两点之间是否存在一条拐角数不超过2的路径?
原题地址:http://acm.hdu.edu.cn/showproblem.php?pid=1175
对于下图:
1 0 0 0 0 0 0
0 2 2 2 2 2 0
0 0 0 2 2 2 0
2 2 0 0 2 2 0
2 2 2 1 0 0 0
1到1的最短路径显然不是问题的解.事实上,除了遍历所有路径,我不能想到更好的办法来寻找满足题设的路径.因此我只能选用DFS来解决这个问题.
附上DFS代码:
/* 地图 */
int map[1000][1000];
/* 方向 */
int dir[8] = {0, 0, -1, 1, -1, 1, 0, 0};
/* 当前地图的宽度和高度 */
int M, N;
/*
* 深度优先搜索
*
* @return 1:可以连在一起,0:不能连在一起
*
* @param deepth - 深度
* @param conner - 路径上已有的拐角个数
* @param direction - 当前的方向
* @param bx, by, ex, ey - 起点和终点的坐标
*/
int dfs(int deepth, int conner, int direction,
int bx, int by, int ex, int ey)
{
int conner2, d, nx, ny;
int ret, save;
/*
* 由于会直接在地图上记录阻塞节点(写入51),
* 所以把节点原来的值保存在sava变量中
*/
save = map[bx][by];
map[bx][by] = 51;
/* 遍历四个方向 */
for (d = 0; d < 4; ++d) {
conner2 = conner;
/*
* 如果当前方向与源方向不同,则拐角个数+1
* 如果拐角个数多余2个,放弃当前方向
*/
if (d != direction)
if (++conner2 > 2)
continue;
nx = bx + dir[d];
ny = by + dir[d + 4];
/* 到达终点 */
if (nx == ex && ny == ey) {
ret = 1;
goto exit;
}
/* 下一个节点合法性与合理性测试,这次,测试被放到了for循环内部 */
if (nx >= 0 && nx < M && ny >= 0 && ny < N
&& !map[nx][ny]
&& dfs(deepth + 1, conner2, d,nx, ny, ex, ey)) {
ret = 1;
goto exit;
}
}
ret = 0;
exit:
map[bx][by] = save;
return ret;
}
注意:1175并非不能用BFS.
事实上,我感觉以拐角数作为度量的BFS优于上述DFS算法.
思路来自于1728,可惜我莫名其妙WA,至今没找到原因TT
终于,在DFS和BFS的选择问题上可以少一些纠结了 :-)