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];
}

最新文章

  1. hibernate UUID问题
  2. OpenCV Template Matching Subpixel Accuracy
  3. Android开发学习笔记:浅谈显示Intent和隐式Intent
  4. 好久没有写博客了,发现Live Writer也更新了
  5. UIScrollView解决无法触发手势
  6. SharePoint部署工具SPSD
  7. Winform知识
  8. Python爬取百度贴吧图片
  9. R6010 -abort() has been called错误分析及其解决方法
  10. redhat6.0 安装ORACLE11GR2过程记录
  11. java 构造器学习笔记
  12. ZooKeeper 入门
  13. Json作为配置文件注意事项
  14. Java基础-Eclipse环境搭建(02)
  15. logstash配置多入多出并互相隔离
  16. 3-html 缩写-地址-文字方向-引用块-题注的格式
  17. IIC_slaver 的仿真之路
  18. LeetCode题解之 Subtree of Another Tree
  19. Can&#39;t get Kerberos realm
  20. Hadoop2.2.0分布式安装配置详解[2/3]

热门文章

  1. easyui以及js前端开发常见问题、用法整理(最重要的样式和图标自定义)
  2. 第一章-硬件组成及linux发展历史(1)
  3. 【题解】Luogu P5072 [Ynoi2015]盼君勿忘
  4. 【RMAN】使用RMAN的 Compressed Backupsets备份压缩技术 (转载)
  5. default activity not found的问题
  6. DownAlbum:Chrome的pinterest批量下载插件
  7. Linux 解决 firefox 中文页面乱码问题
  8. K-近邻
  9. WebBrowser获取完整COOKIE
  10. CSS--外发光与内阴影