Java实现无向连通图中的“割点”问题

直接上代码,详细请见注释或者下方留言。

package cut.point;

import java.util.Scanner;
import java.util.Stack;

/**
 * "轰炸重要城市"问题:
 * 假设当前我们拥有一个 地区的城市地图,但是只有一个原子弹,为了让这颗原子弹发挥最大的效果,要阻断这个地区各个城市中最关键的一个交通要塞,那么这个原子弹该投放在哪里?
 * 其实真有原子弹就不会考虑这么多了(~……~),扯回正题。这种问题模型化之后,就是让我们从一个无向连通图中选择一个“割点”,去掉这个点之后,不再是连通图。
 * 关键词:DFS、割点、无向连通图、重要城市
 * 特殊说明:有些方法的参数比较多,主要是这次不想用一些全局静态变量来处理,所以就全部通过传参解决!
 * @author XZP
 *一组测试数据:
6 7 
1 4 
1 3 
4 2
3 2
2 5 
2 6
5 6
 */
public class BombingImportantCity {

    public static void main(String[] args) {
        int INF = 99999; // 人为设定的最大值
        Scanner sc = new Scanner(System.in);
        int i; // 游标
        int v1, v2; //暂时存储边两个顶点的编号
        int n = sc.nextInt(); // 顶点数
        int m = sc.nextInt(); // 边的条数
//      Edge[] edges = new Edge[m + 1]; // 存储边的数组,下标从1开始
        int[][] edges = new int[n + 1][n +1]; // 存放边的邻接矩阵
        int[] num = new int[n + 1]; // 存储第一遍从顶点1dfs遍历情况下的时间戳,数组下标代表顶点编号,值表示第几个访问到(时间戳)
        int[] low = new int [n + 1]; // 表示每个顶点在不经过父顶点能回到的最小时间戳(比较拗口,大致可以理解为:根据num数组找到父节点,然后用dfs遍历,能够达到的最小时间戳)
        // 初始化low数组,比较重要
        for (i = 1; i <= n; i++) {
            low[i] = INF;
        }
        
        // 读入m条边
        for (i =1; i <= m; i++) {
            v1 = sc.nextInt();
            v2 = sc.nextInt();
            edges[v1][v2] = 1; // 其他都为零表示两点之间不可达或者是自身
        }
        // 求num数组(每个顶点对应的时间戳)
        dfs(1, edges, num, n);
        
        // 得到割点
        int cutPoint = judgeCutPoint(low, edges, n, num, 1);
        if (cutPoint == 0) {
            System.out.println("没有割点!");
        } else {
            System.out.println("割点编号为: " + cutPoint);
        }
    }
    /**
     * 以当前节点为起始点排除父节点的情况下深度优先遍历以获取当前点能访问的最小时间戳
     * @param low 待计算、更新的最小访问时间戳
     * @param child 当前节点
     * @param parent 当前节点的父节点(根据num矩阵来的)
     * @param edges 边的邻接矩阵
     * @param num 时间戳矩阵
     * @param n 顶点个数
     */
    public static void dfsExParent(int[] low, int child,int parent, int[][] edges, int[] num, int n) {
        int[] book = new int[n + 1]; // 用于判断一个顶点是否已经被放到栈里面去过了,避免环引起的错误
        Stack<Integer> search = new Stack<Integer>();
        search.push(child);
        book[child] = 1;
        while (!search.isEmpty()) {
            // 首先从栈顶去一个元素,并将这个顶点相连的顶点入栈
            int top = search.pop();
            // 用更小的时间戳替换掉
            if (num[top] < low[child]) {
                low[child] = num[top];
            }
            for (int i = 1; i <=n; i++) {
                if (i !=parent && num[top] < i && edges[top][i] == 1 && book[i] == 0) {
                    search.push(i);
                    book[i] = 1;
                }
            }
        }
    }
    /**
     * 判断一个点是否是割点的方法
     * @param low low数组,存储当前节点在不经过父节点的情况下能够访问节点的最小时间戳
     * @param edges 边的邻接矩阵
     * @param n 顶点个数
     * @param num 时间戳数组
     * @param start 起始点
     * @return
     */
    public static int judgeCutPoint(int[] low, int[][] edges, int n, int[] num, int start) {
        for (int i = 1; i <= n; i++) { // 依次计算节点i的low[i]值
            int parent = findParent(i, num, n);
            // 排除掉父节点的情况下,使用dfs遍历,将遍历到的时间戳值用更小的替换大的时间戳值,不断更新low[i]的值
            dfsExParent(low, i, parent, edges, num, parent);
            if (i != start && low[i] >= num[parent]) { // 要注意排除起始点,否则程序在第一个点处就返回了,显然这是不对的
                return i;
            }
        }
        return 0;
    }
    /**
     * 根据时间戳数组找一个节点的父节点
     * @param i 当前节点编号
     * @param num 时间戳数组
     * @param n 顶点个数
     * @return 正确情况下返回父节点的编号,如果返回为0表示出错了!
     */
    public static int findParent(int i, int[] num, int n) {
        if (num[i] == i) {
            return i;
        } else {
            int parent = num[i] - 1; // 父节点的时间戳为当前的时间戳减一
            for (int j = 1; j <= n; j++) {
                if (num[j] == parent) {
                    return j;
                }
            }
            return 0; // 是一种出错的返回
        }
    }
    /**
     * 计算num数组的dfs方法
     * @param start 起始点
     * @param edges 边数组
     * @param num num数组
     * @param n 顶点个数
     */
    public static void dfs(int start, int[][] edges, int[] num, int n) {
        int[] book = new int[n + 1]; // 用于判断一个顶点是否已经被放到栈里面去过了,避免环引起的错误
        int number = 1; // 被访问到的编号
        Stack<Integer> search = new Stack<Integer>();
        search.push(start);
        book[start] = 1;
        while (!search.isEmpty()) {
            // 首先从栈顶去一个元素,并将这个顶点相连的顶点入栈
            int top = search.pop();
            num[top] = number; // 为num数组赋值
            number++;
            for (int i = 1; i <=n; i++) {
                if (edges[top][i] == 1 && book[i] == 0) {
                    search.push(i);
                    book[i] = 1;
                }
            }
        }
    }
}

