DFS(深度优先遍历)

DFS(深度优先遍历)

1. DFS原理

原理

  • DFS是利用系统栈进行递归,不同于BFS的一层一层遍历,DFS会一条路走到底,不撞南墙不回头。
  • DFS的代码写起来比BFS少了很多,因为BFS需要自己实现队列,而DFS我们不需要实现栈。
  • DFS存在爆栈的风险(系统栈溢出)。

2. DFS之连通性模型

AcWing 1112. 迷宫

问题描述

分析

  • 需要注意一点:如果起始位置位于障碍物上则无法到达

代码

  • C++
#include <iostream>
#include <cstring>

using namespace std;

const int N = 110;

int n;
char g[N][N];
bool st[N][N];
int xa, ya, xb, yb;
int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1, 0, -1};

bool dfs(int x, int y) {
    
    if (g[x][y] == '#') return false;  // 起点可能就在障碍物上
    if (x == xb && y == yb) return true;
    
    st[x][y] = true;
    for (int i = 0; i < 4; i++) {
        int a = x + dx[i], b = y + dy[i];
        if (a >= 0 && a < n && b >= 0 && b < n && !st[a][b] && g[a][b] != '#') {
            if (dfs(a, b)) return true;
        }
    }
    return false;
}

int main() {
    
    int T;
    scanf("%d", &T);
    
    while (T--) {
        scanf("%d", &n);
        for (int i = 0; i < n; i++) scanf("%s", g[i]);
        scanf("%d%d%d%d", &xa, &ya, &xb, &yb);
        
        memset(st, 0, sizeof st);
        
        if (dfs(xa, ya)) puts("YES");
        else puts("NO");
    }
    
    return 0;
}

AcWing 1113. 红与黑

问题描述

分析

  • floodfill模型,可以使用BFS或者DFS解决,可以参考网址:floodfill,这个网址有本题的BFSDFS两种写法。

代码

  • C++
#include <iostream>
#include <queue>
#include <algorithm>

#define x first
#define y second

using namespace std;

typedef pair<int, int> PII;
const int N = 25;

int n, m;  // 行数,列数
char g[N][N];

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

// flood fill
int dfs(int x, int y) {

    int res = 1;
    g[x][y] = '#';
    for (int i = 0; i < 4; i++) {
        int a = x + dx[i], b = y + dy[i];
        if (a >= 0 && a < n && b >= 0 && b < m && g[a][b] == '.')
            res += dfs(a, b);
    }

    return res;
}

int main() {

    while (cin >> m >> n, n || m) {
        for (int i = 0; i < n; i++) cin >> g[i];
        int x = 0, y = 0;
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < m; j++)
                if (g[i][j] == '@') {
                    x = i, y = j;
                    break;
                }
        }
        cout << dfs(x, y) << endl;
    }
}

3. DFS之搜索顺序

AcWing 1116. 马走日

问题描述

分析

  • 可以跳的八个方向使用偏移量技巧,如下:

在这里插入图片描述

代码

  • C++
#include <iostream>
#include <cstring>

using namespace std;

const int N = 10;

int n, m;
bool st[N][N];  // 记录是否被遍历过
int ans;  // 使用全局变量记录答案
int dx[8] = {-2, -1, 1, 2, 2, 1, -1, -2};
int dy[8] = {1, 2, 2, 1, -1, -2, -2, -1};

void dfs(int x, int y, int cnt) {  // cnt为当前遍历第几个点
    
    if (cnt == n * m) {
        ans++;
        return;
    }
    
    st[x][y] = true;
    for (int i = 0; i < 8; i++) {
        int a = x + dx[i], b = y + dy[i];
        if (a < 0 || a >= n || b < 0 || b >= m) continue;
        if (st[a][b]) continue;
        dfs(a, b, cnt + 1);
    }
    st[x][y] = false;
}

int main() {
    
    int T;
    cin >> T;
    
    while (T--) {
        int x, y;
        cin >> n >> m >> x >> y;
        
        memset(st, 0, sizeof st);
        ans = 0;
        dfs(x, y, 1);
        
        cout << ans << endl;
    }
    
    return 0;
}

AcWing 1117. 单词接龙

问题描述

分析

  • 前一个单词的后缀和后一个单词的前缀相等即可将两个单词拼接,并且拼接部分的长度要大于1,同时小于两个单词中任意一个单词的长度。

  • 分为两步:

    (1)预处理出一个二维数组g,g[i][j]=k表示第j个单词可以接到第i个单词前面,且最小的重合部分长度为k

    (2)暴搜所有方案。

代码

  • C++
#include <iostream>

using namespace std;

