<蓝桥杯软件赛>零基础备赛20周--第12周--DFS基础(必考)

报名明年4月蓝桥杯软件赛的同学们,如果你是大一零基础,目前懵懂中,不知该怎么办,可以看看本博客系列:备赛20周合集
20周的完整安排请点击:20周计划
每周发1个博客,共20周。
在QQ群上答疑
:929486294

文章目录

  • 1. 搜索概述
  • 2. DFS概述
    • 2.1 DFS的代码框架
  • 3. DFS与排列
    • 3.1 输出全排列
    • 3.2 输出部分排列
  • 4. DFS与组合
  • 5. DFS与连通性
  • 6. DFS例题
    • N皇后
    • 与或异或
    • 有奖问答
    • 飞机降落
    • 最大数字
    • 买瓜
  • 7. 习题

第12周:DFS

  如果问蓝桥杯一定会考什么,绝对不会缺席的那种,答案肯定是:DFS!
  例如2023年蓝桥杯省赛,有8道题和DFS有关,或者直接用DFS,或者基于DFS的高级算法:有奖问答、分糖果、飞机降落、与或异或、树上选点、异或和、景区导游、像素放置。

1. 搜索概述

  有人说,只要学通了搜索,在蓝桥杯省赛上就能稳得二等奖。这个说法有开玩笑的意思,其实他的重点是强调搜索的重要性。
  为什么搜索这么重要?因为搜索是“暴力法”的具体实现,暴力法是最直接、最充分利用计算机强大算力的方法。竞赛题目很多可以用暴力法做,即使暴力法的效率不高,在蓝桥杯这样的赛制中,也可能通过部分测试,得到一些分数。
  什么是暴力法?一道算法题,给定输入,有对应的输出。自然地会有一种最简单的解题方法:把所有可能的情况都罗列出来,然后逐一检查,根据给定的输入找到对应的输出,从而得到答案。这种方法称为暴力法(Brute force)。
  虽然所有问题都能用暴力法来求解,但暴力法也往往是“低效”的代名词。暴力法虽然低效,但优点是简单,相对其他“高效”算法,暴力法的代码一般都更短、更好写。在蓝桥杯大赛中,当拿到一个题目后,如果没有更好的思路,可以考虑用暴力法,通过一部分测试,拿到部分分数,这是蓝桥杯大赛的必备技能。
  暴力法的主要手段之一是搜索。很多情况下,如果不会写搜索的代码,那么连暴力法都实现不了。这是为什么搜索如此重要的原因。
  搜索包括两种基本技术:宽度优先搜索(BFS,Breadth-First Search)、深度优先搜索(DFS,Depth-First Search)。它们是算法竞赛常见的知识点,DFS尤其常见,是蓝桥杯考核最多的知识点。
  下面以老鼠走迷宫为例说明BFS和DFS的原理。
  给定一个迷宫,迷宫内道路错综复杂,老鼠从入口进去后,如何找到出口?由于老鼠对迷宫一无所知,它只能暴力地搜索所有可能的道路,直到找到一个出口。
  有两种走迷宫方案,它们分别体现了BFS和DFS的原理。
  (1)一只老鼠走迷宫。它在每个路口,都选择先走右边(或者都选择先走左边),能走多远就走多远;直到碰壁无法再继续往前走,然后往回退一步,这一次走左边,然后继续往下走。用这个办法,只要没遇到出口,就会走遍所有的路,而且不会重复,这里规定回退不算重复走(即使算重复走,也只多走一次)。这个方法就是DFS,概况DFS的思路:一路到底、逐步回退
  (2)一群老鼠走迷宫。假设老鼠无限多,这群老鼠进去后,在每个路口,都派出部分老鼠探索所有没走过的路。走某条路的老鼠,如果碰壁无法前行,就停下;如果到达的路口已经有别的老鼠探索过了,也停下。很显然,在遇到出口前,所有的道路都会走到,而且不会重复。这个方法就是BFS,概况BFS的思路:全面扩散,逐层递进
  BFS和DFS的计算量是多少?对迷宫问题来说,计算量体现在它们在迷宫内部走了多少路口和边。显然不管是BFS和DFS,每个路口和边,它们都只走一次。设路口有n个,边有m条,计算量是O(n+m)。概括地说,BFS和DFS都能找到出口,且都需要暴力搜到所有的路口和道路,它们的计算量是一样的。
  DFS的优势:DFS能搜索并输出从入口到出口的所有路径,BFS不行。从入口到出口的路径数量极多,例如一个十几个路口,几十条边的迷宫,路径可能多达数万。一般不会要求输出路径,或输出路径的数量。
  BFS的优势:BFS能快速找到最短路径,DFS很困难。为什么BFS能快速找到最短路径?以某个路口为例,当有老鼠第一次到达这个路口时,这只老鼠走过的路径一定是最短路径。因为如果是兜圈子过来的,这只老鼠就不是第一个到了。BFS搜最短路径,计算量是O(n+m)的。在算法竞赛中,用BFS求最短路径是常见考点。如果用DFS求最短路径,只能先搜出所有路径,再比较得到最短的。由于路径数量极多,所以计算量极大。
  编码时,BFS需要用队列这种数据结构来实现,“BFS = 队列”;DFS用递归实现,“DFS = 递归”。DFS的编码比BFS的编码简单一些。

