[中等]
你需要访问 n
个房间,房间从 0
到 n - 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 i−1 的天数是第几天。如果第 i − 1 i-1 i−1 间房在第 x x x 天访问到(第一次访问只能是奇数次访问,因为 1 是奇数啊),则下一次访问第 i − 1 i-1 i−1 间房就是这个房间偶数次被访问了,那么中间会相隔多少天呢?我们发现给定的 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 x−y 天,然后再加一天来访问第 i + 1 i+1 i+1 间房,所以第 i + 1 i+1 i+1 间房间的第一次访问天数是 x + x − y + 1 x+x-y+1 x+x−y+1 。
好吧,上述的思路至少我是这么想的,不过测试用例过不了就是了,说明肯定是哪里有问题,应该是动态规划的数组没有正确的初始化导致的。但是第
0
0
0 号房间的第一次访问就是第一天啊,初始化不就是 dp[0] = 1
嚒……
官方题解
实在是想不出来有什么问题,所以看了看官方的解法,也是动态规划,并且核心关键也是我想到的那样:回到先前已经访问过的房间,经过相同的天数差就能回到当前房间了。不过官解的动态规划数组的含义和我的思路不一样,官解的 dp
数组记录的是第一次以偶数次访问房间的天数……
官解给出了两个规律:
- 首次到达房间 i i i 时一定是奇数次(1 次),那么接下来必定回退到 [ 0 , i ] [0,\ i] [0, i]
- 首次到达房间 i i i 时,房间 [ 0 , i ) [0,\ i) [0, i) 一定都是已经访问过了偶数次
第一条规律我想到了,第二条规律是什么鬼?真的没错吗?我仔细想了一下还真是这样,如果第 i − 1 i-1 i−1 间房是奇数次,那么不可能前进到第 i i i 间房间,同理的,既然我们能前进到 i − 1 i-1 i−1 ,则 i − 2 i-2 i−2 此时也肯定是偶数次,因为我们访问过一间房间并且检测到访问次数变成偶数次之后,才会前进。发生回退操作的时候也是一样的,回退到之前已经访问过的某间房间,导致其访问次数变为奇数,则从那间房间开始也只能回退了,导致更多的房间变为奇数次访问,它们都只能执行回退操作,直到它们各自都再次被访问一次,访问次数再次变为偶数次,才能前进,才能回溯回到最远的位置。
所以我们定义
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=back∑i−1dp[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[back−1] 。当我们处于位置 0 的时候,无法求前缀和。
然后,到目前为止 d p [ i ] dp[i] dp[i] 计算的都还是差值,现在我们需要把 d p [ i ] dp[i] dp[i] 转为前缀和累积量,因此需要在 d p [ i − 1 ] dp[i-1] dp[i−1] 的基础上累积。
最后,我们需要得到房间 n − 1 n-1 n−1 的访问天数,则对应的动态数组位置是 n − 2 n-2 n−2 。