const int N = 21;

int n;
string word[N];
int g[N][N];  // 有向有权图
int used[N];  // 记录单词使用了几次
int ans;

// last表示当前考察的单词的编号
void dfs(string dragon, int last) {
    
    ans = max((int)dragon.size(), ans);
    
    used[last]++;
    for (int i = 0; i < n; i++)
        if (g[last][i] && used[i] < 2)
            dfs(dragon + word[i].substr(g[last][i]), i);
    used[last]--;
}

int main() {
    
    cin >> n;
    for (int i = 0; i < n; i++) cin >> word[i];
    char start;
    cin >> start;
    
    // 建图
    for (int i = 0; i < n; i++)
        for (int j = 0; j < n; j++) {
            string a = word[i], b = word[j];
            for (int k = 1; k < min(a.size(), b.size()); k++)
                if (a.substr(a.size() - k, k) == b.substr(0, k)) {
                    g[i][j] = k;
                    break;
                }
        }
        
    for (int i = 0; i < n; i++)
        if (word[i][0] == start)
            dfs(word[i], i);
    
    cout << ans << endl;
    
    return 0;
}

AcWing 1118. 分成互质组

问题描述

分析

  • 一组一组的考虑,依次考察所有没有分到某组的数据,如果一个数据能放入当前考察的组,则直接放入,如果当前组不能放入任何一个数据了,那么新开一组,知道所有数据都被分到某组内,类似于贪心的过程。
  • 这个过程会得到最优解,如果一个数据能放入当前考察的组T,但是却没有放入,而是新开了一组,如果这样做的话可以得到最优解,那么完全可以将这个数据放到组T中,结果仍是最优解。

代码

  • C++
#include <iostream>

using namespace std;

const int N = 10;

int n;
int p[N];
int group[N][N];
bool st[N];
int ans = N;

int gcd(int a, int b) {
    return b ? gcd(b, a % b) : a;
}

bool check(int group[], int gc, int i) {
    for (int j = 0; j < gc; j++)
        if (gcd(p[group[j]], p[i]) > 1)
            return false;
    return true;
}

// g: 组的编号,gc: 组内元素个数
// tc: 当前一共搜索元素个数,start: 这一组目前搜索到的下标
void dfs(int g, int gc, int tc, int start) {
    
    if (g >= ans) return;
    if (tc == n) ans = g;
    
    bool flag = true;
    for (int i = start; i < n; i++) 
        if (!st[i] && check(group[g], gc, i)) {
            st[i] = true;
            group[g][gc] = i;
            dfs(g, gc + 1, tc + 1, i + 1);
            st[i] = false;
            
            flag = false;
        }
    // 新开一组
    if (flag) dfs(g + 1, 0, tc, 0);
}

int main() {
    
    cin >> n;
    for (int i = 0; i < n; i++) cin >> p[i];
    
    dfs(1, 0, 0, 0);
    
    cout << ans << endl;
    
    return 0;
}

4. DFS之剪枝与优化

优化内容:

(1)优化搜索顺序:大部分情况下,我们应该优先搜索分支较少的节点

(2)排除等效冗余:等效的答案应该只被搜索一次。比如选择组合数,那么先选1再选2 和 先选2再选1是等效的。

(3)可行性剪枝:如果在搜索过程中可以判断之后的状态都不合法了,可以直接回溯。

(4)最优性剪枝:如果当前搜索的结果状态合法,但是无论如何都不能达到最优解,可以直接回溯。

AcWing 165. 小猫爬山

问题描述

分析

  • 我们遍历依次遍历每只小猫,除了第一只小猫(只能新开一辆缆车),其他小猫可以有多种选择,放在有猫的某个缆车中 或者 新租一个缆车放这个小猫

  • 考虑如何剪枝:

    (1)优化搜索顺序:我们应该按照小猫重量从大到小排序遍历小猫,这样分支比较少。

    (2)排除等效冗余:无

    (3)可行性剪枝:如果某只小猫放置在某个缆车中会超过缆车的最大容量,则应该直接回溯。

    (4)最优性剪枝:如果当前搜索方案需要的缆车数量大于当前最优解,则应该直接回溯。

代码

  • C++
#include <iostream>
#include <algorithm>

using namespace std;

const int N = 20;

int n, m;
int w[N];  // 小猫重量
int sum[N];  // 缆车被占用的重量
int ans = N;