2. DFS概述

  DFS是算法竞赛的必考知识点,在任何一场算法竞赛中都不会缺席。
  (1)知识点:DFS是必学的基础算法,是暴力技术的体现。
  (2)编码:DFS的编码很有技巧性,能很好地考验编码能力。
  (3)扩展:DFS是很多高级算法的基础。
  (4)应用:DFS的应用很广,题目可难可易。
  2023年的蓝桥杯省赛,有三道纯DFS题(有奖问答、与或异或、分糖果,仅仅只用到DFS,没有用到其他知识点)是作为填空题出现的,一题仅有5分。这说明出题人认为DFS是必须掌握的基础知识点,一道纯DFS题只能给5分。这一年还有多道大题用到DFS,但是需要结合其他算法。

2.1 DFS的代码框架

  下面给出DFS的代码框架,DFS用递归实现。

ans;                              //答案,常常用全局变量表示
void dfs(层数,其他参数){
    if (到达目的地、或者出局){    //到达最底层,或者满足条件退出 
        更新答案ans;              //答案一般用全局变量表示,ans是最优解
        return;                   //递归返回,即返回到上一层
    }
    (剪枝)                        //在进一步DFS之前剪枝
    for (用i遍历下一层所有可能的情况)    //对每一个情况继续DFS 
        if (used[i] == 0) {        //如果状态i没有处理过,就可以进入下一层dfs
            used[i] = 1;           //标记状态i为已经使用,在后续dfs时不能再使用
            dfs(层数+1,其他参数);      //下一层,即搜小规模后继续dfs
            used[i] = 0;           //恢复状态i,回溯时,不影响上一层对这个状态的使用
        }
    return;                        //返回到上一层
}

  代码看起来不长,但是对初学者来说还是有学习难度。在DFS框架中,最让初学者费解的是第10行和第12行,它们是对递归的理解。
  第10行的used[i] = 1,称为“保存现场”,或“占有现场”。
  第12行的used[i] = 0,称为“恢复现场”,或“释放现场”。
  请读者在大量编码的基础上,再回头体会这个框架的作用。
  DFS常见的应用场景有:排列组合、连通性。

3. DFS与排列

3.1 输出全排列

  本节从DFS的一个基础应用开始:生成排列。排列问题在蓝桥杯题目中常常出现。
  全排列问题是典型的“暴力”问题,n个数的全排列,共有n!种,在每个排列中,每个数出现一次,而且只出现一次。

例题 全排列问题

  如果n比较小,可以写n个for循环输出全排列。但是这种简单方法只能用于较小的n,如果n比较大,这种方法的代码非常冗长、难看。
  DFS非常适合实现全排列,代码清晰、简洁、优美。下面的C++代码能从小到大打印排列。

#include
using namespace std;
int n;
int vis[10];    // 访问标记
int a[10];      //需要做全排列的数组
int b[10];      //当前DFS得到的全排列
void dfs(int step) {
    if (step == n+1) {     //已经对n个数做了全排列,输出全排列
        for (int i=1; i<=n; i++)
            printf("%5d",b[i]);
        printf("\n");
        return;            //结束,不再继续DFS
    }
    for (int i = 1; i <= n; i++) {    //遍历每个a[i],放进全排列中
        if (vis[i] == 0) {   // 数字a[i]不在前面得到的排列中
            b[step] = a[i];  // 把a[i]放进排列
            vis[i] = 1;      // 保存现场:a[i]不能在后面继续用
            dfs(step+1);     // 继续把后面的数放进排列
            vis[i] = 0;      // 恢复现场:a[i]重新可以使用
        }
    }
    return;
}
int main() {
    cin >> n;
    for (int i=1; i<=n; i++)  a[i]=i;   //赋值得到n个数
    dfs(1);  //对a[1]~a[n]做全排列
    return 0;
}

  代码用b[1]~b[n]记录一个全排列。下面以n=3为例,图解代码的执行过程。圆圈内的数字是全排列的数字,例如最上面一行生成的排列是{1, 2, 3}。一共生成6个全排列。用下面的图解释DFS生成全排列的过程。
<蓝桥杯软件赛>零基础备赛20周--第12周--DFS基础(必考)_第1张图片
  代码第27行从dfs(1)开始,这是第一次进入dfs()。图中相关的是标注A的位置。第14行进入for循环,在第16行赋值:i=1时b[1]=1;i=2时b[1]=2;i=3时b[1]=3。对应图中最左边三条线上标注的i=1、i=2、i=3。
  i=1时,在17行vis[1]=1,表示a[1]=1已经放在排列中,后续不能再用。然后进入dfs(2),这是第二次进入dfs()。图中相关的是标注B的位置。
  dfs(2)时,第14行进入for循环。当i=1时,第15行判断vis[1]已经被使用,所以不再继续,用图中最上面的虚线表示i=1不再继续。i=2和i=3可以继续往后执行,例如i=2时,先赋值b[2]=2;然后进入dfs(3),并赋值b[3]=3;最后进入dfs(4),在第8行判断已经得到全排列,输出第一个全排列{1, 2, 3}。图中相关的是标注C的位置。
  后续过程,读者可以继续模拟。特别注意第17行vis[i]=1和第19行vis[i]=0的作用。
  Java代码。

import java.util.Scanner;
public class Main {
    static int n;
    static boolean[] vis;
    static int[] a;
    static int[] b;
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        n = scanner.nextInt();
        vis = new boolean[10];
        a = new int[10];
        b = new int[10];
        for (int i = 1; i <= n; i++)  a[i] = i;        
        dfs(1);
    }
    static void dfs(int step) {
        if (step == n + 1) {
            for (int i = 1; i <= n; i++) 
                System.out.printf("%5d", b[i]);
            System.out.println();
            return;
        }
        for (int i = 1; i <= n; i++) {
            if (vis[i] == false) {
                b[step] = a[i];
                vis[i] = true;
                dfs(step + 1);
                vis[i] = false;
            }
        }
    }
}

Python代码。

