当碰到岔道口时,总是以"深度"作为前进的关键词,不碰到死胡同就不回头,因此把这种搜索方式称为“深度优先搜索”
深度优先搜索是一种枚举所有完整路径以遍历所有情况的搜索方法。
那么如何实现深度搜索呢?把迷宫中的关键节点用字母代替
1.从第一条路可以得到先后访问的节点为ABDH,此时H到达了死胡同,于是退回D;再到I,但是I也是死胡同,再次退回到D;接着来看J,很不幸,J还是死胡同,于是退回D,但此时D的岔路已经走完了,则退回到B。
2.在B再选择E,又有三条岔路(K,L,M),一次进行枚举。。。
3.退回A,访问A的另一个岔路C。。。。
4.一直到找到出口为止。
这个过程是不是和出栈入栈的过程很相似?第一步,ABD入栈,然后把D的三条岔路HIJ先后入栈出栈,再把D出栈。。。
因此,深度优先算可以使用栈来实现。也可以用递归来实现。
回顾Fibonacci数列的定义:可以把F(n)分为两个部分F(n-1)F(n-2),那么死胡同就是F(0)和F(1),也就是递归边界。
使用递归时,系统会调用一个叫系统栈的东西来存放递归中每一层的状态,因此使用递归来实现DFS的本质其实还是用栈。
背包问题
eg:有n件物品,每件物品的重量为w[i],价值为c[i]。现在需要选出若干件物品放入一个容量为V的背包中,使得在选入背包的物品重量和不超过容量V的前提下,让背包中物品的价值之和最大,求最大价值。(1<=n<=20)
显然,每次都要对物品进行选择,因此DFS的参数中必须记录当前处理的物品编号index。而题目中涉及了物品的重量总和不能超过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])这条分支。
那么算法是怎么选择走那条路呢?事实上DFS算法只是从第一条路开始遍历,把第一条路走到死胡同,然后退回到上一步,选择DFS(index+1,其他不加)这条路,也就是死胡同上一个分叉的另一个值,把这个分叉的值都遍历完了之后,又退回上一个节点,循环往复。把要记录的特征值用一个数或数组记录,等遍历完了之后就能得到需要的值了
一旦index增长到了n,说明已经把n件物品处理完毕,此时记录的sumW和sumC就是所选物品的总重量和总价值。如果sumW不超过V且sumC大于一个全局记录的最大总价值的遍历maxValue,就说明当前的这种选择方案可以得到更大的价值,于是用sumC更新maxValue。
具体代码如下:
#define _CRT_SECURE_NO_WARNINGS
#include <cstdio>
#define MAXN 30
int n, maxValue = 0;
int V = MAXN;
int w[MAXN], c[MAXN];
int index = 0;
void DFS(int index, int sumW, int sumC)
{
if (index == n) //最后一个物品也考察完了,这下进入的index是n了
{
if (sumW <= V) //如果书包还不满
{
if (sumC > maxValue) //如果这轮的装法比上次的maxValue还大,就更新
maxValue = sumC;
}
return; //如果这次进入是装了某物,返回出去就执行第二个DFS,考察不装它的情况;如果这次进入是不装某物的,就返回到考察前一个物品要不要装的地方了
}
DFS(index + 1, sumW, sumC); //编号为index的物品不放进去,sumw和sumC不变,调用函数考察编号为index+1的物品
DFS(index + 1, sumW + w[index], sumC + c[index]); //编号为index的物品放进去,sumw和sumC都各自增加
}
int main()
{
scanf("%d %d", &n, &V);
for (int i = 0; i < n; i++)
{
printf("w\n");
scanf("%d", &w[i]);
printf("c\n");
scanf("%d" ,&c[i]);
}
DFS(0, 0, 0); //物品的编号是从0到index-1
printf("%d\n", maxValue);
return 1;
}
个人因为非常菜,所以在DFS函数的最后两个递归那儿看了一段时间。
事实上是一个枚举:
DFS(index + 1, sumW, sumC);执行到了index==n时,才会return,然后再执行DFS(index + 1, sumW + w[index], sumC + c[index]);就是这句话:选择了五件物品后,就进入了死胡同,退回最近的一个岔道口。事实上他是从index=n-1开始进行这项操作的。n-1是顶点。
可以这么理解:假设有5个物品ABCDE
我先选择了E,可以,记录maxValue,下面看E+D,E+D可以,更新maxValue,再看E+D+C,可以,更新maxValue,E+D+C+B,太重了,不行,退回,看E+D+C+A,不行,退回,看E+D+B,可以,更新maxValue(比原maxValue大的话)。。。这么遍历一遍,就可以得到最大值。这里比原代码多了一个判定,就是如果相加重量已经超过了的话,就直接退回。
void DFS(int index,int sumW,int sumC)
{
if(index==n)
{
return; //已经完成对n件物品的选择
}
DFS(index+1,sumW,sumC); //不选第n件物品
//只有加入第index件物品后未超过容量V,才能继续
if(sumW+w[index]<=V){
if(sumC+c[index]>ans)
{
ans=sumC+c[index];//更新最大maxValue
}
DFS(index+1,sumW+w[index],sumC+c[index])//选择第index件物品
}
}
这种通过题目条件的限制来节省DFS计算量的方法称作剪枝。
事实上,上面的问题给出了一类常见DFS问题的解决方法,即给定一个序列,枚举这个序列的所有子序列(可以不连续)。枚举所有子序列,从中根据某种特征选出最有子序列。
这个问题也等价于枚举从N个整数中选择K个数的所有方案。
例如这样一个问题:给定N个整数(可能有负数);从中选择K个数,使得这K个数之和恰好等于一个给定的整数X;如果有多种方案,选择他们中元素平方和最大的一个,给出的数据能保证这样的方案唯一。
与之前的问题类似,此处仍然需要记录当前的编号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<vector>
#include <cstdio>
#define maxn 10010
using namespace std;
//序列A中n个数选k个数使得和为x,最大平方和为maxSumSqu
int n, k, x, maxSumSqu = -1, A[maxn];
//temp存放临时方案,ans存放平方和最大的方案
vector<int> temp, ans;
//当前处理index号整数,当前已选整数个数为nowK
//当前已选整数之和为sum,当前已选整数平方和为sumSqu
void DFS(int index,int nowK,int sum,int SumSqu)
{
if(nowK==k && sum==x)
{
if(SumSqu>maxSumSqu)
{
maxSumSqu = SumSqu;
ans = temp;
}
return;
}
//已经处理完n个数,或者超过k个数,或者和超过x,返回
if (index == n || nowK > k||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);
}
最后几步就是选择路径、迭代,return前可以视为操作。
感觉这类问题的难点都是DFS的边界判定条件(死胡同)比较难想出来,想出来了其实都还好。太菜了太菜了
接下来看一道题(我是懒狗);
PAT A1103.Integer Factorization
将一个正整数N,写为N=n1^P+…+nk ^P的形式,其中ni数列为非递减数列,N,K,P给定,对于有多种组合的情况,选择ni之和最大的一个数列,如果还有多种方案,就按照底数序列的字典最大的方案。如果没有答案,就输出“impossible”1<P<=7;N>=K;
下面是错误代码
#include<cstdio>
#include<stdlib.h>
#include<iostream>
#include<vector>
#include<algorithm>
#include<cmath>
#include<string.h>
#define MAXN 400;
using namespace std;
int N, K, maxSum = -1, nowK = 0,curSum=0;//maxSum为当前最大的数列的和,curSum为得到的数列的P次方和
vector<int> ans, temp;
double maxNum=0,P; //表示index能达到的最大值
int A[400];//存放index数组
void DFS(int index,int nowK,int sum)
{
if(nowK==K)
{
if(sum>maxSum)//暂时只考虑这一种情况
{
maxSum = sum;
ans = temp;
}
}
if(nowK>K||index>=maxNum)
{
return;
}
//如果需要记录下一个数的话
temp.push_back(A[index]);
DFS(index,nowK+1,sum);
temp.pop_back();
//不选这个数据
DFS(index + 1, nowK, sum);
}
int main()
{
//输入数据
scanf("%d %d %lf",&N,&K,&P);
memset(A, 0, sizeof(A));
//计算最大数maxNum
maxNum = floor((pow(N,1/P)));
cout <<maxNum << endl; //得到了index的最大值
//将index值输入A数列
for (int i = 0; i < maxNum;i++)
{
A[i] = i;//第一位0要拿掉
}
DFS(1, 0, 0);
int count = 0;
while(count<K)
{
cout << ans[count] << " ";
count++;
}
system("pause");
return 1;
}
没想明白就开始写了。这个是错的。
涉及的多项式有幂运算。怎么确定index值的范围呢,我的想法是,根据数学规律,ni的值一定小于N的值对P开根号,所以index<N1/P,这里就得到了一个index的数列:
1<index<N1/P
对于 DFS函数中,需要做边界值判定,需要判断哪些值的边界呢?
1.判断现在有的值的P次方和是否超过N,超过则需要return,退回上一个节点,换一个值
2.判断nowK是否大于K,如果nowK==K则需要return
3.判断所有可能答案中sum和最大的值
4.无解的状态是什么呢?无解要输出impossible。应该是在计算后ans仍为为初始化的状态
5.index中的值是可以重复选的,要体现出来
目前想到的就这么些。
那构思一下DFS的函数:
void DFS(int index,int nowK,int sum,int posSum)//posSum为P次方和
那根据上面写的决策试着写一下函数的代码:
#include<stdio.h>
#include<string.h>
#include<algorithm>
#include<string.h>
#include<iostream>
#include<cmath>
#include<vector>
using namespace std;
int N, K, maxSum,stdNum;//stdNum为最大index可以达到的值
vector<int> ans, temp;
double P;
void DFS(int index,int nowK,int sum,int posSum)
{
if(nowK==K)//满员了,死胡同,判定这个符不符合要求,最后都是return
{
if(posSum==N && sum>maxSum)//决策符不符合要求,如果符合要求,就记录这个数列ans
{
maxSum = sum;
ans = temp;
}
return //结束判定就return
}
if(index>stdNum||posSum>N)//补强边界条件
{
return;
}
DFS(index+1,nowK,sum,posSum);//这样生成的序列是升序,而题目要求是降序
if(nowK<K && posSum<=N) //减少岔路
{
temp.push_back(index);
DFS(index, nowK+1, sum + index, posSum + pow(index, P));
temp.pop_back();
}
}
根据自己的想法写的,还没对过答案。这里没有考虑如果有sum一样的情况怎么判断选择哪个可能答案的情况。
看了一下答案,在减少岔路的判定上其用的是index-1>=0;emmm我觉得我这样也可以吧。
关于剪枝的知识以后再补充吧。
算法笔记