这周主要回顾了基本的搜索算法。自己找了点时间重新刷了刷记忆化搜索,动态规划记录最优路径以及最长上升子序列及其优化。这次课上主要讲了bfs,dfs回溯的基础搜索算法,所以详细整理一下。
搜索算法,本质就是有目的对解进行枚举,相比普通的枚举算法,可以限定搜索方向,避开本无意义的无效枚举。
首先是
dfs深度优先搜索
我课上印象比较深的例题那个素数环。
如果没有学习dfs搜索算法,其实这题也能写,用for循环无目的的枚举每个位置该填的数,直到某个状态满足了素数环的定义,就输出。但是很明显这样的时间复杂度奇高达到了O(n^n)。
但是搜索可以简化这一操作,每次同样枚举当前该填的数,但是可以记录某个数是否被使用,如果被使用就没必要填他,而转向枚举下一个数。同时运用回溯的思想,可以让搜索在状态已经不合理的情况下回溯到上一状态。不必再继续枚举直到终点。
从这里也可以看出,dfs的特点就是每次搜索按一条合法的特定线路搜索,直到不合法或者枚举完成才返回,直到遍历所有状态。期间我们可以通过判断调整dfs的方向。使其更多的避免无效搜索,加快搜索进程,这一操作也被称为剪枝。
与之相对的就是
bfs广度优先搜索
广度优先搜索如其名,就是每个步骤所有的状态全部搜索一遍,直到其中某个状态达到了搜索的目标,才停止。广搜常用队列实现,每次push一个父节点状态,并在遍历到这个父节点状态时将其所有可能的状态拓展,类似于大水漫灌。由此搜索的思路,我们可以知道,第一次搜索到的结果,必为最短操作数的结果。
从这两种搜索的思路来看,其实都有些暴力。dfs不到终点不回头,bfs在没达到目标的情况下,都要把所有可能都遍历一遍。所以基于这两个基本的搜索思路又衍生出了好多优化的算法,这块涉及到图了就不继续了。
但有一个与其联系比较紧密的,且又非常重要的算法或者说思维就是dp动态规划。
如dp中经典的01背包问题,如果用朴素的搜索来写,其实就是搜索每个物品拿不拿的状态,如果超过了背包的体积就回溯并在搜索完所有物品后取最优解返回。
dfs搜索思路大致如下
void dfs(int dep, int sum, int vleft) {//sum为当前的总价值,vleft为背包当前剩余体积
if (vleft < 0) return;//如果容积不够还拿,就返回
if (dep > n ||vleft==0) {
if (sum > mx) mx = sum;
return;
}
int vnext = vleft - v[dep];//vnext为拿了当前物品,对下一个物品决策的时候背包剩余体积
dfs(dep + 1, sum + w[dep], vnext);//搜拿该物品的情况
dfs(dep + 1, sum, vleft);//搜不拿该物品的情况
}
但是这样时间复杂度太高了,也太蠢了,每个可能都试一遍,直到不合法或者能拿的物品全拿完才返回。所有我们可以对他进行优化,比如有些情况下,明明之前尝试过了,但是遇到这种情况还是得再绕一次,大大加大了时间复杂度,所以我们可以采用记忆化的想法,记录每次搜索过的状态,如果搜索过这个状态直接返回结果即可。所以又引出了记忆化搜索,用一个rec数组记录搜索的状态。
int rec[10000][10000] = { 0 };
int dfs(int dep ,int vnow) {//dep为搜索深度,也就是当前有几个物品
int res=0;
if (rec[dep][vnow] > 0) return rec[dep][vnow];//记忆数组已经记录过就输出
if (dep == n) return 0;//搜索终点,物品都搜完了拿不了,返回0
if (vnow - vb[dep] <0)
res = dfs(dep + 1, vnow); //拿不了的
else if (vnow - vb[dep] >=0)
res= max(dfs(dep + 1, vnow), dfs(dep + 1, vnow - vb[dep]) + w[dep]);//取每个路线的最大值
return rec[dep][vnow]=res;//记忆
}
但是如果状态数过多,数组开不下或者消耗内存太大了怎么办?所以想到将递归形式的搜索转换为迭代。
for (int j = 1; j <= n; j++) {//物品数
for (int i = v; i >= 1; i--) {//背包体积,从后往前逆向枚举
//保证可以用到上一层外循环的dp值
if (i - vb[j] >= 0)
dp[i] = max(dp[i], dp[i - vb[j]] + w[j]);
}
}
这样一起整理,思路就清晰很多了。
本周还学习的一些重点因为篇幅过长,打算单独开一篇博客。