n = int(input())
vis = [0] * 10
a = [0] * 10
b = [0] * 10
def dfs(step):
    if step == n + 1:
        for i in range(1, n+1):
            print("%5d" %b[i], end="")
        print()
        return
    for i in range(1, n+1):
        if vis[i] == 0:
            b[step] = a[i]
            vis[i] = 1
            dfs(step + 1)
            vis[i] = 0
for i in range(1, n+1): a[i] = i
dfs(1)

3.2 输出部分排列

  上述代码是打印n个数的全排列,如果需要打印n个数中任意m个数的排列,略作修改即可。例如n=4,m=3,修改C++代码这两处:第8行把step=n+1改为step=m+1,第9行i<=n改为i<=m。

4. DFS与组合

  如果要打印n个数中m个数的组合,同样可以用DFS。
  DFS时,选或不选第k个数,就实现了各种组合。下面举两个例子。
  (1)输出二进制。以打印000 ~ 111为例,下面是Python代码,C++和Java略。

vis = [0]*10
def dfs(k):           #深搜到第k个数
    if k == 4:        #已经得到3个数的组合
        for i in range (1,4):  print(vis[i],end='')
        print('  ',end='')      
    else:
        vis[k] = 0    #第k个选数字0,或者理解为第k个不选(0表示不选)
        dfs(k + 1)    #继续搜下一个
        vis[k] = 1    #第k个选数字1,或者理解为第k个选中(1表示选中)
        dfs(k + 1)    #继续搜下一个
dfs(1) 

  输出:000 001 010 011 100 101 110 111
  如果要反过来,只需要交换7、9行,输出:111 110 101 100 011 010 001 000
  (2)输出组合。以包含3个元素的集合{a, b,c}为例,输出它所有的子集。
  下面是Python代码,C++和Java略。代码输出: c b bc a ac ab abc

a = [' ', 'a', 'b', 'c']
vis = [0] * 10
def dfs(k):
    if k == 4:
        for i in range(1, 4):
            if vis[i]:    print(a[i], end='')
        print('  ', end='')
    else:
        vis[k] = 0     #不选中第k个元素
        dfs(k + 1)     #继续搜下一个元素
        vis[k] = 1     #选第k个元素
        dfs(k + 1)     #继续搜下一个元素
dfs(1)

5. DFS与连通性

  连通性判断是DFS最常见的应用。连通性判断是图论中的一个简单问题,给定一张图,图由点和边组成,要求找到互相连通的部分。连通性判断有三种实现方法:BFS、DFS、并查集,用DFS最简单方便。
  在竞赛题中,图常常用方格图给出,每个方格可以向上下左右四个方向走。
  DFS判断连通性,步骤如下:
  (1)从任意一个点u开始遍历,标记u已经搜过。一般从第一个点开始。
  (2)DFS搜索u的所有符合连通条件的邻居点。已经搜过的点标记为已经搜过,后面不用再搜。扩展u的邻居点时,应该判断这个邻居点是不是在边界内。
  (3)DFS结束,找到了与u连通的所有点,这是一个连通块。
  (4)不与u连通的、其他没有访问到的点,继续用上述步骤处理,找到所有的连通块。
  DFS搜连通的计算复杂度如何?因为每个点只用搜一次且必须至少搜一次,共N个点,DFS的复杂度是O(N)的,不可能更好了。
例题 最大连通
C++代码。

#include
using namespace std;
int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1, 0, -1};  //四个方向
char g[100][100];
int n = 30, m = 60;
int dfs(int x, int y){      //当前位于坐标[x,y]
    if (g[x][y] == '0')   return 0;
    g[x][y] = '0';         //把这个点从1改为0,后面不再搜它
    int ans = 1;           //统计这个连通块的大小
    for (int i = 0; i < 4; i++ ) {           //遍历它的4个邻接
        int nx = x + dx[i], ny = y + dy[i];   //一个邻居的坐标
        if (nx<0 || ny<0 || nx>=n || ny>=m)   continue;    //这个邻居是否在边界内
        ans += dfs(nx, ny);
    }
    return ans;
}
int main(){
    for (int i = 0; i < n; i++ )  cin >> g[i];
    int ans = 0;
    for (int i = 0; i < n; i++ )
        for (int j = 0; j < m; j++ )
            if (g[i][j] == '1')
                ans = max(ans, dfs(i, j));
    cout << ans;
    return 0;
}

java代码

import java.util.Scanner;
public class Main {
    static int[] dx = {-1, 0, 1, 0};
    static int[] dy = {0, 1, 0, -1};
    static char[][] g;
    static int n = 30, m = 60;
    static int dfs(int x, int y) {
        if (g[x][y] == '0') return 0;
        g[x][y] = '0';
        int cnt = 1;
        for (int i = 0; i < 4; i++) {
            int nx = x + dx[i], ny = y + dy[i];
            if (nx < 0 || ny < 0 || nx >= n || ny >= m) continue;
            cnt += dfs(nx, ny);
        }
        return cnt;
    }
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        g = new char[n][m];
        for (int i = 0; i < n; i++)  g[i] = scanner.nextLine().toCharArray();        
        int ans = 0;
        for (int i = 0; i < n; i++) 
            for (int j = 0; j < m; j++) 
                if (g[i][j] == '1') 
                    ans = Math.max(ans, dfs(i, j));
        System.out.println(ans);
    }
}

python代码

dx = [-1, 0, 1, 0]
dy = [0, 1, 0, -1]
n,m = 30,60
def dfs(x, y):
    if g[x][y] == '0':  return 0
    g[x][y] = '0'
    cnt = 1
    for i in range(4):
        nx = x + dx[i]
        ny = y + dy[i]
        if nx < 0 or ny < 0 or nx >= n or ny >= m:  continue
        cnt += dfs(nx, ny)
    return cnt
