第五篇——动态规划

动态规划

背包问题
背包问题

核心套路

优化一般就是优化状态转移方程

01背包
特点:每个物品仅能使用一次
重要变量&公式解释
f[i][j]:表示所有选法集合中,只从前i个物品中选,并且总体积≤≤j的选法的集合,它的值是这个集合中每一个选法的最大值.
状态转移方程
f[i][j] = max(f[i-1][j], f[i-1][j-v[i]]+w[i])
f[i-1][j]:不选第i个物品的集合中的最大值
f[i-1][j-v[i]]+w[i]:选第i个物品的集合,但是直接求不容易求所在集合的属性,这里迂回打击一下,先将第i个物品的体积减去,求剩下集合中选法的最大值.
问题
集合如何划分

一般原则:不重不漏,不重不一定都要满足(一般求个数时要满足)

如何将现有的集合划分为更小的子集,使得所有子集都可以计算出来.

//无优化版

#include <iostream>

using namespace std;

const int N = 1010;

int n, m;
int v[N], w[N];
int f[N][N];

int main() {
    cin >> n >> m;
    for(int i = 1; i <= n; i++) cin >> v[i] >> w[i];
    for(int i = 1; i <= n; i++) {
        for(int j = 0; j <= m; j++) {
            f[i][j] = f[i-1][j];
            if(j>=v[i]) f[i][j] = max(f[i][j], f[i-1][j-v[i]]+w[i]);
        }
    }

    cout << f[n][m] << endl;

 return 0;    
}

//有优化版
/*

1. f[i] 仅用到了f[i-1]层, 
2. j与j-v[i] 均小于j
   3.若用到上一层的状态时,从大到小枚举, 反之从小到大哦
   */
   #include <iostream>

using namespace std;

const int N = 1010;

int n, m;
int v[N], w[N];
int f[N];

int main() {
    cin >> n >> m;
    for(int i = 1; i <= n; i++) cin >> v[i] >> w[i];
    for(int i = 1; i <= n; i++) 
        for(int j = m; j >= v[i]; j--) 
            f[j] = max(f[j], f[j-v[i]]+w[i]);
    cout << f[m] << endl;
 return 0;    
}
完全背包问题
#include<iostream>
using namespace std;
const int N = 1010;
int f[N][N];
int v[N],w[N];
int main()
{
    int n,m;
    cin>>n>>m;
    for(int i = 1 ; i <= n ;i ++)
    {
        cin>>v[i]>>w[i];
    }

    for(int i = 1 ; i<=n ;i++)
    for(int j = 0 ; j<=m ;j++)
    {
        for(int k = 0 ; k*v[i]<=j ; k++)
            f[i][j] = max(f[i][j],f[i-1][j-k*v[i]]+k*w[i]);
    }
    
    cout<<f[n][m]<<endl;

}

优化思路
我们列举一下更新次序的内部关系:

f[i , j ] = max( f[i-1,j] , f[i-1,j-v]+w ,  f[i-1,j-2*v]+2*w , f[i-1,j-3*v]+3*w , .....)
f[i , j-v]= max(            f[i-1,j-v]   ,  f[i-1,j-2*v] + w , f[i-1,j-2*v]+2*w , .....)

由上两式,可得出如下递推关系:

  f[i][j]=max(f[i,j-v]+w , f[i-1][j]) 

有了上面的关系,那么其实k循环可以不要了,核心代码优化成这样:

for(int i = 1 ; i <=n ;i++)
for(int j = 0 ; j <=m ;j++)
{
    f[i][j] = f[i-1][j];
    if(j-v[i]>=0)
        f[i][j]=max(f[i][j],f[i][j-v[i]]+w[i]);
}
这个代码和01背包的非优化写法很像啊!!!我们对比一下,下面是01背包的核心代码

for(int i = 1 ; i <= n ; i++)
for(int j = 0 ; j <= m ; j ++)
{
    f[i][j] = f[i-1][j];
    if(j-v[i]>=0)
        f[i][j] = max(f[i][j],f[i-1][j-v[i]]+w[i]);
}

两个代码其实只有一句不同(注意下标)

f[i][j] = max(f[i][j],f[i-1][j-v[i]]+w[i]);//01背包

f[i][j] = max(f[i][j],f[i][j-v[i]]+w[i]);//完全背包问题

因为和01背包代码很相像,我们很容易想到进一步优化。核心代码可以改成下面这样

for(int i = 1 ; i<=n ;i++)
    for(int j = v[i] ; j<=m ;j++)//注意了,这里的j是从小到大枚举,和01背包不一样
    {
            f[j] = max(f[j],f[j-v[i]]+w[i]);
    }

综上所述,完全背包的最终写法如下:

