第8章 更多精彩算法
第1节 镖局运镖–图的最小生成树(Kruskal)
从某一起点可以到达任意点,路径的总长度最短。
算法基本思想:首先选择最短的一条边,然后选择次短的边,…一直选择n-1条边。但是新加入的边不能构成回路,也就是说,如果边的两端顶点连通了,就不能再加入了。而判断两个点是否连通,可以使用并查集(7.4)。
//测试数据
6 9
2 4 11
3 5 13
4 6 3
5 6 4
2 3 6
4 5 7
1 2 1
3 4 9
1 3 2
//结果
19
package ch8;
import java.util.Scanner;
public class Test1 {
class edge{
int u;
int v;
int w;
}
edge[] e = new edge[10];
int n,m;//n个顶点,m条边
int[] f= new int[7];//f大小是n+1
int sum = 0;
int count = 0;
{
for(int i=0;i<e.length;i++) {
e[i] = new edge();
}
}
//快速排序
void quicksort(int left,int right) {
if(left>right) {
return;
}
int i = left;
int j = right;
while(i!=j) {
while(e[j].w>=e[left].w && i<j) {
j--;
}
while(e[i].w<=e[left].w && i<j) {
i++;
}
if(i<j) {
edge t = e[i];
e[i] = e[j];
e[j] = t;
}
}
edge t = e[left];
e[left] = e[i];
e[i] = t;
quicksort(left,i-1);
quicksort(i+1,right);
return;
}
//寻找祖先
int getf(int v) {
if(f[v]==v) {
return v;
}else {
f[v] = getf(f[v]);
return f[v];
}
}
int merge(int v,int u) {
int t1 = getf(v);
int t2 = getf(u);
if(t1!=t2) { //不在同一集合时合并,返回1
f[t2] = t1;
return 1;
}
return 0;
}
//main
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
Test1 t1 = new Test1();
t1.n = sc.nextInt();
t1.m = sc.nextInt();
for(int i=1;i<=t1.m;i++) {
t1.e[i].u = sc.nextInt();
t1.e[i].v = sc.nextInt();
t1.e[i].w = sc.nextInt();
}
//快速排序
t1.quicksort(1, t1.m);
//并查集初始化
for(int i=1;i<=t1.n;i++) {
t1.f[i] = i;
}
//Kruskal算法核心
for(int i=1;i<=t1.m;i++) {
if(t1.merge(t1.e[i].u, t1.e[i].v) == 1) {
t1.count++;
t1.sum += t1.e[i].w;
}
if(t1.count == t1.n-1) {
break;
}
}
System.out.println(t1.sum);
}
}
第2节 再谈最小生成树(Prim)
先随便选择一个点,然后选择一条最短的边,这样就有2个点连接在一起了。接着在从2个点的边里寻找最短的边,并且要能连接到没有连接的其他点。这样每次加入一个点。
package ch8;
import java.util.Scanner;
public class Test2 {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
final int inf = 999999;
int count = 0;
int sum = 0;
int n = sc.nextInt();
int m = sc.nextInt();
int book[] = new int[n+1]; //标记点是否在树中
int[] dis = new int[n+1]; //树到点的距离
int[][] e = new int[n+1][n+1];//存储图
//初始化e
for(int i=1;i<=n;i++) {
for(int j=1;j<=n;j++) {
if(i==j) {
e[i][j] = 0;
}else {
e[i][j] = inf;
}
}
}
//读入m条边
for(int i=1;i<=m;i++) {
int t1 = sc.nextInt();
int t2 = sc.nextInt();
int t3 = sc.nextInt();
e[t1][t2] = t3;
e[t2][t1] = t3;
}
//初始化dis
for(int i=1;i<=n;i++) {
dis[i]=e[1][i];
}
//Prim核心部分
book[1] = 1;
count++;
while(count<n) {
int min = inf;
int j=0;
for(int i=1;i<=n;i++) {
if(book[i]==0 && dis[i]<min) {
min = dis[i];
j = i;
}
}
book[j]=1;
count++;
sum +=dis[j];
//更新树到顶点的距离
for(int k=1;k<=n;k++) {
if(book[k]==0&& dis[k]>e[j][k]) {
dis[k] = e[j][k];
}
}
}
System.out.println(sum);
}
}
使用堆存储图的话,可以降低时间复杂度。
第3节 重要城市–图的割点
割点:能将图分成不同部分的点,去掉割点后图不再连通。
求割点的方法:从某个点开始进行深度优先遍历,遍历每个点,遍历到顶点u时,如果有未访问的顶点v在不经过u的情况下不能回到已经访问的点,则说明u是割点。
测试数据
6 7
1 4
1 3
4 2
3 2
2 5
2 6
5 6
结果
2
package ch8;
import java.util.Scanner;
public class Test3 {
int n,m;//n个顶点,m条边
int [][] e = new int[9][9];//使用e存储图,实际应该使用邻接表存储。
int root;
int [] num = new int[9]; //顶点时间戳
int [] low = new int[9]; //顶点能够访问到的最早时间戳
int [] flag = new int[9];//标记是否是割点
int index; //用来进行时间戳的递增
//求两个数中较小的数
int min(int a,int b) {
return a<b ? a : b;
}
//深度优先遍历
void dfs(int cur, int father) {
int child = 0; //在生成树中cur顶点的儿子数
index++; //时间戳递增
num[cur] =index;
low[cur] =index;
//枚举与cur相连的所有顶点i
for(int i=1;i<=n;i++) {
if(e[cur][i]==1) { //i和cur相连
if(num[i]==0) { //如果num[i]==0,说明i没被访问过
child++; //cur的儿子+1
dfs(i,cur); //继续深度优先遍历
//更新cur的最早时间戳
low[cur] = min(low[cur],low[i]);
//如果cur不是根节点且满足low[i]>=num[cur],则cur是割点
if(cur!=root && low[i]>=num[cur]) {
flag[cur]=1;
}
//如果cur是根节点且 有两个儿子,则这个根节点是割点
if(cur==root && child==2) {
flag[cur]=1;
}
}else if(i!=father) {
//i曾经被访问过,且i不是cur的父亲,则需要更新low[cur]
low[cur] = min(low[cur],num[i]);
}
}
}
}
//初始化图,读入边
public void init(Scanner sc) {
n = sc.nextInt();
m = sc.nextInt();
for(int i=1;i<=n;i++) {
for(int j=1;j<=n;j++) {
e[i][j] = 0;
}
}
for(int i=1;i<=m;i++) {
int x = sc.nextInt();
int y = sc.nextInt();
e[x][y] = 1;
e[y][x] = 1;
}
}
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
Test3 t = new Test3();
t.init(sc);
t.root = 1;
t.dfs(1,t.root); //从1号顶点开始深度优先遍历
for(int i=1;i<=t.n;i++) {
if(t.flag[i] ==1) {
System.out.print(" "+i);
}
}
}
}
第4节 关键道路–图的割边
只要将low[v]>=num[u] 改为ow[v]>num[u] 。相等时表示可以回到父亲,去掉等号表示连父亲都回不到了(也回不到祖先)。如果既不能回到父亲,也不能回到祖先,那么u-v这条边就是割边。
测试数据
6 6
1 4
1 3
4 2
3 2
2 5
5 6
结果
5 - 6
2 - 5
package ch8;
import java.util.Scanner;
public class Test3 {
int n,m;//n个顶点,m条边
int [][] e = new int[9][9];//使用e存储图,实际应该使用邻接表存储。
int root;
int [] num = new int[9]; //顶点时间戳
int [] low = new int[9]; //顶点能够访问到的最早时间戳
int [] flag = new int[9];//标记是否是割点
int index; //用来进行时间戳的递增
//求两个数中较小的数
int min(int a,int b) {
return a<b ? a : b;
}
//深度优先遍历
void dfs(int cur, int father) {
int child = 0; //在生成树中cur顶点的儿子数
index++; //时间戳递增
num[cur] =index;
low[cur] =index;
//枚举与cur相连的所有顶点i
for(int i=1;i<=n;i++) {
if(e[cur][i]==1) { //i和cur相连
if(num[i]==0) { //如果num[i]==0,说明i没被访问过
child++; //cur的儿子+1
dfs(i,cur); //继续深度优先遍历
//更新cur的最早时间戳
low[cur] = min(low[cur],low[i]);
//满足low[i]>num[cur],则cur-i是割边
if(low[i]>num[cur]) {
System.out.println(cur+" - "+i);
flag[cur]=1;
}
}else if(i!=father) {
//否则,i曾经被访问过,且i不是cur的父亲,则需要更新low[cur]
low[cur] = min(low[cur],num[i]);
}
}
}
}
//初始化图,读入边
public void init(Scanner sc) {
n = sc.nextInt();
m = sc.nextInt();
for(int i=1;i<=n;i++) {
for(int j=1;j<=n;j++) {
e[i][j] = 0;
}
}
for(int i=1;i<=m;i++) {
int x = sc.nextInt();
int y = sc.nextInt();
e[x][y] = 1;
e[y][x] = 1;
}
}
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
Test3 t = new Test3();
t.init(sc);
t.root = 1;
t.dfs(1,t.root); //从1号顶点开始深度优先遍历
}
}
第5节 我要做月老–二分图最大匹配
测试数据
6 5
1 4
1 5
2 5
2 6
3 4
结果
3
package ch8;
import java.util.Scanner;
public class Test5 {
int [][]e;
int [] match;
int [] book;
int n,m;
int sum;
int dfs(int u) {
for(int i=1;i<=n;i++) {
if(book[i]==0 && e[u][i] ==1) { //i没有访问过且 可以和u配对
book[i] = 1;
if(match[i]==0 || dfs(match[i])==1) { //i没有配对过,或match[i]找到了新的配对
match[i] = u;
match[u] = i;
return 1;
}
}
}
return 0;
}
void init() {
Scanner sc = new Scanner(System.in);
n = sc.nextInt();
m = sc.nextInt();
e = new int[n+1][n+1];
for(int i=1;i<=m;i++) {
int t1 = sc.nextInt();
int t2 = sc.nextInt();
e[t1][t2] = 1;
e[t2][t1] = 1;
}
match = new int[n+1];
book = new int[n+1];
}
public static void main(String[] args) {
Test5 t = new Test5();
t.init();
for(int i=1;i<=t.n;i++) {
for(int j=1;j<=t.n;j++) {
t.book[j] = 0;
}
if(t.dfs(i)==1) { //寻找增广路
t.sum++;
}
}
System.out.println(t.sum);
}
}