g = list()
for i in range(30): g.append(list(input().strip()))
ans = 0
for i in range(n):
    for j in range(m):
        if g[i][j] == '1':
            ans = max(ans, dfs(i, j))
print(ans)

例题 全球变暖
  这是典型的连通性问题,计算步骤是:遍历一个连通块(找到这个连通块中所有的’#‘,并标记已经搜过,不用再搜);再遍历下一个连通块……;遍历完所有连通块,统计有多少个连通块。
  什么岛屿不会被完全淹没?若岛中有个陆地(称为高地),它周围都是陆地,那么这个岛不会被完全淹没。用DFS搜出有多少个岛(连通块),检查这个岛有没有高地,统计那些没有高地的岛(连通块)的数量,就是答案。
  C++代码。

#include
using namespace std;
const int N = 1010;
char mp[N][N];                //地图
int vis[N][N]={0};            //标记是否搜过
int d[4][2] = {{0,1}, {0,-1}, {1,0}, {-1,0}}; //四个方向
int flag;                     //用于标记这个岛中是否被完全淹没
void dfs(int x, int y){
    vis[x][y] = 1;            //标记这个'#'被搜过。注意为什么放在这里
    if( mp[x][y+1]=='#' && mp[x][y-1]=='#' &&
        mp[x+1][y]=='#' && mp[x-1][y]=='#'   )
        flag = 1;             //上下左右都是陆地,这是一个高地,不会淹没
    for(int i = 0; i < 4; i++){     //继续DFS周围的陆地
        int nx = x + d[i][0], ny = y + d[i][1];
        if(vis[nx][ny]==0 && mp[nx][ny]=='#')    //注意为什么要判断vis[][]                
            dfs(nx,ny);             //继续DFS未搜过的陆地,目的是标记它们
    }
}
int main(){
    int n;   cin >> n;
    for (int i = 0; i < n; i++)   cin >> mp[i];
    int ans = 0 ;
    for(int i = 0; i < n; i++)    //DFS所有像素点
        for(int j = 0; j < n; j++)
            if(mp[i][j]=='#' && vis[i][j]==0){
                flag = 0;         //假设这个岛被淹
                dfs(i,j);         //找这个岛中有没有高地,如果有,置flag=1
                if(flag == 0) ans++;   //这个岛确实被淹了,统计被淹没岛的数量                          
            }
    cout<<ans<<endl;
    return 0;
}

Java代码

import java.util.Scanner;
public class Main {
    static char[][] mp;
    static int[][] vis;
    static int[][] d = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}};
    static int flag;
    static void dfs(int x, int y) {
        vis[x][y] = 1;
        if (mp[x][y + 1]=='#' && mp[x][y-1]=='#' && mp[x+1][y]=='#' && mp[x-1][y]=='#') 
            flag = 1;        
        for (int i = 0; i < 4; i++) {
            int nx = x + d[i][0], ny = y + d[i][1];
            if (nx>=0 && ny>=0 && nx<mp.length && ny<mp[0].length && vis[nx][ny]==0 && mp[nx][ny] == '#') 
                dfs(nx, ny);            
        }
    }
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        int n = scanner.nextInt();
        mp = new char[n][n];
        vis = new int[n][n];
        for (int i = 0; i < n; i++)    mp[i] = scanner.next().toCharArray();        
        int ans = 0;
        for (int i = 0; i < n; i++) 
            for (int j = 0; j < n; j++) 
                if (mp[i][j] == '#' && vis[i][j] == 0) {
                    flag = 0;
                    dfs(i, j);
                    if (flag == 0)  ans++;                    
                }
        System.out.println(ans);
    }
}

Python代码。需要在第2行用setrecursionlimit()设置递归深度,否则不能通过100%的测试。

import sys
sys.setrecursionlimit(60000)            #设置递归深度
def dfs(x,y):
    d = [(0,1),(0,-1),(1,0),(-1,0)]
    global flag
    global vis
    global mp
    vis[x][y] = 1
    if mp[x][y+1]=='#' and mp[x][y-1]=='#' and mp[x+1][y]=='#' and mp[x-1][y]=='#':
        flag = 1
    for i in range(4):
        nx = x+d[i][0]
        ny = y+d[i][1]
        if vis[nx][ny]==0 and mp[nx][ny]=='#': dfs(nx,ny)
n = int(input())
mp =[]
for i in range(n):  mp.append(list(input()))
vis = []
for i in range(n):  vis.append([0]*n)
ans = 0
for i in range(n):
    for j in range(n):
        if vis[i][j]==0 and mp[i][j]=='#':
            flag = 0
            dfs(i,j)
            if flag == 0:  ans+=1
print(ans)

6. DFS例题

N皇后

链接 N皇后
  n皇后问题是DFS的经典应用。把n个皇后放在棋盘上,要求不同列、不同行、不在同一条对角线。只能用暴力法尝试所有可能的方案,去掉非法方案,得到合法方案。这实际是排列问题,用DFS搜索所有排列最方便。
  题目还要求输出3种方案,而且是字典序的前3种方案。这不难实现,只要按行从1到n、列从1到n的顺序尝试放皇后的方案即可。得到的合法方案,就是字典序的。
  本题的主要技巧是如何判断同行、同列、同对角线上是否已经有其他皇后。
  设当前处理到第x行,前1 ~ x-1行已经放好了皇后。由于一行只放一个皇后,所以两个皇后同行的冲突情况不用再判断,只需要判断同列、同对角线的情况,对角线又分为主对角线、副对角线。下图中三根虚线,vis是同列,vis1是主对角线,vis2是副对角线。
