【AcWing】算法基础课-动态规划

目录

1、闫式DP分析法

2、背包问题

2.1 01背包问题

朴素版本

优化版本

2.2 完全背包问题

朴素版本

优化版本

2.3 多重背包问题

朴素版本

二进制优化

2.4 分组背包问题

3、线性DP

3.1 数字三角形

3.2 最长上升子序列

3.3 最长公共子序列

4、区间DP

5、数位统计DP

6、状态压缩DP

6.1 蒙德里安的梦想

朴素写法

去除无效状态的优化写法


1、闫式DP分析法

闫式DP分析法是从集合的角度分析DP问题。DP问题实际上就是求有限集中的最值、数量、是否存在等问题,而有限集中的元素通常是非常多的,此时就可以使用DP来进行优化。

DP问题的分析分为两个阶段。

状态表示

第一个阶段是状态表示,这是一个化零为整的过程,每次处理问题时,不是一个元素一个元素处理,而是每次都枚举一个子集(一类东西)。就是把一些有相似点的元素,化称一个子集,然后用某一状态表示。

状态表示要从两个角度分析:集合和属性
首先,要搞清楚状态表示的f[i]表示的集合是什么。对于集合的描述通常是所有满足...条件/方案的集合。正因为f[i]表示的是集合,是一类东西,不是一个东西,所以可以优化时间复杂度。其次,f[i]表示的是一个集合,但f[i]是一个数。存的这个数与集合的关系称为属性。属性一般是最大值/最小值/数量。

状态计算

第二阶段是状态计算,这是一个化整为零的过程。如何求出f[i]?需要将f[i]表示的集合划分成若干子集,每个子集单独去求。划分子集时每个子集要求做到不重、不漏。当然,不漏是一定要有的,但是不重就不一定,如求数量时要不重,而求最值时可以重复。求f[i]时,就分别求每一个子集的f[i],最后综合每一个子集的f[i],就是结果。划分集合的依据时:寻找最后一个不同点。

2、背包问题

2.1 01背包问题

2. 01背包问题 - AcWing题库

01背包问题就是每件物品只能选0次或1次

朴素版本


01背包问题是一个有限集中求最值的问题,因为物品数量、背包容量都是有限的,这也就限制了方案数是有限的,方案数是2^N个。问题就是从2^N个方案中选择一个符合题意的解法。若是暴力解法,就是枚举所有方案,时间复杂度是O(2^N)。而使用DP就可以优化时间复杂度

状态表示

背包问题本质是一个选择问题,选择问题的状态表示都是很类似的。第一维是只考虑前i个物品,后几维一般是题目的限制。在这道题中状态表示可以使用f(i, j)。f(i, j)表示的集合是所有只考虑前i个物品,且总体积不超过j的选法的集合。f(i, j)的属性是集合中所有方案的价值的最大值。当f(i, j)全算完后,答案就是f(N, V)

状态计算

状态计算时我们需要划分f(i, j)表示的集合。划分的依据是找最后一个不同点。在这道题中,选最后一个物品的方法,有选和不选两种,所以将集合划分为所有不选第i个物品的方案和所有选择第i个物品的方案这两个子集。每个子集要么选了第i个物品,要么没选。所以是不重不漏的。f(i, j)的取值就是取左边和右边值较大的哪一个。
现在来看如何求出左右两个子集的最大值。左边的集合表示的是所有不选第i个物品的方案的集合,很明显就是f(i - 1, j)。右边的集合表示的是所有选择第i个物品的方案的集合,若我们固定选择第i个物品,我们可以将选法分成变与不变两个集合,不变的是第i个物品组成的集合,因为每次都会选择,变的是前i - 1个物品,且总体积不超过j - vi的选法组成的集合,因为这i - 1个物品可以自由选择,即f(i - 1, j - vi)。所以,右边是f(i - 1, j - vi) + wi
f(i, j) = max(f(i - 1, j), f(i - 1, j - vi) + wi)
注意:当j < vi时,右边集合是不存在的

#include<iostream>
#include<algorithm>
using namespace std;

const int N = 1010;
int f[N][N], v[N], w[N], n, m;