#include<iostream>
using namespace std;
const int N = 1010;
int f[N];
int v[N],w[N];
int main()
{
    int n,m;
    cin>>n>>m;
    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<<f[m]<<endl;

}
多重背包问题I
  1. 基本解题思路:
    一个基本思路是,将此问题转换为01背包求解!

比如物品1有3件,每件价值为2,我们不妨创建3个物品1,存在数组v和数组w中

最终更新一下总物品数n即可,然后套用01背包问题进行求解。

  1. 优化时间复杂度到O(mlogn) (多重背包问题II的解法)
    前面提到的多重背包问题的解法,是把多件物品,转换成多个单件物品,添加到v和w数组中。

如10件物品A, 则插入十条记录到v和w数组中,实际上这一过程可以进行优化!

我们可以把十件物品A分成若干份,这若干份必须可以组合成0~10以内的任何一个数字。

做法是:1,2,4,…,2(k-1),10-2k+1

即:10可以分为 1,2,4,3

显然这四个数字,可以组合成0~10以内的任何一个数字,如 8 = 1 + 4 + 3

每一份对应的体积和价值,用系数乘以1件物品的体积和价值。

这么做的好处,可以把时间复杂度从O(nm)降为O(m log n),剩下的继续用01背包问题的解法求解。

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll N=1e5+100;
ll v[N],w[N];
ll f[N];
int main()
{
    ll n,m;
    ll cnt=1;
    cin>>n>>m;
    ll a,b,c;
    for(ll i=1;i<=n;i++)
    {
        cin>>a>>b>>c;
        for(ll j=1;j<=c;j++)
        {
            v[cnt]=a;
            w[cnt]=b;
            cnt++;
        }//将多重背包一个一个拆出来
    }
    for(ll i=1;i<=cnt;i++)
    {
        for(ll j=m;j>=v[i];j--)
        {
            f[j]=max(f[j],f[j-v[i]]+w[i]);
        }
    }//01背包
    cout<<f[m];
    return 0;
}

多重背包问题II

在这里插入图片描述

思路和多重背包问题I一样,但这题的数据范围变成1000了,非优化写法时间复杂度O(n^3) 接近 1e9

必超时。

优化多重背包的优化
首先,我们不能用完全背包的优化思路来优化这个问题,因为每组的物品的个数都不一样,是不能像之前一样推导不优化递推关系的。(详情看下面引用的博文)

引用我之前写的博客:动态规划-完全背包问题

我们列举一下更新次序的内部关系:

f[i , j ] = max( f[i-1,j] , f[i-1,j-v]+w , f[i-1,j-2v]+2w , f[i-1,j-3v]+3w , …)
f[i , j-v]= max( f[i-1,j-v] , f[i-1,j-2v] + w , f[i-1,j-2v]+2*w , …)
由上两式,可得出如下递推关系:
f[i][j]=max(f[i,j-v]+w , f[i-1][j])
接下来,我介绍一个二进制优化的方法,假设有一组商品,一共有11个。我们知道,十进制数字 11 可以这样表示
11=1011(B)=0111(B)+(11−0111(B))=0111(B)+0100(B)
11=1011(B)=0111(B)+(11−0111(B))=0111(B)+0100(B)

正常背包的思路下,我们要求出含这组商品的最优解,我们要枚举12次(枚举装0,1,2…12个)。

现在,如果我们把这11个商品分别打包成含商品个数为1个,2个,4个,4个(分别对应0001,0010,0100,0100)的四个”新的商品 “, 将问题转化为01背包问题,对于每个商品,我们都只枚举一次,那么我们只需要枚举四次 ,就可以找出这含组商品的最优解。 这样就大大减少了枚举次数。

这种优化对于大数尤其明显,例如有1024个商品,在正常情况下要枚举1025次 , 二进制思想下转化成01背包只需要枚举10次。

优化的合理性的证明
先讲结论:上面的1,2,4,4是可以通过组合来表示出0~11中任何一个数的,还是拿11证明一下(举例一下):

首先,11可以这样分成两个二进制数的组合:
11=0111(B)+(11−0111(B))=0111(B)+0100(B)
11=0111(B)+(11−0111(B))=0111(B)+0100(B)

其中0111通过枚举这三个1的取或不取(也就是对0001(B),0010(B),0100(B)的组合),可以表示十进制数0~7( 刚好对应了 1,2,4 可以组合出 0~7 ) , 0~7的枚举再组合上0100(B)( 即 十进制的 4 ) ,可以表示十进制数 0~11。其它情况也可以这样证明。这也是为什么,这个完全背包问题可以等效转化为01背包问题,有木有觉得很奇妙

怎么合理划分一个十进制数?
上面我把11划分为