<蓝桥杯软件赛>零基础备赛20周--第12周--DFS基础(必考)_第2张图片
  同列vis上的点有什么特征?它们的y坐标相同。用数组vis[]表示列,vis[y]=1表示第y列已经放了皇后。
  主对角线vis1上的点有什么特征?观察上图,沿着主对角线vis1走一步,x坐标加1,y坐标减一,那么x+y不变。也就是说,一条vis1线上的所有点的x+y都相等。例如一条主对角线上的点[1, 5]、[2, 4]、[3, 3]、[4, 2]、[5,1],都有x+y=6。用数组vis1[]表示主对角线,vis1[x+y]=1表示坐标[x, y]所在的主对角线上已经有皇后。
  副对角线vis2上的点有什么特征?沿着副对角线vis2走一步,x和y坐标都加1,那么x-y不变。例如一条副对角线上的点[1, 3]、[2, 4]、[3, 5]、[4, 6],都有x-y=-2。用数组vis2[]表示副对角线,vis2[x-y+n]=1表示坐标[x, y]所在的副对角线上已经有皇后。这里不能直接用x-y,因为它可能为负数,加上n后就变成了正数。n也可以改为一个大于n的数k,只要保证x-y+k>0就行,不过要记得把vis2[]开大一些,防止溢出。
  除了以上技巧,代码的其他部分就是常见的DFS求排列。
C++代码。

#include
using namespace std;
int n,tot;            //n: 行数;  tot:方案数
int ans[20];          //ans[x]:        第x行皇后放在第几列
int vis[30];          //vis[y]=1:      第y列放了皇后
int vis1[30];         //vis1[x+y]=1:   主对角线放了皇后
int vis2[30];         //vis2[x-y+n]=1: 副对角线放了皇后
void dfs(int x){      //第x行,枚举所有列
    if(x == n+1)  {   //已经放完n行
        tot++;
        if(tot <= 3){
            for(int i=1; i<=n; i++)  cout<<ans[i]<<" ";
            cout<<endl;
        }
        return ;
    }
    for(int y = 1; y <= n; y++){     //枚举第x行的坐标(x, y)
        if(vis[y])       continue;   //第y列已经有皇后 
        if(vis1[x+y])    continue;   //主对角线已经有皇后 
        if(vis2[x-y+n])  continue;   //副对角线已经有皇后
        ans[x] = y;
        vis[y] = 1;  vis1[x+y] = 1;  vis2[x-y+n] = 1; //保存现场
        dfs(x + 1);                  //继续下一行
        vis[y] = 0;  vis1[x+y] = 0;  vis2[x-y+n] = 0; //恢复现场
    }
}
int main(){
    cin >> n;
    dfs(1);
    cout<<tot<<endl;
    return 0;
}

java

import java.util.Scanner;
public class Main {
    static int n, tot;            //n: 行数;  tot:方案数
    static int[] ans;             //ans[x]:        第x行皇后放在第几列
    static int[] vis;             //vis[y]=1:      第y列放了皇后
    static int[] vis1;            //vis1[x+y]=1:   主对角线放了皇后
    static int[] vis2;            //vis2[x-y+n]=1: 副对角线放了皇后
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        n = scanner.nextInt();
        ans = new int[n + 1];
        vis = new int[n + 1];
        vis1 = new int[2 * n + 1];
        vis2 = new int[2 * n + 1];
        dfs(1);
        System.out.println(tot);
    }
    static void dfs(int x) {      //第x行,枚举所有列
        if (x == n + 1) {         //已经放完n行
            tot++;
            if (tot <= 3) {
                for (int i = 1; i <= n; i++)  System.out.print(ans[i] + " ");
                System.out.println();
            }
            return;
        }
        for (int y = 1; y <= n; y++) {        //枚举坐标(x, y)
            if (vis[y] == 1)      continue;   //第y列已经有皇后
            if (vis1[x+y] == 1)   continue;   //主对角线已经有皇后
            if (vis2[x-y+n] == 1) continue;   //副对角线已经有皇后
            ans[x] = y;
            vis[y] = 1;  vis1[x+y] = 1;  vis2[x-y+n] = 1; //保存现场
            dfs(x + 1);                       //继续下一行
            vis[y] = 0;  vis1[x+y] = 0;  vis2[x-y+n] = 0; //恢复现场
        }
    }
}

python

#pypy
n = int(input())
tot = 0              # tot:方案数
ans = [0] * 20       # ans[x]:        第x行皇后放在第几列
vis = [0] * 30       # vis[y]=1:      第y列放了皇后
vis1 = [0] * 30      # vis1[x+y]=1:   主对角线放了皇后
vis2 = [0] * 30      # vis2[x-y+n]=1: 副对角线放了皇后
def dfs(x):
    global tot
    if x == n+1:     # 已经放完n行
        tot += 1
        if tot <= 3:
            for i in range(1, n+1):  print(ans[i], end=' ')
            print()
        return
    for y in range(1, n+1):     # 枚举第x行的坐标(x, y)
        if vis[y] or vis1[x+y] or vis2[x-y+n]: continue             
        ans[x] = y
        vis[y], vis1[x+y], vis2[x-y+n] = 1,1,1     # 保存现场
        dfs(x + 1)              # 继续下一行
        vis[y], vis1[x+y], vis2[x-y+n] = 0,0,0     # 恢复现场
dfs(1)
print(tot)

与或异或

