动态规划
主要思想
顾名思义,就是动态地根据每个阶段的状态解决问题,而且这种解决方法是可以通过代码表达的,而能够应用这种方法的问题有以下特点——
每个阶段的状态是未知的,但是可预知且可列的,并且与前一阶段相关,这种特点常常决定了它的递推式;
同时,满足最优子结构,子问题的最优构成了整体的最优,所以递推是可行的;
在决策定下后,该决策也不会因为后续阶段的决策而受到影响。
0/1背包问题
问题描述
有n个背包,已知每个背包的重量wi和价值vi,在总重量W固定的情况下,选择背包,使总价值V最大。
递归实现
基本递推式
设f(i,y)表示总重量为y时,放入i,…n背包可以得到的最大价值,那么:
f(i, y) = max{ f(i + 1, y), F(i + 1, y - w[i]) + v[i] }, if i < n && y >= w[i];//递归式1
f(i, y) = f(i + 1, y) if i < n && y < w[i]; // 递归式2
f(i, y) = v[n], if i == n && y >= w[n]//终止条件1
else f(n, y) = 0;//终止条件2
代码实现
int RecursiveDp(int s, int w){
if(s == n){
if(w[n] > w){
return 0;
}
else return v[n];
}
if(w[s] > w){
return RecursiveDp(s + 1, w);
}
else return max(RecursiveDp(s + 1, w), Recursive(s + 1, w - w[s]) + v[s]);
}
时间复杂度
T(n) = 2*T(n - 1) + c (c为常数)
则时间复杂度为O(2 ^ n)
价值为整数时的非递归实现
基本思路
设置一个二维数组dp[n][W],代替f(i,y),减少了重复调用的那部分时间(用空间换时间)
代码实现
int n,w;
int dp[MAX_N][MAX_N];
void CreateDp(){
for(int i = 0; i <= w; i ++){
dp[n][i] = w[n] >= i ? 0 : v[n];
}
for(int i = n - 1; i >= 1; i --){
for(int j = 0; j <= w; j ++){
if(w[i] > j){
dp[i][j] = dp[i + 1][j];
}
else{
dp[i][j] = max(dp[i + 1][j], dp[i + 1][j - w[i]] + v[i]);
}
}
}
}
int solve(){
return dp[1][w];
}
复杂度
空间复杂度:dp数组大小为n*w,如果w很大,其实就有风险;
时间复杂度:创建dp数组的时间复杂度为O(n * w),如果w = 2 ^ n, 时间复杂度就很高,dp的实现的时间复杂度为O(1)
元组法
基本思想
如果总重量较大,dp数组的空间会很大,需要得到优化,通过观察,我们发现dp数组的每一行的值是重复的,直到在某个位置发生突变,而且这种突变是有规律的,那么我们只需要存入这些突变的点
通过实例理解
实例:n=5,v=[6,3,5,4,6],w=[2,2,6,5,4] 且c=10 ,求f (1,10).
f(1,10)=max{f(2,10),f(2,10-2)+6}=15
- 跳跃的位置即是和是否加入该背包紧密相关,同时与前一个情况做了比较
代码实现
int n,w;
int jw[MAX_N];
int jv[MAX_N];
int Jumppoint(int num, int wei){
jw[0] = 0;
jv[0] = 0;
jw[1] = w[0];
jv[1] = v[0];//初始状态
int left = 0, right = 1, next = 2;//[left,right]为上一层的比较和结合范围 ,next表示构建新的元组的索引
for(int i = 1; i < num; i ++){//依次向上更新 和f(i,y)的含义刚好相反
int k = left;
for(int j = left; j <= right; j ++){//以k为索引,依次结合
if(jw[j] + w[i] > wei)break;//一旦超重,就不用看后面了,因为重量递增,价值递增
int nw = jw[j] + w[j];
int nv = jv[j] + v[j];//结合情况
for(; k <= right && jw[k] < nw; k ++, next ++){
jw[next] = jw[k];
jv[next] = jv[k];
}//小于结合重量的跳跃点不受影响,但还是跳跃点
for(; k <= right && jw[k] == nw; k ++){
if(jv[k] > nv)nv = jv[k];
}//如果和结合重量相等,则价值大的优先
if(nv > jv[next - 1]){
jw[next] = nw;
jv[next] = nv;
next ++;
}//判断新结合的这个点是否为合格的跳跃点,做到重量递增的时候,价值也递增
else continue;
for(; k <= right && jv[k] <= nv; k ++);//后面的点重量更大,如果价值更小,则无效了
}
for(;k <= right; k ++, next ++){
jw[next] = jw[k];
jv[next] = jv[k];
}
left = right + 1;
right = next - 1;
}
return jv[right];//最后一个点就是所需结果
}
int solve(){
return Jumppoint(n,w);
}
复杂度
空间复杂度:最坏为2 * (2 ^ n - 1) * 2
时间复杂度: 最坏也为 O(2 ^ n)
所以使用这种方法是不稳定的