手撕算法——深度优先搜索—DFS

什么是搜索?
        搜索,是⼀种枚举,通过穷举所有的情况来找到最优解,或者统计合法解的个数。因此,搜索有时候也叫作暴搜。
搜索⼀般分为深度优先搜索(DFS)宽度优先搜索(BFS)

一、递归型枚举与回溯剪枝初识

回溯与剪枝

  • 回溯:当在搜索的过程中,遇到⾛不通或者⾛到底的情况时,就回头。
  • 剪枝:剪掉在搜索过程中,剪掉重复出现或者不是最优解的分⽀。

方法: 

  1. 画决策树;
  2. 根据决策树写递归。

1. 枚举子集

题⽬来源:洛⾕

题⽬链接:B3622 枚举子集(递归实现指数型枚举) - 洛谷

难度系数:★

(1)题目描述

(2)算法原理

设⼀共有 个数,分别是 1 ≤n≤10 [1, 2, 3] 。 i Y 则表⽰第 名同学参加合唱;为 N 则表⽰ 。「从前往后」考虑每⼀个数,针对当前这个数「选」或者「不 选」,我们可以画出如下「决策树」: 

 

设计递归函数:

  • 重复⼦问题:针对某⼀位,「选」或者「不选」这个数。因为最终结果要按照「字典序」输出,我们可以「先考虑不选」,然后「再考虑选」;
  • 实现⽅式参考代码和注释,结合「决策树」⼀起看会很清晰。

(3)参考代码

#include <iostream>

using namespace std;

int n;
string path; // 记录递归过程中,每一步的决策

void dfs(int pos)
{
    if(pos > n)
    {
        // path 就存着前 n 个人的决策
        cout << path << endl;
        return;
    }

    // 不选
    path += 'N';
    dfs(pos + 1);
    path.pop_back(); // 回溯,清空现场

    // 选
    path += 'Y';
    dfs(pos + 1);
    path.pop_back(); // 清空现场
}

int main()
{
    cin >> n;

    dfs(1);

    return 0;
}

2.  组合型枚举

题⽬来源:洛⾕

题⽬链接:P10448 组合型枚举 - 洛谷

难度系数:★

(1)题目描述 

(2)算法原理

设 n =4,m=3 3 ,「从前往后」考虑 个位置应该选哪个数,我们可以画出如下决策树:

设计递归函数:

  • 重复⼦问题:当前这⼀位,应该放哪个数上去。因为这是⼀个「组合」问题,不涉及排列,所以我 们当前位置开始放的数,应该是「上次决策的数的下⼀位」。
  • 实现⽅式参考代码和注释,结合「决策树」⼀起看会很清晰。

(3)参考代码

#include <iostream>
#include <vector>

using namespace std;

int n, m;
vector<int> path;
// path.size();

void dfs(int begin)
{
    if(path.size() == m)
    {
        for(auto x : path) cout << x << " ";
        cout << endl;
        return;
    }

    for(int i = begin; i <= n; i++)
    {
        path.push_back(i);
        dfs(i + 1);
        path.pop_back(); // 清空现场
    }   
}

int main()
{
    cin >> n >> m;

    dfs(1);

    return 0;
}

3. 枚举排列

题⽬来源:洛⾕

题⽬链接:B3623 枚举排列(递归实现排列型枚举) - 洛谷

难度系数:★

(1)题目描述

(2)算法原理

设 n =3,k =2 ,⼀共要选出两个数,可以依次「考虑要选出来的数」是谁,画出如下决策树:

设计递归函数: 

  • 重复⼦问题:考虑这⼀位要放上什么数。因为是「排列」问题,所以我们直接从 开始枚举要放 的数。
  • 剪枝:在这⼀条路径中,我们「不能选择之前已经选择过的数」。
  • 实现⽅式参考代码和注释,结合「决策树」⼀起看会很清晰。

(3)参考代码

#include <iostream>
#include <vector>

using namespace std;

const int N = 15;

int n, k;
vector<int> path;
bool st[N]; // 标记一下哪些数已经选过了

