一、背包问题全集
1、01背包
每个物品只有一个
代码有注释行,放上普通版与滚动数组优化版
普通版:
01背包问题
#define ll long long
using namespace std;
const int maxn=1e3+7;
int v[maxn];
int w[maxn];
int dp[maxn][maxn];//选到第几件物品,当前背包内装的物品体积总和,当前价值为
int main(){
int N,V;
read(N);//快读,代码省略了
read(V);
for(int i=1;i<=N;i++){
read(v[i]);
read(w[i]);
}
dp[0][0]=0;
for(int i=1;i<=N;i++){
for(int j=0;j<=V;j++){
if(j<v[i]){
dp[i][j]=dp[i-1][j];
}
else{
dp[i][j]=max(dp[i-1][j-v[i]]+w[i],dp[i-1][j]);
}
//cout<<dp[i][j]<<' ';
}
//cout<<endl;
}
int ans=0;
for(int i=0;i<=V;i++){
ans=max(ans,dp[N][i]);
}
cout<<ans<<endl;
}
滚动数组版:
#define ll long long
using namespace std;
const int maxn=1e3+7;
int v[maxn];
int w[maxn];
int last[maxn];//当前背包内装的物品总体积为,当前价值为
int now[maxn];
int main(){
int N,V;
read(N);
read(V);
for(int i=1;i<=N;i++){
read(v[i]);
read(w[i]);
}
last[0]=0;
for(int i=1;i<=N;i++){
for(int j=0;j<=V;j++){
if(j+v[i]>V){
now[j]=last[j];
}
else{
now[j]=max(last[j+v[i]]+w[i],last[j]);
}
last[j]=now[j];
//cout<<now[j]<<' ';
}
//cout<<endl;
}
int ans=0;
for(int i=0;i<=V;i++){
ans=max(now[i],ans);
}
cout<<ans<<endl;
}
2、完全背包
每个物品有无限个
一般思路:(在我没学之前想到的,n^3写法)
因为每一种物品都有无限个,那么选到每种物品,可以有多种状态转移的办法。
可以考虑这样的状态转移:
for(int i=1;i<=N;i++){
for(int j=0;j<=V;j++){
//枚举第i个物品选几个
for(int k=0;k*v[i]<=j;k++){
dp[i][j]=max(dp[i-1][j-k*v[i]]+k*w[i],dp[i][j]);
}
}
}
但是由于复杂度过大,会TLE。
于是分析状态转移过程,上段代码的
dp[i][j]=max(dp[i-1][j],dp[i-1][j-v[i]]+w[i],dp[i-1][j-2*v[i]+2*v[i] ... );
dp[i][j-v]=max(. dp[i-1][j-v[i],dp[i-1][j-2*v[i]]+w[i] ... );
可知,dp[i][j]=max(dp[i-1][j],dp[i-1][j-v]+w[i])
这样以来,复杂度被压缩到n^2
using namespace std;
const int maxn=1e3+7;
int v[maxn];
int w[maxn];
int dp[maxn][maxn];//选到第几类物品,当前背包内已有的物品总体积,当前价值为
int main(){
int N,V;
read(N);
read(V);
for(int i=1;i<=N;i++){
read(v[i]);
read(w[i]);
}
for(int i=1;i<=N;i++){
//枚举第i个物品选几个
for(int j=0;j<=V;j++){
dp[i][j]=dp[i-1][j];
if(j>=v[i]){
dp[i][j]=max(dp[i][j],dp[i][j-v[i]]+w[i]);
}
}
}
int ans=0;
for(int i=0;i<=V;i++){
ans=max(dp[N][i],ans);
}
cout<<ans<<endl;
}
这个也可以优化成滚动数组版。
3、多重背包
每个物品有有给定数目个
这类问题,一般思路可以想出一个n^3的写法,但是这种显然会在数据量稍稍上去一点就TLE。
这个时候可以用到一种特殊的优化方式——二进制优化。
设每种物品有S个。
假如S=100个,考虑将这个物品打包成1,2,4,…,32以及(100-63)个一组,每个包裹最多被选择一次。(2n+1-1 <= S)
运用数学归纳法可以证明存在一种选法,能凑出0~100中任意一个数。
所以,可以考虑将每一种物品打包成1,…,
2n ,
s-((2n+1)-1),转化为01背包问题。
多重背包问题 II
代码实现:
using namespace std;
const int maxn=2e4+5000;
int dp[maxn];
int v[maxn];
int w[maxn];
int s[maxn];
int nv[maxn];
int nw[maxn];
int main(){
int N,V;
cin>>N>>V;
int cnt=0;
for(int i=1;i<=N;i++){
cin>>v[i]>>w[i]>>s[i];
int k=1;
while(k<=s[i]){
cnt++;
nv[cnt]=v[i]*k;
nw[cnt]=w[i]*k;
s[i]-=k;
cout<<k<<endl;
k*=2;
}
if(s[i]>0){
cout<<s[i]<<endl;
cnt++;
nv[cnt]=v[i]*s[i];
nw[cnt]=w[i]*s[i];
}
}
//01背包
for(int i=1;i<=cnt;i++){
for(int j=V;j>=nv[i];j--){
dp[j]=max(dp[j],dp[j-nv[i]]+nw[i]);
}
}
cout<<dp[V]<<endl;
}
其实这个还可以继续优化,下面引入单调队列优化:
最终复杂度降低至O(NV)!
详细见我的另一篇博客~
单调队列及其应用
4、分组背包
每个小组内物品相互冲突,最多只能选1个(基础版),求在物品体积不超过背包容量能获得的最大价值。
分组背包问题
const int maxn=110;
int dp[maxn][maxn];
int main(){
int N,V;
cin>>N>>V;
for(int i=1;i<=N;i++){
int s;
cin>>s;
for(int j=1;j<=s;j++){
int v,w;
cin>>v>>w;
//每组选一个,看状态转移
for(int k=0;k<=V;k++){
dp[i][k]=max(dp[i-1][k],dp[i][k]);
if(k>=v)
dp[i][k]=max(dp[i][k],dp[i-1][k-v]+w);
}
}
}
cout<<dp[N][V]<<endl;
}
5、背包问题方案相关
求方案数:
背包问题求方案数
//背包问题求方案数
const int maxn=1e3+7;
const int mod=1e9+7;
int dp[maxn][maxn];
ll cnt[maxn][maxn];
int v[maxn],w[maxn];
int main(){
int N,V;
cin>>N>>V;
for(int i=1;i<=N;i++){
cin>>v[i]>>w[i];
}
for(int i=0;i<=V;i++){
cnt[0][i]=1;
}
for(int i=1;i<=N;i++){
for(int j=0;j<=V;j++){
dp[i][j]=dp[i-1][j];
cnt[i][j]=cnt[i-1][j];
if(j>=v[i]&&dp[i][j]<dp[i-1][j-v[i]]+w[i]){
//需要更新
dp[i][j]=dp[i-1][j-v[i]]+w[i];
cnt[i][j]=cnt[i-1][j-v[i]];
}
else if(j>=v[i]&&dp[i][j]==dp[i-1][j-v[i]]+w[i]){
cnt[i][j]=(cnt[i][j]+cnt[i-1][j-v[i]])%mod;
}
}
}
cout<<cnt[N][V]<<endl;
}
求具体方案:
背包问题求具体方案
const int maxn=1e3+7;
int dp[maxn][maxn];//必须记录所有物品的状态,选择方案时才可以判断这个物品是不是方案中的一种
int v[maxn],w[maxn];
int last[maxn];//记录达到该方案的
int main(){
int N,V;
cin>>N>>V;
for(int i=1;i<=N;i++){
cin>>v[i]>>w[i];
}
for(int i=N;i>=1;i--){
for(int j=0;j<=V;j++){
dp[i][j]=dp[i+1][j];
if(j>=v[i]){
dp[i][j]=max(dp[i+1][j-v[i]]+w[i],dp[i][j]);
}
}
}
int vol=V;
for(int i=1;i<=N;i++){
//从小到大枚举,保证字典序最小
//cout<<"Vol="<<vol<<endl;
if(vol-v[i]>=0&&dp[i][vol]==dp[i+1][vol-v[i]]+w[i]){
cout<<i<<' ';
vol-=v[i];
}
}
cout<<endl;
}
二、线性dp的一些优化
1、最长上升子序列(nlgn)优化
考虑另外一种描述状态的方法,dp数组具有单调性,就可以使用二分来优化了。
详见代码:
const int maxn=1e5+7;
int a[maxn];
int dp[maxn];//dp[i]=minn,长度为i的上升子序列的最小结尾
int main(){
//nlogn的最长上升子序列
memset(dp,INF,sizeof dp);
int n;
cin>>n;
for(int i=1;i<=n;i++){
cin>>a[i];
}
int ans=0;
dp[0]=-INF;
for(int i=1;i<=n;i++){
//二分
int l=0,r=i;
int maxx=0;
while(l<=r){
int mid=(l+r)>>1;
if(dp[mid]<a[i]){
maxx=mid;
l=mid+1;
}
else{
r=mid-1;
}
}
if(dp[maxx+1]>a[i]){
dp[maxx+1]=a[i];
}
ans=max(maxx+1,ans);
}
cout<<ans<<endl;
}
三、树形dp
没有上司的舞会
详解在代码中,可以作为树形dp参考模版
#define pb push_back
const int maxn=6e3+7;
vector<int> mp[maxn];
int con[maxn];//欢乐程度
int dp[maxn][2];//记录每个节点在选或者不选的情况下的最大权值(不包含当前节点权值)
int vvis[maxn];//防止多次递归导致tle
void solve(int v){
//v为当前节点,从树根开始
int ans0=0;
for(int i=0;i<mp[v].size();i++){
if(!vvis[mp[v][i]])
solve(mp[v][i]);
vvis[mp[v][i]]=1;
ans0+=max(dp[mp[v][i]][1]+con[mp[v][i]],dp[mp[v][i]][0]);
}
dp[v][0]=ans0;
int ans=0;
for(int i=0;i<mp[v].size();i++){
if(!vvis[mp[v][i]])
solve(mp[v][i]);
vvis[mp[v][i]]=1;
ans+=dp[mp[v][i]][0];
}
dp[v][1]=ans;
}
int vis[maxn];
int main(){
//建图
int n;
cin>>n;
for(int i=1;i<=n;i++){
cin>>con[i];
}
int a,b;
while(~scanf("%d%d",&a,&b)){
if(a==0&&b==0){
break;
}
vis[a]=1;
mp[b].pb(a);//记录相关节点(从上往下)
}
//寻找根节点。
int root=0;
for(int i=1;i<=n;i++){
if(!vis[i]){
root=i;
break;
}
}
//cout<<"root="<<root<<endl;
solve(root);
cout<<max(dp[root][1]+con[root],dp[root][0])<<endl;
}
四、状压dp
蒙德里安的梦想
//状态压缩dp
const int maxn=1e4+7;
ll c[20][maxn];//记录每列的某种情况可行的次数,0为不放,1为放
//基本思路:枚举横着放的小方块的个数
//dp数组表示该列xx行放了一个横着的小方块
bool st[maxn];
int main(){
int n,m;
while(cin>>n>>m){
if(n==0&&m==0)
break;
memset(c,0,sizeof c);
int up=1<<n;
//st数组必须预处理,否则会t
for(int i=0;i<=up-1;i++){
int t=i;
int cnt=0;
st[i]=1;
for(int j=1;j<=n;j++){
if((t&1)==0){
cnt++;
}
else{
if(cnt%2!=0){
st[i]=0;
}
cnt=0;
}
t>>=1;
}
if(cnt%2!=0){
st[i]=0;
}
}
c[0][0]=1;
for(int i=1;i<=m;i++){
for(int j=0;j<=up-1;j++){
for(int k=0;k<=up-1;k++){
//1不能左右相邻
//同一列连续的0都为偶数
if((j&k)==0&&st[j|k]==1){
c[i][j]+=c[i-1][k];
}
}
}
}
cout<<c[m][0]<<endl;//答案就是最后一列什么都不放的方案数
}
}
还有另外一种状压dp,往往应用于一张图上。
最短Hamilton路径
//求最小Hamilton路径
const int maxn=(1<<20)+7;
int mp[25][25];
int dp[maxn][25];
int main(){
int n;
cin>>n;
for(int i=0;i<=n-1;i++){
for(int j=0;j<=n-1;j++){
cin>>mp[i][j];
}
}
memset(dp,INF,sizeof dp);
dp[0][0]=0;
for(int i=0;i<=((1<<n)-1);i++){
//枚举状态
//枚举这个状态可以由哪些状态转移过来
for(int j=0;j<=n-1;j++){
//到达了哪些点
if((i>>j)&1){
//这个点到达了
int k=i-(1<<j);
for(int x=0;x<=n-1;x++){
if(dp[k][x]!=INF)
dp[i][j]=min(dp[i][j],dp[k][x]+mp[x][j]);
}
}
}
}
int fin=0;
for(int i=0;i<=n-1;i++){
fin+=(1<<i);
}
cout<<dp[fin][n-1]<<endl;
}
五、区间dp
codefoeces D. Flood Fill
const int maxn=5e3+7;
int dp[maxn][maxn][2];//dp[l][r][0/1]=达到该状态的最小操作数,l,r分别为左右区间,0/1表示当前区间颜色与左边界/右边界相同
int a[maxn];
int main(){
int n;
cin>>n;
int x;
int len=0;
for(int i=1;i<=n;i++){
cin>>x;
if(x!=a[len]&&len>0){
a[++len]=x;
}
if(len==0){
a[++len]=x;//把同色的区间直接合并
}
}
//跑一遍区间dp
//初始化
memset(dp,INF,sizeof dp);
for(int i=1;i<=len;i++){
dp[i][i][1]=0;
dp[i][i][0]=0;
}
//cout<<"len="<<len<<endl;
for(int i=2;i<=len;i++){
//枚举区间长度
//cout<<"i="<<i<<endl;
for(int j=1;j<=len-i+1;j++){
int l=j,r=j+i-1;
//枚举区间起点
//那么区间终点已知
//往左边转移得到当前区间
//只可能从dp[l+1][r][0/1]得到
//cout<<"j="<<j<<endl;
if(a[l]==a[r]){
dp[l][r][0]=min(dp[l][r][0],dp[l+1][r][1]);
}
else{
dp[l][r][0]=min(dp[l][r][0],dp[l+1][r][1]+1);
}
dp[l][r][0]=min(dp[l][r][0],dp[l+1][r][0]+1);
//cout<<"dp="<<dp[j][j+i-1][0]<<endl;
//往右边转移得到当前区间
//只可能从dp[l][r-1][0/1]的搭配
if(a[l]==a[r]){
dp[l][r][1]=min(dp[l][r][1],dp[l][r-1][0]);
}
else{
dp[l][r][1]=min(dp[l][r][1],dp[l][r-1][0]+1);
}
dp[l][r][1]=min(dp[l][r][1],dp[l][r-1][1]+1);
}
//cout<<endl;
}
cout<<min(dp[1][len][1],dp[1][len][0])<<endl;
}
六、数位dp
P2657 [SCOI2009] windy 数
//可以通过预处理dp数组,然后再运用数位dp的思想去做
const int maxn=12;
int dp[maxn][maxn];
void init(){
for(int i=0;i<=9;i++) dp[1][i]=1;
for(int i=2;i<maxn;i++){
for(int j=0;j<=9;j++){
for(int k=0;k<=9;k++){
if(abs(k-j)>=2)
dp[i][j]+=dp[i-1][k];
}
}
}
}
int DP(int n){
if(!n) return 0;
vector<int>dig;
while(n){
dig.pb(n%10);
n/=10;
}
int res=0;
int last=-2;
for(int i=(int)dig.size()-1;i>=0;i--){
int x=dig[i];
for(int j=(i==(int)dig.size()-1);j<x;j++){
if(abs(j-last)>=2){
res+=dp[i+1][j];
}
}
if(abs(last-x)>=2) last=x;
else break;
if(!i) res++;
}
//单独处理前导0的情况
for(int i=1;i<dig.size();i++){
for(int k=1;k<=9;k++){
res+=dp[i][k];
}
}
return res;
}
int main(){
init();
int a,b;
cin>>a>>b;
cout<<DP(b)-DP(a-1)<<endl;
}