决策单调性优化

决策单调性优化

决策单调性是在最优化 DP 中的可能出现的一种性质。

对于形如

\[f_i = \min_{j = 0}^{i - 1} \{ f_j + w(j, i) \} \]

的转移方程,记 \(p_i\) 为令 \(f_i\) 取得最小值的 \(j\) 的值(最优决策点)。若 \(p\) 单调不降,则称 \(f\) 具有决策单调性。

单峰性转移的指针优化

对于某个状态的最优转移点,其左边的贡献向左单调递减,右边也向右单调递减,则转移具有单峰性。

考虑记录一个指针 \(p\) 表示决策点,由于决策单调性,\(p\) 不会往回跳。

进入了每一层之后,如果后面更优,则 \(p\) 向后跳,否则停住并转移。根据单峰性这样子一定不会漏最优解。

均摊后总时间复杂度线性。

四边形不等式

以上述转移方程为例,四边形不等式的一个形式为:

若对于任意 \(a \leq b \leq c \leq d\) 均满足:

\[w(a, c) + w(b, d) \leq w(a, d) + w(b, c) \]

则称函数 \(w\) 满足四边形不等式,简记为交叉优于包含。

若恒取等,则称 \(w\) 满足四边形恒等式。

可以证明若 \(w\) 满足四边形不等式,则上述转移方程满足决策单调性。

证明:因为 \(a \to d, b \to c\) 不优,而 \(a \to c, b \to d\) 更优,于是有决策单调性。

四边形不等式的另一个形式为:

若对于任意 \(i \leq j\) 均满足:

\[w(i, j) + w(i + 1, j + 1) \leq w(i, j + 1) + w(i + 1, j) \]

则称函数 \(w\) 满足四边形不等式。

在此引入一个概念:区间包含单调性:

对于任意 \(a \leq b \leq c \leq d\) 均满足:

\[w(b, c) \leq w(a, d) \]

则称函数 \(w\) 满足区间包含单调性,简记为内优于外。

四边形不等式的一些性质:

  • 若函数 \(w_1(i, j), w_2(i, j)\) 均满足四边形不等式,则对于任意 \(c_1, c_2 \geq 0\) ,函数 \(c_1 w_1 + c_2 w_2\) 也满足四边形不等式。
  • 若存在函数 \(f(x), g(x)\) 使得 \(w(i, j) = f(i) - g(j)\) ,则函数 \(w\) 满足四边形恒等式。若 \(f(x), g(x)\) 单调增,则函数 \(w\) 还满足区间包含单调性。
  • \(h(x)\) 是一个单调增加的下凸函数,若函数 \(w(i, j)\) 满足四边形不等式和区间包含单调性,则复合函数 \(h(w(i, j))\) 也满足四边形不等式和区间包含单调性。
  • \(h(x)\) 是一个下凸函数,若函数 \(w(i, j)\) 满足四边形恒等式和区间包含单调性,则复合函数 \(h(w(i, j))\) 也满足四边形不等式。

二分队列优化

使用条件

一般形式为:对于任意两个决策 \(j_1 < j_2\) ,存在一个 \(x\) 满足 \(i \leq x\)\(j_1\) 优于 \(j_2\)\(i > x\)\(j_1\) 劣于 \(j_2\)

可以证明基于四边形不等式的转移方程同样满足该条件。

特殊的是该条件可以不满足决策单调性,形式为:对于任意两个决策 \(j_1 < j_2\) ,存在一个 \(x\) 满足 \(i \leq x\)\(j_1\) 劣于 \(j_2\)\(i > x\)\(j_1\) 优于 \(j_2\)

实现

建立一个队列维护决策点,队列中保存若干三元组 \((j, l, r)\) ,表示最优决策点为 \(j\) 的区间为 \([l, r]\)

遍历枚举 \(i\) ,执行以下操作:

  • 检查队头:设队头为 \((j_0, l_0, r_0)\) ,若 \(r_0 = i - 1\) ,则删除队头;否则令 \(l_0 \leftarrow i\)
  • 取队头保存最优决策点 \(j\) 进行转移求出 \(f_i\)
  • 尝试插入新决策 \(i\) ,步骤如下:
    • 取出队尾,记为 \((j_t, l_t, r_t)\)
    • 若对于 \(f_{l_t}\) 来说, \(i\) 决策优于 \(j_t\) 决策,记 \(pos = l_t\) ,删除队尾重新执行上一步。
    • 否则若对于 \(f_{r_t}\) 来说,\(i\) 决策优于 \(j_t\) 决策,则在 \([l_t, r_t]\) 上二分查找位置 \(pos\) ,满足 \([pos, n]\) 的最优决策点均为 \(i\)
    • 将三元组 \((i, pos, n)\) 插入队尾。

时间复杂度 \(O(n \log n)\)

P1912 [NOI2009] 诗人小G

\[f_i = \min (f_j + |(s_i - s_j) + (i - j) - (L + 1)|^P) \]

按上述方法实现即可。

#include <bits/stdc++.h>
typedef long double ldb;
using namespace std;
const int N = 1e5 + 7, S = 3e1 + 7;

