• 关于DP动规


     

     

    今天学了动规,简单记录一下自己理解了的:(要不俺就忘了)

    首先,啥是DP???

    动态规划,其实就是组合子问题的解来解决整个问题的解,由于每个子问题他只判断一次,所以不会重复计算,那就很牛啊!!!

    专业术语(复制加粘贴):

    1、 阶段:把所给求解问题的过程恰当地分成若干个相互联系的阶段,以便于按一 定的次序去求解,过程不同,阶段数就可能不同.描述阶段的变量称为阶段变量。

    2、状态:某一阶段的出发位置成为状态,通常一个阶段有多个状态。状态通常可以用一个或一组数来描述,称为状态变量。

    3、决策:一个阶段的状态给定以后,从该状态演变到下一阶段某个状态的一种选 择(行动)称为决策。描述决策变量称决策变量

    4、策略和最优策略:所有阶段的决策有序组合构成一个策略。 最优效果的策略叫最优策略。

    5. 状态转移方程:前一阶段的终点就是后一阶段的起点,对前一阶段的状态作出某种决策, 产生后一阶段的状态,这种关系描述了由k阶段到k+1阶段状态的演变规律,称 为状态转移方程。

     

    其实也没什么好说的,直接上题吧!

    T1:从n个数中取出k个数,使得他们的和最大

    解一:sort排序

    这就没啥可讲的了,你就建一个cmp让sort从大到小排序,选出前n大的数相加,就好啦,上代码!

    复制代码
    //和最大问题(sort) 
    #include<iostream>
    #include<algorithm>
    using namespace std;
    int a[1000000];//我不知道数据 
    int sum;
    int cmp(int x,int y)
    {
        return x>y;
    }
    int main()
    {
        int n,k;//n是总个数,k是取出的数的个数 
        cin>>n>>k;
        for(int i=0;i<n;i++)
        {
            cin>>a[i];
        }
        sort(a,a+n,cmp);
        for(int i=0;i<k;i++)
        {
            sum+=a[i];
        } 
        cout<<sum;
        return 0;
    }
    复制代码

    解二:DP

    用动规来解决,首先要想出状态转移方程(好像有点难很难)

    我们首先定义一个二维数组f[i][j],前面的表示第几个数(i),后面表示已经选取了几个数(j)

    那么我们分将其成两种情况:我选或不选当前的数,

    当我选取当前数时,它们的和就是上次的结果加上这个数,上次的结果就是f[i-1][j-1](因为目前的数是i,所以上一个的考虑到的数就是i-1,又因我考虑了这个数,所以上个数就是j-1),当前的数是a[i]。那我们如果加上这个数,那么现在所选数之和就是 f[i-1][j-1]+a[i]

    那么如果不选前面的数时所得和其实就是选取上一个数时的和,即 f[i-1][j]

    所以我们就可以得出其状态转移方程:f[i][j]=max( f[i-1][j-1]+a[i],f[i-1][j])

    其他就好说了,具体如下

    复制代码
     1 #include<iostream>
     2 #define lxl long long
     4 #define db double
     5 using namespace std;
     6 int f[10005][10005];
     8 int a[N];
     9 int n,k; 
    11 int main(){
    12     cin>>n>>k;
    13     for(int i=1;i<=n;i++)
    14      cin>>a[i];
    15     for(int i=1;i<=n;i++){
    16         for(int j=1;j<=k;j++){
    17             f[i][j]=max(f[i-1][j-1]+a[i],f[i-1][j]);
    18         }
    19     }
    20     cout<<f[n][k];
    21     return 0;
    22 }
    复制代码

     

    T2.关于线性DP

    从n个数中找出最长上升子序列(可以不连续),输出其长度

    解法一、dfs搜索(坑)

    复制代码
     1 #include<iostream>
     2 #include<cstdio>
     3 #define ll long long
     4 int n,a[1001],ans=0;
     5 void dfs(int c,int now,int len)
     6 {
     7     if(c==n)
     8     {
     9         if(len>ans) ans=len;
    10         return;
    11     }
    12     for(int i=c;i<=n;i++)
    13     {
    14         if(a[i]>now) dfs(i,a[i],len+1);
    15     }
    16 }
    17 int main()
    18 {
    19     scanf("%d",&n);
    20     for(int i=1;i<=n;i++)
    21     {
    22         scanf("%d",&a[i]);
    23     }
    24     dfs(1,-1,0);
    25     printf("%d",ans);
    26     return 0;
    27 }
    复制代码

    我就提一句,dfs中c代表的是当前状态,即目前处理到的数,now代表前一个采用了的数,ans代表当前状态的上升序列长度,最后比较每一个ans,找出最大的即可

    但是很遗憾,人家太慢了。就过了两个点。。。

    那我还是讲讲其他的方法吧

    解法二、记忆化搜索

    不算很难

    直接上核心代码理解一下吧,毕竟人家不是咱的重点关注对象

    复制代码
    f[1][1]=a[1][1];
    for(int i=2;i<=n;i++){
        for(int j=1;j<=i;j++)
         f[i][j]=max(f[i-1][j-1],f[i-1][j])+a[i][j];
    }
    int ans=0;
    for(int i=1;i<=n;i++)
     ans=max(ans,f[n][i]);
    cout<<ans;
    复制代码

     

    正统解法,解法三:DP

    用倒推正推都可以

    ①正推(其实就是 fk[x] =min{ fk-1[yi] +d [yi,x]}的一个载体)

    复制代码
     1 #include<cstdio>
     2 #define ll long long
     3 using namespace std;
     4 int n,a[1001],f[1001],p[1001];
     5 int main()
     6 {
     7     scanf("%d",&n);
     8     for(int i=1;i<=n;i++)
     9     {
    10         scanf("%d",&a[i]);
    11     }
    12     f[1]=1;
    13     for(int i=2;i<=n;i++)
    14     {
    15         f[i]=0;
    16         for(int j=1;j<i;j++)
    17         {
    18             if(a[j]<a[i]&&f[j]>f[i])
    19             {
    20                 f[i]=f[j];
    21                 p[i]=j;
    22             }
    23         }
    24         f[i]++;
    25     }
    26     int ans=f[1],k=1;
    27     for(int i=2;i<=n;i++)
    28     {
    29         if(f[i]>ans)ans=f[k=i];
    30     }
    31     printf("%d",ans);
    32     return 0;
    33 }
    复制代码

    啥意思呢,是这样的

     首先我们要明确我们定义的三个数组分别代表着啥:

    a数组代表我输入的数组,f[i]数组代表i对应的最长子上升子序列长度,p数组用于表示f[i]的最优值j的位置上,就是它的上一个既满足在它前面,又比他小的数然后我们可以举个例子:

    比如 :                       1         4        3        

    首先 i 等于2,也就是从第2个数 4 开始,我们让f[1]等于1(等会你就知道为啥了),再用一个 j ,他的范围是 [1,i),也就是找 i前面的数,那这里由于j<i,所以 j 就只能等于1,那a[j]就等于1,我们判断一下,如果a[j]<a[i]而且f[j]>f[i],那我们就让i的长度变成j的长度,并让p[i]=j,结束后 i 变成3,j从0到1开始遍历,是0的时候,a[j]代表1,a[i]等于3,由于满足a[j]<a[i]而且f[j](它就是 f[1]==1)>f[i](==0),咱就进行一下操作:让f[i]=f[j]=1,i的前驱(q[i])=j=1;再轮到j=2,由于不满足a[j]<a[i],无法进行以下操作,所以不管他,那么现在我就又有了a[3]的前驱,重复以上操作,我们就得到了最优解,和他们所对应的前驱,自然就可以得到最长上升子序列啦!!!

    为啥要这样??

    原因是我们要找的是最长上升子序列,所以前面的数a[j]必须比后面的数a[i]大(a[j]<a[i]),而且 j 对应的序列长度应该大于 i 对应的,原因是我们想让最长的变成i所对应的长度,所以只有在 j 对应的长度比i对应的长度长的时候我才交换他们的长度,以保证 f[i]对应的是最大值,

    p[i]=j 表示的是 i 的上一个即在它前面有比他小的数是 j(类似于它的前驱),我们把他记录下来,目的是便于最后输出。

    最后让f[i]的长度加1,因为我们这些数每遇到一个就会加1,殊不知其实一开始只有一个数的时候长度为0,所以最后加1才是最终的长度。

     

    倒序也一样:( fk[x ]= min{ fk+1[yi]+d [x,yi]})

    复制代码
     1 //最长上升子序列(逆推) 
     2 #include<iostream>
     3 #include<cmath>
     4 using namespace std;
     5 int a[1001];
     6 int f[1001];//对应的最长上升子序列长度 
     7 int p[1001];
     8 int ans;
     9 //p数组用于记录 f[i]的最优值j的位置 ,就是它的上一个既满足在他前面有比他小的数, 
    10 int main()
    11 {
    12     int n;
    13     cin>>n;
    14     for(int i=1;i<=n;i++) cin>>a[i];
    15     f[n]=1;p[n]=0;
    16     for(int i=n-1;i>0;i--)
    17     {
    18         f[i]=0;
    19         for(int j=i+1;j<=n;j++)
    20         {
    21             if(a[i]<=a[j]&&f[j]>f[i])
    22 //两个条件:1.j对应的数比i大(保证上升)2.j的长度要大于i的长度,要不换他干啥
    23             {
    24                 f[i]=f[j];//让i的长度变成j的长度
    25                 p[i]=j;//记录下来他的上一个即在它前面,又比他小的数,方便最后输出 
    26             } 
    27         }
    28         f[i]++; 
    29     } 
    30     ans=f[1];
    31     int k=1;
    32     for(int i=2;i<=n;i++)
    33     {
    34         if(f[i]>ans) ans=f[k=i];//好高级 
    35     }
    36     cout<<ans<<endl;//这是最长上升子序列长度
    37     while(k>0)
    38     {
    39         cout<<a[k]<<" ";
    40          k=p[k];
    41     } 
    42     return 0;
    43 }
    复制代码

    道理跟正推就是一样的

     

    说了这么多了,总结一下吧

    其实不难发现,我们会把一个大问题分成无数个小问题,但是他有一个前提。就是子问题的局部最优会导致整个问题的全局最优,也就是说无论过去我是咋决策的,现在的最优决策都是由原来的子问题的最优决策构成的,那也就是说一旦我们解决了子问题的最优,那以后的较大规模的问题也就迎刃而解了,而且一个问题的最优解只会受上一个最优子问题的影响,其他的子问题非最优解对现在的问题没有任何影响!

    这就是老师讲的最优化原理无后效性

    那我就思考了一下我刚才提及到的最长上升子序列是怎么体现出这个特点的呢??(仅个人观点)

    仔细想想,其实我一开始就记录了每一个数的前驱,即它的上一个既满足在它前面,又比他小的数。我虽然全都记录下来了,但是我们只将最后我们得出的最长上升子序列的每个元素和其对应的前驱输出,而不是把每个元素对应的前驱全都输出,也就是错误的(不满足条件的)我就不输出了,这就保证了我的没满足全局最优的数并不会影响我们最后满足条件的数列,这就满足了DP的基本特征(条件),即无后效性,所以才可以用DP来做这道题。

    还有就是,做这道题的思路是这样的:

    1、划分阶段(在最长上升子序列一题中就是指的 i 和 j 对应的每一个阶段吧)

    2、确定状态和变量(在这一题中我们运用了三个数组,分别表示  a[]原始数列、f[i] :i对应的最长子序列长度、g[i]: f[i]的最优值j的位置 ,就是它的上一个既满足在他前面有比他小的数)

    3、确定状态转移方程

    4、找出边界条件(这里就是 i 到头就行)

     

    T3、坐标DP

    一般就是个二维数组,,,

    比如这道摘花生的题,说是一片地,一些坐标上有花生,现在你在左上角,只能向右或向下移动,问你一路上能摘多少花生?

    我们肯定要先建一个二位数组记录每个点花生的数量(没有那肯定就是0啊),那我现在就想写出状态转移方程,怎么思考?

    反正我到一个点要不就是从上面下来要不就是从左边往右走,那我们就比较上面的摘花生个数和左面总共摘得的花生个数,取最大值在加上这个点摘得的花生数量就是到这个点后你所最多能摘的花生数,即

    f[i,j]=max{  f[i-1 , j],f[ i , j-1] } + a[i,j]

    这样再用一个递推就能实现了。那我们想,我们所得到的目前阶段的最大值,肯定是不会受非最优子阶段的影响。所以是可以用DP做的。

     

    也整理了最近做过的几道题

     

    T4、

    题目讲的就是在一个数组中找到某几个连续的数使得它们的和最大,针对这样的题,当然使用DP啦

    先输入他们,f[i]数组代表是 i 状态时的最好方案(最大的和),值得注意的是,f[1]的状态就是a[i],因为就他一个。,我们现在的主要任务就是寻找此问题的状态转移方程,

    仔细想想,我们会发现,我们想要求得的第i个状态的最好方案无非就两种,要么加他,要么不加他,再取个max就好

    得到:f[i]=max(f[i-1]+a[i],a[i]) 

    下面就顺水推舟的出来了

    复制代码
     1 #include<iostream>
     2 #include<cmath>
     3 using namespace std;
     4 int a[1000001];
     5 int f[1000001];
     6 int maxn=-1;
     7 int main()
     8 {
     9     int n;
    10     cin>>n;
    11     for(int i=1;i<=n;i++)
    12     {
    13         cin>>a[i];
    14     }
    15     f[1]=a[1];
    16     for(int i=1;i<=n;i++)
    17     {
    18         f[i]=max(f[i-1]+a[i],a[i]);
    19         maxn=maxn>f[i]? maxn:f[i];
    20     }
    21     cout<<maxn;
    22     return 0;
    23 
    24 }
    复制代码

     

     T5、

    求两个序列的最长公共子串的长度,这个子串要求在序列中是连续的

    只要找到我们想要的状态转移方程就好,关键就是咋想呢??

    我先遍历每一个数组元素,将其分为两种情况,其中一种就是两个元素不相同的,我直接continue进行下一个就好,对于两者相同的,其长度不就是两个元素各自的数组中其前一个数的长度再加1吗,那方程就自然而然的推出来了:f[i][j]=f[i-1][j-1]+1,然后我们只要比较每一个f[i][j],取他们的最大值就好了

    代码如下

    复制代码
    #include<iostream>
    #include<cstdio>
    using namespace std;
    char a[1001],b[1001];
    int f[1001][1001];
    int ans;
    int main()
    {
        int m,n;
        cin>>m>>n;
        for(int i=1;i<=m;i++)
        {
            cin>>a[i];
        }
        for(int i=1;i<=n;i++)
        {
            cin>>b[i];
        }
        for(int i=1;i<=m;i++)
        {
            for(int j=1;j<=n;j++)
            {
                if(a[i]==b[j])
                {
                    f[i][j]=f[i-1][j-1]+1;
                    ans=max(ans,f[i][j]);
                }
            }
        }
        cout<<ans;
        return 0;
    }
    复制代码

     

    还有就是关于数字金字塔啥的,其实都一样,找出它的状态转移方程就好了,比如金字塔那玩意,想要在金字塔的上面走一条路使得经过的数字之和最大,我们只要用过列出状态转移方程(直接看代码吧,不啰嗦了,就是这个数它上面的和左上的f的max再加上自己就好了),得到每个金字塔最低端的终点,再比较哪个终点的f最大就输出,然后就开开心心地A掉了

    复制代码
     1 #include<iostream>
     2 #include<cmath>
     3 using namespace std;
     4 int a[1001][1001];
     5 int f[1001][1001];
     6 int ans;
     7 int main()
     8 {
     9     int n;
    10     cin>>n;
    11     for(int i=1;i<=n;i++)
    12     {
    13         for(int j=1;j<=i;j++)
    14         {
    15             cin>>a[i][j];
    16         }
    17     }
    18     f[1][1]=a[1][1];
    19     for(int i=2;i<=n;i++)
    20     {
    21         for(int j=1;j<=i;j++)
    22         {
    23             f[i][j]=max(f[i-1][j-1],f[i-1][j])+a[i][j];
    24         }
    25     }
    26     for(int i=1;i<=n;i++)
    27     {
    28         ans=ans>f[n][i]? ans:f[n][i];
    29     }
    30     cout<<ans;
    31     return 0;
    32 }
    复制代码

    。。。。。。。

     

    其实还有很多很多很难很难的题,小生孤陋寡闻,才疏学浅,这里就不再体现,以后会查缺补漏,日臻完善的!!

    我就记录到这了,再见!!

     

     2022/3/17

  • 相关阅读:
    Spring AOP详解之面向切面编程
    java.lang.OutOfMemoryError- unable to create new native thread 问题排查
    sql查询之字符函数
    Linux内核设计与实现 第十二章 内存管理
    查找算法【二叉查找树】 - 二叉查找树的插入
    C++内存管理:其五、指针类型转换与嵌入式指针
    金仓数据库 KingbaseES 插件identity_pwdexp
    Python-正则表达式使用
    Node.js | 常用内置模块之 path 路径模块
    Linux应用-ElasticSearch安装
  • 原文地址:https://www.cnblogs.com/zch061023/p/16014487.html