Skip to content

Commit 49740f5

Browse files
author
Obsession-kai
committed
update day29/31
1 parent 175ad4b commit 49740f5

File tree

3 files changed

+224
-0
lines changed

3 files changed

+224
-0
lines changed
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
// 题目链接:https://leetcode.cn/problems/nge-tou-zi-de-dian-shu-lcof/?envType=study-plan&id=lcof
2+
// day28/31
3+
// 第 28 天主题为:搜索与回溯算法(困难)
4+
// 包含三道题目:
5+
// 剑指offer19.正则表达式匹配
6+
// 剑指offer49.丑数
7+
// 剑指offer60.n个骰子的点数
8+
package main
9+
10+
//这道题我的第一反应是做排列组合问题。看题解才知道是 dp 问题。
11+
12+
//1 个骰子共有 6 中可能的点数,两个骰子点数范围为 [2,12],共有 11 种可能的点数,对 n 个骰子,点数范围为 [n,6*n],共有 5 * n + 1 种可能的点数。
13+
//
14+
//如果我们知道了 x-1 个骰子所有的点数及其概率,那 x 个骰子的点数及其概率可以通过其推出。对 x 个骰子,也就是在 x-1 的基础上添加一个骰子,
15+
//该骰子的点数可为 1-6,每个点数的概率均为 1/6,那我们可以对 x-1 个骰子的点数进行遍历,再对 1-6 开启第二层遍历,设当前遍历到 x-1 个骰子的点数为 m,
16+
//第 x 个骰子遍历到的点数为 n,那么 dp[i][m+n] += dp[x-1][m]/6
17+
//
18+
//动态规划三步:
19+
//1. 确定dp数组及下标含义:dp[i][j] 代表 i 个骰子 点数为 j 的概率,则 len(dp) = n+1, len(dp[0]) = 6*n+1
20+
//2. 数组初始化:一个骰子的点数及其概率我们是已知的,dp[1][1] - dp[1][6] 的值均为 1/6,dp 数组中其余值初始化为 0 即可,其余有效信息将由 1 个骰子的点数及其概率得出。
21+
//3. 状态转移方程
22+
//
23+
//dp[n][x]=\sum_{i=1}^6dp[n-1][x-i]*1/6
24+
//
25+
//可以看出,该状态转移方程是一个理论上合理的方案,但用此公式求解的话,需要考虑越界的问题,是一种逆向的递推公式,我们将其改为正向即可,
26+
//通过 x-1 个骰子的点数推导 x 个骰子的点数。
27+
28+
func dicesProbability(n int) []float64 {
29+
// dp数组初始化
30+
dp := make([][]float64,n+1)
31+
for i:=0;i<n+1;i++{
32+
dp[i] = make([]float64,6*n+1)
33+
}
34+
for i:=1;i<=6;i++{
35+
dp[1][i] = float64(1)/float64(6)
36+
}
37+
// 已知1个骰子的点数及其概率,向后推导至n个骰子的点数
38+
for i:=2;i<=n;i++{
39+
// 通过 i-1 个骰子的点数,求第 i 个骰子的点数
40+
for j:=i-1;j<=6*(i-1);j++{
41+
// 每个骰子的点数为1-6,概率均为1/6
42+
for k:=1;k<=6;k++{
43+
dp[i][j+k] += (dp[i-1][j])/6
44+
}
45+
}
46+
}
47+
return dp[n][n:]
48+
}
49+
50+
//另外,感觉这道题是 dp 思路很好的练习题目
51+
//一个阶段有个多个状态,且当前阶段的每个状态会影响到下一个阶段的多个状态(至多6个)
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
//题目链接:https://leetcode.cn/problems/chou-shu-lcof/?envType=study-plan&id=lcof
2+
package main
3+
4+
import "container/heap"
5+
6+
//丑数的递推性质: 丑数只包含因子 2, 3, 5,因此有 “丑数 = 某较小丑数 * 某因子” (因子为 2、3、5)。
7+
//该性质是解题的关键
8+
9+
//刚开始我们只拥有丑数 1,然后 经过 1 与因子 2、3、5 相乘,得到三个丑数,将该三个丑数再次与因子相乘,
10+
//又得到部分丑数(数目无法确定,因为可能有重复出现的数字),我们不断经过此步骤,可以得到很多丑数
11+
//
12+
//那如何得到第 n 个呢?
13+
//上面得到的丑数是无序的,我们可以使用动态规划得到排好序的丑数,丑数数字 dp 初始化时只有一个元素 1,三个因子的指针 p2、p3、p5 均指向 1,
14+
//计算得到 三个因子指向的丑数 与 该因子的 乘积,取最小值,就是下一个丑数,然后找到对应的指针,将该指针自增1,指向下一个丑数,
15+
//每次可以得到一个丑数,n-1次循环可得到第 n 个丑数。
16+
//
17+
//动态规划三步:
18+
//1. 定义dp数组大小及下表含义:dp[i] 代表第 i 个丑数
19+
//2. dp 数组状态初始化:dp[1] = 1,三个指针 p2,p3,p5=1,1,1
20+
//3. 状态转移方程,dp[i] = min(dp[p2]*2,dp[p3]\*3,dp[p5]\*5),之后找到对应的指针,将该指针自增1
21+
22+
func nthUglyNumber(n int) int {
23+
dp := make([]int,n+1)
24+
// dp 数组初始化,只有 1 一个丑数
25+
dp[1] = 1
26+
// 三个指针初始化指向第一个丑数
27+
p2,p3,p5:= 1,1,1
28+
for i:=2;i<=n;i++{
29+
// 寻找三个指针指向元素与对应因子乘积的最小值
30+
num := min(dp[p2]*2,min(dp[p3]*3,dp[p5]*5))
31+
dp[i] = num
32+
// 找到对应指针,该指针右移(即自增1)
33+
// 可能对应不止一个指针
34+
if num == dp[p2] * 2{
35+
p2++
36+
}
37+
if num == dp[p3] * 3{
38+
p3++
39+
}
40+
if num == dp[p5] * 5{
41+
p5++
42+
}
43+
}
44+
return dp[n]
45+
}
46+
47+
func min(x, y int) int {
48+
if x < y{
49+
return x
50+
}
51+
return y
52+
}
53+
54+
55+
//此题我们还可以用小根堆来解决
56+
//
57+
//初始时堆为中只有第一个丑数 1 。
58+
//
59+
//每次取出堆顶元素 x,则 x 是堆中最小的丑数,由于 2x, 3x, 5x 也是丑数,因此将 2x, 3x, 5x 加入堆。
60+
//
61+
//上述做法会导致堆中出现重复元素的情况。为了避免重复元素,可以使用哈希集合去重,避免相同元素多次加入堆。
62+
//
63+
//在排除重复元素的情况下,第 n 次从最小堆中取出的元素即为第 n 个丑数。
64+
65+
type maxH []int
66+
67+
func (this maxH) Len() int{ return len(this)}
68+
func (this maxH) Less (i,j int) bool {
69+
return this[i] < this[j]
70+
}
71+
func (this maxH) Swap (i,j int){
72+
this[i],this[j] = this[j],this[i]
73+
}
74+
func (this *maxH) Push(v interface{}){
75+
*this = append(*this,v.(int))
76+
}
77+
func (this *maxH) Pop() interface{}{
78+
old := *this
79+
n := len(old)
80+
res := old[n-1]
81+
*this = old[:n-1]
82+
return res
83+
}
84+
85+
86+
func nthUglyNumber_2(n int) int {
87+
uglys := &maxH{1}
88+
factors := []int{2,3,5}
89+
record := map[int]struct{}{}
90+
record[1] = struct{}{}
91+
for i:=1;;i++{
92+
num := heap.Pop(uglys).(int)
93+
if i == n{
94+
return num
95+
}
96+
for _,f := range factors{
97+
next := num * f
98+
if _,has := record[next];!has{
99+
heap.Push(uglys,next)
100+
record[next] = struct{}{}
101+
}
102+
}
103+
}
104+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
//题目链接:https://leetcode.cn/problems/zheng-ze-biao-da-shi-pi-pei-lcof/?envType=study-plan&id=lcof
2+
package main
3+
4+
//本题解参考自LeetCode官方题解
5+
6+
//题目中的匹配是一个「逐步匹配」的过程:我们每次从字符串 p 中取出一个字符或者「字符 + 星号」的组合,
7+
//并在 s 中进行匹配。对于 p 中一个字符而言,它只能在 s 中匹配一个字符,匹配的方法具有唯一性;而对于 p 中字符 + 星号的组合而言,
8+
//它可以在 s 中匹配任意自然数个字符,并不具有唯一性。因此我们可以考虑使用动态规划,对匹配的方案进行枚举。
9+
//
10+
//我们用 dp[i][j] 表示 s 的前 i 个字符与 p 中的前 j 个字符是否能够匹配。在进行状态转移时,我们考虑 p 的第 j 个字符的匹配情况:
11+
//如果 p 的第 j 个字符是一个小写字母,那么我们必须在 s 中匹配一个相同的小写字母
12+
// 如果 s 的第 i 个字符与 p 的第 j 个字符不相同,那么无法进行匹配;
13+
// 否则我们可以匹配两个字符串的最后一个字符,完整的匹配结果取决于两个字符串前面的部分,dp[i][j] =dp[i-1][j-1]
14+
//
15+
//若 p 的第 j 个字符为 ‘*’,我们可以对 p 的第 j-1 个字符匹配任意次,匹配 0 次的情况下,有 dp[i][j] = dp[i][j-2],
16+
//在匹配 s中字符 1、2 次的情况下,有 dp[i][j] = dp[i-1][j-2]、dp[i][j]= dp[i-2][j-2],
17+
//如果用这种方式进行转移,那么我们就需要枚举这个组合到底匹配了 s 中的几个字符,会增导致时间复杂度增加,并且代码编写起来十分麻烦。
18+
//我们不妨换个角度考虑这个问题:字母 + 星号的组合在匹配的过程中,本质上只会有两种情况:
19+
//- 匹配 s 末尾的一个字符,将该字符扔掉,而该组合还可以继续进行匹配;
20+
//- 不匹配字符,将该组合扔掉,不再进行匹配。
21+
//
22+
//最终的状态转移方程如下:
23+
//- if p[j]!='*' && match(i,j),dp\[i\]\[j\] = dp\[i-1\]\[j-1\]
24+
//- if p[j]!='*' && !match(i,j),dp\[i\]\[j\] = false
25+
//- if p[j] == '*' && match(i,j-1),dp\[i\]\[j\] = dp\[i\]\[j-2\]
26+
//- if p[j] == '*' && !match(i,j-1),dp\[i\]\[j\] = dp\[i-1\]\[j\] || dp\[i\]\[j-2\]
27+
//其中,match(i,j) 是判断 s[i-1] 与 p[j-1] 是否匹配的辅助函数.
28+
29+
//字符串的字符下标是从 0 开始的,因此在实现上面的状态转移方程时,需要注意状态中每一维下标与实际字符下标的对应关系。
30+
31+
func isMatch(s string, p string) bool {
32+
m,n := len(s),len(p)
33+
dp := make([][]bool,m+1)
34+
for i:=0;i<=m;i++{
35+
dp[i] = make([]bool,n+1)
36+
}
37+
// 状态初始化,模式和正则表达式均为空时,匹配成功
38+
dp[0][0] = true
39+
// match 用于匹配单个字符
40+
// s[i-1] 与 p[j-1] 是否匹配成功
41+
match := func(i,j int) bool {
42+
if i == 0{
43+
return false
44+
}
45+
if p[j-1] == '.'{
46+
return true
47+
}
48+
return s[i-1] == p[j-1]
49+
}
50+
// i 从 0,j 从 1 开始遍历
51+
// 因为当s不为空,而p为空的时候,匹配一定失败
52+
for i:=0;i<=m;i++{
53+
for j:=1;j<=n;j++{
54+
// 对应正则为 字符+* 组合
55+
if p[j-1] == '*'{
56+
dp[i][j] = dp[i][j-2]
57+
if match(i,j-1){
58+
dp[i][j] = dp[i][j] || dp[i-1][j]
59+
}
60+
} else {
61+
// 对应 正则为单个字符 的情况
62+
if match(i,j){
63+
dp[i][j] = dp[i-1][j-1]
64+
}
65+
}
66+
}
67+
}
68+
return dp[m][n]
69+
}

0 commit comments

Comments
 (0)