LeetCode 1997. 访问完所有房间的第一天

1997. 访问完所有房间的第一天

[中等]

你需要访问 n 个房间,房间从 0n - 1 编号。同时,每一天都有一个日期编号,从 0 开始,依天数递增。你每天都会访问一个房间。

最开始的第 0 天,你访问 0 号房间。给你一个长度为 n下标从 0 开始 的数组 nextVisit 。在接下来的几天中,你访问房间的 次序 将根据下面的 规则 决定:

  • 假设某一天,你访问 i 号房间。
  • 如果算上本次访问,访问 i 号房间的次数为 奇数 ,那么 第二天 需要访问 nextVisit[i] 所指定的房间,其中 0 <= nextVisit[i] <= i
  • 如果算上本次访问,访问 i 号房间的次数为 偶数 ,那么 第二天 需要访问 (i + 1) mod n 号房间。

请返回你访问完所有房间的第一天的日期编号。题目数据保证总是存在这样的一天。由于答案可能很大,返回对 109 + 7 取余后的结果。

示例 1:

输入: nextVisit = [0,0]
输出: 2
解释:

  • 第 0 天,你访问房间 0 。访问 0 号房间的总次数为 1 ,次数为奇数。
    下一天你需要访问房间的编号是 nextVisit[0] = 0
  • 第 1 天,你访问房间 0 。访问 0 号房间的总次数为 2 ,次数为偶数。
    下一天你需要访问房间的编号是 (0 + 1) mod 2 = 1
  • 第 2 天,你访问房间 1 。这是你第一次完成访问所有房间的那天。

示例 2:

输入: nextVisit = [0,0,2]
输出: 6
解释:
你每天访问房间的次序是 [0,0,1,0,0,1,2,...]
第 6 天是你访问完所有房间的第一天。

示例 3:

输入: nextVisit = [0,1,2,0]
输出: 6
解释:
你每天访问房间的次序是 [0,0,1,1,2,2,3,...]
第 6 天是你访问完所有房间的第一天。

提示:

  • n == nextVisit.length
  • 2 <= n <= 105
  • 0 <= nextVisit[i] <= i

这题的题目光读懂就需要一点时间了,我第一次看的时候知道很复杂所以打算先靠暴力能过多少测试用例,结果题目就理解错了,我以为状态的转移是依赖于访问房间 i i i 时天数是奇数还是偶数,结果是访问到的那个房间访问次数是奇数还是偶数,这不得开一个对应长度的数组或者哈希表来记录访问次数?根据这个向前推进的模式,还不知道每间房会不会访问太多次以至于需要取模否则溢出呢。

func firstDayBeenInAllRooms(nextVisit []int) int {
    var n int = len(nextVisit)

    var mod int = int(1e9+7)
    var day int = 0
    var idx int = 0
    var visit map[int]int = make(map[int]int)

    for len(visit) < n {
        visit[idx]++
        day = (day+1)%mod
        if visit[idx] % 2 == 0 {
            // even day
            idx = (idx+1)%n
        } else {
            // odd day
            idx = nextVisit[idx]
        }
    }

    return day-1
}

好家伙,不出所料,超时了,而且能过的测试用例还不多,只有 12% 左右……

再想想,访问房间 i i i 的时候,只能是从前面的房间访问,那么这就是典型的依赖于子问题的形式,所以很容易想到这题的正确做法是动态规划。但是看题目的这个访问房间的模式,状态转移方程感觉不会很容易想出来……

我自己乱写的一个动态规划:

func firstDayBeenInAllRooms(nextVisit []int) int {
    var n int = len(nextVisit)
    var mod int = int(1e9+7)
    var dp []int = make([]int, n)
    dp[0] = 1
    for i := 1; i < n; i++ {
        if dp[i-1] % 2 == 0 {
            // visit on even
            dp[i] = (dp[i-1]+1)%mod
        } else {
            // visit on odd
            if nextVisit[i-1] == i-1 {
                dp[i] = dp[i-1]+2
            } else {
                dp[i] = dp[i-1]-dp[nextVisit[i-1]]+dp[i-1]+1
            }
        }
    }
    fmt.Println(dp)
    return dp[n-1]
}

答案肯定是不对的,不过我感觉应该是这个思路了,只是不知道细节在哪里错了。首先如果访问到房间 i i i 的次数是奇数,那么接下来只能访问的房间范围是 [ 0 ,   i ] [0,\ i] [0, i] ,整体上来看就是无法继续前进,只能回退到之前的房间,最好情况就是原地不动,再来访问一次;如果访问了房间 i i i 之后,其访问次数是偶数,那么我们可以前进一个位置。根据上面的发现,只有当访问了房间 i i i 偶数次之后,我们才能前进。因此如果想要访问房间 i i i ,则我们需要知道以最小的偶数次访问房间 i − 1 i-1 i1 的天数是第几天。如果第 i − 1 i-1 i1 间房在第 x x x 天访问到(第一次访问只能是奇数次访问,因为 1 是奇数啊),则下一次访问第 i − 1 i-1 i1 间房就是这个房间偶数次被访问了,那么中间会相隔多少天呢?我们发现给定的 n e x t V i s i t nextVisit nextVisit 数组中的位置都是固定的,不存在随机性,所以如果我们回退到之前已经访问过的某一间房间,则肯定还是以同样的访问路径再走一遍最后回到当前房间,所以假设第一次访问 n e x t V i s i t [ i ] nextVisit[i] nextVisit[i] 房间的天数是 y y y ,则回到房间 i i i 需要的天数是 x − y x-y xy 天,然后再加一天来访问第 i + 1 i+1 i+1 间房,所以第 i + 1 i+1 i+1 间房间的第一次访问天数是 x + x − y + 1 x+x-y+1 x+xy+1

