记忆化搜索与动态规划
01背包问题
题目描述
有 N 件物品和一个容量是 V 的背包。每件物品只能使用一次。
第 i 件物品的体积是 vi,价值是 wi。
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出最大价值。
输入格式
第一行两个整数,N,V,用空格隔开,分别表示物品数量和背包容积。
接下来有 N 行,每行两个整数 vi,wi,用空格隔开,分别表示第 i 件物品的体积和价值。
输出格式
输出一个整数,表示最大价值。
数据范围
0
输入样例
4 5
1 2
2 4
3 4
4 5
输出样例
8
分析
首先考虑朴素做法,即枚举所有可能的情况,输出其中价值最大的那一个
时间复杂度O(2n)
代码如下
#include
using namespace std;
const int N=1010;
int V[N],W[N];
int n,v;
int res;
void dfs(int k,int w,int m) //k代表当前选的是第k+1个物品,w是当前的物品价值,m是剩余背包容量
{
if(k==n) //所有物品都选过一遍
{
res=max(res,w);
return ;
}
if(m>=V[k])
{
dfs(k+1,w+W[k],m-V[k]);
dfs(k+1,w,m);
}
else
{
dfs(k+1,w,m);
}
}
int main()
{
cin>>n>>v;
for(int i=0;i<n;i++)
{
cin>>V[i]>>W[i];
}
dfs(0,0,v);
cout<<res<<endl;
return 0;
}
提交结果:Time Limit Exceeded
这种写法的时间复杂度是指数级别的,当N=100的时候,最坏就需要O(2100)的时间,而题目中N<=1000,接下来就需要考虑对这种暴力解法进行优化。
在上述搜索的过程中,有些中间过程是相同的,它们的结果是相同的,但在搜索的过程中,并不会记录这些值,每次都需要重新计算。从这个层面,可以考虑记录中间结果,对上述算法进行优化。
优化后的代码
#include
#include
using namespace std;
const int N=1010;
int V[N],W[N];
int dp[N][N];
int n,v;
int dfs(int i,int j)
{
if(dp[i][j]>=0)
{
return dp[i][j];
}
int res;
if(i==n)
{
res=0;
}
else if(j<V[i])
{
res=dfs(i+1,j);
}
else
{
res=max(dfs(i+1,j),dfs(i+1,j-V[i])+W[i]);
}
return dp[i][j]=res;
}
int main()
{
cin>>n>>v;
memset(dp,-1,sizeof(dp));
for(int i=0;i<n;i++)
{
cin>>V[i]>>W[i];
}
cout<<dfs(0,v)<<endl;
return 0;
}
提交结果:Accepted
时间复杂度:O(NV)
对于同样的参数只会在第一次被调用时执行递归部分,第二次之后都会直接返回。这种方法被称为记忆化搜索。
根据上面的算法用到的记忆化数组,记dp[i][j]为从前i个物品中选出总体积不超过j的物品时总价值的最大值,可得以下递推式
d p [ 0 ] [ j ] = 0 dp[0][j]=0 dp[0][j]=0
d p [ i ] [ j ] = { d p [ i − 1 ] [ j ] , j < V [ i ] m a x ( d p [ i − 1 ] [ j ] , d p [ i − 1 ] [ j − V [ i ] ] + W [ i ] ) , j > = V [ i ] dp[i][j]=\left\{ \begin{aligned} &dp[i-1][j], & j
AC代码
#include
#include
using namespace std;
const int N=1010;
int V[N],W[N];
int dp[N][N];
int n,v;
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=1;j<=v;j++)
{
if(j<V[i]) dp[i][j]=dp[i-1][j];
else dp[i][j]=max(dp[i-1][j],dp[i-1][j-V[i]]+W[i]);
}
}
cout<<dp[n][v]<<endl;
return 0;
}
这个算法的时间复杂度也是O(NV),但是简洁了很多。以这种方式一步步按顺序求出问题的解的方法被称作动态规划法。
观察上面的代码发现,dp[i][j]的取值只与上一层(即第i-1行)的值有关,且j
AC代码
#include
#include
using namespace std;
const int N=1010;
int V[N],W[N];
int dp[N];
int n,v;
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--)
{
dp[j]=max(dp[j],dp[j-V[i]]+W[i]);
}
}
cout<<dp[v]<<endl;
return 0;
}
需要注意的是,使用滚动数组时,j应该反过来循环,即从后面向前面覆盖,且循环的条件是j>=V[i],这是因为j 装箱问题 题目描述 输入描述: 1个整数,表示箱子容量 输出描述: 1个整数,表示箱子剩余空间。 示例1 输入 24 输出 0 分析 可以看作是简化的01背包问题,将物品的体积看作价值,求出最大的价值,再用总体积减去最大的价值,可求得箱子剩余空间的最小值。首先可以考虑采用记忆化搜索的方法,采用记忆化搜索的方法注意进行初始化,可以初始化为-1,而不能是0,因为f[i][j]==0时,无法确定当前状态的最大价值是0,还是没有搜索过。 还可以采用01背包状态转移那样的方法做,将体积当做权值,数组可以是二维的,也可以是一维的(滚动数组),这两种方法的核心代码如下 二维数组 滚动数组 此题还有另外一种思路,用bool数组f[i][j]表示前i个物品能否放满体积为j的背包,记录最大的j,即为能放下的最大体积,再用容量减去最大体积,就可以得到空间的最小值。按照这个思路,仍有二维数组和滚动数组两种做法。 二维数组 滚动数组 注意 滑雪 Description Michael喜欢滑雪百这并不奇怪, 因为滑雪的确很刺激。可是为了获得速度,滑的区域必须向下倾斜,而且当你滑到坡底,你不得不再次走上坡或者等待升降机来载你。Michael想知道载一个区域中最长底滑坡。区域由一个二维数组给出。数组的每个数字代表点的高度。下面是一个例子 Input 输入的第一行表示区域的行数R和列数C(1 <= R,C <= 100)。下面是R行,每行有C个整数,代表高度h,0<=h<=10000。 Output 输出最长区域的长度。 Sample Input 5 5 Sample Output 25 分析 F [ i ] [ j ] = m a x { f [ i − 1 ] [ j ] + 1 if ( a [ i − 1 ] [ j ] < a [ i ] [ j ] ) f [ i + 1 ] [ j ] + 1 if ( a [ i + 1 ] [ j ] < a [ i ] [ j ] ) f [ i ] [ j − 1 ] + 1 if ( a [ i ] [ j − 1 ] < a [ i ] [ j ] ) f [ i ] [ j + 1 ] + 1 if ( a [ i ] [ j + 1 ] < a [ i ] [ j ] ) F[i][j] = max\begin{cases} f[i - 1][j] + 1 &\text{if } (a[i - 1][j] F[i][j]=max⎩⎪⎪⎪⎨⎪⎪⎪⎧f[i−1][j]+1f[i+1][j]+1f[i][j−1]+1f[i][j+1]+1if (a[i−1][j]<a[i][j])if (a[i+1][j]<a[i][j])if (a[i][j−1]<a[i][j])if (a[i][j+1]<a[i][j]) AC代码 注意 总结 1.记忆化搜索和动态规划从根本上来讲就是一个东西,两者的核心思想均为:利用对于相同参数答案相同的特性,对于相同的参数(循环式的dp体现为数组下标),记录其答案,免去重复计算,从而起到优化时间复杂度的作用 2.做动态规划的一般步骤: 第一步,结合原问题和子问题确定状态 第二步,确定转移方程 第三步,考虑需不需优化 第四步,确定编程实现方式
有一个箱子容量为V(正整数,0 ≤ V ≤ 20000),同时有n个物品(0<n ≤ 30),每个物品有一个体积(正整数)。
要求n个物品中,任取若干个装入箱内,使箱子的剩余空间为最小。
1个整数,表示有n个物品
接下来n行,分别表示这n个物品的各自体积
6
8
3
12
7
9
7
#include
for(int i=1;i<=n;i++)
{
for(int j=1;j<=v;j++)
{
if(j<a[i]) f[i][j]=f[i-1][j];
else f[i][j]=max(f[i-1][j],f[i-1][j-a[i]]+a[i]);
}
}
for(int i=1;i<=n;i++)
{
for(int j=v;j>=a[i];j--)
{
dp[j]=max(dp[j],dp[j-a[i]]+a[i]);
}
}
#include
#include
f[0][0]或f[0]初值为1,体积为0的时候认为背包是放满的,这样设定保证j=a[i]时,f[i][j]可以赋值为1
一个人可以从某个点滑向上下左右相邻四个点之一,当且仅当高度减小。在上面的例子中,一条可滑行的滑坡为24-17-16-1。当然25-24-23-…-3-2-1更长。事实上,这是最长的一条。
1 2 3 4 5
16 17 18 19 6
15 24 25 20 7
14 23 22 21 8
13 12 11 10 9
记忆化搜索,用f[i][j]表示从(i,j)滑下的最长路径长度,对于每个点(i,j)能滑的最长长度与周围四个点有关,f[i][j]的初始值为1,代表自身的长度为1,然后用(i,j)点的高度与周围四个点的高度比较,状态转移方程如下。#include
如果(i,j)位于边界要注意判断四周的位置是否合法,而且最大值的位置不确定,要取所有位置上的最大值。此外本题没有采用递推的方式,即用两重for循环遍历,而是采用记忆化搜索,这是因为每个点的取值与周围四个点有关,而在01背包中每个点的取值只与上一层有关,因此此题适合用记忆化搜索。
状态的参数一般有
1)描述位置的:前(后)i单位,第i到第j单位,坐标为(i,j),第i个之前(后)且必须
取第i个等
2)描述数量的:取i个,不超过i个,至少i个等
3)描述对后有影响的:状态压缩的,一些特殊的性质
1)检查参数是否足够;
2)分情况:最后一次操作的方式,取不取,怎么样取——前一项是什么
3)初始边界是什么
4)注意无后效性。比如说,求A就要求B,求B就要求C,而求C就要求A,这就不符合无后效性了
根据状态枚举最后一次决策(即当前状态怎么来的)就可确定出状态转移方程!
1)递推
2)记忆化搜索