void dfs()
{
    if(path.size() == k)
    {
        for(auto x : path) cout << x << " ";
        cout << endl;
        return;
    }

    for(int i = 1; i <= n; i++)
    {
        if(st[i]) continue;
        path.push_back(i);
        st[i] = true;
        dfs();
        // 恢复现场
        path.pop_back();
        st[i] = false;
    }
}

int main()
{
    cin >> n >> k;

    dfs();

    return 0;
}

4. 全排列问题

题⽬来源:洛⾕

题⽬链接:P1706 全排列问题 - 洛谷

难度系数:★

(1)题目描述

(2)算法原理

跟上⼀道题的决策⼀样,我们可以枚举每⼀位应该放上什么数,只不过少了 的限制。剪枝的策略还 是⼀样的,那就是在路径中,「不能选择之前已经选过的数」。

(3)参考代码

#include <iostream>
#include <vector>

using namespace std;

const int N = 15;

int n;
bool st[N];
vector<int> path;

void dfs()
{
    if(path.size() == n)
    {
        for(auto x : path)
        {
            printf("%5d", x);
        }
        cout << endl;
        return; 
    }

    for(int i = 1; i <= n; i++)
    {
        if(st[i]) continue;

        path.push_back(i);
        st[i] = true;
        dfs();
        // 恢复现场
        path.pop_back();
        st[i] = false;
    }
}

int main()
{
    cin >> n;

    dfs();

    return 0;
}

二、DFS

1. 选数

题⽬来源:洛⾕

题⽬链接:P1036 [NOIP 2002 普及组] 选数 - 洛谷

难度系数:★

(1)题目描述 

(2)算法原理

「组合」型枚举,路径⾥⾯记录选择数的「总和」。在选出个数之后,判断「是否是质数」。

(3)参考代码

#include <iostream>

using namespace std;

const int N = 25;

int n, k;
int a[N];

int ret;
int path; // 记录路径中所选择的数的和

bool isprime(int x)
{
    if(x <= 1) return false;
    // 试除法
    for(int i = 2; i <= x / i; i++)
    {
        if(x % i == 0) return false;
    }
    return true;
}

void dfs(int pos, int begin)
{
    if(pos > k)
    {
        if(isprime(path)) ret++;
        return;
    }

    for(int i = begin; i <= n; i++)
    {
        path += a[i];
        dfs(pos + 1, i + 1);
        path -= a[i]; // 恢复现场
    }
}

int main()
{
    cin >> n >> k;
    for(int i = 1; i <= n; i++) cin >> a[i];

    dfs(1, 1);

    cout << ret << endl;

    return 0;
}

2. ⻜机降落

题⽬来源:洛⾕

题⽬链接:P9241 [蓝桥杯 2023 省 B] 飞机降落 - 洛谷

难度系数:★★

(1)题目描述 

(2)算法原理

枚举所有⻜机的「全排列」,判断是否存在⼀种排列,使的全部的⻜机都能安全降落。

剪枝: 

  • 当前路径⾥⾯只能选没有选过的⻜机;
  • 如果这架⻜机不能正常降落,剪掉;
  • 如果已经找到⼀种安全降落的⽅式,停⽌枚举,可以通过「递归的返回值」判断是否搜索成功。

(3)参考代码

#include <iostream>
#include <cstring>

using namespace std;

const int N = 15;

int n;
int t[N], d[N], l[N];

bool st[N]; // 标记路径中哪些飞机已经摆放过

bool dfs(int pos, int end)
{
    if(pos > n)
    {
        return true;
    }

    for(int i = 1; i <= n; i++)
    {
        if(st[i] == true) continue; // 剪枝
        if(end > t[i] + d[i]) continue; // 剪枝
        int newend = max(t[i], end) + l[i];
        st[i] = true;
        if(dfs(pos + 1, newend)) return true;
        st[i] = false; // 回复现场
    }

    return false;
}

int main()
{
    int T; cin >> T;

    while(T--) // 多组测试数据的时候,一定要注意清空数据
    {
        memset(st, 0, sizeof st);
        cin >> n;
        for(int i = 1; i <= n; i++) cin >> t[i] >> d[i] >> l[i];

        if(dfs(1, 0)) cout << "YES" << endl;
        else cout << "NO" << endl;
    }


    return 0;
}

