【DFS】学习

我看的b站上的这个视频,感觉讲得还不错~https://www.bilibili.com/video/BV1S24y1p7iH/?spm_id_from=333.880.my_history.page.click

递归实现指数型枚举

题目链接:92. 递归实现指数型枚举 - AcWing题库

从 1∼n这 n个整数中 随机选取任意多个,输出所有可能的选择方案。
输入格式
输入一个整数 n。
输出格式
每行输出一种方案。
同一行内的数必须升序排列,相邻两个数用恰好 1个空格隔开。
对于没有选任何数的方案,输出空行。
本题有自定义校验器(SPJ),各行(不同方案)之间的顺序任意。
数据范围
1≤n≤15
输入样例:
3
输出样例:
3
2
2 3
1
1 3
1 2
1 2 3

(本来尝试用画图工具的,但我还是太拉了,没有ipad好伤

//这道题要求任意选取多个
//也就是对每一个数都有选或不选两种情况
//要求输出要按照字典序,
//因此我们遍历每一个数,选或不选,并保存其状态
//遍历每一个数字,选了就将其输出

感觉写这种题之前还是要把思路搞清楚才能写清楚。

#include <iostream>
#include <algorithm>
using namespace std;
const int N =20;
int n;
int st[N];//记录状态 0表示还没考虑,1表示选,2表示不选 
void dfs(int x)
{
    if(x>n)//递归退出条件
    {
        for(int i=0;i<=n;i++)
        {
            if(st[i]==1)
            {
                cout<<i<<" ";    
            }    
        }    
        cout<<endl;
        return ;
    }    
    st[x]=2;//先选
    dfs(x+1);
    st[x]=0;
    
    st[x]=1; 
    dfs(x+1);
    st[x]=0;
}
int main()
{
    
    cin>>n; 
    dfs(1);
    return 0;
}

递归实现排列型枚举

题目链接:P1706 全排列问题 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

题目描述
按照字典序输出自然数 1 到 n 所有不重复的排列,即 n 的全排列,要求所产生的任一数字序列中不允许出现重复的数字。
输入格式
一个整数 n。
输出格式
由 1∼n 组成的所有不重复的数字序列,每行一个序列。
每个数字保留
5 个场宽。
输入输出样例
输入 #1复制
3
输出 #1复制
1 2 3
1 3 2
2 1 3
2 3 1
3 1 2
3 2 1

首先分析一下这道题叭

C++中全排列函数next_permutation 用法_Marcus-Bao的博客-CSDN博客库函数牛逼


1.依次枚举每个位置应该放哪个数

#include <bits/stdc++.h>
using namespace std;
const int N=15;
int n; 
bool st[N];
int arr[N];
//全排列问题,每个数字都要选,只是选的位置不同
//因此我们遍历每个位置,讨论选哪一个数
//同时记录在这次排列中,哪些数已经被选过了
//因为要输出所有全排列,因此我们用数组记录答案 
 
void dfs(int x)//对位置进行遍历 
{
    if(x>n)
    {
        for(int i=1;i<=n;i++)
        {
            printf("%5d",arr[i]);
        }
        cout<<endl;
    }
    for(int i=1;i<=n;i++)//对每一个数字进行讨论 
    {
        if(!st[i])//如果没有访问过当前数 
        {
            st[i]=true;
            arr[x]=i;//记录位置上填的数
            dfs(x+1);
            arr[x]=0;
            st[i]=false; 
        }    
    } 
}
signed main()
{
    scanf("%d",&n);
    dfs(1); 
    return 0;
}

STL牛逼

扫描二维码关注公众号,回复: 14729161 查看本文章
#include<iostream>
#include<stdio.h>
#include<algorithm>
using namespace std;
const int N=10;
int n;
int a[N];
int main()
{
    scanf("%d",&n);
    for(int i=1;i<=n;i++)
    {
        a[i]=i;
    }
    
    do{
        for(int i=1;i<=n;i++)
        {
            printf("%5d",a[i]);
        }
        printf("\n");
    }while(next_permutation(a+1,a+n+1));
    return 0;
}

递归实现组合型枚举

题目链接:P1157 组合的输出 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

这题说人话,就是从n个数中取出r个,就是数学中的组合,注意数字不要求顺序。

简单画个示意图:

#include <bits/stdc++.h>
using namespace std;
const int N=15;
int n,r; 
bool st[N];
int arr[N];
//从n个数中抽出r个数,不分顺序 
//也就是只选r个数,即遍历r个位置 
//对每一个位置遍历所有数,选或不选,选过就不再选,因此要记录状态
//最后要输出结果,用数组记录 
//***这是一个组合问题,观察输出答案可以知道后面的数都会比第一位大
//***这样做才能保证不出现重复的 
void dfs(int x,int start)//对位置进行遍历 
{
    if(x>r)
    {
        for(int i=1;i<=r;i++)
        {
            printf("%3d",arr[i]);
        }
        cout<<endl;
        return;
    } 
    for(int i=start;i<=n;i++)
    {
            arr[x]=i;
            dfs(x+1,i+1);
            arr[x]=0;
    }
}
signed main()
{
    cin>>n>>r; 
    dfs(1,1); 
    return 0;
}

简单分析一下这里为什么要传入两个参数

看运行结果明显错误的在后面出现了比他大的元素(如5,4,3),这就导致了组合出现多个。因此要额外传入一个start的限制,使其遍历后面的数字。(纯菜鸟,初学,求指点)


补充一下剪枝:

还是这个图,这里剪枝的原理是能选的数已经不够填充位置了,

小总结

对以上三种模型做个小总结:

指数型枚举时,每个数对应几种情况,就需要st数组记录是哪一种情况,每种情况都要在dfs中写出,最后遍历st数组输出需要的情况下的数。只用st

排列型枚举时,比如123的全排列,就需要对每个位置每个数进行遍历,要判断这个数是否被选择过,没有被选择过就需要更新状态同时保存答案,要用到st和arr

组合型枚举时,从n个数中取出r个,这种题是不要求顺序的,但最后输出要求按字典序;因此,我们还是遍历位置,对每一个位置上的数进行枚举,然后接着进行下一个位置,但注意枚举的数字会减少,因此加入start,同时arr保存答案。

选数

题目链接:P1036 [NOIP2002 普及组] 选数 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

这种不要求顺序的,求组合数都可以这样遍历位置,dfs传入两个参数记录位置和下一位遍历数

要求顺序的排列数问题,就需要加一个if语句判断是否使用过,同样的遍历位置进行递归

这题就是一个求组合数的变形题。。

#include <bits/stdc++.h>
using namespace std;
const int N=25;
int n,k; 
int arr[N];
int b[N];
long long sum;
int ans;
//从n个数中任选k个整数相加,组合数问题
bool isprime(int x)
{
    for(int i=2;i<=x/i;i++)
    {
        if(x%i==0) return false;
    }
    return true;
}
void dfs(int x,int start)//对位置进行遍历 
{
    if(x>k)
    {    sum=0;
        for(int i=1;i<=k;i++)
        {
            sum+=arr[i];
        }
        if(isprime(sum)) ans++;
        return ;
    }
    for(int i=start;i<=n;i++)
    {
        arr[x]=b[i];
        dfs(x+1,i+1);
        arr[x]=0;
    } 
}
signed main()
{
    cin>>n>>k; 
    for(int i=1;i<=n;i++)
    {
        scanf("%d",&b[i]);
    }
    dfs(1,1); 
    cout<<ans;
    return 0;
}

这里学到了一个小细节 ,判断素数的函数里,一般是写成i*i<=x,这里为了防止爆数据写成i<=x/i

for(int i=2;i<=x/i;i++)

了解一下剪枝叭

就是图上的这种情况,比如3,19这条线没有数可选了,但位置还没有填满这种分枝剪掉就可以啦;

也就是再dfs里加上一个判断,删去 已选择数+可选择数<k 的分支

对于选数这道题,代码实现一下看看:

    if((x-1)+n-start+1<k)//x代表当前位置,x-1求出已经选择数
    {
        return ;
    }
#include <iostream>
#include <algorithm>
using namespace std;
const int N =30;
int n,k;
int  arr[N];//存答案
int number[N];//存数组 
int cnt=0;
bool isprime(int x)
{
    for(int i=2;i<=x/i;i++)
    {
        if(x%i==0)
        {
            return false;
        }
    }
    return true;
}
void dfs(int x,int start)//遍历位置 ,start代表应该从这开始遍历数 
{
    if((x-1)+n-start+1<k)
    {
        return ;
    }
    if(x>k)//要选k个数 
    {
        int sum=0;
        for(int i=1;i<=k;i++)
        {
            sum+=arr[i];
        } 
        if(isprime(sum))    cnt++;
        return ;
    }
    for(int i=start;i<=n;i++)//遍历数字 
    {
        arr[x]=number[i];
        dfs(x+1,i+1);
        arr[x]=0; 
    }
}
int main()
{
    cin>>n>>k; 
    for(int i=1;i<=n;i++)
    {
        cin>>number[i];//输入数据 
    } 
    dfs(1,1);
    cout<<cnt<<endl; 
    return 0;
}

剪枝大概就是这样,比想象的要简单。同时,剪枝可以缩短很多时间哦~

烤鸡问题

题目链接:P2089 烤鸡 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

简单分析一下题目,10 种配料,每个配料1~3克,美味程度就在10~30这个区间

10个配料,每个调料有3种选择,选择数就有3的10次方个,配料的选择之间没有关系,也就是指数型枚举。

依次枚举每个调料放几克

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

int n;
int res=0;//存方案数
int arr[11];//存临时方案
int mem[10001][11]; 
void dfs(int x,int sum)
{
    if(sum>n) return;//剪枝 
    
    if(x>10)
    {
        if(sum==n)
        {
            res++;
            for(int i=1;i<=10;i++)
            {
                mem[res][i]=arr[i];
            }
        }
        return ;
    } 
    
    for(int i=1;i<=3;i++)
    {
        arr[x]=i;
        dfs(x+1,sum+i);
        arr[x]=0;
    }
}

int main()
{
    cin>>n;
    dfs(1,0);
    cout<<res<<endl;
    for(int i=1;i<=res;i++)
    {
        for(int j=1;j<=10;j++)
        {
            printf("%d ",mem[i][j]);
        }
        printf("\n");
    }
    return 0;
}

火星人

题目链接:P1088 [NOIP2004 普及组] 火星人 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

#include <iostream>
#include <cstdio>
using namespace std;
const int n=10010;
int N,M; //手指数目和要加上的小整数M<=100 
int mars[n];//手指排列顺序 
int arr[n];//记录方案 
bool st[n];//记录状态 
int res=0;
int cnt;
int flag=0; 
void dfs(int x)//列举位置 
{
    if(flag) return ;
    if(x>N)
    {
        res++;//完成了一次全排列
        if(res==M+1)
        {
            flag=1;//只要找到了答案下一次就不用递归了 
            for(int i=1;i<=N;i++)
            {
                cout<<arr[i]<<" ";
            }
        }
        return;
    }
    for(int i=1;i<=N;i++)//遍历数字 
    {
        if(res==0)//这道题唯一的变化就在这,一次全排列都没有完成的时候让i=火星人原始的手指
        {
            i=mars[x];//第一次从火星人刚开始的手,即火星人的手指排列arr数组第一次存的数据 
        }
        if(!st[i])
        {
            arr[x]=i;
            st[i]=true;
            dfs(x+1);//
            arr[x]=0;
            st[i]=false; 
        }
    }    
}
int main()
{
    cin>>N>>M;
    for(int i=1;i<=N;i++)
    {
        cin>>mars[i];
    }
    dfs(1);//直接从火星人目前手指开始枚举,枚举M次就可以了 
    return 0;
}

这题问了佬,知道了一种next_permutation库函数的方法,查了一下是c++的一个全排列函数

C++ STL全排列 next_permutation 用法 - 知乎 (zhihu.com)


火柴棒等式

题目链接:P1149 [NOIP2008 提高组] 火柴棒等式 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

#include<iostream>
#include<stdio.h>
using namespace std;
int count[10010]={6,2,5,5,4,5,6,3,7,6};
int n;
int cnt=0;
int arr[1000];

void dfs(int x,int sum)
{
    if(sum>n) return;
    if(x>3)
    {
        if(sum==n&&arr[1]+arr[2]==arr[3]) cnt++;
        return;
    } 
    for(int i=0;i<=1000;i++)//遍历数字 
    {
        arr[x]=i;
        dfs(x+1,sum+count[i]);
        arr[x]=0;    
    }
    
}
int main()
{
    cin>>n;
    n-=4;//减去加号和等号的火柴数 
    for(int i=10;i<=1000;i++)
    {
        count[i]=count[i%10]+count[i/10];
    } 
    dfs(1,0);
    cout<<cnt;
    return 0;
}

PERKET

题目链接:P2036 [COCI2008-2009#2] PERKET - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

补一个求绝对值:(15条消息) C\C++ 中的绝对值函数:abs()、cabs()、fabs()、labs()_c++绝对值函数_YogLn的博客-CSDN博客

#include<bits/stdc++.h>
using namespace std;
const int N=15;
int n;//种类数 
int s[N];
int b[N];
int res=1e9;
int st[N];
void dfs(int x)
{
    if(x>n)
    {
        bool tl=false;
        int sum1=1;
        int sum2=0;
        for(int i=1;i<=n;i++)
        {
            if(st[i]==1)
            {
                tl=true;
                sum1*=s[i];
                sum2+=b[i];
            }
        }
        if(tl) res=min(res,abs(sum1-sum2));
        return ;
    }
    
    st[x]=1;
    dfs(x+1);
    st[x]=0;
    
    st[x]=2;
    dfs(x+1);
    st[x]=0;
    
}
int main()
{
    cin>>n;
    for(int i=1;i<=n;i++)
    {
        scanf("%d%d",&s[i],&b[i]);
    }
    dfs(1);
    cout<<res<<endl;
    return 0;
}

奇怪的电梯

题目链接:P1135 奇怪的电梯 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

#include<bits/stdc++.h>
using namespace std;

const int N=210;
int n,a,b;//n总楼层,从a楼到b楼 
int k[N]; 

int res=1e9;
bool st[N];//存每层楼走没走过 

void dfs(int x,int cnt)//当前按了cnt次按钮 
{
    //剪枝 
    if(cnt>=res) return; 
    if(x<0||x>n) return ;
    if(x==b)
    {
        res=min(res,cnt);
        return;
    }
    st[x]=true; 
    if(x+k[x]<=n&&!st[x+k[x]])//上
    {
        st[x+k[x]]=true;
        dfs(x+k[x],cnt+1);
        st[x+k[x]]=false;
    }
    if(x>k[x]&&!st[x-k[x]])//下 
    {
        st[x-k[x]]=true;
        dfs(x-k[x],cnt+1);
        st[x-k[x]]=false;
    } 

}
int main()
{
    cin>>n>>a>>b;
    for(int i=1;i<=n;i++)
    {
        cin>>k[i];
    }
    dfs(a,0);
    if(res==1e9)
    {
        cout<<"-1"<<endl;
        return 0;
    } 
    cout<<res;
    return 0;
}

每层楼最多走一次的方案一定比每层楼走多次的方案要好

被卡掉了,暴力搜索过不了,应该是要用bfs。

P1683 入门

题目链接:P1683 入门 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

遇到的问题:

  1. 地图怎么存? 用字符类型的二维数组

  1. 怎么走?

  1. 怎么转弯?

  1. 怎么只走'.'

  1. 瓷砖计数问题?能重复走,但不能重复计数,定义一个bool类型数组记录状态就可以了。

#include<bits/stdc++.h>
using namespace std;

const int N=30;
int  n,m;//n行m列
char g[N][N];
int res=0; //记录走过的瓷砖数 
int dx[]={-1,0,1,0};
int dy[]={0,1,0,-1};
bool st[N][N]; 
void dfs(int x,int y)//当前按了cnt次按钮 
{
    
    for(int i=0;i<n;i++)
    {
        int a=x+dx[i]; int b=y+dy[i];
        if(a<0||a>=n||b<0||b>=m) continue;//越界,
        if(g[a][b]!='.') continue;//不能走 
        if(st[a][b]) continue;//如果这个点已经走过了,不走了
        //开始走a,b这个点 
        st[a][b]=true; 
        res++;
        dfs(a,b); 
    }
}
int main()
{
    cin>>m>>n;
    for(int i=0;i<n;i++)
    {
        scanf("%s",g[i]);
    } 
    for(int i=0;i<n;i++)
    {
        for(int j=0;j<m;j++)
        {
            if(g[i][j]=='@')
            {
                st[i][j]=true; 
                dfs(i,j);
            }
        }
    }
    res++;
    cout<<res;
    return 0;
}

不回溯的原因是,在每一次dfs里都会往四个方向走,也就是会再考虑一遍上一个起点。

如果回溯的话,下一次dfs里又会走一遍上次的点,又会重复计数。

FLOOD FILL 洪水填充模型 最大联通集问题

题目链接:P1596 [USACO10OCT]Lake Counting S - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

#include<bits/stdc++.h>
using namespace std;

const int N=110;
const int M=110;
char s[N][M];//存地形 
int n,m; 
int dx[]={-1,-1,0,1,1,1,0,-1};
int dy[]={0,1,1,1,0,-1,-1,-1};
int res;
bool st[N][M];
void dfs(int x,int y)
{
    for(int i=0;i<8;i++)
    {
        int a=x+dx[i]; int b=y+dy[i];
        if(a<0||a>=n||b<0||b>=m) continue;//要记得是否越界 
        if(s[a][b]=='W'&&!st[a][b])
        {
            st[a][b]=true;
            dfs(a,b);
            //st[x][y]=false; 不能回溯 
            //要注意这种问题,每次进行dfs都会考虑到8个方向,一定不要回溯!!
        }else continue;    
    }
}
int main()
{
    cin>>n>>m;
    for(int i=0;i<n;i++)
    {
        cin>>s[i];
    }
    for(int i=0;i<n;i++)
    {
        for(int j=0;j<m;j++)
        {
            if(s[i][j]=='W'&&!st[i][j])//注意W要大写,服了。。 
            {
                st[i][j]=true; //1
                dfs(i,j);
                res++;
            }
        }
    }
    cout<<res;
    
    return 0;
}

这题有1或无1这步都能AC,但从逻辑上讲,有1在dfs中就不会再访问这个点了,也就相当于剪枝。

如果没有1,那么在dfs中还会进行访问

洛谷的记录也证明了这个想法;

无1

有1

棋盘问题

题目链接:1114. 棋盘问题 - AcWing题库

这题的主要点在于,要求同一行同一列上最多只有一个棋子,也就是对每一行都遍历选列,画出递归树就可以发现跟排列型相似。

#include<iostream>
#include<queue>
#include<cstring>
using namespace std;
const int N=10; 
int n,k;//在n*n的矩阵里放k个棋子 
int dx[]={-1,0,1,0};
int dy[]={0,1,0,-1};
char g[N][N];
bool st[N];//记录每列放没放过
int res=0; //摆放方案数 
void dfs(int x,int cnt)//遍历行 
{
    if(cnt==k)//如果已经选了k个,方案数就++ 
    {
        res++;
        return ;
    } 
    if(x>=n) return ;//越界了 
    for(int i=0;i<n;i++)//遍历列 
    {
        if(!st[i]&&g[x][i]=='#')
        {
            st[i]=true;
            dfs(x+1,cnt+1);//当前行已经选择了i列,直接往下搜 
            st[i]=false;//回溯 
        } 
    } 
    dfs(x+1,cnt);//如果上面的cnt+1之后直接等于k了,循环直接退出,发生了错误
    //这里的作用是在最后一次cnt+1=cnt之后强行再次遍历下一行 
}
int main()
{
    while(cin>>n>>k,n>0&&k>0)//很多组数据 
    {
        for(int i=0;i<n;i++)
        {
            cin>>g[i];
        }
        res=0;//对每一组数据,res初始都为0 
        dfs(0,0);
        cout<<res<<endl; 
    }
    return 0;
} 

数的划分

题目链接:P1025 [NOIP2001 提高组] 数的划分 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

#include<iostream>
#include<queue>
#include<cstring>
using namespace std;
const int N=10;
int n,k; 
int arr[N];
int res=0;
void dfs(int x,int start,int sum)//从第x个位 
{
    if(sum>n) return;
    if(x>k)
    {
        if(sum==n)
        {
            res++;
        }
        return;
    }    
    for(int i=start;sum+i*(k-x+1)<=n;i++)//剪枝之后才能AC 
    {
        arr[x]=i;
        dfs(x+1,i,sum+i); 
        arr[x]=0;//恢复现场 
    }
}
int main()
{
    cin>>n>>k;
    dfs(1,1,0);
    cout<<res;
    
    return 0;
} 

单词接龙

题目链接:P1019 [NOIP2000 提高组] 单词接龙 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

猜你喜欢

转载自blog.csdn.net/m0_74183164/article/details/129491746
dfs