线性DP:状态转移表达式存在明显的线性关系。
区间DP:与顺序有关,状态与区间有关。
题目1:数字三角形。
解题思路:直接DP即可,f[i][j]
可以来自f[i-1][j] + a[i][j]
和f[i-1][j-1] + a[i][j]
,注意f[i-1][j]
不存在的情况(最后一个点)和f[i-1][j-1]
不存在的情况(第一个点)。
C++代码如下,
#include
using namespace std;
const int N = 510;
int n;
int a[N][N];
int f[N][N];
int main() {
cin >> n;
for (int i = 0; i < n; ++i) {
for (int j = 0; j < i + 1; ++j) {
cin >> a[i][j];
}
}
for (int i = 0; i < N; ++i) {
for (int j = 0; j < N; ++j) {
f[i][j] = -0x3f3f3f3f;
}
}
f[0][0] = a[0][0];
for (int i = 1; i < n; ++i) {
for (int j = 0; j < i + 1; ++j) {
f[i][j] = max(f[i][j], f[i-1][j] + a[i][j]);
if (j - 1 >= 0) f[i][j] = max(f[i][j], f[i-1][j-1] + a[i][j]);
}
}
int res = -0x3f3f3f3f;
for (int j = 0; j < n; ++j) res = max(res, f[n-1][j]);
cout << res << endl;
return 0;
}
题目2:最长上升子序列。注意可以连续,比如3 1 2 1 8 5 6
,那么它的最长上升子序列为1 2 5 6
,长度为4。
解题思路:DP即可。
状态定义,f[i]
表示以第 i i i元素结尾的上升子序列的最大长度。
状态转移,有,
f[i] = 1
。a[i-1] < a[i]
,则f[i-1] + 1
。a[i-2] < a[i]
,则f[i-2] + 1
。a[0] < a[i]
,则f[0] + 1
。C++代码如下,
#include
using namespace std;
const int N = 1010;
int n;
int a[N];
int f[N];
int main() {
cin >> n;
for (int i = 0; i < n; ++i) cin >> a[i];
for (int i = 0; i < n; ++i) {
f[i] = 1;
for (int j = 0; j < i; ++j) {
if (a[j] < a[i]) f[i] = max(f[i], f[j] + 1);
}
}
int res = 0;
for (int i = 0; i < n; ++i) res = max(res, f[i]);
cout << res << endl;
return 0;
}
同时输出最长上升子序列,那么此时只需要记住当前状态是从哪一个状态转移过来的,然后逆序输出即可。C++代码如下,
#include
#include
using namespace std;
const int N = 1010;
int n;
int a[N];
int f[N];
int g[N];
int main() {
cin >> n;
for (int i = 0; i < n; ++i) cin >> a[i];
for (int i = 0; i < n; ++i) {
f[i] = 1;
g[i] = -1; //g[i]=-1表示它是子序列的起点
for (int j = 0; j < i; ++j) {
if (a[j] < a[i]) {
if (f[i] < f[j] + 1) {
f[i] = f[j] + 1;
g[i] = j;//g[i]=j表示状态f[i]是从状态f[j]转移过来的
}
}
}
}
int res = 0;
for (int i = 0; i < n; ++i) {
if (f[i] > f[res]) {
res = i;
}
}
cout << f[res] << endl;//输出最长子序列的长度
vector<int> ans; //最长子序列
for (int i = res; i != -1; i = g[i]) {
ans.emplace_back(a[i]);
}
reverse(ans.begin(), ans.end());
for (int i = 0; i < ans.size(); ++i) cout << ans[i] << " ";
cout << endl;
return 0;
}
测试样例,
输入:
7
3 1 2 1 8 5 6
输出:
4
1 2 5 6
题目3:最长公共子序列,给定字符串a和字符串b,求它们的最长公共子序列。例如acbd
和abedc
,则它们的最长公共子序列为abd
,长度为3。
思路:利用DP来求解,由于这里求的是最大长度,因此在状态转移时,可以适当放大,以便可以表示相应转移路径。
状态定义,f[i][j]
:从字符串a前i个中选且从字符串b中前j个中选的公共子序列的最大长度。
状态转移,有,
f[i-1][j-1]
。f[i][j-1]
。f[i-1][j]
。a[i]=b[j]
,则f[i-1][j-1] + 1
。进一步思考,由于进行了放大,因此第(2)种情况和第(3)种情况包含了第(1)种情况。因此在代码实现时,可以考虑如下状态转移f[i][j-1]
、f[i-1][j]
和f[i-1][j-1] + 1
。
C++代码如下,
#include
using namespace std;
const int N = 1010;
char a[N], b[N];
int n, m;
int f[N][N];
int main() {
cin >> n >> m;
cin >> a + 1 >> b + 1;
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= m; ++j) {
f[i][j] = max(f[i-1][j], f[i][j-1]);
if (a[i] == b[j]) f[i][j] = max(f[i][j], f[i-1][j-1] + 1);
}
}
cout << f[n][m] << endl;
return 0;
}
题目4:合并相邻石子的最小代价。例如1 3 5 2
,合并前两堆石子1
和3
的代价是4,得到4 5 2
。
解题思路:区间合并类DP,状态定义围绕着区间,然后怎么层层递进计算状态,比较考验。答案是枚举区间长度来计算。
状态定义,f[i][j]
:合并第i~j堆石子的最小代价。
状态转移,有,
f[i][i] + f[i + 1][j] + s[j] - s[i - 1]
,其中s[i]
表示前缀和。f[i][i + 1] + f[i + 2][j] + s[j] - s[i - 1]
。f[i][i + 2] + f[i + 3][j] + s[j] - s[i - 1]
。f[i][j - 1] + f[j][j] + s[j] - s[i - 1]
。初始化,f[i][i]=0
,然后其余初始化为正无穷大,比如1e9
。
最终答案为,f[1][n]
。
注意状态的遍历,是通过区间长度从1到n实现的。
C++代码如下,
#include
using namespace std;
const int N = 310;
int a[N];
int s[N];
int f[N][N];
int n;
int main() {
cin >> n;
for (int i = 1; i <= n; ++i) cin >> a[i];
for (int i = 1; i <= n; ++i) s[i] += s[i-1] + a[i];
for (int len = 2; len <= n; ++len) {
//枚举左端点
for (int i = 0; i + len - 1 <= n; ++i) {
int l = i, r = i + len - 1;
f[l][r] = 1e9;
for (int k = l; k < r; ++k) {
f[l][r] = min(f[l][r], f[l][k] + f[k+1][r] + s[r] - s[l-1]);
}
}
}
cout << f[1][n] << endl;
return 0;
}