目录
2.可运用的问题:有组合问题,切割问题,子集问题,排列问题,棋盘问题这五种基本问题
1.素数环一个环由n个圈组成,将自然数1-n放入圈内,使得任意相邻圈的两个数之和均为素数。第一个圈的元素均为1。下图为n=6时的一个例子:
1.什么是dfs,以及算法的基础是什么?
dfs:深度优先搜索算法,是一种用于遍历或搜索树或图的算法.沿着树的深度遍历树的节点,尽可能深的搜索树的分支。当节点v的所在边都己被探寻过,搜索将回溯到发现节点v的那条边的起始节点。这一过程一直进行到已发现从源节点可达的所有节点为止。如果还存在未被发现的节点,则选择其中一个作为源节点并重复以上过程,整个进程反复进行直到所有节点都被访问为止。属于盲目搜索。简单来说就是一条路走到黑,直到没路了,或者找到结果才返回。
深度优先搜索算法的基础:递归与回溯
递归与回溯是相辅相成的,有递归就会有回溯,递归函数的下面部分就是回溯的过程以及回溯的逻辑(即递归是回溯的基础)
2.可运用的问题:有组合问题,切割问题,子集问题,排列问题,棋盘问题这五种基本问题
- 组合问题:N个数里面按一定规则找出k个数的集合
- 排列问题:N个数按一定规则全排列,有几种排列方式
- 切割问题:一个字符串按一定规则有几种切割方式
- 子集问题:一个N个数的集合里有多少符合条件的子集
- 棋盘问题:N皇后,解数独等等
3.如何去理解深度优先搜索的回溯算法
回溯算法是一个很抽象的东西,但是所有的回溯算法都可以抽象成一个树状结构,可以将其抽象成一个n叉树问题。如果满足递归的条件,树枝可以无限增加,直到找到所需要数据为止,如果不满足,树枝则会折断。树的深度取决于要搜索问题的层数,树的宽度取决于每个节点处理集合的大小。为方便理解,我增加出以下模板:
void 函数名(参数)//回溯算法的参数一般比较多,要根据具体情况进行分析
{
if(终止条件)
{
收集最后节点结果
return ;
}
//单层搜索的逻辑:
for(集合的元素)//遍历的是集合里的每一个元素,也可能是集合节点的子节点个数
{
处理节点
递归函数
回溯操作(撤销处理节点的操作)
}
return ;
}
4.(回溯)样例
1.素数环
一个环由n个圈组成,将自然数1-n放入圈内,使得任意相邻圈的两个数之和均为素数。第一个圈的元素均为1。下图为n=6时的一个例子:
程序样例
输入为一个整数n
- 6
- 8
输出分别为
-
1 4 3 2 5 6
1 6 5 2 3 4 -
1 2 3 8 5 6 7 4
1 2 5 8 3 4 7 6
1 4 7 6 5 8 3 2
1 6 7 4 3 8 5 2
本题是可以直接套用上述模板来写的,可以仔细思考一。
话不多说,直接上代码,详细解释在代码注释中
#include<stdio.h>
#include<math.h>
int su(int m);
void huan(int t);
int n;
int a[100]= {0};
int b[100]= {0};
int main() {
scanf("%d",&n);
a[0]=1; //初始化变量
b[0]=1;
huan(1); //从1开始遍列
return 0;
}
void huan(int t) {
int i;
if(t==n&&su(a[n-1]+a[0])) { //终止条件有两个,一个是首尾相接是否是素数,一个是圈内元素的个数,需要达到n(由用户输入)
for(i=0; i<n; i++) { //输出结果(但是每一次的输出都只是一个满足条件的“树枝”,不满足条件的会被筛掉)
printf("%d ",a[i]); //如果该if条件成立,则该“树枝”到头了
}
printf("\n");
} else {
for(i=2; i<=n; i++) {
if(b[i-1]==0) { //是否被使用
a[t]=i; //给数组a赋值
b[i-1]=1; //已经使用,在递归
if(su(a[t]+a[t-1])) { //回溯操作(这需要满足条件(为素数))
huan(t+1);
}
b[i-1]=0; //递归完,在给第二次循环使用,确保数据不变
}
}
}
}
int su(int m) { //该函数用于判断是否是素数,是huan函数的一个判断条件,对应模板中的终止条件
int i;
if(m<3) return 0;
else {
for(i=2; i<=sqrt(m); i++) {
if(m%i==0) {
return 0;
break;
}
}
}
return 1;
}
2.题目:代码部队
.这一道作者本人好早之前写的题,方法一样。(但是链接一时半会没找到,撮合看吧)
输入:
3
4
5
6
输出
2 1 3 4
1 2 3 4 5
4 5 1 2 3 6
方法一:找规律(因为只要输出全其中的一种,所以还是有规律的)
#include <stdio.h>
int main() {
int m;
scanf("%d",&m);
while(m--) {
int n,i;
scanf("%d",&n);
int a[101]= {0}; //定义数组
a[n]=n,a[n-1]=n-1;;
if(n==3)
a[1]=1,a[2]=2;
else if(n%2==0) {
for(i=1; i<=n-2; i++) {
if(i%2==0)
a[i]=i-1; //规律
else
a[i]=i+1;
}
}
else if(n%2!=0) {
a[1]=1,a[2]=2,a[3]=3;
for(i=4; i<=n-2; i++) {
if(i%2==0)
a[i]=i+1;
else
a[i]=i-1;
}
}
for(i=1; i<=n; i++)
printf("%d ",a[i]);
printf("\n");
}
return 0;
}
方法二:当然就是我们现在讲的,you know
#include<stdio.h>
int su(int x,int p); //回溯法
void huan(int t);
int n,x,y,z; //定义全局变量
int a[101]= {0};
int b[101]= {0};
int main() {
int m;
scanf("%d",&m);
while(m--) {
scanf("%d",&n);
a[101]= {0},b[101]= {0};
z=0;
x=0;
y=0; //因为要输入m次,要重新归0,否则只会输出一次结果
a[n]=n;
huan(1); //从1开始遍例
}
return 0;
}
void huan(int t) {
int i;
if(x==0&&y==n-1) { //输出条件,有n给数,并且x最后的值为n,并x在变化的过程中永远小于等于n
for(i=1; i<=n; i++) {
printf("%d ",a[i]);
}
printf("\n");
z=1; //防止继续输出结果而赋值,用于第40行的条件判定【if(z)break;】
} else {
for(i=1; i<n; i++) { //a[1]=i遍例
int s=x;
if(b[i]==0) { //如果i没被使用
a[t]=i;
b[i]=1; //意味着i已经被使用过了
y++;
x=su(x,i); //x的值变化
if(x<n) {
huan(t+1);
}
b[i]=0; //保持原有值不变
x=s;
y--;
}
if(z)break; //题目中说只输出一个n个数的答案即可,所以加终止条件
}
}
}
int su(int x,int p) { //判断x的值的函数
if(x<p)
return x+p;
else
return 0;
}
3.题目:迷宫
题目链接:https://www.luogu.com.cn/problem/P1605
输入:
2 2 1
1 1 2 2
1 2
输出:
1
这是洛谷上的一道dfs的入门题,我们通过这题来了解一下dfs
话不多说,直接上代码,当然,也不止我这一种方法,可能也有更好的
#include<stdio.h>
int a[11][11],l,r;
int dx[4]= {0,0,1,-1};
int dy[4]= {-1,1,0,0}; //上加下减,左减右加
int n,m,t,sum=0,q,p;
void fun(int x,int y) {
if(x==q&&y==p) {
sum++; //终止条件,可以套用模板,但是sum得设置为全局变量
} else {
for(int i=0; i<=3; i++) {
if(a[x+dx[i]][y+dy[i]]==0&&y+dy[i]>0&&x+dx[i]>0) {
a[x][y]=1; //代表(x,y)已经走过了
// printf(" %d %d\n",q,p); //验证数据是否正确
fun(x+dx[i],y+dy[i]); //回溯,递归
a[x][y]=0; //保证循环中数据不变
}
}
}
}
int main() {
int x,y;
scanf("%d %d %d",&n,&m,&t);
scanf("%d %d %d %d",&x,&y,&q,&p);
for(int i=1; i<=t; i++) {
scanf("%d %d",&l,&r);
a[l][r]=1; //这里是障碍点,可以理解为不能走,可以设置为已经走过的,赋值为1
}
for(int i=1; i<=n; i++) //墙壁 因为定义的数组大小是11*11的,如果不设置墙壁,就无法得到题目中说的n*m的矩形,所以要设置墙壁规定大小
a[i][m+1]=1;
for(int j=1; j<=m; j++)
a[n+1][j]=1;
fun(x,y);
printf("%d",sum);
return 0;
}
可以看到这个基础题的代码和上述模版十分相似,但是后续的难题就不能完全依靠模板了,那个只是起到一个辅助的思路,主要还是需要自己思考。
4.题目:填涂颜色
题目链接:https://www.luogu.com.cn/problem/P1162
输入:
6
0 0 0 0 0 0
0 0 1 1 1 1
0 1 1 0 0 1
1 1 0 0 0 1
1 0 0 0 0 1
1 1 1 1 1 1
输出:
0 0 0 0 0 0
0 0 1 1 1 1
0 1 1 2 2 1
1 1 2 2 2 1
1 2 2 2 2 1
1 1 1 1 1 1
没有充分利用dfs的模板,但是需要用到dfs的思路:
上代码:
#include <stdio.h>
int n,map[35][35],vis[35][35]; //大小n*n,图,是否访问过(0/1标记)
int dx[4]= {0,0,1,-1};
int dy[4]= {-1,1,0,0};
void dfs(int i,int j) {
if(map[i][j] || vis[i][j] || i<1 ||i>n || j<1 || j>n)return; //遇到由1组成墙壁会返回,遇到边界也会返回
vis[i][j]=1;
for(int z=0; z<=3; z++) //四个方向递归
dfs(i+dx[z],j+dy[z]);
}
int main() {
scanf("%d",&n);
for(int i=1; i<=n; i++) //贪心,从边缘DFS一定能把所有外面的0都访问过
for(int j=1; j<=n; j++)
scanf("%d",&map[i][j]);
for(int i=1; i<=n; i++) { //确保无漏网之鱼
dfs(1,i);
dfs(n,i); //列(最左边和最右边)
dfs(i,1); //行(最上边和最下边)
dfs(i,n);
}
for(int i=1; i<=n; i++) {
for(int j=1; j<=n; j++)
if(map[i][j])
printf("%d ",1);
else printf("%d ",vis[i][j]?0:2); //运算符(更快)
if(i!=n)
printf("\n");
}
return 0;
}
5.题目:八皇后 Checker Challenge
想必前面的题都小菜一碟,解决起来都游刃有余,本也一样,不要被他外表迷惑,万变不离其宗
题目链接:https://www.luogu.com.cn/problem/P1219
输入:
6
输出:
2 4 6 1 3 5
3 6 2 5 1 4
4 1 5 2 6 3
4
代码如下:
#include<stdio.h>
#include<math.h>
int sum=0,n;
int a[50],b[50];
int fun(int x,int y) {
for(int i=1; i<x; i++) { //判断是否位于对角线上,是则返回0,不是返回1(这个本人是用斜率来判断的)
int sj=abs(y-a[i]),sg=x-i;
if(sj==sg)
return 0;
}
return 1;
}
void fds(int m) {
if(m>n) {
if(sum<3) { //题目说只要输出前面三种即可
for(int i=1; i<m; i++)
printf("%d ",a[i]);
printf("\n");
}
sum++; //总解数
} else {
for(int i=1; i<=n; i++) { //熟悉的模板
a[m]=i;
if(b[i]==0) {
b[i]=1; //已经被使用(在下一个递归中,他已经被使用的)
if(fun(m,i))
fds(m+1);
b[i]=0; //循环中数据不变,重新变为为被使用(在该循环过程中,他可以继续被使用)
}
}
}
}
int main() {
scanf("%d",&n);
fds(1);
printf("%d",sum);
return 0;
}
6.题目:PERKET
既然你已经能够熟练掌握,那么下面这道题目也能轻松应对吧,这是一道让你更加自信的题目
题目链接:https://www.luogu.com.cn/problem/P2036
输入:
1
3 10
2
3 8
5 8
4
1 7
2 6
3 8
4 9
输出:
7
1
1
就不多解释了
#include<stdio.h>
#include<math.h>
struct ab {
int a,b;
} sum[11];
int min,n,s,t=1,k=0,num[11];
void fds(int m) {
for(int i=m; i<=n; i++) {
t*=sum[i].a,k+=sum[i].b;
if(min>abs(t-k))
min=abs(t-k);
// printf("%d %d\n",t,k);
fds(i+1);
t/=sum[i].a,k-=sum[i].b ;
}
}
int main() {
int i;
scanf("%d",&n);
for(i=1; i<=n; i++)
scanf("%d %d",&sum[i].a,&sum[i].b);
min=abs(sum[1].a-sum[1].b );
fds(1);
printf("%d",min);
return 0;
}
7.题目:单词接龙
链接:单词接龙
我个人觉得还是挺有难度的,有兴趣的可以尝试。因为难一点,可能不容易理解,我会写一下我的思路,更加方便你们理解。
输入:
5
at
touch
cheat
choose
tact
a
输出:
23
思路:首先,四个关键点
1.两个单词合并时,合并部分取的是最小重叠部分
2.相邻的两部分不能存在包含关系就是说如果存在包含关系,就不能标记为使用过。
3.每个单词最多出现两次
4.输入的最后一行为一个单个字符,表示“龙”开头的字母。你可以假定以此字母开头的“龙”一定存在。
我建议直接从第四行入手,先找到头,在找尾巴,因为这里就头和尾两部分,尾巴就需要考虑连接问题,所有我用mt函数,标记哪些单词可以首尾相接,y是尾巴,x是尾巴前面的一个单词,如果函数返回值是0,说明没有重和部分,如果是x的长度或者y的长度,则说明两者之间含有包含关系,也不行,还要注意单词使用次数,不能超过2次,然后在使用我们熟知的dfs即可,下面请看代码。
上代码:
#include<bits/stdc++.h> //基本上所有题目都是可以分割成一部分一部分来写的
using namespace std;
int n;
char tr[30][100]; //存储字符串
int ycl[30][30]; //两个字母的最小重叠部分
int vis[30]; //判断单词使用次数,超过2就不在使用
int mt(int x, int y) { //判断能不能链接,能的话返回x单词后连接一个y单词的最小重叠部分,这里x表示尾巴,y表示要连接的单词
bool sb=true; // 1 是不是显得很高级,当然,也可以用 int 然后1 0表示正确 错误
int ky=0;
for(int i=strlen(tr[x])-1; i>=0; i--) { //从x单词尾部向前看看最小重叠部分是从哪里开始的,以为因为是倒着来,所以保证是最小的
for(int j=i; j<strlen(tr[x]); j++) {
if(tr[x][j]!=tr[y][ky++]) {
sb=false; // 0
break;
}
}
if(sb) //如果说当前以k为开头的前一个单词后缀 ,是后面单词的前缀,就马上返回重叠部分。(strlen(x)-i是找出来的规律)
return strlen(tr[x])-i; //直接结束函数
ky=0;
sb=true; //不行就继续,然后变量初始化
}
return 0;
}
char ch; //记录开头字母
int sum=-1; //答案(英文忘了怎么打了)
int num=0; //每次搜到的当前最长串
void dfs(int p) { //p为尾部单词编号(p的后缀就是“龙”的后缀,因为p已经连接到”龙“后面了)
bool sb=false; // 0
for(int j=1; j<=n; j++) {
if(vis[j]>=2) continue; //使用了两次就跳过
if(ycl[p][j]==0) continue; //两单词之间没有重合部分就跳过
if(ycl[p][j]==strlen(tr[p]) || ycl[p][j]==strlen(tr[j])) continue;//两者存在包含关系就跳过
num+=strlen(tr[j])-ycl[p][j]; //两单词合并再减去最小重合部分
vis[j]++; //使用次数加1
sb=true; //标记一下当前已经成功匹配到一个可以连接的部分
dfs(j); //接上去
num-=strlen(tr[j])-ycl[p][j]; //回溯,就要再减回去那一部分长度
vis[j]--; //模板了,初始化变量
}
if(sb==false) { //jx==false说明不能再找到任何一个单词可以相连了
sum=max(sum,num); //更新ans,只要最大值
}
return;
}
int main() { //注意,这个才是主函数
cin>>n;
for(int i=1; i<=n; i++)
cin>>tr[i];
cin>>ch;
for(int i=1; i<=n; i++)
for(int j=1; j<=n; j++)
ycl[i][j]=mt(i,j); //提前处理,这里哪些单词可以接在哪些单词的后面
for(int i=1; i<=n; i++) { //从头到尾看一下有没有以指定开头字母为开头的单词
if(tr[i][0]==ch) { //如果有,就以当前单词为基准进行搜索。
vis[i]++; //使用次数加1
num=strlen(tr[i]); //更新当前串长度
dfs(i); //接上
vis[i]=0; //变量初始化
}
}
printf("%d",sum);
return 0;
}
小结:平凡不一定平庸