链接 与或异或
  题目的思路很简单。10个逻辑门,每个逻辑门有与、或、异或三种选项,在这10个逻辑门的所有排列中(共 3 10 = 59049 3^{10}=59049 310=59049种),问有多少种排列的最后计算结果是1。
  这一题是DFS求组合的简单应用。读者可以与前面DFS求组合的基本应用比较。
  下面是C++代码。代码的计算量有多少?dfs()在第27行中继续做3次dfs(),一共有10个逻辑门,所以一共做了 3 10 = 59049 3^{10}=59049 310=59049次dfs。每次dfs在check()中做两重for循环约十几次。所以总计算量很小。
  C++代码

#include
using namespace std;
int a[5][5]={{1,0,1,0,1}};  //记录图中圆圈内的值,并初始化第1行
int gate[11];               //记录10个逻辑门的一种排列
int ans;                    //答案
int logic(int x, int y, int op){  //逻辑操作:c=1:与; c=2:或; c=3:异或
    if(op == 1) return x & y;     //与
    if(op == 2) return x | y;     //或
    return x ^ y;                 //异或
}
int check(){  //检查10个逻辑门的排列,最后out是否为1
    int op = 0;
    for(int i = 1; i <= 4; i++)           //从上到下有4行逻辑门
        for(int j = 0; j <= 4 - i; j++)   //每一行从左到右
            a[i][j] = logic(a[i-1][j], a[i-1][j+1], gate[op++]);
    if(a[4][0]) return 1;                 //out=1,结果正确
    return 0;
}
void dfs(int k){    //第k个逻辑门
    if(k == 10){    //一共有10个逻辑门,现在都分配好了。下面模拟这一种组合方式
        if(check())  ans++;    //out=1,结果正确
        return;
    }
    for(int i = 1; i <= 3; i++){    //第k个逻辑门有三种选择:与、或、异或
        gate[k] = i;                  //记录第k个逻辑门:与、或、异或
        dfs(k + 1);                 //继续深搜第k+1个逻辑门
    }
}
int main(){
    dfs(0);
    cout<<ans;
    return 0;
}

  读者可能注意到,在dfs()中并没有做DFS常见的“保存现场、恢复现场”操作。因为10个逻辑门是相互独立的,每个逻辑门独立选“与、或、异或”,与其他逻辑门无关。而前面介绍的数字的全排列,一个数字被使用后,就不能放到其他位置,所以需要用“保存现场”占住它。
java代码

public class Main {
    static int[][] a = new int[5][5];        //记录图中圆圈中的值
    static int[] gate = new int[11];         //记录10个逻辑门的一种排列
    static int ans;                          //答案
    static int logic(int x, int y, int op) { //逻辑操作:c=1:与; c=2:或; c=3:异或
        if(op == 1) return x & y;            //与
        if(op == 2) return x | y;            //或
        return x ^ y;                        //异或
    }
    static int check() {    //检查10个逻辑门的排列,最后out是否为1
        int op = 0;
        a[0][0] = 1; a[0][1] = 0; a[0][2] = 1; a[0][3] = 0; a[0][4] = 1;
        for(int i = 1; i <= 4; i++)            //从上到下有4行逻辑门
            for(int j = 0; j <= 4 - i; j++)    //每一行从左到右
                a[i][j] = logic(a[i-1][j], a[i-1][j+1], gate[op++]);        
        if(a[4][0] == 1) return 1;             //out=1,结果正确
        return 0;
    }
    static void dfs(int k) {    //第k个逻辑门
        if(k == 10) {           //10个逻辑门都分配好了。下面模拟这一种组合方式
            if(check() == 1) ans++;    //out=1,结果正确
            return;
        }
        for(int i = 1; i <= 3; i++) {    //第k个逻辑门有三种选择:与、或、异或
            gate[k] = i;        //记录第k个逻辑门:与、或、异或
            dfs(k + 1);         //继续深搜第k+1个逻辑门
        }
    }
    public static void main(String[] args) {
        dfs(0);
        System.out.println(ans);
    }
}

python代码

a = [[0 for _ in range(5)] for _ in range(5)]     #记录图中圆圈中的值
gate = [0] * 11                 #记录10个逻辑门的一种排列
ans = 0                         #答案
def logic(x, y, op):            #逻辑操作:c=1:与; c=2:或; c=3:异或
    if op == 1:   return x & y  #与
    if op == 2:   return x | y  #或
    return x ^ y                #异或
def check():                    #检查10个逻辑门,最后out是否为1
    op = 0
    a[0] = [1, 0, 1, 0, 1]
    for i in range(1, 5):             #从上到下有4行逻辑门
        for j in range(0, 5 - i):     #每一行从左到右
            a[i][j] = logic(a[i-1][j], a[i-1][j+1], gate[op])
            op += 1
    if a[4][0] == 1: return 1          #out=1,结果正确        
    return 0
def dfs(k):  #第k个逻辑门
    global ans
    if k == 10:        #一共有10个逻辑门,现在都分配好了。下面模拟这一种组合方式
        if check() == 1: ans += 1       #out=1,结果正确            
        return
    for i in range(1, 4):        #第k个逻辑门有三种选择:与、或、异或
        gate[k] = i              #记录第k个逻辑门:与、或、异或
        dfs(k + 1)               #继续深搜第k+1个逻辑门
dfs(0)
print(ans)

有奖问答

链接 有奖问答
  这一题是C++组的填空题,填空题对运行时间要求不高,可以用比较耗时的算法做。
  本题的正解是动态规划DP,计算量小,运行时间快。不过因为是填空题,可以用暴力方法做,即用DFS编码直接搜索所有情况。虽然DFS的计算量比DP大很多,但是思路简单,编码时间短。
  下面是C++代码。dfs()模拟做题过程,思路直接。
C++

