在acwing上学习算法时的一点思考与总结。
思路:动态规划解决问题的方式就是将一个大问题分解成多个子问题。每个子问题的决策都会影响下一个子问题的决策,也就是i状态会受到i-1状态的影响。根据这个特点,我们可以写出状态转移方程。状态转移方程的意义就在于不断更新f[ i ][ j ] 的结果。
思考:dp的优势就在于当考虑i状态时,i-1前面的状态就可以不用考虑了,他们的最优解已经被计算出来存在f[ i ][ j ]里了。所以我们只要考虑i和i-1状态的关系,然后更新i状态下的最优解就可以了。就这样一直往下推进,直到i到边界。
f[ i ][ j ]有两层含义:一个大集合(存储了所有不同状态下的结果) + 属性(最大值最小值)
dp代码思路:首先想清楚用几维的f来表示状态,以及对应的i和j分别表示什么,比如在最长上升序列中只需要用一维f[ i ]就表示数组中以w[ i ]结尾的最长上升序列。在背包问题中f[i][j]表示前i个物品,体积不超过j时的最大价值。然后根据状态的前后影响关系写出状态转移方程。
干讲还是有点抽象,下面结合几个例题。
思路:因为限制条件(有限体积)的存在,我们每次放一个物品到背包里都会影响下一个物品是否能放,也就是i状态会受到i-1状态的影响。因此我们要用dp来找出所有符合条件的结果,把他放在集合f[ i ][ j ]中,比如f[ 3 ][ 4 ]表示从前三个物品中取并且体积不超过4时的最大价值。最后通过遍历这个集合直接输出最大值。
二维
#include
using namespace std;
const int N = 1010;
int n, m;
int f[N][N]; //表示前i个物品,体积小于等于j时的最大价值
int v[N], w[N]; //v表示体积,w表示权重(价值)
int main()
{
cin>>n>>m;
for(int i = 1; i <= n; i ++) cin>>v[i]>>w[i];
for(int i = 1; i <= n; i ++) //遍历前i个物品
for(int j = 1; j <= m; j++) //体积
{
f[i][j] = f[i-1][j]; //表示不放物品
if(j >= v[i]) //j要大于等于当前放置物体的体积
f[i][j] = max(f[i][j], f[i-1][j-v[i]] + w[i]); // 从1 ~ i-1中物品选,体积为j-v[i]时的最大价值加上当前物品的价值。
}
int res = 0;
for(int i = 0; i <= m; i ++) res = max(res,f[n][i]); //遍历取四个物品时的最大价值
cout<
优化为一维后的代码
解释一下为什么j是从最大体积开始往下遍历:
首先我们要知道这道背包问题的每个物品只能取一次,也就是说f[i]的状态计算必须由f[ i - 1][ j ]而来(f[ i - 1]表示前i-1个物品不超过j的最大价值)。
综述:i是由i-1得到的,而此时若j还是由小到大,那么i更新是就会是以在i循环内更新的j来进行更新比j大的数,但这不符合我们二维分析时的情况,因此我们需要从小到大,这样大j在更新时,是用的仍然是小j在i-1时更新的值。
#include
using namespace std;
const int N = 1010;
int n, m;
int v[N], w[N];
int f[N];
int main()
{
cin>>n>>m;
for(int i = 1; i <= n; i ++) cin>>v[i]>>w[i];
for(int i = 1; i <= n; i ++ )
for(int j = m; j >= v[i]; j --)
f[j] = max(f[j], f[j-v[i]] + w[i]);
int res =0;
for(int i = 0; i <= m; i ++) res = max(res, f[i]);
cout<
差异就在于此题里,物品有无限个,所以j就要从v[ i ]开始遍历到最大体积。
#include
using namespace std;
const int N = 1010;
int n,m;
int v[N], w[N], f[N];
int main()
{
cin>>n>>m;
for(int i = 0; i < n; i ++) cin>>v[i]>>w[i];
for(int i = 0; i < n; i ++)
for(int j = v[i]; j <= m; j ++) //从v[i]遍历到最大体积
{
f[j] = max(f[j], f[j-v[i]]+w[i]);
}
cout<
思路:将多重背包问题拆成01背包问题来做。假设一件物品的体积价值和数量分别是1 2 3,就可以拆成(1,2) 或 (2,4)或(3,6) 相当于是等价替换成三件新的物品,每个物品只能取一次,又回到了01背包问题。
#include
using namespace std;
const int N = 105;
int n,m;
int v[N], w[N], s[N], f[N];
int main()
{
cin>>n>>m;
for(int i = 0; i < n; i ++) cin>>v[i]>>w[i]>>s[i];
for(int i = 0; i < n; i ++)
for(int j = m; j >= v[i]; j --)
//下面这两代码合并起来看,和01背包的做法是一样的
for(int k = 0; k <= s[i] && k*v[i] <= j; k ++)
f[j] = max(f[j],f[j-k*v[i]]+k*w[i]);
cout<
思路:也是拆成01背包问题来做,只不过不能再像上面那样循环一遍来拆分,一定会超时的。所以就用二进制的方法来拆分。因为二进制可以表达所有整数,比如一个在[ 0 , 255 ]的数字,它可以用1 2 4 8 16 32 64 128组合表示,所以时间复杂度就从 O(n^3)降到O(n^2logS)
#include
using namespace std;
const int N = 12010, M = 2010;
int a, b, n, m, s;
int v[N], w[N], f[M];
int main()
{
int cnt = 0;
cin>>n>>m;
for(int i = 1; i <= n; i ++)
{
cin>>a>>b>>s;
int k = 1;
while( k <= s)
{
cnt ++;
v[cnt] = k * a;
w[cnt] = k * b;
s = s - k;
k *= 2;
}
if(s > 0) //将剩下的s进行处理
{
cnt ++;
v[cnt] = s * a;
w[cnt] = s * b;
}
}
n = cnt; //更新n的个数
for(int i = 1; i <= n; i ++)
for(int j = m; j >= v[i]; j --)
f[j] = max(f[j], f[j-v[i]] + w[i]);
cout<