搜索与图论
一.构建图
List<Integer>[] --普通图
List<int[]> --边
List<int[]>[] --有权图
二.DFS与BFS
1.DFS
// 防止重复遍历同一个节点,若果输入的图是无环的可以不要
boolean[] visited;
// 从节点 s 开始 DFS 遍历,将遍历过的节点标记为 true
void traverse(List<Integer>[] graph, int s) {
if (visited[s]) {
return;
}
/* 前序遍历代码位置 */
// 将当前节点标记为已遍历
visited[s] = true;
for (int t : graph[s]) {
traverse(graph, t);
}
/* 后序遍历代码位置 */
}
2.BFS
// 计算从起点 start 到终点 target 的最近距离
int BFS(Node start, Node target) {
Queue<Node> q; // 核心数据结构
Set<Node> visited; // 避免走回头路
q.offer(start); // 将起点加入队列
visited.add(start);
while (q not empty) {
int sz = q.size();
/* 将当前队列中的所有节点向四周扩散 */
for (int i = 0; i < sz; i++) {
Node cur = q.poll();
/* 划重点:这里判断是否到达终点 */
if (cur is target)
return step;
/* 将 cur 的相邻节点加入队列 */
for (Node x : cur.adj()) {
if (x not in visited) {
q.offer(x);
visited.add(x);
}
}
}
}
// 如果走到这里,说明在图中没有找到目标节点
}
3.双向BFS
class Solution {
Queue<String> q1 = new LinkedList<>();
Queue<String> q2 = new LinkedList<>();
HashMap<String, Integer> m1 = new HashMap<>();
HashMap<String, Integer> m2 = new HashMap<>();
HashSet<String> set;
public int ladderLength(String beginWord, String endWord, List<String> wordList) {
set = new HashSet<>(wordList);
if (!set.contains(endWord)) {
return 0;
}
int res = bfs(beginWord, endWord);
return res == -1 ? 0 : res + 1;
}
int bfs(String beginWord, String endWord) {
q1.offer(beginWord);
m1.put(beginWord, 0);
q2.offer(endWord);
m2.put(endWord, 0);
while (!q1.isEmpty() && !q2.isEmpty()) {
int t = -1;
if (q1.size() <= q2.size()) {
t = update(q1, m1, m2);
} else {
t = update(q2, m2, m1);
}
if (t != -1) {
return t;
}
}
return -1;
}
int update(Queue<String> q, HashMap<String, Integer> cur, HashMap<String, Integer> other) {
for (int k = 0; k < q.size(); k++) {
String poll = q.poll();
for (int i = 0; i < poll.length(); i++) {
for (int j = 0; j < 26; j++) {
String str = poll.substring(0, i) + String.valueOf((char) ('a' + j)) + poll.substring(i + 1);
if (set.contains(str)) {
if (cur.containsKey(str)) {
continue;
}
if (other.containsKey(str)) {
return cur.get(poll) + 1 + other.get(str);
} else {
q.offer(str);
cur.put(str, cur.get(poll) + 1);
}
}
}
}
}
return -1;
}
}
三.拓扑排序
1.DFS实现拓扑排序
class Solution {
boolean[] visited ;
boolean[] onPath ;
boolean ok = false;
List<Integer> path = new ArrayList<>();
public int[] findOrder(int numCourses, int[][] prerequisites) {
List<Integer>[] graph = buildGrahp(numCourses,prerequisites);
visited = new boolean[numCourses];
onPath = new boolean[numCourses];
for(int i = 0;i<numCourses;i++){
traverse(graph,i);
}
if(ok){
return new int[]{};
}
Collections.reverse(path);
int[] res = new int[numCourses];
for(int i = 0;i<path.size();i++){
res[i] = path.get(i);
}
return res;
}
public void traverse(List<Integer>[] graph,int s){
if(onPath[s]){
ok=true;
}
if(visited[s]||ok){
return;
}
visited[s] = true;
onPath[s] = true;
for(int t : graph[s]){
traverse(graph,t);
}
path.add(s);
onPath[s] = false;
}
public List<Integer>[] buildGrahp(int numCourses,int[][] prerequisites){
List<Integer>[] graph = new ArrayList[numCourses];
Arrays.setAll(graph,i->new ArrayList<Integer>());
for(int[] e : prerequisites){
int from = e[1];
int to = e[0];
graph[from].add(to);
}
return graph;
}
}
2.BFS实现拓扑排序
class Solution {
int[] res ;
public int[] findOrder(int numCourses, int[][] prerequisites) {
List<Integer>[] graph = buildGrahp(numCourses,prerequisites);
Queue<Integer> q = new LinkedList<>();
int[] indegree = new int[numCourses];
res = new int[numCourses];
for(int[] e : prerequisites){
int from = e[1];
int to = e[0];
indegree[to]++;
}
for(int i = 0;i<numCourses;i++){
if(indegree[i]==0){
q.offer(i);
}
}
int count = 0;
while(!q.isEmpty()){
int cur = q.poll();
res[count++] = cur;
for(int next : graph[cur]){
indegree[next]--;
if(indegree[next]==0){
q.offer(next);
}
}
}
if(count!=numCourses){
return new int[]{};
}
return res;
}
public List<Integer>[] buildGrahp(int numCourses,int[][] prerequisites){
List<Integer>[] graph = new ArrayList[numCourses];
Arrays.setAll(graph,i->new ArrayList<Integer>());
for(int[] e : prerequisites){
int from = e[1];
int to = e[0];
graph[from].add(to);
}
return graph;
}
}
四.最短路
1.堆优化的Dijkstra
class State {
// 图节点的 id
int id;
// 从 start 节点到当前节点的距离
int distFromStart;
State(int id, int distFromStart) {
this.id = id;
this.distFromStart = distFromStart;
}
}
// 输入一个起点 start,计算从 start 到其他节点的最短距离
int[] dijkstra(int start, List<int[]>[] graph) {
// 定义:distTo[i] 的值就是起点 start 到达节点 i 的最短路径权重
int[] distTo = new int[graph.length];
Arrays.fill(distTo, Integer.MAX_VALUE);
// base case,start 到 start 的最短距离就是 0
distTo[start] = 0;
// 优先级队列,distFromStart 较小的排在前面
Queue<State> pq = new PriorityQueue<>((a, b) -> {
return a.distFromStart - b.distFromStart;
});
// 从起点 start 开始进行 BFS
pq.offer(new State(start, 0));
while (!pq.isEmpty()) {
State curState = pq.poll();
int curNodeID = curState.id;
int curDistFromStart = curState.distFromStart;
// // 在这里加一个判断就行了,其他代码不用改
// if (curNodeID == end) {
// return curDistFromStart;
// }
if (curDistFromStart > distTo[curNodeID]) {
continue;
}
// 将 curNode 的相邻节点装入队列
for (int[] neighbor : graph[curNodeID]) {
int nextNodeID = neighbor[0];
int distToNextNode = distTo[curNodeID] + neighbor[1];
// 更新 dp table
if (distTo[nextNodeID] > distToNextNode) {
distTo[nextNodeID] = distToNextNode;
pq.offer(new State(nextNodeID, distToNextNode));
}
}
}
return distTo;
}
2.Bellman-Ford
Bellman - ford算法擅长解决有边数限制的最短路问题。
public static void bellman_ford() {
Arrays.fill(dist, INF);
dist[1] = 0;
for (int i = 0; i < k; i ++) {
//备份dist数组
backup = Arrays.copyOf(dist, n + 1);
// m是边数
for (int j = 0; j < m; j ++) {
// node代替点与点的连接关系,用数组也可以
Node node = list[j];
int x = node.x;
int y = node.y;
int z = node.z;
dist[y] = Math.min(dist[y], backup[x] + z);
}
}
if (dist[n] > INF / 2) {
System.out.println("impossible");
} else {
System.out.println(dist[n]);
}
}
例题:
import java.io.*;
import java.util.*;
class Main {
private static int N = 510;
private static int M = 10010;
private static int[] backup = new int[N]; // 用于备份之前迭代的dist数组
private static int[] dist = new int[N]; // 从1号点到 n号点的距离
private static Node[] list = new Node[M]; // 结构体
private static int n; // 总点数
private static int m; // 总边数
private static int k; // 最多经过k条边
private static int INF = 0x3f3f3f3f;
public static void main(String[] args) throws IOException {
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in));
String[] str1 = bufferedReader.readLine().split(" ");
n = Integer.parseInt(str1[0]);
m = Integer.parseInt(str1[1]);
k = Integer.parseInt(str1[2]);
for (int i = 0; i < m; i ++) {
String[] str2 = bufferedReader.readLine().split(" ");
int x = Integer.parseInt(str2[0]);
int y = Integer.parseInt(str2[1]);
int z = Integer.parseInt(str2[2]);
list[i] = new Node(x, y, z);
}
bellman_ford();
}
public static void bellman_ford() {
Arrays.fill(dist, INF);
dist[1] = 0;
for (int i = 0; i < k; i ++) {
//备份dist数组
backup = Arrays.copyOf(dist, n + 1);
for (int j = 0; j < m; j ++) {
Node node = list[j];
int x = node.x;
int y = node.y;
int z = node.z;
dist[y] = Math.min(dist[y], backup[x] + z);
}
}
if (dist[n] > INF / 2) {
System.out.println("impossible");
} else {
System.out.println(dist[n]);
}
}
}
class Node
{
int x, y, z;
public Node(int x,int y,int z)
{
this.x = x;
this.y = y;
this.z = z;
}
}
3.SPFA
class Solution {
private int INF = 0x3f3f3f3f;
public int networkDelayTime(int[][] times, int n, int k) {
// spfa
int[][] g = new int[n + 1][n + 1];
for (int i = 1; i <= n; i++) {
Arrays.fill(g[i], INF);
}
for (int[] time : times) {
g[time[0]][time[1]] = time[2];
}
int[] dist = new int[n + 1];
Arrays.fill(dist, INF);
dist[k] = 0;
spfa(g, dist, n, k);
int ans = 0;
for (int i = 1; i <= n; i++) {
ans = Math.max(ans, dist[i]);
}
return ans >= INF ? -1 : ans;
}
private void spfa(int[][] g, int[] dist, int n, int k) {
boolean[] inque = new boolean[n + 1];
Queue<Integer> queue = new LinkedList<>();
queue.offer(k);
inque[k] = true;
while (!queue.isEmpty()) {
int i = queue.poll();
inque[i] = false;
for (int j = 1; j <= n; j++) {
int tmp = dist[i] + g[i][j];
if (tmp < dist[j]) {
dist[j] = tmp;
if (!inque[j]) {
queue.offer(j);
inque[j] = true;
}
}
}
}
}
}
4.Floyd
class Solution {
private int INF = 0x3f3f3f3f;
public int networkDelayTime(int[][] times, int n, int k) {
// floyd
int[][] g = new int[n + 1][n + 1];
for (int i = 1; i <= n; i++) {
Arrays.fill(g[i], INF);
}
for (int[] time : times) {
g[time[0]][time[1]] = time[2];
}
int[][] dist = g;
dist[k][k] = 0;
floyd(dist, n);
int ans = 0;
for (int i = 1; i <= n; i++) {
ans = Math.max(ans, dist[k][i]);
}
return ans >= INF ? -1 : ans;
}
private void floyd(int[][] dist, int n) {
for (int x = 1; x <= n; x++) {
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
dist[i][j] = Math.min(dist[i][j], dist[i][x] + dist[x][j]);
}
}
}
}
}
五.最小生成树
1.Kruskal算法
int minCostConnectPoints(int[][] points) {
int n = points.length;
// 生成所有边及权重
List<int[]> edges = new ArrayList<>();
for (int i = 0; i < n; i++) {
for (int j = i + 1; j < n; j++) {
int xi = points[i][0], yi = points[i][1];
int xj = points[j][0], yj = points[j][1];
// 用坐标点在 points 中的索引表示坐标点
edges.add(new int[] {
i, j, Math.abs(xi - xj) + Math.abs(yi - yj)
});
}
}
// 将边按照权重从小到大排序
Collections.sort(edges, (a, b) -> {
return a[2] - b[2];
});
// 执行 Kruskal 算法
int mst = 0;
UF uf = new UF(n);
for (int[] edge : edges) {
int u = edge[0];
int v = edge[1];
int weight = edge[2];
// 若这条边会产生环,则不能加入 mst
if (uf.connected(u, v)) {
continue;
}
// 若这条边不会产生环,则属于最小生成树
mst += weight;
uf.union(u, v);
}
return mst;
}
class UF {
// 连通分量个数
private int count;
// 存储每个节点的父节点
private int[] parent;
// n 为图中节点的个数
public UF(int n) {
this.count = n;
parent = new int[n];
for (int i = 0; i < n; i++) {
parent[i] = i;
}
}
// 将节点 p 和节点 q 连通
public void union(int p, int q) {
int rootP = find(p);
int rootQ = find(q);
if (rootP == rootQ)
return;
//可以颠倒
parent[rootQ] = rootP;
// 两个连通分量合并成一个连通分量
count--;
}
// 判断节点 p 和节点 q 是否连通
public boolean connected(int p, int q) {
int rootP = find(p);
int rootQ = find(q);
return rootP == rootQ;
}
public int find(int x) {
if (parent[x] != x) {
parent[x] = find(parent[x]);
}
return parent[x];
}
// 返回图中的连通分量个数
public int count() {
return count;
}
}
2.Prim
六.二分图
1.染色法
class Solution {
// 记录图是否符合二分图性质
private boolean ok = true;
// 记录图中节点的颜色,false 和 true 代表两种不同颜色
private boolean[] color;
// 记录图中节点是否被访问过
private boolean[] visited;
// 主函数,输入邻接表,判断是否是二分图
public boolean isBipartite(int[][] graph) {
int n = graph.length;
color = new boolean[n];
visited = new boolean[n];
// 因为图不一定是联通的,可能存在多个子图
// 所以要把每个节点都作为起点进行一次遍历
// 如果发现任何一个子图不是二分图,整幅图都不算二分图
for (int v = 0; v < n; v++) {
if (!visited[v]) {
traverse(graph, v);
}
}
return ok;
}
// DFS 遍历框架
private void traverse(int[][] graph, int v) {
// 如果已经确定不是二分图了,就不用浪费时间再递归遍历了
if (!ok) return;
visited[v] = true;
for (int w : graph[v]) {
if (!visited[w]) {
// 相邻节点 w 没有被访问过
// 那么应该给节点 w 涂上和节点 v 不同的颜色
color[w] = !color[v];
// 继续遍历 w
traverse(graph, w);
} else {
// 相邻节点 w 已经被访问过
// 根据 v 和 w 的颜色判断是否是二分图
if (color[w] == color[v]) {
// 若相同,则此图不是二分图
ok = false;
return;
}
}
}
}
}
2.匈牙利算法
匈牙利算法在解决二分图最大匹配问题中的应用