第8章 更多精彩算法

简介: 第8章 更多精彩算法

第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);
  }
}


相关文章
|
5天前
|
云安全 人工智能 自然语言处理
|
9天前
|
人工智能 Java API
Java 正式进入 Agentic AI 时代:Spring AI Alibaba 1.1 发布背后的技术演进
Spring AI Alibaba 1.1 正式发布,提供极简方式构建企业级AI智能体。基于ReactAgent核心,支持多智能体协作、上下文工程与生产级管控,助力开发者快速打造可靠、可扩展的智能应用。
872 28
|
4天前
|
机器学习/深度学习 人工智能 自然语言处理
Z-Image:冲击体验上限的下一代图像生成模型
通义实验室推出全新文生图模型Z-Image,以6B参数实现“快、稳、轻、准”突破。Turbo版本仅需8步亚秒级生成,支持16GB显存设备,中英双语理解与文字渲染尤为出色,真实感和美学表现媲美国际顶尖模型,被誉为“最值得关注的开源生图模型之一”。
462 4
|
6天前
|
机器学习/深度学习 人工智能 数据可视化
1秒生图!6B参数如何“以小博大”生成超真实图像?
Z-Image是6B参数开源图像生成模型,仅需16GB显存即可生成媲美百亿级模型的超真实图像,支持中英双语文本渲染与智能编辑,登顶Hugging Face趋势榜,首日下载破50万。
398 19
|
13天前
|
数据采集 人工智能 自然语言处理
Meta SAM3开源:让图像分割,听懂你的话
Meta发布并开源SAM 3,首个支持文本或视觉提示的统一图像视频分割模型,可精准分割“红色条纹伞”等开放词汇概念,覆盖400万独特概念,性能达人类水平75%–80%,推动视觉分割新突破。
826 59
Meta SAM3开源:让图像分割,听懂你的话
|
2天前
|
弹性计算 网络协议 Linux
阿里云ECS云服务器详细新手购买流程步骤(图文详解)
新手怎么购买阿里云服务器ECS?今天出一期阿里云服务器ECS自定义购买流程:图文全解析,阿里云服务器ECS购买流程图解,自定义购买ECS的设置选项是最复杂的,以自定义购买云服务器ECS为例,包括付费类型、地域、网络及可用区、实例、镜像、系统盘、数据盘、公网IP、安全组及登录凭证详细设置教程:
180 114
|
9天前
|
人工智能 前端开发 算法
大厂CIO独家分享:AI如何重塑开发者未来十年
在 AI 时代,若你还在紧盯代码量、执着于全栈工程师的招聘,或者仅凭技术贡献率来评判价值,执着于业务提效的比例而忽略产研价值,你很可能已经被所谓的“常识”困住了脚步。
461 40
大厂CIO独家分享:AI如何重塑开发者未来十年