好吧,上述的思路至少我是这么想的,不过测试用例过不了就是了,说明肯定是哪里有问题,应该是动态规划的数组没有正确的初始化导致的。但是第 0 0 0 号房间的第一次访问就是第一天啊,初始化不就是 dp[0] = 1 嚒……

官方题解

实在是想不出来有什么问题,所以看了看官方的解法,也是动态规划,并且核心关键也是我想到的那样:回到先前已经访问过的房间,经过相同的天数差就能回到当前房间了。不过官解的动态规划数组的含义和我的思路不一样,官解的 dp 数组记录的是第一次以偶数次访问房间的天数……

官解给出了两个规律:

  1. 首次到达房间 i i i 时一定是奇数次(1 次),那么接下来必定回退到 [ 0 ,   i ] [0,\ i] [0, i]
  2. 首次到达房间 i i i 时,房间 [ 0 ,   i ) [0,\ i) [0, i) 一定都是已经访问过了偶数次

第一条规律我想到了,第二条规律是什么鬼?真的没错吗?我仔细想了一下还真是这样,如果第 i − 1 i-1 i1 间房是奇数次,那么不可能前进到第 i i i 间房间,同理的,既然我们能前进到 i − 1 i-1 i1 ,则 i − 2 i-2 i2 此时也肯定是偶数次,因为我们访问过一间房间并且检测到访问次数变成偶数次之后,才会前进。发生回退操作的时候也是一样的,回退到之前已经访问过的某间房间,导致其访问次数变为奇数,则从那间房间开始也只能回退了,导致更多的房间变为奇数次访问,它们都只能执行回退操作,直到它们各自都再次被访问一次,访问次数再次变为偶数次,才能前进,才能回溯回到最远的位置。

所以我们定义 d p [ i ] dp[i] dp[i] 为奇数次访问房间 i i i 之后,前进到房间 i + 1 i+1 i+1 所需花费的天数(第一次前进到房间 i + 1 i+1 i+1 则是访问次数为 1 ,也是一个奇数,因此 d p [ i + 1 ] dp[i+1] dp[i+1] 仍然是再奇数次访问的基础上)。奇数次访问了房间 i i i ,则回退到 n e x t V i s i t [ i ] nextVisit[i] nextVisit[i] ,那么前进到 n e x t V i s i t [ i ] + 1 nextVisit[i]+1 nextVisit[i]+1 号房间需要 d p [ n e x t V i s i t [ i ] ] dp[nextVisit[i]] dp[nextVisit[i]] 天,以此类推,再次返回位置 i i i 所需要的天数求和公式是(设 n e x t V i s i t [ i ] = b a c k nextVisit[i]=back nextVisit[i]=back ):
d p [ i ] = ∑ j = b a c k i − 1 d p [ j ] dp[i] = \sum_{j=back}^{i-1} dp[j] dp[i]=j=backi1dp[j]
注意,我们回到 b a c k back back 位置以及前进到 i + 1 i+1 i+1 位置还需要额外的各花费 1 天,所以最后还要 + 2 +2 +2

我们如果每次计算 d p [ i ] dp[i] dp[i] 都需要这样回退的话,则最坏情况是 O ( n 2 ) O(n^2) O(n2) 的(回退到位置 0 ,则每次都要过一遍数组,到当前位置的所有元素)。我们发现 d p dp dp 数组一旦完成计算,其结果就是不变的,则可以使用前缀和完成 O ( 1 ) O(1) O(1) 的区间求和问题。

我们重新定义 d p [ i ] dp[i] dp[i] 所表示的含义:从第 0 天开始到达房间 i i i 所花费的天数,

func firstDayBeenInAllRooms(nextVisit []int) int {
    var mod int = int(1e9+7)
    var n int = len(nextVisit)
    var dp []int = make([]int, n)

    dp[0] = 2 // 第一次访问房间 1 需要 2 天
    for i := 1; i < n; i++ {
        var backIdx int = nextVisit[i]
        dp[i] = dp[i-1]+2

        if backIdx != 0 {
            dp[i] = (dp[i] - dp[backIdx-1] + mod) % mod // avoid negative
        }
        dp[i] = (dp[i] + dp[i-1]) % mod
    }
    return dp[n-1-1]
}

解释一下这段代码。注意,其中一些操作是由于我们使用了前缀和计算区间和的方式。比如第一行 dp[i] = dp[i-1]+2 ,这里就是刚才分析提到的额外的两天。然后这一段:

if backIdx != 0 {
	dp[i] = (dp[i] - dp[backIdx-1] + mod) % mod // avoid negative
}

就是常见的前缀和数组求区间和的方式,前缀和数组中求区间 [ b a c k ,   i ] [back,\ i] [back, i] 的区间和,则需要 p r e f i x S u m [ i ] − p r e f i x S u m [ b a c k − 1 ] prefixSum[i] - prefixSum[back-1] prefixSum[i]prefixSum[back1] 。当我们处于位置 0 的时候,无法求前缀和。

然后,到目前为止 d p [ i ] dp[i] dp[i] 计算的都还是差值,现在我们需要把 d p [ i ] dp[i] dp[i] 转为前缀和累积量,因此需要在 d p [ i − 1 ] dp[i-1] dp[i1] 的基础上累积。

最后,我们需要得到房间 n − 1 n-1 n1 的访问天数,则对应的动态数组位置是 n − 2 n-2 n2

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值