struct Node {
	int j, l, r;
} q[N];

ldb f[N];
int s[N], g[N];
bool ed[N];
char str[N][S];

int n, L, P, head, tail;

template <class T = int>
inline T read() {
	char c = getchar();
	bool sign = c == '-';
	
	while (c < '0' || c > '9')
		c = getchar(), sign |= c == '-';
	
	T x = 0;
	
	while ('0' <= c && c <= '9')
		x = (x << 1) + (x << 3) + (c & 15), c = getchar();
	
	return sign ? (~x + 1) : x;
}

inline ldb mi(ldb a, int b) {
	ldb res = 1;
	
	for (; b; b >>= 1, a *= a)
		if (b & 1)
			res *= a;
	
	return res;
}

inline ldb calc(int i, int j) {
	return f[j] + mi(abs(s[i] - s[j] - L), P);
}

inline int BinarySearch(int l, int r, int i, int j) {
	int pos = r + 1;

	while (l <= r) {
		int mid = (l + r) >> 1;

		if (calc(mid, i) <= calc(mid, j))
			pos = mid, r = mid - 1;
		else
			l = mid + 1;
	}
	
	return pos;
}

signed main() {
	int T = read();
	
	while (T--) {
		n = read(), L = read() + 1, P = read();
		
		for (int i = 1; i <= n; ++i) {
			scanf("%s", str[i]);
			s[i] = s[i - 1] + strlen(str[i]) + 1;
		}
		
		q[head = tail = 1] = (Node) {0, 1, n};
		
		for (int i = 1; i <= n; ++i) {
			if (q[head].r == i - 1)
				++head;

			f[i] = calc(i, g[i] = q[head].j);
			int pos = n + 1;

			while (head <= tail) {
				if (calc(q[tail].l, i) <= calc(q[tail].l, q[tail].j))
					pos = q[tail--].l;
				else {
					pos = BinarySearch(q[tail].l, q[tail].r, i, q[tail].j);
					q[tail].r = pos - 1;
					break;
				}
			}

			if (pos != n + 1)
				q[++tail] = (Node) {i, pos, n};
		}
		
		if (f[n] > 1e18)
			puts("Too hard to arrange");
		else {
			printf("%.0LF\n", f[n]);
			fill(ed + 1, ed + 1 + n, false);
			
			for (int i = n; i; i = g[i])
				ed[i] = true;
			
			for (int i = 1; i <= n; ++i)
				printf("%s%c", str[i], " \n"[ed[i]]);
		}
		
        puts("--------------------");
	}
	
	return 0;
}

二分栈优化

使用条件

向后枚举时,某种决策只会被更前的决策反超。

实现

用单调栈维护所有有用的决策,其中栈顶是当前最优决策。

计算完 \(f_i\) 后,考虑求决策点为 \(i\) 的后缀。由于决策单调性,所以可以二分。

具体地,每次将 \(i\) 与栈顶的决策比较,若栈顶的决策区间内 \(i\) 恒优则弹栈,否则求出分界点后修改栈顶决策区间并压入 \(i\) 及相关后缀。

应用

P5504 [JSOI2011] 柠檬

将一个数列分成若干段,从每一段中选定一个数 \(s_0\) ,假设这个数有 \(t\) 个,则这一段价值为 \(s_0 t^2\) 。求每一段的价值和的最大值。

\(n \leq 10^5\)

不难得到转移方程:

\[f_i = \max_{j = 1}^{i} \{ f_{j - 1} + s_i \times (sum_i - sum_j + 1)^2 \} \]

可以发现最优决策点一定满足 \(s_i = s_j\) ,否则独立成段更优。于是固定 \(j\) 时,\(s_i \times (sum_i - sum_j + 1)^2\) 单调递增。故对于一个 \(j_1 < j_2\) ,存在一个分界点满足分界点前 \(j_2\) 更优,分界点后 \(j_1\) 更优。于是有决策单调性,用二分栈优化即可。

#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int N = 1e5 + 7;

vector<int> sta[N];

ll f[N];
int a[N], buc[N], s[N];

int n, top;

template <class T = int>
inline T read() {
	char c = getchar();
	bool sign = (c == '-');
	
	while (c < '0' || c > '9')
		c = getchar(), sign |= (c == '-');
	
	T x = 0;
	
	while ('0' <= c && c <= '9')
		x = (x << 1) + (x << 3) + (c & 15), c = getchar();
	
	return sign ? (~x + 1) : x;
}

inline ll calc(int j, int t) {
	return f[j - 1] + 1ll * a[j] * t * t;
}

inline int check(int x, int y) {
	int l = 1, r = n, ans = n + 1;

	while (l <= r) {
		int mid = (l + r) >> 1;

		if (calc(x, mid - s[x] + 1) >= calc(y, mid - s[y] + 1))
			ans = mid, r = mid - 1;
		else
			l = mid + 1;
	}

	return ans;
}

