DP总结复习回顾

MrMorgan / 2024-02-17 / 原文

参考了Peppa_Even_Pig相关文章

什么是DP呢

 动态规划算法是通过拆分问题,定义问题状态和状态之间的关系,使得问题能够以递推(或者说分治)的方式去解决。 动态规划算法的基本思想与分治法类似,也是将待求解的问题分解为若干个子问题(阶段),按顺序求解子阶段,前一子问题的解,为后一子问题的求解提供了有用的信息。在求解任一子问题时,列出各种可能的局部解,通过决策保留那些有可能达到最优的局部解,丢弃其他局部解。依次解决各子问题,最后一个子问题就是初始问题的解。

能采用DP去做的前提一般具有三个性质

1、最优子结构;2、无后效性(不被前面影响);

背包DP

<1>01背包

有N件物品和一个容量是V的背包,每件物品只能使用一次。 第i件物品的体积是Vi,价值是Wi. 求将哪些物品装入背包,使其总体积不超过背包容量,且总价值最大。输入N,V后,接下来N行,输入Vi,Wi。

问题分析:

01背包本质是由上层状态转移而来(满足条件的前提),由于其只能选一次,状态的转移沿对角线方向;初始化为题目所要求;(一般为f[0][0] = 0; memset(f, 0, sizeof(f));); 当某一层状态转移满时,其后的状态可能会出现不可预料的错误(一般是由于初始化导致的)(初始化非常重要!!!)(其实最优解就在最后一行的某个地方);当初始化为0xcf时, 可能会出现f[][]数组中有0xcf+符合条件的最优价值的情况;cout << f[n][m]; 也无法出现最优解;(其实f数组中可能根本没有最优解,(初始化已经出现错误,体现了初始化的重要性),max也没用);

朴素做法:

点击查看代码

#include <iostream>
#include <cstring>
using namespace std;
int f[2005][2005]; //f[i][j]表示前i件物品,容量为j时最大价值; 
int m, n;
int w[10005], c[10005];
int main() {
	cin >> m >> n;
	memset(f, 0, sizeof(f));
	for (int i = 1; i <= n; i++) {
		cin >> w[i] >> c[i];
	}
	f[0][0] = 0;
	for (int i = 1; i <= n; i++) {
		for (int j = 0; j <= m; j++) {
			f[i][j] = f[i - 1][j];
		}
		for (int j = w[i]; j <= m; j++) {
			f[i][j] = max(f[i][j], f[i - 1][j - w[i]] + c[i]);
		}
	}
	cout << f[n][m];
	return 0;
}

*也可以优化成滚动数组做法:

点击查看代码
#include <bits/stdc++.h>
using namespace std;
int c[10005], w[10005]; //w[i]为第i件物品的重量,c[i]为第i件物品的价值; 
int f[10005]; //f[i]为 容量为i时的最大价值; 
int m, n; //m为背包容量,n为总物品数量;
int main() {
	cin >> m >> n;
	memset(f, 0, sizeof(f)); //初始化,等待赋值; 
	for (int i = 1; i <= n; i++) {
		cin >> w[i] >> c[i];
	}
	f[0] = 0; //初始化,当容量为0时最大价值为0; 
	for (int i = 1; i <= n; i++) {
		for (int j = m; j >= w[i]; j--) {
			f[j] = max(f[j], f[j - w[i]] + c[i]);
		}
	}
	cout << f[m];
	return 0;
}

Q:为什么要内层循环要倒序呢?

A:因为每种物品只能选一次, 其实就是第i层的状态只能由第i-1层的状态转移一次得来,若顺序,则当第i层第j个状态被更新时,它就到了第i-1层,后面的第i层的j + w[i]个状态又可能被此状态更新,这样一个物品就拿了两次、 如此循环往复,则物品都能哪无限次了。

接下来是最终优化版:

点击查看代码
#include<bits/stdc++.h>
#define N 1145
using namespace std;
int v[N],w[N];
int n,V;
int f[N];
int main()
{
	cin>>n>>V;
	for(int i=1;i<=n;i++) cin>>v[i]>>w[i];
	for(int i=1;i<=n;i++)
	{
		for(int j=V;j>=v[i];j--)
		{
			f[j]=max(f[j],f[j-v[i]]+w[i]);
		}
	}
	cout<<f[V];
	return 0;
}

<2>完全背包

也就是将物品改成无限个。

在上面Q&A中已经提及,只需把内层循环顺序即可。

点击查看代码
#include<bits/stdc++.h>
#define N 100000
using namespace std;
int m,n;
int f[N],v[N],w[N];
int ans;
int main()
{
	cin>>m>>n;
	for(int i=1;i<=n;i++) cin>>v[i]>>w[i];
	for(int i=1;i<=n;i++)
	{
		for(int j=v[i];j<=m;j++)
		{
			f[j]=max(f[j],f[j-v[i]]+w[i]);
		}
	
	}
	
	cout<<"max="<<f[m];
	return 0;
}

<3>多重背包

也就是一个物品取有限次。

朴素版本:

点击查看代码
#include<bits/stdc++.h>
#define N 1010
using namespace std;
int f[N],v[N],w[N],c[N];
int n,m;
int main()
{
	cin>>n>>m;
	for(int i=1;i<=n;i++) cin>>v[i]>>w[i]>>c[i];
	for(int i=1;i<=n;i++)
	{
		for(int j=1;j<=c[i];j++)
		{
			for(int k=m;k>=v[i];k--)
			{
				f[k]=max(f[k],f[k-v[i]]+w[i]);
			}
		}
	}
	cout<<f[m];
	return 0;
}

显然,时间复杂度飙到了O(n^3),遇到大点的数据就寄。

这里考虑二进制优化:

也就是将一个物品分解成多个二进制数表示的形式,然后再用01背包求解。

如:17=2+4+8+2+1

代码实现:

点击查看代码
#include <bits/stdc++.h>
using namespace std;
int n, m, num;
int w[1000005], c[1000005]; //要开大一点,因为要分解; 
int f[1000005];
int main() {
	cin >> n >> m; //物品数量和背包容积; 
	int a, b, num; //每件物品的重量,价值,个数; 
	int cnt = 0;
	for (int i = 1; i <= n; i++) {
		cin >> a >> b >> num;
		for (int j = 1; j <= num; j <<= 1) { // j *= 2; 
			w[++cnt] = a * j;
			c[cnt] = b * j;
			num -= j; //这样就可以保证每件物品只能选一次(因为如果选多次就超过了原来的num;
		}
		if (num) { //如果num不能完全分解,那就把剩下的直接存入; 
			w[++cnt] = a * num;
			c[cnt] = b * num;
		}
	}
	for (int i = 1; i <= cnt; i++) {
		for (int j = m; j >= w[i]; j--) {
			f[j] = max(f[j], f[j - w[i]] + c[i]);
		}
	}
	cout << f[m];
	return 0;
}