我报名参加金石计划1期挑战——瓜分10万奖池,这是我的第4篇文章,丑数II [问题本质 & 优先队列 & 动规] - 掘金 (juejin.cn)
前言
抓住问题本质,是解题的关键,也是举一反三的关键。丑数的本质是2x/3x/5x,可采用优先队列选择最小x,也可以动规取得当前最小丑数作为当前状态。
一、丑数II
二、优先队列 & 动规
1、优先队列
// 丑数II
public class NthUglyNumber {
// core:抓住问题本质,1 2x 3x 5x 每次取最小值为x,将2x/3x/5x加入堆中,set去重,取到第n个最小数即可。
public int nthUglyNumber(int n) {
PriorityQueue<Long> que = new PriorityQueue<>();
que.add(1l);
return getNth(que, n);
}
public int getNth(PriorityQueue<Long> que, int n) {
int r = 1;
Set<Long> pool = new HashSet<>();
while (n-- != 0) {
r = que.poll().intValue();
long v1 = r * 2l, v2 = r * 3l, v3 = r * 5l;
if (!pool.contains(v1)) {
que.add(v1);
pool.add(v1);
}
if (!pool.contains(v2)) {
que.add(v2);
pool.add(v2);
}
if (!pool.contains(v3)) {
que.add(v3);
pool.add(v3);
}
}
return r;
}
}
2、动态规划
// 抓住问题本质,进行动态规划。
class NthUglyNumber2 {
// 1 2x 3x 5x 每次取其结果的最小值作为下一个状态,并更新x++
public int nthUglyNumber(int n) {
// 状态定义
long[] f = new long[n + 1]; // f[i]表示第i个丑数。
// 初始化
f[1] = 1;
// 三个指针,分别指着2x/3x/5x的变量x.
int i = 1, j = 1, k = 1;
// 状态转移。
for (int idx = 2; idx <= n; idx++) {
long v1 = 2l * f[i], v2 = 3l * f[j], v3 = 5l * f[k];
// 取最小。
long min = Math.min(v1, Math.min(v2, v3));
// 记录下一个最小丑数。
f[idx] = min;
// 看3个变量x设满足条件,谁++。
if (v1 == min) ++i;
if (v2 == min) ++j;
if (v3 == min) ++k;
}
// 最终状态。
return (int) f[n];
}
}
总结
1)抓住问题本质是关键,选择不同的数据结构是举一反三,丑数的本质为2x/3x/5x为丑数。
2)优先队列,快速获取最小/最大元素;Set的不重复性可以去重。
3)动态规划,找到状态,将大问题转换为规模更小性质相同的子问题,寻找状态转移。
4)int经常溢出,可long替代,而平时的普通数字都是int,如果不带l,那么乘除来的结果就是int,只是最终赋值给了long型。而Long这种包装型就有严格的类型,不会隐式转换,直接编译不通过,Double与Float同理。
参考文献
[1] LeetCode 丑数II