zoukankan      html  css  js  c++  java
  • 初识DP

    写在前面的话:

    其实在去年寒假奥赛集训的时候,就已经接触DP了,但自己是真得对那时的自己很无语,不会,想不通,记不住就不管了,也没想过要一定把它吃透--但该来的总还是要来的。

    所以现在就来玩好玩的DP吧。

    DP分类:

    一、简单基础dp

    这类dp主要是一些状态比较容易表示,转移方程比较好想,问题比较基本常见的。主要包括递推、背包、LIS(最长递增序列),LCS(最长公共子序列)

    二、区间dp

    三、树形dp

    四、数位dp

    五、状态压缩dp

    好,柿子先从软的捏

    先了解dp的基本思想吧。

    在现实生活中,有些过程可以分成若干个相互联系的阶段,在它的每一阶段都要做出决策,从而使整个过程达到最好的活动效果。其中,各个阶段决策的选取既依赖当前面临的状态,又影响以后的发展,当个各阶段决策确定后,就构成了一个决策序列。

    动态规划问题满足三大重要性质

    • 最优子结构性质:如果问题的最优解所包含的子问题的解也是最优的,我们就称该问题具有最优子结构性质(即满足最优化原理)。最优子结构性质为动态规划算法解决问题提供了重要线索。
    • 子问题重叠性质:子问题重叠性质是指在用递归算法自顶向下对问题进行求解时,每次产生的子问题并不总是新问题,有些子问题会被重复计算多次。动态规划算法正是利用了这种子问题的重叠性质,对每一个子问题只计算一次,然后将其计算结果保存在一个表格中,当再次需要计算已经计算过的子问题时,只是在表格中简单地查看一下结果,从而获得较高的效率。
    • 无后效性:将各阶段按照一定的次序排列好之后,对于某个给定的阶段状态,它以前各阶段的状态无法直接影响它未来的决策,而只能通过当前的这个状态。换句话说,每个状态都是过去历史的一个完整总结。这就是无后向性,又称为无后效性。

     OK,现在就开始看第一个简单的模型吧--数字金字塔。

                   
                     7
                   3 8
                  8 1 0
                 2 7 4 4
                4 5 2 6 5

    大体思想:

    法1:顺推法

    路径起点确定,中间点与终点相对不确定,定义f[x][y]为从(1,1)出发到达(x,y)的路径最大权值和。

    因为要使从(x,y)到终点值最大,就要使(1,1)到(x,y)值最大,并且,到达(x,y)的路径就只有两条,一个左上,一个右上,当然,两边的点也不必担心,因为它的左上或右上的值为零,状态转移方程依然成立。

     f[x][y]=max{f[i-1][y-1],f[i-1][y]}+a[x][y]。

    最后,ans为f[n][1~n]最大的一个

    法2:逆推法(新颖的脑回路)

    由顶向下分析,自底向上计算。

    f[x][y]=max{f[x+1][y+1]+f[x+1][y]}+a[x][y]。

    for(int i=1;i<=n;i++)

    f[n][i]=a[n][i];

    推荐博文;https://www.cnblogs.com/Renyi-Fan/p/9285495.html

    and then  

    LIS(最长上升子序列) 

    #include<iostream>
    #include<cstdio>
    using namespace std;
    int a[100],n,f[100],ans;//f[]表示从1~~n每个数所包含的最大上升子序列
    int main() //a[]记录数字
    {
    cin>>n;
    for(int i=1;i<=n;i++)
    {
    cin>>a[i];
    }
    for(int i=1;i<=n;i++)
    {
    f[i]=1;
    for(int j=1;j<i;j++)
    {
    if(a[j]<a[i])
    f[i]=max(f[i],f[j]+1);//f[j]+1表示若以a[j]为a[i]的下一个子序列时的最大子序列数
    }//有可能a[j]过小以至于子序列数过短
    }
    for(int i=1;i<=n;i++)
    {
    if(f[i]>ans)
    ans=f[i];//包含递推思想
    }
    cout<<ans;
    return 0;
    }

    以上为0(n^2)

    我自己的理解:

    f[i]表示以a[i]结尾的序列的最长长度,其序列最短也还是有一个的(本身),so初始化为f[i]=1;

    后面如果有比a[i]小的,例如我们先设为a[k]吧,那我们可以选择将以a[i]结尾的那一串数接在a[k]的后面,此时以a[k]结尾的序列长度就为f[i]+1;

    当然,选择这个a[i]接在后面不一定是最长的,所以我们要将目前已知f[i]的值与选择a[i]接在前面从而得到的值比较,

    f[i]=max(f[j]+1,f[i])。

    依次遍历a[1]~a[n],然后sort一下就能得到lis了。

    有0(nlongn)的写法,但是,好吧,我不会啊。。。

    但不妨碍我粘个代码:

     1 int n;
     2     cin>>n;
     3     for(int i=1;i<=n;i++)
     4     {
     5         cin>>a[i];
     6         f[i]=0x7fffffff;
     7         //初始值要设为INF
     8         /*原因很简单,每遇到一个新的元素时,就跟已经记录的f数组当前所记录的最长
     9         上升子序列的末尾元素相比较:如果小于此元素,那么就不断向前找,直到找到
    10         一个刚好比它大的元素,替换;反之如果大于,么填到末尾元素的下一个q,INF
    11                 就是为了方便向后替换啊!*/ 
    12     }
    13     f[1]=a[1];
    14     int len=1;//通过记录f数组的有效位数,求得个数 
    15     /*因为上文中所提到我们有可能要不断向前寻找,
    16     所以可以采用二分查找的策略,这便是将时间复杂
    17     度降成nlogn级别的关键因素。*/ 
    18     for(int i=2;i<=n;i++)
    19     {
    20         int l=0,r=len,mid;
    21         if(a[i]>f[len])f[++len]=a[i];
    22         //如果刚好大于末尾,暂时向后顺次填充 
    23         else 
    24         {
    25         while(l<r)
    26         {   
    27             mid=(l+r)/2;
    28             if(f[mid]>a[i])r=mid;
    29     //如果仍然小于之前所记录的最小末尾,那么不断
    30     //向前寻找(因为是最长上升子序列,所以f数组必
    31     //然满足单调) 
    32             else l=mid+1; 
    33         }
    34         f[l]=min(a[i],f[l]);//更新最小末尾 
    35         }
    36     }
    37     cout<<len;

    LCS(最长公共子序列):

    用 dp[i][j] 来表示第一个串的前 i位,第二个串的前j位的 LCS 的长度。

     1 //T:最长公共子序列 
     2 #include <cstdio>
     3 #include <algorithm>
     4 
     5 #define MAXN 2111
     6 
     7 using namespace std;
     8 
     9 int n, m;
    10 int a[MAXN], b[MAXN];
    11 int f[MAXN][MAXN];
    12 int main() {
    13     scanf("%d%d", &n, &m);
    14     for(int i = 1; i <= n; i++) scanf("%d", &a[i]);
    15     for(int i = 1; i <= m; i++) scanf("%d", &b[i]);
    16     
    17     for(int i = 1; i <= n; i++) {
    18         for(int j = 1; j <= m; j++) {
    19             
    20             f[i][j] = max(f[i - 1][j], f[i][j - 1]);
    21             
    22             if(a[i] == b[j]) f[i][j] = max(f[i][j], f[i - 1][j - 1] + 1);
    23             
    24             else f[i][j] = max(f[i][j], f[i - 1][j - 1]);
    25             
    26         }
    27     }
    28     printf("%d
    ", f[n][m]);
    29     return 0;
    30 }

     个人以为DP是贪心思想与递推思想的结合

    写得非常好:https://www.luogu.org/blog/pks-LOVING/junior-dynamic-programming-dong-tai-gui-hua-chu-bu-ge-zhong-zi-xu-lie

    OK,现在来看一些题目:

    硬币问题
    您有无限多的硬币,硬币的面值为 1,5,10,50,100,500.
    给定一个数额 w,问您最少用多少枚硬币可以凑出 w.

    依据生活经验,我们可以采用这种策略:
    先尽量用500的,然后尽量用100的……以此类推。
    e.g. 666 = 1*500+1*100+1*50+1*10+1*5+1*1,共用10枚硬币。
    这就是贪心了。
    我们每次使用一个硬币,总能最大程度地解决问题(把剩下要凑的数
    额变小)。可是,贪心是一种只考虑眼前情况的策略。尽管这一套硬
    币面值可以采用贪心策略,但是迟早要栽跟头的。

    我们考虑一组新的硬币面值:1,5,11.
    于是有了一个反例:如果我们要凑出15,贪心策
    15 = 11+4*1,共用 5 枚硬币。
    而最佳策略是:
    15 = 3*5,共用3枚硬币。

    贪心策略自此陷入困境:鼠目寸光。
    在 w=15 时,贪心策略选择了面值 11 的硬币(因为这样可以尽可
    能降低要凑的数额)。
    在选择了面值为 11 的硬币之后,我们只好面对 w=4 的处境。

    我们重新分析刚刚的情况:
    w=15时,我们取了11,接下来面对w=4的情况。
    w=15时,如果我们取5,接下来就面对w=10的情况。
    我们记“凑出n需要用到的最少硬币数量”为 f(n).

    那么,如果我们取了 11,则:
    cost = f(4) + 1 = 4 + 1 = 5.
    解释:我们用了一枚面值为 11 的硬币,所以加
    接下来面对的是 w=4 的情况。f(4) 我告诉你等
    相应地,如果我们选择取 5,则:
    cost = f(10) + 1 = 2 + 1 = 3.

    那么,w=15时,我们选哪枚硬币呢?
    cost最低的那一个!
    11: cost = f(4) + 1 = 4 + 1 = 5.
    5: cost = f (10) + 1 = 2 + 1 = 3.
    1: cost = f (14) + 1 = 4 + 1 = 5.
    选择5,f( 15) = 3,即为答案!

    f(n) = min {f(n− 1) , f(n − 5) ,f(n − 11) } + 1

    for(int i = 1; i <=n ; i++{
      cost=INF;
      if(i-1>=0) cost=min(cost,f[i-1]+1);
      if(i-5>=0) cost=min(cost,f[i-5]+1);
      if(i-11>=0) cost=min(cost,f[i-11]+1);
      f[i]=cost;
    }
    cout<<f[n];

    依次将当前面值n与n-1 n-5 n-11面值的最小硬币数进行状态转移

    这个算法的时间复杂度显然是O(n).为什么比暴力要快呢?
    我们暴力枚举了“使用的硬币”,然而这属于冗余信息。
    我们要的是答案,根本不关心这个答案是怎么凑出来的。
    要求出f(15),只需要知道f (14) , f(10) , f(4)的值。
    其他信息不需要。

    要求出f(15),只需要知道f (14) , f(10) , f(4)的值,而
    f (14) , f(10) , f(4)是如何算出来的,对之后的问题没有影响。
    “未来与过去无关”,这就是无后效性。

  • 相关阅读:
    MySQL性能指标及计算方法(go)
    Flashback for MySQL 5.7
    oracle_外部表的两种实现方式oracle_loader[datapump]
    oracle数据库内存调整之增加内存
    Leetcode: Binary Tree Paths
    Leetcode: Verify Preorder Sequence in Binary Search Tree
    Leetcode: Meeting Rooms II
    Leetcode: Meeting Rooms
    Leetcode: Flatten 2D Vector
    Leetcode: Count Univalue Subtrees
  • 原文地址:https://www.cnblogs.com/becase/p/11809013.html
Copyright © 2011-2022 走看看