进阶训练-动态规划

线性dp,背包问题,区间dp,树形dp,环形与后效性处理,状压dp,计数类dp,数位dp,倍增优化,数据结构优化,单调队列优化,斜率优化,四边形不等式

线性dp

>

  • 从集合角度考虑dp问题:

    • 状态表示
      • 集合
      • 属性(max,min,数量。。。)
    • 状态计算 —— 集合的划分
      • 重要划分依据:“最后一步”
      • 划分原则:不重不漏
        1
        2
        3
        
        for 阶段
            for 状态
                for 决策
        
  • 技巧

    • 在设计状态转移方程时,不一定要以“如何计算出一个状态”的形式给出,也可以考率“一个已知状态应该更新哪些后续阶段的未知状态”(即从某状态的入边与出边综合考虑)

    • 在实现状态转移方程时,要注意观察决策集合的范围随着状态的变化情况。对于“决策集合中的元素只增多不减少”的情景,可以用一个临时变量来标记决策集合的当前信息,避免重复扫描

      1
      2
      3
      4
      5
      6
      7
      
      rep(i,1,n){
          int val=inf;
          rep(j,1,m){
              val=min(val,f[i-1][j]); //此处即为上诉情况
              f[i][j]=val+abs(a[i]-b[j]);
          }
      }
      
    • 求解线性dp问题,先确定阶段,若阶段不足以表示一个状态,则可以把所需的附加信息也作为状态的维度

    • 在确定dp状态时,要选择最小的能覆盖整个状态空间的“维度集合”,若dp状态由多个维度构成,则应检查这些维度之间能否相互导出,用尽量少的维度覆盖整个状态空间,排除冗余维度

    • 输出方案:从终态往前推即可

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      
      //
      int i=n,j=m,k=sit;
      while(i){
          if(f[i][j][k]==f[i-1][j][k]) i--;
          else {
              ans[++tot]=i;
              k-=w[i];
              i--,j--;
          }
      }
      //
      int i=n,j=m,h=0;
      while(i && j){
          if(f[i][j]==f[i][j-i]) h++, j-=i;
          else {
              rep(k,0,i-1){
                  if(f[i][j] == f[k][j-i+k] + k*(gs[i]-gs[k])){
                      rep(u,k+1,i) ans[g[u].second] = h+1;
                      j-=i-k , i-=i-k;
                      break;
                  }
              }
          }
      }
      //01背包
      rep(i,1,n){
          //最后一个物品n的状态不是由n+1推来的
          if(vol>=v[i] && i==n) {
              printf("%d ",n);
              break;
          }
          if(vol<=0) break;
          //要保证vol-v[i]不会越界
          if(vol-v[i]>=0 && f[i][vol] == f[i+1][vol-v[i]] + w[i]){
              printf("%d ",i);
              vol-=v[i];
          }
      }
      
    • 有时可以通过额外的算法确定dp状态的计算顺序,如贪心排序等,以确保无后效性

    • 有时可在状态空间中运用等效手法对状态进行缩放

      1
      2
      3
      4
      5
      6
      7
      8
      9
      
      rep(i,1,n){
          rep(j,1,m){
              if(j<i) continue;
              f[i][j]=f[i][j-i]; //缩放
              rep(k,0,i-1){
                  f[i][j]=min(f[i][j],f[k][j-i+k] + k*(gs[i]-gs[k]));
              }
          }
      }
      

背包问题

戳这里


