李智‘Blog

读了一些书,看了一些文章,编过一些小例程,搞了一些没有什么技术含量的开发工作。


  • 首页

  • 关于

  • 归档

  • 朋友圈

  • 公益404

  • 搜索

戳气球问题

发表于 2020-09-26 | 分类于 算法
字数统计: 453 | 阅读时长 ≈ 2分钟

312.戳气球

遇到求最值的算法问题,首先要思考的就是:如何穷举出所有可能的结果?

穷举主要有两种算法,就是回溯算法和动态规划,前者就是暴力穷举,而后者是根据状态转移方程推导「状态」。

如何将戳气球问题转化成回溯算法呢?其实就是想穷举戳气球的顺序,不同的戳气球顺序可能得到不同的分数,把所有可能的分数中最高的那个找出来。

回溯算法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
int res = Integer.MIN_VALUE;
/* 输入一组气球,返回戳破它们获得的最大分数 */
int maxCoins(int[] nums) {
backtrack(nums, 0);
return res;
}
/* 回溯算法的伪码解法 */
void backtrack(int[] nums, int socre) {
if (nums 为空) {
res = Math.max(res, score);
return;
}
for (int i = 0; i < nums.length; i++) {
int point = nums[i-1] * nums[i] * nums[i+1];
int temp = nums[i];
// 做选择
在 nums 中删除元素 nums[i]
// 递归回溯
backtrack(nums, score + point);
// 撤销选择
将 temp 还原到 nums[i]
}
}

动态规划

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
int maxCoins(int[] nums) {
int n = nums.length;
// 添加两侧的虚拟气球
int[] points = new int[n + 2];
points[0] = points[n + 1] = 1;
for (int i = 1; i <= n; i++) {
points[i] = nums[i - 1];
}
// base case 已经都被初始化为 0
int[][] dp = new int[n + 2][n + 2];
// 开始状态转移
// i 应该从下往上
for (int i = n; i >= 0; i--) {
// j 应该从左往右
for (int j = i + 1; j < n + 2; j++) {
// 最后戳破的气球是哪个?
for (int k = i + 1; k < j; k++) {
// 择优做选择
dp[i][j] = Math.max(
dp[i][j],
dp[i][k] + dp[k][j] + points[i]*points[j]*points[k]
);
}
}
}
return dp[0][n + 1];
}

编辑距离

发表于 2020-09-24 | 分类于 算法
字数统计: 1,047 | 阅读时长 ≈ 5分钟

72.编辑距离

解决两个字符串的动态规划问题,一般都是用两个指针 i,j 分别指向两个字符串的最后,然后一步步往前走,缩小问题的规模。

1.递归

base case 是 i 走完 s1 或 j 走完 s2,可以直接返回另一个字符串剩下的长度。

对于每对字符 s1[i] 和 s2[j],可以有四种操作:

1
2
3
4
5
6
7
8
if s1[i] == s2[j]:
啥都别做(skip)
i, j 同时向前移动
else:
三选一:
插入(insert)
删除(delete)
替换(replace)

⏬⏬⏬

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
34
35
36
37
38
public int minDistance(String word1, String word2) {
return dp(word1, word2, word1.length() - 1, word2.length() - 1);
}

//返回 s1[0..i] 和 s2[0..j] 的最小编辑距离
int dp(String s1, String s2, int i, int j) {
// base case
if (i == -1) return j + 1;
if (j == -1) return i + 1;

if (s1.charAt(i) == s2.charAt(j)) {
// 本来就相等,不需要任何操作
// s1[0..i] 和 s2[0..j] 的最小编辑距离等于
// s1[0..i-1] 和 s2[0..j-1] 的最小编辑距离
// 也就是说 dp(i, j) 等于 dp(i-1, j-1)
return dp(s1, s2, i - 1, j - 1); // 啥都不做
} else {
// 插入
// 直接在 s1[i] 插入一个和 s2[j] 一样的字符
// 那么 s2[j] 就被匹配了,前移 j,继续跟 i 对比
// 操作数加一

// 删除
// 直接把 s[i] 这个字符删掉
// 前移 i,继续跟 j 对比
// 操作数加一

// 替换
// 直接把 s1[i] 替换成 s2[j],这样它俩就匹配了
// 同时前移 i,j 继续对比
// 操作数加一
return Math.min(
Math.min(dp(s1, s2, i, j - 1) + 1, // 插入
dp(s1, s2, i - 1, j) + 1), // 删除
dp(s1, s2, i - 1, j - 1) + 1 // 替换
);
}
}