// u: 当前遍历到的小猫; k: 目前需要的缆车数量
void dfs(int u, int k) {
    
    // (4) 最优性剪枝
    if (k >= ans) return;
    if (u == n) {
        ans = k;
        return;
    }
    
    for (int i = 0; i < k; i++)
        if (sum[i] + w[u] <= m) {  // (3) 可行性剪枝
            sum[i] += w[u];
            dfs(u + 1, k);
            sum[i] -= w[u];  // 恢复现场
        }
    
    // 新开一辆缆车
    sum[k] = w[u];
    dfs(u + 1, k + 1);
    sum[k] = 0;  // 恢复现场
}

int main() {
    
    cin >> n >> m;
    for (int i = 0; i < n; i++) cin >> w[i];
    
    // (1) 优化搜索顺序
    sort(w, w + 5, greater<int>());
    
    dfs(0, 0);
    
    cout << ans << endl;
    
    return 0;
}

AcWing 166. 数独

问题描述

分析

  • 我们可以依次填写每个空格,枚举所有情况即可。

  • 考虑如何剪枝:

    (1)优化搜索顺序:我们应该分支较少的格子,即可填数字较少的格子。

    (2)排除等效冗余:无

    (3)可行性剪枝:当前空位中所填写的内容不能与所在行、列、九宫格有重复数组。

    (4)最优性剪枝:无。

  • 另外我们还可以使用位运算优化:

    我们可以使用一个9位的二进制数据表示某一行或者某一列或者某个九宫格的状态,二进制位是0代表可以该行中已经有该数据了,比如0b110100000代表该行(或者该列、或者该九宫格)还可以填写9或者8或者6。

    求解二进制中最右侧的1可以使用lowbit操作。

代码

  • C++
#include <iostream>

using namespace std;

const int N = 9, M = 1 << N;

int ones[M], map[M];  // ones[i]表示i二进制表示中1的个数; map[i]表示log2(i)
int row[N], col[N], cell[3][3];  // 二进制优化
char str[100];

// 将row, col, cell二进制标识全部置为1,代表可以放置数字
void init() {
    for (int i = 0; i < N; i++)
        row[i] = col[i] = (1 << N) - 1;
    
    for (int i = 0; i < 3; i++)
        for (int j = 0; j < 3; j++)
            cell[i][j] = (1 << N) - 1;
}

// is_set = true的话,在(x, y)放置上数字t,否则取消放置
void draw(int x, int y, int t, bool is_set) {
    if (is_set) str[x * N + y] = '1' + t;
    else str[x * N + y] = '.';
    
    int v = 1 << t;
    if (!is_set) v = -v;
    
    row[x] -= v;
    col[y] -= v;
    cell[x / 3][y / 3] -= v;
}

int lowbit(int x) {
    return x & -x;
}

// 获取(x, y)位置可以放置的数据
int get(int x, int y) {
    return row[x] & col[y] & cell[x / 3][y / 3];
}

// cnt: 目前剩余需要填写位置的数量
bool dfs(int cnt) {
    
    if (!cnt) return true;
    
    int minv = 10;  // 记录最少需要填写的次数
    int x, y;
    for (int i = 0; i < N; i++)
        for (int j = 0; j < N; j++)
            if (str[i * N + j] == '.') {
                int state = get(i, j);
                if (ones[state] < minv) {
                    minv = ones[state];
                    x = i, y = j;
                }
            }
    
    int state = get(x, y);
    for (int i = state; i; i -= lowbit(i)) {
        int t = map[lowbit(i)];
        draw(x, y, t, true);
        if (dfs(cnt - 1)) return true;
        draw(x, y, t, false);
    }
    
    return false;
}

int main() {
    
    for (int i = 0; i < N; i++) map[1 << i] = i;
    for (int i = 0; i < 1 << N; i++)
        for (int j = 0; j < N; j++)
            ones[i] += i >> j & 1;
    
    while (cin >> str, str[0] != 'e') {
        init();
        
        int cnt = 0;  // 需要填写的位置的数量
        for (int i = 0, k = 0; i < N; i++)
            for (int j = 0; j < N; j++, k++)
                if (str[k] != '.') {
                    int t = str[k] - '1';
                    draw(i, j, t, true);
                }
                else cnt++;

        dfs(cnt);
        
        puts(str);
    }
    
    return 0;
}

AcWing 167. 木棒

问题描述

