并查集和DFS实现船问题(连通性判断)

题目如下,判断下列矩阵中船的个数(连续的相同数据称为一条船)例如如下矩阵
0 0 1 1
1 1 0 0
0 1 0 0
会输出两条船,这里的两条船分别指的是下图中的粗体部分和斜体部分
0 0 1 1
1 1 0 0
0 1 0 0
那么我们如何做到判断他们的连续情况呢。

方法一DFS法(递归法)

DFS方法就是我们所谓的递归方法,我们在考虑一个数据周围的数据是否和他相同,从而实现连通。所以我们最先想到的应该就是一种最暴力的方法:
①先找到一个元素。
②判断其上下左右的元素是否与他相同,并做出标记。
③对数组边界作出处理。
这种思维是最简单粗暴的思维,我们从这里切入,对我们的思路进行改善,
我们需要解决如下问题:
①用简洁的代码判断一个数组元素的周围元素是否和他相同
②对数组边界的处理
③对已经考虑过的元素进行标记
④进行计数

好的,下面我们来逐一解决上面四个问题
①对于一个元素周围的元素是否与他相同,如果我们采用循环的方式,很难准确找到边界,所以我们在这里采用递归的方式,一次判断该元素的上下左右的四个方向的元素,直到找到与这个元素不同的值。
②对于边界问题,只需要设立好循环次数并且在递归的进入条件中添加边界判定即可
③对于已经考虑过得元素的标记问题,我这里采用了加入对照数组的方法,即建立一个与给出数组相同长度、相同数组元素的数组。将对照数组中遍历过得数组元素全部赋值为0,并把对照数组元素非0作为进入递归的条件。
④在进行循环的过程中,遇到递归后满足条件的情况,就使得计数器加1
下面我们来看代码实现:

import java.util.*;

public class Boat {

	public static final int N = 4;

	public static void dfs(int arr[][],int cheak[][], int x, int y) {
		if(judge(arr,x,y)&&cheak[x][y]!=0&&arr[x][y]==1) {
			cheak[x][y]=0;//将便利过的元素赋值为0
			dfs(arr,cheak,x+1,y);//分别判断当前元素的上下左右四个元素,并且
			dfs(arr,cheak,x,y+1);//继续递归深入判断
			dfs(arr,cheak,x-1,y);
			dfs(arr,cheak,x,y-1);
		}
		
	}
	//考虑边界问题
	public static boolean judge(int arr[][], int x, int y) {
		if (x <0 || x >=arr.length || y < 0 || y >=arr.length)
			return false;
		return true;
	}
	public static void main(String[] args) {

		int sum = 0;
		int[][] cheak =  { { 0, 0, 1, 1 }, { 1, 1, 0, 0 }, { 0, 1, 0, 0 }, { 0, 0,0, 0} };
		int[][] arr = { { 0, 0, 1, 1 }, { 1, 1, 0, 0 }, { 0, 1, 0, 0 }, { 0, 0,0, 0 } };
		for(int i=0;i<arr.length;i++) {
			for(int j=0;j<arr[i].length;j++) {
				if(arr[i][j] == 1&&cheak[i][j]!=0) {
					dfs(arr,cheak,i,j);
					sum++;
				}
			}
		}
		System.out.println(sum);
	}
}

方法二并查集法

