美团点评的最后一道笔试题(算法方向,第五道题)
题意:给定一个n(1到100000),m(1到100000),一个长度为n的01数组(只有0和1),然后m行操作,操作要么是查询当前最长不降序列长度,要么是给定x和y,翻转数组中x到y的一段(即位于[x,y]区间的数,0变成1,1变成0)。
题解:我用的是线段树,实现起来三四百行,有更好的办法欢迎交流。
首先考虑一个01数组计算最长不降序列的办法。由于是01序列,所以不降序列具有形式00001111,即前面为0,后面为1的序列(可以没有1或0)。所以只需要对每个位置,计算出当前位置前面的0的个数和后面的1的个数的和,取最大值就是最长不降序列的长度。
接着考虑翻转,我们可以维护上面提到的和值,然后查询的时候取最大即可。问题是翻转的时候怎么维护。设翻转区间为[x,y],则整个数组分成3段,[0,x - 1], [x,y], [y+1, end]
设zero[i]为位置i前面(包括自己)0的个数,为位置i后面(包括自己)1的个数,我们维护的是sum[i] = zero[i] + one[i],以及最大值
对于[0, x-1]这一段,zero[i]不变,而one[i]的改变都一致,增加的为[x,y]这一段中原来0比1多的个数为 -diff(x, y)(设diff(x,y)为[x,y]中1比0多的个数)
对于[y + 1,end]也一样,one[i]不变,zero[i]增加的 个数为diff(x,y)
所以我们可以用另外一个线段树来维护每一段0的个数(线段树最简单的应用),翻转的时候个数等于区间长度减去当前0的个数,就可以求出diff(x,y)。
而且有了这个线段树,我们也可以在log(n)的时间内求出一个zero[i]或者one[i]
最后我们要维护[x,y]这一段。考虑一个数组整个翻转的情况,比如
0100110,翻转成1011001,则zero[i]和onei都发生了什么改变?
0到位置i共有i+1个位置,所以我们只要知道前面原来有多少个1就是翻转后0的个数,即 n e w Z e r o [ i ] = i + 1 − z e r o [ i ] newZero[i] = i + 1 - zero[i] newZero[i]=i+1−zero[i], 同样 n e w O n e [ i ] = n + 1 − i − o n e [ i ] newOne[i] = n + 1 - i - one[i] newOne[i]=n+1−i−one[i]
现在考虑[x,y]是位于中间,将下标左移到0,先减去[0,x-1]的影响,然后最后加回来,得 n e w Z e r o [ i ] = i − x + 1 − ( z e r o [ i ] − z e r o [ x − 1 ] ) + z e r o [ x − 1 ] = i − x + 1 − z e r o [ i ] + 2 ∗ z e r o [ x − 1 ] newZero[i] = i - x + 1 - (zero[i] - zero[x-1]) + zero[x-1]=i-x+1-zero[i] +2*zero[x-1] newZero[i]=i−x+1−(zero[i]−zero[x−1])+zero[x−1]=i−x+1−zero[i]+2∗zero[x−1]
同样 n e w O n e [ i ] = y − i + 1 − o n e [ i ] + 2 ∗ o n e [ y + 1 ] newOne[i] = y - i + 1 - one[i] + 2*one[y+1] newOne[i]=y−i+1−one[i]+2∗one[y+1]
虽然newZero和newOne都和具体的下标i相关,但是它们的和 n e w S u m = n e w Z e r o + n e w O n e [ i ] = ( y − x ) + 2 − s u m [ i ] + 2 ∗ ( z e r o [ x − 1 ] + o n e [ y + 1 ] ) newSum = newZero + newOne[i] = (y-x) + 2 - sum[i]+ 2 * (zero[x - 1] + one[y + 1]) new