《算法笔记》深度优先搜索(DFS)初步学习

当碰到岔道口时,总是以"深度"作为前进的关键词,不碰到死胡同就不回头,因此把这种搜索方式称为“深度优先搜索”

深度优先搜索是一种枚举所有完整路径以遍历所有情况的搜索方法。
那么如何实现深度搜索呢?把迷宫中的关键节点用字母代替
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我觉得我这样也可以吧。

关于剪枝的知识以后再补充吧。

算法笔记

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值