分析

  • 首先明确一下概念:木棒指原来为被砍断的,木棍指砍断后的。

  • 我们从小到大依次枚举每一根木棒的长度length,然后依次枚举每个木棒是由哪些木棍拼出来的。

  • 考虑如何剪枝:

    (1)优化搜索顺序:我们应该先枚举比较长的木棍,这样分支比较少。

    (2)排除等效冗余

    ​ (2.1)如果一个木棒可以由三个木棍拼接而成,那么这三个木棍的顺序无所谓,因此我们应该按照组合数的顺序来枚举,具体实现可以通过在递归函数中传入一个start参数,即下一根木棍的下标要从哪里开始。

    ​ (2.2)如果当前木棍加到当前木棒中失败了,则直接略过后面所有长度相等的木棍

    可以用反证法证明。比如3,4号木棍长度相同,3号木棍不能放入当前木棒中(即不是一个合法方案),而4号木棍却可以放入(即是一个合法方案),那么3号木棍一定会被放入到后面的某个木棒中,此时组成一个合法方案,我们可以交换3,4的位置,此时仍然是一个合法方案,矛盾。

    ​ (2.3)如果是木棒的第一根木棍失败,则一定失败。

    可以用反证法证明。由于在拼木棒时从大到小选择每根木棍,所以如果当前木棍放在后面某根木棒中合法了,那么如果是放在了第一根,那么可以将两个长木棍交换,此时也一定是合法解,矛盾;如果不是放在了第一根,那么当前木棒的第一根一定是当前木棍之前的某根木棍,那么由于是从前往后枚举的,所以这种情况一定在搜索当前木棒的第一根木棍的分支中全部搜索过了,且已经发现无解了,而这个分支是要在当前搜索分支之前搜索的,所以这种情况也是不存在的。

    ​ (2.4)如果是木棒的最后一根木棍失败,则一定失败。

    可以用反证法证明。当前木棍如果可以放在当前考察的木棒最后一个位置,但是最后不是一个合法方案,而该木棍放入到后面某个木棒中却成功了,此时我们仍然可以交换两者的位置,仍然是一种合法方案,矛盾。

    (3)可行性剪枝

    ​ (3.1)假设木棍的长度之和为sum,则必须满足length能被sum整除。

    ​ (3.2)如果将当前木棍放入当前木棒中,大于length,则不是合法方案。

    (4)最优性剪枝:无。

代码

  • C++
#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 70;

int n;
int w[N], sum, length;  // length: 枚举的木棒的长度
bool st[N];

// u: 当前枚举到的木棒编号; s: 当前木棒长度
// start: 当前枚举到的木棍编号
bool dfs(int u, int s, int start) {
    
    if (u * length == sum) return true;
    if (s == length) return dfs(u + 1, 0, 0);
    
    // 剪枝2-1,i从start开始枚举
    for (int i = start; i < n; i++) {
        if (st[i]) continue;
        if (s + w[i] > length) continue;  // 剪枝3-2: 可行性剪枝
        
        st[i] = true;
        if (dfs(u, s + w[i], i + 1)) return true;
        st[i] = false;
        
        // 剪枝2-3
        if (!s) return false;
        
        // 剪枝2-4
        if (s + w[i] == length) return false;
        
        // 剪枝2-2
        int j = i;
        while (j < n && w[j] == w[i]) j++;
        i = j - 1;
    }
    
    return false;
}

int main() {
    
    while (cin >> n, n) {
        memset(st, 0, sizeof st);
        sum = 0;
        for (int i = 0; i < n; i++) {
            cin >> w[i];
            sum += w[i];
        }

        // 剪枝1:优化搜索顺序
        sort(w, w + n);
        reverse(w, w + n);
        
        length = 1;
        while (true) {
            // 剪枝3-1: 可行性剪枝
            if (sum % length == 0 && dfs(0, 0, 0)) {
                cout << length << endl;
                break;
            }
            length++;
        }
    }
    
    return 0;
}

AcWing 168. 生日蛋糕

问题描述

