算法伴学笔记 Day 01 | DFS入门

⭐纵星河万里,亦不及你一垂眸。 --Samsara_soul

🙌题单(List-DFS 01)

由浅入深 循序渐进

📋A 洛谷-P2089 烤鸡

Label 指数型枚举 模板本板
Level 普及-

📋B 洛谷-P1088 [NOIP2004 普及组] 火星人

Label 排列型枚举 可行性剪枝
Level 普及-

📋C 洛谷-P1149 [NOIP2008 提高组] 火柴棒等式

Label 指数型枚举 可行性剪枝
Level 普及-

📋D 洛谷-P1219 [USACO1.5] 八皇后 Checker Challenge

Label 暴力搜索 模板
Level 普及/提高-

题型分类 参考博客 DFS(深度优先搜索)8种题型
剪枝策略 参考博客 深搜的剪枝技巧C++详解
友情链接 哔哩哔哩-DFS正确入门方式 | DFS + 递归与递推习题课(上) | 一节课教你爆搜!


---A 题 详 解---


🟢原题回顾🟢

📋A 洛谷-P2089 烤鸡

Label 指数型枚举
Level 普及-

题目背景

猪猪 Hanke 得到了一只鸡。

题目描述

猪猪 Hanke 特别喜欢吃烤鸡(本是同畜牲,相煎何太急!)Hanke 吃鸡很特别,为什么特别呢?因为他有 10 10 10 种配料(芥末、孜然等),每种配料可以放 1 1 1 3 3 3 克,任意烤鸡的美味程度为所有配料质量之和。
加入每种配料质量有三种选择,共加入十种配料,可能方案数 N = 3 10 = 59049 N=3^{10}=59049 N=310=59049
现在, Hanke 想要知道,如果给你一个美味程度 n n n ,请输出这 10 10 10 种配料的所有搭配方案。

输入格式

一个正整数 n n n,表示美味程度。

输出格式

第一行,方案总数。
题目要求先输出方案总数res,而方案总数需要在搜索结束时才可得到。因此,需要使用数组mem[][10]存储可行方案

第二行至结束, 10 10 10 个数,表示每种配料所放的质量,按字典序排列。
每种配料方案数为3,故而是指数型枚举
什么是字典序 ⭐参考 百度百科-字典序

如果没有符合要求的方法,就只要在第一行输出一个 0 0 0

样例 #1

样例输入 #1

11

样例输出 #1

10
1 1 1 1 1 1 1 1 1 2 
1 1 1 1 1 1 1 1 2 1 
1 1 1 1 1 1 1 2 1 1 
1 1 1 1 1 1 2 1 1 1 
1 1 1 1 1 2 1 1 1 1 
1 1 1 1 2 1 1 1 1 1 
1 1 1 2 1 1 1 1 1 1 
1 1 2 1 1 1 1 1 1 1 
1 2 1 1 1 1 1 1 1 1 
2 1 1 1 1 1 1 1 1 1

提示

对于 100 % 100\% 100% 的数据, n ≤ 5000 n \leq 5000 n5000

原题回顾 END

🟢AC代码及注解🟢

#include<iostream>
using namespace std;

const int N = 12; //略开大些亦可

int n; // 食材美味度 输入数据
int arr[N]; //存放当前方案
int res;  //记录方案数
int mem[10000][N]; // 用于记录答案

void dfs(int x,int sum) //x表示搜索到的层数 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 ;  //注意return位置 不在if语句内 
    }
    
    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 << "\n";
    for(int i=1;i<=res;i++)
    {
        for(int j=1;j<=10;j++)
        {
            cout << mem[i][j] <<" ";
        }
        cout << "\n";
    }
    return 0;
}

🟢洛谷评测🟢



---B 题 详 解---


🟢原题回顾🟢

📋B 洛谷-P1088 [NOIP2004 普及组] 火星人

Label 排列型枚举 可行性剪枝
Level 普及-

题目描述

人类终于登上了火星的土地并且见到了神秘的火星人。人类和火星人都无法理解对方的语言,但是我们的科学家发明了一种用数字交流的方法。这种交流方法是这样的,首先,火星人把一个非常大的数字告诉人类科学家,科学家破解这个数字的含义后,再把一个很小的数字加到这个大数上面,把结果告诉火星人,作为人类的回答。
步骤 ——1.破解含义 2.加上一个较小数 3.翻译结果
火星人用一种非常简单的方式来表示数字――掰手指。火星人只有一只手,但这只手上有成千上万的手指,这些手指排成一列,分别编号为 1 , 2 , 3 , ⋯ 1,2,3,\cdots 1,2,3,。火星人的任意两根手指都能随意交换位置,他们就是通过这方法计数的。

