当前位置: 首页 > news >正文

动态规划——区间DP,计数类DP,数位统计DP

本博客部分内容参考:《算法竞赛进阶指南》

一.区间DP

  划重点:

  以前所学过的线性DP一般从初始状态开始,沿着阶段的扩张向某个方向递推,直至计算出目标状态。

  区间DP也属于线性DP的一种,它以“区间长度”作为DP的“阶段”,使用两个坐标(区间的左、右端点)描述每个维度。在区间DP中,一个状态由若干个比它更小且包含于它的区间所代表的状态转移而来,因此区间DP的决策往往就是划分区间的方法。区间DP的初态一般就由长度为1的“元区间”构成。

  下面介绍一道经典题:石子合并

题目描述:


设有N堆石子排成一排,其编号为1,23,…,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)。

输出格式

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

数据范围

1N300,1≤N≤300

输入样例:

4
1 3 5 2

输出样例:

22

 

DP思路:

  状态表示:我们用数组F[i,j]来描述DP状态,表示闭区间[i,j]的石子合并所需的最小代价。

  对于任意时刻,任意区间[i,j]的石子,若第l堆与第r堆石子已经被合并,那么说明l~r之间任意一堆石子都已经被合并,这样才能保证l与r相邻。而要使第l堆与第r堆合并,必然存在一个整数k,使得第l~k堆石子先合并成一堆,第k+1~r堆石子合并成一堆,然后这两堆石子才能合并。于是,划分点k便是转移的决策区间长度当然要作为DP的阶段,那么便可以得到状态转移方程。

  状态计算:状态转移方程:F[l,r]=min{F[l,k]+F[k+1,r]}+ΣAi。其中k∈[l,r),i∈[l,r]。ΣAi表示区间[l,r]内每个数的和。对于求这个和,我们可以用一维的前缀和来实现:用数组sum[i]表示到以第i个数字结尾的前缀和,便可以得到ΣAi=sum[r]-sum[l-1]。

AC代码如下:

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
using namespace std;
const int N = 300+10;
int f[N][N],sum[N];

inline int read()
{
    int x=0,f=1;char ch=getchar();
    while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
    while(ch<='9'&&ch>='0')x=(x<<1)+(x<<3)+(ch^48),ch=getchar();
    return x*f;
}

int main()
{
    int n=read();
    for(int i=1;i<=n;i++)
    {
        int x=read();
        sum[i]=sum[i-1]+x;
    }

    for(int m=1;m<=n-1;m++)  //注意DP循环中的各个边界
        for(int i=1,j=i+m;j<=n;i++,j++)
        {
            int minn=2e+9;
            for(int k=i;k<=j-1;k++)
                minn=min(minn,f[i][k]+f[k+1][j]);
            f[i][j]=minn+sum[j]-sum[i-1];
        }

    printf("%d\n",f[1][n]);
    return 0;
}

 

二.计数类DP

  计数类DP和数位统计DP两类问题都特别强调“不重不漏”,统计对象的可能情况一般比较多,通常需要精确的划分和整体性的计算。因此,使用动态规划抽象出问题中的“子结构”和推导的“阶段”,将有助于我们准确而高效地进行求解。

  下面介绍一道经典题:整数划分

  这里介绍这道题的两种DP解法:

  注意:类似于整数划分这种计数类DP考虑顺序(排列)与不考虑顺序(组合)是不同的算法

 

1.完全背包解法

  状态表示:
  F[i,j]表示只从1~i中选,且总和等于j的方案数。

  状态转移方程:
  F[i,j] = F[i - 1,j] + F[i - 1,j - i];

  对于任意一种状态F[i,j]是由什么状态转移而来的问题,我们可以类似于背包问题地考虑Ai这个数的选与不选两种情况。而F[i,j]就是这两种情况的数量之和。

  如果不选,那么就是从前i-1个数中选出若干个数使和为j的方案数;如果选,那么就是从前i+1个数中选出若干个数使和为j-i的方案数。

  那么再类比背包问题中将二维数组优化成一维的方式,我们就可以得到一维数组的状态转移方程:F[j]=F[j]+F[j - i];

  最后再注意一下边界:F[0] = 1。

  代码如下:

#include <iostream>
#include <algorithm>

using namespace std;

const int N = 1010, mod = 1e9 + 7;

int n;
int f[N];

int main()
{
    cin >> n;

    f[0] = 1;
    for (int i = 1; i <= n; i ++ )
        for (int j = i; j <= n; j ++ )
            f[j] = (f[j] + f[j - i]) % mod;

    cout << f[n] << endl;

    return 0;
}

 

 

2.
  状态表示:
  F[i,j]表示总和为i,总个数为j的方案数

  状态转移方程:
  F[i,j] = F[i - 1,j - 1] + F[i - j,j];

  参考上面一种方法的DP思路,我们可以把所有状态的集合划分成两类,一类是选择了数字“1”的,另一类是不选择数字“1”的。那么F[i,j]仍然是这两类情况之和。

  如果不选“1”,那么就是从j-1个数中选出总和为i-1的方案数;如果选“1”,那么就是从j个数中选出总和为i-j的方案数。

  最后再将总和为n的所有情况求和即可。

  代码如下:

