当前位置: 首页 > news >正文

线段树优化DP

不会线段树的,出门左转oi-wiki

正如标题,利用了线段树的区间查询的特点,可以将区间查询优化到 \(log\) 级别。

例题:P3970 [TJOI2014] 上升子序列

总感觉这道题有点似曾相识。。。

解法

如果是只要下标不同,就很好做了。

先想一下不管如果两个上升子序列相同,那么只需要计算一次这个条件时的做法。

当前这个数可以先查询比它小的数的总个数,那么再加上它自己就是它的贡献了。

可是有了如果两个上升子序列相同,那么只需要计算一次条件,怎么办呢?

我们可以用一个 map 去存它上一次的贡献,如果相同就不管它。
如果不同,那么将两次贡献的差值加入线段树内即可,还要更新为新的贡献值。

提示:如果不想打权值线段树,就使用离散化。

代码

戳我喵~(这是树状数组写法,可以替换成线段树)
const int mod = 1e9 + 7; //不要忘记了modint n = re;
int a[N], b[N];
lint dp[N];lint tree[N << 2];#define lowbit(x) x & (-x)void update_add(int x, lint add) { //对应你线段树的区间修改for (; x <= n; x += lowbit(x)) tree[x] += add;
}lint query(int x) { //对应你的线段树区间查询lint ans = 0;for (; x; x -= lowbit(x)) ans += tree[x];return ans;
}signed main() {for (int i = 1; i <= n; i++) b[i] = a[i] = re;sort(b + 1, b + 1 + n); //离散化int m = unique(b + 1, b + 1 + n) - b - 1;for (int i = 1; i <= n; i++) a[i] = lower_bound(b + 1, b + 1 + m, a[i]) - b;for (int i = 1; i <= n; i++) {lint x = (query(a[i] - 1) % mod + 1) % mod; //查询比自己小的值的个数if (x != dp[a[i]]) update_add(a[i], (x - dp[a[i]] + mod) % mod), dp[a[i]] = x;}wr((query(m) % mod - m + mod) % mod);
}

T2:CF833B The Bakery

解法

这是一道经典的 DP 优化问题。朴素 DP 为:

  • \(dp[i][j]\) 表示将前 \(i\) 个元素分成 \(j\) 段的最大总价值。
  • 转移:\(dp[i][j] = \max_{t = j-1}^{i-1} \big( dp[t][j-1] + \text{cost}(t+1, i) \big)\),其中 \(\text{cost}(l, r)\) 表示区间 \([l, r]\) 中不同数字的个数。

直接计算复杂度 \(O(k n^2)\),无法通过。需要优化。

线段树优化

观察到当 \(i\) 向右移动一位时,所有以 \(t\) 为上一段结束位置的状态值 \(dp[t][j-1] + \text{cost}(t+1, i)\) 中,只有一部分需要更新:因为新加入的元素 \(a_i\) 会对所有 \(t < i\)\(a_i\)\((t+1, i-1]\) 中未出现过的区间贡献加 1。具体来说,设 \(pre[x]\) 表示值 \(x\) 上一次出现的位置(在扫描 \(i\) 的过程中动态维护),则当 \(i\)\(i-1\) 变成 \(i\) 时,对于所有 \(t \in [pre[a_i], i-1]\)\(\text{cost}(t+1, i) = \text{cost}(t+1, i-1) + 1\)(因为 \(a_i\)\([t+1, i-1]\) 中未出现过);对于 \(t < pre[a_i]\)\(\text{cost}\) 不变。

因此我们可以为每个 \(j\) 维护一棵线段树,叶子节点 \(t\) 存储值 \(dp[t][j-1] + \text{cost}(t+1, \text{当前} i)\)。当 \(i\)\(1\)\(n\) 枚举时:

  1. 将区间 \([pre[a_i], i-1]\)\(1\)(表示这些 \(t\) 对应的 \(\text{cost}\) 增加)。
  2. 查询区间 \([j-1, i-1]\) 的最大值,即为 \(dp[i][j]\)
  3. 更新 \(pre[a_i] = i\)

这样在计算第 \(j\) 层时,我们需要事先用 \(dp[*][j-1]\) 初始化线段树。\(dp[0][0] = 0\),其余为负无穷。每次计算完 \(dp[i][j]\) 后,\(dp[i][j]\) 会被用于下一层 \(j+1\) 的初始化,但下一层的线段树需要重新建树(基于新的 \(j\))。

复杂度 \(O(k n \log n)\),空间 \(O(k n)\) 但可以滚动优化,不过 \(n\) 较大时通常需要开 \(O(nk)\),这里 \(k \le 50\),可以接受。

代码

