倍增优化DP
概述
倍增优化DP是一类常见的DP优化方式,多见于多询问的DP问题,这是因为倍增优化DP可以在 的时间内拼出一个区间,如果用普通DP,很可能出现爆空间等错误。
倍增优化DP的基本状态设计是 ,通常表示 走 步出现的结果或构成的区间的贡献。当然,这只是框架,还要加其他状态依题目具体而定。
在设计倍增的状态时,要注意几点:
- “步”,必须是整体。注意:权值往往是不能划分的,但是,权值所属的物品有时可以当做 “步”。
- 树上倍增的题目, 表示 到 级祖先路径上的信息,或表示 的 祖先的信息。
- 倍增修改相对困难,若一道题的倍增支持修改,那么 中的 一般不大,由于 是 的指数,所以当做常数看待,这种题基本用数据结构维护。
- 是由两个 合并得来,可以理解为满足区间可加性。
- 如果是一步步跳,那么一般考虑倍增。
- 如果 表示的不是区间,那么通常要记录从 出发,走 步,走到哪里。不然不能用两个 的状态拼凑出 的状态。
应用
这种优化思想在题目中用的很多,在不少算法中也有涉及。
倍增求LCA :设 表示 的 辈祖先节点,可以发现,这里的 “步” 就是每次跳的一步,不是权值, 预处理, 查询。
RMQ区间最值 :设 表示区间 的最值,可以做到 预处理, 查询。
题目
跑路
先找上面提到的 “步”,发现无论是 “秒”,还是两个点之间的长度,都是 ,于是我们可以设秒为步,规范的说:设 表示从 出发,经过 秒能否到 ,然后在 的 之间连权值为 的边(跑路器每秒可以跑 米),跑一遍最短路即可。
为什么要用倍增?题目提示很明显:
每秒钟可以跑 千米。
const int N = 100; int n, m; int g[N][N][N]; int f[N][N]; void init() { fr(k, 1, 65) fr(z, 1, n) fr(i, 1, n) fr(j, 1, n) g[i][j][k] |= (g[i][z][k - 1] && g[z][j][k - 1]); } void Floyed() { fr(k, 1, n) fr(i, 1, n) fr(j, 1, n) if(k != i && i != j && j != k) f[i][j] = min(f[i][j], f[i][k] + f[k][j]); } int main() { cin >> n >> m; fr(i, 1, m) { int u, v; cin >> u >> v; g[u][v][0] = 1; } init(); memset(f, 0x3f, sizeof(f)); fr(i, 1, n) fr(j, 1, n) fr(k, 0, 65) if(g[i][j][k]) f[i][j] = 1; Floyed(); cout << f[1][n] << '\n'; return 0; }
Minimal Segment Cover
首先,肯定不能设出 这种从 开始,跨越 长度的最小线段数,这种倍增纯粹胡来。因为权值一般是不能放进 里面的,这不符合我们对 "步" 的定义,而且没有办法转移,因为没有办法将两个 的状态进行合并。
可以设 表示从点 出发,跨越 条线段,最远可以到达的点,这里用到了上面提及的技巧,“权值是不能作为状态的,但是权值的主人往往可以作为状态”。很容易写出转移方程:
初始化 数组一定不能只算端点值,要将区间中间的点也计算进去。详细见代码。
对于每次询问,我们从 出发,用类似二进制拆分的思想,从高位到低位依次向后跳,依次统计答案,这里的代码实现有点类似倍增求LCA,详见代码。
const int N = 5e5 + 10; int n, m, maxn; int f[N][20]; int main() { cin >> n >> m; fr(i, 1, n) { int l, r; cin >> l >> r; f[l][0] = max(f[l][0], r); maxn = max(maxn, r); } fr(i, 1, maxn) if(f[i - 1][0] >= i) f[i][0] = max(f[i][0], f[i - 1][0]); fr(i, 1, 19) fr(j, 0, maxn) { f[j][i] = max(f[j][i], f[f[j][i - 1]][i - 1]); } while(m --) { int l, r, np, ans = 0; cin >> l >> r; np = l; rf(i, 19, 0) { if (f[np][i] < r) ans += (1 << i), np = f[np][i]; //不能写小于等于,如果r是右端点,那么跳再多次也是白搭 } if (f[np][0] >= r) cout << ans + 1 << '\n'; else cout << -1 << '\n'; } return 0; }
[CERC2017]Donut Drone
如果没有修改操作,直接设 表示从 出发,走 步可以到的坐标,就是道纯纯的屑题。
但是有修改操作。。。
众所周知,暴力可以极大的降低题目难度。
考虑每次先暴力的将在矩阵上的点 跳到第一列 ,然后一圈圈的跳,最后再将没有跳够的暴力补上。如果中间的跳圈可以倍增实现,那么查询一次的时间复杂度就是 ,完全可以接受。
由于我们只关心从第一列的某行,跳几圈会到第一列的第几行,所以可以设 表示从第1列的 行出发,跳 圈,会跳到哪里。
现在我们考虑如何支持修改。
我们再维护一个数组 表示在第 行,从 开始走,走完区间 ,也就是走到 ,会处于哪一行。考虑这个dp如何转移,显然,这种类似区间dp的东西可以选择一个断点,记作 . 于是:
由于这不是最优化或者计数问题,所以我们可以随意取一个点,取中点吧。设 ,式子改写为:
是不是很像线段树的pushup
?
没错,这个东西是满足区间可加性的,所以考虑用线段树储存。
对于修改 的状态,最近的会影响 列的状态,所以对 列进行单点修改即可。
于是, 就是 的答案,在每次询问时,暴力修改倍增数组 即可。
时间复杂度:
const int N = 2100; int n, m, q; int a[N][N]; int tr[N << 2][N], f[N][31]; int to(int x, int y) { int ty = (y+1<=m?y+1:1), tx1 = x, tx2 = (x+1<=n?x+1:1), tx3 = (x-1>=1?x-1:n); if (a[tx1][ty] > a[tx2][ty] && a[tx1][ty] > a[tx3][ty]) return tx1; else if (a[tx2][ty] > a[tx1][ty] && a[tx2][ty] > a[tx3][ty]) return tx2; else return tx3; } struct Segment_Tree { void pushup(int nd) { fr(i, 1, n) tr[nd][i] = tr[nd << 1 | 1][tr[nd << 1][i]]; } void build(int nd, int l, int r) { if (l == r) { fr(i, 1, n) tr[nd][i] = to(i, l); return ; } int mid = l + r >> 1; build(nd << 1, l, mid); build(nd << 1| 1, mid + 1, r); pushup(nd); } void change(int nd, int l, int r, int x) { // 更新第 x 列 if (l > x || r < x) return ; if (x == l && l == r) { fr(i, 1, n) tr[nd][i] = to(i, l); return ; } int mid = l + r >> 1; change(nd << 1, l, mid, x); change(nd << 1 | 1, mid + 1, r, x); pushup(nd); } }T; int main() { cin >> n >> m; fr(i, 1, n) fr(j, 1, m) cin >> a[i][j]; cin >> q; T.build(1, 1, m); int nx, ny; nx = ny = 1; while(q --) { string s; int k, x, y; cin >> s; if (s == "move") { cin >> k; if (ny != 1) { while(k) { nx = to(nx, ny); ny = (ny+1<=m?ny+1:1); k --; if (ny == 1) break; } } if (!k) { cout << nx << ' ' << ny << '\n'; continue; } fr(i, 1, n) f[i][0] = tr[1][i]; fr(i, 1, 30) fr(j, 1, n) f[j][i] = f[f[j][i - 1]][i - 1]; int cnt = k / m, bcnt = k % m; rf(i, 30, 0) { if ((1 << i) <= cnt) { nx = f[nx][i]; cnt -= (1 << i); } } fr(i, 1, bcnt) { nx = to(nx, ny); ny = (ny+1<=m?ny+1:1); } cout << nx << ' ' << ny << '\n'; } else { cin >> x >> y >> k; a[x][y] = k; int ty = (y-1>=1?y-1:m); T.change(1, 1, m, ty); } } return 0; }
开车旅行
为了方便,首先用set
求出小A和小B分别从城市i出发,会走到的城市,分别记作 .
多询问的DP问题,比较明显是倍增了。
考虑如何倍增,可以发现,记城市个数和天数都是可以的,毕竟两者都可以看做一步,或者说一个整体。
设 表示,从城市 出发,走 天,第一天是 小A/小B 开车,会到达的城市。
转移方程也很简单:
其中 的情况一定要注意,因为这两天先是A开车,再是B开车。
由于题目还要求小A和小B分别行驶的路程,我们设 表示从城市 出发,走 天,小A/小B 先开车,小A行驶的距离; 表示从城市 出发,走 天,小A/小B 先开车,小B行驶的距离。
转移方程类似于 数组,详见代码。
const int N = 1e5 + 10; const LL INF = 1e12; int n, m, h[N]; int ga[N], gb[N]; LL f[18][N][2], da[18][N][2], db[18][N][2]; set<PII> s; void init() { s.insert({INF, 0}); s.insert({INF + 1, 0}); s.insert({-INF, 0}); s.insert({-INF - 1, 0}); //提前插入4个最值,有利于避免越界。 rf(i, n, 1) { PII t = {h[i], i}; auto it = s.lower_bound(t); ++it; vector <PII> g; g.clear(); fr(j, 0, 3) { g.push_back(*it); --it; } LL minn = INF + 10, cmin = INF + 10; int p1 = 0, p2 = 0; rf(j, 3, 0) { LL d = abs(g[j].first - h[i]); if (d < minn) { cmin = minn; minn = d; p2 = p1; p1 = g[j].second; } else if (d < cmin) { cmin = d; p2 = g[j].second; } } s.insert({h[i], i}); ga[i] = p2; gb[i] = p1; } } void calc(int p, int x, int &la, int &lb) { la = lb = 0; rf(i, 17, 0) { if (f[i][p][0] && la + lb + da[i][p][0] + db[i][p][0] <= x) { la += da[i][p][0]; lb += db[i][p][0]; p = f[i][p][0]; } } } int main() { ios::sync_with_stdio(0); cin.tie(0); cout.tie(0); cin >> n; fr(i, 1, n) cin >> h[i]; init(); fr(i, 1, n) { f[0][i][0] = ga[i]; f[0][i][1] = gb[i]; da[0][i][0] = abs(h[i] - h[ga[i]]); db[0][i][1] = abs(h[i] - h[gb[i]]); } fr(i, 1, 17) fr(j, 1, n) fr(k, 0, 1) { if (i == 1) f[i][j][k]=f[i-1][f[i-1][j][k]][1-k]; else f[i][j][k]=f[i-1][f[i-1][j][k]][k]; if (i == 1) da[i][j][k]=da[0][j][k]+da[0][f[0][j][k]][1-k]; else da[i][j][k] = da[i-1][j][k] + da[i-1][f[i-1][j][k]][k]; if (i == 1) db[i][j][k]=db[0][j][k]+db[0][f[0][j][k]][1-k]; else db[i][j][k] = db[i-1][j][k] + db[i-1][f[i-1][j][k]][k]; } int x; cin >> x >> m; LL maxh = -INF, res = 0; double minn = INF; fr(i, 1, n) { int la, lb; calc(i, x, la, lb); double s = lb?la*1.0/lb:INF; if (s < minn || (s == minn && h[i] > maxh)) { minn = s; maxh = h[i]; res = i; } } cout << res << '\n'; fr(i, 1, m) { int x, s, la, lb; cin >> s >> x; calc(s, x, la, lb); cout << la << ' ' << lb << '\n'; } return 0; }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
· 为什么 退出登录 或 修改密码 无法使 token 失效