日撸 Java 三百行(35 天: 涂色问题)

本文详细介绍了如何使用深度优先搜索(DFS)解决经典涂色问题,将地图区域用有限颜色涂色,确保相邻区域颜色不同。通过抽象成无向图,线性DFS枚举实现,阐述了代码实现思路,包括初始化、核心DFS部分及染色合理性判断,并提供了数据模拟案例。文章强调了计算机枚举和暴力搜索在处理此类问题中的重要性,并指出暴力算法在优化中的启示作用。
摘要由CSDN通过智能技术生成

注意:这里是JAVA自学与了解的同步笔记与记录,如有问题欢迎指正说明

目录

一、关于涂色问题

二、代码实现思路

三、代码实现过程

1、初始化

2、核心代码(DFS部分)

3、染色合理性判断

四、数据模拟

总结


一、关于涂色问题

涂色问题是一类非常经典的组合排列的问题,想必诸位很多在高中就接触过这类问题。我这里以一道题目的形式让大家回顾下这类经典问题:

         现在有上述的地图区域,为了明显表示地图边界,我们要求给地图每个区域图上不同颜色,但由于色彩优先,只能用5种不同的颜色给图中标A、B、C、D的各部分涂色,每部分只涂一种颜色,相邻部分涂不同颜色以保证区分,则不同的涂色方法有多少种?

这类问题的解法在高中也许我们已经驾轻就熟了,简单来说就是枚举。我们先假设A选择5个颜色当中的其中一种,这样就有5种情况;而B这个时候先不管C怎么样,只知道A已经使用了一个颜色,那么相邻的B只能选择A用剩下的其中4个中的一个;C同理先不管D怎么样,它只能在A、B用剩下的色彩中选择一个,显然是3中情况;D同理4种。综上总情况为5*4*3*4=240种。

        今天特地在图的练习中提到这个问题,显然,这类问题具有非常明显的数据结构图的特征。因为这种地图区域之间紧密相连的关系等同于图当中的结点相连,具体来说,因为这个相邻是双向的,所以对应于我们的无向图结构。就如下图所示:

         假设今天的任务是:用代码求出每种染色的组合,你会这么想?我们当时写这道题的时候采用了枚举的思路,实际在程序中也是如此。我们会采用具有枚举搜索性质的方法去完成这个过程,我们俗称为“ 暴力 ”解法。因此对于图结构的枚举,同时又涉及暴力,因此自然而然得出结论:使用DFS完成。

二、代码实现思路

        首先抽象下问题:涂色问题可以抽象为一个无向图,这个无向图每个结点可以携带一个权值作为图的颜色,然后任何一个结点的权值必须与其相邻结点不同;然后确定下我们的信息载体,我们可以按照结点的序号依次给出每个结点的权值(颜色),这样就无需额外设置一个图结构去存储颜色信息;最后确定数据类型特征,色彩不用专门用字符去说明,我们默认以自然序列0,1,2,...,n去表示某个颜色就好(我们不考虑这些下标与哪些颜色对应),默认颜色标记数组默认为-1表示没有尚未染色,同时线性表下标索引与结点序号对应(在下图看来就是ABCD)。

        虽然这道题计算采用的是DFS思路同时又涉及图的模拟,但其完成的套路却并不是基于DFS的结点遍历。通过上面的分析,算法目标是得到一个颜色标记数组paraCurrentColoring,因为这个数组与区域通过下标对应,能指导我们染色。正好,便可只通过遍历颜色数组,得到对应下标的结点,然后判断结点相连的其余结点的染色情况从而决定当前结点的颜色。总结来看,本题虽然涉及图,但是本质上还是一个线性DFS的枚举问题,图的作用只体现于:线性枚举的规则是基于图的连通性的。        

        我们在刚刚分析涂色问题的时候,我做了一个比较明显的强调(划线部分),我们在对一个涂色区域进行考虑的时候忽略了其余的我们不关心的部分,所以在程序实现考虑冲突的时候只用考虑已经涂色的区域就好了。介于本算法计划对颜色数组从左至右搜索,若当前位于颜色标记数组的\(i\)位置,那么这时,任意位置\(k(0\leqslant k <  i)\)都是已经被染色过的,所以我们考虑连通问题只需要考虑\(k\)结点就好。

三、代码实现过程

1、初始化

	/**
	 *********************
	 * Coloring. Output all possible schemes.
	 * 
	 * @param paraNumColors The number of colors.
	 *********************
	 */
	public void coloring(int paraNumColors) {
		// Step 1. Initialize.
		int tempNumNodes = connectivityMatrix.getRows();
		int[] tempColorScheme = new int[tempNumNodes];
		Arrays.fill(tempColorScheme, -1);

		coloring(paraNumColors, 0, tempColorScheme);
	}// Of coloring

        很多搜索问题都需要基本的搜索资料准备,常见的方法便是如此在搜索递归部分的外围套一个“套子”。在这个套子里面,我们可以完成众多基本的初始化(这里完成了对于颜色标记数组的初始化),同时也增加了递归部分的扩充性和维护性。

