[JZOJ3233] 照片

题目

题目大意

有一个 01 01 01序列。给你一堆区间,每个区间中有且仅有一个 1 1 1点。
问最多的 1 1 1点个数。


思考历程

感觉这题特别经典,似乎在哪里见过,又好像没有见过。
一开始朝贪心方面想……想不出来……
后来想DP,还是想不出来……
直到WHH大爷跑过来兴奋地说:不就是个差分约束吗!
于是我心态崩了,去搜题解……
然而搜到的全是DP……
理解了一下DP的方法,虽然理解,但是极度不熟练……
可是这题对代码能力的考验又很强……我本想自己打出极其恶心的方法,但后来还是仔细看了看题解(还有)标程。
几乎是对着标打出来了……
心态崩了……


正解

先说DP。
f i f_i fi表示选 i i i的最多的 1 1 1点数目。
方程为 f i = max ⁡ f j + 1 f_i=\max f_j +1 fi=maxfj+1
现在问题是 j j j的范围怎么求。
这就很考验人的分析能力了——从两个方面考虑:

  1. 唯一性:所有包括 i i i的区间内都不能再选别的。这样可以求出右边界 R i R_i Ri,为包含 i i i的区间的最小左边界 − 1 -1 1
  2. 必要性:所有不包括 i i i的区间( i i i之前的)中都必须要有一个选。这样可以求出右边界 L i L_i Li,为不包含 i i i的区间的最大左边界

这样,求出 L i L_i Li R i R_i Ri,就可以转移了。
如果不刻意追求代码的完美,其实这个时候用线段树来辅助转移就好了,简单粗暴(如果是比赛,我肯定就这么打了)。
但是实际上可以优化。
首先可以证明出一个结论: L L L R R R都是递增的。
如果 L i − 1 > L i L_{i-1}>L_i Li1>Li,由于包含了 i i i的区间也会包含 i − 1 i-1 i1(除非区间左边界为 i i i,但显然这种情况下 L i − 1 ≤ L i L{i-1}\leq L_i Li1Li), L i − 1 L_{i-1} Li1为包含 i − 1 i-1 i1区间的最小左边界 − 1 -1 1,所以 L i − 1 L_{i-1} Li1可以被更新,所以不成立。
如果 R i − 1 > R i R_{i-1}>R_i Ri1>Ri,由于不包含 i − 1 i-1 i1的区间也不包含 i i i R i R_i Ri为不包含 i i i区间的最大右边界,所以 R i R_i Ri可以被更新,所以不成立。

既然都是递增的,那就可以很愉快地单调队列DP了……
有了各种单调的性质,代码也可以简单很多,也不需要打线段树或堆了……
然而……如果是在比赛,想这些的时间还不如用来打线段树呢……

还有一种方法是差分约束。
假如我们做一遍前缀和,对于区间 [ l , r ] [l,r] [l,r],就会有 s l + 1 = s r s_l+1=s_r sl+1=sr,相当于 s l + 1 ≥ s r s_l+1\geq s_r sl+1sr s r − 1 ≥ s l s_r-1\geq s_l sr1sl。还有,将每个左右端点排个序记为 x i x_i xi,那么还有 x i − 1 ≤ x i x_{i-1}\leq x_i xi1xi
差分约束即可……当然我没有打过,所以纯属瞎哔哔……


代码

using namespace std;
#include <cstdio>
#include <cstring>
#include <algorithm>
#define N 200010
int n,m;
struct Section{
	int l,r;
} s[N];
inline bool cmps(const Section &a,const Section &b){
	return a.l<b.l || a.l==b.l && a.r<b.r;
}
int L[N],R[N];
int q[N],head,tail;
int f[N];
int main(){
	scanf("%d%d",&n,&m);
	for (int i=1;i<=n+1;++i)
		R[i]=i-1;
	for (int i=1;i<=m;++i){
		scanf("%d%d",&s[i].l,&s[i].r);
		L[s[i].r+1]=max(L[s[i].r+1],s[i].l);
		R[s[i].r]=min(R[s[i].r],s[i].l-1);
	}
	for (int i=n;i>=1;--i)
		R[i]=min(R[i],R[i+1]);
	for (int i=2;i<=n+1;++i)
		L[i]=max(L[i],L[i-1]);
	memset(f,127,sizeof f);
	f[0]=0;
	for (int i=1,j=0;i<=n+1;++i){
		for (;j<=R[i];++j){
			if (f[j]==0x7f7f7f7f)
				continue;
			while (head<=tail && f[q[tail]]<f[j])
				tail--;
			q[++tail]=j;
		}
		while (head<=tail && q[head]<L[i])
			head++;
		if (head<=tail)
			f[i]=f[q[head]]+1;
	}
	if (f[n+1]==0x7f7f7f7f)
		printf("-1\n");
	else
		printf("%d\n",f[n+1]-1);
	return 0;
}

总结

真是一道单调队列的好题。
当然,如果是比赛,我肯定会选择打线段树的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值