P4243 [JSOI2009]等差数列 题解
Post time: 2020-02-01 18:37:08
题目链接Link,题目内容不再赘述。
这题我用的是线段树,因为是区间操作,线段树比较好写主要指的是代码比较短,而且错误率不高,常数对于我这种蒟蒻来说已经很不错了。
不会线段树的点这里Link
下面我们来解决一下操作:
A s t a b
:把 \(s\sim t\) 的数分别加上一个等差数列,数列首项为 \(a\),公差为 \(b\)。
对于这个修改操作,很容易想到把线段树维护的每个单点定义成差分数组,即:
for(int i=1;i<=n;i++) b[i]=a[i]-a[i-1];
这样这一个操作就可以通过一次区间操作和两次单点修改来解决:
modify(1,l,l,x);
if(l!=r) modify(1,l+1,r,y);
if(r!=n) modify(1,r+1,r+1,-x-(r-l)*y);
这个地方要提醒大家注意操作的范围。如果 \(l=r\) 只需要修改一个数即可,如果 \(r=n\) 就可以省去差分数组最后的那个单点修改操作(这个卡了好久。
B s t
:问把区间 \([s,t]\) 划分为若干个等差的段后,最少的段数。
这个操作才是这题黑色的原因,因为它并不像看起来那样简单。很容易会想到记录每个点与上个点的差值是否相等,相等为 \(1\),不等为 \(0\),然后通过判断真假来合并答案。
但是这样会有很大的问题,最难解决的就是两个数不论前一个数怎么样,这两个数一定是等差数列。
这样就会发现很难得到转移方程。所以我们这样来定义:
struct data{
int s[2][2],lval,rval;
//lval和rval是左右端点的差分值
//其中s[0/1][0/1]表示去掉左端点或去掉右端点后这个区间的最小段数
//0表示不包含,1表示包含,如s[0][1]表示包含右端点但不包含左端点
};
这样我们可以推出一个复杂的转移,我是运用重载运算符来实现的。
t[p].x=t[ls].x+t[rs].x;
data operator +(const data &y)const{
data x;
x.lval=lval,x.rval=y.rval;
x.s[0][0]=s[0][1]+y.s[1][0]-(rval==y.lval);
x.s[0][0]=min(x.s[0][0],min(s[0][0]+y.s[1][0],s[0][1]+y.s[0][0]));
x.s[1][0]=s[1][1]+y.s[1][0]-(rval==y.lval);
x.s[1][0]=min(x.s[1][0],min(s[1][0]+y.s[1][0],s[1][1]+y.s[0][0]));
x.s[0][1]=s[0][1]+y.s[1][1]-(rval==y.lval);
x.s[0][1]=min(x.s[0][1],min(s[0][0]+y.s[1][1],s[0][1]+y.s[0][1]));
x.s[1][1]=s[1][1]+y.s[1][1]-(rval==y.lval);
x.s[1][1]=min(x.s[1][1],min(s[1][0]+y.s[1][1],s[1][1]+y.s[0][1]));
return x;
}
可能不太好理解,我举一个小例子:
x.s[0][0]=s[0][1]+y.s[1][0]-(rval==y.lval);
x.s[0][0]=min(x.s[0][0],min(s[0][0]+y.s[1][0],s[0][1]+y.s[0][0]));
- 对于一个区间的
s[0][0]
(即开区间,不再赘述)来说:
它可以是:
左子区间的左开右闭区间 \(+\) 右子区间的左闭右开区间
如果左子区间的右端点的差分值 \(=\) 右子区间右端点的差分值,那么左子区间最右边的那段就可以和右子区间最左边的那段连为一段,即总段数 \(-1\)。
另外就是对于零散值的判断,在上面代码的第二行进行了判断。这样就完成了。
注意一些线段树具体细节即可。
点击查看代码
#include<iostream>
#include<cstdio>
#define N (1000000+21)
#define INF (1000000+21)
#define ls p<<1
#define rs p<<1|1
#define md (t[p].l+t[p].r)>>1
//记得define要加括号!不然会死的很惨!
using namespace std;
struct data{
//新定义一个struct可以重载运算符,操作较为方便。
int s[2][2],lval,rval;
//重载运算符转移
data operator +(const data &y)const{
data x;
x.lval=lval,x.rval=y.rval;
x.s[0][0]=s[0][1]+y.s[1][0]-(rval==y.lval);
x.s[0][0]=min(x.s[0][0],min(s[0][0]+y.s[1][0],s[0][1]+y.s[0][0]));
x.s[1][0]=s[1][1]+y.s[1][0]-(rval==y.lval);
x.s[1][0]=min(x.s[1][0],min(s[1][0]+y.s[1][0],s[1][1]+y.s[0][0]));
x.s[0][1]=s[0][1]+y.s[1][1]-(rval==y.lval);
x.s[0][1]=min(x.s[0][1],min(s[0][0]+y.s[1][1],s[0][1]+y.s[0][1]));
x.s[1][1]=s[1][1]+y.s[1][1]-(rval==y.lval);
x.s[1][1]=min(x.s[1][1],min(s[1][0]+y.s[1][1],s[1][1]+y.s[0][1]));
return x;
}
};
struct SegTree{int l,r,add;data x;}t[N*4];
int a[N],b[N];
int n,m;
//然后底下的都是线段树的基本操作了
void build(int p,int l,int r){
t[p].l=l,t[p].r=r;
if(l==r){
t[p].x.lval=t[p].x.rval=b[l];
t[p].x.s[0][1]=t[p].x.s[1][0]=t[p].x.s[1][1]=1;
return;
}
int mid=(l+r)>>1;//尽量用位运算
build(ls,l,mid);
build(rs,mid+1,r);
t[p].x=t[ls].x+t[rs].x;
}
void pushup(int p,int v){
t[p].add+=v;
t[p].x.lval+=v;
t[p].x.rval+=v;
}
void pushdown(int p){
if(!t[p].add||t[p].l==t[p].r) return;
pushup(ls,t[p].add);
pushup(rs,t[p].add);
t[p].add=0;
}
data query(int p,int l,int r){
if(l<=t[p].l&&t[p].r<=r) return t[p].x;
pushdown(p);
int mid=md;
if(r<=mid) return query(ls,l,r);
if(l>mid) return query(rs,l,r);
return query(ls,l,r)+query(rs,l,r);
}
void modify(int p,int l,int r,int v){
if(l<=t[p].l&&t[p].r<=r){
pushup(p,v);
return;
}
pushdown(p);
int mid=md;
if(l<=mid) modify(ls,l,r,v);
if(r>mid) modify(rs,l,r,v);
t[p].x=t[ls].x+t[rs].x;
}
int main(){
scanf("%d",&n);
for(int i=1;i<=n;i++) scanf("%d",&a[i]);
for(int i=1;i<=n;i++) b[i]=a[i]-a[i-1];//差分数列
build(1,1,n);
scanf("%d",&m);
for(int i=1;i<=m;i++){
char c;
int l,r,x,y;
cin>>c;//读入字符一定不要用scanf和getchar!会出人命的!
if(c=='A'){
scanf("%d%d%d%d",&l,&r,&x,&y);
modify(1,l,l,x);
if(l!=r) modify(1,l+1,r,y);//注意这里的判断,不然可能会出现一些奇奇怪怪的错误,得不偿失
if(r!=n) modify(1,r+1,r+1,-x-(r-l)*y);
}
else{
scanf("%d%d",&l,&r);
if(l==r) printf("1\n");
else printf("%d\n",query(1,l+1,r).s[1][1]);
}
}
return 0;
}