最长递增子序列错误答案

Posted

技术标签:

【中文标题】最长递增子序列错误答案【英文标题】:longest increasing subsequence wrong answer 【发布时间】:2022-01-07 18:54:23 【问题描述】:

我为最长递增子序列编写了一个递归解决方案,它工作得非常好。但是当我在相同的代码上应用 dp 时,它给出了不同的答案。 问题链接:https://practice.geeksforgeeks.org/problems/longest-increasing-subsequence-1587115620/1 递归代码:

int LISrecursive(int arr[], int n, int currIndex, int maxVal) 
    if (currIndex == n) 
        return 0;
    
    int included = 0, notIncluded = 0;
    if (arr[currIndex] > maxVal) 
        included = 1 + LISrecursive(arr, n, currIndex + 1, arr[currIndex]);
    
    notIncluded = LISrecursive(arr, n, currIndex + 1, maxVal);

    return max(notIncluded, included);


DP 代码:

int LISdp(int arr[], int n, int currIndex, int maxVal, vector<int> &dp) 
    if (currIndex == n) 
        return 0;
    
    if (dp[currIndex] != -1) return dp[currIndex];
    int included = 0, notIncluded = 0;
    if (arr[currIndex] > maxVal) 
        included = 1 + LISdp(arr, n, currIndex + 1, arr[currIndex], dp);
    
    notIncluded = LISdp(arr, n, currIndex + 1, maxVal, dp);

    return dp[currIndex] = max(notIncluded, included);


int32_t main() 
    int n;
    cin >> n;
    int arr[n];
    vector<int> dp(n, -1);
    for (int i = 0; i < n; i++) 
        cin >> arr[i];
    
    cout << LISrecursive(arr,n,0,-1); 
    cout << LISdp(arr, n, 0 , -1, dp);
    return 0;

我不知道我做错了什么? 对于这个测试用例 6 (n) 6 3 7 4 6 9 (arr[]) 递归代码给出 4 个答案(正确) 但是 DP 代码给出了 3 个答案(不正确)

【问题讨论】:

您的代码中的dp 是什么? dp 是一个初始填充为 -1 的向量。 编译失败。没有在您的 LIS 中声明 dp 并且您没有将 dp 传递给您的 LIS。 @UjjvalUjjval 我稍微编辑了您的代码以使其编译。确保您的代码是minimal reproducible example 考虑输入 arr=5 1 2 3 并使用调试器逐步完成该案例。您的dp 条目正在记住第一个通过的maxVal 的值,这不会给较低的数字提供机会。 【参考方案1】:

当我想到动态规划时,我通常将其分解为两个步骤:

    用“在递归之前包含当前元素”解决递归 再次”与“在再次递归之前不包括当前元素”相比。这正是您对递归解决方案所做的。

    从步骤 1 中获取递归解决方案并添加先前计算结果的缓存以避免重复递归。缓存,可以概念化为一个多维矩阵,它将传递给递归函数的所有非常量变量参数映射到最终结果。

在您的情况下,每个递归步骤都有两个变量,currIndexmaxValan 实际上是整个递归过程中的常量。 递归步骤的非常量参数个数就是你缓存中的维数。所以你需要一个二维表。我们可以使用一个大的二维 int 数组,但这会占用大量内存。我们可以使用嵌套的哈希表对达到同样的效率。

您的主要错误是您的缓存只有一维 - 与currIndex 相比,无论maxVal 的值如何,都缓存结果。另一个错误是使用向量而不是哈希表。您拥有的矢量技术有效,但无法扩展。而当我们添加第二个维度时,内存使用的规模就更差了。

因此,让我们将缓存类型定义为 unordered_map(哈希表),它将 currIndex 映射到另一个哈希表,将 maxVal 映射到递归结果。您也可以使用元组,但 geeksforgeeks 编码网站似乎不喜欢这样。没关系,我们可以这样定义:

typedef std::unordered_map<int, std::unordered_map<int, int>> CACHE;

那么您的 DP 解决方案实际上只是在递归函数顶部将查找插入到 CACHE 中,并在函数底部插入到 CACHE 中。

