UVA12170 Easy Climb 题解(单调队列优化DP)

请喜之郎在第六次化峥的路上遇到了 \(n\) 座大山排成一排,编号从 \(1\)\(n\)。每座山有高度 \(h_i\)

请喜之郎精通奇门遁甲之术,能用法术移动山石。一次操作中,喜之郎选定一座山 \(2 \le i \le n - 1\),令
\(h_i \leftarrow h_i - 1\)\(h_i \leftarrow h_i + 1\)
同时操作不能使山的高度修改为负数。

特别注意:操作不能改变第一座山和最后一座山的高度!

爬山艰辛劳累,为了保存喜之郎的体力,喜之郎希望操作结束后任意相邻两座山的高度差绝对值不大于 \(d\)。即要求满足:

\[|h_i - h_{i+1}| \le d \quad (1 \le i < n). \]

求达到这一条件的最小操作次数。如果无解,输出 \(-1\)

这个题考场上想到 slope trick 去了。但是事实上不需要那么麻烦(slope trick 是否可做其实我还不知道)。

一个朴素的想法是,设 \(f_{i,j}\) 为考虑了前 \(i\) 个数,第 \(i\) 个数改为了 \(j\) 的最小代价,有转移式:

\[f_{i,j}=\min_{k\in[j-d,j+d]}f_{i-1,k}+|a_i-j| \]

要优化这个 DP,首先需要观察到一个非常厉害的性质:如果我们 \(a_{i-1}\) 确定了,那么 \(a_i\) 其实只有三种取值:\(a_{i-1}-d,a_i,a_{i-1}+d\),分别对应:在区间下方要移动到区间的最低点,在区间里不需要更改,在区间上方要移动到区间的最高点。

这个性质其实还是好观察到的。因为假设你没有移动到一个极值点,但是你移动了,那么你移动到对应的极值点一定不劣(你 \(a_i\) 移动了,后面的 \(a_{i+1}\) 就不需要移动了),且合法性也不劣(之前合法的现在还是合法)。

然后,根据这个性质,你就可以知道,所有合法的 \(j\) 一定能被表示为某个 \(a_t+kd\) 的形式,因为你每次修改一定会把他修改为上一个的 \(d\) 倍,或者干脆不修改。于是,所有合法的取值其实只有 \(O(n^2)\) 种,所以你第二维可以优化到 \(O(n^2)\) 的级别。

于是你用单调队列优化转移,最后时间复杂度是 \(O(n^3)\) 的。

const int MAXN = 305;
int n, d, a[MAXN], f[2][MAXN * MAXN];

void work() {
	cin >> n >> d;
	for (int i = 1; i <= n; ++i)
		cin >> a[i];
	if (abs(a[n] - a[1]) > (n - 1) * d)
		return cout << -1 << endl, void();
	set<int> st;
	for (int i = 1; i <= n; ++i) {
		for (int j = -n; j <= n; ++j) {
			int val = a[i] + j * d;
			if (val >= 0) st.insert(val);
		}
	}
	vector<int> vals;
	map<int, int> mp;
	for (auto x:st) {
		vals.push_back(x);
		mp[x] = (int)vals.size() - 1;
	}
	int V = vals.size();
	for (int i = 0; i < V; ++i) {
		f[1][i] = INT_MAX;
	}
	f[1][mp[a[1]]] = 0;
	for (int i = 2; i <= n; ++i) {
		fill(f[i & 1], f[i & 1] + 1 + V, INT_MAX);
		deque<int> q;
		int k = 0;
		for (int j = 0; j < V; ++j) {
			while (q.size() && vals[q.front()] < vals[j] - d) q.pop_front();
			while (k < V && vals[k] < vals[j] - d) ++k;
			while (k < V && vals[k] <= vals[j] + d) {
				while (q.size() && f[i & 1 ^ 1][q.back()] >= f[i & 1 ^ 1][k]) q.pop_back();
				q.push_back(k);
				k++;
			}
			f[i & 1][j] = f[i & 1 ^ 1][q.front()] + abs(a[i] - vals[j]);
		}
	}
	cout << f[n & 1][mp[a[n]]] << endl;
}

\(O(n\log n)\) 解法

注意到我们的转移式相当于是对一个凸函数取区间 \(\min\)(相当于是对 \(0_{[l,r]}\) 做闵可夫斯基和)再加上另一个另一个凸函数,所以最终结果也一定是一个凸函数,所以我们可以直接维护凸包做。考场上卡住的点主要是在于,初始时的凸包是一个点(\(a_1\))为 \(0\),其他点都为 \(+\inf\),这个我不知道怎么处理。

posted @ 2025-11-22 14:33  小蛐蛐awa  阅读(3)  评论(0)    收藏  举报