分析

  • 分析题目可知下层的蛋糕直径一定严格大于上层蛋糕的直径。与题目不同,这里假设最上面一层蛋糕为第1层蛋糕,最底部的蛋糕为第m层蛋糕。

  • 我们一次枚举每层蛋糕即可。

  • 蛋糕的总表面积为: R m 2 + ( 2 R m h + . . . 2 R 1 h ) R_m^2+(2R_mh+...2R_1h) Rm2+(2Rmh+...2R1h)。忽略 π \pi π

  • 考虑如何剪枝:

    (1)优化搜索顺序:我们应该从最底层蛋糕向最上层蛋糕搜索(从第m层向第1层搜索),这样搜索空间比较小。另外因为R是平方级别的,我们应该先枚举R,再枚举h,并且按照从大到小的顺序枚举R、h,这样留给之后的决策就会更少,搜索空间会减小。

    (2)排除等效冗余:无

    (3)可行性剪枝

    ​ (3.1)假设当前考察的是第u层,因为其上面还有u-1层且下层比上层半径大,所以 u < = R ( u ) < = R ( u + 1 ) − 1 u<=R(u)<=R(u+1)-1 u<=R(u)<=R(u+1)1,另外假设第u层以下的体积是V,则 n − V > = R ( u ) 2 h n-V>=R(u)^2h nV>=R(u)2h,所以 R ( u ) < = ( n − V ) / h < = n − V R(u)<=\sqrt{(n-V)/h}<=\sqrt{n-V} R(u)<=(nV)/h <=nV ,所以 u < = R ( u ) < = m i n ( R ( u + 1 ) − 1 , n − V ) u<=R(u)<=min(R(u+1)-1,\sqrt{n-V}) u<=R(u)<=min(R(u+1)1,nV )

    ​ (3.2)同理: u < = H ( u ) < = m i n ( H ( u + 1 ) − 1 , ( n − V ) / R 2 ) u<=H(u)<=min(H(u+1)-1,(n-V)/R^2) u<=H(u)<=min(H(u+1)1,(nV)/R2)

    ​ (3.3)从上到下前u层的体积最小值minv[u],以及前u层的表面积最小值mins[u]。假设第u层以下的体积是V,表面积是S(已经包含了 R m 2 R_m^2 Rm2),我们必须保证 m i n v [ u ] + V < = n minv[u]+V<=n minv[u]+V<=n m i n s [ u ] + S < a n s mins[u]+S<ans mins[u]+S<ans(ans是我们当前记录的最小表面积)。

    ​ (3.4)考虑从上到下前u层的面积和体积之间的关系(假设第u层以下的体积是V,表面积是S(已经包含了 R m 2 R_m^2 Rm2)):
    S 1 u = ∑ k = 1 u 2 R k h k n − V = ∑ k = 1 u R k 2 h k S_{1u} = \sum_{k=1}^{u}2R_kh_k \\ n-V= \sum_{k=1}^{u}R_k^2h_k S1u=k=1u2RkhknV=k=1uRk2hk

    S 1 u = ∑ k = 1 u 2 R k h k = 2 R u + 1 ∑ k = 1 u R k h k R u + 1 > 2 R u + 1 ∑ k = 1 u R k 2 h k S_{1u} = \sum_{k=1}^{u}2R_kh_k = \frac{2}{R_{u+1}}\sum_{k=1}^{u}R_kh_kR_{u+1} > \frac{2}{R_{u+1}}\sum_{k=1}^{u}R_k^2h_k \\ S1u=k=1u2Rkhk=Ru+12k=1uRkhkRu+1>Ru+12k=1uRk2hk

    所以有:
    S 1 u > 2 ( n − V ) R u + 1 S_{1u} > \frac{2(n-V)}{R_{u+1}} S1u>Ru+12(nV)
    考虑极端情况,当V取到n的时候, S 1 u = 0 S_{1u}=0 S1u=0,可以取到等号,因此:
    S 1 u ≥ 2 ( n − V ) R u + 1 S_{1u} \ge \frac{2(n-V)}{R_{u+1}} S1uRu+12(nV)
    所以有
    S + S 1 u ≥ S + 2 ( n − V ) R u + 1 S + S_{1u} \ge S + \frac{2(n-V)}{R_{u+1}} S+S1uS+Ru+12(nV)
    如果:
    S + S 1 u ≥ S + 2 ( n − V ) R u + 1 ≥ a n s S + S_{1u} \ge S + \frac{2(n-V)}{R_{u+1}} \ge ans S+S1uS+Ru+12(nV)ans
    的话,可以提前结束(ans是我们当前记录的最小表面积)

    (4)最优性剪枝:无。

代码

  • C++
#include <iostream>
#include <cmath>

using namespace std;

const int N = 25, INF = 1e9;

int n, m;  // 总体积、层数
int minv[N], mins[N];  // 从上到下最小体积和,面积和(只考虑侧面积)
int R[N], H[N];  // R[1],H[1]表示最高层的半径和高度
int ans = INF;

// u: 当前遍历那一层; v:已经使用过的体积; 
// s:占用的表面积(已经包含了Rm^2)
void dfs(int u, int v, int s) {
    
    // 优化3-3
    if (v + minv[u] > n) return;
    if (s + mins[u] >= ans) return;
    
    // 优化3-4
    if (s + 2 * (n - v) / R[u + 1] >= ans) return;
    
    if (!u) {
        if (v == n) ans = s;
        return;
    }
    
    // 优化1,3-1,3-2
    for (int r = min(R[u + 1] - 1, (int)sqrt(n - v)); r >= u; r--)
        for (int h = min(H[u + 1] - 1, (n - v) / r / r); h >= u; h--) {
            int t = 0;
            if (u == m) t = r * r;
            R[u] = r, H[u] = h;
            dfs(u - 1, v + r * r * h, s + 2 * r * h + t);
        }
}

