leetcode 672之Bulb Switcher II

出处:http://www.fengchang.cc/post/121

原题:

https://leetcode.com/problems/bulb-switcher-ii/description/

我用中文简述一下题干:

上面的四个方块是四个灯泡开关,

下面的一系列椭圆是一组灯泡,初始状态全亮。

上面四个按钮的作用分别是:

全:按一下则所有灯泡切换状态(从亮到黑, 或从黑到亮)

偶:所有偶数位灯泡切换状态

奇:所有奇数位灯泡切换状态

隔三: 所有1+3k(k=0,1,2...)灯泡切换状态(也就是图中黄色标注的这些位置)

现在给定灯泡个数n,以及确定的按开关的次数m(每次只能按一个按钮),求最终灯泡序列的状态组合有可能有多少种。

例如n=1,m=1,则最终灯泡序列的状态可能为[on], [off],即2种,返回2

再如n=2,m=1, 则最终灯泡序列的状态可能为 [on, off], [off, on], [off, off],即3种,返回2.

====以上是我对题干的解读,总感觉有时候Leetcode题干的表述不是那么清晰。不过我这里翻译如此。

解题思路:

关于最终状态的求解,本质上是从初始状态经过一些列操作演化而来,那么如果可以先求出这一系列按开关序列操作本身的组合数x,就相当于确定了最终灯泡状态的上限,再排除这x个组合中可能导致灯泡状态结果重合的情况(即两种不同的操作序列导致同一个灯泡状态的情况),就可以求出最终结果。

求这个操作组合数x,即四个按钮按m次,换一种思路就是:有四个槽,往里投币m次,可重复投同一个槽,最终槽位状态会是里面分别有多少个币。然而再细想一下,这个问题还可以做一个等价的转换,每个槽只要有2个硬币,就可以像俄罗斯方块一样消去,因为同一个按钮按任意偶数次都相当于没按,那么依照这种思路,最终每槽位中可能的状态只能是0或者1,分别代表不按或者按,考虑到槽位总共只有4个,那么槽位状态的组合最多只有2^4=16种,这就是我们要求的最终灯泡状态的上限。这一点很重要,如果不做这样的等价转换,则组合数可能非常大,再从这些组合数中去推敲另一个组合数,计算量就会很大。

那么如何求出具体有哪些按钮操作组合情况呢?既然上面说了上限只有16种,所以是一个跟m规模无关的问题,与其根据m去求,不如拿16种状态一个个去验证, 验证通过即为一种组合情况。为了方便,可以用4位二进制数来代表(从0000到1111),例如1001表示按“全”和“隔三”两个按钮, 1100,代表按“全”和“偶”两个按钮,以此类推。。。有了这种代表方式后,如何验证m次操作可以达到这种等价状态呢?很简单,考虑这个例子:1110这个状态能否通过5次操作达到等价操作?可以,因为这个序列中有3个1,那么我先把5次中的三次按照顺序给按了,剩下两次(偶数次)随便挑一个按钮给按了(上面说了,任意按偶数次按钮等价于没按),根据这个例子可以知道一个原则,我们先看最后这个状态序列码中有多少个1,再拿m去比对,如果m比1的个数还少,那么显然达不到,如果正好相等,那么一定可以达到,如果m大于这个数,则看大出多少,如果大出的数是偶数,则可以达到。

通过以上方式,就可以求出所有m次操作可达的等价操作序列。下一步就是确定两个操作序列应用于n个灯泡时,是否可能达到同一个灯泡序列状态,这一步求解思路如下:假如我们要判断1001和1110这两个操作序列能否使灯泡达到同状态,可以通过找这两个序列码的“编辑距离”来算,所谓“编辑距离”就是从状态A变化到状态B要经过哪些码位变换(即同一位置的状态翻转),例如从1001到1110需要经过哪些变换呢?首先,第一位相同,不需要变换,变换操作记为0,第二位状态不同,变换操作记为1,第三位第四位也不同,都记为1,那么可以记为两种状态的“编辑距离为0111,也就是从1001这个状态切换到1110这个状态,只需要做0111对应的操作即可达到。