//***********实践证明:这种存储方式在某些情况下比较优化,但是由于dfs便于找到下一个顶点,最好还是用邻接矩阵*******************
/**
 * 表示一条边的对象
 * @author XZP
 *
 */
class Edge {
    private int v1;
    private int v2;
    public Edge(int v1, int v2) {
        this.v1 = v1;
        this.v2 = v2;
    }
    // getter
    public int getV1() {
        return v1;
    }
    public int getV2() {
        return v2;
    }
}

*********************************************************更新*******************************************************
昨天到后面没啥效率了,今早更新一下一个小bug(所以效率才是生产力,囧,今早三分钟搞定,昨天实在写的有些疲乏了),代码:

package cut.point;

import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;
import java.util.Stack;

import javax.swing.text.AsyncBoxView.ChildState;

/**
 * "轰炸重要城市"问题:
 * 假设当前我们拥有一个 地区的城市地图,但是只有一个原子弹,为了让这颗原子弹发挥最大的效果,要阻断这个地区各个城市中最关键的一个交通要塞,那么这个原子弹该投放在哪里?
 * 其实真有原子弹就不会考虑这么多了(~……~),扯回正题。这种问题模型化之后,就是让我们从一个无向连通图中选择一个“割点”,去掉这个点之后,不再是连通图。
 * 关键词:DFS、割点、无向连通图、重要城市
 * 特殊说明:有些方法的参数比较多,主要是这次不想用一些全局静态变量来处理,所以就全部通过传参解决!
 * @author XZP
 *一组测试数据:
6 7 
1 4 
1 3 
4 2
3 2
2 5 
2 6
5 6
 */
public class BombingImportantCity {