一个火星人用一个人类的手演示了如何用手指计数。如果把五根手指――拇指、食指、中指、无名指和小指分别编号为 1 , 2 , 3 , 4 1,2,3,4 1,2,3,4 5 5 5,当它们按正常顺序排列时,形成了 5 5 5 位数 12345 12345 12345,当你交换无名指和小指的位置时,会形成 5 5 5 位数 12354 12354 12354,当你把五个手指的顺序完全颠倒时,会形成 54321 54321 54321,在所有能够形成的 120 120 120 5 5 5 位数中, 12345 12345 12345 最小,它表示 1 1 1 12354 12354 12354 第二小,它表示 2 2 2 54321 54321 54321 最大,它表示 120 120 120。下表展示了只有 3 3 3 根手指时能够形成的 6 6 6 3 3 3 位数和它们代表的数字:

三进制数代表的数字
123 123 123 1 1 1
132 132 132 2 2 2
213 213 213 3 3 3
231 231 231 4 4 4
312 312 312 5 5 5
321 321 321 6 6 6

显而易见的是 火星人的每根手指都是唯一的,不可能在两个位置同时出现。手指序列即1~n的全排列【排列型枚举】
现在你有幸成为了第一个和火星人交流的地球人。一个火星人会让你看他的手指,科学家会告诉你要加上去的很小的数。你的任务是,把火星人用手指表示的数与科学家告诉你的数相加,并根据相加的结果改变火星人手指的排列顺序。输入数据保证这个结果不会超出火星人手指能表示的范围。

输入格式

共三行。
第一行一个正整数 N N N,表示火星人手指的数目( 1 ≤ N ≤ 10000 1 \le N \le 10000 1N10000)。
第二行是一个正整数 M M M,表示要加上去的小整数( 1 ≤ M ≤ 100 1 \le M \le 100 1M100)。
下一行是 1 1 1 N N N N N N 个整数的一个排列,用空格隔开,表示火星人手指的排列顺序。

输出格式

N N N 个整数,表示改变后的火星人手指的排列顺序。每两个相邻的数中间用一个空格分开,不能有多余的空格。

样例 #1

样例输入 #1

5
3
1 2 3 4 5

样例输出 #1

1 2 4 5 3

提示

对于 30 % 30\% 30% 的数据, N ≤ 15 N \le 15 N15

对于 60 % 60\% 60% 的数据, N ≤ 50 N \le 50 N50

对于 100 % 100\% 100% 的数据, N ≤ 10000 N \le 10000 N10000

原题回顾 END

🟢详解🟢

不剪枝代码(WA示例)

#include <iostream>
using namespace std;

const int N = 10010;  //外星人手指数不超过10000

int n; // 外星人手指数 输入数据
int m; // 要加上的小数 输入数据

int mars[N]; // 记录外星人初始排列 输入数据
int arr[N];  // 记录当前外星人手指序
bool st[N];  // 记录是否被选(手指是唯一的 一根手指不能同时出现在两个地方)
int res;  //记录递增次数
//bool return0 = false;  //停止标志

void dfs(int x)
{
    //if (return0)
      //  return;

    if (x > n){  //遍历到尾部
        res++;
        if (res == m + 1){   //目标答案在此
            //return0 = true;
            for (int i = 1; i <= n; i++){
                cout << arr[i] << " ";  //输出答案
            }
        }
        return;
    }

    for (int i = 1; i <= n; i++){
        if (!res){  //最重要的一步 需要基于外星人所给数字开始遍历
            i = mars[x];
        }
        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++){
        scanf("%d", &mars[i]);
    }
    dfs(1);
    return 0;
}

评测结果

在这里插入图片描述

过程分析

此代码运行过程中到达指定解处可以输出正确答案,但是搜索继续向后进行,时间开销过大。
此时不难看出题目中较小数之深意

优化

增加bool return0 = false; //停止标志
dfs函数修改

