线性 DP
最长上升子序列问题是一个经典的线性动态规划问题。
例题:B3637 最长上升子序列
分析:设原始数组为
考虑如何进行状态转移,也就是寻找一个递推关系,用之前计算过的某些
最终的答案就是所有
参考代码
#include <cstdio> #include <algorithm> using std::max; const int N = 5005; int a[N], dp[N]; int main() { int n; scanf("%d", &n); for (int i = 1; i <= n; i++) scanf("%d", &a[i]); int ans = 0; for (int i = 1; i <= n; i++) { dp[i] = 1; for (int j = 1; j < n; j++) { if (a[j] < a[i]) dp[i] = max(dp[i], dp[j] + 1); } ans = max(ans, dp[i]); } printf("%d\n", ans); return 0; }
还有一个时间复杂度更低的做法。用
在一开始,只考虑
假设数组
下一个数是
下一个数是
下一个数是
到目前为止,大概可以总结出一个算法。一个接一个地考虑数组
例如,下一个考虑的数是
同理,对于
最终,最长上升子序列的长度是
分析一下这个做法的时间复杂度,对于每个
实际上,可以发现
参考代码
#include <cstdio> #include <algorithm> using std::max; using std::lower_bound; const int N = 5005; int a[N], dp[N]; int main() { int n; scanf("%d", &n); for (int i = 1; i <= n; i++) scanf("%d", &a[i]); int ans = 0; // 记录最长上升子序列的长度 for (int i = 1; i <= n; i++) { // 在dp[1]~dp[ans]间进行二分查找 int idx = lower_bound(dp + 1, dp + ans + 1, a[i]) - dp; if (idx > ans) ans++; // 可以接在dp数组最后一个有效元素后面,长度加1 dp[idx] = a[i]; // 将二分出的位置替换为a[i] } printf("%d\n", ans); return 0; }
例题:P1020 [NOIP1999 提高组] 导弹拦截
分析:先考虑第
题目第
参考代码
#include <cstdio> #include <algorithm> using std::lower_bound; using std::upper_bound; const int N = 100005; int a[N], dp[N]; int main() { int n = 0, x; while (scanf("%d", &x) != -1) { a[++n] = x; } // 第1问 // 求最长不上升子序列的长度,相当于倒过来求最长不下降子序列的长度 int ans = 0; for (int i = n; i >= 1; i--) { // 注意:最长上升子序列是lower_bound,最长不下降子序列是upper_bound int idx = upper_bound(dp + 1, dp + ans + 1, a[i]) - dp; if (idx > ans) ans++; dp[idx] = a[i]; } printf("%d\n", ans); // 第2问 // 等价于求最长上升子序列的长度 ans = 0; for (int i = 1; i <= n; i++) { int idx = lower_bound(dp + 1, dp + ans + 1, a[i]) - dp; if (idx > ans) ans++; dp[idx] = a[i]; } printf("%d\n", ans); return 0; }
例题:最长公共子序列
给出两个字符串,求最长的这样的子序列,要求满足子序列的每个字符都能在两个原字符串中找到,而且每个字符的先后顺序和原字符串中的先后顺序一致。
例如,两个字符串分别是abcfbc
和abfcab
,它们的最长公共子序列长度是,如 abfc
。
设两个字符串分别为
这个状态定义,还是遵循最优子结构的思想。要解决的是两个比较长的字符串之间的问题,对两个字符串各自截取前若干个字符形成的子串,看看子串里面的答案能否计算出来。如果能,把子串延长一些,看看能否转移,最终计算出的
状态转移方程:
考虑两个子串的最后一位
若两个子串的最后一位
考虑边界情况,容易发现
总的时间复杂度是
例题:AT_dp_f LCS
分析:本题需要在求最长公共子序列时把这个序列找出来。一个直观的想法是:除了记录每个状态的最长公共子序列的长度,再配一个相应的数组记录每个状态对应的字符串。状态转移时,除了转移长度,也转移相应的字符串。由于涉及到大量的字符串复制,这个做法比较慢,并且要占用很大的空间。
另一个思路是,记录每个状态是转移自前面的哪个状态的,也就是记录每个状态的父亲状态。在状态转移方程中,可以看到,对于
参考代码
#include <cstdio> #include <cstring> const int N = 3005; char s[N], t[N], ans[N]; int dp[N][N], from[N][N]; int main() { scanf("%s%s", s + 1, t + 1); int lens = strlen(s + 1), lent = strlen(t + 1); for (int i = 1; i <= lens; i++) { for (int j = 1; j <= lent; j++) { if (s[i] == t[j]) { dp[i][j] = dp[i - 1][j - 1] + 1; from[i][j] = 0; } else { if (dp[i - 1][j] > dp[i][j - 1]) { dp[i][j] = dp[i - 1][j]; from[i][j] = 1; } else { dp[i][j] = dp[i][j - 1]; from[i][j] = 2; } } } } int x = lens, y = lent; int n = 0; while (x > 0 && y > 0) { if (from[x][y] == 0) { // 转移来源标记等于0表示是一次公共字符 ans[++n] = s[x]; x--; y--; } else if (from[x][y] == 1) { x--; } else { y--; } } for (int i = n; i >= 1; i--) printf("%c", ans[i]); return 0; }
习题:P9753 [CSP-S 2023] 消消乐
解题思路(35 分)
对于一个固定的字符串,怎么判断它“可消除”?
可以采用类似括号匹配的方法:维护一个栈,按顺序遍历字符串,若当前字符等于栈顶,则将栈顶弹出,否则将当前字符入栈。如果最终栈为空则说明整个串是“可消除的”。
因此最直接的做法就是枚举所有的子串,对每个子串用一个栈来模拟这个过程,验证是否“可消除”。
时间复杂度为
参考代码
#include <cstdio> #include <stack> using std::stack; using ll = long long; const int N = 2000005; char s[N]; int main() { int n; scanf("%d", &n); scanf("%s", s + 1); ll ans = 0; for (int i = 1; i <= n; i++) { for (int j = i; j <= n; j++) { // 子串i~j stack<char> stk; for (int k = i; k <= j; k++) { if (!stk.empty() && stk.top() == s[k]) stk.pop(); else stk.push(s[k]); } if (stk.empty()) ans++; } } printf("%lld\n", ans); return 0; }
解题思路(50 分)
在前面那个做法中可以发现,考虑对于子串
时间复杂度为
参考代码
#include <cstdio> #include <stack> using std::stack; using ll = long long; const int N = 2000005; char s[N]; int main() { int n; scanf("%d", &n); scanf("%s", s + 1); ll ans = 0; for (int i = 1; i <= n; i++) { stack<char> stk; for (int j = i; j <= n; j++) { // 子串i~j if (!stk.empty() && stk.top() == s[j]) stk.pop(); else stk.push(s[j]); if (stk.empty()) ans++; } } printf("%lld\n", ans); return 0; }
解题思路
分析数据范围,站在常见的线性 DP 问题视角思考这个问题。
设
那么对于每个
设
考虑如何计算
这个做法的时间复杂度是
如何证明这个时间复杂度?可以参考 暴力跳做法的复杂度证明,可以证明每一个位置最多被后面
参考代码
#include <cstdio> #include <stack> using std::stack; using ll = long long; const int N = 2000005; char s[N]; int dp[N], last[N]; int main() { int n; scanf("%d%s", &n, s + 1); ll ans = 0; for (int i = 1; i <= n; i++) { int j = i - 1; while (j > 0 && s[j] != s[i]) { j = last[j] - 1; } if (j > 0) { dp[i] = dp[j - 1] + 1; last[i] = j; } ans += dp[i]; } printf("%lld\n", ans); return 0; }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!