signed main() {
	n = read();

	for (int i = 1; i <= n; ++i)
		s[i] = ++buc[a[i] = read()];

	for (int i = 1; i <= n; ++i) {
		int c = a[i];
		#define tp1 sta[c][sta[c].size() - 1]
		#define tp2 sta[c][sta[c].size() - 2]

        while (sta[c].size() >= 2 && check(tp2, tp1) <= check(tp1, i))
        	sta[c].pop_back();

        sta[c].emplace_back(i);

        while (sta[c].size() >= 2 && check(tp2, tp1) <= s[i])
        	sta[c].pop_back();

        f[i] = calc(tp1, s[i] - s[tp1] + 1);
        #undef tp1
        #undef tp2
	}

	printf("%lld", f[n]);
	return 0;
}

整体二分优化

实现

某些 DP 形式如下:

\[f_{i, j} = \min_{k \leq j} (f_{i - 1, k} + w(k, j)) \ \ \ \ (1 \leq i \leq n, 1 \leq j \leq m) \]

其中 \(i \in [1, n], j \in [1, m]\) ,共 \(n \times m\) 个状态,每个状态有 \(O(m)\) 个决策,时间复杂度 \(O(nm^2)\)

\(m_{i, j}\) 为上述转移中最小化 \(k\) 的值,若 \(\forall i, j, m_{i, j} \leq m_{i, j + 1}\) ,则我们可以用分治优化。

