动态规划 dfs序+LIS

题目链接:https://ac.nowcoder.com/acm/contest/368/B

题意

在这里插入图片描述

题解

首先把第二个条件倒过来变成如果在右子树中选一个点,在左子树中选的点都要比它大,这样就相当于 根 < 右子树 < 左子树。可以按照这个顺序搞出一个dfs序列,然后在上面做LIS即可。

二分优化LIS: dp[i]表示长度为i的上升子序列的最后一位数最小是多少。

代码

//https://ac.nowcoder.com/acm/contest/368/B
#include <bits/stdc++.h>
using namespace std;
typedef pair<int,int> pii;
const int maxn = 2e5+5;
vector<int> G[maxn];
int w[maxn],son[2][maxn],dp[maxn];
int a[maxn];
int p;
void dfs(int u) {
    if(u == 0)
        return;
    a[p++] = w[u];
    dfs(son[1][u]);
    dfs(son[0][u]);
}
void solve() {
    dp[0] = a[0];
    int cnt = 0;
    for(int i = 1; i < p; ++i) {
    	if(a[i] > dp[cnt]) dp[++cnt] = a[i];
    	else {
    		int pos = int(lower_bound(dp,dp+cnt+1,a[i])-dp);
    		dp[pos] = a[i];
    	}
    }
    cout << cnt+1 << endl;
}
int main() {
    int n;
    scanf("%d", &n);
    for(int i = 0; i < n; ++i) {
       scanf("%d", &w[i]);
    }
    for(int i = 1; i <= n; ++i) {
        for(int j = 0; j < 2; ++j) {
            int u;
            scanf("%d", &u);
            if(u) {
                son[j][i] = u;
            }
        }
    }
    dfs(1);
    solve();
    return 0;
}
<think>我们被要求回答关于最长不下降子列(Longest Non-decreasing Subsequence, LNDS)的动态规划(DP)和记忆化搜索(DFS)实现方法。根据引用内容,我们可以总结出以下信息: 1. 动态规划是解决最长不下降子列问题的经典方法,通常有两种实现方式:自底向上的递推(DP)和自顶向下的记忆化搜索(DFS+Memoization)。 2. 引用[3]提到了最长不下降子列的优化解法($O(N \log N)$)以及如何输出列,但用户问题主要关注动态规划和记忆化搜索,因此我们重点放在这两种方法上,优化方法可以简要提及。 3. 引用[4]和[5]分别给出了最长上升子列(LIS)和最长下降子列(LDS)的动态规划代码,我们可以参考其思路,但注意问题是最长不下降(即非严格递增,允许相等)。 ### 问题分析 最长不下降子列问题:给定一个列,找到最长的子列,使得这个子列非严格递增(即对于子列中的任意两个相邻元素,有$a_i \leq a_j$,其中$i<j$)。 ### 方法一:动态规划(递推) 设$dp[i]$表示以第$i$个元素结尾的最长不下降子列的长度。 状态转移方程: $$ dp[i] = \max_{\substack{1 \leq j < i \\ a[j] \leq a[i]}} \{ dp[j] \} + 1 $$ 如果不存在这样的$j$,则$dp[i]=1$(即自身作为一个子列)。 最后,整个列的最长不下降子列长度为$\max_{1\leq i \leq n} dp[i]$。 时间复杂度:$O(n^2)$,因为对于每个$i$,需要遍历$i$之前的所有元素。 ### 方法二:记忆化搜索(DFS+Memoization) 记忆化搜索是动态规划的一种自顶向下的实现方式。我们使用递归函数`dfs(i)`来计算以第$i$个元素结尾的最长不下降子列的长度。为了避免重复计算,使用一个数组`memo`来保存已经计算过的结果。 递归函数定义: - `dfs(i)`:返回以第`i`个元素结尾的最长不下降子列的长度。 递归过程: - 对于每个`i`,遍历`j`从`0`到`i-1`,如果`a[j] <= a[i]`,则`dfs(i) = max(dfs(i), dfs(j)+1)`。 - 如果没有任何`j`满足条件,则`dfs(i)=1`。 同时,使用`memo`数组记录已经计算过的`dfs(i)`,避免重复递归。 ### 代码实现 下面分别给出动态规划(递推)和记忆化搜索的代码实现(使用C++,因为引用中的代码是C++)。 #### 动态规划(递推)代码 ```cpp #include <iostream> #include <algorithm> using namespace std; int main() { int n; cin >> n; int a[n], dp[n]; for (int i = 0; i < n; i++) { cin >> a[i]; } int maxLen = 0; for (int i = 0; i < n; i++) { dp[i] = 1; // 初始化,至少包含自己 for (int j = 0; j < i; j++) { if (a[j] <= a[i]) { // 不下降:允许相等 dp[i] = max(dp[i], dp[j] + 1); } } maxLen = max(maxLen, dp[i]); } cout << maxLen << endl; return 0; } ``` #### 记忆化搜索(DFS+Memoization)代码 ```cpp #include <iostream> #include <algorithm> #include <cstring> using namespace std; const int MAX_N = 1000; int a[MAX_N]; int memo[MAX_N]; // memo[i]存储以第i个元素结尾的最长不下降子列长度,初始为-1表示未计算 int dfs(int i) { if (memo[i] != -1) return memo[i]; int res = 1; // 至少包含自身 for (int j = 0; j < i; j++) { if (a[j] <= a[i]) { res = max(res, dfs(j) + 1); } } memo[i] = res; return res; } int main() { int n; cin >> n; for (int i = 0; i < n; i++) { cin >> a[i]; } memset(memo, -1, sizeof(memo)); int maxLen = 0; for (int i = 0; i < n; i++) { maxLen = max(maxLen, dfs(i)); } cout << maxLen << endl; return 0; } ``` ### 优化方法($O(N \log N)$) 引用[3]提到了利用二分查找和单调性优化到$O(N \log N)$的方法。我们维护一个数组`d`,其中`d[i]`表示长度为`i`的最长不下降子列的最后一个元素的最小可能值(注意,这里是最小可能值,这样后续可以扩展更多元素)。由于`d`数组是单调不减的,我们可以使用二分查找来更新。 具体步骤: 1. 初始化一个数组`d`,长度为0(表示没有元素)。 2. 遍历原数组每个元素`a[i]`: - 在`d`数组中查找第一个大于`a[i]`的位置(如果是严格上升则用`lower_bound`,但这里不下降可以用`upper_bound`来允许相等?注意:不下降列允许相等,所以应该用`upper_bound`?实际上,我们通常用`lower_bound`来查找第一个大于等于`a[i]`的位置,但为了维护不下降,我们允许相等,所以这里使用`upper_bound`来查找第一个大于`a[i]`的位置,然后替换该位置上的值。但具体实现时,对于不下降列,我们通常使用非递减的`d`数组,并利用`lower_bound`找到第一个大于等于`a[i]`的位置,然后替换。但注意,这样得到的是最长不下降子列,但长度正确,列不一定实际存在,但长度正确。 - 更准确地说,我们维护的`d`数组是单调不减的。对于当前元素`a[i]`,我们使用`upper_bound`查找第一个大于`a[i]`的位置(因为允许相等,所以相等时我们可以放在相同长度的位置,但为了尽可能延长列,我们希望`d`数组中的值尽可能小,所以当遇到相等的值时,我们也可以替换,因为不会影响后面的扩展)。实际上,标准做法是: 如果`a[i]`大于等于`d`的最后一个元素,则加入`d`(长度+1); 否则,在`d`中找到第一个大于`a[i]`的位置(用`upper_bound`),然后替换为`a[i]`。 这样,数组`d`的长度就是最长不下降子列的长度。 代码示例: ```cpp #include <iostream> #include <algorithm> #include <vector> using namespace std; int main() { int n; cin >> n; vector<int> a(n); for (int i = 0; i < n; i++) { cin >> a[i]; } vector<int> d; // d 将存储最长不下降子列的最后一个元素的最小可能值 for (int i = 0; i < n; i++) { // 找到第一个大于 a[i] 的位置 auto it = upper_bound(d.begin(), d.end(), a[i]); if (it == d.end()) { // 如果没找到大于a[i]的元素,说明a[i]可以接在d后面(因为d中所有元素都<=a[i]) d.push_back(a[i]); } else { // 替换第一个大于a[i]的位置上的值为a[i] *it = a[i]; } } cout << d.size() << endl; return 0; } ``` 注意:这里使用`upper_bound`(找第一个大于`a[i]`的位置)是因为我们允许相等,所以当有相等元素时,我们希望替换掉大于`a[i]`的第一个元素,这样可以保证`d`数组仍然是非递减的。如果使用`lower_bound`(找第一个大于等于`a[i]`的位置),那么当遇到相等元素时,我们就会替换掉相等的元素,这样也是可以的,但是替换相等元素没有意义(因为相等元素不影响长度),所以两种方法都可以,但通常使用`upper_bound`。 但是,对于最长不下降子列,更常见的做法是使用`lower_bound`来维护一个非递减的列,但注意替换条件。实际上,我们也可以这样: ```cpp auto it = lower_bound(d.begin(), d.end(), a[i]+1); // 第一个大于a[i]的位置 ``` 或者直接使用`upper_bound(d.begin(), d.end(), a[i])`,因为`upper_bound`就是找大于`a[i]`的第一个位置。 ### 输出列 引用[3]提到了如何输出列。在$O(N^2)$的DP方法中,我们可以记录前驱节点。具体做法: - 在计算`dp[i]`时,记录使`dp[i]`取得最大值的`j`(即前驱节点)。 - 最后,从整个列中`dp[i]`最大的位置开始,回溯前驱节点,即可得到列。 在$O(N \log N)$方法中,输出列较为复杂,但引用[3]给出了一种方法:在更新`d`数组的同时,记录每个位置对应的原数组中的元素,并记录每个元素的前驱。但通常,我们只要求长度,所以输出列不是必须的。 ### 总结 - 动态规划(递推)和记忆化搜索都可以解决最长不下降子列问题,时间复杂度为$O(n^2)$。 - 优化方法使用二分查找和贪心思想,将时间复杂度降为$O(n \log n)$,但只用于求长度,列输出较复杂。 - 记忆化搜索本质上是动态规划的自顶向下实现,避免了手动设计递推顺,但会有递归开销。 ### 相关问题 根据用户的问题,我们生成了以下相关问题: 1. 最长不下降子列问题中,动态规划方法和记忆化搜索方法在时间复杂度和空间复杂度上有何异同? 2. 如何输出最长不下降子列的具体元素(而不仅仅是长度)? 3. 最长不下降子列的$O(n \log n)$算法是如何利用二分查找和贪心思想的? 4. 动态规划求解最长不下降子列时,如何优化空间复杂度? 5. 在记忆化搜索中,如何避免递归深度过大导致的栈溢出问题?</think>### 最长不下降子列的动态规划与记忆化搜索实现 #### 一、问题定义 给定列 $\{a_1, a_2, \dots, a_n\}$,求最长子列长度,满足 $\forall i<j,\ a_i \leq a_j$(非严格递增)。 #### 二、动态规划(递推)实现 **状态定义**: $dp[i]$ 表示以 $a_i$ 结尾的最长不下降子列长度。 **状态转移方程**: $$ dp[i] = \max_{\substack{1 \leq j < i \\ a_j \leq a_i}} \{ dp[j] \} + 1 $$ 若不存在满足条件的 $j$,则 $dp[i] = 1$。 **最终结果**: $$ \max_{1 \leq i \leq n} dp[i] $$ **代码实现**: ```cpp #include <iostream> #include <algorithm> using namespace std; int main() { int n; cin >> n; int a[n], dp[n]; for (int i = 0; i < n; i++) cin >> a[i]; int maxLen = 0; for (int i = 0; i < n; i++) { dp[i] = 1; // 初始化(至少包含自身) for (int j = 0; j < i; j++) { if (a[j] <= a[i]) { // 允许相等 dp[i] = max(dp[i], dp[j] + 1); } } maxLen = max(maxLen, dp[i]); } cout << maxLen << endl; return 0; } ``` **时间复杂度**:$O(n^2)$,**空间复杂度**:$O(n)$[^1][^5]。 --- #### 三、记忆化搜索(DFS + Memoization)实现 **核心思想**: - 自顶向下递归,通过`memo`数组避免重复计算。 - `dfs(i)` 返回以 $a_i$ 结尾的最长长度。 **递归方程**: $$ dfs(i) = \max_{\substack{0 \leq j < i \\ a_j \leq a_i}} \{ dfs(j) + 1 \} $$ **代码实现**: ```cpp #include <iostream> #include <cstring> #include <algorithm> using namespace std; const int MAX_N = 1010; int a[MAX_N], memo[MAX_N]; // memo[i] 缓存dfs(i)的结果 int dfs(int i) { if (memo[i] != -1) return memo[i]; // 已计算则直接返回 int res = 1; // 最小长度为1(自身) for (int j = 0; j < i; j++) { if (a[j] <= a[i]) { res = max(res, dfs(j) + 1); // 递归子问题 } } memo[i] = res; // 记录结果 return res; } int main() { int n; cin >> n; for (int i = 0; i < n; i++) cin >> a[i]; memset(memo, -1, sizeof(memo)); // 初始化为未计算 int maxLen = 0; for (int i = 0; i < n; i++) maxLen = max(maxLen, dfs(i)); // 对每个位置调用dfs cout << maxLen << endl; return 0; } ``` **时间复杂度**:$O(n^2)$(每个状态计算一次),**空间复杂度**:$O(n)$[^1][^2][^4]。 --- #### 四、两种方法对比 | **特性** | 动态规划(递推) | 记忆化搜索 | |------------------|------------------------|----------------------| | **实现方向** | 自底向上(迭代) | 自顶向下(递归) | | **计算顺** | 显式顺(从前往后) | 隐式顺(递归栈) | | **代码复杂度** | 较低 | 较高(需处理递归) | | **适用场景** | 依赖关系简单 | 依赖关系复杂 | | **空间优化** | 可滚动数组优化 | 难优化 | > **关键区别**:动态规划显式定义计算顺,记忆化搜索通过递归隐式求解,本质都是对状态空间的遍历[^1][^2]。 --- #### 五、优化方法($O(n \log n)$) 通过维护单调数组 + 二分查找: ```cpp #include <iostream> #include <vector> #include <algorithm> using namespace std; int main() { int n; cin >> n; vector<int> a(n), d; // d: 存储长度为i的子列的最小末尾值 for (int i = 0; i < n; i++) cin >> a[i]; for (int i = 0; i < n; i++) { // 找到d中第一个 > a[i] 的位置 auto it = upper_bound(d.begin(), d.end(), a[i]); if (it == d.end()) d.push_back(a[i]); // 可扩展列长度 else *it = a[i]; // 优化d数组的值 } cout << d.size() << endl; // 输出最长长度 return 0; } ``` **原理**:`d` 数组始终保持有,通过二分查找快速定位插入位置[^3]。 --- ### 相关问题 1. **动态规划与分治法的本质区别是什么?** > 动态规划要求子问题重叠且有最优子结构,通过记忆化避免重复计算;分治法子问题独立无重叠[^2]。 2. **如何输出最长不下降子列的具体元素(而不仅是长度)?** > 需在DP过程中记录前驱节点,最终回溯输出完整列[^3]。 3. **$O(n \log n)$ 算法为何能正确求解?其数学依据是什么?** > 依赖于贪心思想:维护的 `d` 数组具有单调性,替换操作保证后续可扩展性[^3]。 4. **记忆化搜索在哪些场景下优于递推动态规划?** > 当状态转移依赖复杂拓扑(如树形DP)时,记忆化更易实现[^1][^4]。 5. **如何将最长不下降子列问题扩展为三维(如时间列分析)?** > 需增加状态维度(如 $dp[i][j]$),但时间复杂度升至 $O(n^2)$ 或更高。 [^1]: 动态规划入门思路: dfs暴力 --> 记忆化搜索 --> 递推DP。 [^2]: 动态规划通过记忆化存储子问题解,减少重复计算。 [^3]: 利用单调性二分查找可将复杂度优化至 $O(n \log n)$。 [^4]: 记忆化搜索本质是递归形式的动态规划。 [^5]: 动态规划通过两层循环实现状态转移。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值