dfs算法入口参数浅析

好久不写博客,懒了也胖了。之前半年一直在忙着保研的事情,算法也落下了不少,最近仓促准备蓝桥杯,刷了几道题目。

之前一致认为dfs算法算是我用的比较熟练的,但是最近遇到了一些相关的题目,让我不得不重新审视这个算法。

dfs算法就不多介绍,一句话说他就是”一条路走到底,不撞南墙不回头”的算法,本文主要研究dfs算法的入口函数时的参数选择问题。

问题引入:

  用dfs求排列组合的结果,例如求C(13,5),即在13个不同的物品中取5个物品,共有多少种情况?

问题初解:

 说实话,很简单,遍历13个物品,每个物品有拿和不拿两种情况,再考虑回溯和剪枝就行。代码如下:

#include<iostream>
#include<cstdlib>
#include<cstdio>
#include<memory.h>
#include<algorithm>
using namespace std;
int res=0;
int N=13,M=5;
void dfs(int ind,int sum)
{
	if(sum==M){               
	   res++;
	   return ;
	} 
	if (ind>=N || sum>M)    
	  return ; 
	dfs(ind+1,sum);
	dfs(ind+1,sum+1);
	return ;
}
int main()
{   
	dfs(0,0); 
	printf("%d\n",res);
	return 0;
} 

但是其实这里有一些看似不起眼但是仔细研究很有味道的小问题(我是这么觉得)。比如这里dfs为什么从dfs(0,0)开始?对于ind>=N || sum>M 和sum==M的判断能否交换顺序?这里物品的编号ind范围是0-(N-1)还是1-N?

对于dfs算法,了解其每个参数的意义是非常重要的,这里ind就表示当前遍历到的物品的编号,sum就表示当前手中已经拿到的物品总数。那么这sum个物品中是不是包括编号为ind的物品呢?其实这个问题就是解决之前所有问题的一个切入口。这里的sum是不包括编号为ind的物品的。

我们可以假想这样一个场景:物品编号为0-12,我们两手空空站在放有第0号物品的桌子面前开始dfs,即dfs的入口dfs(0,0)。在不断拿物品,不断走向编号更高的物品时,我们只要手中的物品数sum==M,那么就return,结束了,这是其中一种拿物品的方式,当然在当前物品编号ind>=N或者手中的物品总数sum>M我们就停止了,这不是一种符合的拿物品的方式。

对于另一个问题:ind>=N || sum>M 和sum==M的判断能否交换顺序?读者可以稍微修改上述代码运行下,显然是不行的。其实仔细想一下:当我们遍历完所有的物品,即走到了最后一个物品(N-1)后面一张桌子(N),这是我们的ind虽然是大于等于N,但是如果我们手中正好拿到了M个物品,那么也是符合的,如果直接根据ind>=N就return,就会少算这种情况。所以要先判断sum==M,如果满足,那么就找到了一种情况,这时候不用考虑手中物品的编号大于等于N,因为我们手中的物品是不包括ind的,即肯定是小于ind的。

问题引申:

有读者可能会想,物品的编号我偏偏是从1-N,那又该怎样Code呢?如下:

#include<iostream>
#include<cstdlib>
#include<cstdio>
using namespace std;

int res=0;
int N=13,M=5; 
void dfs(int ind,int sum)
{
	if(ind>N || sum>M)      
		return ;
	if(sum==M) {          
		res++;
		return ;
	}
	
	dfs(ind+1,sum);            
	dfs(ind+1,sum+1);         
	return ;
}
int main()
{
	dfs(0,0);         
	printf("%d\n",res);
	return 0;
}

这是我们看到,仍然是从dfs(0,0)开始,但是两个判断的顺序换了。

对于这种情况,我们仍然可以考虑一种情景,那就是:我们离开放有编号为ind物品的桌子后手中的物品数是sum,即sum中已经遍历完物品编号为ind的物品(注意这里与上一种情况的不同),那么对于两个判断ind>N || sum>M和sum==M能否交换,答案仍然是”No”。我们可以想一下,假如先判断sum==M,因为我们手中的sum包括当前编号为ind的物品,如果不幸,ind是大于最大编号N的,那么肯定是不符合的。所以我们要先判断手中的最大编号ind是不是已经”越界”了,顺便判断手中的物品数量有没有多于sum,如果都没问题的话,再判断手中的数量是不是等于sum。

这里也许会有人问?既然dfs意义是包含ind的总物品数是sum,那么为什么dfs入口是dfs(0,0),其实是完全可以写成dfs(1,0)~dfs(1,4),即一号物品我们分别拿了0-4个 ,但是显得有点冗余,我们可以假设编号1的桌子之前还有张编号为0的桌子(虚拟的),我们经过0号桌子不拿东西,就变成了dfs(0,0)入口。

“找茬”:

可能还会有读者想:凭什么0-(N-1)就是不包括ind,1-N就是包括ind,我偏要0-(N-1)时也要包括ind,那么该怎么写呢?如下:

#include<iostream>
#include<cstdlib>
#include<cstdio>
#include<memory.h>
#include<algorithm>
using namespace std;
int cnt=0;
//物品编号从0-(nn-1) -拿了(不推荐)
int N=13,M=5;
void dfs(int ind,int sum)
{
	if (ind>=N || sum>M)   
	  return ; 
	
	if(sum==M)     
	{              
	   cnt++;
	   return ;
	}
	 
	dfs(ind+1,sum);
	dfs(ind+1,sum+1);
	return ;
}
int main()
{   
	dfs(-1,0); 
	printf("%d\n",cnt);
	return 0;
}              

我们可以看到如果是包括ind,不管你是下标0-(N-1)还是1-N,那么都要先判断手中最大编号ind有没有越界,然后再判断手中物品总数sum。 对于0-(N-1)包括ind,我们同样在入口时候使用dfs(-1,0),即虚拟一张编号为-1的桌子。

总结:1.先判断ind,还是先判断sum>M,是有dfs意义决定的,即包括ind就要先判断ind,

        2.下标0-(N-1)推荐使用不包括ind的情况

        3.下标1-N推荐使用包括ind的情况

PS:推荐的情况都是为了使得入口能够用dfs(0,0)的形式,毕竟dfs(-1,0)感觉不太好看。不过只要形成自己的算法体系就好。

点赞