动态规划篇——DP问题

本次我们介绍动态规划篇的DP问题,我们会从下面几个角度来介绍:

  • 区间DP
  • 计数DP
  • 树状DP
  • 记忆化搜索

区间DP

我们通过一个案例来讲解区间DP:

/*题目展示*/

题目名:石子合并

设有 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

我们对问题采用DP分析思路:

状态表示:f[i][j]

状态集合意义:表示将第i堆石子到第j堆石子堆在一起的合并方式

状态集合属性:保存其消耗的Min

状态计算方式:
我们首先用s[i]来存储前i个石头的总值,为了方便我们计算部分石头范围的总值(前缀法)
我们的每个f[i][j]都是最小值,我们希望采用k作为中间点,当使用k作为中间点时,f[i][j]为最小值
因此我们f[i][j] = Math.min(f[i][j],f[i][k]+f[k+1][j]+s[j]-s[i-1])
同时由于我们的比较大的数据都是由小数据产生的,所以我们根据石块范围大小来从头开始赋值

我们给出实际代码展示:

import java.util.*;

public class SectionDP {
public static void main(String[] args){ Scanner scan = new Scanner(System.in); // 数据准备,s是前i位石头和,f存放区间最优解
int N = 310;
int[] s = new int[N];
int[][] f = new int[N][N]; // 输入数据
int n = scan.nextInt();
for(int i = 1 ; i <= n ; i ++ ) s[i] = scan.nextInt(); // 前缀和
for(int i = 1 ; i <= n ; i ++ ) s[i] += s[i - 1]; //这里是枚举的每种长度,比如n等于4,比如长度3,右边下标不超过n,求f[1-3]和f[2-4]里面的最小值
for(int len = 2 ; len <= n ; len ++ ){
// 然后我们从第1位开始枚举,枚举当前长度的所有情况
for(int i = 1; i + len - 1 <= n ; i ++ ){
// 每种长度的j
int j = i + len - 1;
// 因为要枚举的是k里面的最小值,所以赋一个很大的数,
// 如果没有赋最大的数,你的f[i][j] 初始值是0,所以最小是永远会被是0,最后输出也会是0
f[i][j] = (int)1e9;
// 关键步骤,当前大区间最优解是由小区间最优解产生的!!!
for(int k = i ; k < j ; k ++ ){
f[i][j] = Math.min(f[i][j],f[i][k] + f[k + 1][j] + (s[j] - s[i - 1]));
}
}
} // 最后输出1~n的石子和最优解即可
System.out.println(f[1][n]);
}
}

计数DP

我们通过一个案例来讲解计数DP:

/*题目展示*/

题目名:整数划分

一个正整数 n 可以表示成若干个正整数之和,形如:n=n1+n2+…+nk,其中 n1≥n2≥…≥nk,k≥1。

(上述的意思就是不需要计算重复情况,例如5=2+2+1=1+2+2是一种情况)

我们将这样的一种表示称为正整数 n 的一种划分。

现在给定一个正整数 n,请你求出 n 共有多少种不同的划分方法。

/*输入格式*/

共一行,包含一个整数 n。

/*输出格式*/

共一行,包含一个整数,表示总划分数量。

由于答案可能很大,输出结果请对 10e9+7 取模。

/*数据范围*/

1 ≤ n ≤ 1000

/*输入样例*/

5

/*输出样例*/

7

我们对问题采用DP分析思路:

状态表示:f[i][j]

状态集合含义:表示前i个数搭配总数相加为j

状态集合属性:表示数量

状态计算:
原本形式为:
f[i][j] = f[i-1][j] + f[i-1][j-i] + f[i-1][j-2i] + ... + f[i-1][j-si]
但由于:
f[i][j-i] = f[i-1][j-i] + f[i-1][j-2i] + ... + f[i-1][j-si]
所以我们可以优化为:
f[i][j] = f[i-1][j] + f[i][j-i]
同时我们可以用移动数组来进行叠层优化:
f[j] = f[j] + f[j-i];(注意:这里的f[j]是上一层,f[i-1]是这一层,我们需要注意遍历顺序)

最后我们给出解题代码:

import java.util.*;

public class Main {
public static void main(String[] args){ Scanner scan = new Scanner(System.in); int N = 1010,mod = (int)(1e9 + 7); // f[i]为总和为i的方法总数
int[] f = new int[N]; int n = scan.nextInt(); // 初始化:一个数都不选,总和是0,是一种方案f[i][0],前i个数中选,总和恰好等于0,只有一种都不选
f[0] = 1; // 进行n次遍历,这里是i的遍历,也就是前i个数选的遍历
for(int i = 1 ; i <= n ; i ++ ){
// 进行n次遍历,这里是j的遍历,也就是给f[j]赋值的遍历
for(int j = i ; j <= n ; j ++ ){
//状态表示:f[i][j] = f[i - 1][j] + f[i][j - i]
f[j] = (f[j] + f[j - i]) % mod;
}
}
System.out.println(f[n]);
}
}

