记录结果再利用的"动态规划"
2018-09-24 15:01:37
动态规划(DP: Dynamic Programming)是算法设计方法之一,在程序设计竞赛中经常被选作题材。在此,我们考察一些经典的DP问题,来看看DP究竟是何种类型的算法。
一、01背包问题
问题描述:
有n个重量和价值分别为wi,vi的物品。从这些物品中挑选出总重量不超过W的物品,求所有挑选方案中价值总和的最大值。
限制条件:
1<=n<=100
1<=wi,vi<=100
1<=W<=10000
问题求解:
这是被称为背包问题的一个著名问题。这个问题要如何求解比较好呢?不妨先用最朴素的方法,针对每个物品是否放入背包进行搜索试试看。这个想法的代码如下。
// 暴力搜索
int naiveSearch(int i, int restW) {
if (i == n) return 0; // 已经没有物品了
if (w[i] <= restW)
return Math.max(naiveSearch(i + 1, restW), naiveSearch(i + 1, restW - w[i]) + v[i]);
else return naiveSearch(i + 1, restW);
}
只不过,这种方法的搜索深度是n,而且每一层的搜索都需要两次分支,最坏就需要O(2^n)的时间,当n比较大的时候就没有办法进行求解了。所以需要怎么办呢?为了优化算法,我们可以发现,在递归调用的过程中,有许多状态被重复计算了,因此,如果我们把第一计算得到的值记录下来,那么第二次就不需要进行不必要的计算了。
// 暴力搜索 + 记忆化存储
int naiveSearchPolish(int i, int restW) {
if (i == n) return dp[i][restW] = 0;
if (dp[i][restW] >= 0) return dp[i][restW];
if (w[i] <= restW)
return dp[i][restW] = Math.max(naiveSearch(i + 1, restW), naiveSearch(i + 1, restW - w[i]) + v[i]);
else return dp[i][restW] = naiveSearch(i + 1, restW);
}
这个微小的改进能降低多少时间复杂度呢?对于同样的参数,只会在第一次调用到时需要执行递归部分,第二次之后就可以直接返回。参数组合总共nW种,而函数内只有两次递归,所以只需要O(nW)的复杂度就可以解决这个问题。只需要略微改良,可解的问题规模就可以大幅提高。这种方法一般称为记忆化搜索。
使用记忆化数组自底向上递推的方法称为动态规划,下面我们就来看一下递推式。
dp[i + 1][j] : 从0到i总共i + 1个物品中选出总重量不超过j的物品的总价值最大值。
初始值dp[0][j] = 0。
dp[i + 1][j] = dp[i][j] if w[i] > j
= max(dp[i][j], dp[i][j - w[i]] + v[i]) others
// dp
int dpSolve() {
int[][] dp = new int[n + 1][W + 1];
for (int i = 0; i < n; i++) {
for (int j = 0; j <= W; j++) {
if (w[i] > j) dp[i + 1][j] = dp[i][j];
else dp[i + 1][j] = Math.max(dp[i][j], dp[i][j - w[i]] + v[i]);
}
}
return dp[n][W];
}
二、最长公共子序列问题
问题描述:
给定两个字符串s,t。求出这两个字符串最长的公共子序列的长度。
限制条件:
1<=s.length(),t.length()<=1000
问题求解:
经典的动态规划问题,即LCS。定义递推式如下:
dp[i][j] : s中前i个字符和t中前j个字符的最长公共子序列长度。
初始值:dp[0][j] = 0, dp[i][0] = 0
递推式:dp[i + 1][j + 1] = dp[i][j] + 1 if s[i] == t[j]
dp[i + 1][j + 1] = max(dp[i + 1][j], dp[i][j + 1]) others
public int LCS(String s, String t) {
if (s.length() == 0 || t.length() == 0) return 0;
int len1 = s.length();
int len2 = t.length();
int[][] dp = new int[len1 + 1][len2 + 1];
for (int i = 0; i < len1; i++) {
for (int j = 0; j < len2; j++) {
if (s.charAt(i) == t.charAt(j)) dp[i + 1][j + 1] = dp[i][j] + 1;
else dp[i + 1][j + 1] = Math.max(dp[i + 1][j], dp[i][j + 1]);
}
}
return dp[len1][len2];
}
三、完全背包问题
问题描述:
有n种重量和价值分别为wi,vi的物品。从这些物品中挑选总重量不超过W的物品,求出挑选物品价值总和的最大值。在这里,每种物品可以挑选任意多件。
限制条件:
1 <= n <= 100
1 <= wi, vi <= 100
1 <= W <= 10000
问题求解:
经典的动态规划问题,首先我们可以先定义一下相关递推公式的含义。
dp[i + 1][j] : 挑选前i件物品在背包容量为j的情况下能够达到的总和最大值
dp[0][j] = 0
dp[i + 1][j] = max ( dp[i][j - k * w[i]] + k * v[i]) k = 0,1,...,j / w[i]
显然,这种递推式子需要三重循环进行求解,那么其时间复杂度就是O(n * W ^ 2)。
一般来说这种递推式都是可以进行简化的,这里介绍一下简化的思路,具体来说就是,建立dp[i + 1][j] 和 dp[i + 1][j - w[i]]之间的联系,显然的,这两者的递推公式很多项都是重合的,因此,我们可以使用dp[i + 1][j - w[i]]来对dp[i + 1][j]进行表示。
进行优化之后的递推公式为:
dp[0][j] = 0
dp[i + 1][j] = max (dp[i][j], d[i + 1][j - w[i]] + v[i])
这样的话,三层的循环就可以降到二重,因此时间复杂度依然是O(nW)。
int completeKnapsack(int[] w, int[] v, int W) {
int n = w.length;
int[][] dp = new int[n + 1][W + 1];
for (int i = 0; i < n; i++) {
for (int j = 0; j <= W; j++) {
if (w[i] > j) dp[i + 1][j] = dp[i][j];
else dp[i + 1][j] = Math.max(dp[i][j], dp[i + 1][j - w[i]] + v[i]);
}
}
return dp[n][W];
}
四、01背包问题之2
问题描述:
有n个重量和价值分别为wi,vi的物品。从这些物品中挑选总重量不超过W的物品,求所有挑选方案中价值总和的最大值。
限制条件:
1 <= n <= 100
1 <= wi <= 10^7
1 <= vi <= 100
1 <= W <= 10 ^ 9
问题求解:
乍一看,似乎没有什么不同,但是限制条件其实是有了变化,如果依然使用最初的01背包的模板,那么本题是会TLE的,但是在看vi的值都是非常小的,因此这里我们可以变换一下递推公式的含义。
dp[i + 1][j] : 挑选前i件物品获得价值j的最小重量
dp[0][0] = 0
dp[0][j] = INF
dp[i + 1][j] = min (dp[i][j], dp[i][j - v[i]] + w[i])
int extendDp(int[] w, int[] v, int W) {
int n = w.length;
int[][] dp = new int[n + 1][100 * 100 + 1];
for (int i = 1; i < dp[0].length; i++) dp[0][i] = 10000; // 不要使用MAX_VALUE,会爆掉
for (int i = 0; i < n; i++) {
for (int j = 0; j < dp[0].length; j++) {
if (v[i] > j) dp[i + 1][j] = dp[i][j];
else dp[i + 1][j] = Math.min(dp[i][j], dp[i][j - v[i]] + w[i]);
}
}
int res = 0;
for (int i = 0; i < dp[0].length; i++) if (dp[n][i] <= W) res = i;
return res;
}
五、多重部分和问题
问题描述:
有n种不同大小的数字ai,每种各mi个。判断是否可以从这些数字中选出若干个使得他们的和恰好为K。
限制条件:
1 <= n <= 100
1 <= ai, mi <= 100000
1 <= K <= 100000
问题求解:
显然的,本题和完全背包有点类似的,唯一的区别就是不再是无限个数的数字可以获得,而是加上了限制,但是递推式还是差不多的嘛。
dp[i + 1][j] : 取i + 1个数字(也就是取前i个数字)求和得到j的真假。
dp[0][0] = true
dp[0][j] = false
dp[i + 1][j] = dp[i][j] | dp[i][j - a[i]] | ... | dp[i][j - k * a[i] 0 <= k <= min(mi, j / ai)
这种解法可以看作比较朴素的动态规划的解法,事实上,这里的时间复杂度为O(nKm),理论上是会超时的。
一般用DP求取bool结果的话,会有不少的浪费,同样的时间复杂度往往能够获得更多的信息。
在这个问题中,我们不光能够求出能否得到目标的数字,同时还可以把得到时ai剩余的个数给计算出来,这样就可以减少时间复杂度。
dp[i + 1][j] : 用前i中数字相加和得到j时第i种数字最多能够剩余多少(不能得到j的情况下为 - 1)
dp[i + 1][j] = mi if dp[i][j] >= 0
-1 if j < ai | dp[i + 1][j - ai] <= 0
dp[i + 1][j - ai] - 1 others
boolean multiSum(int[] a, int[] m, int K) {
int n = a.length;
int[] dp = new int[K + 1];
Arrays.fill(dp, -1);
dp[0] = 0;
for (int i = 0; i < n; i++) {
for (int j = 0; j <= K; j++) {
if (dp[j] >= 0) dp[j] = m[i];
else if (j < a[i] || dp[j - a[i]] <= 0) dp[j] = -1;
else dp[j] = dp[j - a[i]] -1;
}
}
return dp[K] >= 0;
}
六、最长上升子序列问题
问题描述:
有一个长为n的数列,请求出这个序列中最长上升的子序列的长度。上升子序列是指对于任意i < j都满足ai < aj的子序列。
限制条件:
1 <= n <= 1000
0 <= ai <= 1000000
问题求解:
经典的动态规划问题,LIS。
朴素的O(n^2)的解法应该是非常容易想到的,这里就不做讲解了。LIS的最优解法的时间复杂度是O(nlogn)。
dp[i] : 长度为i + 1的上升子序列中末尾元素的最小值,不存在的话为INF。
每次寻找i的lowerBound作为插入点,最后寻找INF的lowerBound即可。
public int lengthOfLIS(int[] nums) {
int[] dp = new int[nums.length];
Arrays.fill(dp, Integer.MAX_VALUE);
for (int i : nums) {
int idx = lowerBound(dp, i);
dp[idx] = i;
}
return lowerBound(dp, Integer.MAX_VALUE);
} private int lowerBound(int[] nums, int k) {
int lb = -1;
int ub = nums.length;
while (ub - lb > 1) {
int mid = lb + (ub - lb) / 2;
if (nums[mid] >= k) ub = mid;
else lb = mid;
}
return ub;
}
七、划分数
问题描述:
有n个无区别的物品,将他们划分成不超过m组,求出划分方法数模M的余数。
限制条件:
1 <= m <= n < =1000
2 <= M <= 10000
问题求解:
dp[i][j] : j的i划分的总数
显然i > j的时候,dp[i][j] = dp[i - 1][j]
i < j的时候,dp[i][j] = dp[i][j - i] + dp[i - 1][j],这个公式的含义是,如果划分结果为i组,如果全部非0,那么就等于dp[i][j - i]的划分数个数,如果有为0的情况,那么就可以使用dp[i - 1][j]来计算。
int partitionNums(int n, int m) {
int[][] dp = new int[m + 1][n + 1];
dp[0][0] = 1;
for (int i = 1; i <= m; i++) {
for (int j = 0; j <= n; j++) {
if (i > j) dp[i][j] = dp[i - 1][j];
else dp[i][j] = dp[i - 1][j] + dp[i][j - i];
}
}
return dp[m][n];
}
八、多重集组合数
问题描述:
有n种物品,第i中物品有ai个。不同种类的物品可以相互区分但相同种类的无法区分。从这些物品中取出m个话,有多少种取法?求出方案数模M的余数。
限制条件:
1 <= n <= 1000
1 <= m <= 1000
1 <= ai <= 1000
2 <= M <= 10000
问题求解:
dp[i + 1][j] : 从前i种物品中取出j个的组合总数
dp[i + 1][j] = sum(dp[i][j - k]) 0 <= k <= min(ai, j)
和完全背包类似,这里也可以尝试建立dp[i + 1][j]和dp[i + 1][j - 1]之间的联系,将递推公式进行化简得到:
dp[i + 1][j] = dp[i + 1][j - 1] + dp[i][j] - dp[i][j - 1 - ai]
这样复杂度就降到O(nm)了。
int dpSolve(int[] a, int m, int mod) {
int n = a.length;
int[][] dp = new int[n + 1][m + 1];
Arrays.fill(dp[0], 0);
for (int i = 0; i <= n; i++) dp[i][0] = 1;
for (int i = 0; i < n; i++) {
for (int j = 1; j <= m; j++) {
// 有取余的情况下,要避免减法运算的结果为负数
if (j - 1 - a[i] >= 0) {
dp[i + 1][j] = (dp[i + 1][j - 1] + dp[i][j] - dp[i][j - 1 - a[i]] + mod) % mod;
}
else {
dp[i + 1][j] = (dp[i + 1][j - 1] + dp[i][j]) % mod;
}
}
}
return dp[n][m];
}
记录结果再利用的"动态规划"的更多相关文章
- 记录结果再利用的"动态规划"之背包问题
参考<挑战程序设计竞赛>p51 https://www.cnblogs.com/Ymir-TaoMee/p/9419377.html 01背包问题 问题描述:有n个重量和价值分别为wi.v ...
- poj 2385 Apple Catching(记录结果再利用的动态规划)
传送门 https://www.cnblogs.com/violet-acmer/p/9852294.html 题意: 有两颗苹果树,在每一时刻只有其中一棵苹果树会掉苹果,而Bessie可以在很短的时 ...
- NOIP 提高组 2014 飞扬的小鸟(记录结果再利用的DP)
传送门 https://www.cnblogs.com/violet-acmer/p/9937201.html 参考资料: [1]:https://www.luogu.org/blog/xxzh242 ...
- poj 2229 Sumsets(记录结果再利用的DP)
传送门 https://www.cnblogs.com/violet-acmer/p/9852294.html 题意: 将一个数N分解为2的幂之和共有几种分法? 题解: 定义dp[ i ]为数 i 的 ...
- cocos2d 缓存池 对象的再利用
1.简单的叙述说明池 例如,我们知道,游戏的游戏类型跑酷,游戏元素都在不断重复.游戏的内容将继续从屏幕右侧的创建,当元件在屏幕的左侧的,将消失.假设不变new 对象.release 对象 性能影响.怎 ...
- DELPHI XE10,JSON 生成和解析,再利用INDYHTTP控件POST
Delphi XE10,Json 生成和解析,再利用indyhttp控件Post 年09月20日 :: 阅读数: --不多说,直接上代码 procedure TFrmMain.Brand; var J ...
- Dual Path Networks(DPN)——一种结合了ResNet和DenseNet优势的新型卷积网络结构。深度残差网络通过残差旁支通路再利用特征,但残差通道不善于探索新特征。密集连接网络通过密集连接通路探索新特征,但有高冗余度。
如何评价Dual Path Networks(DPN)? 论文链接:https://arxiv.org/pdf/1707.01629v1.pdf在ImagNet-1k数据集上,浅DPN超过了最好的Re ...
- 知识图谱-生物信息学-医学论文(BMC Bioinformatics-2022)-挖掘阿尔茨海默病相关KG来确定潜在的相关语义三元组用于药物再利用
论文标题: Mining On Alzheimer's Diseases Related Knowledge Graph to Identity Potential AD-related Semant ...
- python+opencv选出视频中一帧再利用鼠标回调实现图像上画矩形框
最近因为要实现模板匹配,需要在视频中选中一个目标,然后框出(即作为模板),对其利用模板匹配的方法进行检测.于是需要首先选出视频中的一帧,但是在利用摄像头读视频的过程中我唯一能想到的方法就是: 1.在视 ...
随机推荐
- Bitbucket备份恢复
我们需要备份什么? home directory:contains repository data, log files, plugins, and so on. database:contains ...
- centos 设置时间为北京时间
https://www.cnblogs.com/biaopei/p/7730462.html
- C语言动态链表数据结构实现的学生信息项目
注:此项目来源于吕鑫老师的教程 #define _CRT_SECURE_NO_WARNINGS #include <iostream> #include <conio.h> u ...
- C语言实现随机生成0~100的数
#include <iostream> #include <time.h> int main() { srand((unsigned)time(NULL));//srand() ...
- 第一次怎么把本地git仓库的内容push到远程仓库?
使用git push origin <分支名> -f 这种方式可以用本地仓库的内容覆盖远程仓库.
- 关于no matching key exchange method found. Their offer: diffie-hellman-group1-sha1的解决办法
原文链接:https://mycyberuniverse.com/error/no-matching-key-exchange-method-found-openssh7.html What caus ...
- Maven集成Tomcat插件
目录 类似插件及版本区别: 本地运行,启动嵌入式tomcat: 错误一: 错误二: Idea运行调试: vscode运行调试: 远程部署: 项目中的pom.xml配置: Tomcat中的tomcat- ...
- vue 弹性布局 实现长图垂直居上,短图垂直居中
vue 弹性布局 实现长图垂直居上,短图垂直居中 大致效果如下图,只考虑垂直方向.长图可以通过滚动条看,短图居中效果,布局合理 html代码(vue作用域内): <div class=" ...
- SQL语句执行的顺序机制
From Where Group by Having Select 表达式 Distinct ORDER BY TOP/OFFSET-FETCH
- Google advertiser api开发概述
对象.方法和服务 AdWords API 主要供 AdWords 的高级用户使用.如果您是 AdWords 新手,或需要复习 AdWords 基本概念,请查看 AdWords 基础知识页面. 对象层级 ...