这段代码有点小问题就是,这个解法是暴力解法,存在重叠子问题,需要用动态规划技巧来优化。

对于子问题 dp(i-1, j-1),如何通过原问题 dp(i, j) 得到呢?有不止一条路径,比如 dp(i, j) -> #1 和 dp(i, j) -> #2 -> #3。一旦发现一条重复路径,就说明存在巨量重复路径,也就是重叠子问题。

阅读全文 »

动态规划状态压缩

发表于 2020-09-18 | 分类于 算法
字数统计: 2,089 | 阅读时长 ≈ 9分钟

动态规划技巧对于算法效率的提升非常可观,一般来说都能把指数级和阶乘级时间复杂度的算法优化成 O(N^2),但动态规划也是可以进行阶段性优化的,比如说常听说的「状态压缩」技巧,就能够把很多动态规划解法的空间复杂度进一步降低,由 O(N^2) 降低到 O(N)。

阅读全文 »

dp 数组的遍历方向

发表于 2020-09-17 | 分类于 算法
字数统计: 430 | 阅读时长 ≈ 2分钟

做动态规划问题时,肯定会对dp数组的遍历顺序有些头疼,拿二维dp数组来举例

有时候是正向遍历:

1
2
3
4
int[][] dp = new int[m][n];
for (int i = 0; i < m; i++)
for (int j = 0; j < n; j++)
// 计算 dp[i][j]

有时候反向遍历:

1
2
3
for (int i = m - 1; i >= 0; i--)
for (int j = n - 1; j >= 0; j--)
// 计算 dp[i][j]

有时候可能会斜向遍历:

1
2
3
4
5
6
7
// 斜着遍历数组
for (int l = 2; l <= n; l++) {
for (int i = 0; i <= n - l; i++) {
int j = l + i - 1;
// 计算 dp[i][j]
}
}
阅读全文 »

最优子结构

发表于 2020-09-17 | 分类于 算法
字数统计: 1,155 | 阅读时长 ≈ 4分钟

「最优子结构」是某些问题的一种特定性质,并不是动态规划问题专有的。也就是说,很多问题其实都具有最优子结构,只是其中大部分不具有重叠子问题,所以我们不把它们归为动态规划系列问题而已。

先举个很容易理解的例子:假设学校有 10 个班,已经计算出了每个班的最高考试成绩。那么现在要求计算全校最高的成绩,会不会算?当然会,而且不用重新遍历全校学生的分数进行比较,而是只要在这 10 个最高成绩中取最大的就是全校的最高成绩。

提出的这个问题就符合最优子结构:可以从子问题的最优结果推出更大规模问题的最优结果。算每个班的最优成绩就是子问题,知道所有子问题的答案后,就可以借此推出全校学生的最优成绩这个规模更大的问题的答案。

所以看,这么简单的问题都有最优子结构性质,只是因为显然没有重叠子问题,所以我们简单地求最值肯定用不出动态规划。

阅读全文 »

表达式求值算法

发表于 2020-09-15 | 分类于 算法
字数统计: 801 | 阅读时长 ≈ 3分钟

224.基本计算器

227.基本计算器II

1、输入一个字符串,可以包含+ - * / ()、数字、空格,你的算法返回运算结果。

2、要符合运算法则,括号的优先级最高,先乘除后加减。

3、除号是整数除法,无论正负都向 0 取整(5/2=2,-5/2=-2)。

4、可以假定输入的算式一定合法,且计算过程不会出现整型溢出,不会出现除数为 0 的意外情况。

比如输入如下字符串,算法会返回 9:

1
2
>3 * (2-6 /(3 -7))
>

可以看到,这就已经非常接近我们实际生活中使用的计算器了,虽然我们以前肯定都用过计算器,但是如果简单思考一下其算法实现,就会大惊失色:

1、按照常理处理括号,要先计算最内层的括号,然后向外慢慢化简。这个过程我们手算都容易出错,何况写成算法呢!

2、要做到先乘除,后加减,这一点教会小朋友还不算难,但教给计算机恐怕有点困难。

3、要处理空格。我们为了美观,习惯性在数字和运算符之间打个空格,但是计算之中得想办法忽略这些空格。

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
static int i = 0;
public int calculate(String s) {
/*
将 减法、乘法、除法 转换为 加法
某个数 num, 如果前面的对应的运算符是 -,那么 将 -num 压入栈中
这样,我们只需在最后将栈的元素全部弹出,完成加法操作,即可得到最终结果

对于括号,它存在递归性质
即
3 * (2 + 4 * 3) + 2
= 3 * calculate(2 + 4 * 3) + 2
= 3 * 14 + 2
即我们可以将括号内的字符串当作一个运算式,再递归调用本函数,最终返回一个数值
*/
return dfs(s);
}