11=0111(B)+(11−0111(B))=0111(B)+0100(B)
11=0111(B)+(11−0111(B))=0111(B)+0100(B)
是因为 0111(B)刚好是小于11的最大的尾部全为1的二进制 ( 按照上面的证明,这样的划分没毛病 ) , 然后那个尾部全为1的数又可以 分解为 0000…1 , 0000…10 , 0000…100 等等。

对应c++代码:

//设有s个商品,也就是将s划分
for(int k = 1 ; k <= s ;k*=2)
{
s-=k;
goods.push_back({vk,wk});
}
if(s>0)
goods.push_back({vs,ws});
终究AC代码:01优化+二进制优化
ac代码

#include<iostream>
#include<algorithm>
#include<vector>
using namespace std;
const int N = 2010;
int f[N],n,m;
struct good
{
    int w,v;
};

int main()
{
    cin>>n>>m;
    vector<good> Good;
    good tmp;

    //二进制处理
    for(int i = 1 ; i <= n ; i++ )
    {
        int v,w,s;
        cin>>v>>w>>s;
        //坑,k <= s
        for(int k = 1 ; k <= s ; k*=2 )
        {
            s-=k;
            Good.push_back({k*w,k*v});
        }
        if(s>0) Good.push_back({s*w,s*v});
    }
    
    //01背包优化+二进制
    for(auto t : Good)
        for(int j = m ; j >= t.v ; j--)
            f[j] = max(f[j] , f[j-t.v]+t.w ); //这里就是f[j]


    cout<<f[m]<<endl;
    return 0;

}
分组背包问题

在这里插入图片描述

#include<bits/stdc++.h>
using namespace std;

const int N=110;
int f[N][N];  //只从前i组物品中选,当前体积小于等于j的最大值
int v[N][N],w[N][N],s[N];   //v为体积,w为价值,s代表第i组物品的个数
int n,m,k;

int main(){
    cin>>n>>m;
    for(int i=1;i<=n;i++){
        cin>>s[i];
        for(int j=0;j<s[i];j++){
            cin>>v[i][j]>>w[i][j];  //读入
        }
    }

    for(int i=1;i<=n;i++){
        for(int j=0;j<=m;j++){
            f[i][j]=f[i-1][j];  //不选
            for(int k=0;k<s[i];k++){
                if(j>=v[i][k])     f[i][j]=max(f[i][j],f[i-1][j-v[i][k]]+w[i][k]);  
            }
        }
    }
    cout<<f[n][m]<<endl;
}
因为只用到了第i-1列,所以可以仿照01背包的套路逆向枚举体积

#include<bits/stdc++.h>
using namespace std;

const int N=110;
int f[N];
int v[N][N],w[N][N],s[N];
int n,m,k;

int main(){
    cin>>n>>m;
    for(int i=0;i<n;i++){
        cin>>s[i];
        for(int j=0;j<s[i];j++){
            cin>>v[i][j]>>w[i][j];
        }
    }

    for(int i=0;i<n;i++){
        for(int j=m;j>=0;j--){
            for(int k=0;k<s[i];k++){    //for(int k=s[i];k>=1;k--)也可以
                if(j>=v[i][k])     f[j]=max(f[j],f[j-v[i][k]]+w[i][k]);  
            }
        }
    }
    cout<<f[m]<<endl;
}


线性DP
数字三角形
最长上升子序列I
最长上升子序列II
最长公共子序列
最短编辑距离
编辑距离
#include<bits/stdc++.h>
using namespace std;
const int N = 15;
const int M = 1001;
int n,m,f[N][N];
char s[M][N];

int edit_dis(char a[],char b[]) {
    int lena = strlen(a+1);
    int lenb = strlen(b+1);
    for(register int i=1; i<=lena; i++) f[i][0] = i;
    for(register int i=1; i<=lenb; i++) f[0][i] = i;
    for(register int i=1; i<=lena; i++)
        for(register int j=1; j<=lenb; j++) {
            f[i][j] = min(f[i-1][j]+1 , f[i][j-1]+1);
            if(a[i]==b[j]) f[i][j] = min(f[i][j] , f[i-1][j-1]);
            else f[i][j] = min(f[i][j] , f[i-1][j-1]+1);
        }
    return f[lena][lenb];
}

int main(){
    scanf("%d%d",&n,&m);
    for(register int i=0; i<n; i++) scanf("%s",s[i]+1);
    while(m--) {
        char q[N];
        int limit;
        scanf("%s%d",q+1,&limit);

        int ans = 0;
        for(register int i=0; i<n; i++) 
            if(edit_dis(s[i],q)<=limit) ans++; 
        printf("%d\n",ans);
    }
    return 0;
}


区间DP
石子合并
计数类DP
整数划分
数位统计DP
计数问题
状态压缩DP
蒙德里安的梦想
最短Hamilton路径
树形DP
没有上司的舞会
记忆化搜索
滑雪
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值