用分治处理决策单调性问题
决策单调性是 dp 转移方程的一种性质。一般而言,我们有许多方法优化一个具有决策单调性的转移方程。
这里主要讲解一种用分治解决决策单调性问题的方法。
引入
先看一道题:CF868F
我们可以想到一个 暴力。定义 为令点 为第 段的最后一个点产生的最小代价。则可以得出:
其中 即为这一段的代价。答案即为 。这是好求的。
显然,我们需要砍掉一个 才能通过此题。然后我们通过打表得到了一个性质:对于 与 的转移点(这两个状态由它们的转移点转移而来),如果 ,则 。也就是说,这个转移具有决策单调性!
发现
也许已经有人想要大显身手了,但是我们先来关注一下我们是如何得出决策单调性的。
让我们了解一下四边形不等式。对于 ,如果有四个点 且 ,那么我们就说这个转移方程满足四边形不等式(最大也是可以的,反过来就行了)。这一性质也被称作“包含劣于相交”。
那么我们发现,如果一个形如 的转移方程满足四边形不等式,那么这个方程具有决策单调性。
考虑证明:考虑反证法,然后读者自证。
在实际做题中,我们通过感觉感应到四边形不等式的存在(有时也会直接感应到决策单调性),然后打表加以证明。
分治
我们并不知道这个代价函数除了决策单调性以外的性质,也就是说,尽管我们知道了点 的决策点 ,对于大于点 的点 ,我们仍然要暴力枚举转移点 。这很容易被卡掉。
考虑更好地排除转移点,对于需要求解的区间 ,我们取区间中点 ,并暴力计算出 的转移点 。如果原区间转移范围在 之间,那么我们可以分两半。 转移范围在 , 转移范围在 。
考虑证明时间复杂度。对于分治的每一层,我们暴力计算的时间复杂度总和都是 。但由于分治树只有 层,所以总复杂度变为 。
修改
现在我们需要解决的问题,是我们无法快速求出 。
我们发现虽然我们无法快速求出 ,但我们可以快速地由 转移到 。
这时我们就有一个很憨的想法:类似莫队,我们直接在上一次查询的基础上一个一个暴力转移。
很神奇的是,这个方法复杂度还是 的!考虑两个指针进行跳跃。对于从 到 的跳跃,时间复杂度不会超过区间长度。那么这一层的时间复杂度总和还是 的。然后分治树只有 层,于是总时间复杂度还是 。
实现
注意由于这里也是一个一个的跳跃,所以跳跃顺序和莫队一样有讲究。具体可以看代码。
#include <iostream>
#include <algorithm>
#include <string.h>
using namespace std;
const int N = 1e5 + 5, K = 21;
int n, k;
int a[N];
long long dp[N], g[N];
long long ton[N];
long long f(int L, int R) {
static long long l = 1, r = 0, ans = 0;
auto del = [&](int i) {
ton[a[i]]--;
ans -= ton[a[i]];
};
auto add = [&](int i) {
ans += ton[a[i]];
ton[a[i]]++;
};
while (l > L) add(--l);
while (r < R) add(++r);
while (l < L) del(l++);
while (r > R) del(r--);//important
return ans;
}
void solve(int l, int r, int dl, int dr) {
int mid = (l + r) >> 1, dec = 0;
for (int i = dl; i <= min(dr, mid - 1); ++i) {
if (dp[mid] > g[i] + f(i + 1, mid)) {
dp[mid] = g[i] + f(i + 1, mid);
dec = i;
}
}
if (l == r) return ;
solve(l, mid, dl, dec);
solve(mid + 1, r, dec, dr);
}
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
cin >> n >> k;
for (int i = 1; i <= n; ++i) cin >> a[i];
long long tmp = 0;
for (int i = 0; i <= n; ++i) {
tmp += ton[a[i]];
dp[i] = tmp;
ton[a[i]]++;
}
for (int i = 1; i <= n; ++i) ton[i] = 0;
for (int lask = 1; lask <= k - 1; ++lask) {
for (int i = 1; i <= n; ++i) g[i] = dp[i], dp[i] = 2e13;
solve(lask + 1, n, lask, n - 1);
}
cout << dp[n] << endl;
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· C#/.NET/.NET Core技术前沿周刊 | 第 29 期(2025年3.1-3.9)
· 从HTTP原因短语缺失研究HTTP/2和HTTP/3的设计差异