动态规划中根据数据量猜解法
根据数据规模选择动态规划方法,数据大用优化状态压缩,小数据用朴素枚举,平衡时间与空间复杂度。
动态规划中根据数据量猜解法
动态规划中根据数据量猜解法
打怪兽
- a[i] 范围大,b[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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#include <iostream>
#include <vector>
#include <climits>
#include <algorithm>
using namespace std;
// 方法1:a[i] 范围大,b[i] 范围小的适用版本
// 时间复杂度:O(n * 所有怪兽的钱数累加和)
int fc(int n, const vector<int> &a, const vector<int> &b) {
int m = 0;
for (int i = 1; i <= n; i++) m += b[i];
// dp[i][j] : 花的钱不能超过j,通过前i个怪兽,最大能力是多少
vector<vector<int>> dp(n + 1, vector<int>(m + 1, INT_MIN));
dp[0][0] = 0; // 初始能力为0,花费为0
for (int i = 1; i <= n; i++) {
for (int j = 0; j <= m; j++) {
// 如果上一个状态在 j 金钱下的能力 >= 当前怪兽所需能力,可以直接通过
if (dp[i - 1][j] >= a[i])
dp[i][j] = dp[i - 1][j];
// 如果选择贿赂怪兽
if (j >= b[i] && dp[i - 1][j - b[i]] != INT_MIN)
dp[i][j] = max(dp[i][j], dp[i - 1][j - b[i]] + a[i]);
}
}
for (int j = 0; j <= m; j++)
if (dp[n][j] != INT_MIN)
return j;
return -1;
}
int main() {
int n;
while (cin >> n) {
vector<int> a(n + 1), b(n + 1);
for (int i = 1; i <= n; i++) {
cin >> a[i] >> b[i];
}
cout << fc(n, a, b) << '\n';
}
return 0;
}
- 空间优化版本
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 <climits>
#include <algorithm>
using namespace std;
// 方法2:方法1的空间优化版本
int fc(int n, const vector<int> &a, const vector<int> &b) {
int m = 0;
for (int i = 1; i <= n; i++) m += b[i];
vector<int> dp(m + 1, INT_MIN);
dp[0] = 0;
for (int i = 1; i <= n; i++) {
for (int j = m; j >= 0; j--) {
int cur = INT_MIN;
if (dp[j] >= a[i])
cur = dp[j];
if (j >= b[i] && dp[j - b[i]] != INT_MIN)
cur = max(cur, dp[j - b[i]] + a[i]);
dp[j] = cur;
}
}
for (int j = 0; j <= m; j++)
if (dp[j] != INT_MIN)
return j;
return -1;
}
int main() {
int n;
while (cin >> n) {
vector<int> a(n + 1), b(n + 1);
for (int i = 1; i <= n; i++) {
cin >> a[i] >> b[i];
}
cout << fc(n, a, b) << '\n';
}
return 0;
}
- a[i] 范围小,b[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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#include <iostream>
#include <vector>
#include <climits>
#include <algorithm>
using namespace std;
// 方法3:a[i] 范围小,b[i] 范围大的适用版本
// 时间复杂度:O(n * 所有怪兽能力累加和)
int fc(int n, const vector<int> &a, const vector<int> &b) {
int m = 0;
for (int i = 1; i <= n; i++) m += a[i];
// dp[i][j] : 能力正好是j,并且确保能通过前i个怪兽,需要至少花多少钱
vector<vector<int>> dp(n + 1, vector<int>(m + 1, INT_MAX));
dp[0][0] = 0;
for (int i = 1; i <= n; i++) {
for (int j = 0; j <= m; j++) {
// 如果当前能力 j >= 当前怪兽要求,能力不变,不花钱
if (j >= a[i] && dp[i - 1][j] != INT_MAX)
dp[i][j] = dp[i - 1][j];
// 贿赂怪兽,能力提升 a[i],代价加 b[i]
if (j >= a[i] && dp[i - 1][j - a[i]] != INT_MAX)
dp[i][j] = min(dp[i][j], dp[i - 1][j - a[i]] + b[i]);
}
}
int res = INT_MAX;
for (int j = 0; j <= m; j++)
res = min(res, dp[n][j]);
return (res == INT_MAX ? -1 : res);
}
int main() {
int n;
while (cin >> n) {
vector<int> a(n + 1), b(n + 1);
for (int i = 1; i <= n; i++) {
cin >> a[i] >> b[i];
}
cout << fc(n, a, b) << '\n';
}
return 0;
}
- 空间优化版本
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 <climits>
#include <algorithm>
using namespace std;
// 方法4:方法3的空间优化版本
int fc(int n, const vector<int> &a, const vector<int> &b) {
int m = 0;
for (int i = 1; i <= n; i++) m += a[i];
vector<int> dp(m + 1, INT_MAX);
dp[0] = 0;
for (int i = 1; i <= n; i++) {
for (int j = m; j >= 0; j--) {
int cur = INT_MAX;
if (j >= a[i] && dp[j] != INT_MAX)
cur = dp[j];
if (j >= a[i] && dp[j - a[i]] != INT_MAX)
cur = min(cur, dp[j - a[i]] + b[i]);
dp[j] = cur;
}
}
int res = *min_element(dp.begin(), dp.end());
return (res == INT_MAX ? -1 : res);
}
int main() {
int n;
while (cin >> n) {
vector<int> a(n + 1), b(n + 1);
for (int i = 1; i <= n; i++) {
cin >> a[i] >> b[i];
}
cout << fc(n, a, b) << '\n';
}
return 0;
}
选择k个数字使得两集合累加和相差不超过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
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
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
#include <iostream>
#include <vector>
#include <cstdlib>
#include <ctime>
#include <numeric>
using namespace std;
// 选择k个数字使得两集合累加和相差不超过1
// 给定一个正数n,表示1~n这些数字都可以选择
// 给定一个正数k,表示要从1~n中选择k个数字组成集合A,剩下数字组成集合B
// 希望做到集合A和集合B的累加和相差不超过1
// 如果能做到,返回集合A选择了哪些数字,任何一种方案都可以
// 如果不能做到,返回长度为0的数组
// 2 <= n <= 10^6
// 1 <= k <= n
// 根据总和和k值生成一个可行的方案(返回选择了哪些数)
// sum:期望子集A的累加和
// n:范围是1~n
// k:A集合要挑选k个数字
vector<int> generate(long long sum, int n, int k) {
long long minKSum = (long long) (k + 1) * k / 2; // 最小选择前k个数的和
int range = n - k; // 剩余数字的可操作范围
// 如果目标sum太小或太大,无法达到
if (sum < minKSum || sum > minKSum + (long long) range * k) return {};
// 需要在最小基础上增加的值
long long need = sum - minKSum;
int rightSize = need / range; // 选择几个最大的数放到集合中
int midIndex = (k - rightSize) + (need % range); // 中间的那个调整值
int leftSize = k - rightSize - (need % range == 0 ? 0 : 1); // 前缀部分
vector<int> res(k);
for (int i = 0; i < leftSize; i++)
res[i] = i + 1;
if (need % range != 0)
res[leftSize] = midIndex;
for (int i = k - 1, j = 0; j < rightSize; i--, j++)
res[i] = n - j;
return res;
}
// 主方法
vector<int> pick(int n, int k) {
long long sum = (long long) (n + 1) * n / 2;
// 先尝试总和的一半
vector<int> res = generate(sum / 2, n, k);
// 再尝试 +1(奇数情况下)
if (res.empty() && (sum & 1))
res = generate(sum / 2 + 1, n, k);
return res;
}
// 记忆化搜索判断是否能划分(验证用)
bool fc(int n, int i, int k, int s, vector<vector<vector<int>>> &dp) {
if (k < 0 || s < 0) return false;
if (i == n + 1) return k == 0 && s == 0;
if (dp[i][k][s] != 0) return dp[i][k][s] == 1;
bool res = fc(n, i + 1, k, s, dp) || fc(n, i + 1, k - 1, s - i, dp);
dp[i][k][s] = res ? 1 : -1;
return res;
}
bool canSplit(int n, int k) {
int sum = (n + 1) * n / 2;
int wantSum = sum / 2 + (sum % 2);
vector<vector<vector<int>>> dp(n + 2, vector<vector<int>>(k + 1, vector<int>(wantSum + 1)));
return fc(n, 1, k, wantSum, dp);
}
// 判断一个方案是否正确(对数器验证)
bool pass(int n, int k, const vector<int> &res) {
if (res.empty()) {
return !canSplit(n, k);
} else {
if ((int) res.size() != k) return false;
int sum = (n + 1) * n / 2;
int pickSum = accumulate(res.begin(), res.end(), 0);
return abs(pickSum - (sum - pickSum)) <= 1;
}
}
// 对数器:随机生成数据验证
int main() {
srand(time(0));
int N = 60;
int testTime = 5000;
cout << "测试开始" << endl;
for (int i = 0; i < testTime; ++i) {
int n = rand() % N + 2;
int k = rand() % n + 1;
vector<int> res = pick(n, k);
if (!pass(n, k, res))
cout << "出错了!n = " << n << ", k = " << k << endl;
}
cout << "测试结束" << endl;
return 0;
}
P1439 【模板】最长公共子序列
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
#include <iostream>
#include <vector>
using namespace std;
// ends 逆序
// 二分查找,在 ends[0...len) 中找第一个 >= target 的位置
// 失败时返回 len
int binarySearch(const vector<int> &ends, int len, int target) {
int l = 0, r = len - 1, m;
while (l <= r) {
m = (l + r) / 2;
if (ends[m] >= target) {
r = m - 1;
} else {
l = m + 1;
}
}
return l;
}
// 计算最长公共子序列长度
int fc(int n, const vector<int> &a, vector<int> &b) {
vector<int> where(n + 1); // where[x] = x 在 a 中的位置
for (int i = 0; i < n; i++)
where[a[i]] = i;
// 将 b 映射为 a 中的索引序列,问题转为 LIS
for (int i = 0; i < n; i++)
b[i] = where[b[i]];
// LIS 求解
vector<int> ends(n); // ends[i] 代表长度为 i+1 的最小结尾值
int len = 0;
for (int i = 0; i < n; i++) {
int pos = binarySearch(ends, len, b[i]);
if (pos == len) {
ends[len++] = b[i]; // 增加新的子序列长度
} else {
ends[pos] = b[i]; // 替换已有的最小结尾值
}
}
return len;
}
int main() {
int n;
while (cin >> n) {
vector<int> a(n), b(n);
for (int i = 0; i < n; i++) cin >> a[i];
for (int i = 0; i < n; i++) cin >> b[i];
cout << fc(n, a, b) << '\n';
}
return 0;
}
1187. 使数组严格递增
- 记忆化搜索
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
71
#include <iostream>
#include <vector>
#include <algorithm>
#include <climits>
using namespace std;
class Solution {
public:
// 记忆化搜索解法
int makeArrayIncreasing(vector<int> &arr1, vector<int> &arr2) {
// 排序并去重
sort(arr2.begin(), arr2.end());
int m = 1;
for (int i = 1; i < arr2.size(); i++)
if (arr2[i] != arr2[m - 1])
arr2[m++] = arr2[i];
int n = arr1.size();
vector<int> dp(n, -1); // 记忆化数组,初始化为 -1
int res = dfs(arr1, arr2, n, m, 0, dp);
return res == INT_MAX ? -1 : res;
}
// arr1 长度为 n,arr2 有效部分长度为 m
// arr2 有效部分可以替换 arr1 中的数字
// arr1[0..i-1] 已经严格递增且 arr1[i-1] 一定没有替换
// 返回让 arr1 整体都严格递增,arr1[i...] 范围上还需要几次替换
// 如果做不到,返回无穷大
int dfs(vector<int> &arr1, vector<int> &arr2, int n, int m, int i, vector<int> &dp) {
if (i == n) return 0;
if (dp[i] != -1) return dp[i];
int res = INT_MAX; // 遍历所有的分支,所得到的最少的操作次数
int pre = i == 0 ? INT_MIN : arr1[i - 1]; // 前一位的数字
int find = binarySearch(arr2, m, pre); // arr2 有效长度 m 的范围上,找到刚比 pre 大的位置
// 枚举arr1[i...] 范围上,第一个不需要替换的位置 j
for (int j = i, k = 0, next; j <= n; j++, k++) {
if (j == n) {
res = min(res, k); // 全部换完了
} else {
if (pre < arr1[j]) { // 可以不换
next = dfs(arr1, arr2, n, m, j + 1, dp);
if (next != INT_MAX) res = min(res, k + next);
}
if (find < m) {
pre = arr2[find++]; // 尝试替换
} else {
break; // 替换失败,退出循环
}
}
}
dp[i] = res;
return res;
}
// 在 arr2[0..len-1] 中找第一个大于 target 的位置
// 失败时返回 len
int binarySearch(vector<int> &arr2, int len, int target) {
int l = 0, r = len - 1, m;
while (l <= r) {
m = (l + r) / 2;
if (arr2[m] > target) {
r = m - 1;
} else {
l = m + 1;
}
}
return l;
}
};
- 严格位置依赖
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>
#include <climits>
using namespace std;
class Solution {
public:
int binarySearch(vector<int> &arr2, int len, int target) {
int l = 0, r = len - 1, m;
while (l <= r) {
m = (l + r) / 2;
if (arr2[m] > target) {
r = m - 1;
} else {
l = m + 1;
}
}
return l;
}
// 动态规划解法
int makeArrayIncreasing(vector<int> &arr1, vector<int> &arr2) {
sort(arr2.begin(), arr2.end());
arr2.erase(unique(arr2.begin(), arr2.end()), arr2.end());
int m = arr2.size();
int n = arr1.size();
// dp[i]:从第 i 位开始需要的最少替换次数
vector<int> dp(n + 1, 0);
for (int i = n - 1; i >= 0; i--) {
int res = INT_MAX;
int pre = i == 0 ? INT_MIN : arr1[i - 1];
int find = binarySearch(arr2, m, pre);
for (int j = i, k = 0, next; j <= n; j++, k++) {
if (j == n) {
res = min(res, k);
} else {
if (pre < arr1[j]) {
next = dp[j + 1];
if (next != INT_MAX) {
res = min(res, k + next);
}
}
if (find < m) {
pre = arr2[find++];
} else {
break;
}
}
}
dp[i] = res;
}
return dp[0] == INT_MAX ? -1 : dp[0];
}
};
本文由作者按照 CC BY 4.0 进行授权