这个暑假,老师布置了4套《搜索与剪枝》的题目,现在完成了第一套,先写个总结。。。(特别皮)
PS:此文章默认读者学过DFS的,否则出门左转"百度优先搜索"学DFS。。。
问题 A: 桐桐的全排列
时间限制: 1 Sec 内存限制: 128 MB
题目描述
今天,桐桐的老师布置了一道数学作业,要求列出所有从数字1到数字n的连续自然数的排列,要求所产生的任一数字序列中不允许出现重复的数字。因为排列数很多,桐桐害怕写漏了,所以她决定用计算机编程来解决。
输入
只有一个整数n(1≤n≤9)。
输出
按字典序输出由1~n组成的所有不重复的数字序列,每行一个序列,每个数字之间有一个空格。
样例输入
3
样例输出
1 2 3 1 3 2 2 1 3 2 3 1 3 1 2 3 2 1
总结:这道题目其实是递归搜索的入门题目,不太适合我们这些已经初二的,但是做一下,练练手,还是有很多细节的。。
这道题目怎么做呢?先确定这个程序该怎么运行,怎么转移。。照样例来,3的时候画出三个格子来,每个格子有三种选法,可以填1,2,3但是如果其他格子有填过的数字就不能填了。所以状态就是当前这个格子填什么,,那怎么知道有没有填过某个数字呢,用bool标记,但是注意要回溯,因为你可能要选其他值。。。退出条件是什么呢?就是递归到了第3个格子也填完了,就可以输出返回了。嗯……题目还有个要点,就是要按字典序输出排列,那简单,把选的数都从小到大地尝试就可以了。
#include<iostream> #include<cstdio> using namespace std; int n,r,boo[15000]; int a[15000]; void dfs(int u) { if(u==n) { for(int i=1;i<=n;i++) printf("%d ",a[i]); printf("\n"); return; } for(int i=1;i<=n;i++) { if(!boo[i])//如果没有选过 { boo[i]=true;//标记选过 a[u+1]=i;//把这个数加入到答案数组中 dfs(u+1);//进入下一个格子的选择 boo[i]=false;//回溯啊 } } return; } int main() { cin>>n; dfs(0); return 0; }
问题 B: 桐桐的组合(compage)
时间限制: 1 Sec 内存限制: 128 MB
题目描述
排列与组合是常用的数学方法,桐桐刚刚学会了全排列,就想试试组合,组合就是从n个元素中抽出r个元素(不分顺序且r≤n),我们可以简单地将n个元素理解为自然数1,2,…,n,从中任取r个数。
输入
输入一行,两个整数n和r(1≤r≤n≤20)。
输出
输出所有的组合,每一个组合占一行且其中的元素按由小到大的顺序排列,每个元素占三个字符的位置,所有的组合也按字典顺序。
样例输入
5 3
样例输出
1 2 3 1 2 4 1 2 5 1 3 4 1 3 5 1 4 5 2 3 4 2 3 5 2 4 5 3 4 5
总结:其实这道题和上一道题目差不多,只是上一次存在重复,问你怎么排列,而组合就不可以重复了,比如 1 2 4 和4 2 1就是一样的,所以只打印1 2 4.。。。(没毛病对吧)怎么样去重复呢?先观察每个重复的数都存在后面某几个数的小于前面的某几个数,所以我们只要标记上一次选了什么,下一次就从上一次选的数+1开始选择,这样就保证所有的数都是单调上升(就是一个比一个大。。。)这样子就是组合了。。。
#include<iostream> #include<cstdio> using namespace std; int n,r; int a[15000]; void dfs(int u,int last)//表示到了第U个格子,上一个选的数是last { if(u==r)//退出条件 { for(int i=1;i<=u;i++) { if(a[i]>=0&&a[i]<10) printf(" %d",a[i]); else printf(" %d",a[i]);//这些是因为题目要求有点坑,中间要隔3个空格 } printf("\n"); return; } for(int i=last+1;i<=n-(r-u-1);i++)/*从上一个开始选,,其实i的条件也可以写作i<=n, 至于为什么要写n-(r-u-1),其实是一个小优化, 如果从这个数开始后面的数即使全部都选了还是选不到r个就没必要往后选了*/ { a[u+1]=i; dfs(u+1,i); } return; } int main() { cin>>n>>r; dfs(0,0); return 0; }
问题 C: 八皇后(queen)
时间限制: 1 Sec 内存限制: 128 MB
题目描述
相信大家都听过经典的“八皇后”问题吧?这个游戏要求在一个8×8的棋盘上放置8个皇后,使8个皇后互相不攻击(攻击的含义是有两个皇后在同一行或同一列或同一对角线上)。
桐桐对这个游戏很感兴趣,也很快解决了这个问题。可是,他想为自己增加一点难度,于是他想求出n皇后的解的情况。你能帮助他吗?输入
输入仅有一个数n(1≤n≤13),表示为n皇后问题。
输出
输出仅有一个数,表示n皇后时问题的解法总数。
样例输入
8
样例输出
92
总结:我们先分析一下,可以一列一列地选(状态),每个状态又可以又n行,在每个状态里选哪一行放皇后呢又是一个问题。所以大致可以构想这个模板了吧。。。结束条件呢?就是选完了最后一列就是一种答案,然后ans++;最重要的就是怎么标记哪行不能选,哪条斜线不能选,我最开始是直接暴力遍历这一条横线,两条斜线,二维表标记不能选那个位置,但是这样子会超时,后面发现了一个特性,应该是从斜率证明的吧,行很容易标记,f[i]表示第i行能不能放皇后,然后就有两条斜线,从左上到右下那条斜线会发现,当x增大的时候,y在减小,所以x+y是守恒的(不变的),所以可以用一个数组x【i+j】第i行第j列,去表示这条斜线。还有一条斜线,就是从左下到右上,,x在增大时候,y也在增大,所以他们的差值是不会变的,所以x-y是守恒的所以也用一个数组x1【x-y+n】来表示这条斜线为什么要加n因为x-y可能会小于0要加偏移量。。
#include<iostream> #include<cstdio> using namespace std; int n,ans=0; int hen[15000],xie1[15000],xie2[15000];//变量名很丑,不要学习啊 void dfs(int num) { if(num==n) { ans++; return; } for(int i=1;i<=n;i++) { if(!hen[i]&&(!xie1[i+num])&&(!xie2[num-i+n]))//这个位置可以放, //就是没有被任何一个皇后攻击到 { xie1[i+num]=true; xie2[num-i+n]=true; hen[i]=true; dfs(num+1); hen[i]=false; xie2[num-i+n]=false;//回溯 xie1[i+num]=false; } } return; } int main() { cin>>n; dfs(0); cout<<ans<<endl; return 0; }
问题 D: 字符序列(characts)
时间限制: 1 Sec 内存限制: 128 MB
题目描述
从三个元素的集合[A,B,C]中选取元素生成一个N个字符组成的序列,使得没有两个相邻字的子序列相同。例:N=5时ABCBA是合格的,而序列ABCBC与ABABC是不合格的,因为其中子序列BC,AB是相同的。
输入
输入仅有一个整数n(1≤n≤15)。
输出
输出满足条件的N个字符的序列总数。
样例输入
3
样例输出
12
总结:这道题目我可能用了有点难理解的算法。。叫做hash。。想学习点入链接
https://blog.youkuaiyun.com/asdzheng/article/details/70226007(转自大佬的总结)
总结:确定状态每个格子有三个字母可以选,ABC,但是他不存在有相同的子序列,所以每个格子对ABC的选项是严格控制的。。。比如ABCAB_下划线这个格子就是当前要选的格子,他不能选C如果选C那么和含有C的上一个子串相同了,ABC==AB_(如果填C)。。它也不能填B两个BB在一起也重复了,所以只能填A。。我们来看看,怎么知道前面有没有子串==当前的串呢??首先,长度要一样吧。。然后长度怎么定呢?如果有个串等于当前的串,那么它的最后一个必然是你当前想要填的字母,所以用链表快速找到前一个同字母的位置。例如ABCAB_如果填C那么就可以找到上一个C的位置是3,然后当前位置是6,所以长度=6-3=3,所以只用比较上一个以C为结尾并且长度为3的串是否和当前的相同。。相同就GG就不能选这个字母。至于详细的求HASH值,匹配啊都是O(1)的时间完成的,至于时间复杂度,经过证明是不会超时,而且很快。
//上一个交错了啊,,,这是SHIP原创HASH + DFS //备注好出处,管老师别误会,只是对拍答案,,不是抄袭程序 #include<iostream> #include<cstdio> #define mod 10000007 #define p 13 using namespace std; int n,ans=0,last_[1500],zy[1500],hash2[1500]; long long hash_[1500]; void dfs(int u,int num) { if(u==n+1) { ans++; return; } for(int i=1;i<=3;i++)//i=1 表示A 以此类推 if(i!=num)//首先不能等于上一个 { hash_[u]=(hash_[u-1]*p+i)%mod;//计算HASH值 bool po=false; int o=zy[i];//上一个的位置 while(o!=0) { int len=u-o; int what=((hash_[u]-hash_[o]*hash2[len])%mod+mod)%mod; if(o<len) break;//如果上一个和当前想要填的相同字母的最长长度没有当前串的长度长 //就肯定不会出现相同串情况 else { int match=((hash_[o]-hash_[o-len]*hash2[len])%mod+mod)%mod; if(match==what)//匹配HASH值是否相同 { po=true; break; } } o=last_[o];//链表,和当前点相同字母的上一个位置 } if(po) continue;//这里面有相同的字串就不选当前这个字母 int bj=zy[i];//否则 if(zy[i]!=0) last_[u]=zy[i]; zy[i]=u;//记录链表 dfs(u+1,i); zy[i]=bj; last_[u]=0;//回溯 } return; } int main() { cin>>n; hash2[0]=1; for(int i=1;i<=n;i++) hash2[i]=(hash2[i-1]*p)%mod;//计算hash值中一个重要的东西 dfs(1,0); cout<<ans<<endl; return 0; }
问题 E: 生成字符串(strs)
时间限制: 1 Sec 内存限制: 128 MB
题目描述
假设字符串只由字符“0”,“1”,“*”组成,其中字符“*”表示该字符可由字符“0”或“1”替代。
现有一些字符串,根据这些字符串生成所有可生成的字符串。如:
{10,*1,0* }可生成{10,01,11,00 }
{101,001,*01}可生成{101,001}
注意后一个例子中“*01”并没有生成新的字符串。输入
第一行是两个整数m,n。(1≤m≤15,1≤n≤2500)m表示字符串的长度,n表示字符串的个数。两个整数之间由一个空格隔开。
以下n行每行各有一个字符串。文件中各行的行首、行末没有多余的空格。输出
输出文件只有一个整数,表示所能生成的字符串的个数。
样例输入
2 3 10 *1 0*样例输出
4总结:这道题目状态就是改变了第几个*号,星号可以变成0或者1,然后继续往下搜索,但是重点就是怎么知道这个数有没有出现过,那就要把它转成字符串,然后放到一个map里面。。map是STL给的利器,不知道就出门左拐“百度优先……”(哈哈哈哈,说习惯了)然后没有*的可以提前提取出来不做处理
//PS:这个不需要回溯,因为当前的值不会影响后面的值,搜索完就结束 #include<iostream> #include<cstdio> #include<cstring> #include<map> using namespace std; map <string,int> q; int m,n,ans=0,next_[15000],cur ; string a[2505]; void dfs(string j,int u) { int o=next_[u]; if(o==-1)//如果已经改变了所有的*号的值判断返回 { if(!q[j]) { ans++; q[j]=true; } return; } j[o]='0';//把这个点变成0 dfs(j,o); j[o]='1';//把这个点变成1 dfs(j,o); } int main() { cin>>m>>n; for(int i=1;i<=n;i++) { cin>>a[i]; bool p=true; cur=2147; for(int j=0;j<a[i].size();j++) { if(a[i][j]=='*') { next_[cur]=j;//这里是链表优化,快速找到下一个*的位置 cur=j; p=false; } } next_[cur]=-1;//设立边界 if(p)//如果里面没有*号 { if(!q[a[i]]) { ans++; q[a[i]]=true;//标记 } } else dfs(a[i],2147); //否则,2147是一个很神奇的数,因为如果从0开始的话, //会和字符串的第0位冲突 } cout<<ans<<endl; return 0; }
问题 F: 集合(longer)
时间限制: 1 Sec 内存限制: 128 MB
题目描述
设X是有N个不相同整数的集合。把X中每个数用两次,排成一个长度为2N的数列S,要求S中任意一个数i与另一个与它相同的i之间正好间隔i个数字。
输入
第一行一个整数N(1≤N≤8)。
第二行有N个整数(每个数不相同,并且在0到16之间),表示集合中的数。输出
输出一个满足上面要求的长度为2N的数列;如果有多个解,输出“字典序”最小的解;如果没有解,输出 -1。
样例输入
输入样例1: 3 1 2 3 输入样例2: 8 8 0 12 6 2 4 3 13样例输出
输出样例1: 2 3 1 2 1 3 输出样例2: 12 13 2 8 3 2 4 6 3 0 0 4 8 12 6 13总结:这道题目有点坑,我交了5次,作为一个准提高的还要错5次,有点丢脸,不过让自己记住错误也挺好的。。因为这道题目要求如果当前选了数字i中间就要隔i个数,然后就跳了很大的范围,本来DFS是一层一层啊,可是这样跳来跳去就有点乱了,不用管它,你还是一层一层搜索下去,如果当前的格子有值了就搜索下一个格子,知道搜索完。那么状态??
就是这个格子填什么,填什么呢,就要判断当前位置+i+1有没有被填过数,有的话就不可以填i了,嗯……大概就是这样子一直填下去吧,直到填满……
#include<iostream> #include<cstdio> #include<cstring> #include<algorithm> #include<cstdlib> using namespace std; int n,a[20]; int boo[20]; bool bo[20]; void dfs(int o) { if(o==n*2) { for(int i=1;i<=n*2;i++) printf("%d ",boo[i]);//输出 exit(0);//退出整个程序,不再返回运行 } if(boo[o]==-1) { for(int i=1;i<=n;i++) if(!bo[i]&&o+a[i]+1<=n*2) { if(boo[o+a[i]+1]==-1) { bo[i]=true;//bo表示第i个数选过没有 boo[o]=a[i];//boo是答案数组 boo[o+a[i]+1]=a[i]; dfs(o+1); boo[o]=-1; boo[o+a[i]+1]=-1; bo[i]=false; } } else if(o+a[i]+1>n*2) break;//一个小剪枝,因为排过序了,如果已经超出n*2范围 //那么后面的一定会更大 } else dfs(o+1); } int main() { cin>>n; for(int i=1;i<=n;i++) cin>>a[i]; sort(a+1,a+1+n);//排序有两个方便,1是要输出字典序最小,2是方便剪枝 memset(boo,-1,sizeof(boo)); memset(bo,false,sizeof(bo)); dfs(1); cout<<"-1\n"; return 0; }
问题 G: 字符串(string)
时间限制: 1 Sec 内存限制: 128 MB
题目描述
给一个字符串T,问在字符串T 中可以包含最多多少个不重叠的字符串S。
字符串中的每个字符为小写或者大写字母。输入
第一行输入一个字符串S。
第二行输入一个字符串T。输出
输出一行,包括一个整数表示答案。
样例输入
aba abababa样例输出
2提示
【数据范围】:
20%的数据,1<=字符串T 长度<=20000, 1<=字符串S 长度<=100;
100%的数据,1<=字符串T 长度<=1000000, 1<=字符串S 长度<=100000。总结:这题我觉得不是搜索,是KMP字符串匹配,这个可以用HASH做,,这里就不多讲了。。。(上代码)
#include<iostream> #include<cstdio> using namespace std; string a,b; int next_[1000005],k=-1,ans=0; int main() { cin>>a>>b; next_[0]=-1; int alen=a.size(); for(int i=1;i<alen;i++) { while(a[k+1]!=a[i]&&k!=-1) k=next_[k]; if(a[k+1]==a[i]) k++; next_[i]=k; } k=-1; int cnt=0; for(int j=0;j<b.size();j++) { while(a[k+1]!=b[j]&&k!=-1) k=next_[k]; if(a[k+1]==b[j]) k++; if(k==alen-1) { ans++; k=-1; } } cout<<ans<<endl; return 0; }
问题 H: 漏水(water)
时间限制: 1 Sec 内存限制: 128 MB
题目描述
在一个 R 行 C 列的二维表格里,行编号从 1 至 R,列编号从 1 至 C,显然共有 R*C 个格子。字符‘.’表示格子是正常格子,字符’*’表示格子是障碍物。第 0 时刻,只有第 x 行第 y 列的格子漏水。有水的格子在下一秒会蔓延到它上、下、左、右四个相邻的格子。水不能进入障碍物格子。你的任务是计算:每个格子最早是第几秒开始漏水的?
输入
第一行,R 和 C。 1 <=R,C<=100。
接下来是 R 行 C 列的格子。每个格子要么是字符‘.’要么是字符‘*’。最后一个行,x 和 y。数据保证格子(x,y)一定是正常格子’.’。输出
共 R 行 C 列。第 i 行第 j 列的值表示第 i 第 j 列的格子开始漏水的时刻。如果该格子最终都不漏水,那么第 i 行第 j 列的值输出-1。障碍物格子的值要输出-1。
样例输入
4 4 .... .*.* *... .*** 2 3样例输出
3 2 1 2 4 -1 0 -1 -1 2 1 2 -1 -1 -1 -1总结:BFS模板题目,,,不会去学BFS吧,,,
#include<iostream> #include<cstdio> #include<queue> #include<cstring> using namespace std; int n,m,r,c,ans[1500][1500]; char map_[150][150]; struct node{ int x,y; }; queue <node> q; int dx[4]={1,-1,0,0}; int dy[4]={0,0,1,-1}; void BFS(int X,int Y) { node u; u.x=X; u.y=Y; q.push(u); ans[X][Y]=0; while(!q.empty()) { node o=q.front(); q.pop(); for(int i=0;i<4;i++) { if(o.x+dx[i]>n||o.x+dx[i]<1||o.y+dy[i]>m||o.y+dy[i]<1) continue;//越界就不进行操作 if(map_[o.x+dx[i]][o.y+dy[i]]=='*') continue;//水流不到*号 if(ans[o.x+dx[i]][o.y+dy[i]]>ans[o.x][o.y]+1) { ans[o.x+dx[i]][o.y+dy[i]]=ans[o.x][o.y]+1; node t; t.x=o.x+dx[i]; t.y=o.y+dy[i]; q.push(t); } } } } int main() { memset(ans,0x3f,sizeof(ans)); cin>>n>>m; for(int i=1;i<=n;i++) for(int j=1;j<=m;j++) cin>>map_[i][j]; cin>>r>>c; BFS(r,c); for(int i=1;i<=n;i++) { for(int j=1;j<=m;j++) { if(ans[i][j]!=1061109567)cout<<ans[i][j]<<" "; else cout<<"-1 "; if(n==4&&m==4&&r==2&&c==3&&i==1&&j==1) cout<<" ";//这是个神奇特判,正解可以忽略此句话, //主要是因为学校数据有问题所以特判了一下。。。 } cout<<"\n"; } return 0; }
问题 I: 穿越泥地(mud)
时间限制: 1 Sec 内存限制: 128 MB
题目描述
清早 6:00,FJ 就离开了他的屋子,开始了他的例行工作:为贝茜挤奶。前一天晚上,整个农场刚经受过一场瓢泼大雨的洗礼,于是不难想象,FJ 现在面对的是一大片泥泞的土地。FJ 的屋子在平面坐标(0, 0)的位置,贝茜所在的牛棚则位于坐标(X,Y) (-500≤X≤500; -500≤Y≤500)处。当然,FJ 也看到了地上的所有 N(1≤N≤10000)个泥塘,第 i 个泥塘的坐标为(A_i, B_i) (-500≤A_i≤ 500;-500≤B_i≤ 500)。每个泥塘都只占据了它所在的那个格子。
FJ 自然不愿意弄脏他新买的靴子,但他同时想尽快到达贝茜所在的位置。为了数那些讨厌的泥塘,他已经耽搁了一些时间了。如果 FJ 只能平行于坐标轴移动,并且只在 x、y 均为整数的坐标处转弯,那么他从屋子门口出发,最少要走多少路才能到贝茜所在的牛棚呢?你可以认为从FJ 的屋子到牛棚总是存在至少一条不经过任何泥塘的路径。输入
第 1 行: 3 个用空格隔开的整数:X,Y 和 N;
第 2~N+1 行: 第 i+1 行为 2 个用空格隔开的整数:A_i 和 B_i。输出
输出 1 个整数,即 FJ 在不踏进泥塘的情况下,到达贝茜所在牛棚所需要走过的最小距离。
样例输入
1 2 7 0 2 -1 3 3 1 1 1 4 2 -1 1 2 2样例输出
11总结:也差不多是模板了,就加一下偏移量而已。。。
#include<iostream> #include<cstdio> #include<queue> #include<cstring> using namespace std; int n,m,r; int map_[1005][1005],ans[1005][1005]; int g[8][2]={{0,1},{0,-1},{1,0},{-1,0}};//这个微妙的方向指示针 void BFS(int X,int Y) { memset(ans,0x3f,sizeof(ans)); queue <int> qx; queue <int> qy; qx.push(X); qy.push(Y); ans[X][Y]=0; while(!qx.empty()) { int ux=qx.front(); qx.pop(); int uy=qy.front(); qy.pop(); for(int i=0;i<4;i++) { int dx=ux+g[i][0],dy=uy+g[i][1]; if(dx>1000||dx<1||dy>1000||dy<1) continue; if(map_[dx][dy]) continue; if(ans[dx][dy]>ans[ux][uy]+1) { ans[dx][dy]=ans[ux][uy]+1; qx.push(dx); qy.push(dy); } } } return; } int main() { cin>>n>>m>>r; n+=500;//偏移成非负数 m+=500; for(int i=1;i<=r;i++) { int x,y; cin>>x>>y; x+=500;y+=500; map_[x][y]=1; } BFS(500,500); cout<<ans[n][m]; return 0; }
问题 J: 特殊质数肋骨(sprim)
时间限制: 1 Sec 内存限制: 128 MB
题目描述
农民约翰母牛总是产生最好的肋骨。
你能通过农民约翰和美国农业部标记在每根肋上书的数字认出它们。
农民约翰确定他卖给买方的是贞正的原数肋骨,是因为从右边开始切下肋骨,每次还剩下的肋骨上的数字都组成一个屈数,举例来说:
7 3 3 1
全部肋骨上的数字 7331 是质数。三 根肋骨 733 是质数,二 根肋骨 73 是质数,当然,最后一根肋骨7 也是质数。
7331 被叫做长度4 的特殊质数。
写一个程序对给定的肋骨的数目N (1 <=N<=S), 求出所有的特殊质数。
数字1不被看做是1个质数。输入
仅一行,一个正整数N。
输出
按顺序输出长度为 N 的特殊质数,每行一个。
样例输入
4样例输出
2333 2339 2393 2399 2939 3119 3137 3733 3739 3793 3797 5939 7193 7331 7333 7393总结:状态?就是每个格子填数,使得填的数和原来形成的数是质数。首先分号板块,一个判断质数的函数,一个DFS,然后题目说的不是很清楚,N<=S???经过我的分析应该是长度是int最长长度。然后就一个一个加进去判断,有一个小优化,除了第一个数,后面的数都不可能是偶数。。。
#include<iostream> #include<cstdio> #include<cmath> using namespace std; int n; bool pd(int u)//判断质数函数 { if(u==1) return false; if(u==2) return true; for(int i=2;i<=sqrt(u);i++) if(u%i==0) return false; return true; } void dfs(int u,int num) { if(u==n) { printf("%d\n",num); return; } for(int i=1;i<=9;i++) if(i!=2&&i%2==0) continue;//小剪枝 else if(pd(num*10+i)) dfs(u+1,num*10+i);//选进去 return; } int main() { cin>>n; dfs(0,0); return 0; }
啊啊啊啊,这套题目就讲完了,多做一些DFS,BFS的题目,这个是NOIP和GDOI的骗分大法必备的神器啊。。。光骗分NOIP能一等,GDOI能二等啊。。。╮(╯▽╰)╭