深度优先搜索(DFS)
作者:互联网
深度优先搜索会走遍所有路径,并且每次走到死胡同就代表一条完整路径的形成。这就是说,深度优先搜索是一种枚举所有完整路径以遍历所有情况的搜索方法。
使用递归可以很好地实现深度优先搜索。当然也可以使用非递归的方法实现DFS,但是非递归的方法一般情况下会比递归的方法要麻烦。在使用递归的时候,系统还会调用一个叫系统栈的东西来存放递归中的每一层状态,因此使用递归来实现DFS的本质其实还是栈。
接下来讲解一个例子,读者需要从中理解其中包含的DFS思想,并尝试学习写出本例的代码。
题目:
有n件物品,每件物品的重量为w[i],价值为c[i]。现在需要选出若干件物品放入一个容量为V的背包中,使得在选入背包的物品重量和不超过容量V的前提下,让背包中物品的价值之和最大,求最大价值。(1≤n≤20)
思考:
在这个问题中,需要从n件物品中选择若干件物品放入背包,使它们的价值之和最大。这样的话,对每件物品都有选或者不选两种选择,而这就是所谓的“岔道口”。那么什么是“死胡同”呢?——题目要求选择的物品重量总和不能超过V,
因此一旦选择的物品重量总和超过 V ,就会到达“死胡同”,需要返回最近的“岔道口”。
显然,每次都要对物品进行选择,因此DFS函数的参数中必须记录当前处理的物品编号 index。而题目中涉及了物品的重量与价值,
因此也需要参数来记录在处理当前物品之前,已选物品的总重量 sumW 与总价值 sumC
函数的声明:应该为void DFS(int index,int sumW,int sumC);
于是,如果选择不放入index号物品,那么sumW与sumC就将不变,接下来处理 index + 1号物品,即前往DFS(index + 1, sumW, sumC)这条分支;
而如果选择放入index号物品,那么sumW将增加当前物品的重量 w[index],sumC 将增加当前物品的价值 c[index] ,接着处理index + 1号物品,
即前往DFS(index + 1, sumW + w[index], sumC + c[index])这条分支。
一旦index增长到了n,则说明已经把n件物品处理完毕(因为物品下标为从0到n - 1),此时记录的sumW和sumC就是所选物品的总重量和总价值。
如果sumW不超过V且sumC大于一个全局的记录最大总价值的变量 maxValue,就说明当前的这种选择方案可以得到更大的价值,于是用sumC更新maxValue。
下面的代码体现了上面的思路,请注意“岔道口”和“死胡同”在代码中是如何体现的:
#include<cstdio> const int maxn = 30; int w[maxn], c[maxn], V,maxValue=0,n;//maxValue为最大价值。 index为当前物品的编号,sumW,sumC为当前物品的总重量,和总价值。 void DES(int index, int sumW, int sumC) { if (index == n ) {//已经完成了对n件物品的选择 if (sumC > maxValue && sumW <= V) { maxValue = sumC; } return; } DES(index + 1, sumW, sumC); DES(index + 1, sumW + w[index], sumC + c[index]); } int main() { scanf("%d%d", &n, &V); for (int i = 0; i < n; i++) { scanf("%d%d", &w[i], &c[i]); } DES(0, 0, 0); printf("%d", maxValue); return 0; }
可以注意到,由于每件物品有两种选择,因此上面代码的复杂度为O(2^n),这看起来不是很优秀。
但是可以通过对算法的优化,来使其在随机数据的表现上有更好的效率。
在上述代码中,总是把n件物品的选择全部确定之后オ去更新最大价值,但是事实上忽视了背包容量不超过V这个特点。
也就是说,完全可以把对sumW的判断加入“岔道口”中,只有当sumW≤V时才进入岔道,这样效率会高很多,
代码如下,只更改了DFS:
void DES(int index, int sumW, int sumC) { if (index = n + 1) { return; } DES(index + 1, sumW, sumC);//不选第index件物品,这一语句也可以移动到,if 语句中. if (sumW + w[index] <= V) { if (sumC + c[index] > maxValue) { maxValue = sumC + c[index];//更新最大价值maxValue } DES(index + 1, sumW + w[index], sumC + c[index]);//选第index件物品 } }
可以看到,原先第二条岔路是直接进入的,但是这里先判断加入第index件物品后能否满足容量不超过V的要求,
只有当条件满足时才更新最大价值以及进入这条岔路,这样可以降低计算量,使算法在数据不极端时有很好的表现。
这种通过题目条件的限制来节省DFS计算量的方法称作剪枝(前提是剪枝后算法仍然正确)。
剪枝是一门艺术,学会灵活运用题目中给出的条件,可以使得代码的计算量大大降低,很多题目甚至可以使时间复杂度下降好几个等级。
事实上,上面的这个问题给出了一类常见DFS问题的解决方法,即给定一个序列,枚举
这个序列的所有子序列(可以不连续)。例如对序列**(1,2,3)来说,它的所有子序列为{ 1 }、{ 2 }、{ 3 }、{ 1,2 }、{ 1,3 }、{ 2,3 }、{ 1,2,3 }**
枚举所有子序列的目的很明显—一可以从中选择一个"最优"子序列,使它的某个特征是所有子序列中最优的;
如果有需要,还可以把这个最优子序列保存下来。显然,这个问题也等价于枚举从N个整数中选择K个数的所有方案。
例如这样一个问题:
给定N个整数(可能有负数),从中选择K个数,使得这K个数之和恰好等于一个给定的整数X:如果有多种方案,选择它们中元素平方和最大的一个。
数据保证这样的方案唯一。
例如,从4个整数 { 2, 3, 3, 4 }中选择2个数。
使它们的和为6,显然有两种方案{ 2,4 }与(3, 3},其中平方和最大的方案为{ 2,4 }。
思考:
与之前的问题类似,此处仍然需要记录当前处理的整数编号 index;由于要求恰好选择K个数,因此需要一个参数 nowK 来记录当前已经选择的数的个数;
另外,还需要参数 sum 和 sumSqu 分别记录当前已选整数之和与平方和。于是 DFS 就是下面这个样子:
void DFS(int index, int nowK, int sum, int sumSqu){…}
此处主要讲解如何保存最优方案,即平方和最大的方案。首先,需要一个数组temp,用
以存放当前已经选择的整数。这样,当试图进入“选index号数”这条分支时,就把A[index]
加入temp中;而当这条分支结束时,就把它从temp中去除,使它不会影响“不选index号数”这条分支。
接着,如果在某个时候发现当前已经选择了K个数,且这K个数之和恰好为x时,就去判断平方和是否比已有的最大平方和maxSumSqu还要大;
如果确实更大,那么说明找到了更优的方案,把temp赋给用以存放最优方案的数组ans。这样,当所有方案都枚举完毕后,ans存放的就是最优方案,maxSumSqu存放的就是对应的最优值。
下面给出了主要部分的代码,建议读者能完整理解并自行写出 :
#include<cstdio> #include<vector> using namespace std; const int maxn = 30; int n, k, x, maxSumSqu = -1,A[maxn]; vector<int> temp, ans;//temp存放临时方案,ans存放平方和最大的方案 //当前处理index号整数,当前已选取的整数个数时nowK //当前已选整数之和为sum,当前已选整数平方和为sumSqu void DES(int index, int nowK, int sum, int sumSqu) { if (nowK == k && sum == x) { if (sumSqu > maxSumSqu) { maxSumSqu = sumSqu; ans = temp;//将temp赋值给ans } return; } if (nowK > k || sum > x || index == n) return; temp.push_back(A[index]); DES(index + 1, nowK + 1, sum + A[index], sumSqu + A[index] * A[index]); temp.pop_back();//这一步很重要,删除加入的元素 DES(index + 1, nowK, sum, sumSqu);//不选择第index个元素 } int main() { scanf("%d%d%d", &n, &k, &x); for (int i = 0; i <n; i++) { scanf("%d", &A[i]); } DES(0, 0, 0, 0); for (vector<int>::iterator it = ans.begin(); it != ans.end(); it++) { printf("%d ", *it);//输出最优子序列 } printf("%d", maxSumSqu);//输出最大的平方和 }
上面这个问题中的每个数都只能选择一次,现在稍微修改题目:
假设N个整数中的每个都可以被选择多次,那么选择K个数,使得K个数之和恰好为X。
例如有三个整数1、4、7,需要从中选择5个数,使得这5个数之和为17。显然,只需要选择3个1和2个7,即可得到17。
这个问题只需要对上面的代码进行少量的修改即可。由于每个整数都可以被选择多次,因此当选择了 index 号数时,不应当直接进入 index + 1 号数的处理。
显然,应当能够继续选择 index 号数,直到某个时刻决定不再选择 index 号数,就会通过“不选 index号数”这条分支进入 index + 1 号数的处理。
因此只需要把“选index号数”这条分支的代码修改为
DFS(index, nowK + 1, sum + A[index], sumSqu + A[index] * A[index]);
即可
标签:index,优先,int,DFS,选择,sumW,搜索,sumC 来源: https://www.cnblogs.com/migang/p/14671842.html