#include
using namespace std;
int ans=0;
void dfs(int x,int score,int k){    //x:第x题; score:得分; k:对错
    if(k==0) score=0;        //答错了,归零
    else{
        score+=10;           //答对了
        if(score==100)       //剪枝
            return;          //100分不是符合要求的答题情况,所以ans不用加1
    }
    if(score==70) ans++;     //70分,答案加1
    if(x==30) return;         //共30题,剪枝
    dfs(x+1,score,0);        //继续做题。0: 答错了
    dfs(x+1,score,1);        //继续做题。1:答对了
}
int main(){
    dfs(0,0,0);
    cout<<ans;
}

  代码的计算量有多大?第13、14行继续做2次dfs,计算复杂度是 O ( 2 n ) O(2^n) O(2n)。当n=30时,约十亿次,运行时间小于1分钟。
  这一题出题人考核的就是DFS。故意让n=30,用DFS刚好能1分钟运行结束得到答案。如果n更大一些,运行时间就太长了,DFS就不合适了。
  这一题不适合用python,因为python的循环很慢,运行时间极长。

飞机降落

链接 飞机降落
  题目看起来似乎可以用贪心求解,请读者思考是否可行。
  本题的N很小,计算量不大,可以求N架飞机的全排列,然后逐一验证验证每个全排列,如果有合法的一个全排列就打印YES,如果所有全排列都不合法就打印NO。
  求全排列可以用库函数next_permutation()。本题给的时间限制是2秒,最多有10!个全排列,用next_permutation()的代码运行时间正好2秒,勉强通过测试。
  next_permutation()的缺点是不能剪枝:必须先求得一个完整全排列,然后再判断这个全排列是否合法。而大部分全排列的前几项就已经不合法了,不用求完整的全排列。
  本题更保险的方法是自己写DFS求全排列,可以在验证一个全排列时在中间剪枝,运行时间比next_permutation()短很多,仅有0.1秒。
  C++代码。第12行剪枝,如果这架飞机安排不了就跳过它,相当于终止计算这个全排列。

#include 
using namespace std;
int T[15],D[15],L[15];
int n;
int vis[15],ans;
void dfs(int plane,int time){
    if(plane==n){       //n架飞机都安排好了能降落
        ans=1;
        return;
    }
    for(int i=0;i<n;i++){
        if(!vis[i] && time<=T[i]+D[i]){  //剪枝
            int t = time;                //t:安排给飞机i的降落时间
            if(t<T[i])  t=T[i];          //飞机i还没到,只能等它
            vis[i]=1;                   
            dfs(plane+1,t+L[i]);
            vis[i]=0;
        }
    }
}
int main(){
    int m; cin >>m;  //m是测试组数
    while(m--){
        cin >> n;
        for(int i=0;i<n;++i)  cin >> T[i] >> D[i] >> L[i];
        ans = 0;
        dfs(0,0);
        if(ans) cout<<"YES\n";
        else    cout<<"NO\n";
    }
    return 0;
}

java代码

import java.util.Scanner;
public class Main {
    private static int[] T;
    private static int[] D;
    private static int[] L;
    private static int n;
    private static int[] vis;
    private static int ans;
    private static void dfs(int plane, int time) {
        if(plane == n) {
            ans = 1;
            return;
        }
        for(int i = 0; i < n; i++) {
            if(vis[i] == 0 && time <= T[i] + D[i]) {
                int t = time;
                if(t < T[i]) t = T[i];
                vis[i] = 1;
                dfs(plane + 1, t + L[i]);
                vis[i] = 0;
            }
        }
    }
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        int m = scanner.nextInt();
        while(m-- > 0) {
            n = scanner.nextInt();
            T = new int[n];   D = new int[n];    L = new int[n];
            vis = new int[n];
            for(int i = 0; i < n; i++) {
                T[i] = scanner.nextInt();
                D[i] = scanner.nextInt();
                L[i] = scanner.nextInt();
            }
            ans = 0;
            dfs(0, 0);
            if(ans == 1)    System.out.println("YES");
            else            System.out.println("NO");
            
        }
    }
}

python代码

def dfs(plane, time):
    global ans
    if plane == n:
        ans = 1
        return
    for i in range(n):
        if vis[i] == 0 and time <= T[i] + D[i]:
            t = time
            if t < T[i]:    t = T[i]
            vis[i] = 1
            dfs(plane + 1, t + L[i])
            vis[i] = 0
m = int(input())
for _ in range(m):
    n = int(input())
    T,D,L = [],[],[]
    vis = []
    for i in range(n):
        t, d, l = map(int, input().split())
        T.append(t)
        D.append(d)
        L.append(l)
        vis.append(0)
    ans = 0
    dfs(0, 0)
    if ans == 1:   print("YES")
    else:          print("NO")

最大数字

链接 最大数字
  要把N变成最大数字,可以用贪心思路依次处理N每一位:先把最高位尽量变成最大的数字,再把次高位尽量变成最大数字,…,等等。
  首先明确一点,1号操作和2号操作不要混用,因为它们互相抵消,混用浪费。
  如何把最高位尽量变大?设最高位的数字是d。
  先试试1号操作,得到最大数字x,x最大能到9。取操作次数t = min(A, 9-d),其中9-d表示能得到9的次数;如果A次到不了9,就做A次。
  再试试2号操作,因为是减1,所以只有能变成9才有意义。如果有B>d,那么可以减d+1次变成9。
  其他位也这样处理,直到用完操作次数A和B。
  以上过程可以直接模拟,见下面的Java代码。也可以用DFS来实现,见下面的C++和Python代码。
  C++代码。dfs(i, v)的参数i表示当前处理到第i位,例如N=123,第一次dfs(0, 0),i=0表示第0位是1。dfs(i, v)的参数v是已经得到的值,例如第0位的数字1处理完后,1变成9,那么v=9。