    public static void main(String[] args) {
        int INF = 99999; // 人为设定的最大值
        Scanner sc = new Scanner(System.in);
        int i; // 游标
        int v1, v2; //暂时存储边两个顶点的编号
        int n = sc.nextInt(); // 顶点数
        int m = sc.nextInt(); // 边的条数
//      Edge[] edges = new Edge[m + 1]; // 存储边的数组,下标从1开始
        int[][] edges = new int[n + 1][n +1]; // 存放边的邻接矩阵
        int[] num = new int[n + 1]; // 存储第一遍从顶点1dfs遍历情况下的时间戳,数组下标代表顶点编号,值表示第几个访问到(时间戳)
        int[] low = new int [n + 1]; // 表示每个顶点在不经过父顶点能回到的最小时间戳(比较拗口,大致可以理解为:根据num数组找到父节点,然后用dfs遍历,能够达到的最小时间戳)
        // 初始化low数组,比较重要
        for (i = 1; i <= n; i++) {
            low[i] = INF;
        }
        
        // 读入m条边
        for (i =1; i <= m; i++) {
            v1 = sc.nextInt();
            v2 = sc.nextInt();
            edges[v1][v2] = 1; // 其他都为零表示两点之间不可达或者是自身
            edges[v2][v1] = 1; // 无向图对称
        }
        // 求num数组(每个顶点对应的时间戳)
        dfs(1, edges, num, n, low);
        
        // 得到割点
        int cutPoint = judgeCutPoint(low, edges, n, num, 1);
        if (cutPoint == 0) {
            System.out.println("没有割点!");
        } else {
            System.out.println("割点编号为: " + cutPoint);
        }
    }
    /**
     * 以当前节点为起始点排除父节点的情况下深度优先遍历以获取当前点能访问的最小时间戳
     * @param low 待计算、更新的最小访问时间戳
     * @param child 当前节点
     * @param parent 当前节点的父节点(根据num矩阵来的)
     * @param edges 边的邻接矩阵
     * @param num 时间戳矩阵
     * @param n 顶点个数
     */
    public static void dfsExParent(int[] low, int child,int parent, int[][] edges, int[] num, int n, int start) {
        int[] book = new int[n + 1]; // 用于判断一个顶点是否已经被放到栈里面去过了,避免环引起的错误
        Stack<Integer> search = new Stack<Integer>();
        search.push(child);
        book[child] = 1;
        while (!search.isEmpty()) {
            // 首先从栈顶去一个元素,并将这个顶点相连的顶点入栈
            int top = search.pop();
            // 用更小的时间戳替换掉
            if (num[top] < low[child]) {
                low[child] = num[top];
            }
            for (int i = 1; i <=n; i++) {
                if (i !=parent && edges[top][i] == 1 && book[i] == 0) { // 不经过父节点访问其他子节点不等同于不访问父节点!!!
                    search.push(i);
                    book[i] = 1;
                }
            }
        }
    }
    /**
     * 判断一个点是否是割点的方法
     * @param low low数组,存储当前节点在不经过父节点的情况下能够访问节点的最小时间戳
     * @param edges 边的邻接矩阵
     * @param n 顶点个数
     * @param num 时间戳数组
     * @param start 起始点
     * @return
     */
    public static int judgeCutPoint(int[] low, int[][] edges, int n, int[] num, int start) {
        for (int i = 1; i <= n; i++) { // 依次计算节点i的low[i]值
            int parent = findParent(i, num, n);
            // 排除掉父节点的情况下,使用dfs遍历,将遍历到的时间戳值用更小的替换大的时间戳值,不断更新low[i]的值
            dfsExParent(low, i, parent, edges, num, n, start);
            if (low[i] >= num[parent]) { // 要注意排除起始点,否则程序在第一个点处就返回了,显然这是不对的
                // 还要判断是否是根节点
                if (parent != start) {
                    return parent;
                } else {
                    // 判断根节点且至少有两个孩子,这两个孩子没有其他可达到的的其他路径的情况下才是割点
                    if (isRootCutPoint(edges, start, n)) {
                        return start;
                    }
                }
            }
        }
        return 0;
    }
    public static boolean isRootCutPoint(int[][] edges, int root, int n) {
        boolean flag = false;
        List<Integer> child = new ArrayList<>();
        // 计算孩子数
        for (int i = 1; i <= n; i++) {
            if (edges[root][i] == 1) {
                child.add(i);
            }
        }
        int size = child.size();
        for (int i = 0; i< size; i++) {
            for (int j = i + 1; j < size; j ++) {
                if (!isReachable(child.get(i), child.get(j), edges, n, root)) {
                    return true;
                }
            }
        }
        return flag;
    }
    public static boolean isReachable(int a, int b, int[][] edges, int n, int root) {
        int[] book = new int[n + 1]; // 用于判断一个顶点是否已经被放到栈里面去过了,避免环引起的错误
        Stack<Integer> search = new Stack<Integer>();
        search.push(a);
        book[a] = 1;
        while (!search.isEmpty()) {
            // 首先从栈顶去一个元素,并将这个顶点相连的顶点入栈
            int top = search.pop();
            if (top == b) {
                return true;
            }
            for (int i = 1; i <=n; i++) {
                if (i != root && edges[top][i] == 1 && book[i] == 0) {
                    search.push(i);
                    book[i] = 1;
                }
            }
        }
        return false;
    }
    /**
     * 根据时间戳数组找一个节点的父节点
     * @param i 当前节点编号
     * @param num 时间戳数组
     * @param n 顶点个数
     * @return 正确情况下返回父节点的编号,如果返回为0表示出错了!
     */
    public static int findParent(int i, int[] num, int n) {
        if (num[i] == i) {
            return i;
        } else {
            int parent = num[i] - 1; // 父节点的时间戳为当前的时间戳减一
            for (int j = 1; j <= n; j++) {
                if (num[j] == parent) {
                    return j;
                }
            }
            return 0; // 是一种出错的返回
        }
    }
    /**
     * 计算num数组的dfs方法
     * @param start 起始点
     * @param edges 边数组
     * @param num num数组
     * @param n 顶点个数
     */
    public static void dfs(int start, int[][] edges, int[] num, int n, int[] low) {
        int[] book = new int[n + 1]; // 用于判断一个顶点是否已经被放到栈里面去过了,避免环引起的错误
        int number = 1; // 被访问到的编号
        Stack<Integer> search = new Stack<Integer>();
        search.push(start);
        book[start] = 1;
        while (!search.isEmpty()) {
            // 首先从栈顶去一个元素,并将这个顶点相连的顶点入栈
            int top = search.pop();
            num[top] = number; // 为num数组赋值
            low[top] = number; // 最开始就是自己
            number++;
            for (int i = 1; i <=n; i++) {
                if (edges[top][i] == 1 && book[i] == 0) {
                    search.push(i);
                    book[i] = 1;
                }
            }
        }
    }
}