void dfs(int x)
{
    if (return0)
        return;

    if (x > n){  //遍历到尾部
        res++;
        if (res == m + 1){   //目标答案在此
            return0 = true;
            for (int i = 1; i <= n; i++){
                cout << arr[i] << " ";  //输出答案
            }
        }
        return;
    }

    for (int i = 1; i <= n; i++){
        if (!res){  //最重要的一步 需要基于外星人所给数字开始遍历
            i = mars[x];
        }
        if (!st[i]){
            arr[x] = i;
            st[i] = true;
            dfs(x + 1);
            arr[x] = 0;
            st[i] = false;
        }
    }
}

至此 dfs搜到正确答案后即可华丽退出🙌

洛谷评测

在这里插入图片描述


---C 题 详 解---


🟢原题回顾🟢

📋C 洛谷-P1149 [NOIP2008 提高组] 火柴棒等式

Label 排列型枚举 可行性剪枝
Level 普及-

题目描述

给你 n n n 根火柴棍,你可以拼出多少个形如 A + B = C A+B=C A+B=C 的等式?等式中的 A A A B B B C C C 是用火柴棍拼出的整数(若该数非零,则最高位不能是 0 0 0)。用火柴棍拼数字 0 ∼ 9 0\sim9 09 的拼法如图所示:

注意:

  1. 加号与等号各自需要两根火柴棍;
  2. 如果 A ≠ B A\neq B A=B,则 A + B = C A+B=C A+B=C B + A = C B+A=C B+A=C 视为不同的等式( A , B , C ≥ 0 A,B,C\geq0 A,B,C0);
  3. n n n 根火柴棍必须全部用上。
    所有等式都含有+ 与 -其共耗费四根火柴棍,此项火柴开销固定

输入格式

一个整数 n ( 1 ≤ n ≤ 24 ) n(1 \leq n\leq 24) n(1n24)

输出格式

一个整数,能拼成的不同等式的数目。

样例 #1

样例输入 #1

14

样例输出 #1

2

样例 #2

样例输入 #2

18

样例输出 #2

9

提示

【输入输出样例 1 解释】

2 2 2 个等式为 0 + 1 = 1 0+1=1 0+1=1 1 + 0 = 1 1+0=1 1+0=1

【输入输出样例 2 解释】

9 9 9 个等式为

0 + 4 = 4 0+4=4 0+4=4 0 + 11 = 11 0+11=11 0+11=11 1 + 10 = 11 1+10=11 1+10=11 2 + 2 = 4 2+2=4 2+2=4 2 + 7 = 9 2+7=9 2+7=9 4 + 0 = 4 4+0=4 4+0=4 7 + 2 = 9 7+2=9 7+2=9 10 + 1 = 11 10+1=11 10+1=11 11 + 0 = 11 11+0=11 11+0=11

原题回顾 END

🟢详解🟢

不够完美的AC代码

#include<iostream>
using namespace std;

const int N = 1000; 

int n;
int fire[10]={6,2,5,5,4,5,6,3,7,6};  //一一对应的火柴数
int arr[4];  //存储等式中的三个数字A B C
int res;  //方案数

int cal(int num)  //数组中没有的,使用已有数据计算
{
    int add_sum=0;
    if(num<10) return fire[num];
    else
    {
        while(num){
            add_sum+=fire[(num%10)];
            num/=10;
        }
    return add_sum;
    }
}
void dfs(int x,int sum)  //A+B=C中  x=1,2,3 分别对应A,B,C sum是使用火柴数
{
    //剪枝
    if(sum>n) return ;

    if(x>3){
        if(sum==n && arr[1]+arr[2]==arr[3]){  //同时满足题中两个条件1、给定火柴数 2、等式成立
            res++;
        }
        return ;
    }

    for(int i=0;i<=N;i++){ //普普通通搜一搜
        arr[x]=i;
        dfs(x+1,sum+cal(i)); //此处调用cal函数
        arr[x]=0;
    }
}

int main(){
    cin >> n;
    n=n-4;
    dfs(1,0);
    printf("%d",res);
    return 0;
}

洛谷评测

在这里插入图片描述

优化

注意到10以上的fire可能会大量的重复用到,故可把cal递归方式改为递推,并且使用数组存储

#include<iostream>
using namespace std;

const int N = 1000; 