private int dfs(String s) {
Deque<Integer> stack = new LinkedList<>();

//记录某个连续的数,比如 "42",那么我们首先 num = 4,然后遇到 2 ,num = num * 10 + 2 = 42
int num = 0;
char op = '+';
for (; i < s.length(); i++) {
char ch = s.charAt(i);

//遇到左括号,递归运算内部子式
if (ch == '(') {
++i;
num = dfs(s);
}

if (Character.isDigit(ch)) {
num = num * 10 + (ch - '0');
}
//不是数字,不是空格(运算符 或 '(' 或 ')' ) 或者 到了最后一个字符,那么根据前面记录的 op 操作符 将数字压栈,然后将新的运算符 ch 赋值给 op
if (!Character.isDigit(ch) && ch != ' ' || i == s.length() - 1) {
switch (op) {
case '+':
stack.push(num);
break;
case '-':
stack.push(-num);
break;
case '*':
int pre = stack.pop();
stack.push(pre * num);
break;
case '/':
pre = stack.pop();
stack.push(pre / num);
break;
}
num = 0;
op = ch;
}
/*
遇到右括号,退出循环,然后计算结果, 返回上一层 dfs
这一步写在最后是因为,当 ch 为 右括号 时,那么我们需要先将前面已经得到的 num 压入栈中,再退出循环
*/
if (ch == ')') {
break;
}
}
int res = 0;
while (!stack.isEmpty()) {
res += stack.pop();
}
return res;
}

完全背包问题

发表于 2020-09-14 | 分类于 算法
字数统计: 306 | 阅读时长 ≈ 1分钟

518.零钱兑换II

这个问题转化为背包问题的描述形式:

有一个背包,最大容量为 amount,有一系列物品 coins,每个物品的重量为 coins[i],每个物品的数量无限。请问有多少种方法,能够把背包恰好装满?

若只使用 coins 中的前 i 个硬币的面值,若想凑出金额 j,有 dp[i][j] 种凑法。

如果你不把这第 i 个物品装入背包,也就是说你不使用 coins[i] 这个面值的硬币,那么凑出面额 j 的方法数 dp[i][j] 应该等于 dp[i-1][j],继承之前的结果。

如果你把这第 i 个物品装入了背包,也就是说你使用 coins[i] 这个面值的硬币,那么 dp[i][j] 应该等于 dp[i][j-coins[i-1]]。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public int change(int amount, int[] coins) {
int n = coins.length;
int[][] dp = new int[n + 1][amount + 1];
// base case
for (int i = 0; i <= n; i++)
dp[i][0] = 1;

for (int i = 1; i <= n; i++) {
for (int j = 1; j <= amount; j++)
if (j - coins[i - 1] >= 0)
dp[i][j] = dp[i - 1][j]
+ dp[i][j - coins[i - 1]];
else
dp[i][j] = dp[i - 1][j];
}
return dp[n][amount];
}

子集背包问题

发表于 2020-09-14 | 分类于 算法
字数统计: 342 | 阅读时长 ≈ 2分钟

416.分割等和子集

背包问题大致的描述是什么:

给你一个可装载重量为 W 的背包和 N 个物品,每个物品有重量和价值两个属性。其中第 i 个物品的重量为 wt[i],价值为 val[i],现在让你用这个背包装物品,最多能装的价值是多少?

那么对于这个问题,我们可以先对集合求和,得出 sum,把问题转化为背包问题:

给一个可装载重量为 sum / 2 的背包和 N 个物品,每个物品的重量为** nums[i]。现在让你装物品,是否存在一种装法,能够恰好将背包装满?

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
public boolean canPartition(int[] nums) {
int sum = 0;
for (int num : nums) sum += num;
// 和为奇数时,不可能划分成两个和相等的集合
if (sum % 2 != 0) return false;
int n = nums.length;
sum = sum / 2;
boolean[][] dp = new boolean[n + 1][sum + 1];
Arrays.fill(dp, false);
// base case
for (int i = 0; i <= n; i++)
dp[i][0] = true;

for (int i = 1; i <= n; i++) {
for (int j = 1; j <= sum; j++) {
if (j - nums[i - 1] < 0) {
// 背包容量不足,不能装入第 i 个物品
dp[i][j] = dp[i - 1][j];
} else {
// 装入或不装入背包
dp[i][j] = dp[i - 1][j] || dp[i - 1][j - nums[i - 1]];
}
}
}
return dp[n][sum];
}

