[CF39C]Moon Craters

15 篇文章 0 订阅
14 篇文章 0 订阅

题目

传送门 to luogu

思路

在很多年以前,有一道叫做守卫,是一道区间 d p \tt dp dp 的题,而我惨痛爆零。它教给了我很重要的一课, d p \tt dp dp 是需要发现相互独立的子问题的。

现在,我们回到这道题。假设我们选择了一个圆,将剩余的圆分成两组,使得其互不干扰,能做到吗?

答案是可以,因为一个圆的边界会自然地切开空间。也就是说,我们选择了一个圆,然后只有两种圆还可以选,一种在圆内(包含或者内切),一种在圆外(相离或外切),而这两种是 互不干扰 的。

话说,我们可以先将圆转化为其直径,问题就变成了选择很多线段。但是后文我可能仍然称呼其为圆。

为了让圆内、圆外这两组的分布不那么离谱,我们将所有的圆按照左端点排序。我们每次只决策最靠左的一个圆,就会让剩余的圆的分布自然而美好。对于左端点相同的,我们自然是应该先决策右端点大的。然后剩下的圆就可以分成两部分,依据左端点是否小于当前圆的右端点。

但是,我们很悲伤的发现,我们没有完全摆脱选择的这个圆对别人的约束。我们得把它塞到状态里。我们可以写出一个 f ( i , j , x ) f(i,j,x) f(i,j,x) 表示,第 i i i 到第 j j j 个圆可选,右端点不可以超过 x x x 。这是 O ( n 3 ) \mathcal O(n^3) O(n3) 的。怎么办?

突然发觉, j j j x x x 有必然的联系。因为我们的 j j j 是左端点小于 x x x 的圆中,最右的一个。如果二者有必然联系,我们可不可以试着将二者用一个值来表示?

f ( i , j ) f(i,j) f(i,j) 表示,从第 i i i 个圆开始,考虑所有左端点不超过 j j j 的圆,且不能选择包含 j j j 的圆。这里的 j j j 明显只有 n n n 个取值了,就是每个圆的右端点。这样就完全可做了!

R i R_i Ri 表示, ∀ \forall 圆,左端点不超过第 i i i 个圆的右端点,最靠右的一个。若圆 i i i 右端点为 m i m_i mi ,那么

f ( i , j ) = max ⁡ [ f ( i + 1 , j ) , f ( i + 1 , m i ) + f ( R i + 1 , j ) + 1 ] f(i,j)=\max[f(i+1,j),f(i+1,m_i)+f(R_i+1,j)+1] f(i,j)=max[f(i+1,j),f(i+1,mi)+f(Ri+1,j)+1]

左边表示不选,右边表示选。注意,这个圆本身也要满足不能包含 j j j ,所以 m i ≤ j m_i\le j mij

R i R_i Ri 可以用 O ( n log ⁡ n ) \mathcal O(n\log n) O(nlogn) 预处理, j j j m i m_i mi 可以 O ( n log ⁡ n ) \mathcal O(n\log n) O(nlogn) 压缩至 [ 1 , n ] [1,n] [1,n] ,所以总复杂度是 O ( n 2 ) \mathcal O(n^2) O(n2)

代码

#include <cstdio>
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
inline int readint(){
	int a = 0; char c = getchar(), f = 1;
	for(; c<'0'||c>'9'; c=getchar())
		if(c == '-') f = -f;
	for(; '0'<=c&&c<='9'; c=getchar())
		a = (a<<3)+(a<<1)+(c^48);
	return a*f;
}
template < class T >
void getMax(T&a,T b){ if(a < b) a = b; }

const int MaxN = 2001;
// dp[i][j]:从第i个开始,直到左端点不小于R[j]
// R[j]:将所有右端点取出来排序
int dp[MaxN][MaxN], R[MaxN];

struct Range{
	int l, r, id;
	bool operator < (const Range &that) const {
		if(l != that.l)
			return l < that.l;
		return r > that.r;
	}
} node[MaxN];

// Rid:该区间右端点对应哪个R
int Rid[MaxN], nxt[MaxN], n;
void output(int i,int j){
	if(i == n) return ;
	if(dp[i][j] == dp[i+1][j])
		return output(i+1,j);
	printf("%d ",node[i].id+1);
	output(i+1,Rid[i]), output(nxt[i],j);
}

int main(){
	n = readint();
	for(int i=0; i<n; ++i){
		int o = readint(), x = readint();
		node[i].l = o-x, node[i].r = o+x;
		node[i].id = i, R[i] = node[i].r;
	}
	sort(node,node+n), sort(R,R+n);
	for(int i=0; i<n; ++i)
		for(int j=0; j<n; ++j)
			if(node[i].r == R[j])
				Rid[i] = j;
	for(int i=0; i<n; ++i){
		nxt[i] = n;
		for(int j=0; j<n; ++j)
			if(node[i].r <= node[j].l){
				nxt[i] = j; break;
			}
	}
	for(int i=n-1; ~i; --i)
		for(int j=0; j<n; ++j){
			dp[i][j] = dp[i+1][j]; // 不选
			if(node[i].r <= R[j]) // 选
				getMax(dp[i][j],dp[i+1][Rid[i]]
					+dp[nxt[i]][j]+1);
		}
	printf("%d\n",dp[0][n-1]), output(0,n-1);
	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值