不知道什么是动态规划的,传送门在这儿:[干货]动态规划十问十答
动态规划进阶:动态规划:从新手到专家
相信看完上面两个链接的博客后,应该对于动态规划有一个新的认识和了解了。接下来就来看看LintCode上DP(下文我将以DP或者Dynamic Programming表示动态规划)的题目。
强烈推荐次博客,将动规分类总结做的很好:动态规划完整笔记
常见的动态规划题类型有好几种,有的动规是可以用滚动数组优化的,以下分别归类介绍:
1 坐标型动态规划题:
state: f[x]表示我从起点走到坐标x, f[x][y] 表示我从起点走到坐标x,y....
function: 研究走到x,y这个点之前的一步
initialize: 起点
answer:终点
给了一些书,让几个人去拷贝,一个人只能拷贝连续的几本书(比如拷贝了第1、2、3本),但是一个人不能跳着拷贝(比如就不能拷贝第1、3本,必须得连续的拷贝才行)。每本书都有不同的页数,翻译一页需要1分钟。
给定一个包含了页数的数组,和翻译员的人数。问你最少需要多少分钟才能把这些书翻译完。
这其实是一道坐标型动态规划题,假设动态规划的方程为dp[m][n],这就代表前m本书被前n个人拷贝所需的最少时间。接下来我们来找出动规的转移方程。为了方便起见,我们假设数组的起始index是从1开始的。
假设书的数组是[3, 2, 4],人数K=2. 那么我们需要求的就是dp[3][2],也就是前3本书被前2个人拷贝所需要的最少时间。我们从后往前倒推,不难得知dp[3][2]是有可能从dp[3][1],max(dp[2][1], pages[3]), max(dp[1][1], pages[2] + pages[3]) 演变而来的。但是这里面可以排除dp[3][1],因为如果前3本书都给一个人翻译了,那第二个人就无用武之地了,所以这样的转换是无意义的。
因此我们可以得到dp[3][2]的转移方程如下:
dp[3][2] = Min(Max(dp[2][1], pages[3]), Max(dp[1][1], pages[2] + pages[3]));
而且不难得知dp[1][n] = dp[1][3] = dp[1][2] = dp[1][[1] = pages[1]; 因为5个人翻译1本书跟1个人翻译一本书的时间是一样的,剩下的4个人没有用武之地,所以毫无意义。
也不难得知dp[n][1] = pages[1] + pages[2] + ... + pages[n]; 因为一个人翻译n本书的时间等于n本书的页数之和。
所以以上两个“不难得知”可以用于初始化dp方程。
我们把dp方程generalize一下如下所示:
DP[m][n] = MIN( MAX(DP[m-1][n-1], pages[m]), MAX(DP[m-2][n-1], pages[m-1]+pages[m]), ... , MAX(DP[1][n-1], pages[2]+pages[3]+...+pages[m]) );
数组长度是N,人数是K,则时间复杂度是O(N * N * K)
public class Solution {
/**
* @param pages: an array of integers
* @param k: an integer
* @return: an integer
*/
public int calSum(int[] sum, int startIndex, int endIndex) {
return sum[endIndex] - sum[startIndex];
}
public int copyBooks(int[] pages, int k) {
if (pages == null || pages.length <= 0 || k < 1) {
return 0;
}
int[][] f = new int[pages.length + 1][k + 1];
int[] sum = new int[pages.length + 1];
sum[0] = 0;
// initialization
f[0][0] = f[1][0] = f[0][1] = 0;
f[1][1] = pages[0];
for (int i = 1; i < f.length; i++) {
f[i][1] = pages[i - 1] + f[i - 1][1];
sum[i] = sum[i - 1] + pages[i - 1];
}
for (int i = 1; i < f[0].length; i++) {
f[1][i] = f[1][1];
}
// DP function
for (int i = 2; i <= pages.length; i++) {
for (int j = 2; j <= k; j++) {
f[i][j] = Integer.MAX_VALUE;
for (int x = 1; x <= i - 1; x++) {
int result = Math.max(f[x][j - 1], calSum(sum, x, i));
f[i][j] = Math.min(result, f[i][j]);
}
}
}
// result
return f[pages.length][k];
}
}
上一道题的变种,还是一个人只能翻译连续的几本书,不过数组变成了每个人翻译一本书所需要的时间了。
Given n books( the page number of each book is the same) and an array of integer with size k means k people to copy the book and the i th integer is the time i th person to copy one book). You must distribute the continuous id books to one people to copy. (You can give book A[1],A[2] to one people, but you cannot give book A[1], A[3] to one people, because book A[1] and A[3] is not continuous.) Return the number of smallest minutes need to copy all the books.
Given n = 4
, array A = [3,2,4]
, .
Return 4
( First person spends 3 minutes to copy book 1, Second person spends 4 minutes to copy book 2 and 3, Third person spends 4 minutes to copy book 4. )
不难得知,dp[1][1] = arr[1] * 1, dp[2][1] = arr[1] * 2, dp[n][1] = arr[1] * n
我们把dp方程generalize一下如下所示:
DP[m][n] = MIN( MAX(DP[m][n-1], arr[n] * 0), MAX(DP[m-1][n-1], arr[n] * 1), ... , MAX(DP[0][n-1], arr[n] * m) );
需要注意的是,如果发现arr[n] * m 已经大于DP[k][n-1]的时候,就不用再继续往下循环了,以此可以减少运行时间。然后还可以用滚动数组来优化空间复杂度。
public int copyBooksII(int n, int[] times) {
return copyBooksWithRollingArray(n, times);
}
public int copyBooksWithRollingArray(int n, int[] times) {
int len = times.length;
// state
int[][] dp = new int[n + 1][2];
// initialize
for (int i = 0; i <= n; i++) {
dp[i][0] = times[0] * i;
}
// dp function
for (int j = 1; j < len; j++) {
for (int i = 1; i <= n; i++) {
int a = j % 2;
int b = 1 - a;
dp[i][a] = Integer.MAX_VALUE;
for (int k = 0; k <= i; k++) {
int tmp = Math.max(dp[i-k][b], times[j] * k);
dp[i][a] = Math.min(tmp, dp[i][a]);
if (dp[i-k][b] <= times[j] * k) {
break;
}
}
}
}
// result
return dp[n][(len-1)%2];
}
public int copyBooksII2D(int n, int[] times) {
int len = times.length;
// state
int[][] dp = new int[n + 1][len + 1];
// initialize
for (int i = 0; i <= n; i++) {
dp[i][1] = times[0] * i;
}
// dp function
for (int i = 1; i <= n; i++) {
for (int j = 2; j <= len; j++) {
dp[i][j] = Integer.MAX_VALUE;
for (int k = 0; k <= i; k++) {
int tmp = Math.max(dp[i-k][j-1], times[j-1] * k);
dp[i][j] = Math.min(tmp, dp[i][j]);
if (dp[i-k][j-1] <= times[j-1] * k) {
break;
}
}
}
}
// result
return dp[n][len];
}
这是一道二维坐标型动态规划。思路: 分析大问题的结果与小问题的相关性 f[i][j] 表示以i和j作为正方形的右下角可以扩展的最大边长
eg: 1 1 1
1 1 1
1 1 1 (traget)
traget 的值与3部分相关:
1. 青蓝色的正方形部分 f[i - 1][j - 1]
2. 紫粉红色 target上面的数组 up[i - 1][j] 即target上面的点 往上延伸能达到的最大长度
3. 红色的target左边的数组 left[i][j - 1]
如果 target == 1
f[i][j] = min (left[i][j - 1], up[i - 1][j], f[i - 1][j - 1]) + 1;
对于left和up数组 可以在intialization的时候用o(n^2)扫描整个矩阵实现!
优化思路1:
因为
f[i - 1][j] = left[i - 1][j]
f[i][j - 1] = up[i][j - 1]
这样 不需要额外的建立left和up数组
if (target == 1)
f[i][j] = min (f[i - 1][j - 1], f[i][j - 1],f[i - 1][j]) + 1;
public int maxSquare(int[][] matrix) {
int ans = 0;
if (matrix == null || matrix.length == 0 || matrix[0].length == 0) {
return ans;
}
int n = matrix.length;
int m = matrix[0].length;
// 状态
int[][] f = new int[n][m];
// 初始化
for (int i = 0; i < n; i++) {
f[i][0] = matrix[i][0];
ans = Math.max(f[i][0], ans);
}
for (int i = 0; i < m; i++) {
f[0][i] = matrix[0][i];
ans = Math.max(f[0][i], ans);
}
// 方程
for (int i = 1; i < n; i++) {
for (int j = 1; j < m; j++) {
if (matrix[i][j] == 1) {
f[i][j] = Math.min(Math.min(f[i-1][j-1], f[i][j-1]), f[i-1][j]) + 1;
} else {
f[i][j] = 0;
}
ans = Math.max(ans, f[i][j]);
}
}
// 结果
return ans * ans;
}
优化思路2:
由于f[i][j]只和前3个结果相关
f[i - 1][j - 1] f[i][j - 1]
f[i - 1][j] f[i][j]
故只需要保留一个2行的数组!!!
列上不能优化,因为2重循环的时候 下列的时候依赖于上列的结果,上列的结果需要保存到计算下列的时候用。
只能在行上滚动,不能行列同时滚动!!!
--------------------
state:
f[i][j] 表示以i和j作为正方形右下角可以扩展的最大边长
function:
if matrix[i][j] == 1
f[i % 2][j] = min(f[(i - 1) % 2 ][j], f[(i - 1) % 2][j - 1], f[i%2][j - 1]) + 1;
initialization:
f[i%2][0] = matrix[i][0]
f[0][j] = matrix[0][j]
answer:
max{f[i%2][j]}
public int maxSquare_rollingArray(int[][] matr