三色旗问题通解

背景描述:

描述:假设有一条绳子,上面挂有红、白、蓝三种颜色的旗子,没有顺序。

要求:将旗子分类,按蓝、白、红的顺序排列。

问题:如何移动,可使总移动次数最少。

限制:只能在绳子上操作,每次只能交换其中两面旗子。

例子:

原始状态:

         

最终状态:

         

该问题是由荷兰人Dijkstra提出的,所以该问题又叫荷兰旗问题。

 

解析:

一般讲到这种问题,大家都不自然地会想到部分快排。先来看种简单的快排写法:

func QuickSort(slice []int) {
	if len(slice) <= 1 {
		return
	}

	t := slice[0]
	l := 0
	r := len(slice) - 1
	for l < r {
		for ; l < r && slice[r] > t; r-- {
		}
		for ; l < r && slice[l] <= t; l++ {
		}
		slice[l], slice[r] = slice[r], slice[l] // swap
	}
	slice[l], slice[0] = slice[0], slice[l] // swap
	QuickSort(slice[:l])
	QuickSort(slice[l+1:])
}

简单粗暴的写法。

网上盛行的三色题解法就是根据上面写法得来的。

 

我们先把问题简化,将三色旗改成双色旗:

123456

按上面快排的思路,就是在右端找一个蓝色的(5),在左端找一个红色的(1),调换顺序:

523416

重复这个过程,从右端找到一个蓝色的(4),左端找到一个红色的(3),调换顺序:

524316

这样就按要求完成了分类过程。

总体思路很简单,从两端找对方需要颜色的旗子,对换。即需要两个游标,左端为蓝色游标,右端为红色游标,蓝色游标找非蓝色的,红色游标找非红色的,和对方调换,当两个游标相遇时,过程完成。

 

那现在把问题还原,三色旗问题怎么解?比如开头的顺序:

123456789

一个蓝色游标,一个红色游标,可白色的怎么办呢?

回过头来再仔细观察代码,可以发现,把等于t这一情况,都划分到左半部分了,它把整个数组分为两种情况:一是小于等于t的,一种是大于t的,关键在于这个小于等于中的等于,提醒我们,是不是可以把白色划分到蓝色里面去,即把蓝色和白色当成一种颜色,那么,问题又归结到上面双色旗问题了。划分完双色旗后,我们可以再把蓝白色旗当成双色旗进行划分,即可完成全过程。该过程容易验证,故不在这里画图说明了。

做程序的人吧,就这点不好,完美主义,总想一次把所有事做完,即只扫一遍,就把顺序给调换完。

好吧,书接前文。我们看到,把蓝白当成一种颜色,和红色组成的双色旗问题里面,我们明确的知道左右两端点,即知道游标的起点,但在蓝白内部做双色旗划分,没有划分完时,是不知道蓝有多少,白有多少的。只知道蓝色的游标起点是0,但白色右端的游标在哪呢?

这时,我们需要从另一种快排写法中找思路了:

func QuickSort(slice []int) {
	if len(slice) <= 1 {
		return
	}

	l := 0
	for i := 1; i < len(slice); i++ {
		if slice[i] <= slice[0] {
			l++
			slice[l], slice[i] = slice[i], slice[l] // swap
		}
	}
	slice[l], slice[0] = slice[0], slice[l] // swap
	QuickSort(slice[:l])
	QuickSort(slice[l+1:])
}

这种写法不刚好解决了刚才的问题了吗?i和l都是从左往右扫的。这种划分方法用在蓝白双色旗上:游标都从左端开始,当扫到蓝色时,和白色调换。这种方法和上面方法相结合,不是非常完美吗?

整理一下思路:

整体上换蓝白、红双色旗划分,蓝白色为左游标,红色右游标;右游标找蓝白色,左游标找红色,找到后调换;调换后左边蓝白双色划分:如果是蓝色,和最左端的白色调换。

OK,我们尝试这一过程:

123456789

左游标找到1,右游标找到9,调换:

923456781

蓝白色内部判定,9是蓝色,和它前面最早的白色调换,没有,跳过。继续左游标找到3,左游标内部操作:3是蓝色判定,和它前面最早的白色2调换:

932456781

左游标继续找到4,右游标找到7,调换:

932756481

左游标继续找到6,蓝白内部判定:6和最早的白色2调换:

936752481

