我们将整数 x
的 权重 定义为按照下述规则将 x
变成 1
所需要的步数:
如果 x
是偶数,那么 x = x / 2
如果 x
是奇数,那么 x = 3 * x + 1
比方说,x=3 的权重为 7 。因为 3 需要 7 步变成 1 (3 –> 10 –> 5 –> 16 –> 8 –> 4 –> 2 –> 1)。
给你三个整数 lo
, hi
和 k
。你的任务是将区间 [lo, hi]
之间的整数按照它们的权重 **升序排序 **,如果大于等于 2 个整数有 相同 的权重,那么按照数字自身的数值 升序排序 。
请你返回区间 [lo, hi]
之间的整数按权重排序后的第 k
个数。
注意,题目保证对于任意整数 x
(lo <= x <= hi)
,它变成 1
所需要的步数是一个 32 位有符号整数。
示例 1:
**输入:** lo = 12, hi = 15, k = 2
**输出:** 13
**解释:** 12 的权重为 9(12 --> 6 --> 3 --> 10 --> 5 --> 16 --> 8 --> 4 --> 2 --> 1)
13 的权重为 9
14 的权重为 17
15 的权重为 17
区间内的数按权重排序以后的结果为 [12,13,14,15] 。对于 k = 2 ,答案是第二个整数也就是 13 。
注意,12 和 13 有相同的权重,所以我们按照它们本身升序排序。14 和 15 同理。
示例 2:
**输入:** lo = 7, hi = 11, k = 4
**输出:** 7
**解释:** 区间内整数 [7, 8, 9, 10, 11] 对应的权重为 [16, 3, 19, 6, 14] 。
按权重排序后得到的结果为 [8, 10, 11, 7, 9] 。
排序后数组中第 4 个数字为 7 。
提示:
1 <= lo <= hi <= 1000
1 <= k <= hi - lo + 1
题目分析 我们要按照权重为第一关键字,原值为第二关键字对区间 [lo, hi]
进行排序,关键在于我们怎么求权重。
方法一:递归 思路
记 x 的权重为 f(x),按照题意很明显我们可以构造这样的递归式:
f(x) = \left { \begin{aligned} 0 &, & x = 1 \ f(3x + 1) + 1 &, & x \bmod{2} = 1 \ f(x}{2}) + 1 &, & x \bmod{2} = 0 \end{aligned} \right .
于是我们就可以递归求解每个数字的权重了。
代码
[sol1-C++] 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 class Solution {public : int getF (int x) { if (x == 1 ) return 0 ; if (x & 1 ) return getF (x * 3 + 1 ) + 1 ; else return getF (x / 2 ) + 1 ; } int getKth (int lo, int hi, int k) { vector <int > v; for (int i = lo; i <= hi; ++i) v.push_back (i); sort (v.begin (), v.end (), [&] (int u, int v) { if (getF (u) != getF (v)) return getF (u) < getF (v); else return u < v; }); return v[k - 1 ]; } };
[sol1-Java] 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 class Solution { public int getKth (int lo, int hi, int k) { List<Integer> list = new ArrayList <Integer>(); for (int i = lo; i <= hi; ++i) { list.add(i); } Collections.sort(list, new Comparator <Integer>() { public int compare (Integer u, Integer v) { if (getF(u) != getF(v)) { return getF(u) - getF(v); } else { return u - v; } } }); return list.get(k - 1 ); } public int getF (int x) { if (x == 1 ) { return 0 ; } else if ((x & 1 ) != 0 ) { return getF(x * 3 + 1 ) + 1 ; } else { return getF(x / 2 ) + 1 ; } } }
[sol1-Python3] 1 2 3 4 5 6 7 8 9 10 class Solution : def getKth (self, lo: int , hi: int , k: int ) -> int : def getF (x ): if x == 1 : return 0 return (getF(x * 3 + 1 ) if x % 2 == 1 else getF(x // 2 )) + 1 v = list (range (lo, hi + 1 )) v.sort(key=lambda x: (getF(x), x)) return v[k - 1 ]
复杂度分析
记区间长度为 n,等于 hi - lo + 1
。
时间复杂度:这里的区间一定是 [1, 1000] 的子集,在 [1, 1000] 中权重最大数的权重为 178,即这个递归函数要执行 178 次,所以排序的每次比较的时间代价为 O(178),故渐进时间复杂度为 O(178 \times n \log n)。
空间复杂度:我们使用了长度为 n 的数组辅助进行排序,同时再使用递归计算权重时最多会使用 178 层的栈空间,故渐进空间复杂度为 O(n + 178)。
方法二:记忆化 思路
我们知道在求 f(3) 的时候会调用到 f(10),在求 f(20) 的时候也会调用到 f(10)。同样的,如果单纯递归计算权重的话,会存在很多重复计算,我们可以用记忆化的方式来加速这个过程,即「先查表,再计算」和「先记忆,再返回」。我们可以用一个哈希映射作为这里的记忆化的「表」,这样保证每个元素的权值只被计算 1 次。在 [1, 1000] 中所有 x 求 f(x) 的值的过程中,只可能出现 2228 种 x,于是效率就会大大提高。
代码如下。
代码
[sol2-C++] 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 class Solution {public : unordered_map <int , int > f; int getF (int x) { if (f.find (x) != f.end ()) return f[x]; if (x == 1 ) return f[x] = 0 ; if (x & 1 ) return f[x] = getF (x * 3 + 1 ) + 1 ; else return f[x] = getF (x / 2 ) + 1 ; } int getKth (int lo, int hi, int k) { vector <int > v; for (int i = lo; i <= hi; ++i) v.push_back (i); sort (v.begin (), v.end (), [&] (int u, int v) { if (getF (u) != getF (v)) return getF (u) < getF (v); else return u < v; }); return v[k - 1 ]; } };
[sol2-Java] 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 class Solution { Map<Integer, Integer> f = new HashMap <Integer, Integer>(); public int getKth (int lo, int hi, int k) { List<Integer> list = new ArrayList <Integer>(); for (int i = lo; i <= hi; ++i) { list.add(i); } Collections.sort(list, new Comparator <Integer>() { public int compare (Integer u, Integer v) { if (getF(u) != getF(v)) { return getF(u) - getF(v); } else { return u - v; } } }); return list.get(k - 1 ); } public int getF (int x) { if (!f.containsKey(x)) { if (x == 1 ) { f.put(x, 0 ); } else if ((x & 1 ) != 0 ) { f.put(x, getF(x * 3 + 1 ) + 1 ); } else { f.put(x, getF(x / 2 ) + 1 ); } } return f.get(x); } }
[sol2-Python3] 1 2 3 4 5 6 7 8 9 10 11 12 13 class Solution : def getKth (self, lo: int , hi: int , k: int ) -> int : f = {1 : 0 } def getF (x ): if x in f: return f[x] f[x] = (getF(x * 3 + 1 ) if x % 2 == 1 else getF(x // 2 )) + 1 return f[x] v = list (range (lo, hi + 1 )) v.sort(key=lambda x: (getF(x), x)) return v[k - 1 ]
复杂度分析
时间复杂度:平均情况下比较的次数为 n \log n,把 2228 次平摊到每一次的时间代价为 O(2228}{n \log n}),故总时间代价为 O(2228}{n \log n} \times n \log n) = O(2228)。
空间复杂度:我们使用了长度为 n 的数组辅助进行排序,哈希映射只可能存在 2228 种键,故渐进空间复杂度为 O(n + 2228)。由于这里我们使用了记忆化,因此递归使用的栈空间层数会均摊到所有的 n 中,由于 n 的最大值为 1000,因此每一个 n 使用的栈空间为 O(2228/1000}) \approx O(2),相较于排序的哈希映射需要的空间可以忽略不计。