3. ⼋皇后

题⽬来源:洛⾕

题⽬链接:P1219 [USACO1.5] 八皇后 Checker Challenge - 洛谷

难度系数:★★

(1)题目描述

(2)算法原理

枚举策略:

  • 「⼀⾏⼀⾏」的放皇后:从第⼀⾏开始,尝试在每⼀列上放皇后;
  • 如果当前列放上皇后之后「没有冲突」,就标记⼀下这个「放法」,记录⼀下当前⾏的决策,然后 「递归」考虑下⼀⾏;
  • 等到「所有⾏」都放置完毕之后,输出本次枚举的决策。 

枚举策略应该是⽐较容易想到的,这道题的难点在于如何判断「在这⼀列放上这个皇后之后,是否冲突」。当我们⼀⾏⼀⾏放的时候,「⾏是不会产⽣冲突的」。产⽣冲突的只有「列」,「主对⻆
线」,以及「副对⻆线」。我们可以⽤三个数组分别标记: 

  • col[i] = true 表⽰第 i ⾏放置了⼀个
  • dig1[j i + n] = true 表⽰ y = x + (j i) 条「主对⻆线上放置了⼀个
  • dig2[j + i] = true 表⽰ y = −x + (j + i) 条「副对⻆线上放置了⼀个

(3)参考代码

#include <iostream>
#include <vector>

using namespace std;

const int N = 15;

int n;
bool col[N], st1[N * 2], st2[N * 2];

int ret;
vector<int> path;

void dfs(int x)
{
    if(x > n)
    {
        ret++;
        if(ret <= 3)
        {
            for(auto x : path) cout << x << " ";
            cout << endl;
        }
        return;
    }

    for(int y = 1; y <= n; y++)
    {
        // 判断能不能摆在这一列
        if(col[y] || st1[y - x + n] || st2[y + x]) continue; // 剪枝
        col[y] = st1[y - x + n] = st2[y + x] = true;
        path.push_back(y);
        dfs(x + 1);
        col[y] = st1[y - x + n] = st2[y + x] = false;
        path.pop_back();
    }
}

int main()
{
    cin >> n;

    dfs(1);

    cout << ret << endl;

    return 0;
}

4. 数独

题⽬来源:洛⾕

题⽬链接:P1784 数独 - 洛谷

难度系数:★★

(1)题目描述

(2)算法原理

举策略

  • ⼀个⼦⼀个」往⾥⾯
  • 从第⼀⾏第⼀个,填上⼀个突」的,然归」到下⼀个
  • 当某⼀⾏填满之后下⼀⾏起始继续

三个数组帮助判断⼀个数之后是否发⽣冲突。对于3 × 3⽅格,以给每⼀个格⼦编上号,快读位,如下图:

  • row[i][num] = true 表⽰⾏已经放上了列已经放上了 num 这个数
  • col[j][num] = true 表⽰j 列已经放上了列已经放上了 num 这个数
  • st[/ 3][/ 3][num] = true 表⽰[/ 3, / 3]  3 × 3 已经放上了 num 这个数

(3)参考代码

#include <iostream>

using namespace std;

const int N = 10;

int n = 9;
int a[N][N];
bool row[N][N], col[N][N], st[N][N][N];

bool dfs(int i, int j)
{
    if(j == n) 
    {
        // 当这一行填满之后
        i++;
        j = 0;
    }

    if(i == n) return true; // 找到一种合法的情况,就停止递归
    if(a[i][j]) return dfs(i, j + 1);

    for(int x = 1; x <= 9; x++)
    {
        if(row[i][x] || col[j][x] || st[i / 3][j / 3][x]) continue; // 剪枝

        row[i][x] = col[j][x] = st[i / 3][j / 3][x] = true;
        a[i][j] = x;

        if(dfs(i, j + 1)) return true;

        // 恢复现场
        row[i][x] = col[j][x] = st[i / 3][j / 3][x] = false;
        a[i][j] = 0;   
    }

    return false;
}

