第三章 搜索与图论(一)

DFS与BFS

使用的数据结构空间特点
DFSO(h)不具有最短路
BFS队列O( 2 n 2^n 2n)最短路

DFS: O(h) 会记录路径上的所有点, 与高度成正比, 因此有多深, 就会存多少

842. 排列数字

分析

DFS最重要的是顺序, 该以怎么样的顺序去搜索
这里就是假设有3个空位, 顺序就是从第1位开始填写 _ _ _ , 从前往后1位1位填写

比如现在已经填了 2 _ _ , 只要保证每次填的时候不能和前面一样

最开始3个都是空

在这里插入图片描述

要注意恢复现场
在这里插入图片描述

code

#include <iostream>
using namespace std;
const int N = 10;
bool st[N];
int n;
int path[N]; // 保存每个位置上存了哪些数

void dfs(int u){ // 表示搜到第几个空位
    if (u == n) { // 因为第0层考虑第1个空位, 到第n层的时候, 前面n个空位都考虑完了, 所以直接输出答案
        for (int i = 0; i < n; i ++ ) cout << path[i] << ' ';
        cout << endl;
        return ;
    }
    
    for (int i = 1; i <= n; i ++ ){
        if (!st[i]){
            st[i] = true;
            path[u] = i;
            dfs(u + 1);
            st[i] = false;
        }
    }
}

int main(){
    cin >> n;
    
    dfs(0);
    
    return 0;
}

843. n-皇后问题

分析1(像全排列那样搜)

因为同一行同一列只能放一个皇后

从第1行开始看, 看皇后可以放在哪一列,
再递归到下一层, 看第2行皇后可以放到哪
再枚举第3层皇后可以放在哪

可以在枚举的时候, 判断往当前位置放皇后是否冲突, 如果冲突, 直接回溯, 下面就不用搜了, 这个过程叫剪枝

时间复杂度O(n* n!)

因为每行都有n个分支, 每放一行, 下一层递归就会-1, n, n - 1, n - 2, …
有n行 所以是n * n!

code

#include <iostream>
using namespace std;
const int N = 10;
char g[N][N];
int n;
int col[N], dg[N], udg[N];

void dfs(int u){
    if (u == n) {
        for (int i = 0; i < n; i ++ ) cout << g[i] << endl;
        cout << endl;
        
        return ;
    }
    
    for (int i = 0; i < n; i ++ ){
        if (!col[i] && !dg[i + u] && !udg[i - u + n]){ // 一定要加
            col[i] = dg[i + u] = udg[i - u + n] = 1;
            g[u][i] = 'Q';
            dfs(u + 1);
            g[u][i] = '.';
            col[i] = dg[i + u] = udg[i - u + n] = 0;
        }

    }
}

int main(){
    cin >> n;
    
    for (int i = 0; i < n; i ++ )
        for (int j = 0; j < n; j ++ )
            g[i][j] = '.';
    
    dfs(0);
    
    return 0;
}

分析2

可以用一种更原始的方式来枚举八皇后问题

现在写一个最原始的暴力方法:对于 n 2 n^2 n2 个格子,枚举这个格子放或者不放皇后,时间复杂度KaTeX parse error: Double superscript at position 7: O(2^ n^̲2)

code

#include <iostream>
using namespace std;
const int N = 10;
char g[N][N];
int n;
int row[N], col[N], dg[N], udg[N];

void dfs(int x, int y, int s){ // x, y表示当前枚举的位置, s表示当前已经放了多少个皇后
    if (y == n) y = 0, x ++ ;
    if (x == n){
        if (s == n){ // 还需要满足已经放了n个皇后, s有可能小于n
        	// 因为我们有可能1个皇后都没摆, 也有可能只摆了2-3个皇后
            for (int i = 0; i < n; i ++ ) puts(g[i]);
            puts("");
        }
        return ;
    }
    
    // 不放皇后
    dfs(x, y + 1, s);
    
    // 放皇后
    if (!row[x] && !col[y] && !dg[x + y] && !udg[y - x + n]){
        g[x][y] = 'Q';
        row[x] = col[y] = dg[x + y] = udg[y - x + n] = 1;
        dfs(x, y + 1, s + 1);
        g[x][y] = '.';
        row[x] = col[y] = dg[x + y] = udg[y - x + n] = 0;
    }
    

}

int main(){
    cin >> n;
    
    for (int i = 0; i < n; i ++ )
        for (int j = 0; j < n; j ++ )
            g[i][j] = '.';
    
    dfs(0, 0, 0);
    
    return 0;
}

844. 走迷宫

分析

每个位置上的数表示在bfs中第几层被扩展到
在这里插入图片描述

code

#include <iostream>
#include <cstring>
using namespace std;
const int N = 110;
typedef pair<int, int> PII;

int g[N][N];
int d[N][N];
PII q[N * N]; // N * N
int n, m;

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