int n;
int fire[N]={6,2,5,5,4,5,6,3,7,6};  //一一对应的火柴数
int arr[4];  //存储等式中的三个数字A B C
int res;  //方案数


void dfs(int x,int sum)  //A+B=C中  x=1,2,3 分别对应A,B,C sum是使用火柴数
{
    //剪枝
    if(sum>n) return ;

    if(x>3){
        if(sum==n && arr[1]+arr[2]==arr[3]){  //同时满足题中两个条件1、给定火柴数 2、等式成立
            res++;
        }
        return ;
    }

    for(int i=0;i<N;i++){ //普普通通搜一搜
        arr[x]=i;
        dfs(x+1,sum+fire[i]); //此处直接使用数组数据
        arr[x]=0;
    }
}

int main(){
    cin >> n;
    n=n-4;
    for(int i=10;i<N;i++){
        fire[i]=fire[i/10]+fire[i%10];
    }
    dfs(1,0);
    printf("%d",res);
    return 0;
}

洛谷评测

对于测试样例 优化后代码效率提高了一倍左右
在这里插入图片描述
若还想优化,可以试着卡 N N N的范围,不过有一定的风险,可能会漏可行解。(实际上N可以卡到600)
此题完结😊


---D 题 详 解---


🟢原题回顾🟢

📋D 洛谷-P1219 [USACO1.5] 八皇后 Checker Challenge

Label 暴力搜索
Level 普及/提高-

[USACO1.5] 八皇后 Checker Challenge

题目描述

一个如下的 6 × 6 6 \times 6 6×6 的跳棋棋盘,有六个棋子被放置在棋盘上,使得每行、每列有且只有一个,每条对角线(包括两条主对角线的所有平行线)上至多有一个棋子。

上面的布局可以用序列 2   4   6   1   3   5 2\ 4\ 6\ 1\ 3\ 5 2 4 6 1 3 5 来描述,第 i i i 个数字表示在第 i i i 行的相应位置有一个棋子,如下:

行号 1   2   3   4   5   6 1\ 2\ 3\ 4\ 5\ 6 1 2 3 4 5 6

列号 2   4   6   1   3   5 2\ 4\ 6\ 1\ 3\ 5 2 4 6 1 3 5

这只是棋子放置的一个解。请编一个程序找出所有棋子放置的解。
并把它们以上面的序列方法输出,解按字典顺序排列。
请输出前 3 3 3 个解。最后一行是解的总个数。

输入格式

一行一个正整数 n n n,表示棋盘是 n × n n \times n n×n 大小的。

输出格式

前三行为前三个解,每个解的两个数字之间用一个空格隔开。第四行只有一个数字,表示解的总数。

样例 #1

样例输入 #1

6

样例输出 #1

2 4 6 1 3 5
3 6 2 5 1 4
4 1 5 2 6 3
4

提示

【数据范围】
对于 100 % 100\% 100% 的数据, 6 ≤ n ≤ 13 6 \le n \le 13 6n13

原题回顾 END

🟢AC代码🟢

还是比较简单的,就直接放代码了😊

#include<iostream>
using namespace std;

const int N = 15;

int n;  //矩阵规模
int arr[N];  //记录摆放位置
bool st[N],lst[2*N-1],rst[2*N-1];  //列 左与右
int res;

void dfs(int x)
{
    if(x>n){
        res++;
        if(res<4){
            for(int i=1;i<=n;i++){
                cout << arr[i] << ' ';
            }
            cout << '\n';
        }
        return ;
    }

    for(int i=1;i<=n;i++){
        if(!st[i]&&!lst[i+x]&&!rst[i-x]){
            st[i]=true;
            lst[i+x]=true;
            rst[i-x]=true;
            arr[x]=i;
            dfs(x+1);
            st[i]=false;
            lst[i+x]=false;
            rst[i-x]=false;
            arr[x]=0;
        }
    }   
}
int main()
{
    cin >> n;
    dfs(1);
    cout << res;
    return 0;
}

在这里插入图片描述
或许还能优化下🤞

小声哔哔

刚接触八皇后问题的同学可以参考下知名vup@木子喵neko 的视频教程喵~
哔哩哔哩 -【neko算法课】DFS与BFS【5期】

🟪感谢阅读喵 关注作者谢谢喵~💕

  • 58
    点赞
  • 51
    收藏
    觉得还不错? 一键收藏
  • 6
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值