右游标继续找到2,右游标和左游标相遇,整体流程结束。

代码实现:

type Color int

const (
	Blue  Color = 0
	White Color = 1
	Red   Color = 2
)

func ThreeColorFlags(flags []Color) {
	l := 0
	w := 0
	r := len(flags) - 1
	for l < r {
		for ; l < r && flags[l] != Red; l++ {
			if flags[l] == Blue {
				flags[w], flags[l] = flags[l], flags[w] // swap
				w++ // 首个白色游标+1
			}
		}
		for ; l < r && flags[r] == Red; r-- {
		}
		flags[l], flags[r] = flags[r], flags[l] // swap
	}
}

需要注意的是,这份代码里先找左游标,是因为要优先做内部判定。先找哪边的游标一定要根据实际情况而定,如果将白色和红色组成双色旗,划入右边,则应该先找右游标。

 

到此为止,是所有其他博客都讲到过的东西。下面开始吹牛逼环节。

 

我们通过上面过程,可以看到将两种双色旗划分方法整合,可以解决三色旗问题,那么四色旗问题呢?比如我们加上一种黄色。

上面我们将蓝白划分成一种颜色,红单独是一种颜色,如果我们加一种黄色,是不是可以将红黄看成是一种颜色,蓝白是一种颜色,进行这种划分后,内部再各自划分呢?答案是肯定的。假设我们换蓝、白、黄、红的顺序:

func FourColorFlags(flags []Color) {
	l := 0
	w := 0
	y := len(flags) - 1
	r := len(flags) - 1
	for l < r {
		for ; l < r && (flags[l] == Blue || flags[l] == White); l++ {
			if flags[l] == Blue {
				flags[w], flags[l] = flags[l], flags[w] // swap
				w++ // 首个白色游标+1
			}
		}
		for ; l <= r && (flags[r] == Red || flags[r] == Yellow); r-- {
			if flags[r] == Red {
				flags[y], flags[r] = flags[r], flags[y] // swap
				y-- // 最后一个黄色游标-1
			}
		}
		if l < r {
			flags[l], flags[r] = flags[r], flags[l] // swap
		}
	}
}

还记得上面的注意吗?这次右游标也有内部判定,所以在写条件时,右游标的条件成了 l <= r,这一点值得注意,左边判定完后,一定要保证右边也能判定最中间的那面旗子。

 

更进一步:五色旗问题。现在再加一种紫色。按蓝、白、紫、黄、红的顺序。

四色旗似乎是极限了,已经把所有能用,能重复的都用上了,五色旗怎么办?答案是继续重复上面过程。如果我们把双色旗问题看成是1+1模式,三色旗就可以说是2+1模式,上面四色旗则可以看成是2+2模式,五色旗呢?拆成(2+1)+2模式如何?其中的2+1用三色旗2+1模式中2的模式。希望没把你说蒙,我的意思是说,我们不确定紫色最后会有多少个,所以我们需要使蓝、白、紫的游标都从最左开始计数。这样我们把五色旗分成了双色旗:(蓝白紫)+(红黄),(蓝白紫)内部分成双色旗(蓝白)+紫,(红黄)内部分为红+黄,(蓝白)内部分为蓝+白,基本的双色旗操作我们都很熟练了,所以五色旗也不在话下:

func FiveColorFlags(flags []Color) {
	l := 0
	w := 0
	p := 0
	y := len(flags) - 1
	r := len(flags) - 1
	for l < r {
		for ; l < r && (flags[l] == Blue || flags[l] == White || flags[l] == Purple); l++ {
			i := l
			if flags[i] != Purple {
				flags[p], flags[i] = flags[i], flags[p] // swap
				i = p                                   // 将非紫色投入前一个集合
				p++                                     // 首个紫色游标+1
			}
			// 蓝白双色旗判定
			if flags[i] == Blue {
				flags[w], flags[i] = flags[i], flags[w] // swap
				w++                                     // 首个白色游标+1
			}
		}
		for ; l <= r && (flags[r] == Red || flags[r] == Yellow); r-- {
			if flags[r] == Yellow {
				flags[y], flags[r] = flags[r], flags[y]
				y-- // 最后一个黄色游标-1
			}
		}
		if l < r {
			flags[l], flags[r] = flags[r], flags[l] // swap
		}
	}
}

那六色旗,七色旗,八色旗……呢?是不是都解决了?