void bfs(){
    q[0] = {0, 0};
    memset(d, -1, sizeof d);
    d[0][0] = 0;
    
    int hh = 0, tt = 0; // 因为0当前放了数, 所以++ tt才是下一个要放的位置
     
    while (hh <= tt){
        auto t = q[hh ++ ];
        int x = t.first, y = t.second;
        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] == 0 && d[a][b] == -1){
                d[a][b] = d[x][y] + 1;
                q[++ tt ] = {a, b};
            }
        }
            
    }
}

int main(){
    cin >> n >> m;
    
    
    for (int i = 0; i < n; i ++ )
        for (int j = 0; j < m; j ++ )
            cin >> g[i][j];
    
    bfs();
    
    cout << d[n - 1][m - 1] << endl;
    
    return 0;
}

输出路径

开个PII Prev[N][N]

	if (a >= 0 && a < n && b >= 0 && b < m && g[a][b] == 0 && d[a][b] == -1)
		...
        Prev[a][b] = t
        ...
	}

    
    int x = n - 1, y = m - 1;
    while (x || y){
        cout << x << ' ' << y << endl;
        auto t = Prev[x][y];
        x = t.first, y = t.second;
    }

code(输出路径)

#include <iostream>
#include <cstring>
using namespace std;
const int N = 110;
typedef pair<int, int> PII;

int g[N][N];
int d[N][N];
PII q[N * N], Prev[N][N]; // N * N
int n, m;

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

void bfs(){
    q[0] = {0, 0};
    memset(d, -1, sizeof d);
    d[0][0] = 0;
    
    int hh = 0, tt = 0; // 因为0当前放了数, 所以++ tt才是下一个要放的位置
     
    while (hh <= tt){
        auto t = q[hh ++ ];
        int x = t.first, y = t.second;
        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] == 0 && d[a][b] == -1){
                d[a][b] = d[x][y] + 1;
                Prev[a][b] = t;
                q[++ tt ] = {a, b};
            }
        }
            
    }
    
    int x = n - 1, y = m - 1;
    while (x || y){
        cout << x << ' ' << y << endl;
        auto t = Prev[x][y];
        x = t.first, y = t.second;
    }
}

int main(){
    cin >> n >> m;
    
    
    for (int i = 0; i < n; i ++ )
        for (int j = 0; j < m; j ++ )
            cin >> g[i][j];
    
    bfs();
    
    cout << d[n - 1][m - 1] << endl;
    
    return 0;
}

八数码(留到习题课)

树与图的存储

树是一种特殊的图(无环连通图),所以只讲图的存储
图:有向图,无向图
有向图存储方式:
(1)邻接矩阵(就是一个二维数组),用的很少,因为浪费空间 O(n^2), 比较适合存储稠密图( 边 数 ∼ = 点 数 2 边数 \sim= 点数^2 =2)
(2)邻接表,类似于哈希表的拉链法,新节点插入该链的时候采用头插法( 边 数 ∼ = 点 数 边数 \sim= 点数 =)
在这里插入图片描述

树与图的遍历

yxc: 深度优先遍历和宽度优先遍历,每个点只用遍历1次, 因此需要开个boot st[N]数组来表示哪些点已经遍历过了

深度优先遍历(图)

在这里插入图片描述

code

在这里插入图片描述

846. 树的重心

分析

删除1号点后, 其余连通块点数最大值是4
在这里插入图片描述
删除2号点的话, 最多6个点连通
在这里插入图片描述
删除4号点的话, 连通块最大个数是5
在这里插入图片描述
对于这个题, 我们只要能够 求出, 删掉每个点后, 求出其余连通块的的点数的最大值, 然后在所有点里找一个最小的, 就可以了

问题—> 如何快速每个点删除掉后, 连通块点数的最大值
dfs可以算出子树的大小
比如要算4的子树的大小, 递归的处理完3和6, 因为我们是从上面1下来的, 所以不会走回到1, 4往下走的过程中, 就可以统计出3和6的点数了.
3的点数+ 6的点数 + 4一个点, 就可以知道总共的点数了

因此在dfs中可以求出每个子树的数量

假设想看下, 把4删掉最大值怎么算
首先, 看下把4删掉后, 会分成哪些部分, 每个儿子都是一部分, 还有从父节点往上是另外一部分

子节点的size都可以返回回来
惊奇的发现, 上面父节点连通块的点数 = n − s i z e 4 n - size_4 nsize4

数据范围 1 0 5 10^5 105, 因为每个点都只会被遍历1次, 所以树与图的遍历, 不管bfs还是dfs, 时间复杂度都是O(n + m)
在这里插入图片描述

code

#include <iostream>
#include <cstring>
using namespace std;
const int N = 1e5 + 10, M = N * 2;
int h[N], e[M], ne[M], idx;
int n;
bool st[N];
int ans = 1e8;

void add(int a, int b){
    e[idx] = b, ne[idx] = h[a], h[a] = idx ++;
}
 