int main()
{
    for(int i = 0; i < n; i++)
    {
        for(int j = 0; j < n; j++)
        {
            cin >> a[i][j];
            int x = a[i][j];
            if(x)
            {
                // 标记一下
                row[i][x] = col[j][x] = st[i / 3][j / 3][x] = true;
            }
        }
    }

    dfs(0, 0);

    for(int i = 0; i < n; i++)
    {
        for(int j = 0; j < n; j++)
        {
            cout << a[i][j] << " ";
        }
        cout << endl;
    }

    return 0;
}

三、剪枝与优化

剪枝,形象得看,就是剪掉搜索树的分⽀,从⽽减⼩搜索树的规模,排除掉搜索树中没有必要的分 ⽀,优化时间复杂度。

在深度优先遍历中,有⼏种常⻅的剪枝⽅法:

  • 1. 排除等效冗余

        如果在搜索过程中,通过某⼀个节点往下的若⼲分⽀中,存在最终结果等效的分⽀,那么就只需要搜 索其中⼀条分⽀。

  • 2. 可⾏性剪枝

如果在搜索过程中,发现有⼀条分⽀是⽆论如何都拿不到最终解,此时就可以放弃这个分⽀,转⽽搜 索其它的分⽀。

  • 3. 最优性剪枝

在最优化的问题中,如果在搜索过程中,发现某⼀个分⽀已经超过当前已经搜索过的最优解,那么这 个分⽀往后的搜索,必定不会拿到最优解。此时应该停⽌搜索,转⽽搜索其它情况。

  • 4. 优化搜索顺序

在有些搜索问题中,搜索顺序是不影响最终结果的,此时搜索顺序的不同会影响搜索树的规模。 因此,应当先选择⼀个搜索分⽀规模较⼩的搜索顺序,快速拿到⼀个最优解之后,⽤最优性剪枝剪掉 别的分⽀。

  • 5. 记忆化搜索

记录每⼀个状态的搜索结果,当下⼀次搜索到这个状态时,直接找到之前记录过的搜索结果。 记忆化搜索,有时也叫动态规划。

1. 数的划分

题⽬来源:洛⾕

题⽬链接:P1025 [NOIP 2001 提高组] 数的划分 - 洛谷

难度系数:★★

(1)题目描述

(2)算法原理

搜索策略

  • [1, n] 个数放 k ⾥⾯,使的坑⾥⾯的总和是  n 
  • 其中不同的坑⾥⾯数可相同
  •  [1, 2]  [2, 1] 是同⼀法,因是⼀组合型枚。针对每⼀个⾥⾯数应谁的时候,从上⼀个⾥⾯

策略

  • 们填 cnt 此时和是 sum  后续坑位部都填值都会超过 n 。说我们之前填的数太⼤了导致后⾯填都会超过 n 掉。

注意枝位不同⽽导致搜索树的不同

  • 果在进⼊递之前剪枝,进⼊⾮法的函数中
  • 是如果在进⼊递之后剪枝,多进⼊多不合法的函数中

(3)参考代码

#include <iostream>

using namespace std;

int n, k;
int path, ret;

void dfs(int pos, int begin)
{
    if(pos == k)
    {
        if(path == n) ret++;
        return;
    }

    // 可行性剪枝
    // if(path + begin * (k - pos) > n) return;

    for(int i = begin; i <= n; i++)
    {
        // 可行性剪枝
        if(path + i * (k - pos) > n) return;
        
        path += i;
        dfs(pos + 1, i);
        path -= i;
    }
}

int main()
{
    cin >> n >> k;

    dfs(0, 1);

    cout << ret << endl;

    return 0;
}

2. ⼩猫爬⼭

题⽬来源:洛⾕

题⽬链接:P10483 小猫爬山 - 洛谷

难度系数:★★

(1)题目描述

(2)算法原理

搜索策略:依次处每⼀只猫,对于每⼀只猫,们都有式:

  • 么把这只已经缆⻋上
  • 么重新⼀个缆⻋把这只放上去

