7-26,是我们2015年信息学夏令营的第一天,我们主要就是学习了搜索。
=================================================
搜索又分宽度搜索与深度搜索。这两个搜索又有什么区别呢?深度搜索是指深度优先搜索,宽度搜索就是宽度优先搜索。
搜索主要是看搜索树:
——这颗(搜索)树的开始的点就是源点。
小技巧:当一颗搜索树有多个源点时,可以把它们的根连到一个虚点,然后把这个虚点作为总连接点。
深度搜索就是从第一个(源点)一直找到底,然后再返回到最近的一个分支,再继续深度搜索,然后再返回······以此类推。
宽度搜索就是从原点开始,把每一层的点都按顺序找,一直到最后一层。
=================================================
我们这两天主要学的是深度搜索。
深度搜索有两种实现方法:一是栈,二就是我们常用的递归。
栈本身就跟深度搜索很相似,所以实现起来也应该比较容易理解,但是我们没有深入研究,那就等下次再写出来吧。栈大概就像叠盘子,后进来的,放在最上面;出去的也是最上面的先出去。如图:
=================================================
那现在就讲一下递归吧。递归就是一个函数(子程序)自己调用自己。也就是这样:
int find(int k)
{
……
k=find(k+1);
}
可以看到,在这个find函数中,自己调用了自己,使这个程序又开始执行find函数。需要注意的是,在递归过程中,程序会有执行另外一个find,也就是说如果你不断调用自己,那么程序会不断出现新的find函数。进一步说,如果程序无限调用自己,那么就会导致栈溢出,从而使程序崩溃——就像是for或者while的死循环一样。所以递归必须有一个边界条件,当执行到某个程度时,就退出当前的函数。
像这样:
int find(int k)
{
if(k==100) return ;/*边界条件,当k=100时return*/
find(k+1);
}
可以看到这个程序就用到了边界条件,停止递归。
注意:当你使用递归时,新函数中的变量与你一开始那个函数出现的变量不一样。也就是说,上面程序中的一开始的k其实一直都没有改变,只是新函数中传进来的k改变了。
因为使用递归最好的方法就是先找出问题中的子问题。什么是递归的子问题呢?举一个例子吧:
我们知道,上图中的“11”、“2112”、“321123”都是回文数,即正过来读与倒过来读都一样。我们可以看到,“11”在“2112”的中间出现了,换句话说就是第2行比第1行两边多了2;同理,第3行又比第2行两边多了3。由此我们可以推断出回文数的第n行比第(n-1)行两边多了n,也就是f(n)=n f(n-1) n。这些“11”,“2112”等小问题就是递归的子问题。找到这些子问题,实现递归起来自然就比较容易了。
=================================================
那么我们如何通过递归实现搜索呢?下面我们就一个程序来展示出来:
要求打印出“1,2,3,4”的全排列。
#include <iostream>
#include <fstream>
using namespace std;
ifstream fin("qpl.in");
ofstream fout("qpl.out");
#define cout fout
#define cin fin
int n;
bool f[1000];
int ans[1000];
int gcd(int a,int b)
{
if(a%b==0) return b;
return gcd(b,a%b);
}
void _print()
{
for(int i=1;i<=n;i++)
cout<<ans[i]<<" ";
cout<<endl;
}
void search(int m)
{
if(m>n)
{
_print();
return ;
}
for(int i=1;i<=n;i++)
{
if( m==1 || ( m>1 && gcd(i,ans[m-1])==1 ))
if(f[i]==0)
{
f[i]=1;
ans[m]=i;
search(m+1);
f[i]=0;
}
}
}
int main()
{
cin>>n;
search(1);
return 0;
}
重点是看search(递归)这一段。
void search(int m) /*dfs*/
{
if(m>n) /*边界条件*/
{
_print();
return ;
}
for(int i=1;i<=n;i++)
{
if( m==1 || ( m>1 && gcd(i,ans[m-1])==1 )) /* 符合搜索的条件*/
if(f[i]==0)
{
f[i]=1; /* 标记为用过 (f表示这个数是否用过)*/
ans[m]=i; /*把答案记录下来 */
search(m+1); /* 递归----自己调用自己*/
f[i]=0; /*还原 */
}
}
}
=================================================
我们还扯到了一点关于memset的知识,所以有几点要注意的:
① memset需要包含<cstring>库。因为有些编译器会自动包含一些常用哭,所以你在自己运行时即使没包含这个库也不会出错。但是一到测试软件时就直接out了。
② 当你用memset初始化一个int类型的数组时,你最好就是把他初始化为0或-1。若你要初始化成1的话,如图所示:

那究竟是为什么呢?我们来分析一下:int有4字节,所以这个a数组有400字节,也就是说sizeof(a)是400。那么这个a数组中每个元素中的4个单元都会分别填充,而且还是以一种奇怪的方式填充:
这个奇怪的方式是什么呢,因为我还在改题,那就下次在继续写吧。顺便说一下,我们还学了宽搜呀,位操作呀什么的。好,下次再写!