int main()
{
    ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    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;
}

代码中i、j的起始和结束看放在集合中是否有意义

优化版本

DP的优化和分析通常是分开的。DP的所有优化,都是对代码做的等价变形(与问题无关,只是对代码的实现进行了变形),只需保证优化完与原先是等价的即可。
这一题我们没办法对时间进行优化,只能对空间进行优化。
我们通过观察上面代码的状态转移方程回发现,我们在计算f[i][j]时,永远只会用到二维数组的i - 1层,所以我们没必要使用一个二维数组来存储,只需使用一个一维数组即可。倘若我们将上面代码的第一维删掉,我们可以得到:

#include<iostream>
#include<algorithm>
using namespace std;

const int N = 1010;
int f[N], v[N], w[N], n, m;

int main()
{
    ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    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[j] = f[j]; // f[i][j] = f[i - 1][j];
                if(j >= v[i]) 
                    f[j] = max(f[j], f[j - v[i]] + w[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[j] = f[j]与f[i][j] = f[i - 1][j]是否等价?
是等价的,在f[j] = f[j]中,我们算f[j]时,右边的f[j]是没有更新过的,也就是左边是第i层,右边是第i - 1层的,所以两者是等价的。并且因为f[j] = f[j]是恒成立的,所以这一步可以直接不要。
(2)下面两者是否等价?
对于max中的第一个参数并不需要管,因为只是与第二个参数作比较,看第二个参数即可。若j是从0到m递增遍历的,而j - v[i]一定小于j,此时的f[j - v[i]]是第i层的,与原先的代码不符合,为了让两者等价,需要让j从m到0递减遍历。这样才能保证f[j]是第i层时,f[j - v[i]]是i - 1层的。

#include<iostream>
#include<algorithm>
using namespace std;

const int N = 1010;
int f[N], v[N], w[N], n, m;

int main()
{
    ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    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 >= 0;j --)
            if(j >= v[i]) f[j] = max(f[j], f[j - v[i]] + w[i]);
            
    cout << f[m] << endl;
    return 0;
}

此时可将判断条件放到for中

#include<iostream>
#include<algorithm>
using namespace std;

const int N = 1010;
int f[N], v[N], w[N], n, m;

int main()
{
    ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    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;
}

还可以将v和w这两个数组优化掉,因为每次只会用到v[i]和w[i]

#include<iostream>
#include<algorithm>
using namespace std;

const int N = 1010;
int f[N], v, w, n, m;

int main()
{
    ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    cin >> n >> m;
    
    for(int i = 1;i <= n;i ++)
    {
        cin >> v >> w;
        for(int j = m;j >= v;j --)
            f[j] = max(f[j], f[j - v] + w);
    }
    
    cout << f[m] << endl;
    return 0;
}

注意:只有优化时才需要考虑j从前往后遍历还是从后往前遍历,二维数组时是不需要考虑这个的

2.2 完全背包问题

3. 完全背包问题 - AcWing题库

朴素版本

完全背包问题与01背包问题是类似的,只是01背包问题对于每一个物品只能选0次或1次,而完全背包问题,每一件物品都可以选无限次。

这里的状态表示与01背包问题是完全一样的,就不过多赘述了。不同点在于状态计算。在完全背包问题中,因为每个物品是可以选无限次的,所以不能以第i件物品选或不选来对集合进行划分,而是应该按第i件物品选几次来对区间进行划分。
(1)当第i件物品不选时,此时就是在i - 1件物品中选,并让体积<=j,f(i, j) = f(i - 1, j)
(2)当第i件物品选时,此时会分成第i个物品选几次,1,2,...,k,...,n
假设第i件最多选n次,为了不失一般性,我们看第i件物品选k次的情况,假设第i件物品的体积是v,价值是w。此时可以将选k次这个集合划分成两部分,前半部分是在前i - 1物品中选,并让体积
<=j- k*v,后半部分是k个i。所以f(i,j) = f(i - 1, j - k*v) + k*w
这里的k是从0到n的,所以
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 - k*v) + k*w),...
        f(i - 1, j - n*v) + n*w) )

#include<iostream>
using namespace std;
const int N &
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值