「Day 9 & 10—DP问题」
DP问题
定义
什么是 \(DP\),答曰:一种通过将全局问题分解成不同的子问题来进行对复杂问题的计算。
在我看来就是一种递推的 \(ProMax\) 版,依旧是用之前计算过的来推出现在要计算的。
DP板子问题
P1115 最大子段和
思路
我们用 \(dp\) 数组来定义到 \(i\) 为止,最大的子段和,那么我们在面对 \(dp_i\) 的时候,应该怎么操作呢。首先什么情况下我们要继续接着前面的选,就是前面的大于 \(0\) 的时候,否则我们就可以单开一次,让 \(dp_i = a_i\) 即可。
代码
点击查看代码
#include<iostream>
using namespace std;
const int MAXN = 2 * 1e6 + 5;
int a[MAXN];
int dp[MAXN];
int n,ans = -10086;
int main(){
cin >> n;
for(int i = 1;i <= n;i ++) cin >> a[i];
for(int i = 1;i <= n;i ++){
if(dp[i - 1] >= 0) dp[i] = dp[i - 1] + a[i];
else{
dp[i] = a[i];
}
ans = max(dp[i],ans);
}
cout << ans << "\n";
return 0;
}
最大子矩阵问题
思路
首先呢这个题没有找到有这个题的 \(OJ\),所以就直接写了。
最大子矩阵这个问题太大了,如果是枚举的话 \(O(n^4)\) 肯定爆炸,有没有什么稍稍优化一点的方法吗?有。想一想我们刚刚说的最大子段和,如果能将这个二维问题压成一维就好了,那么如何去做呢。可以用一个线性前缀和,\(sum_i,{_j}\) 表示第 \(j\) 列前 \(i\) 个元素的前缀和。
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
//j表示第j列,剩下的那一维和正常的前缀和没什么大区别。
pre[i][j] = pre[i - 1][j] + a[i][j];
}
}
接下来如何求呢,我们既然知道了在第 \(j\) 列上的前缀和了,那么例如
3 3
1 -2 3
-4 5 -6
7 -8 9
对于每一列的前缀和为
1 -2 3
-3 3 -3
4 -5 6
例如我们要算上界为 \(2\) 和下界为 \(3\) 的子矩阵和,也就是最后两行,其每列的和为
3 -3 3
这个该如何算呢?
利用 \(pre[3][1] - pre[2 - 1][1]\) 计算出第一列的,扩展到正常的就是: \(pre[j][k] - pre[i - 1][k]\) 其计算的就是第 \(K\) 行从第 \(i\) 到第 \(j\) 的数的和。
计算后再用最大子段和进行计算即可。
// 求解最大子矩阵和
long long ans = -inf;
for (int i = 1; i <= n; i++) { // 枚举上边界
for (int j = i; j <= n; j++) { // 枚举下边界
long long sum = 0;
// 第 k 列,[i,j] 的和为:pre[j][k] - pre[i - 1][k]
// 所以就转换为最大子段和问题了
for (int k = 1; k <= m; k++) {
long long tmp = pre[j][k] - pre[i - 1][k];
if (sum >= 0) {
sum += tmp;
} else {
sum = tmp;
}
ans = max(ans, sum);
}
}
}
B3637 最长上升子序列
思路+代码
#include<iostream>
using namespace std;
int n;
int a[10005],dp[10005];
int main(){
cin >> n;
for(int i = 1;i <= n;i ++){
cin >> a[i];
}
for(int i = 1;i <= n;i ++){
dp[i] = 1;
for(int j = 1;j < i;j ++){
//保证上升
//如果是最长不下降子序列就把<改为<=即可
if(a[j] < a[i]){
//取最大的
dp[i] = max(dp[i],dp[j] + 1);
}
}
}
int ans = -1;
for(int i = 1;i <= n;i ++){
ans = max(ans,dp[i]);
}
cout << ans << "\n";
return 0;
}
P1439 【模板】最长公共子序列
思路
首先是我们考虑状态,\(dp[i][j]\) 表示前 \(s[i]\) 和前 \(t[i]\) 个字符的最长公共子序列长度,对于一个 \(dp[i][j]\) 来说,我们有两种情况,一是 \(s[i] == t[i]\) 这个时候让 \(dp[i][j] = dp[i - 1][j - 1] + 1\) 即可,否则就取 \(s[i]\) 结尾和 \(t[i]\) 结尾的最大值即可。
代码(无优化50pts)
点击查看代码
#include<iostream>
#include<algorithm>
using namespace std;
int n;
int a[200005],b[200005];
int dp[8005][8005];
int main(){
cin>>n;
for(int i=1;i<=n;i++){
cin>>a[i];
}
for(int i=1;i<=n;i++){
cin>>b[i];
}
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
//两种情况
if(a[i]==b[j]) dp[i][j]=dp[i-1][j-1] + 1;
else dp[i][j]=max(dp[i-1][j],dp[i][j-1]);
}
}
cout<<dp[n][n]<<endl;
return 0;
}
简单背包问题
P1048 [NOIP2005 普及组] 采药
思路
这个题可以说是最正规的 \(01\) 背包了,我们设一个状态 \(dp[i][j]\) 来表示在面对第 \(i\) 个物品时背包容积还剩 \(j\) 时的最大价值。
对于第 \(i\) 个物品有选和不选两种情况:
\(dp[i][j] = dp[i - 1][j]\) (不选的情况)
\(dp[i][j] = max(dp[i - 1][j - w[i]] + v[i],dp[i - 1][j])\) (选的情况)
代码
点击查看代码
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
int dp[21][1010];
int w[21], c[21];
int main() {
int N, V;
cin >> N >> V;
for (int i = 1; i <= N; i++) {
cin >> w[i] >> c[i];
}
for (int i = 1; i <= N; i++) {
for (int j = 0; j <= V; j++) {
if (j >= c[i]) {
dp[i][j]=max(dp[i-1][j-c[i]]+w[i],dp[i-1][j]);
} else {
dp[i][j]=dp[i-1][j];
}
}
}
cout << dp[N][V] << endl;
return 0;
}
P1757 通天之分组背包
思路
这个题和上个题唯一不同是这个题每个物品有好多件,于是我们就可以在转移的过程中枚举件数 \(k\),有如下转移方程
\(dp[i][j] = dp[i - 1][j - w[i] * k] + c[i] * k,dp[i - 1][j](j >= w[i] * k)\)
代码
点击查看代码
for (int i = 1; i <= N; i++) {
for (int j = 0; j <= V; j++) {
for (int k = 0; k <= n[i]; k++) {
if (j >= c[i] * k) {
dp[i][j] = max(dp[i - 1][j - c[i] * k] + w[i] * k, dp[i][j]);
}
}
}
}
P1616 疯狂的采药
思路
这个题和上一个的唯一区别是这个题我物品你可以随便取,那么又该如何去做呢?
朴素的方法是这样:
for (int i = 1; i <= N; i++) {
for (int j = 0; j <= V; j++) {
for (int k = 0; k * c[i] <= j; k++) {
dp[i][j] = max(dp[i - 1][j - c[i] * k] + w[i] * k, dp[i][j]);
}
}
}
但是啊这破玩意时间复杂度太高了啊,那就优化,如何优化嘞?我们发现
\(dp[i][j] = max(dp[i - 1][j],dp[i - 1][j - w[i]] + c[i],dp[i - 1][j - w[i] * 2] - c[i] * 2,...)\)
然鹅
\(dp[i][j - c[i]] = max(dp[i - 1][j - w[i]] + c[i],dp[i - 1][j - w[i] * 2] + c[i] * 2,dp[i - 1][j - w[i] * 3] + c[i] * 3,...)\)
所以
\(dp[i][j] = max(dp[i - 1][j],dp[i][j - w[i]] + c[i])\)
代码
for (int i = 1; i <= n; i++) {
for (int j = 0; j <= v; j++) {
if (j >= c[i]) {
dp[i][j] = max(dp[i][j - c[i]] + w[i], dp[i - 1][j]);
} else {
dp[i][j] = dp[i - 1][j];
}
}
}
简单DP问题
Frog 1
思路
这个题明显是一个构造题,首先构造 \(dp_i\) 表示跳到 \(i\) 为止,最小的花费,那么对于从 \(1~n\) 的枚举过程中,显然 \(dp_i = min(dp_{i-1}+abs(h_i - h_{i-1}),dp_{i-2}+abs(h_i-h_{i-2}))\)。知道这个,问题就迎刃而解了。
代码
点击查看代码
#include<iostream>
#include<cmath>
using namespace std;
const int MAXN = 1e5 + 5;
int dp[MAXN];
int h[MAXN];
int n;
int main(){
cin >> n;
for(int i = 1;i <= n;i ++){
cin >> h[i];
}
dp[1] = 0;
dp[2] = abs(h[2] - h[1]);
for(int i = 3;i <= n;i ++){
dp[i] = min(dp[i - 1] + abs(h[i] - h[i - 1]),dp[i - 2] + abs(h[i] - h[i - 2]));
}
cout << dp[n] << "\n";
return 0;
}
点击查看代码
#include<iostream>
#include<cmath>
#include<cstring>
using namespace std;
const int MAXN = 1e5 + 5;
int dp[MAXN];
int h[MAXN];
int n,k;
int main(){
memset(dp,0x3f3f3f3f,sizeof(dp));
cin >> n >> k;
for(int i = 1;i <= n;i ++){
cin >> h[i];
}
dp[1] = 0;
dp[2] = abs(h[2] - h[1]);
for(int i = 3;i <= n;i ++){
for(int j = max(1,i - k);j <= i;j ++){
dp[i] = min(dp[i],dp[j] + abs(h[i] - h[j]));
}
}
cout << dp[n] << "\n";
return 0;
}
本文来自一名初中牲,作者:To_Carpe_Diem