//***********实践证明:这种存储方式在某些情况下比较优化,但是由于dfs便于找到下一个顶点,最好还是用邻接矩阵*******************
/**
 * 表示一条边的对象
 * @author XZP
 *
 */
class Edge {
    private int v1;
    private int v2;
    public Edge(int v1, int v2) {
        this.v1 = v1;
        this.v2 = v2;
    }
    // getter
    public int getV1() {
        return v1;
    }
    public int getV2() {
        return v2;
    }
}

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容

  • 注:都是在百度搜索整理的答案,如有侵权和错误,希告知更改。 一、哪些情况下的对象会被垃圾回收机制处理掉  当对象对...
    Jenchar阅读 3,212评论 3 2
  • 01 最近在十点读书会,共读模块中看了一本书《查令十字街84号》。一共是十天读一本书,看完后很感动,脑海中也一直出...
    r染染阅读 523评论 0 0
  • 1.今日事今日畢,迴向給自己,建立對自己的信任感。為自己點贊。 学堂学新诗《古朗月行》;新家储藏间置物架安装完毕;...
    李豪亮阅读 87评论 0 0
  • 小雅急匆匆走在路上,一辆摩托车从身边疾驰而过,小雅还在唠叨着:“开这么快,不怕撞着人啊。” 突然,前方传来一个急刹...
    王莲荷阅读 242评论 0 1