假设我们对于固定的 \(i, j\) 计算 \(m_{i, j}\) ,那么我们可以确定 \(\forall j' < j, m_{i, j'} < m_{i, j}\) ,这意味着计算 \(m_{i, j'}\) 时不用考虑那么多其他的点

运用分治思想,递归得到 \(m\) 的上下界,就可以达到 \(O(nm \log m)\) 的时间复杂度,每个 \(m_{i, j}\) 的值只可能出现在 \(\log m\) 个不同点中。

\(w(l, r)\) 不好直接求,而使用莫队可以推出时,整体二分便很好用了。

应用

Ciel and Gondolas

给出二维矩阵 \(A\) ,将 \(1 \sim n\) 分为 \(k\) 段,记一段 \(l \sim r\) 的代价为 \(\sum_{i = l}^r \sum_{j = i + 1}^r A_{i, j}\) ,求 \(k\) 段代价最小值。

\(n \leq 4000, k \leq \min(n, 800), A_{i, j} \geq 0\)

决策单调性显然,整体二分优化模板,时间复杂度 \(O(nk \log n)\)

值得一提的是,这题的 \(w\) 可以快速计算,没必要用整体二分优化,使用 Knuth's Optimization 可以做到 \(O(nk)\)

#include <bits/stdc++.h>
using namespace std;
const int inf = 0x3f3f3f3f;
const int N = 4e3 + 7, K = 8e2 + 7;

int s[N][N], w[N][N], f[N][K];

int n, k;

template <class T = int>
inline T read() {
	char c = getchar();
	bool sign = (c == '-');
	
	while (c < '0' || c > '9')
		c = getchar(), sign |= (c == '-');
	
	T x = 0;
	
	while ('0' <= c && c <= '9')
		x = (x << 1) + (x << 3) + (c & 15), c = getchar();
	
	return sign ? (~x + 1) : x;
}

void solve(int l, int r, int L, int R, const int d) {
	if (L > R)
		return;

	int mid = (L + R) >> 1, pos = 0;
	f[mid][d] = inf;

	for (int i = l; i <= min(mid, r); ++i) {
		int res = f[i - 1][d - 1] + w[i][mid];

		if (res < f[mid][d])
			f[mid][d] = res, pos = i;
	}

	solve(l, pos, L, mid - 1, d), solve(pos, r, mid + 1, R, d);
}

signed main() {
	n = read(), k = read();

	for (int i = 1; i <= n; ++i)
		for (int j = 1; j <= n; ++j)
			s[i][j] = s[i - 1][j] + s[i][j - 1] - s[i - 1][j - 1] + read();

	for (int i = 1; i <= n; ++i)
		for (int j = i; j <= n; ++j)
			w[i][j] = (s[j][j] - s[i - 1][j] - s[j][i - 1] + s[i - 1][i - 1]) / 2;

	for (int i = 1; i <= n; ++i)
		f[i][0] = inf;

	for (int i = 1; i <= k; ++i)
		solve(1, n, 1, n, i);

	printf("%d", f[n][k]);
	return 0;
}

Yet Another Minimization Problem

给定一个序列 \(a_{1 \sim n}\) ,要把它分成 \(k\) 个子段。每个子段的费用是其中相同元素的对数,求所有子段的费用之和的最小值。

\(n \leq 10^5, k \leq \min(n, 20)\)

\(f_{i, j}\) 表示前 \(i\) 个元素分为 \(j\) 段的最小费用,\(w(l, r)\) 表示 \([l, r]\) 的费用,则:

\[f_{i, j} = \max (f_{k - 1, j - 1} + w(k, i)) \]

不难发现 \(w(l, r)\) 满足四边形不等式,故 \(f\) 每一层的转移都具有决策单调性。令 solve(l, r, L, R) 表示 \(f_{i, [L, R]}\) 的决策点在 \([l, r]\) 中,则可以每次计算出 \(f_{i, mid}\) 的决策点,将序列分为两部分分治。

接卸来考虑如何计算 \(f_{i, mid}\) 的决策点,直接计算时困难的,可以类似莫队维护一个指针。分治时相邻两次 \(mid\) 的改变量加起来应与 \(R - L\) 同级,故右端点移动次数为 \(O(n \log n)\) ;左端点一定从上一次分治的某个端点移动而来,在此次分治内移动次数与 \(r - l\) 同级,也是 \(O(n \log n)\)

总时间复杂度 \(O(nk \log n)\)

一道类题:The Bakery

#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const ll inf = 1e18;
const int N = 1e5 + 7, K = 2e1 + 7;

ll f[N][K];
int a[N];

int n, k;

template <class T = int>
inline T read() {
	char c = getchar();
	bool sign = (c == '-');
	
	while (c < '0' || c > '9')
		c = getchar(), sign |= (c == '-');
	
	T x = 0;
	
	while ('0' <= c && c <= '9')
		x = (x << 1) + (x << 3) + (c & 15), c = getchar();
	
	return sign ? (~x + 1) : x;
}

namespace MoAlgorithm {
int cnt[N];

ll result;
int l = 1, r = 0;

inline void add(int x) {
	result += cnt[a[x]]++;
}

inline void del(int x) {
	result -= --cnt[a[x]];
}

inline ll calc(int ql, int qr) {
	while (l > ql)
		add(--l);

	while (r < qr)
		add(++r);

	while (l < ql)
		del(l++);

	while (r > qr)
		del(r--);

	return result;
}
} // namespace MoAlgorithm

void solve(int l, int r, int L, int R, const int d) {
    if (L > R)
        return;
    
	int mid = (L + R) >> 1, pos = 0;
	f[mid][d] = inf;

	for (int i = l; i <= min(mid, r); ++i) {
		ll res = f[i - 1][d - 1] + MoAlgorithm::calc(i, mid);

		if (res < f[mid][d])
			f[mid][d] = res, pos = i;
	}

    solve(l, pos, L, mid - 1, d), solve(pos, r, mid + 1, R, d);
}

signed main() {
	n = read(), k = read();

	for (int i = 1; i <= n; ++i)
		a[i] = read();

	for (int i = 1; i <= n; ++i)
		f[i][0] = inf;

	for (int i = 1; i <= k; ++i)
		solve(1, n, 1, n, i);

	printf("%lld", f[n][k]);
	return 0;
}

P5574 [CmdOI2019] 任务分配问题

给定排列 \(p_{1 \sim n}\) ,将 \(p\) 划分成 \(k\) 段,使每一段的顺序对个数和最小。

\(n \leq 2.5 \times 10^4, k \leq 25\)

\(f_{i, j}\) 表示前 \(i\) 个数分为 \(j\) 段的答案,\(w(l, r)\) 表示 \(p_{l \sim r}\) 的顺序对个数,则:

\[f_{i, j} = \min_{k = 0}^{i - 1} \{ f_{k, j - 1} + w(k + 1, i) \} \]

注意到这里的 \(w(l, r)\) 并不好求,于是考虑采用整体二分维护决策单调性配合莫队+树状数组求顺序对优化即可,时间复杂度 \(O(nk \log^2 n)\)

#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const ll inf = 1e18;
const int N = 2.5e4 + 7, K = 27;

ll f[N][K];
int a[N];

int n, k;

namespace MoAlgorithm {
namespace BIT {
int c[N];

inline void update(int x, int k) {
	for (; x <= n; x += x & -x)
		c[x] += k;
}

inline int query(int x) {
	int res = 0;

	for (; x; x -= x & -x)
		res += c[x];

	return res;
}
} // namespace BIT

ll result;
int l = 1, r = 0;

inline ll calc(int ql, int qr) {
	while (l > ql) {
		--l;
		BIT::update(a[l], 1);
		result += (r - l + 1) - BIT::query(a[l]);
	}

	while (r < qr) {
		++r;
		result += BIT::query(a[r]);
		BIT::update(a[r], 1);
	}

	while (l < ql) {
		result -= (r - l + 1) - BIT::query(a[l]);
		BIT::update(a[l], -1);
		l++;
	}

	while (r > qr) {
		BIT::update(a[r], -1);
		result -= BIT::query(a[r]);
		r--;
	}

	return result;
}
} // namespace MoAlgorithm

void solve(int l, int r, int L, int R, const int d) {
	if (L > R)
		return;

	int mid = (L + R) >> 1, mnpos = 0;
	f[mid][d] = inf;

	for (int i = l; i <= min(mid, r); ++i) {
		ll res = f[i - 1][d - 1] + MoAlgorithm::calc(i, mid);

		if (res < f[mid][d])
			f[mid][d] = res, pos = i;
	}

	solve(l, pos, L, mid - 1, d), solve(pos, r, mid + 1, R, d);
}

signed main() {
	scanf("%d%d", &n, &k);

	for (int i = 1; i <= n; ++i)
		scanf("%d", a + i);

	for (int i = 1; i <= n; ++i)
		f[i][0] = inf;

	for (int i = 1; i <= k; ++i)
		solve(1, n, 1, n, i);

	printf("%lld", f[n][k]);
	return 0;
}

P3515 [POI2011] Lightning Conductor

给出 \(a_{1 \sim n}\) ,对于每个 \(i \in [1, n]\) ,求一个最小的非负整数 \(p\) ,使得对于所有 \(j \in [1, n]\) 都有 \(a_j \leq a_i + p - \sqrt{|i - j|}\)

\(n \leq 5 \times 10^5\)

转化限制条件为:

\[p + a_i \geq \max_{j = 1}^n \{ a_j + \sqrt{|i - j|} \} \]

将绝对值拆开,正反各做一次,得到:

\[p + a_i \geq \max_{j = 1}^{i - 1} \{ a_j + \sqrt{i - j} \} \]

\(w(j, i) = \sqrt{i - j}\) ,则 \(w(j, i)\) 满足四边形不等式,于是 \(f_i = \max_{j = 1}^{i - 1} \{ a_j + \sqrt{i - j} \}\) 满足决策单调性。

#include <bits/stdc++.h>
using namespace std;
const int N = 5e5 + 7;

double a[N], sq[N], f[N];

int n;

template <class T = int>
inline T read() {
	char c = getchar();
	bool sign = (c == '-');
	
	while (c < '0' || c > '9')
		c = getchar(), sign |= (c == '-');
	
	T x = 0;
	
	while ('0' <= c && c <= '9')
		x = (x << 1) + (x << 3) + (c & 15), c = getchar();
	
	return sign ? (~x + 1) : x;
}

inline double calc(int j, int i) {
	return a[j] + sq[i - j];
}

void solve(int l, int r, int L, int R) {
	if (L > R)
		return;

	int mid = (L + R) >> 1, mxpos = l;
	double mx = calc(l, mid);

	for (int i = l + 1; i <= min(r, mid); ++i)
		if (calc(i, mid) > mx)
			mxpos = i, mx = calc(i, mid);

	f[mid] = max(f[mid], calc(mxpos, mid));
	solve(l, mxpos, L, mid - 1), solve(mxpos, r, mid + 1, R);
}

signed main() {
	n = read();

	for (int i = 1; i <= n; ++i)
		a[i] = read(), sq[i] = sqrt(i);

	solve(1, n, 1, n);
	reverse(a + 1, a + 1 + n), reverse(f + 1, f + 1 + n);
	solve(1, n, 1, n);
	reverse(a + 1, a + 1 + n), reverse(f + 1, f + 1 + n);

	for (int i = 1; i <= n; ++i)
		printf("%d\n", (int)ceil(f[i] - a[i]));

	return 0;
}

Knuth's Optimization

\(p_{i, j}\)\(f_{i, j}\) 的最优决策点 \(k\) ,若 \(\forall i < j, p_{i, j - 1} \leq p_{i, j} \leq p_{i + 1, j}\) ,则使用 Knuth's Optimization 可以优化到 \(O(nm)\) 的复杂度。

本质思想为枚举 \([l, r]\) 的决策时,只要在 \([p_{l, r - 1}, p_{l + 1, r}]\) 中枚举决策即可,可以证明总时间复杂度为 \(O(n^2)\)

对于一类序列分 \(k\) 段问题,若 \(k \to k + 1\) ,则决策路径必然交错,即:

如果满足决策单调性,则必然满足路径交错,不会出现包含的情况。

这样所有决策点的决策范围和是 \(O(n)\) ,此时暴力枚举范围就很优秀了。

一般来说,证明这一类二维动态规划的常规方法为先证 \(w(i, j)\) 满足四边形不等式,然后归纳证明 \(f\) 满足四边形不等式,最后利用四边形不等式与 \(p\) 的最优性得到两个不等式,将其合并得到决策单调性。

P4767 [IOI2000] 邮局 加强版

\(n\) 个村庄,放 \(m\) 个邮局,求每个村庄到最近邮局的距离和的最小值。

\(n \leq 3000, m \leq 300\)

\(f_{i, j}\) 表示前 \(i\) 个村庄放 \(j\) 个邮局的最小距离和,\(w(l, r)\) 表示在 \([l, r]\) 范围村庄放一个邮局的最小距离和,则有:

\[f_{i, j} = \min_{k = 0}^{i - 1} \{ f_{k, j - 1} + w(k + 1, i) \} \]

可以证明 \(w(l, r)\) 满足四边形不等式,于是可以决策单调性优化做到 \(O(n^2)\)

当然也可以打表发现四边形不等式与决策单调性。

#include <bits/stdc++.h>
using namespace std;
const int inf = 0x3f3f3f3f;
const int N = 3e3 + 7, M = 3e2 + 7;

int w[N][N], f[N][M], g[N][M];
int a[N];

int n, m;

signed main() {
	scanf("%d%d", &n, &m);

	for (int i = 1; i <= n; ++i)
		scanf("%d", a + i);

	sort(a + 1, a + 1 + n);

	for (int l = 1; l <= n; ++l)
		for (int r = l + 1; r <= n; ++r)
			w[l][r] = w[l][r - 1] + (a[r] - a[(l + r) >> 1]);

	memset(f, inf, sizeof(f));
	f[0][0] = 0;

	for (int j = 1; j <= m; ++j) {
		g[n + 1][j] = n;

		for (int i = n; i; --i)
			for (int k = g[i][j - 1]; k <= g[i + 1][j]; ++k)
				if (f[k][j - 1] + w[k + 1][i] <= f[i][j])
					f[i][j] = f[k][j - 1] + w[k + 1][i], g[i][j] = k;
	}

	printf("%d", f[n][m]);
	return 0;
}

斜率优化

对于某一类型的 DP 方程:

\[f_i = \min(a_i \times b_j + c_j + d_i) \]

两个决策点满足 \(k\) 优于 \(j\) ,即:

\[a_i \times b_k + c_k + d_i \leq a_i \times b_j + c_j + d_i \]

移项得到:

\[-a_i \geq \dfrac{c_k - c_j}{b_k - b_j} \]

可以发现这个式子长得很像斜率,记:

\[slope(j,k) = \dfrac{c_k - c_j}{b_k - b_j} \]

则:

\[-a_i \geq slope(j,k) \]

可以发现它很符合单调队列的性质:

  • \(-a_i \geq slope(q_{head},q_{head+1})\) :因为 \(q_{head}\)\(q_{head+1}\) 前加入,那么这个式子就表示 \(q_{head}\) 的决策不如 \(q_{head+1}\) ,将队首弹出。
  • \(slope(q_{tail-1},q_{tail}) > slope(q_{tail},i)\) :假设我们后面存在一个 \(a_t\) 使得 \(-a_t \geq slope(q_{tail-1},q_{tail})\) ,那么等到 \(q_{tail-1}\) 弹出之后,一定有 \(-a_t \geq slope(q_{tail},i)\)\(q_{tail}\) 也会被弹出,所以 \(q_{tail}\) 实际上一点用都没有,直接弹出。

另一种理解是,原方程等价于:

\[f_i - d_i = \min(a_i \times b_j + c_j) \]

\(a_i \times b_j + c_j\) 看作一条直线,其中 \(j\) 决策提供了斜率和截距,每次就是用 \(a_i\) 去切所有的直线(决策)判断哪个更优。显然最优决策点一定在点集的凸壳上,于是设法维护凸壳即可。

或者说把 \((b_j, c_j)\) 看作点,则每次都是用一条斜率为 \(-a_i\) 的直线去切这个凸包,这样切到的截距就是最优的。

斜率优化的本质是把一类 DP 问题变成数据结构问题,让数据结构维护这个凸包,或者说维护凸包上两点连线的斜率,应具体情况具体分析。

单调队列优化

使用条件:斜率 \(b_j\) 单增,查询点 \(-a_i\) 单增(即 \(a_i\) 单减)。

考虑将决策点用单调队列存储,可以发现这个斜率很符合单调队列的性质:

  • \(-a_i \geq slope(q_{head}, q_{head+1})\) :因为 \(q_{head}\)\(q_{head+1}\) 前加入,那么这个式子就表示 \(q_{head}\) 的决策不如 \(q_{head+1}\) ,将队首弹出。
  • \(slope(q_{tail-1}, q_{tail}) > slope(q_{tail}, i)\) :假设我们后面存在一个 \(a_t\) 使得 \(-a_t \geq slope(q_{tail-1},q_{tail})\) ,那么等到 \(q_{tail-1}\) 弹出之后,一定有 \(-a_t \geq slope(q_{tail},i)\)\(q_{tail}\) 也会被弹出,所以 \(q_{tail}\) 实际上没用,直接弹出即可。

于是做到了 \(O(n)\) 的复杂度。

int head = 1, tail = 1;

for (int i = 1; i <= n; ++i) {
    while (head < tail && slope(q[head], q[head + 1]) <= -A(i))
        ++head;

    f[i] = calc(i, q[head]);

    while (head < tail && slope(q[tail - 1], q[tail]) >= slope(q[tail], i))
        --tail;

    q[++tail] = i;
}

二分队列优化

使用条件:斜率 \(b_j\) 单增。

还是用单调队列维护凸壳,询问时在凸壳上二分即可。

cdq分治优化

设函数 cdq(l, r) 表示求解 \(f_{l \sim r}\) ,返回 \(l \sim r\) 点的凸壳。

先递归处理 \([l, mid]\)\([mid + 1, r]\) 。对于当前层,枚举 \([mid + 1, r]\) 的点,计算 \([l, mid]\) 点的凸壳所产生的贡献。计算完答案后将左右两部分凸壳归并返回。

可以做到 \(O(n \log n)\) 的复杂度。

李超树优化

这是一个很无脑的做法,实现与常数均优于平衡树。

李超树来直接维护一次函数(直线),时间复杂度可以做到 \(O(n \log n)\)

P4655 [CEOI2017] Building Bridges

\(n\) 个柱子,高度为 \(h_{1 \sim n}\) 。若一座桥连接了 \(i, j\) ,付出 \((h_i - h_j)^2\) 的代价。未被桥连接的柱子将会被拆除,付出 \(w_i\) 的代价。求通过桥将 \(1, n\) 两根柱子连接的最小代价,桥不能在端点以外的任何地方相交。

\(n \leq 10^5\)

首先设 \(s_i\) 表示 \(w\) 的前缀和,\(f_i\) 表示联通 \(1, i\) 的代价,则:

\[f_i = h_i^2 + s_{i - 1} + \min_{j = 1}^{i - 1} \{ -2h_ih_j + f_j + h_j^2 - s_j \} \]

用李超树维护直线 \(y = -2h_j x + (f_j + h_j^2 - s_j)\) ,每次查询 \(x = h_i\)\(y\) 的最小值,实际按复杂度 \(O(n \log n)\)

一道类题:Frog 3

#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const ll inf = 1e18;
const int N = 1e5 + 7, V = 1e6 + 7;

struct Line {
	ll k, b;
	
	inline ll operator () (const int x) {
		return k * x + b;
	}
};

ll s[N], f[N];
int h[N], w[N];

int n;

template <class T = int>
inline T read() {
	char c = getchar();
	bool sign = (c == '-');
	
	while (c < '0' || c > '9')
		c = getchar(), sign |= (c == '-');
	
	T x = 0;
	
	while ('0' <= c && c <= '9')
		x = (x << 1) + (x << 3) + (c & 15), c = getchar();
	
	return sign ? (~x + 1) : x;
}

namespace SMT {
Line s[V << 2];

inline int ls(int x) {
	return x << 1;
}

inline int rs(int x) {
	return x << 1 | 1;
}

void maintain(int x, int nl, int nr, Line k) {
	int mid = (nl + nr) >> 1;
	
	if (k(mid) < s[x](mid))
		swap(k, s[x]);
	
	if (nl == nr)
		return;
	
	if (k(nl) < s[x](nl))
		maintain(ls(x), nl, mid, k);
	
	if (k(nr) < s[x](nr))
		maintain(rs(x), mid + 1, nr, k);
}

ll query(int x, int nl, int nr, int pos) {
	if (nl == nr)
		return s[x](pos);
	
	int mid = (nl + nr) >> 1;
	
	if (pos <= mid)
		return min(s[x](pos), query(ls(x), nl, mid, pos));
	else
		return min(s[x](pos), query(rs(x), mid + 1, nr, pos));
}
} // namespace SMT

signed main() {
	n = read();
	
	for (int i = 1; i <= n; ++i)
		h[i] = read();
	
	for (int i = 1; i <= n; ++i)
		s[i] = s[i - 1] + (w[i] = read());
	
	int maxh = *max_element(h + 1, h + 1 + n);
	fill(SMT::s + 1, SMT::s + 1 + 4 * maxh, (Line) {0, inf});
	SMT::maintain(1, 1, maxh, (Line) {-2ll * h[1], f[1] + 1ll * h[1] * h[1] - s[1]});
	
	for (int i = 2; i <= n; ++i) {
		f[i] = 1ll * h[i] * h[i] + s[i - 1] + SMT::query(1, 1, maxh, h[i]);
		SMT::maintain(1, 1, maxh, (Line) {-2ll * h[i], f[i] + 1ll * h[i] * h[i] - s[i]});
	}
	
	printf("%lld", f[n]);
	return 0;
}

应用

P1973 [NOI2011] NOI 嘉年华

给出 \(n\) 个区间,将它们分为两份满足两份区间没有交,可以丢弃区间。

第一问:求两份区间数量较小者的最大值。

第二问:对于所有 \(i \in [1, n]\) ,求强制选取第 \(i\) 个区间时的第一问。

\(n \leq 200\)

首先将时间离散化,设时间值域为 \([1, m]\)

\(cnt(l, r)\) 表示被 \([l, r]\) 完全包含的区间个数,不难 \(O(n^3)\) 预处理得到。

再设 \(f_{i, j}\) 表示以 \(i\) 开始的前缀时刻选择 \(j\) 个区间到第一份,此时第二份最多能选区间的数量。不难得到转移方程:

\[f_{i, j} = \max_{k = 1}^{i - 1} \{ f_{k, j - cnt(k + 1, i)}, f_{k, j} + cnt(k + 1, i) \} \]

答案即为:

\[\max_{i = 0}^n \{ \min(f_{m, i}, i) \} \]

于是第一问可以 \(O(n^3)\) 解决。

对于第二问,钦定强制不丢弃的区间在第一份。设 \(g_{i, j}\) 表示以 \(i\) 开始的后缀时刻选择 \(j\) 个区间到第一份,此时第二份最多能选区间的数量。转移与 \(f\) 是类似的。

那么强制选取的区间 \([l, r]\) 给第二份后,于是有:

\[\max_{i = 1}^{l - 1} \max_{j = r + 1}^m \max_{k = 0}^n \max_{t = 0}^n \min(f_{i, k} + g_{j, t}, k + t + cnt(i + 1, j -1 )) \]

注:因为两份区间地位等价,于是可以钦定 \(cnt(i + 1, j + 1)\) 放第二份。

直接做是 \(O(n^4)\) 的,考虑优化。设:

\[h(l, r) = \max_{i = 0}^n \max_{j = 0}^n \min(f_{l, i} + f_{r, j}, i + j + cnt(l + 1, r - 1)) \]

则答案即为 \(\max_{i = 1}^{l - 1} \max_{j = r + 1}^m h(l, r)\) ,这一部分可以做到 \(O(n^2)\)

接下来考虑如何计算 \(h(l, r)\) 。可以发现 \(\min\) 里面的东西是两份的区间数,由于两份区间地位是相同的,所以会在 \(\min\) 中按两种顺序都会出现。于是可以直接认为 \(k + t + cnt(l + 1, r - 1)\) 是较大值。

接下来问题转化为在右边为较大值的情况下最大化左边的值,注意到 \(f_{l, i}\)\(i\) 的增大而减小,\(g_{r, j}\)\(j\) 的减小而减小。那么当 \(i\) 增加时再减小 \(j\) 显然不优,因此可以单纯减小左、右两边。这一部分时间复杂度降为 \(O(n^3)\)

总时间复杂度 \(O(n^3)\)

#include <bits/stdc++.h>
using namespace std;
const int inf = 0x3f3f3f3f;
const int N = 2e2 + 7, M = 4e2 + 7;

pair<int, int> interval[N];

int cnt[M][M], f[M][N], g[M][N], h[M][M];

int n, m;

signed main() {
	scanf("%d", &n);
	vector<int> vec = {-inf, inf};

	for (int i = 1; i <= n; ++i) {
		scanf("%d%d", &interval[i].first, &interval[i].second);
		interval[i].second += interval[i].first - 1;
		vec.emplace_back(interval[i].first), vec.emplace_back(interval[i].second);
	}

	sort(vec.begin(), vec.end());
	vec.erase(unique(vec.begin(), vec.end()), vec.end());

	for (int i = 1; i <= n; ++i) {
		interval[i].first = lower_bound(vec.begin(), vec.end(), interval[i].first) - vec.begin() + 1;
		interval[i].second = lower_bound(vec.begin(), vec.end(), interval[i].second) - vec.begin() + 1;
	}
	
	m = vec.size();

	for (int i = 1; i <= n; ++i)
		for (int j = 1; j <= interval[i].first; ++j)
			for (int k = interval[i].second; k <= m; ++k)
				++cnt[j][k];

	fill(f[0] + 1, f[0] + 1 + n, -inf);

	for (int i = 1; i <= m; ++i)
		for (int j = 0; j <= n; ++j) {
			f[i][j] = -inf;

			for (int k = 0; k < i; ++k) {
				if (f[k][j] != -inf)
					f[i][j] = max(f[i][j], f[k][j] + cnt[k + 1][i]);

				if (f[k][max(j - cnt[k + 1][i], 0)] != -inf)
					f[i][j] = max(f[i][j], f[k][max(j - cnt[k + 1][i], 0)]);
			}
		}

	int ans = 0;

	for (int i = 0; i <= n; ++i)
		ans = max(ans, min(f[m][i], i));

	printf("%d\n", ans);
	fill(g[m + 1] + 1, g[m + 1] + 1 + n, -inf);

	for (int i = m; i; --i)
		for (int j = 0; j <= n; ++j) {
			g[i][j] = -inf;

			for (int k = i + 1; k <= m + 1; ++k) {
				if (g[k][j] != -inf)
					g[i][j] = max(g[i][j], g[k][j] + cnt[i][k - 1]);

				if (g[k][max(j - cnt[i][k - 1], 0)] != -inf)
					g[i][j] = max(g[i][j], g[k][max(j - cnt[i][k - 1], 0)]);
			}
		}

	for (int i = 1; i <= m; ++i)
		for (int j = i + 2; j <= m; ++j) {
			h[i][j] = -inf;

			for (int k = 0, t = n; k <= n && f[i][k] != -inf; ++k) {
				while (~t) {
					if (min(f[i][k] + g[j][t], k + t + cnt[i + 1][j - 1]) >= h[i][j])
						h[i][j] = min(f[i][k] + g[j][t], k + t + cnt[i + 1][j - 1]);
					else
						break;

					--t;
				}

				++t;
			}
		}

	for (int i = 1; i <= n; ++i) {
		int ans = 0;

		for (int j = 1; j < interval[i].first; ++j)
			for (int k = interval[i].second + 1; k <= m; ++k)
				ans = max(ans, h[j][k]);

		printf("%d\n", ans);
	}

	return 0;
}
posted @ 2024-08-08 19:15  我是浣辰啦  阅读(8)  评论(0编辑  收藏  举报