区间DP(上)
区间DP用于求解子区间最优解的问题,常用于括号匹配、矩阵连乘、石子合并等,状态依赖左右区间。
区间DP(上)
区间DP
1312. 让字符串成为回文串的最少插入次数
基于两侧端点讨论的可能性展开
暴力递归
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
#include <iostream>
#include <vector>
using namespace std;
class Solution {
public:
int minInsertions(string s) {
vector<char> str(begin(s), end(s));
str.push_back('\0');
return recursion(str, 0, s.length() - 1);
}
int recursion(vector<char> &str, int left, int right) {
// 只有一个字符,无需添加
if (left == right) return 0;
// 只有两个字符,相同则无需添加;不同则需要在首部或尾部加上一个字符,ab 变成 aba 或者 bab
if (left + 1 == right) return str[left] == str[right] ? 0 : 1;
// 两个字符以上
if (str[left] == str[right]) {
// 首尾相同
return recursion(str, left + 1, right - 1);
} else {
// 首尾不同,选择插入更少字符就能回文的一段,首部或尾部加上一个字符
return min(recursion(str, left, right - 1), recursion(str, left + 1, right)) + 1;
}
}
};
- 记忆化搜索
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
#include <iostream>
#include <vector>
using namespace std;
class Solution {
public:
int minInsertions(string s) {
int len = s.length();
vector<char> str(begin(s), end(s));
str.push_back('\0');
vector<vector<int>> dp(len, vector<int>(len, -1));
return recursion(str, 0, s.length() - 1, dp);
}
int recursion(vector<char> &str, int l, int r, vector<vector<int>> &dp) {
if (dp[l][r] != -1) return dp[l][r];
int res;
if (l == r) {
// 只有一个字符,无需添加
res = 0;
} else if (l + 1 == r) {
// 只有两个字符,相同则无需添加;不同则需要在首部或尾部加上一个字符,ab 变成 aba 或者 bab
res = str[l] == str[r] ? 0 : 1;
} else {
// 两个字符以上
if (str[l] == str[r]) {
// 首尾相同
res = recursion(str, l + 1, r - 1, dp);
} else {
// 首尾不同,选择插入更少字符就能回文的一段,首部或尾部加上一个字符
res = min(recursion(str, l, r - 1, dp), recursion(str, l + 1, r, dp)) + 1;
}
}
dp[l][r] = res;
return res;
}
};
- 严格位置依赖
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
#include <iostream>
#include <vector>
using namespace std;
class Solution {
public:
int minInsertions(string s) {
int len = s.length();
vector<char> str(begin(s), end(s));
str.push_back('\0');
// 主对角线 dp[i][i] 初始化为 0,表示只有一个字符时,无需添加
vector<vector<int>> dp(len, vector<int>(len, 0));
// 初始化主对角线右边的一条斜线
for (int l = 0; l + 1 < len; ++l)
dp[l][l + 1] = str[l] == str[l + 1] ? 0 : 1;
// dp[i][j] 依赖左侧下方以及左下方的格子,所以 dp 表从主对角线往右上角推
// i 为对角线编号,i = 0 和 1 的对角线已经填过了
for (int i = 2; i < len; ++i) {
for (int l = 0, r = l + i; r < len; ++l, r++) {
if (str[l] == str[r]) {
// 首尾相同,依赖左下方
dp[l][r] = dp[l + 1][r - 1];
} else {
// 首尾不同,依赖左侧和下方
dp[l][r] = min(dp[l][r - 1], dp[l + 1][r]) + 1;
}
}
}
return dp[0][len - 1];
}
};
- 空间压缩
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
#include <iostream>
#include <vector>
using namespace std;
class Solution {
public:
// todo
int minInsertions(string s) {
int len = s.length();
if (len < 2) return 0;
vector<char> str(begin(s), end(s));
str.push_back('\0');
vector<int> dp(len, 0);
dp[len - 1] = str[len - 2] == str[len - 1] ? 0 : 1;
for (int l = len - 3, leftDown, backUp; l >= 0; l--) {
leftDown = dp[l + 1];
dp[l + 1] = s[l] == s[l + 1] ? 0 : 1;
for (int r = l + 2; r < len; r++) {
backUp = dp[r];
if (s[l] == s[r]) {
dp[r] = leftDown;
} else {
dp[r] = min(dp[r - 1], dp[r]) + 1;
}
leftDown = backUp;
}
}
return dp[len - 1];
}
};
486. 预测赢家
基于两侧端点讨论的可能性展开
暴力递归
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
#include <iostream>
#include <vector>
using namespace std;
class Solution {
public:
bool predictTheWinner(vector<int> &nums) {
int sum = 0;
for (int i = 0; i < nums.size(); ++i)
sum += nums[i];
int res = recursion(nums, 0, nums.size() - 1);
return res >= sum - res;
}
// nums[l] 到 nums[r] 上玩家 1 先选,返回能获得的最大总数
int recursion(vector<int> &nums, int l, int r) {
if (l == r)
return nums[l];
if (l + 1 == r)
return max(nums[l], nums[r]);
// 超过两个数
// 选左边,min 表示玩家 1 选过后玩家 2 再选,玩家 2 只会留给更差的情况给玩家 1
int m1 = nums[l] + min(recursion(nums, l + 1, r - 1), recursion(nums, l + 2, r));
// 选右边
int m2 = nums[r] + min(recursion(nums, l + 1, r - 1), recursion(nums, l, r - 2));
// 玩家 1 会从他的两种可能中选最好的
return max(m1, m2);
}
};
- 记忆化搜索
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
#include <iostream>
#include <vector>
using namespace std;
class Solution {
public:
bool predictTheWinner(vector<int> &nums) {
int n = nums.size();
int sum = 0;
for (int i = 0; i < nums.size(); ++i)
sum += nums[i];
vector<vector<int>> dp(n, vector<int>(n, -1));
int res = recursion(nums, 0, n - 1, dp);
return res >= sum - res;
}
// nums[l] 到 nums[r] 上玩家 1 先选,返回能获得的最大总数
int recursion(vector<int> &nums, int l, int r, vector<vector<int>> &dp) {
if (dp[l][r] != -1) return dp[l][r];
int res = 0;
if (l == r) {
res = nums[l];
} else if (l + 1 == r) {
res = max(nums[l], nums[r]);
} else {
// 超过两个数
// 选左边,min 表示玩家 1 选过后玩家 2 再选,玩家 2 只会留给更差的情况给玩家 1
int m1 = nums[l] + min(recursion(nums, l + 1, r - 1, dp), recursion(nums, l + 2, r, dp));
// 选右边
int m2 = nums[r] + min(recursion(nums, l + 1, r - 1, dp), recursion(nums, l, r - 2, dp));
// 玩家 1 会从他的两种可能中选最好的
res = max(m1, m2);
}
dp[l][r] = res;
return res;
}
};
- 严格位置依赖
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
#include <iostream>
#include <vector>
using namespace std;
class Solution {
public:
bool predictTheWinner(vector<int> &nums) {
int n = nums.size();
int sum = 0;
for (int i = 0; i < nums.size(); ++i)
sum += nums[i];
vector<vector<int>> dp(n, vector<int>(n, -1));
// 编号 0 和 1 的对角线
for (int l = 0; l < n - 1; ++l) {
dp[l][l] = nums[l];
dp[l][l + 1] = max(nums[l], nums[l + 1]);
}
dp[n - 1][n - 1] = nums[n - 1];
// 剩下的对角线,从对角线的下方往上填
for (int l = n - 3; l >= 0; l--) {
for (int r = l + 2; r < n; ++r) {
// 依赖左下角和下下方
int m1 = nums[l] + min(dp[l + 1][r - 1], dp[l + 2][r]);
// 依赖左下角和左左侧
int m2 = nums[r] + min(dp[l + 1][r - 1], dp[l][r - 2]);
dp[l][r] = max(m1, m2);
}
}
int res = dp[0][n - 1];
return res >= sum - res;
}
};
1039. 多边形三角剖分的最低得分
基于范围上划分点讨论的可能性展开
暴力递归
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
#include <iostream>
#include <vector>
using namespace std;
class Solution {
public:
int minScoreTriangulation(vector<int> &values) {
int n = values.size();
return recursion(values, 0, n - 1);
}
int recursion(vector<int> &values, int l, int r) {
if (l == r || l + 1 == r) {
return 0;
} else {
// 选一个中间点
int res = 0x7fffffff;
for (int m = l + 1; m < r; ++m) {
// 比较当前最小得分和新的得分
// 新的得分 = 左区间[l, m] 最小得分 + 右区间[m, r] 最小得分 + 顶点标记的乘积
res = min(res, recursion(values, l, m) + recursion(values, m, r) + values[l] * values[m] * values[r]);
}
return res;
}
}
};
- 记忆化搜索
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
#include <iostream>
#include <vector>
using namespace std;
class Solution {
public:
int minScoreTriangulation(vector<int> &values) {
int n = values.size();
vector<vector<int>> dp(n, vector<int>(n, 0x7fffffff));
return recursion(values, 0, n - 1, dp);
}
int recursion(vector<int> &values, int l, int r, vector<vector<int>> &dp) {
if (dp[l][r] != 0x7fffffff) return dp[l][r];
int res = 0x7fffffff;
if (l == r || l + 1 == r) {
res = 0;
} else {
// 选一个中间点
for (int m = l + 1; m < r; ++m) {
// 比较当前最小得分和新的得分
// 新的得分 = 左区间[l, m] 最小得分 + 右区间[m, r] 最小得分 + 顶点标记的乘积
res = min(res, recursion(values, l, m, dp) + recursion(values, m, r, dp) +
values[l] * values[m] * values[r]);
}
}
dp[l][r] = res;
return res;
}
};
- 严格位置依赖
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>
#include <vector>
using namespace std;
class Solution {
public:
int minScoreTriangulation(vector<int> &values) {
int n = values.size();
// 编号为 0 和 1 的对角线都初始化为 0
vector<vector<int>> dp(n, vector<int>(n, 0));
for (int l = n - 3; l >= 0; l--) {
for (int r = l + 2; r < n; ++r) {
dp[l][r] = 0x7fffffff;
for (int m = l + 1; m < r; ++m) {
dp[l][r] = min(dp[l][r], dp[l][m] + dp[m][r] + values[l] * values[m] * values[r]);
}
}
}
return dp[0][n - 1];
}
};
1547. 切棍子的最小成本
基于范围上划分点讨论的可能性展开
记忆化搜索
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
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
class Solution {
public:
int minCost(int n, vector<int> &cuts) {
sort(begin(cuts), end(cuts));
int m = cuts.size();
int len = m + 2;
// 用于计算当前棍子长度
vector<int> arr(len);
// 补上开头结尾
arr[0] = 0;
arr[len - 1] = n;
for (int i = 0; i < m; ++i)
arr[i + 1] = cuts[i];
vector<vector<int>> dp(len, vector<int>(len, -1));
return recursion(arr, 1, m, dp);
}
// [l, r] 位置是切点
int recursion(vector<int> &arr, int l, int r, vector<vector<int>> &dp) {
if (l > r) return 0;
// 只有一个切点时,直接返回这个棍子长度
if (l == r) return arr[r + 1] - arr[l - 1];
if (dp[l][r] != -1) return dp[l][r];
int res = 0x7fffffff;
// 从 [l, r] 中选对切点切下去的顺序
for (int m = l; m <= r; ++m) {
res = min(res, recursion(arr, l, m - 1, dp) + recursion(arr, m + 1, r, dp));
}
// 加上棍子长度
res += arr[r + 1] - arr[l - 1];
dp[l][r] = res;
return res;
}
};
- 严格位置依赖
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
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
class Solution {
public:
int minCost(int n, vector<int> &cuts) {
sort(begin(cuts), end(cuts));
int m = cuts.size();
int len = m + 2;
// 用于计算当前棍子长度
vector<int> arr(len);
// 补上开头结尾
arr[0] = 0;
arr[len - 1] = n;
for (int i = 0; i < m; ++i)
arr[i + 1] = cuts[i];
vector<vector<int>> dp(len, vector<int>(len, 0));
// 只有唯一一个切点时,最小代价就是棍子长度
for (int i = 1; i <= m; ++i)
dp[i][i] = arr[i + 1] - arr[i - 1];
for (int l = m - 1, next; l >= 1; l--) {
for (int r = l + 1; r <= m; ++r) {
next = 0x7fffffff;
// 只依赖左侧和下方的所有格子
for (int k = l; k <= r; ++k)
next = min(next, dp[l][k - 1] + dp[k + 1][r]);
dp[l][r] = arr[r + 1] - arr[l - 1] + next;
}
}
return dp[1][m];
}
};
312. 戳气球
基于范围上划分点讨论的可能性展开
记忆化搜索
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
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
class Solution {
public:
int maxCoins(vector<int> &nums) {
int n = nums.size();
int len = n + 2;
vector<int> arr(len);
// 左右两边补上一个气球
arr[0] = 1;
arr[len - 1] = 1;
for (int i = 0; i < n; ++i) {
arr[i + 1] = nums[i];
}
vector<vector<int>> dp(len, vector<int>(len, -1));
return recursion(arr, 1, n, dp);
}
// 从 arr[l...r] 中选择气球爆炸顺序,返回最大得分
// 前提:arr[l-1] 和 arr[r+1] 没爆炸
int recursion(vector<int> &arr, int l, int r, vector<vector<int>> &dp) {
if (dp[l][r] != -1) return dp[l][r];
int res = 0;
if (l == r) {
// 只剩一个气球
res = arr[l - 1] * arr[l] * arr[r + 1];
} else {
// 比较 l 位置最后被打爆和 r 位置最后被打爆的结果
res = max(arr[l - 1] * arr[l] * arr[r + 1] + recursion(arr, l + 1, r, dp),
arr[l - 1] * arr[r] * arr[r + 1] + recursion(arr, l, r - 1, dp));
// 把中间每个位置都假设成最后被打爆的
for (int k = l + 1; k < r; ++k) {
res = max(res, arr[l - 1] * arr[k] * arr[r + 1] + recursion(arr, l, k - 1, dp) +
recursion(arr, k + 1, r, dp));
}
}
dp[l][r] = res;
return res;
}
};
- 严格位置依赖
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
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
class Solution {
public:
int maxCoins(vector<int> &nums) {
int n = nums.size();
int len = n + 2;
vector<int> arr(len);
// 左右两边补上一个气球
arr[0] = 1;
arr[len - 1] = 1;
for (int i = 0; i < n; ++i) {
arr[i + 1] = nums[i];
}
vector<vector<int>> dp(len, vector<int>(len, -1));
for (int i = 1; i <= n; ++i)
dp[i][i] = arr[i - 1] * arr[i] * arr[i + 1];
for (int l = n, res; l >= 1; l--) {
for (int r = l + 1; r <= n; r++) {
res = max(arr[l - 1] * arr[l] * arr[r + 1] + dp[l + 1][r],
arr[l - 1] * arr[r] * arr[r + 1] + dp[l][r - 1]);
for (int k = l + 1; k < r; k++) {
res = max(res, arr[l - 1] * arr[k] * arr[r + 1] + dp[l][k - 1] + dp[k + 1][r]);
}
dp[l][r] = res;
}
}
return dp[1][n];
}
};
面试题 08.14. 布尔运算
基于范围上划分点讨论的可能性展开
记忆化搜索
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
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
class Solution {
public:
int countEval(string s, int result) {
vector<char> str(begin(s), end(s));
str.push_back('\0');
int n = s.size();
vector<vector<vector<int>>> dp(n, vector<vector<int>>(n, vector<int>()));
vector<int> res = recursion(str, 0, n - 1, dp);
return res[result];
}
// s[l...r] 是表达式的一部分,是合法的表达式
// 返回结果 res[0] 表示这段表达式结果为 false 的可能数,res[1] 表示为 true 的可能数
vector<int> recursion(vector<char> &str, int l, int r, vector<vector<vector<int>>> &dp) {
if (!dp[l][r].empty()) return dp[l][r];
int f = 0, t = 0;
if (l == r) {
// 只有一个字符
f = str[l] == '0' ? 1 : 0;
t = str[l] == '1' ? 1 : 0;
} else {
// 这段表达式中的每个运算符都尝试作为最后一个运算的符号
for (int k = l + 1; k < r; k += 2) {
// 计算 k 位置的运算符的左侧表达式结果为 false 和 ture 的可能数
vector<int> tmp = recursion(str, l, k - 1, dp);
int a = tmp[0];
int b = tmp[1];
// 计算 k 位置的运算符的右侧表达式结果为 false 和 ture 的可能数
tmp = recursion(str, k + 1, r, dp);
int c = tmp[0];
int d = tmp[1];
if (str[k] == '&') {
f += a * c + a * d + b * c;
t += b * d;
} else if (str[k] == '|') {
f += a * c;
t += a * d + b * c + b * d;
} else {
f += a * c + b * d;
t += a * d + b * c;
}
}
}
vector<int> res = {f, t};
dp[l][r] = res;
return res;
}
};
本文由作者按照 CC BY 4.0 进行授权