枝:

  • 在搜索⽤全局变量记录已经搜索来的最⼩缆⻋数量果当搜索已经⽤缆⻋数量⼤于全局记录的最⼩缆⻋数量,那么这个分⽀⼀定不会得最优掉。
  • 优化枚举顺序⼀从⼤到⼩安每⼀只
    • 重量较⼤的猫能速把缆⻋填满,快得到⼀个值;
    • 通过这个值,能前把分⽀较⼤的情前剪掉。
  • 优化枚举策略⼆先考把⼩有的缆⻋上,然后考重新⼀辆⻋

为如反着来,们会先把缆⻋较⼤的情举出来,就起不到剪枝的

(3)参考代码

#include <iostream>
#include <algorithm>

using namespace std;

const int N = 20;

int n, w;
int c[N]; // 小猫的信息

int cnt; // 当前用了多少车
int s[N]; // 每一辆车目前的总重

int ret = N; // 最优解

bool cmp(int a, int b)
{
    return a > b;
}

void dfs(int pos)
{
    // 策略二:最优性剪枝
    if(cnt >= ret) return;

    if(pos > n)
    {
        ret = cnt;
        return;
    }

    // 策略三:优化搜索顺序
    // 先安排在已有的车辆上
    for(int i = 1; i <= cnt; i++)
    {
        // 策略一:可行性剪枝
        if(s[i] + c[pos] > w) continue;
        s[i] += c[pos];
        dfs(pos + 1);
        s[i] -= c[pos]; // 恢复现场
    }

    // 重开一辆车
    cnt++;
    s[cnt] = c[pos];
    dfs(pos + 1);
    // 恢复现场
    s[cnt] = 0;
    cnt--;
}

int main()
{
    cin >> n >> w;
    for(int i = 1; i <= n; i++) cin >> c[i];

    // 策略三:优化搜索顺序
    sort(c + 1, c + 1 + n, cmp);
    
    dfs(1);

    cout << ret << endl;

    return 0;
}

四、记忆化搜索

记忆搜索也是⼀策略

通过⼀个"备忘录"记录第⼀次搜索到的结果,当下⼀次搜索到这个状态时,直接在"备忘录"⾥⾯找结果。

记忆搜索,有时也叫动规划

1. 斐波那契数

题⽬来源:⼒扣

题⽬链接:509. 斐波那契数 - 力扣(LeetCode)

难度系数:★

(1)题目描述

(2)算法原理

在搜索的特别多完全相同⼦问题就可以添加⼀个备忘录,搜索的忘录下⼀次在搜索到这个状接在忘录⾥⾯拿值。

(3)参考代码

class Solution 
{
    int f[35]; // 搞⼀个备忘录 
public:
    int fib(int n) 
{
    memset(f, -1, sizeof f); // 先初始化成⼀定不会存在的值
 
    return dfs(n);    
}
 int dfs(int n)
 {
     // 搜索之前先往备忘录⾥⾯瞅瞅
 
    if(f[n] != -1) return f[n];
    if(n == 0 || n == 1) return n;
     // 返回之前,把结果记录在备忘录中
    f[n] = dfs(n - 1) + dfs(n - 2);
        return f[n];
    }
 };

2. Function

题⽬来源:洛⾕

题⽬链接:P1464 Function - 洛谷

难度系数:★

(1)题目描述

(2)算法原理

题⽬叙述⾮常清楚,仅需按照「题⽬的要函数写出即可。但理的话,果会「超时」。因为我归的「深⼴度」都⾮常⼤

通过把开图」画出来,现,在遇到⼤量样」的问题如下

(因为递过于庞⼤这⾥只画出了⼀):

以在归的把每次算出来的张「忘录」⾥⾯等到下次递进⼊样」的问题之后不⽤乎乎开计」,⽽是在「忘录⾥⾯直把结果拿来」,到⼤量剪枝的果。

(3)参考代码

#include <iostream>

using namespace std;

typedef long long LL;

const int N = 25;

LL a, b, c;
LL f[N][N][N]; // 备忘录

LL dfs(LL a, LL b, LL c)
{
    if(a <= 0 || b <= 0 || c <= 0) return 1;
    if(a > 20 || b > 20 || c > 20) return dfs(20, 20, 20);

    if(f[a][b][c]) return f[a][b][c];

    if(a < b && b < c) return f[a][b][c] = dfs(a, b, c - 1) + dfs(a, b - 1, c - 1) - dfs(a, b - 1, c);
    else return f[a][b][c] = dfs(a - 1, b, c) + dfs(a - 1, b - 1, c) + dfs(a - 1, b, c - 1) - dfs(a - 1, b - 1, c - 1);
}