int LISdp(int arr[], int n, int currIndex, int maxVal, CACHE& cache) 
    if (currIndex == n) 
        return 0;
    

    // check our cache if we've already solved for currIndex and maxVal together
    auto itor1 = cache.find(currIndex);
    if (itor1 != cache.end())
    
        // itor1->second is a reference to cache[currIndex]
        auto itor2 = itor1->second.find(maxVal);
        if (itor2 != itor1->second.end())
        
            // itor2->second is a reference to cache[n][maxVal];
            return itor2->second;
        
    

    int included = 0, notIncluded = 0;
    if (arr[currIndex] > maxVal) 
        included = 1 + LISdp(arr, n, currIndex + 1, arr[currIndex], cache);
    
    notIncluded = LISdp(arr, n, currIndex + 1, maxVal, cache);

    // cache the final result into the 2-d map before returning
    int finalresult = std::max(notIncluded, included);
    cache[currIndex][maxVal] = finalresult; // cache the result
    return finalresult;


然后,使用要求解的输入集的初始调用实际上是将 INT_MIN 作为初始 maxVal 和一个空缓存传递:

int N = 16
int A[N]=0,8,4,12,2,10,6,14,1,9,5,13,3,11,7,15;

CACHE cache;
int result = LISdp(A, N, 0, INT_MIN, cache);

一个小的优化是使 ancache 成为封装您的解决方案的 C++ 类的成员变量,这样就不必为递归的每一步将它们压入堆栈.缓存是通过引用传递的,所以没什么大不了的。

【讨论】:

另外,你能告诉我空间和时间复杂度吗? 你认为空间和时间复杂度是多少?我不是开玩笑的,但你自己评估了什么? 我认为时间和空间复杂度都是O(n^2)。因为有 'n' 个元素,对于每个元素,我们可以有 'n' 个不同的 maxValue。 对于仅递归的解决方案,空间要求为O(N)LISrecursive 函数只会递归到currentIndex == n。因此,递归深度永远不会超过N。每个递归都在堆栈上存储固定数量的字节,并在函数返回并递归备份时有效地释放这些字节。至于运行时,LISrecursive 可能会枚举所有2ⁿ 可能的序列组合。因此,运行时很容易成为O(2ⁿ) 对于动态解决方案,LISdp 可能会存储 N 行的缓存,每列包含数组中的唯一值之一。因此,O(N²) 用于最大空间复杂度。我很想说缓存也使运行时成为O(N²),但我不是 100% 确定。【参考方案2】:

您的代码中有 2 个问题

错误 1

首先在C++中,数组的大小必须是编译时常数。所以,以如下代码sn-ps为例:

int n = 10;
int arr[n]; //INCORRECT because n is not a constant expression

上面的正确写法是:

const int n = 10;
int arr[n]; //CORRECT

同样,以下(您在代码示例中所做的)不正确:

 int n;
 cin >> n;
 int arr[n];// INCORRECT because n is not a constant expression

错误 2

第二在你的函数LISdp,在我看来没有不需要的声明

if (dp[currIndex] != -1) return dp[currIndex];//no need for this statement

你应该删除这个(上面的)语句,程序会产生预期的输出4,如here所示。基本上你还没有想到这一点(LISdp 的工作)。您可以使用调试器查看哪里出错了。

您的代码中可能还有其他问题,但到目前为止我能够发现这两个问题。

【讨论】:

第二个“错误”实际上是 LISdp 的全部要点。如果你把它去掉,你就有与 LISrecursive 相同的功能,使用 O(2**n) 运行时而不是 O(n) @Botje 我知道这是 LISdp 的全部意义所在,但在编写该声明时他犯了一个错误。即使是故意的,错误仍然是错误。 很公平,但 OP 明确询问“我的错误是什么”。告诉他们通过删除该行将 DP 函数简化为简单的递归函数不是很有帮助或指导性的。 @Botje 从根本上说,OP 的两个函数都使用递归。也就是说,两者都是递归函数。仅仅因为他传递了一个额外的参数并以不同的方式命名他的另一个函数并不能使它成为一个非递归函数。通过指出他的程序中存在一些逻辑错误,如果他学会使用调试器,他可以发现这些错误确实算作指出错误,因为很明显问题出在我提到的特定行if (dp[currIndex] != -1) return dp[currIndex]; 这个问题被标记为[动态编程]。这是将指数算法的运行时间减少到多项式时间的众所周知的技术。您没有看到您的答案如何消除了动态编程方面(并且如前所述,恢复为 O(2**n) 而不是 O(n))?

以上是关于最长递增子序列错误答案的主要内容,如果未能解决你的问题,请参考以下文章

笔试题1:最长严格递增子序列

最长递增子序列 && 最大子序列最长递增子序列最长公共子串最长公共子序列字符串编辑距离

O(nlogn)最长递增子序列算法,如何输出所求子序列?

最长递增子序列

[网络流24题] 最长递增子序列

算法 LC 动态规划 - 最大递增子序列