int dfs(int u){ // 计算以u为根的子树中(包含u)所有点的数量
    st[u] = true;
    
    int sum = 1, res = 0; // 当前这个点也算1个点, sum计算当前u子树中点的数量, res表示把u删除后, 子树的数量
    for (int i = h[u]; ~i; i = ne[i]){
        int j = e[i];
        if (!st[j]) {
            int s = dfs(j); // 子树的数量
            res = max(res, s);  // 与res取max
            sum += s; 
        }
    }
    res = max(res, n - sum); // 统计父节点连通快与res的最大值
     
    ans = min(ans, res); // 计算答案
    
    return sum; // 返回以u为根节点子树的数量
     
}

int main(){
    scanf("%d", &n);
    memset(h, -1, sizeof h);
    
    for (int i = 0; i < n - 1; i ++ ){
        int a, b;
        scanf("%d%d", &a, &b);
        add(a, b), add(b, a);
    }
    
    dfs(1);
    
    cout << ans << endl;
    
    return 0;
}

宽度优先搜索(图)

在这里插入图片描述

847. 图中点的层次

分析

计算从1->n的最短路
因为这个图里所有边权都是1, 所以可以用宽搜求最短距离
第1次发现这个点的距离, 就是到这个点的最短路径
在这里插入图片描述

code

注意宽搜中用d[N]距离数组判重

#include <iostream>
#include <cstring>
using namespace std;
const int N = 100010, M = N * 2;
int h[N], e[M], ne[M], idx;
int n, m;
int d[N]; // 宽搜中用距离数组判重
int q[N];


void add(int a, int b){
    e[idx] = b, ne[idx] = h[a], h[a] = idx ++;
}

int bfs(){
    memset(d, -1, sizeof d);
    int hh = 0, tt = 0;
    q[0] = 1, d[1] = 0;
    
    while (hh <= tt){
        int t = q[hh ++ ];
        for (int i = h[t]; ~i; i = ne[i]){
            int j = e[i];
            if (d[j] == -1){
                d[j] = d[t] + 1;
                q[++ tt] = j;
            }
        }
    }
    
    return d[n];
}
int main(){
    scanf("%d%d", &n, &m);
    memset(h, -1, sizeof h);
    
    while (m -- ){
        int a, b;
        scanf("%d%d", &a, &b);
        add(a, b);
    }
    
    cout << bfs() << endl;
    
    return 0;
}

01:55:34开始

拓扑排序

图的宽搜的经典应用—> 求拓扑序列

图的拓扑序列是针对有向图来说的

有向无环图---->拓扑图

度数:
入度: 一个点有几条边进来
出度: 一个点有几条边出去

入度为0的点可以作为起点(因为没有任何点指向我)

queue <--- 所有入度为0的点
while (queue 不空) {
	t<--队头
	枚举t的所有出边t->j;  
	删掉t->j边, d[j] --; // 为什么要删掉t->j呢, 
	if (d[j] == 0) queue <- j
}

如果图中有个环的话, 那么我们找不到任何突破口(突破口, 就是入度为0的点)

命题:一个有向无环图—> 一定至少存在一个入度为0的点

证明: 反证法
假设一个有向无环图, 所有点的入度都不是0, 那么可以随便挑一个点, 由于这个点的入度不是0, 可以找到它的上一个点, 同样上一个点的入度不是0, 可以找到上一个点, 由于每一个点的入度都不是0, 因此可以无穷无尽的往回找下去

假设点数是n, 当我们沿着往回的路径走了n + 1个点的时候, 由抽屉原理, 我们一共找了n + 1个不同的点, 但是总共只有n个点, 所以路径里必然存在两个点相同, 因此必然存在一个环在这里插入图片描述

因此我们可以从这个入度为0的点作为突破口, 把这个点和这个点关联的边全部删掉, 本来是有向无环图, 删掉一个点还是有向无环图, 所以说只要是有向无环图, 不断的去突破, 各个击破, 那么各个击破的序列, 就是拓扑序列了

848. 有向图的拓扑序列

分析

模板

code

#include <iostream>
#include <cstring>
using namespace std;
const int N = 100010;
int n, m;
int h[N], e[N], ne[N], idx;
int q[N], d[N];


void add(int a, int b){
    e[idx] = b, ne[idx] = h[a], h[a] = idx ++;
}

bool topsort(){
    int hh = 0, tt = 0; // tt指向当前点需要放的位置
    // 因为当前队列没放任何物品, 所以tt ++, 表示往当前位置放, 放了之后,
    // tt + 1
    for (int i = 1; i <= n; i ++ ) 
        if (d[i] == 0) q[tt ++ ] = i; // 入度为0的点假如到队列
        
    while (hh != tt){
        int t = q[hh ++ ];
        for (int i = h[t]; ~i; i = ne[i]){
            int j = e[i];
            d[j] --;
            if (d[j] == 0) q[tt ++ ] = j;
        }
    }
    
    if (tt == n) return true;
    return false;
}

int main(){
    scanf("%d%d", &n, &m);
    memset(h, -1, sizeof h);
    
    while (m -- ){
        int a, b;
        scanf("%d%d", &a, &b);
        add(a, b);
        d[b] ++;
    }
    
    if (topsort()){
        for (int i = 0; i < n; i ++ ) cout << q[i] << ' ';
        cout << endl;
    }else puts("-1");
    
    return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值