树状DP

我们通过一个案例来讲解树状DP:

/*题目展示*/

题目名:没有上司的舞会

Ural 大学有 N 名职员,编号为 1∼N。

他们的关系就像一棵以校长为根的树,父节点就是子节点的直接上司。

每个职员有一个快乐指数,用整数 Hi 给出,其中 1≤i≤N。

现在要召开一场周年庆宴会,不过,没有职员愿意和直接上司一起参会。

在满足这个条件的前提下,主办方希望邀请一部分职员参会,使得所有参会职员的快乐指数总和最大,求这个最大值。

/*输入格式*/

第一行一个整数 N。

接下来 N 行,第 i 行表示 i 号职员的快乐指数 Hi。

接下来 N−1 行,每行输入一对整数 L,K,表示 K 是 L 的直接上司。

/*输出格式*/

输出最大的快乐指数。

/*数据范围*/

1 ≤ N ≤ 6000,
−128 ≤ Hi ≤ 127 /*输入样例*/ 7
1
1
1
1
1
1
1
1 3
2 3
6 4
7 4
4 5
3 5 /*输出样例*/ 5

我们对问题采用DP分析思路:

状态表示:f[u][0],f[u][1]

状态集合含义:f[u][0]表示以u为根的子树中选择并且不选择u这个点
f[u][1]表示以u为根的子树中选择并且选择u这个点 状态集合属性:表示MAX 状态计算:
f[u]表示根树,f[s]表示对应的子树
首先我们根据自身是否选择,来设置初始值:f[u][0] = 0,f[u][1] = 1
f[u][0]表示不选择该点,那么子树就可以选择:f[u][0] += Math.max(f[s][1],f[s][0])
f[u][1]表示选择该点,那么子树就不可以选择:f[u][1] += f[s][0]

最后我们给出解题代码:

import java.util.*;

public class Main {

    static int N = 6010,n,idx;

    // 职员的快乐指数(输入值)
static int[] happy = new int[N]; // f[u][0]和f[u][1]的模型
static int[][] f = new int[N][2]; // 单链表模拟树
static int[] h = new int[N],e = new int[N],ne = new int[N]; // 判断是否有父类
static boolean[] hasFather = new boolean[N]; public static void main(String[] args){ Scanner scan = new Scanner(System.in); // 输入
n = scan.nextInt();
for(int i = 1 ; i <= n ; i ++ ) happy[i] = scan.nextInt(); // 邻接表初始设置
Arrays.fill(h,-1); // 设置树
for(int i = 0 ; i < n - 1 ; i ++ ){
int a = scan.nextInt();
int b = scan.nextInt(); // 因为b是a的直系上司,所以需要b->a
add(b,a); // 员工拥有父节点
hasFather[a] = true;
} // 设置根节点为初始值,从1开始判断
int root = 1; // 寻找根节点(找那个没有父类的,就是根节点)
while(hasFather[root]) root ++ ; // 开始递归
dfs(root); // 最后输出的是选根节点跟不选根节点两种方案的最大值
System.out.println(Math.max(f[root][0],f[root][1]));
} // 递归
public static void dfs(int u){ // 不选时,该点初始快乐值为0
f[u][0] = 0; // 选择该点时,该点初始快乐值为1
f[u][1] = happy[u]; // 从该点开始根据子节点更新数据
for(int i = h[u]; i != -1 ; i = ne[i]){ // 子节点
int j = e[i]; // 对子节点进行数据更新
dfs(j); // 对该点进行数据更新 // 如果这个根节点不选,就等于他的所有根节点选与不选的最大值之和
f[u][0] += Math.max(f[j][0],f[j][1]); // 如果这个根节点选,就等于他的所有根节点不选的和
f[u][1] += f[j][0]; }
} // 邻接表衔接
public static void add(int a,int b){
e[idx] = b;
ne[idx] = h[a];
h[a] = idx ++;
}
}

记忆化搜索

我们通过一个案例来讲解记忆化搜索:

/*题目展示*/

给定一个 R 行 C 列的矩阵,表示一个矩形网格滑雪场。

矩阵中第 i 行第 j 列的点表示滑雪场的第 i 行第 j 列区域的高度。

一个人从滑雪场中的某个区域内出发,每次可以向上下左右任意一个方向滑动一个单位距离。

当然,一个人能够滑动到某相邻区域的前提是该区域的高度低于自己目前所在区域的高度。

下面给出一个矩阵作为例子:

1  2  3  4 5

16 17 18 19 6

15 24 25 20 7

14 23 22 21 8

13 12 11 10 9

在给定矩阵中,一条可行的滑行轨迹为 24−17−2−1。

