不超过k条边,用Bellman-Ford
图的存储
无向图是一种特殊的有向图,即双向。
有向图有邻接矩阵、邻接表两种存储方式。所谓邻接矩阵就是二维数组,下标表示点,值表示权重。
邻接表就是每个节点上存放单链表。
数组h // 邻接表(存储的是以u为根的相邻一层的所有子节点)
数组e // 节点数组,存储节点编号
数组ne // nxt数组,存储每个单链表的下一个节点所属的序号
无向图的存储
static int N = 100010, M = 2 * N, idx;
static int h[] = new int[N];
static int e[] = new int[M];
static int ne[] = new int[M];
public static void add(int a, int b){
e[idx] = b;
ne[idx] = h[a];
h[a] = idx ++;
}
原先
无向图添加时,add(a,b)也要add(b,a),每一条边要建两个相反方向。
图的遍历
图的深度优先遍历
static int N = 100010, M = 2 * N, idx;
static int h[] = new int[N];
static int e[] = new int[M];
static int ne[] = new int[M];
static boolean st[] = new boolean[N];
public static void add(int a, int b){
e[idx] = b;
ne[idx] = h[a];
h[a] = idx ++;
}
//深度优先遍历图 u是当前节点,每个节点只被遍历一次,需要用到st标记数组
public static void dfs(int u){
st[u] = true; //标记一下,已经访问过了
//u的所有出边
for(int i = h[u]; i != -1; i = ne[i]){
int j = e[i];
if(!st[j])
dfs(j);
}
}
AcWing 846. 树的重心
【题目描述】
【思路】
枚举删除的节点,计算删除该节点之后剩余的所有点构成的所有连通块中点数的最大值。在所有最大值(每删除一个节点存在一个以该节点为“重心”的最大的连通块)中取最小值。
由于是计算子树的节点数可以使用深度优先遍历。
比如说删除值为4的节点后,计算出左子树(size(3))、右子树(size(6))的节点数,上面的子树就是n - size(4)。取上面联通的点数较大者。
时间复杂度: 不论是DFS还是BFS,由于点数只被遍历一次,所以时间复杂度为O(n + m)
import java.util.Scanner;
import java.lang.Math;
import java.util.Arrays;
public class Main{
static int N = 100010, M = 2 * N;
static int n, idx;
//有N个点,每个点都有一个链表(该链表存储与该点相邻的所有点)
//h[i] 存储编号为i的点,其链表的头结点
static int h[] = new int[N];
static int e[] = new int[M];
static int ne[] = new int[M];
static boolean st[] = new boolean[N];
static int ans = Integer.MAX_VALUE;
public static void add(int a, int b){
e[idx] = b;
ne[idx] = h[a];
h[a] = idx ++;
}
//深度优先遍历图 u是当前节点,每个节点只被遍历一次,需要用到st标记数组
public static int dfs(int u){
st[u] = true; //标记一下,已经访问过了
int sum = 1; //sum是整棵子树的点数
int res = 0; //res是删除u之后所有联通块点数的最大值
//u的所有出边
for(int i = h[u]; i != -1; i = ne[i]){
int j = e[i];
if(!st[j])
{
int s = dfs(j); //获得以j为根节点的子树的点数
//根节点是j的子树是根节点是u的树的一颗子树
res = Math.max(res, s);//断开u之后的连通块的点数
sum += s; // 加上每个子树的点数
}
}
res = Math.max(res, n - sum); // n - sum为剔除以u为根节点的子树的其他联通块的点数
ans = Math.min(ans, res);
return sum;
}
public static void main(String args[]){
Scanner reader =new Scanner(System.in);
n = reader.nextInt();
// 将每个链表的头结点初始化为-1
Arrays.fill(h, - 1);
for(int i = 0; i < n - 1; i ++){
int a = reader.nextInt(), b = reader.nextInt();
add(a, b);
add(b, a);
}
//初始化数组
dfs(1);
System.out.println(ans);
}
}
宽度优先搜索遍历图
import java.util.Scanner;
import java.util.Arrays;
public class Main{
static int idx;// 链表中节点的编号
static int n;
static int N = 100010; //节点数
static int M = 100010 * 2; //边数,无向图中,边数为 节点数*2 - 2
//有n个点 每个点都有自己的一条链表 用来存储于该点相邻的所有点
static int h[] = new int[N]; //链表头结点数组 h[i]存储 值为i的点的链表的头结点编号
static int e[] = new int[M]; // 节点值,对应输入的a或b
static int ne[] = new int[M]; //next域
static boolean st[] = new boolean[N]; // 该节点是否被访问过 保证每个节点只被遍历一次
static int ans = N; // 最大联通块的最小值
//往节点值为a的链表中插入b
public static void add(int a, int b){
e[idx] = b;
ne[idx] = h[a];
h[a] = idx;
idx ++;
}
//以u为根的树的节点数
public static int dfs(int u){
st[u] = true; //标记已经访问过了
int sum = 1; //以u为根的树(包括u)的节点数
int res = 0; // 删除u节点之后 连通块的最大数目
//u去掉后生成的各个子树节点数的最大值 u上面连通块的节点数
// i = h[u]
for(int i = h[u]; i != - 1; i = ne[i]){
int j = e[i];
if( !st[j] ){//编号为j的节点没有被访问过
int s = dfs(j); //u节点为根的单棵子树的节点数
res = Math.max(res, s);//连通块的最大数目
sum += s; //该连通块整棵树的一部分
}
}
res = Math.max(n - sum, res);// n - sum是u上面连通块的节点数 res取所有连通块的最大值
ans = Math.min(res, ans); //在所有联通块最大值 中取最小
return sum;
}
public static void main(String args[]){
Scanner reader = new Scanner(System.in);
n = reader.nextInt();
Arrays.fill(h, - 1); // 初始化每个单链表的头结点 -1表示尾结点
for(int i = 0; i < n - 1; i ++){
int a = reader.nextInt(), b = reader.nextInt();
add(a, b);
add(b, a);
}
dfs(1);
System.out.println(ans);
}
}
AcWing 847. 图中点的层次
【题目描述】
【思路】
用BFS对 图进行遍历
import java.util.*;
public class Main{
static int N = 100010, M = 100010;
static int h[] = new int[N];
static int d[] = new int[N]; //记录1号点到当前点的距离
static int e[] = new int[M];
static int ne[] = new int[M];
static int idx, n, m;
static void add(int a, int b){
e[idx] = b;
ne[idx] = h[a];
h[a] = idx++;
}
public static int bfs(){
Queue<Integer> q = new LinkedList<Integer>();
Arrays.fill(d, - 1);
q.offer(1); //1号点入队
d[1] = 0; //1号点到1号点的距离是1
while(! q.isEmpty()){
int t = q.poll();
//扩展t的邻边
for(int i = h[t]; i != - 1; i = ne[i]){
int j = e[i];
if( d[j] == -1){//没有被访问过
d[j] = d[t] + 1;//d[j]存储j号点离起点的距离,并标记为访问过
q.offer(j);
}
}
}
return d[n];
}
public static void main(String args[]){
Scanner reader = new Scanner(System.in);
n = reader.nextInt();
m = reader.nextInt();
Arrays.fill(h, - 1);
for(int i = 0; i < m; i ++)
{
int a = reader.nextInt(), b = reader.nextInt();
add(a, b);
}
System.out.println( bfs() );
}
}
左孩子右兄弟
AcWing 3422. 左孩子右兄弟
儿子放左边,兄弟放右边
import java.util.Scanner;
import java.util.Arrays;
class Main{
static int idx, N = 100010;
static int h [] = new int[N];
static int e [] = new int[N];
static int ne [] = new int[N];
static void add(int a, int b){ // 加边
e[idx] = b;
ne[idx] = h[a];
h[a] = idx++;
}
static int dfs(int u){
int cnt = 0; // 原多叉树第u层的节点数
int hmx = 0; // 原多叉树子树的最大高度
for(int i = h[u]; i != -1; i = ne[i]){
int j = e[i];
hmx = Math.max(hmx, dfs(j)); // 子树最大高度
cnt ++;
}
return hmx + cnt;
}
public static void main(String args[]){
Scanner reader = new Scanner(System.in);
int n = reader.nextInt();
// 初始化邻接表头结点
Arrays.fill(h, -1);
for(int i = 2; i <= n; i ++){
int p = reader.nextInt();
add(p, i);
}
System.out.println(dfs(1));
}
}
最短路径
Floyd 求最短路
通过上述for循环操作,可以实现将邻接矩阵转为最短路径矩阵。
import java.util.Scanner;
class Main{
static int N = 210;
static int d[][] = new int[N][N];
static int INF = 0x3f3f3f3f;
static int n;
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 ++){
d[i][j] = Math.min(d[i][j], d[i][k] + d[k][j]);
}
}
}
}
public static void main(String args[]){
Scanner reader = new Scanner(System.in);
n = reader.nextInt();
int m = reader.nextInt(), Q = reader.nextInt();
// 初始化:自己到自己为0,到其他节点为无穷(不可达)
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;
}
}
}
// 输入
while(m -- > 0){
int x =reader.nextInt(), y = reader.nextInt(), z = reader.nextInt();
if(x == y) { // 自环
d[x][y] = 0;
}else{
// 重边则选最短边,会有负权边,但是其保证图中不存在负权回路
d[x][y] = Math.min(d[x][y], z);
}
}
floyd();
while(Q-- > 0){
int x =reader.nextInt(), y = reader.nextInt();
// 有负权边,比如当INF+(-2)<INF时也会更新,此时还没有计算出最短路
if(d[x][y] > INF/2){
System.out.println("impossible");
}else{
System.out.println(d[x][y]);
}
}
}
}
单源最短路径
朴素Dijkstra
由于点数n^2 <= 边数m,为稠密图,采用邻接矩阵进行存储。
import java.util.Scanner;
class Main{
static int N = 510;
static int g[][] = new int[N][N]; // 邻接矩阵
static boolean s[] = new boolean[N]; // s集合: 已经找到了最短路的点集合
static int d[] = new int[N]; // 记录到1号点的最短距离
static int n;
static int INF = 0x3f3f3f3f;
public static void djistra(){
// 1号点最短距离为0
d[1] = 0;
// 遍历所有点
for(int i = 0; i < n; i ++){
int t = -1;
// 寻找:不在s集合的点中与1号点距离最近的点t
for(int j = 1; j <= n; j ++){
if(!s[j]&& (t == - 1 || d[t] > d[j])){
t = j;
}
}
// 将t加入s集合
s[t] = true;
// 用t更新其他点的距离
for(int j = 1; j <= n; j ++){
d[j] = Math.min(d[j], d[t] + g[t][j]);
}
}
}
public static void main(String args[]){
Scanner reader = new Scanner(System.in);
n = reader.nextInt();
int m = reader.nextInt();
// 初始化:各个点之间皆不可达
for(int i = 1; i <= n; i ++){
for(int j = 1; j <= n; j ++){
g[i][j] = INF;
}
d[i] = INF;
}
while(m -- > 0){
int a = reader.nextInt(), b = reader.nextInt(), c = reader.nextInt();
g[a][b] = Math.min(g[a][b], c); // 去重边和自环
}
djistra();
if(d[n] == INF){
System.out.println(-1);
}else{
System.out.println(d[n]);
}
}
}
Djistra II
AcWing 850. Dijkstra求最短路 II
n^2 >> m ,为稀疏图,采用邻接表进行存储。
堆优化:在一堆数中找最小值可以使用堆结构。查找操作为:O(1),修改操作为:O(n)。
图源小呆呆
import java.util.Scanner;
import java.util.Queue;
import java.util.PriorityQueue;
import java.util.Arrays;
class Main{
static int N = 200000;
static int h[] = new int[N]; // 头结点数组idx
static int e[] = new int[N]; // 节点编号数组
static int ne[] = new int[N]; // nxt指针数组(指向对应节点的idx)
static int idx;
static boolean s[] = new boolean[N]; // s集合: 已经找到了最短路的点集合
static int w[] = new int[N]; // 边权重
static int d[] = new int[N]; // 记录到1号点的最短距离
static int n;
static int INF = 0x3f3f3f3f;
public static void add(int a, int b, int c){
e[idx] = b;
w[idx] = c;
ne[idx] = h[a];
h[a] = idx;
idx++;
}
// 邻接表会有重复的节点存储
public static void djistra(){
d[1] = 0;
Queue<PIIs> q = new PriorityQueue<PIIs>();
q.offer(new PIIs(1, 0));
while(!q.isEmpty()){
PIIs p = q.poll();
int distance = p.getSecond(), t = p.getFirst();
if(s[t]) {
continue;
}
s[t] = true; // 加入s集合
// 更新所有与j相邻的节点的距离
for(int i = h[t]; i != -1; i = ne[i]){
int j = e[i]; // 节点编号
if(d[j] > distance + w[i]) {
//d[j] = distance + w[j]; 注意不是w[j]
d[j] = distance + w[i];
q.offer(new PIIs(j, d[j]));
}
}
}
}
public static void main(String args[]){
Scanner reader = new Scanner(System.in);
n = reader.nextInt();
int m = reader.nextInt();
// 初始化:各个点之间皆不可达
for(int i = 1; i <= n; i ++){
d[i] = INF;
}
Arrays.fill(h, -1); // 头结点全部初始化为指向-1
while(m -- > 0){
int a = reader.nextInt(), b = reader.nextInt(), c = reader.nextInt();
add(a, b, c);
}
djistra();
if(d[n] == INF){
System.out.println(-1);
}else{
System.out.println(d[n]);
}
}
}
class PIIs implements Comparable<PIIs>{
int first; // 结点编号
int second; //距离
public int getFirst(){
return this.first;
}
public int getSecond(){
return this.second;
}
public int compareTo(PIIs o){ // 升序排序
return this.second - o.second;
}
public PIIs(int first,int second){
this.first = first;
this.second = second;
}
}
负权边
Bellman-Ford 算法
当存在负权回路时,最短路径可能不存在。要求在其到n号点的路径上不能有负权回路,否则最短路径不存在。
此题中还有边数的限制,意味着即使存在负权回路也不会发生一直绕回路转,导致最短路径为负无穷的问题。
import java.util.Scanner;
import java.util.Arrays;
class Main{
static int N = 510, M = 10010;
static int d[] = new int[N]; // 最短距离
static int backup[] = new int[N]; // 备份数组
static Edge e[] = new Edge[M]; // 边
static int n, m, k;
static int INF = 0x3f3f3f3f;
public static int bellmanFord(){
// 初始化
Arrays.fill(d, INF);
d[1] = 0;
// 枚举k次选出k条边
for(int i = 0; i < k; i ++){
backup = Arrays.copyOf(d, n + 1);//由于是从1开始存到n
for(int j = 0; j < m; j ++) { // 选出一条边
int a = e[j].a, b = e[j].b, w = e[j].w;
// attention:用的是backup数组的
d[b] = Math.min(d[b], backup[a] + w);
}
}
if(d[n] > INF / 2) {
return -INF;
}
return d[n];
}
public static void main(String args[]){
Scanner reader = new Scanner(System.in);
n = reader.nextInt();
m = reader.nextInt();
k = reader.nextInt();
for(int i = 0; i < m; i ++){
int x = reader.nextInt(), y = reader.nextInt(), z = reader.nextInt();
// 从a到b的有向边,权重为w
e[i] = new Edge(x, y, z);
}
int t = bellmanFord();
if(t == -INF){
System.out.println("impossible");
}else{
System.out.println(t);
}
}
}
class Edge{
int a, b, w;
public Edge(int aa, int bb, int ww){
a = aa; b = bb; w = ww;
}
}
多源汇最小路径
最小生成树
import java.util.Scanner;
import java.util.Arrays;
class Main{
static int n;
static int N = 510, INF = 0x3f3f3f3f;
static int d[] = new int[N]; // 点到集合的距离
static int g[][] = new int[N][N]; // 邻接矩阵
static boolean s[] = new boolean[N];
public static int cruskal(){
// 初始化所有的点距离为无穷
Arrays.fill(d, INF);
int ans = 0;
for(int i = 0; i < n; i ++){
// 寻找:集合外距离集合最近的点,记为t
int t = -1;
for(int j = 1; j <= n; j ++){
if(!s[j] && (t == - 1 || d[t] > d[j])){
t = j;
}
}
// 不是第一个点且不可达,说明不存在最短路径
if(i != 0 && d[t] == INF){
return INF;
}
// 注意: 集合的最小路径要在更新前操作
if(i!= 0) {
ans += d[t];
}
// 更新其他点到集合的距离
for(int j = 1; j <= n; j ++){
d[j] = Math.min(d[j], g[t][j]);
}
s[t] = true;
}
return ans;
}
public static void main(String args[]){
Scanner reader = new Scanner(System.in);
n = reader.nextInt();
int m = reader.nextInt();
// 不要忽略初始化
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){
// 无向图
int a = reader.nextInt(),b = reader.nextInt(), c = reader.nextInt();
g[a][b] = Math.min(g[a][b], c);;
g[b][a] = g[a][b];
}
int k = cruskal();
if(k == INF){
System.out.println("impossible");
}else{
System.out.println(k);
}
}
}