本文包括4大部分,图基础、最短路径、最小生成树、二分图
一、图基础包含, DFS、BFS、拓扑排序
二、最短路径包含,1.朴素Dijkstra, 2.堆优化Dijksta, 3.Bellman-ford,4.SPFA最短路径,5.SPFA判断负环, 6. Floyd最短路径
三、最小生成树包含,1. Prim, 2.Kruskal
四、1.染色法判定二分图,2.匈牙利算法
一、图基础
DFS和BFS均用邻接表来访问, vi数组记录已访问节点
拓扑排序不需要vi数组记录, 基于inDgree数组来记录入度值, 为0则入自定义队列q来进行BFS, 如果tt + 1 == n,证明入队了n个节点, 完成排序,否则存在环
1.DFS 题目: 树的重心
import java.io.*;
import java.util.*;
/**
* 图的临接表遍历
* 1.定义临接表adj, 访问数组vi
* 2.dfs遍历输入当前节点dfs(node), for循环node为头的单链表节点j
* 如果j未被访问,则dfs(j)
*/
class Main{
static BufferedReader read = new BufferedReader(new InputStreamReader(System.in));
static int N = 100010, idx, n;//二叉树n个节点
static int[] e = new int[N * 2], ne = new int[N * 2], adj = new int[N]; //临接表
static int ans = 0x3f3f3f3f;
static boolean[] vi = new boolean[N];
public static int dfs(int node){
vi[node] = true;
int sum = 1, res = 0;
for(int i = adj[node]; i != -1; i = ne[i]){
int next = e[i];
if(!vi[next]){
int size = dfs(next);
res = Math.max(size, res);
sum += size;
}
}
res = Math.max(res, n - sum); //求以node节点为删除节点后,各个连通块的最大值
ans = Math.min(ans, res);
return sum;
}
public static void main(String[] args) throws Exception{
n = Integer.valueOf(read.readLine());
Arrays.fill(adj, -1);
for(int i = 1; i < n; i++){
String[] s = read.readLine().split(" ");
int a = Integer.valueOf(s[0]);
int b = Integer.valueOf(s[1]);
add(a, b); add(b, a);//构建临接表
}
dfs(1);
System.out.println(ans);
}
public static void add(int a, int b){
e[idx] = b; ne[idx] = adj[a]; adj[a] = idx++;
}
}
2.BFS 图的层次遍历
import java.io.*;
import java.util.*;
class Main{
static BufferedReader read = new BufferedReader(new InputStreamReader(System.in));
static int N = 100010, idx = 0, n, m;
static int[] e = new int[N * 2], ne = new int[N * 2], adj = new int[N];//邻接表
static int[] dist = new int[N];
static boolean[] vi = new boolean[N];
public static void add(int a, int b){
e[idx] = b; ne[idx] = adj[a]; adj[a] = idx++;
}
public static int bfs(){
Queue<Integer> q = new LinkedList();
q.offer(1);
Arrays.fill(dist, 0x3f3f3f3f);
dist[1] = 0;
while(!q.isEmpty()){
int poll = q.poll();
vi[poll] = true;
for(int i = adj[poll]; i != -1; i = ne[i]){
int next = e[i];
if(!vi[next]){
q.offer(next);
dist[next] = Math.min(dist[next], dist[poll] + 1);
}
}
}
if(dist[n] == 0x3f3f3f3f) return -1;
return dist[n];
}
public static void main(String[] args) throws Exception{
String[] s = read.readLine().split(" ");
n = Integer.valueOf(s[0]); m = Integer.valueOf(s[1]);
Arrays.fill(adj, -1);
while(m -- > 0){
s = read.readLine().split(" ");
int a = Integer.valueOf(s[0]);
int b = Integer.valueOf(s[1]);
add(a, b);
}
System.out.println(bfs());
}
}
3.有向图的拓扑序列 有向图的拓扑序列
import java.util.*;
import java.io.*;
class Main{
static BufferedReader read = new BufferedReader(new InputStreamReader(System.in));
static int N = 100010, idx = 0, n, m;
static int[] e = new int[N * 2], ne = new int[N * 2], adj = new int[N];
static int[] q = new int[N];
static int[] inDegree = new int[N];
public static boolean tpsort(){
int hh = 0, tt = -1;
for(int i = 1; i <= n; i++){
if(inDegree[i] == 0) q[++tt] = i;
}
while(hh <= tt){
int poll = q[hh++];
for(int i = adj[poll]; i != -1; i = ne[i]){
int next = e[i];
inDegree[next]--;
if(inDegree[next] == 0) q[++tt] = next;
}
}
return tt + 1 == n;
}
public static void main(String[] args) throws Exception{
String[] s = read.readLine().split(" ");
n = Integer.valueOf(s[0]);
m = Integer.valueOf(s[1]);
Arrays.fill(adj, -1);
while(m -- > 0){
s = read.readLine().split(" ");
int a = Integer.valueOf(s[0]);
int b = Integer.valueOf(s[1]);
add(a, b);
inDegree[b]++;
}
if(!tpsort()){
System.out.println("-1");
}else{
for(int i = 0; i < n; i++){
System.out.print(q[i] + " ");
}
}
}
public static void add(int a, int b){
e[idx] = b; ne[idx] = adj[a]; adj[a] = idx++;
}
}
二、最短路径
1.朴素Dijkstra
import java.util.*;
import java.io.*;
/**
* 算法: 贪心思想
* 1. 初始化距离为无穷大, 临接矩阵也初始化为无穷大
* 2. 找到距离最小的点,从该点出发更新到其他点的距离
* 重复1. 2
* 时间复杂度O(n ^ 2)
*/
class Main{
static BufferedReader read = new BufferedReader(new InputStreamReader(System.in));
static int N = 510;
static int[][] g = new int[N][N];
static int[] dis = new int[N];
static boolean[] vi = new boolean[N];
static int n, INF = 0x3f3f3f3f;
public static int dijkstra(){
Arrays.fill(dis, INF);
dis[1] = 0;
for(int i = 0; i < n; i++){
int minDisNode = -1;
for(int j = 1; j <= n; j++){
if(!vi[j] && (minDisNode == -1 || dis[j] < dis[minDisNode])){
minDisNode = j;
}
}
vi[minDisNode] = true;
for(int j = 1; j <= n; j++){
dis[j] = Math.min(dis[j], dis[minDisNode] + g[minDisNode][j]);
}
}
if(dis[n] == INF) return -1;
return dis[n];
}
public static void main(String[] args) throws Exception{
String[] s = read.readLine().split(" ");
n = Integer.valueOf(s[0]);
for(int i = 1;i <= n; i++) Arrays.fill(g[i], INF);
int m = Integer.valueOf(s[1]);
while(m -- > 0){
s = read.readLine().split(" ");
int a = Integer.valueOf(s[0]);
int b = Integer.valueOf(s[1]);
int w = Integer.valueOf(s[2]);
g[a][b] = Math.min(g[a][b], w);
}
System.out.println(dijkstra());
}
}
2.堆优化Dijksta
import java.io.*;
import java.util.*;
/**
* 算法思路: 贪心
* 1. 稀疏图用临接表
* 2. 维护一个小根堆,存放dist距离和节点node
* 3. 进入未确定最小距离的节点, 遍历临接表,往小根堆里放距离更新的new int[]{dist, node}
* 4. 重复循环步骤3
*/
class Main{
static BufferedReader read = new BufferedReader(new InputStreamReader(System.in));
static int N = 1000010;
static int[] e, ne, w, adj, dist; //临接表可以忽略重边和自环问题
static int idx = 0, n, INF = 0x3f3f3f3f;
static boolean[] vi;
public static void add(int a, int b, int c){
e[idx] = b; w[idx] = c; ne[idx] = adj[a]; adj[a] = idx++;
}
public static int dijkstra(){
PriorityQueue<int[]> pq = new PriorityQueue<>((o1, o2) -> o1[0] - o2[0]);
Arrays.fill(dist, INF);
pq.offer(new int[]{0,1});
while(!pq.isEmpty()){
int[] poll = pq.poll();
int distance = poll[0];
int node = poll[1];
if(vi[node]) continue;
vi[node] = true;
for(int i = adj[node]; i != -1; i = ne[i]){
int j = e[i];
if(dist[j] > distance + w[i]){
dist[j] = distance + w[i];
pq.offer(new int[]{dist[j], j});
}
}
}
if(dist[n] == INF) return -1;
return dist[n];
}
public static void main(String[] args) throws Exception{
e = new int[N]; ne = new int[N]; w = new int[N]; adj = new int[N];
dist = new int[N]; vi = new boolean[N];
String[] s = read.readLine().split(" ");
Arrays.fill(adj, -1);
n = Integer.valueOf(s[0]); int m = Integer.valueOf(s[1]);
while(m -- > 0){
s = read.readLine().split(" ");
int a = Integer.valueOf(s[0]);
int b = Integer.valueOf(s[1]);
int c = Integer.valueOf(s[2]);
add(a, b, c);
}
System.out.println(dijkstra());
}
}
3. Bellman-ford
import java.io.*;
import java.util.*;
class Main{
static class Edge{
int a, b, w;
Edge(int a, int b, int w){
this.a = a; this.b = b; this.w = w;
}
}
static int n, m, k;
static int M = 100010, N = 510, INF = 0x3f3f3f3f;
static Edge[] edges = new Edge[M];
static int[] dist = new int[N];
static int[] backup = new int[N];
static BufferedReader read = new BufferedReader(new InputStreamReader(System.in));
public static int bellmanFord(){
Arrays.fill(dist, INF);
dist[1] = 0;
for(int i = 0; i < k; i++){
backup = dist.clone();
for(int j = 0; j < m; j++){
int a = edges[j].a;
int b = edges[j].b;
int w = edges[j].w;
dist[b] = Math.min(dist[b], backup[a] + w);
}
}
if(dist[n] > INF / 2) return -1;
return dist[n];
}
public static void main(String[] args) throws Exception{
String[] s = read.readLine().split(" ");
n = Integer.valueOf(s[0]); m = Integer.valueOf(s[1]);
k = Integer.valueOf(s[2]);
for(int i = 0; i < m; i++){
s = read.readLine().split(" ");
int a = Integer.valueOf(s[0]);
int b = Integer.valueOf(s[1]);
int w = Integer.valueOf(s[2]);
edges[i] = new Edge(a, b, w);
}
int t = bellmanFord();
if(t == -1) System.out.println("impossible");
else System.out.println(t);
}
}
4.SPFA最短路径
import java.util.*;
import java.io.*;
class Main{
static BufferedReader read = new BufferedReader(new InputStreamReader(System.in));
static int N = 100010;
static int[] e = new int[N], ne = new int[N], w = new int[N], adj = new int[N];
static int n, m, INF = 0x3f3f3f3f, idx = 0;
static boolean[] vi = new boolean[N];
static int[] dist = new int[N];
public static void add(int a, int b, int c){
e[idx] = b; w[idx] = c; ne[idx] = adj[a]; adj[a] = idx++;
}
public static int spfa(){
Arrays.fill(dist, INF);
dist[1] = 0;
Queue<Integer> q = new LinkedList();
q.offer(1);
while(!q.isEmpty()){
int poll = q.poll();
vi[poll] = false;
for(int i = adj[poll]; i != -1; i = ne[i]){
int next = e[i];
if(dist[next] > dist[poll] + w[i]){
dist[next] = dist[poll] + w[i];
if(!vi[next]){
q.offer(next);
vi[next] = true;
}
}
}
}
if(dist[n] == INF) return -1;
return dist[n];
}
public static void main(String[] args) throws Exception{
String[] s = read.readLine().split(" ");
n = Integer.valueOf(s[0]);
m = Integer.valueOf(s[1]);
Arrays.fill(adj, -1);
while(m-- > 0){
s = read.readLine().split(" ");
int a = Integer.valueOf(s[0]);
int b = Integer.valueOf(s[1]);
int c = Integer.valueOf(s[2]);
add(a, b, c);
}
int tmp = spfa();
if(tmp == -1){
System.out.println("impossible");
}else{
System.out.println(tmp);
}
}
}
5.SPFA判断负环
算法分析
使用spfa算法解决是否存在负环问题
1、dist[x] 记录当前1到x的最短距离
2、cnt[x] 记录当前最短路的边数,初始每个点到1号点的距离为0,只要他能再走n步,即cnt[x] >= n,则表示该图中一定存在负环,由于从1到x至少经过n条边时,则说明图中至少有n + 1个点,表示一定有点是重复使用
3、若dist[j] > dist[t] + w[i],则表示从t点走到j点能够让权值变少,因此进行对该点j进行更新,并且对应cnt[j] = cnt[t] + 1,往前走一步
注意:该题是判断是否存在负环,并非判断是否存在从1开始的负环,因此需要将所有的点都加入队列中,更新周围的点
import java.io.*;
import java.util.*;
class Main{
static int N = 10010;
static int[] e = new int[N], ne = new int[N], adj = new int[N], w = new int[N];
static int n, m , idx = 0, INF = 0x3f3f3f3f;
static BufferedReader read = new BufferedReader(new InputStreamReader(System.in));
static boolean[] vi = new boolean[N];
static int[] dist = new int[N];
static int[] cnt = new int[N];
public static boolean spfa(){
Arrays.fill(dist, INF);
Queue<Integer> q = new LinkedList();
for(int i = 1; i <= n; i++){
q.offer(i); vi[i] = true;
}
while(!q.isEmpty()){
int poll = q.poll();
vi[poll] = false;
for(int i = adj[poll]; i != -1; i = ne[i]){
int next = e[i];
if(dist[next] > dist[poll] + w[i]){
cnt[next] = cnt[poll] + 1;
if(cnt[next] >= n) return true;
dist[next] = dist[poll] + w[i];
if(!vi[next]){
q.offer(next);
vi[next] = true;
}
}
}
}
return false;
}
public static void main(String[] args) throws Exception{
String[] s = read.readLine().split(" ");
n = Integer.valueOf(s[0]);
m = Integer.valueOf(s[1]);
Arrays.fill(adj, -1);
while(m -- > 0){
s = read.readLine().split(" ");
int a = Integer.valueOf(s[0]);
int b = Integer.valueOf(s[1]);
int c = Integer.valueOf(s[2]);
add(a, b, c);
}
if(spfa()){
System.out.println("Yes");
}else{
System.out.println("No");
}
}
public static void add(int a, int b, int c){
e[idx] = b; w[idx] = c; ne[idx] = adj[a]; adj[a] = idx++;
}
}
6. Floyd最短路径
import java.io.*;
import java.util.*;
class Main{
static BufferedReader read = new BufferedReader(new InputStreamReader(System.in));
static int n, m, k, N = 210;
static int[][] g = new int[N][N];
static int INF = 0x3f3f3f3f;
public static void floyd(){
for(int k = 1; k <= n; k++){
for(int i = 1; i <= n; i++){
for(int j = 1; j <= n; j++){
g[i][j] = Math.min(g[i][j], g[i][k] + g[k][j]);
}
}
}
}
public static void main(String[] args) throws Exception{
String[] s = read.readLine().split(" ");
n = Integer.valueOf(s[0]);
m = Integer.valueOf(s[1]);
k = Integer.valueOf(s[2]);
//初始化临接矩阵
for(int i = 1; i <= n; i++){
for(int j = 1; j <= n; j++){
if(i == j) g[i][j] = 0;
else g[i][j] = INF;
}
}
while(m-- > 0){
s = read.readLine().split(" ");
int a = Integer.valueOf(s[0]);
int b = Integer.valueOf(s[1]);
int c = Integer.valueOf(s[2]);
g[a][b] = Math.min(g[a][b], c);
}
floyd();
while(k-- > 0){
s = read.readLine().split(" ");
int a = Integer.valueOf(s[0]);
int b = Integer.valueOf(s[1]);
if(g[a][b] > INF/2) System.out.println("impossible");
else System.out.println(g[a][b]);
}
}
}
三、最小生成树
1. Prim
/*
S:当前已经在联通块中的所有点的集合
1. dist[i] = inf
2. for n 次
t<-S外离S最近的点
利用t更新S外点到S的距离
vi[t] = true
n次迭代之后所有点都已加入到S中
联系:Dijkstra算法是更新到起始点的距离,Prim是更新到集合S的距离
*/
import java.io.*;
import java.util.*;
class Main{
static BufferedReader read = new BufferedReader(new InputStreamReader(System.in));
static int N = 510, M = 200010, INF = 0x3f3f3f3f;
static int[][] g = new int[N][N];
static int n, m;
static int[] dist = new int[N];
static boolean[] vi = new boolean[N];
public static int prim(){
Arrays.fill(dist, INF);
int res = 0;
for(int i = 0; i < n; i++){
int node = -1;
for(int j = 1; j <= n; j++){
if(!vi[j] && (node == -1 || dist[node] > dist[j])){
node = j;
}
}
vi[node] = true;
//寻找离集合S最近的点, 如果距离是无穷大,证明不连通
if(i != 0 && dist[node] == INF) return INF;
//距离必须要先加,再更新,因为可能存在自环的情况
if(i != 0) res += dist[node];
for(int j = 1; j <= n; j++){
dist[j] = Math.min(dist[j], g[node][j]);
}
}
return res;
}
public static void main(String[] args) throws Exception{
String[] s = read.readLine().split(" ");
n = Integer.valueOf(s[0]); m = Integer.valueOf(s[1]);
for(int i = 1; i <= n; i++){
for(int j = 1; j <= n; j++){
if(i == j) g[i][j] = 0;
else g[i][j] = INF;
}
}
while(m-- > 0){
s = read.readLine().split(" ");
int a = Integer.valueOf(s[0]);
int b = Integer.valueOf(s[1]);
int c = Integer.valueOf(s[2]);
g[a][b] = g[b][a] = Math.min(g[a][b], c);
}
int t = prim();
if(t == INF) System.out.println("impossible");
else System.out.println(t);
}
}
2.Kruskal
/**
* O(mlogm) m 为边数
* 1.将所有边按权重从小到大排序
* 2.枚举每条边a,b 权重c
* if a,b 两点不连通
* 将a,b边加入集合中
*
* 注意:(1) 2操作是并查集操作
* (2) 需要使用变量cnt来记录加进集合的边数
* 若cnt < n - 1表示不能遍历所有点
*/
import java.util.*;
import java.io.*;
class Main{
static BufferedReader read = new BufferedReader(new InputStreamReader(System.in));
static int N = 100010, M = 200010, INF = 0x3f3f3f3f;
static int n, m, idx = 0;
static int[] father = new int[N];
public static int kruskal(int[][] edges){
Arrays.sort(edges, (o1,o2) -> o1[2] - o2[2]);
int cnt = 0, res = 0;
for(int i = 0; i < m; i++){
int a = edges[i][0]; int b = edges[i][1];
if(find(a) != find(b)){
union(a, b);
res += edges[i][2];
cnt ++;
}
}
if(cnt < n - 1) return INF;
return res;
}
public static int find(int n){
if(father[n] != n) father[n] = find(father[n]);
return father[n];
}
public static void union(int n1, int n2){
int f1 = find(n1); int f2 = find(n2);
if(f1 == f2) return;
father[f1] = f2;
}
public static void main(String[] args) throws Exception{
String[] s = read.readLine().split(" ");
n = Integer.valueOf(s[0]); m = Integer.valueOf(s[1]);
int[][] edges = new int[m][3];
for(int i = 0; i < N; i++) father[i] = i;
for(int i = 0; i < m; i++){
s = read.readLine().split(" ");
int a = Integer.valueOf(s[0]);
int b = Integer.valueOf(s[1]);
int c = Integer.valueOf(s[2]);
edges[i][0] = a; edges[i][1] = b; edges[i][2] = c;
}
int tmp = kruskal(edges);
if(tmp == INF) System.out.println("impossible");
else System.out.println(tmp);
}
}
四、二分图
1.染色法判定二分图
/**
* 染色法
* 将所有点分成两个集合,使得所有边只出现在集合之间,就是二分图
* 二分图:一定不含有奇数环, 不一定是连通图
* dfs版本
* 代码思路:
* 染色可以使用1和2区分不同颜色,用0表示未染色
* 遍历所有点,每次将未染色的点进行dfs, 默认染成1或者2
* 由于某个点染色成功不代表整个图就是二分图,因此只有某个点染色失败才能立刻break/return
* 染色失败相当于至少存在2个点染了相同的颜色
*/
import java.io.*;
import java.util.*;
class Main{
static BufferedReader read = new BufferedReader(new InputStreamReader(System.in));
static int N = 200010, n, m, idx;
static int[] e = new int[N], ne = new int[N], adj = new int[N];
static int[] vi = new int[N];
public static boolean dfs(int curNode, int color){
vi[curNode] = color;
for(int i = adj[curNode]; i != -1; i = ne[i]){
int nextNode = e[i];
if(vi[nextNode] == 0) {
if(!dfs(nextNode, 3 - color)) return false;
}else if(vi[nextNode] == color) return false;
}
return true;
}
public static void main(String[] args)throws Exception{
String[] s = read.readLine().split(" ");
n = Integer.valueOf(s[0]);
m = Integer.valueOf(s[1]);
Arrays.fill(adj, -1);
while(m-- >0){
s = read.readLine().split(" ");
int a = Integer.valueOf(s[0]);
int b = Integer.valueOf(s[1]);
add(a, b); add(b, a);
}
boolean flag = true;
for(int i = 1; i <= n; i++){
if(vi[i] == 0){
boolean rs = dfs(i, 1);
if(!rs) {
flag = false;
break;
}
}
}
if(flag) System.out.println("Yes");
else System.out.println("No");
}
public static void add(int a, int b){
e[idx] = b; ne[idx] = adj[a]; adj[a] = idx++;
}
}
2.匈牙利算法
要了解匈牙利算法必须先理解下面的概念:
匹配:在图论中,一个「匹配」是一个边的集合,其中任意两条边都没有公共顶点。
最大匹配:一个图所有匹配中,所含匹配边数最多的匹配,称为这个图的最大匹配。
下面是一些补充概念:
完美匹配:如果一个图的某个匹配中,所有的顶点都是匹配点,那么它就是一个完美匹配。
交替路:从一个未匹配点出发,依次经过非匹配边、匹配边、非匹配边…形成的路径叫交替路。
增广路:从一个未匹配点出发,走交替路,如果途径另一个未匹配点(出发的点不算),则这条交替 路称为增广路(agumenting path)。
import java.io.*;
import java.util.*;
class Main{
static BufferedReader read = new BufferedReader(new InputStreamReader(System.in));
static int N = 510, M = 200050;
static int[] e = new int[M], ne = new int[M], adj = new int[N];
static int idx = 0;
//vi[]数组我称为临时预定数组,vi[j]=a表示一轮模拟匹配中,女孩j被男孩a预定了。
static boolean[] vi = new boolean[N];
//match[j]=a,表示女孩j的现有配对男友是a
static int[] match = new int[N];
public static boolean find(int leftNode){
for(int i = adj[leftNode]; i != -1; i = ne[i]){
int rightNode = e[i];
if(!vi[rightNode]){ //右边点未被预定
vi[rightNode] = true;
//如果右边女孩没有男朋友,或者她原来的男朋友能够预定其他喜欢的女孩,匹配成功。
if(match[rightNode] == 0 || find(match[rightNode])){
match[rightNode] = leftNode;
return true;
}
}
}
//找不到右边能匹配的点
return false;
}
public static void main(String[] args) throws Exception{
String[] s = read.readLine().split(" ");
int n1 = Integer.valueOf(s[0]);
int n2 = Integer.valueOf(s[1]);
int m = Integer.valueOf(s[2]);
Arrays.fill(adj, -1);
while(m-- > 0){
s = read.readLine().split(" ");
int a = Integer.valueOf(s[0]);
int b = Integer.valueOf(s[1]);
add(a, b);
}
int res = 0;
for(int i = 1; i <= n1; i++){
Arrays.fill(vi, false);
if(find(i)) res++;
}
System.out.println(res);
}
public static void add(int a, int b){
e[idx] = b; ne[idx] = adj[a]; adj[a] = idx++;
}
}