4.9-4.11 闫氏dp分析法

那啥 写这篇只是想总结一下白天听课的成果。

强推一个up 大雪菜  一开始就是找一个什么算法的时候搜到这个up的视频,后来发现这个up讲东西讲的特别清楚,而且是他是真的牛逼,还有自己的网站 Acwing,里面可以刷题啊之类的,还有很多课很多活动啥啥的 ······多厉害不讲了,回到这里,我听的课是https://www.bilibili.com/video/BV1X741127ZM?t=2703

闫氏DP分析法:

①第一阶段是状态表示,是一个化整为零的过程。就是把具有相似属性的东西合并到一个子集中,用一个状态来表示。我们一般用一个数组f[ ]来表示状态,f[i]呢就表示所有满足某个条件的一个集合。然后我们就要确认这个集合的属性,一般是三种:最大值,最小值或是总和。

②第二阶段是状态计算。就是计算每个状态下的值。我们首先确定了f[i]的所表示的状态,然后把这个状态划分为各个子集来求,这些子集一定要具备“不重复”(一般情况下)的特点,且这些子集所组成的f[i]这个集合也具备“不遗漏”。所以每个状态下的值,就是这些子集的值的和。而这些集合的划分依据:找最后一个不同点。

 

以上似乎比较抽象。

举个栗子。

01背包问题

这道最经典的题目,y总在视频里说这个应该是大家接触最多,代码都能背下来的题目(我= =。。。。

题目是这样↓

有 N件物品和一个容量是 V 的背包。每件物品只能使用一次。

第 i 件物品的体积是 vi,价值是 wi。

求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出最大价值。

输入格式

第一行两个整数,N,VN,V,用空格隔开,分别表示物品数量和背包容积。

接下来有 N 行,每行两个整数 vi,wi,用空格隔开,分别表示第 ii 件物品的体积和价值。

输出格式

输出一个整数,表示最大价值。

数据范围

0<N,V≤1000
0<vi,wi≤1000

输入样例:

4 5
1 2
2 4
3 4
4 5

输出样例:

8

①第一阶段:状态表示,就是用一个集合来表示满足某个条件的所有情况。即f[i][j]表示可以装前i件物品,且背包容量为j的情况下的最大值。属性就是最大值

②第二阶段:状态计算,子集的划分依据是最后一个不同点嘛,也就是装不装第i件物品。所以可以分为两个部分(子集):装第i件物品和不装第i件物品。不装第i件物品的情况下最大值就等于f[i-1][j],而装第i件物品的情况下,可以将这个背包分成两个部分,第一部分是第i件物品,这部分是固定的,因为已经确定装第i件物品了,占用的背包体积是v[i],所以背包体积还剩j-v[i],第二部分是前i-1件物品在剩余j-v[i]的体积下的最大值,用f[i-1][j-v[i]]来表示。所以我们所要确定的f[i][j]就是两个子集(即装第i件物品和不装第i件物品)的最大值=>max(f[i-1][j],f[i-1][j-v[i]]+wi).

朴素版代码如下

#include<iostream>
#include<algorithm>
using namespace std;
const int M=1010;
int v[M],w[M];    //v代表各物品重量,w代表各物品价值
int main()
{
    int n,maxv;     //n代表物品数,maxv代表背包最大容量
    cin>>n>>maxv;

    int f[M][M]={0};  //f[i][j]表示到可以装入第i件物品且背包容量最大为j时背包价值的最大值

    for(int i=1;i<=n;i++)
        cin>>v[i]>>w[i];

    for(int i=1;i<=n;i++)
        for(int j=0;j<=maxv;j++)
        {
            f[i][j]=f[i-1][j];         //先读入不装入物品i时的最大价值
            if(j>=v[i])                //如果背包能装的下物品i,再求两个子集的最大价值
                f[i][j]=max(f[i][j],f[i-1][j-v[i]]+w[i]);
        }

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

    return 0;
}

额。。以上是朴素版代码,当然还有进化版。

这个优化的过程只针对代码,与题目本身是没有任何关联的。

我们尝试着用一维数组去代表状态,也就是f[j]来代表背包重量为j时,背包的最大价值。

整个代码其实前面一部分是不用变的,我们试着在循环里去掉i那个数组,去掉以后代码如下(注释是朴素版代码)

    for(int i=1;i<=n;i++)
        for(int j=0;j<=maxv;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]);
        }

我们要保证与原来的朴素版代码相同,第一句修改的原意是使这一层的f[j]等于上一层的f[j],而修改过以后,上一层的f[j]已经计算过了,f[j]=f[j]这句话的意思是不变的,但是这句也是无用功,所以是可以去掉的。