在给定矩阵中,最长的滑行轨迹为 25−24−23−…−3−2−1,沿途共经过 25 个区域。

现在给定你一个二维矩阵表示滑雪场各区域的高度,请你找出在该滑雪场中能够完成的最长滑雪轨迹,并输出其长度(可经过最大区域数)。

/*输入格式*/

第一行包含两个整数 R 和 C。

接下来 R 行,每行包含 C 个整数,表示完整的二维矩阵。

/*输出格式*/

输出一个整数,表示可完成的最长滑雪长度。

/*数据范围*/

1 ≤ R, C ≤ 300,
0 ≤ 矩阵中整数 ≤ 10000 /*输入样例*/ 5 5
1 2 3 4 5
16 17 18 19 6
15 24 25 20 7
14 23 22 21 8
13 12 11 10 9 /*输出样例*/ 25

我们对问题采用DP分析思路:

状态表示:f[i][j]

状态集合含义:f[i][j]表示从i,j位置开始移动所经过的路径

状态集合属性:表示MAX

状态计算:
f[i][j]有四个移动方向,分别是[0][1],[0][-1],[1][0],[-1][0]
我们首先需要判定四个方向是否可以移动:是否达到边界?是否高度不对等?
我们用f[i][j]表示当前位置,f[a][b]表示已经移动后的位置
f[i][j] = Math.max(f[i][j],f[a][b] + 1)

最后我们给出解题代码:

import java.util.*;

public class Main {

    static int N = 310,n,m;

    // h表示高度,f表示当前位置开始移动所移动的最大距离
static int[][] h = new int[N][N];
static int[][] f = new int[N][N]; // 用数组模拟移动上下左右
static int[] dx = {-1,0,1,0},dy = {0,1,0,-1}; public static void main(String[] args){ Scanner scan = new Scanner(System.in); // 赋值
n = scan.nextInt();
m = scan.nextInt(); // 高度赋值
for(int i = 1 ; i <= n ; i ++ )
for(int j = 1 ; j <= m ; j ++ )
h[i][j] = scan.nextInt(); // 将所有的方案初始化成-1,检测该路径是否已经dp过
for(int i = 0 ; i < N ; i ++ ) Arrays.fill(f[i],-1); // 返回值
int res = 0; // 对每个点都进行递归
for(int i = 1 ; i <= n ; i ++ )
for(int j = 1 ; j <= m ; j ++ )
// 取从哪个点开始能滑长度最长
res = Math.max(res,dp(i,j)); System.out.println(res); } public static int dp(int x,int y){ // 如果已经计算过了,就直接返回结果
if(f[x][y] != -1) return f[x][y]; // 最开始只包括自己一个点,f[x][y] = 1
f[x][y] = 1; // 向四个方向遍历
for(int i = 0 ; i < 4 ; i ++ ){ // 移动后的点位
int a = x + dx[i];
int b = y + dy[i]; // 判定条件(边界判定+高度判定)
if(a >= 1 && a <= n && b >= 1 && b <= m && h[a][b] < h[x][y]){
// 进行比较,这里需要对ab点进行dp,若未dp进行dp,若已dp返回其移动距离,加上当前点,然后与最大距离比较
f[x][y] = Math.max(f[x][y],dp(a,b) + 1);
}
} // 返回结果
return f[x][y];
}
}

结束语

好的,关于动态规划篇的DP问题就介绍到这里,希望能为你带来帮助~