int main() {
    
    cin >> n >> m;
    
    for (int i = 1; i <= m; i++) {
        minv[i] = minv[i - 1] + i * i * i;
        mins[i] = mins[i - 1] + 2 * i * i;
    }
    
    R[m + 1] = H[m + 1] = INF;  // 哨兵,因为优化3-1,3-2需要取最小值
    
    // 优化1
    dfs(m, 0, 0);
    
    if (ans == INF) ans = 0;
    cout << ans << endl;
    
    return 0;
}

5. 迭代加深

原理:

每次搜索有一个固定的深度,超过这个深度直接返回。搜索方式仍然是dfs,不需要重新开一个队列。

宽搜的空间复杂度是指数级别的;迭代加深空间复杂度是线性的,因为迭代加深只会记录一条路径上的信息。

迭代加深适用于搜索深度特别深,但是答案在比较浅的层里面。

假设答案在深度为10的层中,则我们需要第一次搜索第1层,第二次搜索第1、2层,第三次搜索第1、2、3层,…,这样有很多层被搜索了很多遍,会不会十分影响性能?答案是会影响一些性能,但是考虑到第10层的节点数一般是大于前9层的节点数,可以忽略不计。

AcWing 170. 加成序列

问题描述

分析

  • 我们依次枚举下一个是什么。

  • 考虑如何剪枝:

    (1)优化搜索顺序:优先枚举较大的数,这样之后的分支比较少。

    (2)排除等效冗余:当我们枚举当前位置填写什么数据时,我们需要遍历前面的任意两个数加和得到的数据,可以会有重复,我们需要排除这些重复的数据,可以在每个节点中开一个判重数组。

    (3)可行性剪枝:当前空位中所填写的内容不能与所在行、列、九宫格有重复数组。

    (4)最优性剪枝:无。

  • 使用迭代加深,层数从1开始(因为至少有一个数1)。如果没有答案,深度加一,继续下一轮搜索。

代码

  • C++
#include <iostream>

using namespace std;

const int N = 110;

int n;
int path[N];  // 记录答案

// u: 当前搜索的深度; k: 迭代加深的深度
bool dfs(int u, int k) {
    
    if (u == k) return path[u - 1] == n;
    
    bool st[N] = {0};
    for (int i = u - 1; i >= 0; i--)
        for (int j = i; j >= 0; j--) {
            int s = path[i] + path[j];
            if (s > n || s <= path[u - 1] || st[s]) continue;
            st[s] = true;
            path[u] = s;
            if (dfs(u + 1, k)) return true;
        }
    return false;
}

int main() {
    
    path[0] = 1;
    while (cin >> n, n) {
        int k = 1;  // 迭代加深的深度
        while (!dfs(1, k)) k++;
        
        for (int i = 0; i < k; i++) cout << path[i] << ' ';
        cout << endl;
    }
    
    return 0;
}

6. 双向DFS

AcWing 171. 送礼物

问题描述

分析

  • 这是一个背包问题,但是不能使用DP的方式求解,因为背包的时间复杂度为O(N*V),但这里V( 2 31 2^{31} 231)的范围太大了,使用DP一定会超时,可以看到N比较小,因此可以使用暴搜来解决。

  • 暴搜的话我们可以枚举每个物品选或者不选,则有 2 46 2^{46} 246种选择,一定会超时,因此需要优化,这里优化的思想是用空间换时间。

  • 我们可以大致将N间物品分为两部分,我们算出在第一部分中的物品能凑出的所有质量的集合A,然后对这一部分数据排序,去重。最后暴搜第二部分的数据,比如我们暴搜到某个质量S时,在A中找到小于W-S的最大值,这就是当前质量S能凑出的最大质量。

  • 如何在一个之和中找到小于某个数据的最大值呢?可以使用二分。

  • 考虑如何剪枝:

    (1)优化搜索顺序:优先枚举较大的数,这样之后的分支比较少。

    (2)排除等效冗余:无。

    (3)可行性剪枝:如果无法加入某件物品,直接回溯。

    (4)最优性剪枝:无。

代码

  • C++
#include <iostream>
#include <algorithm>

using namespace std;

typedef long long LL;

const int N = 46;

int n, m, k;  // k为前一半物品的个数
int w[N];
int weights[1 << 23], cnt = 1;  // weights前一半数组组合得到的质量的集合
int ans;