第二句修改的代码愿意是在第i层循环中,取一个现存在数组里的值与上一层循环中背包容量为j-vi的最大值,而当我们去掉第一维的数组以后,发现我们取的是现存在数组里的值和这一层循环里背包容量为j-vi的最大值也就是max(f[i][j],f[i][j-v[i]]+w[i]),这显然与原意不符合,因为我们在这一层里已经计算过了f[i][j-v[i]],所以我们要修改循环,把j从大到小来循环,当我们做循环时,f[i][j-v[i]]的值是还没计算过的,所以f[j-v[i]]里面存放的自然是上一层的值,就与原来朴素版的代码语义相同了。还有一个可以修改的地方就是,因为我们把第一句无用功删掉以后,就只剩下if判断后面的东西,所以就可以直接把if判断写进for循环的判断里。优化后的代码如下。

#include<iostream>
#include<algorithm>
using namespace std;
const int M=1010;
int v[M],w[M];
int main()
{
    int n,maxv;
    cin>>n>>maxv;

    int f[M]={0};

    for(int i=1;i<=n;i++)
        cin>>v[i]>>w[i];

    for(int i=1;i<=n;i++)
        for(int j=maxv;j>=v[i];j--)
                f[j]=max(f[j],f[j-v[i]]+w[i]);

    cout<<f[maxv]<<endl;

    return 0;
}

 

接下来是完全背包问题

与01背包问题的差别就在于01背包问题是一个物品只能装一次,也就是要么装,要么不装(咱能别装了嘛hhhhh),而完全背包问题是一个物品能装无限次。

有 N 种物品和一个容量是 V 的背包,每种物品都有无限件可用。

第 i 种物品的体积是vi,价值是 wi。

求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出最大价值。

输入格式

第一行两个整数,N,VN,V,用空格隔开,分别表示物品种数和背包容积。

接下来有 N 行,每行两个整数 vi,wi,用空格隔开,分别表示第 i 种物品的体积和价值。

输出格式

输出一个整数,表示最大价值。

数据范围

0<N,V≤1000
0<vi,wi≤1000

输入样例

4 5
1 2
2 4
3 4
4 5

输出样例:

10

①第一阶段:状态表示,依然是f[i][j],代表选到第i件物品,且背包容量最大为j时,背包的最大价值。属性还是最大值。

②第二阶段:状态计算。我们先分成两个子集,一个是不选第i件物品,即f[i][j]=f[i-1][j],第二个集合是选第i件物品,但是因为是完全背包问题,所以第二个集合又可以分成无数个小集合,即选一件第i个物品,选两件第i个物品,选第3件···直到背包装不下为止。所以f[i][j]就是这些集合的最大值,即max(f[i][j-v[i]]+w[i],f[i][j-2*v[i]]+2*w[i],······),将选第i件物品和不选第i件物品再取个最大值,就是f[i][j]=max(f[i-1][j],f[i][j-v[i]]+w[i],f[i][j-2*v[i]]+2*w[i],······).这样一算的话就要进行三重循环了,时间复杂度会非常高。接下来我们就进行一个推导,f[i][j-v[i]](就是把上式的j替换成j-v[i])=max(f[i-1][j-v[i]],f[i][j-2*v[i]]+w[i],f[i][j-3*v[i]]+2*w[i],······)仔细跟f[i][j]一比较,发现f[i][j-v[i]]跟f[i][j]后半部分基本一样,就是少了个w[i],所以f[i][j-v[i]]+w[i]就等于f[i][j]后半部分,将后半部分替换掉以后,f[i][j]=max(f[i-1][j],f[i][j-v[i]]+w[i]),就只需要两重循环了,代码如下。

#include<iostream>
#include<algorithm>
using namespace std;
const int M=1010;
int v[M],w[M],f[M][M];
int main()
{
    int n,maxv;
    cin>>n>>maxv;
    for(int i=1;i<=n;i++)
        cin>>v[i]>>w[i];

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

    cout<<f[n][maxv]<<endl;
}

至于优化的话,其实对比一下01背包问题

//01背包问题的状态f[i][j]=max(f[i-1][j],f[i-1][j-v[i]]+w[i])
//完全背包问题的状态f[i][j]=max(f[i-1][j],f[i][j-v[i]]+w[i])

差只差在后半部分是i-1重循环还是i重循环,根据01背包简化的分析的话,完全背包的简化其实基本不用改变,就连循环都还是从小到大,所以优化后的代码是↓

#include<iostream>
#include<algorithm>
using namespace std;
const int M=1010;
int v[M],w[M],f[M];
int main()
{
    int n,maxv;
    cin>>n>>maxv;
    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<=maxv;j++)
                f[j]=max(f[j],f[j-v[i]]+w[i]);

    cout<<f[maxv]<<endl;
}

 

石子合并问题

设有N堆石子排成一排,其编号为1,2,3,…,N。

每堆石子有一定的质量,可以用一个整数来描述,现在要将这N堆石子合并成为一堆。

每次只能合并相邻的两堆,合并的代价为这两堆石子的质量之和,合并后与这两堆石子相邻的石子将和新堆相邻,合并时由于选择的顺序不同,合并的总代价也不相同。

例如有4堆石子分别为 1 3 5 2, 我们可以先合并1、2堆,代价为4,得到4 5 2, 又合并 1,2堆,代价为9,得到9 2 ,再合并得到11,总代价为4+9+11=24;