首先我们来了解一下有关并查集的内容
概念:
并查集,在一些有N个元素的集合应用问题中,我们通常是在开始时让每个元素构成一个单元素的集合,然后按一定顺序将属于同一组的元素所在的集合合并,其间要反复查找一个元素在哪个集合中。这一类问题近几年来反复出现在信息学的国际国内赛题中,其特点是看似并不复杂,但数据量极大,若用正常的数据结构来描述的话,往往在空间上过大,计算机无法承受;即使在空间上勉强通过,运行的时间复杂度也极高,根本就不可能在比赛规定的运行时间(1~3秒)内计算出试题需要的结果,只能用并查集来描述。
并查集是一种树型的数据结构,用于处理一些不相交集合(Disjoint Sets)的合并及查询问题。常常在使用中以森林来表示。
主要操作方式:
初始化
把每个点所在集合初始化为其自身。
通常来说,这个步骤在每次使用该数据结构时只需要执行一次,无论何种实现方式,时间复杂度均为O(N)。
查找
查找元素所在的集合,即根节点。
合并
将两个元素所在的集合合并为一个集合。
通常来说,合并之前,应先判断两个元素是否属于同一集合,这可用上面的“查找”操作实现。
实例化解释:
上面的说法都太过于抽象了,我个人的理解就是,并查集就是一种“拉帮结派”
比如说一做山上有五伙强盗,这里把他们标号为0、1、2、3、4。然后他们各自为政,自己是自己的头目,然后突然有一天2号强盗给0号强盗吞并了,2变成了0的老大。4号强盗给1号强盗吞并了,4变成了1的老大。这样吞并一次之后这座山上就还剩下了3号、2号、4号三伙强盗,然后2又吞并了3号,就剩下了2、4两伙强盗。
说到这你可能会觉得,你扯这么多没用的,我怎么没发现这并查集和这个判断船数目有什么关系呢?仔细想一下,上面说的每伙强盗之间的吞并关系,是不是可以理解为,把两伙强盗连接到一起,成为了一组。而我们的船问题,恰恰可以理解为判断连在一起的区域有几个。这就成了判断强盗组数的问题。就把问题引回了并查集上。下面我们来逐步分析我们需要的代码
①怎样找到强盗的头目,即当前的元素是否有与他连续的元素,或者说他自己就是单独的一伙强盗
②怎样知道有多少伙强盗,即判断我们需要的船的个数
③怎样实现强盗之间的吞并,即把他们连在一起

下面我们逐一解决这些问题
①我们可以建立一个用于找头目的数组,数组的下标是某一伙强盗,而数组内的值则是这伙强盗的头目。
②我们可以通过找最终大BOSS(自己就是头目),即数组下标和数组内元素相同的个数,来判断有多少组强盗。
③有关于实现吞并操作,我们可以直接利用上面建立的找头目的数组,直接让两个数组元素中的一个存入另一个中,成为他的头目例如

pre[2]=3;//这里就是把3变成2的头目

下面我们来看具体的代码实现

import java.util.*;

public class BoatUnionFind {
	public static final int N = 4;
	public static int[][] arr = { { 0, 0, 1, 1 }, { 1, 1, 0, 0 }, { 0, 1, 0, 0 }, { 0, 0, 0, 0 } };
	static int[] pre;
	public static void main(String[] args) {
		
		int hang = arr.length;
		int lie = arr[0].length;
		pre = new int[hang*lie+1];
		for (int i = 0; i < hang; i++) {
			for (int j = 0; j < lie; j++) {
				int num = hang*i+j;
				pre[num]=num;
			}
		}
		for (int i = 0; i < hang; i++) {
			for (int j = 0; j < lie; j++) {
				if(arr[i][j]==1) {
					int down = i+1;
					int right = j+1;
					if(down<hang && arr[down][j]==1) {
						union(hang*i+j,hang*down+j);
					}
					if(right<lie && arr[i][right]==1) {
						union(hang*i+j,hang*i+right);
					}
				}
			}
		}
		
		int sum=0;
		for(int i=0;i<hang;i++) {
			for(int j=0;j<lie;j++) {
				int num=hang*i+j;
				if(pre[num] == num && arr[i][j]==1) {
					sum++;
				}
			}
		}
		System.out.println(sum);
	}
	
	public static int find(int x) {//寻找这伙盗贼的头目
		while(x!=pre[x]) x=pre[x];
		return x;
	}
	
	public static  void union(int p,int q) {//合并两伙盗贼
		int Rootq = find(q);
		int Rootp = find(p);
		if(Rootq == Rootp) {
			return;
		}
		pre[Rootp]=Rootq;
	}

}

你可能感兴趣的:(Java,数据结构与算法)