DFS
DFS可以用栈来实现,但是用栈实现较为麻烦,所以采用递归来进行实现:
1)递归式就是岔道口
2)递归边界就是死胡同
讲解例子,理解DFS思想:
有n件物品,每件物品的重量为w[i],价值为c[i],现在要选出若干件放入容量为V的背包中,为使背包价值和最大,求最大价值( 1 <= n <= 20)
分析:
1.“岔路口” 和 “死胡同”分别是什么?
每件物品都有选或者不选两种选择–岔路口
一旦选择的物品超过了V,就到达死胡同
2.DFS函数的参数怎么写
a.需要记录当前要处理物品的编号;
b.需要在处理当前物品之前,选物品的总重量sumW 和 总价值sumC
因此:
void DFS(int index, int sumW, int sumC){}
3.岔路口的处理
如果不放入index号物品,则sumW 和 sumC不变,接下来处理index + 1号物品,即前往DFS (index + 1,sumW,sumC)这条分支
如果放入,则前往DFS (index + 1,sumW + w[index], sumC + c[index])
4.死胡同的处理
当index增加到了n,则说明已经把n件物品处理完毕,如果sumW <= V 且sumC >一个全局记录最大总价值变量maxValue,就说嘛当前这种选择方案可以得到更大的价值,于是用 sumC 更新maxValue.
代码实现:
#include <cstdio>
int n, V, maxValue = 0; //物品件数n,背包容量V,最大价值maxValue
const int Maxn = 30;
int w[Maxn],c[Maxn]; //w[i]为每件物品的重量,c[i]为每件物品的价值
//DFS,index为当前处理物品的编号
//sumW,sumC分别为当前总重量和总价值
void DFS(int index, int sumW, int sumC){
//已经完成对n件物品的选择(死胡同)
if(index == n){
if(sumW <= V && sumC > maxValue){
maxValue = sumC;
}
return;
}
//岔道口,注意结合到死路后退回到最近的岔路口继续下一个分支
DFS(index + 1, sumW, sumC);
DFS(index + 1, sumW + w[index], sumC + c[index]);
}
int main(){
scanf("%d%d",&n,&V);
for(int i = 0; i < n; i++){
scanf("%d",&w[i]);
}
for(int i = 0; i < n; i++){
scanf("%d",&c[i]);
}
DFS(0,0,0); //初始为第0件物品,当前总重量和总价值为0
printf("%d\n",maxValue);
return 0;
}
/*
5 8
3 5 1 2 2
4 5 2 1 3
*/
进一步分析,以上代码复杂度为O(2^n),则考虑对该算法进行优化。
上诉代码:
把n件物品全部确定后才更新最大值,忽略了背包容量不超过V这个特点。
改进:
把对sumW的判断 移动 入“岔道口”中,只有当sum<=V时才进入岔道
改进如下:
void DFS(int index, int sumW, int sumC) {
if(index == n){
return;
}
DFS(index + 1, sumW, sumC); //不选第index件商品
//只有加入第index件物品后未超过容量V,才能继续
if(sumW + w[index] <= V) {
if(ans < sumC + c[index]) {
ans = sumC + c[index]; //更新最大价值maxValue
}
DFS(index + 1,sumW + w[index], sumC + c[index]); //选第index件物品
}
}
分析:原先第二条岔路是直接进入的,但是这里有条件满足是才能进入,可以降低计算量,使算法在数据不极端时有很好的表现–剪枝
一类常见的DFS问题的解决方法:给定一个序列,枚举这个序列的所有子序列(可以不连续),从中选择一个“最优”子序列,使它的某个特征是所有子序列中最优的。
例题2.给定N个整数(可能有负数),从中选择K个数,使得这K个数之和恰好等于一个给定的整数X;如果有多种方案,选择他们中元素平方和中最大的一个。
分析:
1.DFS的参数:
处理当前整数的编号index,由于需要选择k个数,因此需要一个参数nowK来记录当前已经选择的个数;还需要记录当前已选整数的和sum和平方和sumSqu:
void DFS(int index, int nowK, int sum, int sumSqu)
2.需要一个整数数组temp,用以存放当前已经选择的整数,所以:
1)当试图进入“选index号数”这条分支时,就把A[index]加入temp中
2)上条分支结束时没需要把它从temp中去除
3.当某个时候发现了已经选择了k个数,并且这k个数之和恰好为x时,就判断平方和是否比已经得到的最大平方和更大:
如果更大,则用temp附给存放最优方案的数组ans.
4.当所有方案都枚举完毕后,ans存放的就是最优方案,maxSumSqu就是对应的最优值
//序列A中n个数选k个使得和为x,最大平方和为maxSumSqu
int n,k,x,maxSumSqu = -1,A[maxn];
//temp存放临时方案,ans存放平方和最大的方案
vector<int> temp, ans;
void DFS(int index, int nowK, int sum, int sumSqu) {
if(nowK == k && sum == x){
if(sumSqu > maxSumSqu){
maxSumSqu = sumSqu;
ans = temp;
}
return;
}
//已经超过k个数,或者已经超过x,或已经处理完n,返回
if(nowK > k || index == n || sum > x) return;
//选index号数
temp.push_back(A[index]);
DFS(index + 1, nowK + 1, sum + A[index], sumSqu + A[index] * A[index]);
temp.pop_back();
//不选index号数
DFS(index + 1,nowK, sum, sumSqu);
}
修改问题:
上诉问题每个数字只能选一次,假设每个数字都能选多次,如何修改代码?
当选择了index号数时,不应该直接进入index+1号数的处理,应该继续选择index号数,直到某个时刻决定不再选index号数,再进行index + 1的处理