二分+三分
二分是一种很强的方法,不要因为ta太普遍而忽视
这里指出ta的必要条件:
答案具有单调性
已知答案的情况下,可以判定答案的可行性
也就是说,如果我们发现一个问题不好直接求解,但是我们可以想办法判定一个解得正确性,那么就可以考虑二分
三分实际上就是凸函数上的“二分”
一般用于凸包,二次函数等的最值求解
可能有些题目的凸性不那么明显,那么我们就可以手玩一下,进行简单的判断
经典例题:二分+并查集
经典例题:三分+秦九韶算法
经典例题:线段树分治+凸包+三分
经典例题:分块+凸包+三分
代码变化比较多,只能给出一些比较典型的
//二分最大值
int EF(int l,int r) {
int ans=0;
while (l<=r) {
int mid=(l+r)>>1;
if (check(mid)) ans=max(ans,mid),l=mid+1;
else r=mid-1;
}
return ans;
}
//二分最大值(double)
const double eps=1e-8;
double EF(double l,double r) {
double ans=0;
while (r-l>=eps) {
double mid=(l+r)/2.0;
if (check(mid)) l=mid;
else r=mid;
}
return (l+r)/2.0;
}
//三分上凸函数
int SF(int l,int r) {
int m1,m2;
while (l<r-1) {
m1=(l+r)>>1;
m2=(m1+r)>>1;
if (f(m1)<f(m2)) l=m1;
else r=m2;
}
return f(l)>f(r)? l:r;
}
int SF(int l,int r) {
int m1,m2;
while (l<=r) {
if (r-l<=2) {
if (r-l==0) return f(l);
if (r-l==1) return max(f(l),f(l+1));
if (r-l==2) return max(f(l),max(f(l+1),f(l+2)));
break;
}
m1=l+(r-l)/3;
m2=r-(r-l)/3;
if (f(m1)<f(m2)) l=m1;
else r=m2;
}
}
//三分上凸函数(double)
double SF(double l,double r) {
double m1,m2;
while (r-l>=eps) {
m1=l+(r-l)/3.0;
m2=r-(r-l)/3.0;
if (f(m1)<f(m2)) l=m1;
else r=m2;
}
return (m1+m2)/2.0;
}
莫队
经典例题:序列上的莫队
经典例题:带修改的莫队
经典例题:不带修改的树上莫队
莫队简直就是暴力的王者,优异而且简单易学
可能有一个小缺点:一般需要一个巨大的数组记录类似颜色数之类的东西
注意
莫队的分块
int cmp(const node &a,const node &b)
{
if (a.block!=b.block) return a.block<b.block;
else return a.y<b.y;
}
Q[i].block=(Q[i].x-1)/unit+1;
树上莫队
树上路径的莫队实际上就是搞出一个
dfs
d
f
s
序
根据路径
(u,v)
(
u
,
v
)
的形态不同,询问不同的区间:
lca(u,v)=u||v l c a ( u , v ) = u | | v : [st[u],st[v]] [ s t [ u ] , s t [ v ] ]
lca(u,v)=p l c a ( u , v ) = p : [ed[u],st[v]]+p [ e d [ u ] , s t [ v ] ] + p
如果一个结点扫了奇数次,那么我们加入ta的贡献
如果一个结点扫了偶数次,那么我们减去ta的贡献
如果我们强行加上修改,就像可修改的序列莫队那样搞就好了
struct node{
int x,y,p,id;
}q[N];
int dfn[N],vis[N],cnt[N],tot=0,ans[N];
void update(int x) { //传入结点编号
int co=c[x];
if (vis[x]) { //扫了偶数次
cnt[co]--;
if (!cnt[co]) tot--;
}
else { //扫了奇数次
cnt[co]++;
if (cnt[co]==1) tot++;
}
vis[x]^=1;
}
void solve() {
int l=1,r=0;
for (int i=1;i<=m;i++) {
while (q[i].y<r) update(dfn[r]),r--;
while (q[i].y>r) r++,update(dfn[r]);
while (q[i].x<l) l--,update(dfn[l]);
while (q[i].x>l) update(dfn[l]),l++;
if (q[i].p) update(p); //处理lca
ans[q[i].id]=tot;
if (q[i].p) update(p);
}
}
可修改的莫队
对于可修改的序列莫队,本质上就像整体二分中记录一个修改指针
细节一
注意我们要先移动区间端点,再移动修改指针
这样能防止重复计算,并且保证我们的
cnt
c
n
t
数组中记录的一定是
[L,R]
[
L
,
R
]
的影响
在处理一个询问之前,暴力将指针移动到“能影响这个询问的修改都处理过了”的这么一个位置
在处理修改操作的时候,我们只有这个操作会影响该区间时才进行
update
u
p
d
a
t
e
但是不敢怎么样我们都要
swap
s
w
a
p
(之所以是
swap
s
w
a
p
,是为了我们修改完还要保证能够恢复)
带修改莫队
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
#include<cmath>
using namespace std;
const int N=1000010;
int n,m,cnt[N],cnt_a=0,cnt_q=0,ans[N];
int c[N],tot=0;
struct node{
int x,y,block,num,id;
}q[N];
//x,y 区间左右端点
//block 分块
//num 在当前询问之前有多少个修改 s
struct point{
int x,y;
}a[N];
int cmp(const node &a,const node &b) {
//block=(x-1)/unit+1;
if (a.block!=b.block) return a.block<b.block;
else return a.y<b.y;
}
void update(int x,int z) {
int co=c[x];
if (z==1) {
cnt[co]++;
if (cnt[co]==1) tot++;
}
else {
cnt[co]--;
if (!cnt[co]) tot--;
}
}
void change(int bh,int z,int l,int r) {
if (a[bh].x>=l&&a[bh].x<=r) update(a[bh].x,-1); //删除从前的影响
swap(c[a[bh].x],a[bh].y);
if (a[bh].x>=l&&a[bh].x<=r) update(a[bh].x,1); //添加影响
}
void solve() {
int L=1,R=0,now=0;
for (int i=1;i<=cnt_q;i++) {
while (R<q[i].y) R++,update(R,1); //移动区间端点
while (R>q[i].y) update(R,-1),R--;
while (L<q[i].x) update(L,-1),L++;
while (L>q[i].x) L--,update(L,1);
while (now<q[i].num) now++,change(now,1,L,R); //移动修改标记
while (now>q[i].num) change(now,-1,L,R),now--;
ans[q[i].id]=tot; //维护答案
}
}
int main()
{
scanf("%d%d",&n,&m);
int unit=sqrt(n);
for (int i=1;i<=n;i++) scanf("%d",&c[i]);
for (int i=1;i<=m;i++) {
char s[10];
int x,y;
scanf("%s",s);
scanf("%d%d",&x,&y);
if (s[0]=='Q') {
cnt_q++;
q[cnt_q].x=x; q[cnt_q].y=y;
q[cnt_q].num=cnt_a; q[cnt_q].block=(x-1)/unit+1;
q[cnt_q].id=cnt_q;
}
else {
cnt_a++;
a[cnt_a].x=x; a[cnt_a].y=y;
}
}
sort(q+1,q+1+cnt_q,cmp);
solve();
for (int i=1;i<=cnt_q;i++) printf("%d\n",ans[i]);
}