P4563 [JXOI2018] 守卫

发布时间 2023-12-05 14:35:27作者: CQWDX

题目传送门 [JXOI2018] 守卫

思路

区间dp。

设状态 $f_{l,r}$ 为在区间 $[l,r]$ 内要放的最少保镖数量。

看到题面第一眼的感觉是不会判两点能否连接。

第二眼发现可以用斜率判。

令 $k_{l,r}$ 为横坐标为 $l,r$ 的两点连线斜率。

有 $k_{l,r}=\frac{h_r-h_l}{r-l}$。

手搓几组样例,得 $\forall x\in[l,r)$,有 $k_{l,r}<k_{x,r}$ 。即对于 $x\in[l,r)$,$(x,r)$ 的连线斜率最小。

且若 $a$ 对于 $b$ 可见,且 $h_c>h_b,c>a$,则 $a$ 对于 $c$ 可见。

令 $p={p_1,p_2\cdots p_m}$ 为 $r$ 点当前能够覆盖的点集。注:下文中 $p$ 按照从右至左斜率单调递减的顺序排序。

注:能被覆盖的点可能不连续。但能被覆盖的点的斜率可以保证从右至左单调递减。

若想为点集连续,可能会导致 “$p$ 为 $r$ 点当前能够覆盖的最远点” 的错误贪心思路。喜提20pt。

如图,$r=G$ 时,$p={E,D,B}$。

接下来是这题个人认为比较难处理的地方。

由于 $p$ 不一定连续,故 $[p_{i+1}+1,p_i-1]$ 对于 $r$ 不可见。

由上述性质得,对于 $j\in[p_i,r)$,$j=p_i$ 是唯一可以覆盖 $[p_{i+1}+1,p_i-1]$ 中至少一个点的点。 即在区间 $(p_{i+1},p_i)$ 右侧唯一能够覆盖到此区间的点为 $p_i$。(有点绕,建议自己手玩一下。)

如图,以 $A$ 为右端点,$[D,A]$ 中能覆盖 $[G,E]$ 的唯一点为 $D$。

当然,也可以选择在 $p_i-1$ 处放置保镖,从内部将点覆盖。

如在点 $E$ 放置保镖。

故 $p_i,p_i-1$ 至少有一处要放置保镖。

这样就成功的将区间 $[p_{i+1}+1,p_i]$ 或 $[p_{i+1}+1,p_i-1]$ 转换为了一个子问题。

区间 $[p_{k+1}+1,p_k-1]$ 的代价即为 $$f_{p_{i+1}+1,p_i-1}=\min{f_{p_{i+1}+1,p_i-1},f_{p_{i+1}+1,p_i}}$$

此时有转移方程:

$$f_{l,r}=\sum\limits_{i=1}^m\min{f_{p_{i+1}+1,p_i-1},f_{p_{i+1}+1,p_i}}$$

时间复杂度 $O(n^3)$,能拿 $70$ pts。

考虑优化。

  • 因为 $p_i$ 是在 $[p_i,r)$ 中与 $r$ 点连线斜率最小的,所以对于$x\in[l,p_i)$,若存在 $f_{x,r}<f_{p_i,r}$,则 $p_{i+1}=x$。 可以考虑将 $p_i$ 在循环里滚动处理。

  • 可以注意到根据上述转移,当处理至 $p$ 点时,区间 $[p,r]$ 可以保证被完全覆盖。有转移方程
    $$f_{l,r}=\min{f_{l,p-1,f_{l,p}}}+f_{p+1,r}$$

干掉一个线性时间复杂度。时间复杂度 $O(n^2)$。

具体实现上有问题的话可以看代码。

#pragma GCC optimize(2)
#include <bits/stdc++.h>
#define eps 1.0000
typedef long long ll;
const int maxn = 5020;
const ll inf = 1e18;
int n;
int h[maxn];
ll f[maxn][maxn], res;
bool check(int a, int b, int p){
	double ka = (h[p] - h[a]) * eps / (p - a);
	double kb = (h[p] - h[b]) * eps / (p - b);
	return ka < kb;
}
int main(){
	scanf("%d", &n);
	for(int i = 1; i <= n; i++) scanf("%d", &h[i]), f[i][i] = 1;
	for(int r = 1; r <= n; r++){
		int pos = -1;
		for(int l = r - 1; l >= 1; l--){
			if(pos == -1 || check(l, pos, r)) pos = l;
			f[l][r] = std::min(f[l][pos - 1], f[l][pos]) + f[pos + 1][r];
		}
	}
	for(int i = 1; i <= n; i++)
		for(int j = i; j <= n; j++)
			res ^= f[i][j];
	printf("%lld", res);
	return 0;
}

初二婴儿,轻喷 qwq

我觉得这篇题解应该是题解区比较详细的?