// u: 当前遍历到的物品编号; s: 当前凑出物品的质量
void dfs1(int u, int s) {
    
    if (u == k) {
        weights[cnt++] = s;
        return;
    }
    
    dfs1(u + 1, s);  // 不选物品u
    if ((LL)s + w[u] <= m) dfs1(u + 1, s + w[u]);  // 选物品u
}

void dfs2(int u, int s) {
    
    if (u == n) {
        int l = 0, r = cnt - 1;
        while (l < r) {
            int mid = l + r + 1 >> 1;
            if ((LL)s + weights[mid] <= m) l = mid;
            else r = mid - 1;
        }
        ans = max(ans, s + weights[l]);
        return;
    }
    
    dfs2(u + 1, s);
    if ((LL)s + w[u] <= m) dfs2(u + 1, s + w[u]);
}

int main() {
    
    cin >> m >> n;
    for (int i = 0; i < n; i++) cin >> w[i];
    
    sort(w, w + n);
    reverse(w, w + n);
    
    k = n / 2;
    dfs1(0, 0);  // 暴搜出第一部分质量集合
    
    sort(weights, weights + cnt);
    cnt = unique(weights, weights + cnt) - weights;
    
    dfs2(k, 0);  // 暴搜第二部分
    
    cout << ans << endl;
    
    return 0;
}

7. IDA*

IDA*原理:

其原理本质相当于在迭代加深的过程中加上一个剪枝,假设当前迭代加深的深度为depth,如果当前搜索的深度为k,如果此时我们发现继续搜索下去无论如何步数都大于depth-k的话,则说明这个分支无解,直接回溯即可。

和BFS中的A*类似,这里也存在一个估价函数f,用于估计还需要搜索的步数,并且要求这个步数一定要小于真实需要的步数。

AcWing 180. 排书

问题描述

分析

  • 复杂度分析:如果我们一次拿出连续的i本书,则有n-i+1种选择。还剩下n-i本书,一共有n-i+1个位置可以插入这些书,除去原来的位置,则还有n-i个位置可以插入,将这些书放到某些书的后面,相当于将某些书放到这些书前面,因此最终的结果需要除以2,i可以从1取到15,因此选法有:
    15 × 14 + 14 × 13 + . . . . + 2 × 1 2 \frac{15\times14+14\times13+....+2\times1}{2} 215×14+14×13+....+2×1
    因为:
    n × ( n − 1 ) + ( n − 1 ) × ( n − 2 ) + . . . . + 2 × 1 = n × ( n + 1 ) × ( n + 2 ) 3 n\times(n-1)+(n-1)\times(n-2)+....+2\times1=\frac{n\times(n+1)\times(n+2)}{3} n×(n1)+(n1)×(n2)+....+2×1=3n×(n+1)×(n+2)
    所以:
    15 × 14 + 14 × 13 + . . . . + 2 × 1 2 = 15 × 16 × 17 2 × 3 = 680 \frac{15\times14+14\times13+....+2\times1}{2}=\frac{15\times16\times17}{2\times3}=680 215×14+14×13+....+2×1=2×315×16×17=680
    每层有680种选择,最多遍历4次,因此最多遍历: 68 0 4 680^4 6804=213,813,760,000

    直接暴搜会超时。

  • 我们可以采用双向宽搜,也可以采用IDA*,这里演示采用IDA*。

  • 我们需要知道我们的估价函数值,这里我们考虑排好序后每个数的后继,n的后继应该是n+1,每次操作我们最多更改3个元素的后继关系,如下图:

在这里插入图片描述

  • 每次迭代前,我们可以计算出当前有多少个后继关系是不正确的,假设一共有tot个后继不正确,则修复这些后继需要的最少步数为:
    ⌈ t o t 3 ⌉ = ⌊ t o t + 2 3 ⌋ \lceil \frac{tot}{3} \rceil = \lfloor \frac{tot+2}{3} \rfloor 3tot=3tot+2

  • 如果当前的步数加上估价函数的值大于迭代加深的步数,则直接可以回溯。

  • 我们每次只需要枚举将长度为i的书放到后面的位置即可,如下图:

在这里插入图片描述

代码

  • C++
#include <iostream>
#include <cstring>

using namespace std;

const int N = 15;

int n;
int q[N];  // 书的编号
int w[5][N];  // 恢复现场使用

// 估价函数
int f() {
    int cnt = 0;
    for (int i = 0; i + 1 < n; i++)
        if (q[i + 1] != q[i] + 1)
            cnt++;
    return (cnt + 2) / 3;
}

// 检查序列是否已经有序
bool check() {
    for (int i = 0; i + 1 < n; i++)
        if (q[i + 1] != q[i] + 1)
            return false;
    return true;
}

