DFS 和 BFS
| 数据结构 | 空间复杂度 | 性质 | |
|---|---|---|---|
| DFS | stack栈 | 不具有最短性,暴力搜索满足条件的结果 | |
| BFS | queue队列 | 最短路径性质 |
深度优先搜索 DFS(暴力搜索)
全排列问题
题目:leetcode-cn.com/problems/pe…
class Solution {
//path 存储路径
List<Integer> path = new ArrayList<>();
//存储最终结果
List<List<Integer>> ans = new ArrayList<>();
public List<List<Integer>> permute(int[] nums) {
int len = nums.length;
if (len == 0) return ans;
//用于nums记录的元素是否被使用过
boolean[] st = new boolean[len];
dfs(nums, 0, len, st);
return ans;
}
public void dfs(int[] nums, int u, int len, boolean[] st) {
//表示已经遍历到最后一个位置,即最后一层
if (u == len) {
ans.add(new ArrayList<Integer>(path));
return ;
}
for (int i = 0; i < len; i++) {
//表示nums当前元素没有被使用,可以使用
if (st[i] != true) {
path.add(nums[i]);
st[i] = true;
//dfs,往下递归
dfs(nums, u + 1, len, st);
//下面两行用于在回溯前恢复现场
path.remove(path.size() - 1);
st[i] = false;
}
}
}
}
N皇后问题
题目位置:leetcode-cn.com/problems/n-…
baike.baidu.com/item/%E7%9A…N皇后介绍
class Solution {
int N = 20;
// 存储满足要求的皇后摆放的结果
char[][] g = new char[N][N];
//加list是因为返回需要的是list
List<List<String>> ans = new ArrayList<>();
List<String> level = new ArrayList<>();
public List<List<String>> solveNQueens(int n) {
if (n == 0) return ans;
//设置数组g元素都是‘.’
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
g[i][j] = '.';
}
}
//当前元素是被使用,那么该元素所在的列也不能被放置元素
boolean[] col = new boolean[N];
//当前元素的右斜方向
boolean[] dg = new boolean[N];
//当前元素的左斜方向
boolean[] udg = new boolean[N];
dfs(0, n, col, dg, udg);
return ans;
}
public void dfs(int u, int n, boolean[] col, boolean[] dg, boolean[] udg) {
//将数组转成list
if (u == n) {
for (int i = 0; i < n; i++) {
String tmp = "";
for (int j = 0; j < n; j++) {
tmp += g[i][j];
}
level.add(new String(tmp));
}
ans.add(new ArrayList<>(level));
level.clear();
return ;
}
//遍历所有满足条件的结果
for (int i = 0; i < n; i++) {
if (col[i] != true && dg[u + i] != true && udg[n - u + i] != true) {
g[u][i] = 'Q';
col[i] = true;
dg[u + i] = true;
udg[n - u + i] = true;
//当前层满足,则进入到下一层,直到最后一层
dfs(u+1,n, col, dg, udg);
//恢复现场
col[i] = false;
dg[u + i] = false;
udg[n - u + i] = false;
g[u][i] = '.';
}
}
}
}
宽度优先搜索 BFS(可以搜索到最短路径)
DFS深度搜索可以保证搜到终点,但是不能保证是最短的。
当所有边的权重都是一样的时候,才可以用BFS来算;当权重不一样的时候,需要使用的专门的最短路径算法进行计算。
queue <- 初始化
while (queue 不为空) {
t <- 队头
拓展
}
例题:走迷宫 www.acwing.com/problem/con…
地图分析:leetcode-cn.com/problems/as…
对于图的BFS与Tree的BFS区别如下:
1、tree只有1个root,而图可以有多个源点,所以首先需要把多个源点都入队。
2、tree是有向的因此不需要标志是否访问过,而对于无向图来说,必须得标志是否访问过!
并且为了防止某个节点多次入队,需要在入队之前就将其设置成已访问!
//存储路径
class Pair { // 其实可以使用int[2] 数组存储
int x;
int y;
public Pair(int x, int y) {
this.x = x;
this.y = y;
}
}
public class Main {
//地图
static int[][] map = null;
//保存走过的路,数组d的元素表示当前走了多少步
//当没有走过的话,则该值为0
static int[][] d = null;
//用于记录当前位置是从之前哪个位置过来的,便于输出路径
static Pair[][] prev = null;
static int n;
static int m;
public static void main(String[] args) throws IOException {
InputStreamReader in = new InputStreamReader(System.in);
BufferedReader br = new BufferedReader(in);
String[] nums = br.readLine().split(" ");
n = Integer.parseInt(nums[0]);
m = Integer.parseInt(nums[1]);
map = new int[n][m];
d = new int[n][m];
prev = new Pair[n][m];
//迷宫map
for(int i = 0; i < n; i++) {
String[] inputs = br.readLine().split(" ");
for (int j = 0; j < m; j++) {
map[i][j] = Integer.parseInt(inputs[j]);
}
}
System.out.println(bfs());
}
public static int bfs() {
//初始化队列
Queue<Pair> q = new LinkedList<Pair>();
int[] dy = {0, 1, 0, -1}, dx = {-1, 0, 1, 0};
//加入起点0, 0
q.offer(new Pair(0, 0));
while (!q.isEmpty()) {
Pair pair = q.poll();
if (pair.x == n - 1 && pair.y == m - 1) {
break;
}
//上左下右 遍历
for (int i = 0; i < 4; i++) {
int x = pair.x + dx[i];
int y = pair.y + dy[i];
if (x >= 0 && x < n && y >= 0 && y < m && map[x][y] == 0 && d[x][y] == 0) {
q.offer(new Pair(x, y));
d[x][y] = d[pair.x][pair.y] + 1;
//存储能到当前x,y的位置
prev[x][y] = pair;
}
}
}
//从终点往前遍历到起点
int x = n - 1, y = m - 1;
while (x != 0 || y != 0) {
System.out.println(x + " " + y);
//prev[x][y] 存储的是能到达当前位置的位置
Pair tmp = prev[x][y];
x = tmp.x;
y = tmp.y;
}
return d[n - 1][m - 1];
}
}
树与图的存储
(1) 邻接矩阵
g[a][b] 存储边a->b,g[a][b]的值存储的是边的权重
(2) 邻接表
// 对于每个点k,开一个单链表,存储k所有可以走到的点。
// h[k]存储单链表的头结点,这里因为存在多个节点,所以有多个单链表
// h[k]解释:下标k是头节点的值,h[k]存储的值是下个元素在e[]中的下标
// e[N] 每个节点的值
// ne[N] 存储 next指针
static int N = 100010, M = N * 2;
int[] h = new int[N], e = new int[M], ne = new int[M];
int idx = 0;
//记录已经被遍历过的点
boolean[] st = new int[N];
// 添加一条边a->b
void add(int a, int b){
e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ;
}
//将所有的头节点初始化为-1
//for (int i = 0; i < N; i++) {
// h[i] = -1;
//}
Arrays.fill(h, -1);
void dfs(int u) {
st[u] = true;//标记一下,已经被搜索过了
for (int i = h[u]; i != -1; i = ne[i]) {
int j = e[i];
if (st[j] != 0) dfs(j);
}
}
拓扑排序
只有有向图才会有拓扑序列,有向无环图一定存在拓扑图。
无环图一定至少存在一个节点的入度为0,而一个有环图一定不存在一个节点的入度为0;
时间复杂度 , n 表示点数,m 表示边数
queue <- 所有入度为0的点
while (queue 不为空) {
t <- 队头;
枚举t的所有出边 t -> j
删除t -> j, d[j]--;
if (d[j] == 0) queue <- j;
}
模板代码
bool topsort(){
int hh = 0, tt = -1;
// d[i] 存储点i的入度
for (int i = 1; i <= n; i ++ ) {
if (d[i] != 0) {
q[ ++ tt] = i;
}
}
while (hh <= tt) {
int t = q[hh ++ ];
for (int i = h[t]; i != -1; i = ne[i]) {
int j = e[i];
if (-- d[j] == 0)
q[ ++ tt] = j;
}
}
// 如果所有点都入队了,说明存在拓扑序列;否则不存在拓扑序列。
return tt == n - 1;
}
最短路问题
最短路问题分类
- 单源最短路问题
- 所有边权重都是正数(解决方法:dijkstra算法)
- 存在负权边,即存在某些边的权重是负数(解决方法:Bellman-Ford 算法 和 spfa算法)
- 多源汇最短路问题(源点即起点,汇点即终点)(解决方法:floyd算法)
最短路问题的核心在于根据给定的数据建图。
总结:优先选择spfa算法。
dijkstra算法
(1)朴素dijkstra算法
适用于稠密图,即边数多。使用邻接矩阵存储路径
算法思路:
初始化距离:dist[1] = 0, dist[i] = 无穷大
s: 当前已经确定最短路径的点
for i : n
t : 不在s中的距离最近的点
t 加到 s 中
用 t 更新其他点的距离
时间复杂是 , n 表示点数,m 表示边数
static int[][] g = new int[N][N]; // 存储每条边
static int[] dist = new int[N]; // 存储1号点到每个点的最短距离
static boolean[] st = new int[N]; // 存储每个点的最短路是否已经确定
static int max = Integer.MAX_VALUE;
//初始化
for (int i = 0; i < n;i ++) {
for (int j = 0; j < m; j++) {
if (i == j) g[i][j] = 0;
else g[i][j] = max;
}
}
// 求1号点到n号点的最短路,如果不存在则返回-1
public int dijkstra(){
//将距离初始化为正无穷
Arrays.fill(dist, max);
dist[1] = 0;
for (int i = 0; i < n - 1; i ++ ) {
// 还未确定最短路的点中,寻找到起点距离最小的点t
int t = -1;
for (int j = 1; j <= n; j ++ ) {
if (st[j] != true && (t == -1 || dist[t] > dist[j])) t = j;
}
//节点t加入到集合S中
st[t] = true;
// 用t更新其他点的距离
for (int j = 1; j <= n; j ++ ) {
dist[j] = min(dist[j], dist[t] + g[t][j]);
}
}
if (dist[n] == max) return -1;
return dist[n];
}
(2)堆优化版dijkstra
适用于稀疏图,即m边数较少。
时间复杂度 , n 表示点数,m 表示边数
typedef pair<int, int> PII;
int n; // 点的数量
int[] h = new int[N], w = new int[N], e = new int[N], ne = new int[N];// 邻接表存储所有边
int idx;
int[] dist = new int[N]; // 存储所有点到1号点的距离
boolean st = new int[N]; // 存储每个点的最短距离是否已确定
// 求1号点到n号点的最短距离,如果不存在,则返回-1
int dijkstra(){
Arrays.fill(dist, 0X3f3f3f3f);
dist[1] = 0;
//使用优先队列存储距离
priorityQueue<PII, vector<PII>, greater<PII>> heap;
heap.push({0, 1}); // first存储距离,second存储节点编号
while (heap.size()) {
auto t = heap.top();
heap.pop();
int ver = t.second, distance = t.first;
if (st[ver] == true) continue;
st[ver] = true;
//使用当前这个点更新其他点
for (int i = h[ver]; i != -1; i = ne[i]) {
int j = e[i];
if (dist[j] > distance + w[i]) {
dist[j] = distance + w[i];
heap.push({dist[j], j});
}
}
}
if (dist[n] == 0x3f3f3f3f) return -1;
return dist[n];
}
Bellman-Ford算法
时间复杂度, n 表示点数,m 表示边数
用于解决有负权边的情况,注意,在这种情况下可能不存在最短路径。
要限定走过的路径边数
注意在模板题中需要对下面的模板稍作修改,加上备份数组,详情见模板题。
int n, m; // n表示点数,m表示边数
int[] dist = new int[N]; // dist[x]存储1到x的最短路距离
// 边,a表示出点,b表示入点,w表示边的权重
class Edge {
int a, b, w;
Edge(int a, int b, int c) {
this.a = a; this.b = b; this.c = c;
}
};
// 求1到n的最短路距离,如果无法从1走到n,则返回-1。
int bellmanFord(){
Arrays.fill(dist, 0x3f3f3f3f);
//memset(dist, 0x3f, sizeof dist);
dist[1] = 0;
// 如果第n次迭代仍然会松弛三角不等式,就说明存在一条长度是n+1的最短路径,由抽屉原理,路径中至少存在两个相同的点,说明图中存在负权回路。
for (int i = 0; i < n; i ++ ){
for (int j = 0; j < m; j ++ ){
int a = edges[j].a, b = edges[j].b, w = edges[j].w;
// if (dist[b] > dist[a] + w)
// dist[b] = dist[a] + w;
dist[b] = Math.min(dist[b], dist[a] + w);
}
}
if (dist[n] > 0x3f3f3f3f / 2) return -1;
return dist[n];
}
spfa 算法
**spfa算法本质上队列优化的Bellman-Ford算法。**使用spfa算法的前提是不能存在负环,当存在负环的时候使用Bellman-Ford算法。
时间复杂度 平均情况下 ,最坏情况下 , n 表示点数,m 表示边数
int n; // 总点数
int h = new int[N], w = new int[N], e = new int[N], ne = new int[N]; // 邻接表存储所有边
int idx;
int[] dist = new int[N]; // 存储每个点到1号点的最短距离
boolean st = new boolean[N]; // 存储每个点是否在队列中
// 求1号点到n号点的最短路距离,如果从1号点无法走到n号点则返回-1
int spfa(){
Arrays.fill(dist, 0x3f3f3f3f);
//memset(dist, 0x3f, sizeof dist);
dist[1] = 0;
Queue<Integer> q = new LinkedList<>();
q.push(1);
st[1] = true;
while (q.size() != 0){
Integer t = q.front();
q.pop();
st[t] = false;
for (int i = h[t]; i != -1; i = ne[i]){
int j = e[i];
if (dist[j] > dist[t] + w[i]){
dist[j] = dist[t] + w[i];
if (st[j] != true){ // 如果队列中已存在j,则不需要将j重复插入
q.push(j);
st[j] = true;
}
}
}
}
if (dist[n] == 0x3f3f3f3f) return -1;
return dist[n];
}
spfa判断图中是否存在负环
时间复杂度是 , n 表示点数,m 表示边数
int n; // 总点数
int h[N], w[N], e[N], ne[N], idx; // 邻接表存储所有边
int dist[N], cnt[N]; // dist[x]存储1号点到x的最短距离,cnt[x]存储1到x的最短路中经过的点数
bool st[N]; // 存储每个点是否在队列中
// 如果存在负环,则返回true,否则返回false。
bool spfa(){
// 不需要初始化dist数组
// 原理:如果某条最短路径上有n个点(除了自己),那么加上自己之后一共有n+1个点,由抽屉原理一定有两个点相同,所以存在环。
queue<int> q;
for (int i = 1; i <= n; i ++ )
{
q.push(i);
st[i] = true;
}
while (q.size())
{
auto t = q.front();
q.pop();
st[t] = false;
for (int i = h[t]; i != -1; i = ne[i])
{
int j = e[i];
if (dist[j] > dist[t] + w[i])
{
dist[j] = dist[t] + w[i];
cnt[j] = cnt[t] + 1;
if (cnt[j] >= n) return true; // 如果从1号点到x的最短路中包含至少n个点(不包括自己),则说明存在环
if (!st[j])
{
q.push(j);
st[j] = true;
}
}
}
}
return false;
}
floyd算法
时间复杂度是 , n 表示点数
final static int INF = 0x3f3f3f3f;
//初始化:
for (int i = 1; i <= n; i ++)
for (int j = 1; j <= n; j ++)
if (i == j) d[i][j] = 0;
else d[i][j] = INF;
// 算法结束后,d[a][b]表示a到b的最短距离
void floyd(){
for (int k = 1; k <= n; k ++)
for (int i = 1; i <= n; i ++)
for (int j = 1; j <= n; j ++)
d[i][j] = min(d[i][j], d[i][k] + d[k][j]);
}
最小生成树
最小生成树的定义:最小生成树是在一个给定的无向图G(V,E)中求一颗树T,使得这棵树拥有图G中所有顶点,且所有边都是来自图G中的边,并且满足整棵树的边权之和最小。
问题种类:铺设公路
两种解决算法
-
普利姆算法(prim)
- 朴素版Prim算法(稠密图),时间复杂度O(n^2)
堆优化版Prim算法(稀疏图),时间 复杂度O(mlogn)(很少使用)
-
克鲁斯卡尔算法(Kruskal)(稀疏图)
朴素版prim算法
时间复杂度是 , n 表示点数,m 表示边数
static int n; // n表示点数
static int INF = 0x3f3f3f3f;
static int[][] g = new int[N][N]; // 邻接矩阵,存储所有边
static int[] dist = new int[N]; // 存储其他点到当前最小生成树(集合)的距离
static boolean st = new int[N]; // 存储每个点是否已经在生成树中
// 如果图不连通,则返回INF(值是0x3f3f3f3f), 否则返回最小生成树的树边权重之和
int prim(){
Arrays.fill(dist, 0x3f);
//memset(dist, 0x3f, sizeof dist);
//所有生成树里的边的长度之和的最小值
int res = 0;
for (int i = 0; i < n; i ++ ){
int t = -1;
for (int j = 1; j <= n; j ++ ) {
if (st[j] != false && (t == -1 || dist[t] > dist[j])) {
t = j;
}
}
if (i != 0 && dist[t] == INF) return INF;
if (i != 0) res += dist[t];
st[t] = true;
for (int j = 1; j <= n; j ++ ) dist[j] = min(dist[j], g[t][j]);
}
return res;
}
Kruskal算法
时间复杂度是 , n 表示点数,m 表示边数
思路:
-
将所有边按权重从小到大排序;
-
枚举每条边a,b,权重c
if (a,b 不连通) 将这条边加入集合中
int n, m; // n是点数,m是边数
int[] p = new int[N]; // 并查集的父节点数组
class Edge // 存储边
{
int a, b, w;
Edge(int a, int b, int c) {
this.a = a;this.b = b; this.c = c;
}
};
int find(int x){ // 并查集核心操作
if (p[x] != x) p[x] = find(p[x]);
return p[x];
}
public int kruskal(){
//sort(edges, edges + m);
Arrays.sort(edge, new Comparator<Edge>() {
@Override
public int compare(Edge o1, Edge o2) {
if(o1.w<o2.w) return -1;
else if(o1.w>o2.w) return 1;
else return 0;
}
});
for (int i = 1; i <= n; i ++ ) p[i] = i; // 初始化并查集
int res = 0, cnt = 0;
for (int i = 0; i < m; i ++ ) {
int a = edges[i].a, b = edges[i].b, w = edges[i].w;
a = find(a);
b = find(b);
if (a != b) { // 如果两个连通块不连通,则将这两个连通块合并
p[a] = b;
res += w;
cnt ++ ;
}
}
if (cnt < n - 1) return INF;
return res;
}
二分图
两种算法
- 染色法,O(n + m)
- 匈牙利算法,O(mn),实际运行时间一般远小于O(mn)
染色法判别二分图
时间复杂度是 , n 表示点数,m 表示边数
int n; // n表示点数
int h[N], e[M], ne[M], idx; // 邻接表存储图
int color[N]; // 表示每个点的颜色,-1表示未染色,0表示白色,1表示黑色
// 参数:u表示当前节点,c表示当前点的颜色
bool dfs(int u, int c) {
color[u] = c;
for (int i = h[u]; i != -1; i = ne[i])
{
int j = e[i];
if (color[j] == -1)
{
if (!dfs(j, !c)) return false;
}
else if (color[j] == c) return false;
}
return true;
}
bool check(){
memset(color, -1, sizeof color);
bool flag = true;
for (int i = 1; i <= n; i ++ )
if (color[i] == -1)
if (!dfs(i, 0))
{
flag = false;
break;
}
return flag;
}
匈牙利算法
时间复杂度是 , n 表示点数,m 表示边数
int n1, n2; // n1表示第一个集合中的点数,n2表示第二个集合中的点数
int h[N], e[M], ne[M], idx; // 邻接表存储所有边,匈牙利算法中只会用到从第一个集合指向第二个集合的边,所以这里只用存一个方向的边
int match[N]; // 存储第二个集合中的每个点当前匹配的第一个集合中的点是哪个
bool st[N]; // 表示第二个集合中的每个点是否已经被遍历过
bool find(int x){
for (int i = h[x]; i != -1; i = ne[i])
{
int j = e[i];
if (!st[j])
{
st[j] = true;
if (match[j] == 0 || find(match[j]))
{
match[j] = x;
return true;
}
}
}
return false;
}
// 求最大匹配数,依次枚举第一个集合中的每个点能否匹配第二个集合中的点
int res = 0;
for (int i = 1; i <= n1; i ++ ){
memset(st, false, sizeof st);
if (find(i)) res ++ ;
}