区间dp

  • 一般表示:f[i][j]-区间[i,j]的属性

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    
    for 长度 2->n
        for 左端点 1->n-len+1
            for 区间断点 l->r-1
    rep(len,2,n){
            rep(l,1,n-len+1){
                r=l+len-1;
                rep(k,l,r-1){
                    f[l][r]=min(f[l][r],f[l][k]+f[k+1][r]);
                }
                f[l][r]+=s[r]-s[l-1];
            }
        }
    
  • 在设置区间断点k时,一定要注意是否会出现k不同而子区间相同的情况,即决策的互斥性

  • 任意选择一个位置断开,复制成2倍长度的链”的方法,是解决dp中环形结构的常用手段之一

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    
    rep(i,n+1,2*n){
            op[i]=op[i-n];
            a[i]=a[i-n];
        }
        init();
        rep(len,2,n){ // 长度
            rep(l,1,2*n-len+1){ // 左端点
                int r=l+len-1;
                rep(p,l,r-1){ // 区间断点
                    if(op[p+1]=='t') {
                        f[l][r][0]=max(f[l][r][0],f[l][p][0]+f[p+1][r][0]);
                        f[l][r][1]=min(f[l][r][1],f[l][p][1]+f[p+1][r][1]);
                    }
                    else {
                        int x=f[l][p][0],y=f[l][p][1];
                        int z=f[p+1][r][0],w=f[p+1][r][1];
                        f[l][r][0]=max(f[l][r][0],max(x*z, max(x*w, max(y*z, y*w))));
                        f[l][r][1]=min(f[l][r][1],min(x*z, min(x*w, min(y*z, y*w))));
                    }
                } 
            } 
        }
    
  • 对于计数类的dp问题,通常一个状态的各个决策之间满足“加法原则”,而每个决策划分的几个子状态之间满足“乘法原则”。在设计状态转移方程的决策方式与划分方法时,一个状态的所有决策之间必须具有互斥性

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    
    for(int len=1; len<=n; len+=2){
               for(int l=1; l+len-1<=n; l++){
                   int r=l+len-1;
                   if(len==1) f[l][r]=1;
                   else if(s[l]==s[r]){
                       for(int k=l; k<r; k+=2){
                           if(s[k]==s[r])
                               f[l][r] = ((ll)f[l][r] + (ll)f[l][k]*f[k+1][r-1])%mod;    
                       }
                   }
               }
           }
    

树形dp

  • 一般以节点从深到浅(子树从小到大,自下而上)的顺序作为DP的“阶段”
  • DP状态表示中,第一维通常是节点编号(代表以该节点为根的子树),大多时候采用递归的方式实现动态规划
  • 对于每个节点x,先递归在它的每个子节点进行dp,回溯时,从子节点向节点x进行状态转移。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    
    void dfs(int x){
        v[x]=1;
        for(int i=head[x];~i;i=e[i].next){
            int y=e[i].to;
            if(v[y]) continue;
            dfs(y);
            f[x][1]+=f[y][0];
            f[x][0]+=max(f[y][1],f[y][0]);
        }
        f[x][1]+=h[x];
    }
    
  • 二次扫描换根dp:这种问题一般出现在无根树确定根节点。一般先子下而上进行树形dp,然后对整棵树进行一次dfs,自上而下进行换根计算。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    
    //d:dp数组,f:答案数组
    void dp(int x){
        v[x]=1;
        for(int i=head[x];~i;i=e[i].next){
            int y=e[i].to;
            if(v[y]) continue;
            dp(y);
            if(deg[y]==1) d[x]+=e[i].w;
            else d[x]+=min(e[i].w,d[y]);
        }
    }
    void dfs(int x){
        v[x]=1;
        for(int i=head[x];~i;i=e[i].next){
            int y=e[i].to;
            if(v[y]) continue;
            if(deg[x]==1) f[y]=d[y]+e[i].w;
            else f[y] = d[y]+min(f[x]-min(d[y],e[i].w) , e[i].w);
            dfs(y);
        }
    }
    

