我正在尝试解决leetcode上最长增加子序列的问题,我必须在数组中找到最长增加子序列。我正在尝试使用动态编程(O(n ^ 2)复杂度)解决它。
我写了2个函数,这两个函数试图分别解决问题,但是只有一个函数起作用。
我尝试使用与第一个功能完全相同的逻辑来优化第二个功能,但是我很难弄清为什么它不起作用。
以下功能有效:
// WORKS
private int dp(int[] a) {
int[][] dp = new int[a.length+1][a.length+1];
for(int p = a.length-1; p >=0; p--) {
for(int i = a.length-1; i >=0; i--) {
if(i >= a.length)
dp[p][i] = 0;
else if(a[i] > a[p])
dp[p][i] = Math.max(dp[p][i+1], 1 + dp[i][i+1]);
else
dp[p][i] = dp[p][i+1];
}
}
return dp[0][0];
}
在第二个函数中,我尝试通过使用2列而不是完整矩阵来减少所需的空间,因为我们只需要i + 1列的值即可填充i列。但是它不起作用(col 1是一个0的数组),我不知道为什么
// DOES NOT WORK
private int dp_optimized(int[] a) {
int[] col1 = new int[a.length+1];
int[] col2 = new int[a.length+1];
for(int p = a.length-1; p >=0; p--) {
for(int i = a.length-1; i >=0; i--) {
if(i >= a.length)
col1[p] = 0;
else if(a[i] > a[p])
col1[p] = Math.max(col2[p], 1+col2[i]);
else
col1[p] = col2[p];
}
for(int i=0; i< col1.length; i++)
col2[i] = col1[i];
}
return col1[0];
}
这基本上是同一回事吗?为什么功能1起作用而功能2不起作用?
调用这些函数的主要方法如下:
public int lengthOfLIS(int[] nums) {
int[] a = new int[nums.length+1];
int[][] dp = new int[a.length+1][a.length+1];
for(int i = 1; i<nums.length+1; i++)
a[i] = nums[i-1];
a[0] = Integer.MIN_VALUE;
// return dp(a)
return dp_optimized(a);
}
任何帮助将不胜感激。谢谢。
答案 0 :(得分:3)
让我们先确保我们正确地理解复发。
定义:dp [p] [i]存储以下问题的答案: 从具有索引[i,a.length()-1]的元素中选择的整数中最长的递增子序列是什么,还有一个附加约束,即第一个元素必须大于a [p](我们将跟踪最后一个元素通过将其索引存储在变量p中获得的元素)
重复发生: dp [p] [i] 的答案是:
现在让我们讨论代码
N ^ 2内存代码:
我对您的正确代码做了一些修改,让我们看一下。
private int dp(int[] a) {
int[][] dp = new int[a.length+1][a.length+1];
for(int p = a.length-1; p >=0; p--) {
for(int i = a.length-1; i >p; i--) {
dp[p][i] = dp[p][i+1]; // Try to leave the i-th item
if(a[i] > a[p]) // Try to pick the i-th item
dp[p][i] = Math.max(dp[p][i], 1 + dp[i][i+1]);
}
}
return dp[0][1];
}
第一次修改:我删除了以下部分if(i >= a.length) col1[p] = 0;
,该条件将永远无法满足。
第二次修改:内部循环在[p + 1,a.length-1]之间进行迭代,因为 i 必须始终大于 p
第三次修改:而不是返回dp[0][0]
,而是返回dp[0][1]
,其中第一项是原始数组中未包含的且具有较小值的附加元素比任何其他项目。 (dp [0] [1]找出元素[1,a.length-1]的LIS,因为要选择的第一个元素没有限制)
减少内存:
让我们更多地考虑以上代码的dp表。表格上的第一个维度是前一个索引,第二个维度是数组 a 中的起始索引(我们正在尝试从给定前一个p的i开始查找LIS)
要减少dp解决方案的内存,您必须问自己两个问题:
1-可以缩小哪个尺寸?
要回答此问题,您必须分别遍历每个维度,并问自己该维度的当前值是否为 x ,我还依赖于当前维度的其他哪些值?这些值中最远的值使我们知道可以在此维度上进行多少缩减。
让我们将上述技术应用于我们的问题。
维数p::如果p的值为 x ,则在第一个子问题dp[p][i+1]
中,我们不会更改x(这是很好),但是在第二个子问题dp[i][i+1]
中, x 更改为 i ,而 i 在[i] +1,a.length-1],因此,该尺寸是不可约的!
维i::如果i的值是 x ,则在第一个子问题dp[p][i+1]
中,我们依赖于 x + 1 ,在第二个子问题dp[i][i+1]
中,我们还依赖于存储在 x + 1 中的值,这很好,很明显,当 i 是 x 我们只需要存储 x + 1 中的值,我们根本不在乎存储在 x +中的值2 或 x + 3 ...
2-循环的顺序应该是什么?
在进行内存减少时,循环的顺序很重要,在要减少的维度上进行循环的循环必须是最外层的!
当我们的外部循环是在第二维 i 上迭代的循环时,内部循环负责计算给定的dp [p] [ i ]的所有值 i 是常量(换句话说,计算dp表中的完整列),在计算之后,我们准备移至 i-1 由于第 i 列中的所有值均已存储并可以使用,因此在计算第(i-1)列中的所有值后,我们可以移至 i-2 并使用仅存储在第(i-1)列中的答案计算 i-2 的所有答案,并忽略存储在第 i 列中的所有值。
所以让我们重新排列代码循环:
private int dp(int[] a) {
int[][] dp = new int[a.length+1][a.length+1];
for(int i = a.length-1; i>0; i--) {
for(int p = i-1; p>=0; p--) {
dp[p][i] = dp[p][i+1]; // Try to leave the i-th item
if(a[i] > a[p]) // Try to pick the i-th item
dp[p][i] = Math.max(dp[p][i], 1 + dp[i][i+1]);
}
}
return dp[0][1];
}
现在,让我们重新排序和修改您在 dp优化函数中编写的代码:
private int dp_optimized(int[] a) {
int[] col1 = new int[a.length+1];
int[] col2 = new int[a.length+1];
for(int i = a.length-1; i>0; i--) {
for(int p = i-1; p>=0; p--) {
col1[p] = col2[p];
if(a[i] > a[p])
col1[p] = Math.max(col1[p], 1+col2[i]);
}
for(int p=0; p< col1.length; p++){
col2[p] = col1[p];
}
}
return col1[0];
}
完成!