2022.10.12 总结
1. 洛谷 P1435
题意
给定一个字符串 \(s\),请你求出最少要插入多少个字符能使 \(s\) 变为回文串。
思路
30 分
要使 \(s\) 变为回文串,就是要使 \(s_1 = s_n,s_2 = s_{n - 1} \dots\)
所以每次只要保证最两边的字符相同即可。
令 \(f(l, r)\) 表示使 \(s_l \sim s_r\) 变成回文串需要插入的最少字符数量。
-
如果 \(s_l == s_r\),则只需要考虑 \(s_{l + 1} \sim s_{r - 1}\) 是否为回文串即可。\(f(l, r) = f(l + 1, r - 1)\)。
-
如果 \(s_l \ne s_r\),则有两种情况:一种是让下一个回文的字符为 \(s_l\),另一种则是让下一个回文的字符为 \(s_r\)。\(f(l, r) = \max \{f(l + 1, r), f(l, r - 1)\} + 1\),自己需要插入一次。
所以,可以直接暴力。
时间复杂度
\(O(2 ^ n)\sim O(3 ^ n)\),指数级别(太慢了)。
空间复杂度
字符串长度为 \(n\),\(O(n)\)。
代码
int f(int l, int r){
if (l >= r) { // 递归边界
return 0;
}
if (s[l] == s[r]) {
return f(l + 1, r - 1);
}
return min(f(l + 1, r), f(l, r - 1)) + 1;
}
100 分(记忆化搜索)
和 30 分差不多(时间复杂度特别优秀,比 DP 要慢一点)
时间复杂度
每个状态只遍历一次,\(O(n ^ 2)\)。
空间复杂度
数组记录状态,\(O(n ^ 2)\)。
代码
int f(int l, int r){
if (l >= r) {
return 0;
}
if (dp[l][r] != -1) { // 记忆化
return dp[l][r];
}
if (s[l] == s[r]) {
return dp[l][r] = f(l + 1, r - 1);
}
return dp[l][r] = min(f(l + 1, r), f(l, r - 1)) + 1;
}
100 分(DP)
拓扑序是区间长度从小到大,所以先枚举区间长度。
时间复杂度
先枚举区间长度,\(O(n)\)。
枚举每种长度的所有区间,\(O(n)\)。
总时间复杂度为 \(O(n ^ 2)\)。
空间复杂度
记录每种状态,\(O(n ^ 2)\)。
代码
for (int i = 0; i < n - 1; i++) {
dp[i][i + 1] = s[i] != s[i + 1]; // 初始状态
}
for (int l = 3; l <= n; l++) {
for (int i = 0, j = l - 1; j < n; i++, j++) { // 转移
if (s[i] == s[j]) {
dp[i][j] = dp[i + 1][j - 1];
} else {
dp[i][j] = min(dp[i + 1][j], dp[i][j - 1]) + 1;
}
}
}
2. 洛谷 P3146
题意
给定一个序列 \(a\),每次可以选择相邻两个相同的元素 \(a_i, a_{i + 1}\) 合并,合并后只剩下一个元素,值为合并前的元素 \(+1\)。
请你求出序列中最大的元素的值。
思路
100 分
每次都是选择两个相邻的元素合并,所以序列中每个元素要么是本来就存在序列中的,要么是由连续的一段子段组成的,所以可以根据这个性质做一些事情。
令 \(dp_{i, j}\) 为 \(a_i \sim a_j\) 的这一段区间最终合并成的元素,如果不可以合并为一个元素,则 \(dp_{i, j} = 0\)。
所以事情就变得很简单了。
时间复杂度
枚举区间长度,\(O(n)\)。
枚举每个区间,\(O(n)\)。
枚举断点,\(O(n)\)。
总时间复杂度为 \(O(n ^ 3)\)。
空间复杂度
记录每个状态,\(O(n ^ 2)\)。
代码
#include <bits/stdc++.h>
using namespace std;
const int N = 310;
int n, a[N], dp[N][N], ans;
int main(){
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> a[i];
dp[i][i] = a[i];
}
for (int len = 2; len <= n; len++) { // 枚举长度
for (int i = 1, j = len; j <= n; i++, j++) { // 枚举区间
for (int k = i; k < j; k++) {
if (dp[i][k] && dp[i][k] == dp[k + 1][j]) {
dp[i][j] = max(dp[i][j], dp[i][k] + 1);
}
}
ans = max(dp[i][j], ans);
}
}
cout << ans;
return 0;
}
3. 洛谷 P1063
题意
有一条项链,上面有 \(N\) 颗珠子,每颗珠子都有头标记和尾标记 \(a_i, b_i\),每颗珠子的尾标记和它的下一颗珠子的头标记是一定相等的。
每次操作可以选择两颗相邻的珠子 \(i, i+1\),将它们变为一颗珠子,那颗新的珠子的头标记为 \(a_i\),尾标记为 \(b_{i + 1}\),释放的能量为 \(a_i \times b_i \times b_{i + 1}\)。
请你求出在只剩下一颗珠子时,最多能释放多少能量。
思路
因为这条项链是一个环,不好处理,所以有以下两种方法处理这个环。
100 分(断环成链)
将这个环从某一个点 \(i\) 断开,变成一条链状,然后做区间 DP,更新最大能量。
时间复杂度
枚举断点,\(O(n)\)。
区间 DP,\(O(n ^ 3)\)。
总时间复杂度为 \(O(n ^ 4)\)。
空间复杂度
记录每个状态,\(O(n ^ 2)\)。
代码
#include <bits/stdc++.h>
using namespace std;
const int N = 310;
int n, a[N], dp[N][N], ans, b[N];
int main(){
cin >> n;
for (int i = 0; i < n; i++) {
cin >> a[i];
}
for (int x = 0; x < n; x++) { // 枚举断点
for (int y = 0; y < n; y++) {
b[y] = a[(x + y) % n];
}
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
dp[i][j] = 0;
}
}
// 区间 DP
for (int len = 2; len <= n; len++) {
for (int i = 0, j = len - 1; j < n; i++, j++) {
for (int k = i; k < j; k++) {
dp[i][j] = max(dp[i][j], dp[i][k] + dp[k + 1][j] + b[i] * b[k + 1] * b[(j + 1) % n]);
}
}
}
ans = max(ans, dp[0][n - 1]);
}
cout << ans;
return 0;
}
100 分(将环拆成两倍长度的链)
将这个环变成两倍长度的链。
\([1, 3, 2, 4]\) -> \([1, 3, 2, 4, 1, 3, 2, 4]\)
对这个序列做区间 DP。
时间复杂度
枚举区间长度 \(len\) ,\(O(n)\)。
枚举 \([0, 2 \times n]\) 的所有长度为 \(len\) 的区间,\(O(2 \times n)\)。
枚举区间断点,\(O(n)\)。
总时间复杂度为 \(O(n ^ 3)\)。
空间复杂度
记录每个状态,\(O(n ^ 2)\)。
代码
#include <bits/stdc++.h>
using namespace std;
const int N = 310;
int n, a[N], dp[N][N], ans;
int main(){
cin >> n;
for (int i = 0; i < n; i++) {
cin >> a[i];
a[i + n] = a[i]; // 将环变为两倍长度的链
}
// 区间 DP
for (int len = 2; len <= n; len++) {
for (int i = 0, j = len - 1; j < 2 * n; i++, j++) {
for (int k = i; k < j; k++) {
dp[i][j] = max(dp[i][j], dp[i][k] + dp[k + 1][j] + a[i] * a[k + 1] * a[j + 1]);
}
}
}
for (int i = 0; i < n; i++) {
ans = max(ans, dp[i][i + n - 1]);
}
cout << ans;
return 0;
}