2、核心代码(DFS部分)

	/**
	 *********************
	 * Coloring. Output all possible schemes.
	 * 
	 * @param paraNumColors The number of colors.
	 * @param paraCurrentNumNodes The number of nodes that have been colored.
	 * @param paraCurrentColoring The array recording the coloring scheme.
	 *********************
	 */
	public void coloring(int paraNumColors, int paraCurrentNumNodes, int[] paraCurrentColoring) {
		// Step 1. Initialize.
		int tempNumNodes = connectivityMatrix.getRows();

		System.out.println("coloring: paraNumColors = " + paraNumColors + ", paraCurrentNumNodes = "
				+ paraCurrentNumNodes + ", paraCurrentColoring" + Arrays.toString(paraCurrentColoring));
		// A complete scheme.
		if (paraCurrentNumNodes >= tempNumNodes) {
			System.out.println("Find one:" + Arrays.toString(paraCurrentColoring));
			return;
		} // Of if

		// Try all possible colors.
		for (int i = 0; i < paraNumColors; i++) {
			paraCurrentColoring[paraCurrentNumNodes] = i;
			if (!colorConflict(paraCurrentNumNodes, paraCurrentColoring)) {
				coloring(paraNumColors, paraCurrentNumNodes + 1, paraCurrentColoring);
			} // Of if
		} // Of for i
	}// Of coloring

        可以发现,这个函数是上一个函数的重载,这样写可以避免相同功能方向各种命名不好管理,当然这个看自己的习惯吧。

        DFS的递归体通常由二个部分构成,自上而下分别为(此结构忽略部分初始化操作):

  1. 条件退出判断部分
  2. 循环找分支部分(多用for)

        具体实现中,这两个部分可以玩出许多花样。比如,可以把队当前函数部分的访问操作放到第1与第2步中间或者第一步之前;第二步操作中,在函数入口后方放的内容可以用作回溯时特别执行的代码(本代码不需要,进程用于解除标记等作用)

        本递归题使用paraCurrentNumNodes表示当前线性部分的索引下标,递归体以此作为递归的深入标识,因此条件退出部分就是判断当前paraCurrentNumNodes是否越界。

        访问部分在退出入口之上,因为代码目标在于枚举所有染色编码,故访问操作为基本打印。

        循环分支部分分别枚举可用的颜色个数(paraNumColors)并将颜色记录在颜色标记数组中,并且判断染色是否合理,合理便进入下一步染色,进入递归体,递归的深入标识+1。这里“ 判断染色是否合理 ”的操作专门用一个函数封装了,见下。

3、染色合理性判断

        染色的合理性判断在第二部分已经描述过了,只需要判断颜色标记的线性数组当前下标之前的部分是否构成连通关系即可:

	/**
	 *********************
	 * Coloring conflict or not. Only compare the current last node with previous
	 * ones.
	 * 
	 * @param paraCurrentNumNodes The current number of nodes.
	 * @param paraColoring        The current coloring scheme.
	 * @return Conflict or not.
	 *********************
	 */
	public boolean colorConflict(int paraCurrentNumNodes, int[] paraColoring) {
		for (int i = 0; i < paraCurrentNumNodes; i++) {
			// No direct connection.
			if (connectivityMatrix.getValue(paraCurrentNumNodes, i) == 0) {
				continue;
			} // Of if

			if (paraColoring[paraCurrentNumNodes] == paraColoring[i]) {
				return true;
			} // Of if
		} // Of for i
		return false;
	}// Of colorConflict

         这里采用的正向判断的逆判断,也就是冲突判断,简单来说,如果返回true说明染色不合理。for循环中先是筛选出连通部分,之后再判断颜色。

四、数据模拟

        为图的连通性过于复杂不方便验证,故采用了一个相对简单的无向图来模拟

        可见无向图的邻接矩阵是个严格的对称矩阵。 

         单元测试代码:

	/**
	 *********************
	 * Coloring test.
	 *********************
	 */
	public static void coloringTest() {
		int[][] tempMatrix = { { 0, 1, 1, 0 }, { 1, 0, 0, 1 }, { 1, 0, 0, 0 }, { 0, 1, 0, 0 } };
		Graph tempGraph = new Graph(tempMatrix);
		//tempGraph.coloring(2);
		tempGraph.coloring(3);
		System.out.println(tempGraph);
	}// Of coloringTest

        输出结果(只显示部分)

总结

        染色问题是个枚举问题,这个过程比较繁琐,但交给计算机的暴力搜索来看似乎就很方便。现实中的枚举现象不在少数,虽然部分问题可以用到组合数学的排列组合思想去计算,但是理论性的东西有时无法适应很灵活的环境。因此枚举依旧是个关键的方法,往往来说,计算机是实现这些方法的关键,毕竟现实生活中的大体量的枚举问题是不可能花时间人力枚举的。

        因此要用计算机去处理真实的现实问题,必须要学会的就是怎么用计算机去枚举和暴力搜索数据,这些应当作为数据抽象化后进行数据处理的基础思想。同时暴力的算法往往是优化的墙梁,因为在暴力算法实现的过程中总会显露出特定问题的特征,这些特征是在单独死扣问题时不容易发现的。这个过程我们习惯称之为剪枝

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值