// depth: 当前迭代深度; max_depth: 迭代加深最大深度
bool dfs(int depth, int max_depth) {
    
    if (depth + f() > max_depth) return false;
    if (check()) return true;
    
    for (int len = 1; len <= n; len++)  // 先遍历长度
        for (int l = 0; l + len - 1 < n; l++) {  // 再遍历左端点
            int r = l + len - 1;
            for (int k = r + 1; k < n; k++) {
                memcpy(w[depth], q, sizeof q);
                int x, y;
                // 将上图中绿色部分移动到红色部分
                for (x = r + 1, y = l; x <= k; x++, y++) q[y] = w[depth][x];
                // 将上图中红色部分移动到绿色部分
                for (x = l; x <= r; x++, y++) q[y] = w[depth][x];
                if (dfs(depth + 1, max_depth)) return true;
                memcpy(q, w[depth], sizeof q);
            }
        }
    return false;
}

int main() {
    
    int T;
    cin >> T;
    
    while (T--) {
        cin >> n;
        for (int i = 0; i < n; i++) cin >> q[i];
        
        int depth = 0;
        while (depth < 5 && !dfs(0, depth)) depth++;
        if (depth >= 5) puts("5 or more");
        else cout << depth << endl;
    }
    
    return 0;
}

AcWing 181. 回转游戏

问题描述

分析

  • 每次都可以进行8种操作中的一种,这样就可以形成一棵树,暴搜即可。

  • 如果存在多种以最少的方案,输出字典序最小的方案:我们只需要按照A~H的顺序枚举每一种操作即可。

  • 如何计算我们的估价函数?即当前状态到最终状态还需要的步数:

    每次操作我们最多改变中间8个格子中某一个格子的值,因此我们可以统计中间8个格子上出现次数最多的数字的出现次数maxv,则我们至少还需要8-maxv次操作才可能到达最终状态。

  • 各个位置的编码以及操作编码如下:

在这里插入图片描述

代码

  • C++
/*
      0     1
      2     3
4  5  6  7  8  9  10
      11    12
13 14 15 16 17 18 19
      20    21
      22    23
*/
#include <iostream>
#include <cstring>

using namespace std;

const int N = 24;

int op[8][7] = {  // 8个方向操作数据的编号
    {0, 2, 6, 11, 15, 20, 22},
    {1, 3, 8, 12, 17, 21, 23},
    {10, 9, 8, 7, 6, 5, 4},
    {19, 18, 17, 16, 15, 14, 13},
    {23, 21, 17, 12, 8, 3, 1},
    {22, 20, 15, 11, 6, 2, 0},
    {13, 14, 15, 16, 17, 18, 19},
    {4, 5, 6, 7, 8, 9, 10}
};

// 正向操作是0、1、2、3、4、5、6、7,如下是对应反向操作
int oppsite[8] = {5, 4, 7, 6, 1, 0, 3, 2};
// 中心八个数据的位置编号
int center[8] = {6, 7, 8, 11, 12, 15, 16, 17};

int q[N];
int path[100];  // path用于存储方案

// 估价函数
int f() {
    static int sum[4];
    memset(sum, 0, sizeof sum);
    for (int i = 0; i < 8; i++) sum[q[center[i]]]++;
    
    int maxv = 0;
    for (int i = 1; i <= 3; i++) maxv = max(maxv, sum[i]);
    
    return 8 - maxv;
}

// 进行数字x对应的操作
void operate(int x) {
    
    int t = q[op[x][0]];
    for (int i = 0; i < 6; i++) q[op[x][i]] = q[op[x][i + 1]];
    q[op[x][6]] = t;
}

// depth: 当前迭代深度; max_depth: 迭代加深最大深度, last: 上一次操作
bool dfs(int depth, int max_depth, int last) {
    
    if (depth + f() > max_depth) return false;
    if (f() == 0) return true;
    
    for (int i = 0; i < 8; i++)
        if (oppsite[i] != last) {
            operate(i);
            path[depth] = i;
            if (dfs(depth + 1, max_depth, i)) return true;
            operate(oppsite[i]);
        }
    return false;
}

int main() {
    
    while (cin >> q[0], q[0]) {
        
        for (int i = 1; i < 24; i++) cin >> q[i];
        
        int depth = 0;
        while (!dfs(0, depth, -1)) depth++;
        
        if (!depth) printf("No moves needed");
        else {
            for (int i = 0; i < depth; i++) printf("%c", 'A' + path[i]);
        }
        printf("\n%d\n", q[6]);
    }
    
    return 0;
}

8. 力扣上的DFS题目(暴搜)

组合总和

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值