int main()
{
    while(cin >> a >> b >> c)
    {
        // 多组测试数据:不需要清空
        if(a == -1 && b == -1 && c == -1) break;
        
        printf("w(%lld, %lld, %lld) = %lld\n", a, b, c, dfs(a, b, c));   
    }

    return 0;
}

3. 天下第⼀

题⽬来源:洛⾕

题⽬链接:P5635 【CSGRound1】天下第一 - 洛谷

难度系数:★★

(1)题目描述

(2)算法原理

⽤递归模拟整个戏过程: dfs(x, y的结由 dfs((x + y) % p, (x + y + y) % p) 得到

因为测试是多组的,并且都是 再加上递归的归的相同问题以把递归改写成记忆搜索。

  • f [x][y] = 1 表⽰ cbw 
  • f [x][y] = 2 表⽰ zhouwc 
  • f [x][y] = 3 ,表⽰这个置已经被访问过被修改成  1 或者 2 ,那就表明平局

注意项:

  • 这道题据范围很 int 类型⼆维数组会溢。但是我们的最终结种情况,⽤ char 类型来存储最终结果,节省

注意:函数用 int   会产生10^9 空间复杂度会空间溢出,用 char 空间复杂度为10^9 / 4 不会空间溢出。

(3)参考代码

#include <iostream>

using namespace std;

const int N = 1e4 + 10;

int x, y, p;
char f[N][N]; // 备忘录

char dfs(int x, int y)
{
    if(f[x][y]) return f[x][y]; // 剪枝

    f[x][y] = '3'; // 这个状态已经访问过了,之后再遇到时,表示平局

    if(x == 0) return f[x][y] = '1';
    if(y == 0) return f[x][y] = '2';
    return f[x][y] = dfs((x + y) % p, (x + y + y) % p);
}

int main()
{
    int T; cin >> T >> p;

    while(T--)
    {
        cin >> x >> y;
        char ret = dfs(x, y);
        if(ret == '1') cout << 1 << endl;
        else if(ret == '2') cout << 2 << endl;
        else cout << "error" << endl;
    }

    return 0;
}

3. 滑雪

题⽬来源:洛⾕

题⽬链接:P1434 [SHOI2002] 滑雪 - 洛谷

难度系数:★★

(1)题目描述 

(2)算法原理

暴⼒遍历整个矩阵看看以当置为起点,最能滑⾏多远离。在有情况⾥⾯最⼤值即可

求出以 [i, j] 为起点的最⼤距离?

  • [i, j]置上下左右瞅⼀瞅果能滑过去就看看下⼀个置为起点,最能滑⾏多远离;
  • 找出四个⽅向上的最远距离,然 +1 。 因为出相同⼦问题⽤ dfs   解决在搜索的遇到⼀样的问题,因可以把递改成记忆搜索的式。

(3)参考代码

#include <iostream>

using namespace std;

const int N = 110;

int n, m;
int a[N][N];
int f[N][N]; // 备忘录

int dx[] = {0, 0, 1, -1};
int dy[] = {1, -1, 0, 0};

int dfs(int i, int j)
{
    if(f[i][j]) return f[i][j];

    int len = 1;
    // 上下左右四个方向搜
    for(int k = 0; k < 4; k++)
    {
        int x = i + dx[k], y = j + dy[k];
        if(x < 1 || x > n || y < 1 || y > m) continue;
        if(a[i][j] <= a[x][y]) continue;

        len = max(dfs(x, y) + 1, len);
    }
    return f[i][j] = len;
}

int main()
{
    cin >> n >> m;
    for(int i = 1; i <= n; i++)
        for(int j = 1; j <= m; j++)
            cin >> a[i][j];

    int ret = 1;
    for(int i = 1; i <= n; i++)
        for(int j = 1; j <= m; j++)
            ret = max(ret, dfs(i, j));

    cout << ret << endl;

    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值