前言
动态规划,就是把问题分解成相同性质规模越小的递推问题,而有时候不仅仅是知道当前的什么状态就可以了,有时候需要对当前进行细分类,从而变成二维dp,或者高维dp。除此之外,中心划分思想也是一个经典的算法思想。
一、使字符串平衡的最少删除次数
二、一题多解
一题多解,打开视野,打开思考角度,训练各种思维方式,完成对问题的深入理解,才能有所提高。
1、动态规划与压缩
定义:f[i][0]为当前以字符a结尾的平衡字符串所删除字符的最少次数;f[i][1]为当前以字符b结尾的平衡字符串所删除字符的最少次数。
当 curCh == 'a’时,f[i][0] = f[i - 1][0];f[i][1] = f[i - 1][1] + 1;
反之,f[i][0] = f[i - 1][0] + 1;f[i][1] = min(f[i - 1][0],f[i - 1][1]);
func minimumDeletions(s string) int {
f := make([][2]int,len(s) + 1)
for i := 1;i <= len(s);i++ {
if s[i - 1] == 'a' {
f[i][0] = f[i - 1][0]
f[i][1] = f[i - 1][1] + 1
}else {
f[i][0] = f[i - 1][0] + 1
f[i][1] = min(f[i - 1][0],f[i - 1][1])
}
}
n := len(s)
if f[n][0] > f[n][1] {
return f[n][1]
}
return f[n][0]
}
func min(x,y int) int {
if x < y {
return x
}
return y
}
状态压缩,
func minimumDeletions(s string) int {
f1,f2 := 0,0
n := len(s)
for i := 0;i < n;i++ {
if s[i] == 'a' {
f2 = f2 + 1
}
if s[i] == 'b' {
f1,f2 =f1 + 1,min(f1,f2)
}
}
return min(f1,f2)
}
func min(x,y int) int {
if x < y {
return x
}
return y
}
总结: 题多自然是优势,更重要的是你在题中学到了多少东西?比如认真分析问题、专心不放任何一处细节、提出好问题和解决方案、学习别人的优雅编码,以及更抽象的思路。
2、中心划分思想
别人的代码,
func minimumDeletions(s string) int {
// b必须在右侧,a必须在左侧,乃平衡字符串。
// 最糟糕的情况 bbaa
// 把不平衡的因子抽象出来,即lb,ra,通过遍历整个字符串,来了解可变因子的情况,在其中记录最少抽象因子个数。
// 最多的情况不过是把a删完/b删完。
// 存看代码看一半天知道是对的一条路径,但是每触摸的到本质,看来源质,才发现是遍历字符串是,在当前字符和右侧相邻字符间画一条线,左边的应该全是a,后边的应该全是b,通过ab因子记录左右情况就知道了。
lb,ra := 0,strings.Count(s,"a")
ans := ra
for _,c := range s {
if c == 'a' {
ra--
}
if t := lb + ra;ans > t {
ans = t
}
if c == 'b' {
lb++
}
}
return ans
}
本质就是,以当前字符与右邻居为中线,看左边有多少个多余的b数lb,右边还剩多少多余的a数ra!
当知道本质是中心划分思想时,我们可以根据源质,另辟蹊径。
给定如下定义,以当前字符与右邻居中间为中线,将字符串分为左子串和右子串,定义左子串中的a为平衡字符(因为能组合成平衡字符串),定义右子串中的b为平衡字符,在当前状态下,其他的都视为非平衡字符。
所以记录左边有多少平衡字符a,右边还剩多少平衡字符b(毕竟是正序遍历,只能看剩余),总字符串为n,那么要删掉的字符就是非平衡字符,一共n - la - rb;
// 知道了源质,就可以开辟出另一条路径
// 遍历字符串,当前字符左侧包括自己的‘a’字符定义为平衡字符,而右边的'b'定义为平衡字符
// 其他左侧b右侧a,在当前划分下,被认定为非平衡字符。
// 中心点划分思想
func minimumDeletions(s string) int {
la,rb := 0,strings.Count(s,"b")
ans,n := len(s) - la - rb,len(s)
for _,c := range s {
if c == 'a' {
la++
}
if c == 'b' {
rb--
}
// la + rb是有效的平衡字符
// n - la - rb,就是当前状态下无法确认的平衡字符--作为无效字符,将其删掉,直接让有效平衡字符组成的字符串平衡。
if t := n - la - rb;ans > t {
ans = t
}
}
return ans
}
// 利用更多潜在规律,比如b和a的ascall码只相差1
func minimumDeletions(s string) int {
num := strings.Count(s,"b") // 平衡字符
n := len(s)
ans := n - num // 总字符数n - 平衡字符num = 不平衡字符数
for _,c := range s {
num += int(('a' - c) * 2 + 1)
if t := n - num;ans > t {
ans = t
}
}
return ans
}
总结
1)动态规划,把问题分解成相同性质规模越小的递推问题,状态压缩,降低空间复杂度,减少没必要的空间浪费。
2)中心划分思想,以当前位置为中心,存储两边的信息(可以使用变量或者数据结构),来判定当前字符串的情况。
3)根据一条解答路径,发现问题的源质(本质)时,就可以从源质出发,另辟蹊径,这也是创新的一种过程,先学表现再溯源最后创新!
4)一题多解,打开视野,打开思考角度,训练各种思维方式,完成对问题的深入理解,才能有所提高。
5)题多自然是优势,更重要的是你在题中学到了多少东西?比如认真分析问题、专心不放任何一处细节、提出好问题和解决方案、学习别人的优雅编码,以及更抽象的思路。
6)利用问题中更多的潜在规律和联系,完成更优雅的编码,如这里b字符和a字符的ascall码只相差1,而每次删除也只删一个,可以写出更加优雅的代码。