那么再回过头来看,我们的目的是求1001和1110这两种操作序列能否达到相同同灯泡状态,而我们又说了通过0111这个操作可以从1001达到1110,那么可想而知,可达相同灯泡状态的等价于说0111这个操作等价于不改变任何灯泡状态。或者换种说法,有三种操作序列A, B, C,分别对应一组按钮操作,并且A+B=C, 如果我们知道A和C对应着同样的灯泡序列,那么B必然意味着等价于啥也没做。而“啥也没做”意味着对任意灯泡,都啥也没做或者做了偶数次同按钮操作。

以上就是全部的思路。代码实现如下:

class Solution:

    def ones(self,num):

        # Note that bin is a built-in

        return bin(num).count('1')

    def reacheable(self, i, m):

        one_count = self.ones(i)

        if(m<one_count):

            return False

        elif (m==one_count):

            return True

        else:

           return ((m-one_count)%2==0)

    def button_composer(self, m):

        raw_list_of_comb = []

        for i in range(0, 16):

            if(self.reacheable(i, m)):

                raw_list_of_comb.append(i)

        return raw_list_of_comb

    def button_compose_reducer(self, n, list_of_comb):

        def pressAllButton(num):

            return (num&8)>0

        def pressEvenButton(num):

            return (num&4)>0

        def pressOddButton(num):

            return (num&2)>0

        def pressTripButton(num):

            return (num&1>0)

        def no_effect(nm, op):

            for index in range(nm):

                opcnt = 0

                label =  index+1

                if(label%2==0 and pressEvenButton(op)):

                    opcnt+=1

                if(label%2!=0 and pressOddButton(op)):

                    opcnt+=1

                if((label-1)%3==0 and pressTripButton(op)):

                    opcnt+=1

                if(pressAllButton(op)):

                    opcnt+=1

                if(opcnt%2!=0):

                    return False

            return True

 

        def same_effect(nm, elm, elm2):

            distance_op = elm ^ elm2

            return no_effect(nm, distance_op)

 

        base_set = [list_of_comb[0]]

        for i in range(1, len(list_of_comb)):

            breakout = False

            for el in base_set:

                if (same_effect(n, el, list_of_comb[i])):

                    breakout=True

                    break;

            if(not breakout):

                base_set.append(list_of_comb[i])

        return len(base_set)

 

    def flipLights(self, n, m):

        """

        :type n: int

        :type m: int

        :rtype: int

        """

        raw_list_of_comb = self.button_composer(m)

        result = self.button_compose_reducer(n, raw_list_of_comb)

        return result

 

 

sol = Solution()

print(sol.flipLights(2,1))

已通过leetcode online judge

后评:感觉这个初始状态全亮的条件对解题无用,所以我猜这个题还会有续集,要么就是题出得不好。

思维之思维:

本题做了几次等价转换,单步来看都不是什么很难理解的玩意儿,但是要想到这些等价转换,需要一定的直觉,例如如何判断两种不同的操作序列是否会导致相同的灯状态,参考的是编辑距离的概念,虽然这个概念最初主要用于文本分析,但是其本质上的思路在本题中可以有完全一样的体现;另一个角度,初始状态+变化操作=>末状态是一种对事物变化本质的思考,有了这个思考,可以忽略中间变化的细节,只看首尾两个状态的差异,只要这个差异是一定的,那么这个中间的变化操作即使有多种,那么也可以认为是等价的,这一点,类似于物理中保守场,在保守场中从A点移动到B点,无论中间经过什么样不同的路径,保守场对物体做功都不变。如果这种思维方式已经内化于心,那么产生以上的直觉就变成自然而然的了。

猜你喜欢

转载自blog.csdn.net/xunileida/article/details/83268029