题解:P4563 [JXOI2018] 守卫

思路

首先观察到一个很重要的性质。

在题目中由于每个保镖只能看到自己的左边,所以对于区间 [l,r][l,r],在 rr 号亭子必然有一个保镖(不然最右边的保镖右边的亭子谁来看着)。

我们画出图分析,发现在 rr 左边最后一个能被看到的亭子(以下记 pp 为这个亭子的位置)的左边就已经成为了一个与其他子区间完全独立的区间,所以考虑动态规划。

状态表示:fl,rf_{l,r} 表示在区间 [l,r][l,r] 之间,最少需要放多少个保镖。

首先我们知道,当 l=rl=r 的时候,fl,r=1f_{l,r}=1。然后我们发现,在得到状态 fl,rf_{l,r} 时,我们不仅需要考虑 [l,p1][l,p-1] 这个区间,还需要考虑 [p+1,r][p+1,r] 这个区间,于是我们考虑维护 sumsum 来求出当前 [p+1,r][p+1,r] 这个区间的保镖数,我们的 ll 需要从大往小枚举。

枚举 ll 的顺序是从大到小的,也就是从右到左,那么 pp 就会被更新。pp 会被更新当且仅当连 llrr 点的斜率小于连 pprr 的斜率(这里读者可以画图来理解)。

首先考虑 sumsum 的计算,当 pp 被更改时,sumsum+min(fl+1,p1,fl+1,p)sum \gets sum+\min(f_{l+1,p-1},f_{l+1,p}),其他情况不变。特别的,当区间大小为一时,sumsum11

接下来,因为我们需要知道 fl,rf_{l,r} 的子区间 fl,pf_{l,p}fl,p1f_{l,p-1} 的值,而 ppp1p-1 是小于 rr 的,所以我们要把 rr 从小到大枚举,这样我们就可以愉快地转移状态了。

状态计算/转移:fl,r=min(fl,p1,fl,p)+sumf_{l,r}=\min(f_{l,p-1},f_{l,p})+sum

Code

#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 5e3 + 10;
int f[N][N], h[N], n, ans;
inline double line(int x, int y) { return (double)(h[x] - h[y]) / (x - y); }
inline bool stop(int x, int y, int z) { return line(z, x) > line(z, y); }
signed main() {
	cin >> n;
	for (int i = 1; i <= n; i++) cin >> h[i];
	for (int r = 1; r <= n; r++) {
		ans ^= (f[r][r] = 1);
		int sum = f[r][r], p = 0;
		for (int l = r - 1; l >= 1; l--) {
			if (!p || !stop(l, p, r)) {//sum只维护了之前的[p+1,r],此时p的位置改变,于是需要更新 sum,即加上 [l+1,p] 或 [l+1,p-1]。
				if(p) sum += min(f[l + 1][p], f[l + 1][p - 1]);// ! 注意当 p 为 0 时,也会进入上个分支,此时 f[l+1][p-1] 会出现访问 f[l+1][-1] 的情况,需要特判掉。
				p = l;
			}
			ans ^= (f[l][r] = min(f[l][p - 1], f[l][p]) + sum);
		}
	}
	cout << ans;
	return 0;
}
posted @ 2024-07-24 11:21  PM_pro  阅读(24)  评论(0)    收藏  举报  来源