动态规划篇——DP问题
动态规划篇——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问题的更多相关文章
- 初探动态规划(DP)
学习qzz的命名,来写一篇关于动态规划(dp)的入门博客. 动态规划应该算是一个入门oier的坑,动态规划的抽象即神奇之处,让很多萌新 萌比. 写这篇博客的目标,就是想要用一些容易理解的方式,讲解入门 ...
- 【学习笔记】动态规划—各种 DP 优化
[学习笔记]动态规划-各种 DP 优化 [大前言] 个人认为贪心,\(dp\) 是最难的,每次遇到题完全不知道该怎么办,看了题解后又瞬间恍然大悟(TAT).这篇文章也是花了我差不多一个月时间才全部完成 ...
- [JSOI2008]Blue Mary的战役地图——全网唯一一篇dp题解
全网唯一一篇dp题解 网上貌似全部都是哈希+二分(反正我是大概baidu了翻了翻)(还有人暴力AC了的..) 哈希还是相对于dp还是比较麻烦的. 而且正确性还有可能被卡(当然这个题不会) 而且还容易写 ...
- Leetcode之动态规划(DP)专题-详解983. 最低票价(Minimum Cost For Tickets)
Leetcode之动态规划(DP)专题-983. 最低票价(Minimum Cost For Tickets) 在一个火车旅行很受欢迎的国度,你提前一年计划了一些火车旅行.在接下来的一年里,你要旅行的 ...
- Leetcode之动态规划(DP)专题-647. 回文子串(Palindromic Substrings)
Leetcode之动态规划(DP)专题-647. 回文子串(Palindromic Substrings) 给定一个字符串,你的任务是计算这个字符串中有多少个回文子串. 具有不同开始位置或结束位置的子 ...
- Leetcode之动态规划(DP)专题-474. 一和零(Ones and Zeroes)
Leetcode之动态规划(DP)专题-474. 一和零(Ones and Zeroes) 在计算机界中,我们总是追求用有限的资源获取最大的收益. 现在,假设你分别支配着 m 个 0 和 n 个 1. ...
- Leetcode之动态规划(DP)专题-486. 预测赢家(Predict the Winner)
Leetcode之动态规划(DP)专题-486. 预测赢家(Predict the Winner) 给定一个表示分数的非负整数数组. 玩家1从数组任意一端拿取一个分数,随后玩家2继续从剩余数组任意一端 ...
- Leetcode之动态规划(DP)专题-264. 丑数 II(Ugly Number II)
Leetcode之动态规划(DP)专题-264. 丑数 II(Ugly Number II) 编写一个程序,找出第 n 个丑数. 丑数就是只包含质因数 2, 3, 5 的正整数. 示例: 输入: n ...
- Leetcode之动态规划(DP)专题-198. 打家劫舍(House Robber)
Leetcode之动态规划(DP)专题-198. 打家劫舍(House Robber) 你是一个专业的小偷,计划偷窃沿街的房屋.每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互 ...
- Leetcode之动态规划(DP)专题-121. 买卖股票的最佳时机(Best Time to Buy and Sell Stock)
Leetcode之动态规划(DP)专题-121. 买卖股票的最佳时机(Best Time to Buy and Sell Stock) 股票问题: 121. 买卖股票的最佳时机 122. 买卖股票的最 ...
随机推荐
- 干货分享!JAVA诊断工具Arthas在Rainbond上实践~
别再担心线上 Java 业务出问题怎么办了,Arthas 帮助你解决以下常见问题: 这个类从哪个 jar 包加载的?为什么会报各种类相关的 Exception? 我改的代码为什么没有执行到?难道是我没 ...
- Mysqldump 的 的 6 大使用场景的导出命令
Mysqldump 选项解析 场景描述 1. 导出 db1.db2 两个数据库的所有数据. mysqldump -uroot -p -P8635 -h192.168.0.199 --hex-blob ...
- Kubernetes DevOps: Harbor
Harbor 是一个 CNCF 基金会托管的开源的可信的云原生 docker registry 项目,可以用于存储.签名.扫描镜像内容,Harbor 通过添加一些常用的功能如安全性.身份权限管理等来扩 ...
- es证书生成方式
./bin/elasticsearch-certutil ca --pem # 生成一个名字叫做elastic-stack-ca.zip的文件 unzip elastic-stack-ca.zip A ...
- Java线程同步的四种方式详解(建议收藏)
Java线程同步属于Java多线程与并发编程的核心点,需要重点掌握,下面我就来详解Java线程同步的4种主要的实现方式@mikechen 目录 什么是线程同步 线程同步的几种方式 1.使用sync ...
- 掌控(control) 方法记录
掌控(control) 题面描述 公元\(2044\)年,人类进入了宇宙纪元.L国有\(n\)个星球,分别编号为\(1\)到\(n\),每一星球上有一个球长.有些球长十分强大,可以管理或掌控其他星球的 ...
- [Android开发学iOS系列] iOS写UI的几种方式
[Android开发学iOS系列] iOS写UI的几种方式 作为一个现代化的平台, iOS的发展也经历了好几个时代. 本文讲讲iOS写UI的几种主要方式和各自的特点. iOS写UI的方式 在iOS中写 ...
- 一篇文章带你了解网页框架——Vue简单入门
一篇文章带你了解网页框架--Vue简单入门 这篇文章将会介绍我们前端入门级别的框架--Vue的简单使用 如果你以后想从事后端程序员,又想要稍微了解前端框架知识,那么这篇文章或许可以给你带来帮助 温馨提 ...
- 2流高手速成记(之四):SpringBoot整合redis及mongodb
最近很忙,好不容易才抽出了时间,咱们接上回 上次我们主要讲了如何通过SpringBoot快速集成mybatis/mybatis-plus,以实现业务交互中的数据持久化,而这一切都是基于关系型数据库(S ...
- 知识图谱-生物信息学-医学顶刊论文(Bioinformatics-2021)-MSTE: 基于多向语义关系的有效KGE用于多药副作用预测
MSTE: 基于多向语义关系的有效KGE用于多药副作用预测 论文标题: Effective knowledge graph embeddings based on multidirectional s ...