最长不下降子序列 (upper_bound)

本文探讨了在特定条件下的最长非降子序列问题,利用upper_bound算法优化求解过程,通过实例解析并提供代码实现。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

题目:

You are given an array of positive integers a1, a2, ..., an × T of length n × T. We know that for any i > n it is true that ai = ai - n. Find the length of the longest non-decreasing sequence of the given array.

Input

The first line contains two space-separated integers: nT (1 ≤ n ≤ 100, 1 ≤ T ≤ 107). The second line contains n space-separated integers a1, a2, ..., an (1 ≤ ai ≤ 300).

Output

Print a single number — the length of a sought sequence.

Examples

Input

4 3
3 1 4 2

Output

5

Note

The array given in the sample looks like that: 3, 1, 4, 2, 3, 1, 4, 2, 3, 1, 4, 2. The elements in bold form the largest non-decreasing subsequence.

分析:

刚开始做这题的时候,因为t太多,所以我一开始觉得暴力过不了。后来想想,数组里的数最大才100个。也就是这个最优长度应该不会超多100*(t-1)。另外学习到可以用upper_bound求最长不下降子序列。

upper_bound每次在数列中找它最该放置的位置,若这个位置可以接在当前最优的队伍后面则长度++,有点类似于贪心模拟的思想。主要是代码简洁了许多,get。(#^.^#)

代码:

#include<iostream>
#include<stdio.h>
#include<queue>
#include<map>
#include<string.h>
#include<algorithm>
using namespace std;
const int maxn = 1e4 + 10;
int a[120],num[maxn],b[310]={0};
int maxcnt=0,cnt=0;
int dp[maxn];
int main() {
    int n,k;
    while(scanf("%d %d",&n,&k)!=EOF)
    {
          int i,j;maxcnt=0,cnt=0;
          memset(b,0,sizeof(b));
          for(i=1;i<=n;i++)
          {
                  scanf("%d",&a[i]);
                  b[a[i]]++;
                  if( maxcnt<b[a[i]])
                  {
                          
                          maxcnt=b[a[i]];
                  }
          }
          for(i=1;i<=min(n,k);i++)
                for(j=1;j<=n;j++)
          {
                  num[++cnt]=a[j];
          }
          int val=0;
          for(i=1;i<=cnt;i++)
          {
                  int pos=upper_bound(dp,dp+val,num[i])-dp;
                  if(pos==val) dp[val++]=num[i];
                  else
                        dp[pos]=num[i];
          }
          if(k>n)
                val+=(k-n)*maxcnt;
          printf("%d\n",val);
    }

    return 0;
}

 

另附上lower_bound求最长上升子序列的用法

#include<iostream>
#include<stdio.h>
#include<queue>
#include<map>
#include<string.h>
#include<algorithm>
using namespace std;
const int maxn = 1e4 + 10;
int num[maxn];
int dp[maxn];
int main() {
    int n;
    while(scanf("%d",&n)!=EOF)
    {
          int i,j;
          for(i=1;i<=n;i++)
          {
                  scanf("%d",&num[i]);

          }

          int len=1;dp[1]=num[1];
          for(i=2;i<=n;i++)
          {
                  if(num[i]>dp[len]) dp[++len]=num[i];
                  else
                  {
                      j=lower_bound(dp+1,dp+len+1,num[i])-dp;
                      dp[j]=num[i];
                  }

          }

          printf("%d\n",len);
    }

    return 0;
}

最后附上一个大佬的详细讲解:

https://www.cnblogs.com/itlqs/p/5743114.html

<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、付费专栏及课程。

余额充值