LeetCode 818. Race Car 折返剪枝

231 篇文章 0 订阅

Your car starts at position 0 and speed +1 on an infinite number line.  (Your car can go into negative positions.)

Your car drives automatically according to a sequence of instructions A (accelerate) and R (reverse).

When you get an instruction "A", your car does the following: position += speed, speed *= 2.

When you get an instruction "R", your car does the following: if your speed is positive then speed = -1 , otherwise speed = 1.  (Your position stays the same.)

For example, after commands "AAR", your car goes to positions 0->1->3->3, and your speed goes to 1->2->4->-1.

Now for some target position, say the length of the shortest sequence of instructions to get there.

Example 1:
Input: 
target = 3
Output: 2
Explanation: 
The shortest instruction sequence is "AA".
Your position goes from 0->1->3.
Example 2:
Input: 
target = 6
Output: 5
Explanation: 
The shortest instruction sequence is "AAARA".
Your position goes from 0->1->3->7->7->6.

 

Note:

  • 1 <= target <= 10000.

---------------------------------------------------------------

上来直接BFS,TLE,然后开始想应该怎么剪枝。。。剪的思路并不容易,看了官方中英文题解感觉都不是非常严格(中文题解的说明还有文案错误),这里详细写一下,首先要明白整个序列的可能操作:

  1. 整个序列不能以R结尾,因为R并不会移动距离,这点并不难想
  2. 连续的R操作可以看做是中间A了0次,实现了降速的效果,再多连续的R也没有任何意义,也就是RA{0}R
  3. 整个序列如果以R开头,都可以等效替代。例如RA{k1}RA{k2}...RA{kn}这个序列,可以用A{k2}...RA{kn}RA{k1}这个序列或者A{k2}...RA{kn}RRA{k1}这个序列来替代(虽然不知道整体序列是奇数还是偶数)

所以,整个序列可以表示成A{k1}RA{k2}...RA{kn}的形式,奇数位置是前进档,偶数位置是倒退档。A{k1},A{k3}...A{奇数位}可以互相交换,同理偶数位可以互联交换。因此,奇数位、偶数位都可以排个序,保证k1>=k3>=k5...,同理k2>=k4>=k6...。而且k1>k2,要不整个序列都前进不了。

接下来从距离的角度理解整个序列的操作,连续A k次,刚好距离是2^k-1。如果target对应A{k1}RA{k2}...RA{kn}的某个序列,那么target = [(2^{k1}-1)+(2^{k3}-1)+...]-[(2^{k2}-1)+(2^{k4}-1)+...],如果最小操作次数的情况下(2^{k1}-1)>target,也就是车已经开过了target,这时候[(2^{k2}-1)+(2^{k4}-1)+...]一定大于[(2^{k3}-1)+...],开过了要反向找补回来嘛。(2^{k1}-1)第一次大于target的时候,刚好k1是k+1,其中k是target表示成二进制后最高位1到最低位一共占了多少位,也就是k = math.floor(math.log2(t + 1))。所以如果这个时候如果车还不调头,也就是k1变成了k+2,[(2^{k2}-1)+(2^{k4}-1)+...]需要反向的次数就要更多,这和目前最小操作次数矛盾。因此,最小操作次数的情况下,k1<=k+1,也就是说开车范围要在[0,2^(k+1)]之间

解法一:有了上面这个思路,就可以BFS不超时了。

import math
class Solution:
    def racecar(self, target: int) -> int:
        layers, dic = [[(0, 1)], []], {(0, 1)}
        c, n, step = 0, 1, 0
        bits = math.floor(math.log2(target+1))
        upper = 1<<(bits+1)
        while (layers[c]):
            step += 1
            for pos, speed in layers[c]:
                rspeed = 1 if speed < 0 else -1
                for cmd in ['A', 'R']:
                    npos, nspeed = (pos + speed, speed<<1) if cmd == 'A' else (pos, rspeed)
                    if (npos == target):
                        return step
                    if (npos<=upper and npos>=0 and (npos, nspeed) not in dic):
                        dic.add((npos, nspeed))
                        layers[n].append((npos, nspeed))
            layers[c].clear()
            c, n = n, c

解法二:解法二来自LeetCode官方解法,把每个位置看成节点,连续A k次,刚好距离是2^k-1看做是边,然后用dijstrala求最短路,但是对于这种相邻节点很多的场景,Dijstrala并没有比BFS快,同理也要限定范围,这里贴一下codes,这种解法还是逆向思维,其实并不推荐:

import heapq

class Solution(object):
    def racecar(self, target):
        K = target.bit_length() + 1
        barrier = 1 << K
        pq = [(0, target)]
        dist = [float('inf')] * (2 * barrier + 1)
        dist[target] = 0
        print(K)
        while pq:
            steps, targ = heapq.heappop(pq) #targ表示从起点0开始,经过steps,距离target有多远
            if dist[targ] > steps: continue

            for k in range(K+1):
                walk = (1 << k) - 1
                steps2, targ2 = steps + k + 1, walk - targ
                if walk == targ: steps2 -= 1 #No "R" command if already exact

                if abs(targ2) <= barrier and steps2 < dist[targ2]:
                    heapq.heappush(pq, (steps2, targ2))
                    dist[targ2] = steps2

        return dist[0]

解法三:解法三用DP,但是对最有子问题的挖掘就变得更加深入了。

回到刚才target对应A{k1}RA{k2}...RA{kn}的某个序列,k = math.floor(math.log2(t + 1)),k1<=k+1,这是冲过target的情况。同理,可以证明最优解时候的k1>=k,因为如果k1=k-1,那么正向的冲刺就要降速到0再来一遍,操作次数从k1变成2*k1。所以最有解的k1只有k和k+1两种情况,那么递推表达式来了,利用f(t)表示冲向t时的最小次数,t的范围满足2^k-1 <= t < 2^(k+1)-1

  • 如果连续A操作k1=k次刚好到达t,那么最优解就有了,也就是k1=k 而且 t=2^k-1,那么f(t)=k
  • 如果连续A操作k1=k+1次,也就是冲过了t,那么最优解可能是k+2+f(2^(k+1)-1-t)
  • 如果连续A操作k1=k次,刚好没过t,k1的次数也不能再少了,此时需要调头,调头后冲刺连续操作A共i次,那么最优解可能是k+2+i+f(t-(2^k-1)+(2^i-1))

所以代码是:

import math

class Solution:
    #A{k1}RA{k2}A{k3}...A{kn},结尾一定不带
    def f(self, t, memo):
        k = math.floor(math.log2(t + 1))
        if ((1 << k) - 1 == t):
            return k
        elif (t in memo):
            return memo[t]
        res = k + 2 + self.f((1 << (k + 1)) - 1 - t, memo) #超过target的情况

        #A{k}RA{i}R,剩下的是f(t-((1<<k)-1)+((1<<i)-1))
        for i in range(k):
            res = min(res, k+i+2+self.f(t-((1<<k)-1)+((1<<i)-1),memo))
        memo[t] = res
        return res

    def racecar(self, target: int) -> int:
        memo = {1: 1, 3: 2, 2: 4}
        res = self.f(target, memo)
        return res
 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值