subsequence 1(牛客多校第五场记忆化搜索+组合数学)

本文解析一道算法竞赛题目,涉及字符串处理、动态规划及组合数学,讲解如何计算给定字符串中大于另一字符串的有效子序列数量,并提供代码实现。

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

链接:https://ac.nowcoder.com/acm/contest/885/G
来源:牛客网

题目描述
You are given two strings s and t composed by digits (characters ‘0’ \sim∼ ‘9’). The length of s is n and the length of t is m. The first character of both s and t aren’t ‘0’.

Please calculate the number of valid subsequences of s that are larger than t if viewed as positive integers. A subsequence is valid if and only if its first character is not ‘0’.
Two subsequences are different if they are composed of different locations in the original string. For example, string “1223” has 2 different subsequences “23”.

Because the answer may be huge, please output the answer modulo 998244353.
输入描述:
The first line contains one integer T, indicating that there are T tests.

Each test consists of 3 lines.

The first line of each test contains two integers n and m, denoting the length of strings s and t.

The second line of each test contains the string s.

The third line of each test contains the string t.

  • 1 \le m \le n \le 30001≤m≤n≤3000.

  • sum of n in all tests \le 3000≤3000.

  • the first character of both s and t aren’t ‘0’.

输出描述:
For each test, output one integer in a line representing the answer modulo 998244353.
示例1
输入
复制
3
4 2
1234
13
4 2
1034
13
4 1
1111
2
输出
复制
9
6
11
说明
For the last test, there are 6 subsequences “11”, 4 subsequcnes “111” and 1 subsequence “1111” that are valid, so the answer is 11.
当时在写B题的时候队友就把这个题讨论完了。。结束之后又看了看,觉得挺好的一个题目。分两种情况,一种是和下面的字符串长度相等的时候,另一种是长于下面字符串的时候。第一种时候我们可以直接写出来,但是第二种的时候,我们就需要讨论了,因为有时候在某一位的数字是一样的。。可以用dp来做,但是我们队的dp实在是不咋样。就用搜索了。一开始就是暴力搜索,T了一发之后改的记忆化搜索。还有用c++11交的时候溢出了,用c++14过得。
代码如下:

#include<stdio.h>
#include<algorithm>
#include<cstring>
using namespace std;
char s[4005];
char s1[4005];
typedef long long ll;
const ll mod=998244353;
 
//组合数打表模板,适用于N<=3000
//c[i][j]表示从i个中选j个的选法。
long long C[4005][4005];
 
void get_C()
{
    C[0][0] = 1;
    for(int i=1;i<=3000;i++)
    {
        C[i][0] = 1;
        for(int j=1;j<=i;j++)
            C[i][j] = (C[i-1][j]+C[i-1][j-1])%mod;
    }
}
ll power(ll a,ll b)
{
    ll ans=1;
    ll res=a%mod;
    while(b)
    {
        if(b&1) ans*=res,ans%=mod;
        res*=res;
        res%=mod;
        b>>=1;
    }
    return ans%mod;
}
ll n,m;
ll dp[4050][4050];
inline ll check(ll pos,ll cnt)
{
    ll sum=0;
    if(dp[pos][cnt]!=-1) return dp[pos][cnt]%mod;
    for(int i=pos;i<=n-(m-cnt);i++)
	{
        if(s[i]>s1[cnt])
		{
            sum=sum+C[n-i][m-cnt]%mod;
            sum=sum%mod;
        }
        else if(s[i]==s1[cnt])
		{
            sum=sum+check(i+1,cnt+1)%mod;
            sum=sum%mod;
        }
    }
    return dp[pos][cnt]=sum%mod;
}
int main()
{
    int t;
    scanf("%d",&t);
    get_C();
    while(t--)
	{
        ll sum=0;
        scanf("%lld%lld",&n,&m);
        for(int i=1;i<=n;i++)
		{
            for(int j=1;j<=n;j++)
			{
                dp[i][j]=-1;
            }
        }
        scanf("%s",s+1);
        scanf("%s",s1+1);
        ll ans=0;
        for(int i=1;i<=n;i++) if(s[i]=='0') ans++;
        if(ans!=0)
		{
            for(int i=m+1;i<=n;i++)
			{
            	for(int j=1;j<=n;j++)
				{
                	if(s[j]!='0')
					{
	                    sum=sum+C[n-j][i-1]%mod;
	                    sum=sum%mod;
                	}
            	}
            	sum=sum%mod;
        	}
        }
        else
		{
            for(int i=m+1;i<=n;i++)
			{
	            sum=sum+C[n][i]%mod;
	            sum=sum%mod;
        	}
    	}
        sum=sum+check(1L,1L)%mod;
        printf("%lld\n",sum%mod);
    }
}

努力加油a啊,(o)/~

<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
发出的红包

打赏作者

starlet_kiss

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值