动态规划篇——DP问题的更多相关文章

  1. 初探动态规划(DP)

    学习qzz的命名,来写一篇关于动态规划(dp)的入门博客. 动态规划应该算是一个入门oier的坑,动态规划的抽象即神奇之处,让很多萌新 萌比. 写这篇博客的目标,就是想要用一些容易理解的方式,讲解入门 ...

  2. 【学习笔记】动态规划—各种 DP 优化

    [学习笔记]动态规划-各种 DP 优化 [大前言] 个人认为贪心,\(dp\) 是最难的,每次遇到题完全不知道该怎么办,看了题解后又瞬间恍然大悟(TAT).这篇文章也是花了我差不多一个月时间才全部完成 ...

  3. [JSOI2008]Blue Mary的战役地图——全网唯一一篇dp题解

    全网唯一一篇dp题解 网上貌似全部都是哈希+二分(反正我是大概baidu了翻了翻)(还有人暴力AC了的..) 哈希还是相对于dp还是比较麻烦的. 而且正确性还有可能被卡(当然这个题不会) 而且还容易写 ...

  4. Leetcode之动态规划(DP)专题-详解983. 最低票价(Minimum Cost For Tickets)

    Leetcode之动态规划(DP)专题-983. 最低票价(Minimum Cost For Tickets) 在一个火车旅行很受欢迎的国度,你提前一年计划了一些火车旅行.在接下来的一年里,你要旅行的 ...

  5. Leetcode之动态规划(DP)专题-647. 回文子串(Palindromic Substrings)

    Leetcode之动态规划(DP)专题-647. 回文子串(Palindromic Substrings) 给定一个字符串,你的任务是计算这个字符串中有多少个回文子串. 具有不同开始位置或结束位置的子 ...

  6. Leetcode之动态规划(DP)专题-474. 一和零(Ones and Zeroes)

    Leetcode之动态规划(DP)专题-474. 一和零(Ones and Zeroes) 在计算机界中,我们总是追求用有限的资源获取最大的收益. 现在,假设你分别支配着 m 个 0 和 n 个 1. ...

  7. Leetcode之动态规划(DP)专题-486. 预测赢家(Predict the Winner)

    Leetcode之动态规划(DP)专题-486. 预测赢家(Predict the Winner) 给定一个表示分数的非负整数数组. 玩家1从数组任意一端拿取一个分数,随后玩家2继续从剩余数组任意一端 ...

  8. Leetcode之动态规划(DP)专题-264. 丑数 II(Ugly Number II)

    Leetcode之动态规划(DP)专题-264. 丑数 II(Ugly Number II) 编写一个程序,找出第 n 个丑数. 丑数就是只包含质因数 2, 3, 5 的正整数. 示例: 输入: n ...

  9. Leetcode之动态规划(DP)专题-198. 打家劫舍(House Robber)

    Leetcode之动态规划(DP)专题-198. 打家劫舍(House Robber) 你是一个专业的小偷,计划偷窃沿街的房屋.每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互 ...

  10. Leetcode之动态规划(DP)专题-121. 买卖股票的最佳时机(Best Time to Buy and Sell Stock)

    Leetcode之动态规划(DP)专题-121. 买卖股票的最佳时机(Best Time to Buy and Sell Stock) 股票问题: 121. 买卖股票的最佳时机 122. 买卖股票的最 ...

随机推荐

  1. keycloak~资源的远程授权

    17.1远程资源授权准备 17.1.1认证和访问流程图 参考:http://www.zyiz.net/tech/detail-141309.html 17.1.2为用户指定角色 可以使用ROLE_US ...

  2. Elasticsearch:跨集群搜索 Cross-cluster search(CCS)及安全

    文章转载自:https://elasticstack.blog.csdn.net/article/details/116569527

  3. 6.监控elasticsearch集群---放弃采用(获取不到数据),建议看另一篇文章:监控elasticsearch

    prometheus监控es,同样采用exporter的方案. 项目地址: elasticsearch_exporter:https://github.com/justwatchcom/elastic ...

  4. Java 读写锁 ReadWriteLock 原理与应用场景详解

    Java并发编程提供了读写锁,主要用于读多写少的场景,今天我就重点来讲解读写锁的底层实现原理@mikechen 什么是读写锁? 读写锁并不是JAVA所特有的读写锁(Readers-Writer Loc ...

  5. 洛谷P1216 [USACO1.5][IOI1994]数字三角形 Number Triangles (DP入门)

    考虑逆推就行了. 1 #include<bits/stdc++.h> 2 using namespace std; 3 int n; 4 int a[1010][1010]; 5 int ...

  6. uni-app 如何优雅的使用权限认证并对本地文件上下起手

    这里给大家分享我在网上总结出来的一些知识,希望对大家有所帮助 1.起因 最近有一个需求,需要使用自定义插件,来对接硬件功能,需要配合对手机的权限进行判断和提示,并在对接后对本地文件进行操作,这里给大家 ...

  7. elementUi使用dialog的进行信息的添加、删除表格数据时进行信息提示。删除或者添加成功的信息提示(SpringBoot+Vue+MybatisPlus)

    文章目录 1.添加新用户,通过dialog的弹窗形式 1.1 添加的按钮 1.2 调用方法设置窗口可见 1.3 窗口代码 1.4 提交注册信息方法 1.5 使用mybatisPlus方法进行添加信息到 ...

  8. 齐博x1内容评论标签的风格制作

    评论的标签如下: {qb:comment name="xxxxx" rows='5'} HTML代码片段 {/qb:comment} 评论涉及到的元素有{posturl} 这个是代 ...

  9. 知识图谱-生物信息学-医学论文(Chip-2022)-BCKG-基于临床指南的中国乳腺癌知识图谱的构建与应用

    16.(2022)Chip-BCKG-基于临床指南的中国乳腺癌知识图谱的构建与应用 论文标题: Construction and Application of Chinese Breast Cance ...

  10. MIPI-DSI协议

    MIPI联盟,即移动产业处理器接口(Mobile Industry Processor Interface 简称MIPI)联盟.MIPI(移动产业处理器接口)是MIPI联盟发起的为移动应用处理器制定的 ...