#include <iostream>
#include <algorithm>

using namespace std;

const int N = 1010, mod = 1e9 + 7;

int n;
int f[N][N];

int main()
{
    cin >> n;

    f[1][1] = 1;
    for (int i = 2; i <= n; i ++ )
        for (int j = 1; j <= i; j ++ )
            f[i][j] = (f[i - 1][j - 1] + f[i - j][j]) % mod;

    int res = 0;
    for (int i = 1; i <= n; i ++ ) res = (res + f[n][i]) % mod;

    cout << res << endl;

    return 0;
}

 

三.数位统计DP

  这类问题其实与DP关系不大,大多都是小学奥数的问题,这里也不多讲,直接放经典题:计数问题

  这道题由于数据范围的限制,直接枚举肯定是过不去的(否则那就是入门题了)。对于这个问题的解法,为了减少上下界的限制,我们不妨计算1~a-1和1~b中各个数字出现的次数,再作一个差,就可以得到答案了。对于统计数字的方法,我们先举一个的例子:

  统计数字“1”在五位数abcde中出现的次数:

  首先看“1”在第一位出现了几次,比较a与1的大小,若a=1,显然是abcde-9999;若a>1,则是10000;

  再看第二位,先加上二位数ab-1乘上1000,再比较b与“1”的大小,若b=1,则再加上三位数cde,若b>1,则加上1000;

  ……

  以此类推,便可以求出第三位、第四位与第五位的次数,最后求一个和即可。

  代码实现如下:

 

  

 

转载于:https://www.cnblogs.com/ninedream/p/11197835.html

相关文章:

  • javascript基础学习六--原型与继承
  • reboot 示例代码
  • 桥模式
  • k8s高可用
  • kafka笔记博客
  • 开发工具之AltiumDesigner
  • 为什么使用消息队列?消息队列有什么优点和缺点?Kafka、ActiveMQ、RabbitMQ、RocketMQ 都有什么优点和缺点?...
  • go guid 和uuid生成
  • 使用策略模式减少if else
  • numpy中的max()函数
  • [NOI2005]聪聪与可可(期望)
  • span底部显示border一半
  • Django之ORM多表操作
  • Dilated/Atrous conv 空洞卷积/多孔卷积
  • PHP 用户登录与退出
  • [笔记] php常见简单功能及函数
  • 【跃迁之路】【641天】程序员高效学习方法论探索系列(实验阶段398-2018.11.14)...
  • Android 初级面试者拾遗(前台界面篇)之 Activity 和 Fragment
  • Android开源项目规范总结
  • Computed property XXX was assigned to but it has no setter
  • css的样式优先级
  • ES6 ...操作符
  • Fastjson的基本使用方法大全
  • hadoop集群管理系统搭建规划说明
  • JavaScript标准库系列——Math对象和Date对象(二)
  • js学习笔记
  • JWT究竟是什么呢?
  • Linux快速配置 VIM 实现语法高亮 补全 缩进等功能
  • PHP 使用 Swoole - TaskWorker 实现异步操作 Mysql
  • Travix是如何部署应用程序到Kubernetes上的
  • 关于使用markdown的方法(引自CSDN教程)
  • 码农张的Bug人生 - 见面之礼
  • 浅谈Golang中select的用法
  • 我这样减少了26.5M Java内存!
  • 一道面试题引发的“血案”
  • 鱼骨图 - 如何绘制?
  • 06-01 点餐小程序前台界面搭建
  • postgresql行列转换函数
  • ​无人机石油管道巡检方案新亮点:灵活准确又高效
  • #stm32整理(一)flash读写
  • (1)常见O(n^2)排序算法解析
  • (7)STL算法之交换赋值
  • (C#)一个最简单的链表类
  • (Mirage系列之二)VMware Horizon Mirage的经典用户用例及真实案例分析
  • (NO.00004)iOS实现打砖块游戏(十二):伸缩自如,我是如意金箍棒(上)!
  • (PWM呼吸灯)合泰开发板HT66F2390-----点灯大师
  • (zhuan) 一些RL的文献(及笔记)
  • (超详细)语音信号处理之特征提取
  • (非本人原创)我们工作到底是为了什么?​——HP大中华区总裁孙振耀退休感言(r4笔记第60天)...
  • (附源码)流浪动物保护平台的设计与实现 毕业设计 161154
  • (紀錄)[ASP.NET MVC][jQuery]-2 純手工打造屬於自己的 jQuery GridView (含完整程式碼下載)...
  • (南京观海微电子)——COF介绍
  • (一)认识微服务
  • (原创)Stanford Machine Learning (by Andrew NG) --- (week 9) Anomaly DetectionRecommender Systems...
  • .bat批处理(六):替换字符串中匹配的子串