高楼扔鸡蛋问题

发表于 2020-09-14 | 分类于 算法
字数统计: 465 | 阅读时长 ≈ 2分钟

887.鸡蛋掉落

1
2
3
4
5
6
7
8
9
10
11
// 当前状态为 (K 个鸡蛋,N 层楼)
// 返回这个状态下的最优结果
int dp(K, N):
int res
for 1 <= i <= N:
res = min(res, 这次在第 i 层楼扔鸡蛋)
return res

base case:
if K == 1: return N
if N == 0: return 0

如果鸡蛋碎了,那么鸡蛋的个数K应该减一,搜索的楼层区间应该从[1..N]变为[1..i-1]共i-1层楼

如果鸡蛋没碎,那么鸡蛋的个数K不变,搜索的楼层区间应该从 [1..N]变为[i+1..N]共N-i层楼

最坏情况下扔鸡蛋的次数,所以鸡蛋在第i层楼碎没碎,取决于哪种情况的结果更大

🔽🔽🔽

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Map<String, Integer> map = new HashMap<>();
int dp(int K, int N) {
int res = Integer.MAX_VALUE;
if (K == 1) return N;
if (N == 0) return 0;
if (map.containsKey(K + "*" + N)) {
return map.get(K + "*" + N);
}
for (int i = 1; i < N + 1; i++) {
// 最坏情况下的最少扔鸡蛋次数
res = Math.min(res,
//最坏情况下
Math.max(dp(K, N - i), // 没碎
dp(K - 1, i - 1)) //碎
+ 1 //在第 i 楼扔了一次
);
}
map.put(K + "*" + N, res);
return res;
}

结果超时了,可以将for循环改成二分搜索

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
Map<String, Integer> map = new HashMap<>();
public int dp2(int K, int N) {
if (!map.containsKey(K+"*"+N)) {
int ans;
if (N == 0)
ans = 0;
else if (K == 1)
ans = N;
else {
int lo = 1, hi = N;
while (lo + 1 < hi) {
int x = (lo + hi) / 2;
int t1 = dp(K - 1, x - 1);
int t2 = dp(K, N - x);

if (t1 < t2)
lo = x;
else if (t1 > t2)
hi = x;
else
lo = hi = x;
}

ans = 1 + Math.min(Math.max(dp(K - 1, lo - 1), dp(K, N - lo)),
Math.max(dp(K - 1, hi - 1), dp(K, N - hi)));
}

map.put(K+"*"+N, ans);
}

return map.get(K+"*"+N);
}

nSum 问题

发表于 2020-09-14 | 分类于 算法
字数统计: 295 | 阅读时长 ≈ 2分钟

1.两数之和

15.三数之和

18.四数之和

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
/**
* 100sum
* 调用前 先给数组排序 Arrays.sort(nums);
*/
public List<List<Integer>> nSumTarget(int[] nums, int n, int start, int target) {
List<List<Integer>> res = new ArrayList<>();
int sz = nums.length;
//至少2sum
if (n < 2 || sz < n) {
return res;
}
//从2sum开始
if (n == 2) {
//双指针
int left = start;
int right = sz - 1;
while (left < right) {
int sum = nums[left] + nums[right];
int lv = nums[left];
int rv = nums[right];
if (sum < target) {
while (left < right && nums[left] == lv) {
left++;
}
} else if (sum > target) {
while (left < right && nums[right] == rv) {
right--;
}
} else {
List<Integer> l = new ArrayList<>();
l.add(lv);
l.add(rv);
res.add(l);
while (left < right && nums[left] == lv) {
left++;
}
while (left < right && nums[right] == rv) {
right--;
}
}
}
} else {
// n > 2时 递归计算(n - 1)sum 的结果
for (int i = start; i < sz; i++) {
List<List<Integer>> sub = nSumTarget(nums, n - 1, i + 1, target - nums[i]);
for (List<Integer> arr : sub) {
//(n - 1)sum + num[i] = nsum
arr.add(nums[i]);
res.add(arr);
}
while (i < sz - 1 && nums[i] == nums[i + 1]) {
i++;
}
}
}
return res;
}

tips:

调用前先给数组排序

1
2
3
4
nsum调用
int[] arr = new int[]{};
Arrays.sort(arr);
nSumTarget(arr, n, 0, target)
123…9
李智

李智

86 日志
13 分类
57 标签
GitHub E-Mail
© 2015 — 2024 李智
由 Hexo 强力驱动
|
主题 — NexT.Mist v5.1.4
博客全站共150.3k字   本站总访问量    您是第个来到的小伙伴