环形dp与后效性处理

  • 环形
    • 在某个星球上,一天由 N 个小时构成,我们称0点到1点为第1个小时、1点到2点为第2个小时,以此类推。
      在第 i 个小时睡觉能够恢复Ui点体力。
      在这个星球上住着一头牛,它每天要休息B个小时。
      它休息的这B个小时不一定连续,可以分成若干段,但是在每段的第一个小时,它需要从清醒逐渐入睡,不能恢复体力,从下一个小时开始才能睡着。
      为了身体健康,这头牛希望遵循生物钟,每天采用相同的睡觉计划。
      另外,因为时间是连续的,即每一天的第N个小时和下一天的第1个小时是相连的(N点等于0点),这头牛只需要在每N个小时内休息够B个小时就可以了。
      请你帮忙给这头牛安排一个睡觉计划,使它每天恢复的体力最多。
    • 此题将环中n(0)和1间断开,分别对n(0)和1点没有睡觉和正在睡觉的情况做dp,取二者最优值.
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      78
      79
      80
      81
      82
      83
      84
      85
      86
      87
      88
      89
      90
      91
      92
      93
      94
      95
      96
      97
      98
      99
      100
      101
      102
      103
      
         //https://www.acwing.com/activity/content/code/content/171286/
          int n,m;
          int f[2][N][10],w[N];
          int main(){
              scanf("%d%d",&n,&m);
              rep(i,1,n){
                  scanf("%d",&w[i]);
              }
              clr(f,-0x3f);
              f[1][0][0]=f[1][1][1]=0;
              rep(i,2,n){
                  rep(j,0,m){
                      if(j>i) continue;
                      if(j==0) f[i&1][j][0]=0;
                      else {
                          f[i&1][j][0]=max(f[(i-1)&1][j][0],f[(i-1)&1][j][1]);
                          f[i&1][j][1]=max(f[(i-1)&1][j-1][0],f[(i-1)&1][j-1][1]+w[i]);
                      }
                  }
              }
              int ans=-inf;
              ans=max(f[n&1][m][0],f[n&1][m][1]);
              clr(f,-0x3f);
              f[1][1][1]=w[1];
              rep(i,2,n){
                  rep(j,0,m){
                      if(j>i) continue;
                      if(j==0) f[i&1][j][0]=0;
                      else {
                          f[i&1][j][0]=max(f[(i-1)&1][j][0],f[(i-1)&1][j][1]);
                          f[i&1][j][1]=max(f[(i-1)&1][j-1][0],f[(i-1)&1][j-1][1]+w[i]);
                      }
                  }
              }
              ans=max(ans,f[n&1][m][1]);
              printf("%d\n",ans);
              return 0;
          }
         ``` 
      # 后效性处理
      > 当遇到状态转换有后效性时,可是尝试用列方程组并通过高斯消元求出当前阶段每一个状态的解
      ```c++
      /*给定一张 N*M 的棋盘,有一个机器人处于(x,y)位置。
      这个机器人可以进行很多轮行动,每次等概论地随机选择停在原地、向左移动一格、向右移动一格或向下移动一格。
      当然机器人不能移出棋盘。
      求机器人从起点走到最后一行的任意一个位置上,所需行动次数的数学期望值。*/
      
      
      #include<bits/stdc++.h>
      #define clr(a,b) memset(a,b,sizeof(a))
      #define ll long long
      #define ull unsigned long long
      #define rep(i,a,b) for(int i=a;i<=b;++i)
      #define repd(i,a,b) for(int i=a;i>=b;--i)
      #define dbg(a,i,n) printf("c",a," \n"[i==n])
      using namespace std;
      const int inf=0x3f3f3f3f;
      const ll linf=(1ll<<62)-1;
      const int N=1e3+7;
      const int M=4e5+7;
      const int mod=1e9+7;
      template <typename T>inline void read(T &x){
          char c;int sign=1;x=0;
          do{c=getchar();if(c=='-')sign=-1;}while(c<'0'||c>'9');
          do{x=x*10+c-'0';c=getchar();}while(c>='0'&&c<='9');
          x*=sign;
      }
      double a[N][N],f[N][N];
      int n,m,x,y;
      void gauss(){
          
          for(int i=1;i<=m;i++){
              //主元化1
              double r=a[i][i];
              a[i][i]/=r,a[i][i+1]/=r;
              if(i<m) a[i][m+1]/=r;
              //下面其他列化0
              double t=a[i+1][i];
              a[i+1][i]=0, a[i+1][i+1]-=t*a[i][i+1], a[i+1][m+1]-=t*a[i][m+1];
          }
          
          for(int i=m;i;i--){
              a[i-1][m+1]-=a[i][m+1]*a[i-1][i];
              a[i-1][i]=0.0;
          }
      }
      int main(){
          cin>>n>>m>>x>>y;
          if(m==1){
              printf(".4lf\n",2.0*(n-x));
              return 0;
          }
          for(int i=n-1;i>=x;i--){
              a[1][1]=2.0/3, a[1][2]=-1.0/3, a[1][m+1]=f[i+1][1]/3+1;
              a[m][m-1]=-1.0/3, a[m][m]=2.0/3, a[m][m+1]=f[i+1][m]/3+1;
              for(int j=2;j<m;j++)
                  a[j][j-1]=-1.0/4, a[j][j]=3.0/4, a[j][j+1]=-1.0/4, a[j][m+1]=f[i+1][j]/4+1;
              gauss();
              for(int j=1;j<=m;j++) f[i][j]=a[j][m+1];
          }
          printf("%.4lf\n",f[x][y]);
      	return 0;
      }
      
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值