且慢,代码不能一直这样无限增长下去。我们发现,旗色越多,越起作用的是什么。很明显,起作用的是第二种快排写法给我们的启示,所以我们是不是可以抛弃从两端划分的方法,统一用从头划分的方法呢?所有游标都从最左端开始,似乎正是我们上面解五色旗时用的方法。我们可以试着将上面代码抽象出来:

func MultiColorFlags(flags []Color) {
	// cursors按旗子颜色的逆序存放起始点,如果旗子颜色是蓝白红,则cursors存放的是红白蓝的游标
	// 因为旗子向前调换的过程是逆序的,这样做方便遍历
	cursors := []int{0}
	for i := 1; i < len(flags); i++ {
		k := i
		for j := 0; j < len(cursors); j++ {
			if flags[k] < flags[cursors[j]] { // 如果该旗子需要放到前面去
				flags[k], flags[cursors[j]] = flags[cursors[j]], flags[k]
				k = cursors[j] // 更新旗子现在的位置
				cursors[j]++   // 更新颜色的起始游标
			} else if flags[cursors[j]] < flags[k] { // 如果该旗子f应该放在该颜色c后面
				// 说明还没有这种旗子f颜色的游标,将该游标插入到颜色c的前面,注意cursors是逆序存储
				cursors = append(cursors[:j], append([]int{k}, cursors[j:]...)...)
				break // 不需要再向前调换了
			} else { // 和前面紧临旗子颜色相同了
				break // 不需要再向前调换了
			}
		}
		if k == 0 { // 如果移动到最头部了
			cursors = append(cursors, 0) // 说明还没有这种颜色的游标
		}
	}
}

这样我们就可以对付任意多色旗问题了。

现在唯一不太好的地方是代码层面的不通用,关于这个可以用标准库sort包改进,sort库定义了Interface:

// A type, typically a collection, that satisfies sort.Interface can be
// sorted by the routines in this package. The methods require that the
// elements of the collection be enumerated by an integer index.
type Interface interface {
	// Len is the number of elements in the collection.
	Len() int
	// Less reports whether the element with
	// index i should sort before the element with index j.
	Less(i, j int) bool
	// Swap swaps the elements with indexes i and j.
	Swap(i, j int)
}

我们可以直接拿来主义:

func Sort(slice sort.Interface) {
	cursors := []int{0}
	for i := 1; i < slice.Len(); i++ {
		k := i
		for j := 0; j < len(cursors); j++ {
			if slice.Less(k, cursors[j]) {
				slice.Swap(k, cursors[j])
				k = cursors[j]
				cursors[j]++
			} else if slice.Less(cursors[j], k) {
				cursors = append(cursors[:j], append([]int{k}, cursors[j:]...)...)
				break
			} else {
				break
			}
		}
		if k == 0 {
			cursors = append(cursors, 0)
		}
	}
}

应用:

type ByColor []Color

func (flags ByColor) Len() int           { return len(flags) }
func (flags ByColor) Less(i, j int) bool { return flags[i] < flags[j] }
func (flags ByColor) Swap(i, j int)      { flags[i], flags[j] = flags[j], flags[i] }

func main() {
	flags := []Color{
		Red, White, Blue, Red, White, Blue, White, Red, Blue,
	}
	Sort(ByColor(flags))
	fmt.Println(flags)
}

最后,再提一下它的时间复杂度,如果有m种不同颜色,n面旗子,则时间复杂度为:O(mn),准确一些是O((m-1)n),比如双色旗问题是O(n),三色旗问题是O(2n)。

 

好了,本篇结束,贴上leetcode第75题.分类颜色地址和题目描述,感兴趣的可以去试试:

    给定一个包含红色、白色和蓝色,一共 n 个元素的数组,原地对它们进行排序,使得相同颜色的元素相邻,并按照红色、白色、蓝色顺序排列。

    此题中,我们使用整数 0、 1 和 2 分别表示红色、白色和蓝色。

注意:
    不能使用代码库中的排序函数来解决这道题。

示例:
输入: [2,0,2,1,1,0]
输出: [0,0,1,1,2,2]

进阶:
    一个直观的解决方案是使用计数排序的两趟扫描算法。
    首先,迭代计算出0、1 和 2 元素的个数,然后按照0、1、2的排序,重写当前数组。
    你能想出一个仅使用常数空间的一趟扫描算法吗?

 

  • 6
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值