戳我喵~
#include <bits/stdc++.h>
#define re read()
#define wr(n) write(n)
#define sp putchar(' ')
#define endl puts("")
#define int long longconst int N = 3e4 + 5010, INF = 1e9;
const double ecnts = 1e-6;using namespace std;using lint = long long;
using ulint = unsigned long long;
using pii = pair<int, int>;int read() {...}
void write(int x) {...}const int mod = 1e9 + 7;int n, K;
int dp[N][60];   // dp[i][j]#define ls p << 1
#define rs p << 1 | 1struct Tree {int l, r;int val, lazy;
}tree[N << 2];void pushup(int p) {tree[p].val = max(tree[ls].val, tree[rs].val);}void pushdown(int p) {if (tree[p].lazy) {tree[ls].val += tree[p].lazy, tree[rs].val += tree[p].lazy;tree[ls].lazy += tree[p].lazy, tree[rs].lazy += tree[p].lazy;tree[p].lazy = 0;}
}// 用第 line 层的 dp 值建树,即 dp[l][line] 作为叶子 t = l 的初值
void build(int p, int l, int r, int line) {tree[p].l = l, tree[p].r = r;tree[p].val = dp[l][line], tree[p].lazy = 0;if (l == r) return ;int mid = l + r >> 1;build(ls, l, mid, line), build(rs, mid + 1, r, line);pushup(p);
}// 区间加 k
void update(int p, int l, int r, int k) {if (l <= tree[p].l && tree[p].r <= r) {tree[p].val += k;tree[p].lazy += k;return ;}pushdown(p);int mid = tree[p].l + tree[p].r >> 1;if (l <= mid) update(ls, l, r, k);if (r > mid) update(rs, l, r, k);pushup(p);
}// 区间查询最大值
int query(int p, int l, int r) {if (l <= tree[p].l && tree[p].r <= r) return tree[p].val;pushdown(p);int ans = 0;int mid = tree[p].l + tree[p].r >> 1;if (l <= mid) ans = max(ans, query(ls, l, r));if (r > mid) ans = max(ans, query(rs, l, r));return ans;
}int a[N], pre[N];signed main() {int n = re, k = re;for (int i = 1; i <= n; i++) a[i] = re;for (int j = 1; j <= k; j++) {// 用 dp[0..n][j-1] 建树,注意线段树节点 t 对应 dp[t][j-1]build(1, 0, n, j - 1);// 初始化 pre 数组,记录每个值在当前层扫描过程中的上一次出现位置for (int i = 1; i <= n; i++) pre[a[i]] = 0;for (int i = 1; i <= n; i++) {// 将区间 [pre[a[i]], i-1] 加 1,表示 cost 增加update(1, pre[a[i]], i - 1, 1);// 查询 [j-1, i-1] 的最大值作为 dp[i][j]dp[i][j] = query(1, j - 1, i - 1);pre[a[i]] = i;   // 更新上一次出现位置}}wr(dp[n][k]), endl;
}
http://www.jsqmd.com/news/412165/

相关文章:

  • .NET 11 预览版 1 中的新兴架构演进:RISC-V 与 LoongArch 支持的深度技术解析与生态展望
  • 从月薪12K到19K*14薪!收藏这份程序员转行大模型学习指南,小白也能逆袭!
  • 收藏!AI时代,你的决策速度够快吗?爆款Demo背后的产品管理瓶颈
  • AI 翻书指南:一文读懂检索增强生成(RAG)从入门到实战
  • LangChain的DeepAgents框架:让复杂智能体开发像搭积木一样简单,收藏必备!
  • 告别“画图扯皮”!AI时代产品经理的转型指南:掌握这招,轻松收藏!
  • 太空光伏电池的紫外辐射试验与远紫外试验
  • vllm: kv cache
  • 250_尚硅谷_统计不同类型的字符个数
  • java16进制计算
  • 绍兴净水器代理商怎么选?专业科普+靠谱供应商推荐 - 小坤哥
  • 舆情监测八大功能全盘点:如何精准赋能全场景?
  • 三维偏序
  • SS中的CSRF,passwordEncoder,authenticationProvider,authenticationManager,securityFilterChain几个概念及调用时机
  • mac安装redis_笔记
  • AI开发-python-milvus向量数据库(2-12 -milvus-向量检索)
  • 以智慧科技,筑就全时段护理守护网
  • 基于COMSOL的拓扑光子晶体光学仿真模型研究:探究一维至三维晶格能带与场分布特性
  • 小白程序员必看:OpenClaw带你体验AI“真正干活”的全新革命!
  • 开源必备:Git 仓库敏感日志文件清理与脱敏教程
  • 掌握Tableau,为大数据分析增添助力
  • 2026执业药师备考前瞻:从机构选择到高效复习,一篇说透 - 品牌测评鉴赏家
  • 向量搜索系统的三个核心优化维度:速度、精度与规模
  • TGDZCalc by Scala(40th)
  • 数据库连接池Druid的最佳实践
  • 【联邦学习高级应用】HIPAA技术专题 原理和实现
  • 【2026免费】基于SpringBoot的社区医院信息平台
  • 2026执业药师培训机构避坑不踩雷,零基础也能高效通关 - 品牌测评鉴赏家
  • Java程序员失业转型大模型开发:3个月实现高薪入职,附副业变现秘籍及104G免费学习资源包(收藏)
  • 北京净水器供应商怎么选?专业科普+5家靠谱品牌推荐 - 小坤哥