如果第二步是先合并2,3堆,则代价为7,得到4 7,最后一次合并代价为11,总代价为4+7+11=22。

问题是:找出一种合理的方法,使总的代价最小,输出最小代价。

输入格式

第一行一个数N表示石子的堆数N。

第二行N个数,表示每堆石子的质量(均不超过1000)。

输出格式

输出一个整数,表示最小代价。

数据范围

1≤N≤300

输入样例:

4
1 3 5 2

输出样例:

22

 

①首先是进行状态表示,至于这个状态表示吧,其实我觉得有时候挺难想到的,所以要多刷题,积累的题目多了,就反应的快点。这题是用f[i][j]表示,从第i堆石子开始到第j堆石子为止,合并起来的最小代价。属性是最小值。

②第二阶段是状态计算。找最后一个不同点,也就是最后一次合并,也就是可以划分为j-i-1个子集。第一个子集是(i)(i+1,i+2,······,j-1,j)这两堆进行合并,即f[i][j]=f[i][i]+f[i+1][j]+i到j的石子的重量和。第二个子集是(i,i+1)(i+2,······,j-1,j)这两堆进行合并,即f[i][j]=f[i][i+1]+f[i+2][j]+i到j的石子的重量和。第三,四到第j个子集以此类推。所以,f[i][j]=min(f[i][i]+f[i+1][j],f[i][i+1]+f[i+2][j],······)+i到j的石子重量和。至于i到j的石子重量和,我们可以在输入的时候就用一个数组前缀和来表示,然后i到j的石子重量和就算s[j]-s[i-1]

(y总说一般这种有区间的dp问题都会先循环区间的长度。)

#include<iostream>
#include<algorithm>
using namespace std;
const int M=305;
int f[M][M],s[M],n;    //s这个数组表示前缀和

int main()
{
    cin>>n;
    for(int i = 1;i <= n; i++ )
    {
        int k;
        cin>>k;
        s[i]=s[i-1]+k;
    }

    for(int len = 2;len <= n; len++)
        for(int i = 1; i <= n-len+1; i++ )
        {
            int j = i+len-1;
            f[i][j] = 1e8;                   //先初始化为一个很大的数
            for(int k = i; k<j; k++ )
                f[i][j] = min(f[i][j],f[i][k]+f[k+1][j]+s[j]-s[i-1]);
        }
    cout<<f[1][n]<<endl;

    return 0;
}

最长公共子序列问题

给定两个长度分别为N和M的字符串A和B,求既是A的子序列又是B的子序列的字符串长度最长是多少。

输入格式

第一行包含两个整数N和M。

第二行包含一个长度为N的字符串,表示字符串A。

第三行包含一个长度为M的字符串,表示字符串B。

字符串均由小写字母构成。

输出格式

输出一个整数,表示最大长度。

数据范围

1≤N≤1000

输入样例:

4 5
acbd
abedc

输出样例:

3

①状态表示,f[i][j]来表示第一个序列的第一个字符到第i个字符与第二个序列的第一个字符到第j个字符的最长公共子序列的长度,这个公共子序列可以不用连续的。属性是最大值。

②状态计算,f[i][j]可以分成四个子集{包含第一个序列第i个字符和第二个序列第j个字符,不包含第一个序列第i个字符但包含第二个序列第j个字符,包含第一个序列第i个字符但不包含第二个序列第j个字符,不包含第一个序列第i个字符和第二个序列第j个字符}。第一个子集是包含第一个序列第i个字符和第二个序列第j个字符,即第i个字符和第j个字符要相等。先判断是否相等,相等的话f[i][j]=f[i-1][j-1]。第二个子集是不包含第i个字符但包含第j个字符,这个子集跟f[i-1][j]其实是不一样的,f[i-1][j]可能包含第j个字符,也能可能不包含,但是它的范围是比我们的第二个子集大的,(其实求的是第二个子集和第四个子集的和),同理,第三个子集如果用f[i][j-1]表示其实也不是完全合理的,因为范围更大了,但是我们求的是最大值,所以重复是没有问题的,所以后三个子集的最大值我们其实是可以用max(f[i-1][j],f[i][j-1])来表示的!第一个子集的条件是第i个字符和第j个字符相等,第二,三,四个子集的条件是第i个字符和第j个字符不相等,判断完条件以后,我们就可以求了。

代码如下。

#include<iostream>

using namespace std;
const int M = 1010;
int f[M][M];

int main()
{
    char a[M],b[M];
    int n,m;
    cin>>n>>m>>a+1>>b+1;

    for(int i = 1;i <= n; i++ )
        for(int j = 1;j <= m; j++)
        {
            if(a[i] == b[j])
                f[i][j] = f[i-1][j-1]+1;
            else
                f[i][j] = max(f[i][j-1],f[i-1][j]);
        }

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

    return 0;
}

Attention:以上代码啊思想啊全部来自y总(b站:大雪菜)! 我只是做个听完课的总结而已,总结的东西我自己甚至都读的不是特别明白= =纯粹就是再复习一遍,去看y总讲的视频会靠谱很多= =

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值