#include
using namespace std;
char s[20];
long long ans;                //ans: 最大值,要用long long
int A,B;                      //A:1号操作剩余次数  B:2号操作剩余次数
void dfs(int i,long long v){     //i:当前处理到第i位,v:前面已经得到的值
    int d = s[i]-'0';         //第i位的数字
    if(s[i]){                 //如果s[i]为'\0',处理结束
        int t = min(A,9-d);   //1操作次数t:最大到9
        A -= t;               //更新A。如果A=0,也要继续dfs,目的是求值:v*10+d+t
        dfs(i+1,v*10+d+t);    //这一位最大是x+t。v*10+d+t是到这一位为止的数值
        A += t;               //恢复现场
        if(B>d){               //操作2:可以减到9
            B -= d+1;          //做d+1次,减到9
            dfs(i+1,v*10+9);
            B += d+1;          //恢复现场
        }
    }
    else   ans = max(ans,v);   //处理结束,得到这次DFS的最大值

}
int main(){
    cin >> s >> A >> B;        //数字N按字符串s读入
    dfs(0,0);                  //从N的最高位开始
    cout << ans;
    return 0;
}

Java代码。直接模拟题意,不用DFS。

import java.util.Scanner;
public class Main {
    public static void main(String[] args) {
        Scanner in = new Scanner(System.in);
        String s=in.next();           //数字N按字符串读入
        int A = in.nextInt();
        int B = in.nextInt();
        int[] a=new int[s.length()];  //把N的每个数字存到a[]中
        for(int i=s.length()-1;i>=0;i--)
            a[i]=(s.charAt(i)-'0');
        for(int i=0;i<s.length();i++)  { //从最高位处理到最低位
            if(a[i]-B < 0)
                if((a[i]+1<(9-a[i])|| A<9-a[i]) || A<B){
                    B -= a[i]+1;
                    a[i] = 9;
                }
            while(a[i]<9 && A!=0) {
                a[i]++;
                A--;
            }
            System.out.print(a[i]);       //打印这一位的最大结果
        }
    }
}

Python代码。用DFS实现,和C++代码一样。

N,A,B=map(int,input().split())
s=list(map(int,str(N)))
def dfs(i,v):
    global A,B,ans
    if i==len(s):
        ans=max(ans,v)
        return    
    d=s[i]
    t=min(A,9-d)
    A -= t
    dfs(i+1,10*v+d+t)
    A+=t     
    if B > d:
        B -= d+1
        dfs(i+1,10*v+9)
        B += d+1
ans=0
dfs(0,0)
print(ans)

买瓜

链接 买瓜
  首先注意到评测用例中的n都不大,估计可以用暴力的搜索解决。
  第i个瓜有三个选项:完整的瓜重Ai、半个瓜重Ai/2、不要这个瓜。本题简单的做法是对所有的瓜进行组合,每个瓜尝试三个选项。共有 3 n 3^n 3n种组合,可以通过约50%的测试。用DFS编码求组合。
  C++代码。用到一个小技巧,为了避免除2出现小数,改为把m乘2,那么每个瓜的3个选项是:2Ai、Ai、0。
c++代码

#include
using namespace std;
int n,m;
int a[40];
int ans=40;
void dfs(int step,int s,int k){ //step:第step个瓜,s:已选中的瓜的总重,k:砍了几刀
    if (s > m || k >= ans)     return;
    if(s==m) {
        ans=min(ans,k);
        return;
    }
    if(step==n+1)  return;
    dfs(step+1,s,k);                   //不选
    dfs(step+1,s+a[step],k+1);         //ai,砍了一刀
    dfs(step+1,s+a[step]*2,k);         //2ai
}
int main(){
    cin>>n>>m;   m<<=1;               //m乘2
    for(int i=1;i<=n;i++)  cin>>a[i];
    dfs(1,0,0);
    cout << (ans == 40? -1: ans) << endl;
}

  本题100%得分的代码需要用到分治法和二分,请自行搜索题解。
  java代码

import java.util.Scanner;
public class Main {
    static int n, m;
    static int[] a;
    static int ans = 40;
    public static void dfs(int step, int s, int k) {
        if (s > m || k >= ans) return;
        if (s == m) {
            ans = Math.min(ans, k);
            return;
        }
        if (step == n + 1) return;
        dfs(step + 1, s, k);
        dfs(step + 1, s + a[step], k + 1);
        dfs(step + 1, s + a[step] * 2, k);
    }
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        n = scanner.nextInt();
        m = scanner.nextInt() << 1;
        a = new int[n + 1];
        for (int i = 1; i <= n; i++)  a[i] = scanner.nextInt();        
        dfs(1, 0, 0);
        System.out.println(ans == 40 ? -1 : ans);
    }
}

python代码

def dfs(step, s, k):
    global ans
    if s > m or k >= ans:   return
    if s == m:
        ans = min(ans, k)
        return
    if step == n + 1:  return
    dfs(step + 1, s, k)
    dfs(step + 1, s + a[step], k + 1)
    dfs(step + 1, s + a[step] * 2, k)
ans = 40
n, m = map(int, input().split())
m <<= 1
a = [0] * (n + 1)
a[1:] = map(int, input().split())
dfs(1, 0, 0)
print(-1 if ans == 40 else ans)

7. 习题

蓝桥杯题库的DFS题目:
https://www.lanqiao.cn/problems/?first_category_id=1&tags=DFS&sort=difficulty&asc=1

你可能感兴趣的:(蓝桥杯软件赛零基础备赛20周,蓝桥杯,深度优先,dfs)