何为动态规划
动态规划的核心思想是利用已知信息逐步推导未知信息,像斐波那契数列就是典型的根据已知推未知的问题。在动态规划的过程中,我们围绕着一个数组或表格进行操作,需要明确我们要维护的状态,状态的初始化,以及状态的转移,像斐波那契数列的状态就是数字,初始化就是第一、二位都为1,而状态的转移就是从第三位开始,每一位都等于前两位之和。
状态的定义
状态需要满足无后效性,即状态一旦确定,之后的操作都不会改变已有的状态,且之后的状态只能有先前的状态转移过来。
状态转移方程
状态转移方程就是状态转移的方式,像斐波那契数列,状态转移方程就是f[i] = f[i-1] + f[i-2].
解题的基本步骤
对于DP问题,我们一般按照确定状态定义,确定状态转移方程,确定初始状态,确定答案状态这个步骤进行。
简单的DP问题——背包DP
背包问题是最简单的一类动态规划问题,根据每种物品的数量可以分为01背包(每种物品只有一个,每个物品只有选与不选两种状态)、完全背包(每种物品的数量有无限个)、多重背包(每种物品有有限个)。
01背包
如何在有限的背包容量下选择有着一定体积的物体并求得最大总价值,这是01背包问题的一般形式,之所以叫它01背包,是因为每个物品只有选择与不选择两种状态,分别对应二进制的0和1。我们用一个例题来详细讲解:
例题 星码 采药
本题是一个典型的01背包问题,根据做题步骤,很容易就能发现状态就是一件物品的选择与否,状态转移方程就是当前总价值与选择后的总价值之间的最大值,而状态的初始化就是,背包总价值为0,总重量也为0,而答案所要的状态是在有限的背包容量下使背包总价值最大,参考代码如下:
#include<bits/stdc++.h>
using namespace std;
using ll = long long;
const int N = 1e3+5;
int w[N],v[N],dp[N],t,m;
//w代表体积,v代表价值,dp为状态
void solve()
{for(int i = 1;i<=t;i++) dp[i] = 0;//状态的初始化,初始状态背包为空for(int i = 1;i<=m;i++) cin >> w[i] >> v[i];for(int i = 1;i<=m;i++)for(int j = t;j>=w[i];j--)dp[j] = max(dp[j],dp[j-w[i]] + v[i]);//j代表当前背包的容量//状态的转移,我们这里采用了滚动数组(新状态会覆盖旧状态)进行优化,为了确保dp[j-w[i]]是上一个状态我们需要倒序操作,//如果正序操作,在计算dp[j]时dp[j-w[i]]就已经被新状态覆盖了假设j = 7,w[i] = 3,很容易就能发现,当j = 8时,它的上一个状态dp[4]已经被修改过//j>=w[i]是为了防止数组越界cout << dp[t] <<"\n";//最终状态即为背包容量为t时价值的最大值
}int main()
{ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);while (cin >> t >> m){if(t==0 && m==0)break;solve();}//本题有多个样例,且在t,m均为0时终止运行return 0;
}
完全背包
与01背包不同,完全背包的每种物品都各有无限个,体现在题目中,我们在使用滚动数组解题时,将倒序操作改为正序操作,在01背包问题中,这样可能会导致重复计算,但在完全背包问题中,我们要的就是重复计算,所以只需要对01背包问题的代码稍加修改即可解决完全背包问题。
例题 星码 无穷背包
#include<bits/stdc++.h>
using namespace std;
using ll = long long;
const int N = 1e5+5;
int m, n,w[505],v[505],dp[N];
//w是物品的体积,v是价值,dp是状态数组
void solve()
{cin >> m >> n;for(int i = 1;i<=n;i++)cin >> w[i] >> v[i];for(int i = 1;i<=n;i++)for(int j = v[i];j<=m;j++) dp[j] = max(dp[j],dp[j-v[i]]+w[i]);//只有j(当前背包容量)大于v[i]时才能考虑装载新物品,所以j直接从v[i]开始//由于每种物品都有无数个,所一01背包问题中正序操作的重复问题就成了我们解题的捷径,为了打倒最终状态,我们可以一直向背包中装入某种物品cout << dp[m] <<"\n";//最终状态,当背包容量为m时的最大价值
}int main()
{ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);int _ = 1;while(_--)solve();return 0;
}
多重背包
多重背包的每种物品有有限个,但我们可以把每个物品都归为一“类”使多重背包问题转化为01背包问题。转化的方式有很多种,如果数据量较小,可以直接重复添加一类物品多次;如果数据量较大,则可考虑利用二进制的性质(每个十进制数都能由二进制表示)对数据进行压缩,以优化时间复杂度。
例题 星码 多重背包
本题的数据量较小,最多有1e4“类”物品,所以我们可以直接暴力存储这些“类”,在完成数据存储后,按照01背包的方法即可解答。
#include<bits/stdc++.h>
using namespace std;
using ll = long long;
const int N = 1e4+10;
int m,n,x,y,z,w[N],v[N],t = 0;
ll dp[N];void solve()
{for(int i = 0;i<=m;i++)dp[i] = 0;//初始化状态,什么都不选时价值为0cin >> m >> n;while(n--){cin >> x >> y >> z;//x,y,z分别是数量、价值、体积while(x--){w[++t] = y;v[t] = z;}//把每件物品都看做一“类”}for(int i = 1;i<=t;i++)for(int j = m;j>=v[i];j--)dp[j] = max(dp[j],dp[j-v[i]]+w[i]);//01背包的标准做法,不过要注意,此时物品的种类数从n(此时n为0)变成了t cout << dp[m] <<"\n";//输出背包容量为m时,背包所能容纳的最大价值
}int main()
{ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);int _ = 1;while(_--)solve();return 0;
}
例题 星码 多重背包二周目
#include<bits/stdc++.h>
using namespace std;
using ll = long long;
const int N = 5e6+9;
int n,m,s,w,v,a[N],b[N],t = 0;
ll dp[N];
//数据量较大,给状态开long long防止溢出
void solve()
{cin >> m >> n;//m,n分别是背包容量,物品种类while(n--){cin >> s >> w >> v;//s,w,v分别是数量、价值、体积int k = 1;while(k<=s){b[++t] = w*k;a[t] = v*k;s-=k;k*=2;}//任意十进制数都能由二进制数表示if(s>0){b[++t] = w*s;a[t] = v*s;}//若干次分割后,可能会有余量,通常较小,可以直接添加到数组中}for(int i = 1;i<=t;i++)for(int j = m;j>=a[i];j--)dp[j] = max(dp[j],dp[j-a[i]]+b[i]);//01背包的标准做法cout << dp[m] <<"\n";
}int main()
{ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);int _ = 1;while(_--)solve();return 0;
}
本人初学算法,能力有限,如有纰漏,敬请指正。