一、
鉴别:题目里有最优、最长、最大、最小、计数等字眼,要考虑是否是DP问题
二、解决思路
将原问题拆解成子问题
- 所有的动归问题基本都可以使用递归求解(想象斐波那契数列),寻找递推公式,这里的设计要考虑无效性,即如果我走了当前这一步,会造成什么后果(比如下一步不能走)。
- 但是利用递归会出现大量的冗余计算,这时要想着怎么优化问题——去冗余(一般问题都是离散的,这样把之前计算的结果保存到数组、二维数组、map等数据结构中,就可以防止重复计算)
- 递归是自顶向下计算,从n推到0,可以使用循环(自底向上)从0推到N设计,避免递归。
三、经典题
- 斐波那契数列(上台阶)
- 机器人在棋盘中的路径
#include <iostream>
#include <vector>
using namespace std;
int uniquePaths(int m, int n);
int main()
{
int a=2,b=4;
int r= uniquePaths(a,b);
cout<<r<<endl;
}
int uniquePaths(int m, int n)
{
int res[m][n];
res[0][0]=1;
for(int i=0;i<m;i++)
res[i][0]=1;
for(int i=1;i<n;i++)
{
res[0][i]=1;
}
for(int i=1;i<m;i++)
{
for(int j=1;j<n;j++)
{
res[i][j]=res[i-1][j]+res[i][j-1];
if(j==1d&&i==1)
res[i][j]=0;
}
}
return res[m-1][n-1];
}
- 排列组合
- 01背包
#include<iostream>
using namespace std;
int main()
{
int N,V;
cin>>N>>V;
int weight[N];
int vaule[N];
for(int i=1;i<=N;i++)
cin>>weight[i]>>vaule[i];
int f[N+1][V+1];
for(int i=0;i<=N;i++)
{
for(int j=0;j<=V;j++)
{
f[i][j]=0;
}
}
for(int i=1;i<=N;i++)
{
for(int j=1;j<=V;j++)
{
f[i][j]=f[i-1][j];
if(j>=weight[i])
f[i][j]=max(f[i][j],f[i-1][j-weight[i]]+vaule[i]);
}
}
cout<<f[N][V];
}
//空间复杂度N*V,时间复杂度N*V
/****************************************************************/
优化空间复杂度的办法,用一个一维数组存储状态 (通用解法,使用滚动数组)
#include<iostream>
using namespace std;
int main()
{
int N,V;
cin>>N>>V;
int weight[N];
int vaule[N];
for(int i=1;i<=N;i++)
cin>>weight[i]>>vaule[i];
int f[V+1];
for(int j=0;j<=V;j++)
{
f[j]=0;
}
for(int i=1;i<=N;i++)
{
for(int j=V;j>=weight[i];j--)
{
f[j]=max(f[j],f[j-weight[i]]+vaule[i]); 改成一维数组
}
}
cout<<f[V];
}
- 完全背包问题(leedcode322)
完全背包问题有一个套路魔板
int main()
{
int N,V;
cin>>N>>V;
int weight[N];
int vaule[N];
for(int i=1;i<=N;i++)
cin>>weight[i]>>vaule[i];
int f[V+1];
for(int j=0;j<=V;j++)
{
f[j]=0;
}
for(int i=1;i<=N;i++)
{
/*******************************************************************************************
///主要的不同就在这里,就是01背包的起始范围反转一下,为什么这样做呢?原因是由于范围是从weight[i]~V,从小到
///到大运行,这样f[j-weight[i](因为在j之前)在本次循环里就算过了,f[j-weight[i]]这个状态的意义是总数为j-weight[i]
时,可能的最值,这里可能已经包含了若干个i物品,因为发f[j-weight]这个状态,是在第i次循环里面得到的。从而实现了
一个i可以被取多次的条件。
*************************************************************************************************/
for(int j=weight[i];j<=V;j++)
{
f[j]=max(f[j],f[j-weight[i]]+vaule[i]); 改成一维数组
}
}
cout<<f[V];
}
复杂度较高的直观写法
#include<iostream>
using namespace std;
int main()
{
int N,V;
cin>>N>>V;
int weight[N];
int vaule[N];
for(int i=1;i<=N;i++)
cin>>weight[i]>>vaule[i];
int dp[V+1];
for(int j=0;j<=V;j++)
dp[j]=0;
for(int i=1;i<=N;i++)
{
for(int j=V;j>=0;j--) ///从后往前便利是为了变一维数组时,用到的是i-1时刻的状态
{
for(int k=0;k<=j/weight[i];k++)
{
dp[j] = max(dp[j], dp[j-weight[i]*k] + k*vaule[i]);
}
}
}
cout<< dp[V]<<endl;
return 0;
}
- 多重背包问题
多重背包是限制单个物品个数的完全背包问题,其实就是在01背包上在套一个循环用来限制单个物体的取样次数。
#include<iostream>
using namespace std;
int main()
{
int N,V;
cin>>N>>V;
int weight[N];int value[N];int count[N];
for(int i=0;i<N;i++)
cin>>weight[i]>>value[i]>>count[i];
int dp[V+1];
for(int i=0;i<V+1;i++)
dp[i]=0;
for(int i=0;i<N;i++)
{
for(int j=V;j>=0;j--)
{
for(int k=0;(k<=count[i])&&(k*weight[i]<=j);k++)
dp[j]=max(dp[j],dp[j-k*weight[i]]+k*value[i]); 这里比01背包多加了一层循环,用来约束单个物体的取样次数问题
}
}
cout<<dp[V];
return 0;
}
优化解法(二进制优化)
多重背包的解法出现了三层循环,可以优化成两层循环,具体的优化思想是,每个物体可以取s次,可以把这s次当成一样的s个独立的物体,这样,多重背包问题就转化成了01背包问题,但是这里面有一个问题,就是将每个物体很多次这个事儿看成独立的物体,s的总次数如果比较多,就会出现较高的复杂度,于是使用取log的方式优化,举个例子,物体a可以选10次
那么现在把10拆分:
10=1+2+4 10-(1+2+4)=3 那么现在将10分成1,2,4,3这四组,我们可以发现,这四组,0~10都可以由这4个数通过某种方式组合得到,所以10次现在就等价于1,2,4,3次,这样就把原本需要拆分10次的问题变成了拆分4次,降低了时间消耗。
#include<iostream>
#include<vector>
using namespace std;
int main()
{
struct Good
{
int w,v;
};
vector<Good> Goods;
int N,V;
cin>>N>>V;
int dp[V+1];
for(int i=0;i<V+1;i++)
dp[i]=0;
for(int i=0;i<N;i++)
{
int w,v,s;
cin>>w>>v>>s;
for(int k=1;k<=s;k*=2)
{
s-=k;
Goods.push_back({w*k,v*k});
}
if(s>0)
Goods.push_back({w*s,v*s});
}
for(auto i:Goods)
{
for(int j=V;j>=i.w;j--)
{
dp[j]=max(dp[j],dp[j-i.w]+i.v);
}
}
cout<<dp[V];
return 0;
}
优化解法2(单调队列解法) ///先放一放,去看看滑动窗口。。。
- 混合背包问题
第一类物品只能用1次
第二类物品可以用无限次
第三类物品可以用多次
混合背包问题其实就是01背包、完全背包、多重背包的组合题,只需要根据条件带入不同的情况就行了
时间复杂度较高的写法(主要是因为多重背包问题造成的,可以使用二进制优化)
#include<iostream>
using namespace std;
int main()
{
int N,V;
cin>>N>>V;
int dp[V+1];
/*struct Thing
{
int kind;
int w;
int V;
};*/
for(int i=0;i<V+1;i++)
dp[i]=0;
for(int i=0;i<N;i++)
{
int w,v,s;
cin>>w>>v>>s;
if(s==-1)
{
for(int j=V;j>=w;j--)
dp[j]=max(dp[j],dp[j-w]+v);
}
else if(s==0)
{
for(int j=w;j<=V;j++)
{
dp[j]=max(dp[j],dp[j-w]+v);
}
}
else
for(int j=V;j>=0;j--)
{
for(int k=0;k*w<=j&&k<=s;k++)
dp[j]=max(dp[j],dp[j-k*w]+k*v);
}
}
cout<<dp[V];
return 0;
}
二进制优化(把所有的数据放一个结构体里)
#include<iostream>
#include<vector>
using namespace std;
int main()
{
int N,V;
cin>>N>>V;
int dp[V+1];
struct Thing
{
int w;
int v;
int kind;
};
vector<Thing>Things;
for(int i=0;i<V+1;i++)
dp[i]=0;
for(int i=0;i<N;i++)
{
int w,v,s;
cin>>w>>v>>s;
if(s==-1)
{
Things.push_back({w,v,s});
}
else if(s==0)
{
Things.push_back({w,v,s});
}
else
{
for(int k=1;k<=s;k*=2)
{
s-=k;
Things.push_back({k*w,k*v,-1});
}
if(s>0)
Things.push_back({s*w,s*v,-1});
}
}
for(auto i:Things)
{
if(i.kind==-1)
{
for(int j=V;j>=i.w;j--)
dp[j]=max(dp[j],dp[j-i.w]+i.v);
}
else
{
for(int j=i.w;j<=V;j++)
{
dp[j]=max(dp[j],dp[j-i.w]+i.v);
}
}
}
cout<<dp[V];
return 0;
}
- 二维背包问题
一个物体不仅有体积,还有重量
用一个二维数组保存状态dp[j][k]表示j体积,k重量下的最大值
#include<iostream>
using namespace std;
int main()
{
int N,V,M;
cin>>N>>V>>M;
int dp[V+1][M+1];
for(int i=0;i<V+1;i++)
{
for(int j=0;j<M+1;j++)
dp[i][j]=0;
}
for(int i=0;i<N;i++)
{
int w,m,v; ///体积、重量、价值
cin>>w>>m>>v;
for(int j=V;j>=w;j--)
{
for(int k=M;k>=m;k--)
dp[j][k]=max(dp[j][k],dp[j-w][k-m]+v);
}
}
cout<<dp[V][M];
return 0;
}
- 分组背包问题
分组背包问题是把物体分成若干组,每一组里只能选一个物体
比如当前循环到了第i组,需要从第i组中挑出一个物体
for(int a=0;a<第i组的物体个数;a++)
{
for(j=V;j>a这个物体的体积;j++) 这里能体现出同一组物体之间的互斥关系
dp[j]=max{dp[j],dp[j-a的体积]+a的价值
}
#include<iostream>
using namespace std;
int main()
{
int N,V;
cin>>N>>V;
int dp[V+1];
for(int i=0;i<V+1;i++)
dp[i]=0;
int weight[101]{0};
int value[101]{0};
for(int i=0;i<N;i++)
{
int a; ///某组有几个物品
cin>>a;
for(int j=0;j<a;j++)
{
cin>>weight[j]>>value[j];
}
for(int k=V;k>=0;k--)
{
for(int r=0;r<a;r++)
{
if(weight[r]<=k)
dp[k]=max(dp[k],dp[k-weight[r]]+value[r]);
}
}
}
cout<<dp[V];
return 0;
}
- 背包问题求方案数
最优选择一共有多少种不同的方案
#include<iostream>
using namespace std;
int main()
{
int mod=100000007;
int N,V;
cin>>N>>V;
int f[V+1];
/*****************************************************************************
注意这里与01背包的区别,01背包的f数组表示体积小于等于j时的最大价值,但是j并不一定全用了,比如体积是10的时候最大价值是5,但是可能在体积是9的时候最大价值就已经是5了,体积是10的时候的最大价值
其实和体积为9的时候是一样的。那么,如何让数组f[j]表示的是正好体积是j的时候的方案数呢?在初始化f[j]
数组的时候,如果f[0]等于0,其他的都初始化为负无穷,这样所有的状态都是从f[0]转换过来的,这样就可以
了。
如果像01背包那样,需要把f[0]数组全部初始化为0
************************************************************************************/
int g[V+1]; ///开一个g数组,表示最大价值正好是j时,总的方案数是多少
for(int i=0;i<V+1;i++)
{
f[i]=INT32_MIN;
g[i]=0;
}
g[0]=1;
for(int i=0;i<N;i++)
{
int w,v;
cin>>w>>v;
for(int j=V;j>=w;j--)
{
int t=max(f[j],f[j-w]+v);
int s=0;
if(t==f[j]) s=s+g[j];
if(t==f[j-w]+v) s=s+g[j-w]; 看t是从哪个状态转移过来的
if(s>=mod) s-=mod;
f[j]=t;
g[j]=s;
}
}
int maxv=0;
for(int i=0;i<=V;i++)
{
maxv=max(maxv,f[i]); ///找出最优的方案
}
int res=0;
for(int i=0;i<=V;i++)
{
if(f[i]==maxv)
{
res+=g[i]; 统计最优方案的方案种数
if(res>=mod)res-=mod;
}
}
cout<<res;
return 0;
}
- 求背包问题的一个具体方案(输出字典排序最小的一个)
#include <iostream>
using namespace std;
int main() {
int N, V;
cin >> N >> V;
int w[N + 1];
w[0] = 0;
int v[N + 1];
v[0] = 0;
int dp[N + 2][V + 2];
for (int i = 0; i < N + 2; i++) {
for (int j = 0; j < V + 2; j++)
dp[i][j] = 0;
}
for (int i = 1; i < N + 1; i++) {
cin >> w[i] >> v[i];
}
从第N个物品开始取,直到取到第一个物体,为什么这么取呢,因为我们想要字典排序最小的输出,所以比
如我们需要先确定第一个物体要不要选,如果要选,把他输出出来,而不是先找第N个物体要不要选。
for (int i = N; i >=1; i--) {
for (int j = 0; j <= V; j++) {
dp[i][j] = dp[i+1][j];
if (j >= w[i])
dp[i][j] = max(dp[i][j], dp[i+1][j - w[i]] + v[i]);
}
}
int vol=V;
for(int i=1;i<=N;i++)
{
if(vol>=w[i]&&dp[i][vol]==dp[i+1][vol-w[i]]+v[i])
{
cout<<i<<" ";
vol-=w[i];
}
}
return 0;
}