深度优先搜索是一种枚举所有完整路径以遍历所有情况的搜索方法。
背包问题:
在n件物品中,每件物品的重量是w[i],价值为c[i]。现在需要选出若干件物品放入一个容量为V的背包中,使得再选入背包的物品重量和不超过容量V的前提下,让背包中的物品的价值之和最大,求最大价值。(1 <= n <= 20)
模仿书中迷宫的例子寻找“岔路口”和“死胡同”,对每件物品都有选或者不选两个选择,这就是“岔路口”。而“迷宫的死胡同”是题目要求选择的物品重量总和不能超过V。所以一旦超过V,就需要返回最近的“岔路口”。
因为每次都要对物品进行选择,所以DFS()函数的参数中必须记录当前处理的物品编号index,而题目涉及了物品的重量和价值,因此也需要参数来记录在处理当前物品前已对物品的总重量sumW和总价值sumC。所以DFS函数声明:
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件物品处理完毕,此时记录sumW和sumC的就是所选物品的总重量和总价值。如果sumW不超过V且sumC大于一个全局的记录最大总价值的变量maxValue,就说明当前的这种选择方案可以得到更大的价值,于是用sumC更新maxValue。
#include <cstdio>
#include <cstdlib>
const int maxn = 30;
int n, V, maxValue = 0;
int w[maxn], c[maxn];
void DFS(int index, int sumW, int sunmC);
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);
printf("%d\n", maxValue);
system("pause");
return 0;
}
void DFS(int index, int sumW, int sumC)
{
if (index == n) //死胡同
{
if (sumW <= V && sumC > maxValue)
maxValue = sumC; //更新最大值
return;
}
DFS(index + 1, sumW, sumC); //不选第index件物品
DFS(index + 1, sumW + w[index], sumC + c[index]); //选第index件物品
}
但是这段代码时间复杂度达到了O(2n),算是非常慢了。但是可以优化一下。
上面总是把n件物品的选择全部确定之后才去更新最大价值,但是事实上忽视了背包容量不超过V这个特点。这就是说,完全可以把对sumW的判断加入“岔路口”中,只有当sumW <= V时才进入岔道,这样效率会高很多:
void DFS(int index, int sumW, int sumC)
{
if (index == n)
return;
DFS(index + 1, sumW, sumC);
if (sumW + w[index] <= V)
{
if (sumC + c[index] > ans)
ans = sumC + c[index];
}
DFS(index + 1, sumW + w[index], sumC + c[index]);
}
这里只有满足加入第index件物品后容量不超过V才能进入这条岔路,这样就降低了计算量。通过这种题目条件的限制来节省DFS计算量的方法叫做剪枝
上述问题给出了一类常见DFS问题,给定一个序列,枚举这个序列的所有子序列(可以不连续),从中选择一个最优子序列
比如:
给定N个整数,从中选择K个数,使K个数之和恰好等于一个给定的整数X;如果有多种方案,选择它们中元素平方和最大的一个。
比如4个整数{2, 3, 3, 4}中选择2个数,使它们和为6,显然有两种方案{2, 4}和{3, 3},其中平方和最大的方案是{2, 4}
此处仍需要继续当前处理的整数编号index;由于需要恰好选择K个数,因此需要一个参数nowK来记录当前已经选择的数的个数;另外,还需要参数sum和sumSqu分别记录当前已选整数之和和平方和,声明如下:
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存放的就是对应的最优值。
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) //k个数和为x
{
maxSumSqu = sumSqu; //更新最大平方和
ans = temp; //更新最优方案
}
return;
}
if (index == n || nowK > k || sum > x)
return;
temp.push_back(A[index]);
DFS(index + 1, nowK + 1, sum + A[index], sumSqu + A[index] * A[index]);
temp.push_back();
//不选index号数
DFS(index + 1, nowK, sum, sumSqu);
}
上述问题如果改成假设N个整数中的每一个都可以被选择多次,那么选择K个数,使得K个数之和恰好为X
这时候只要做出一点修改即可,由于每个整数都可以被选择多次,因此当选择了index号数时,不应当直接进入index+1号。应该继续选择index号数,知道某个时刻决定不再选择index号数
因此只要把
DFS(index + 1, nowK + 1, sum + A[index], sumSqu + A[index] * A[index]);
改为:
DFS(index, nowK + 1, sum + A[index], sumSqu + A[index] * A[index]);
即可