指数级搜索(exponetial search)
这是一种用于处理 时间复杂度为 即时间复杂度和序列长度相关的搜索算法。
一般用于需要在序列上不断二分划分出一段段互不相交的合法区间的题目中。
发现二分的问题在于从 出发找以 开头的合法区间的右端点 的时候时间复杂度为 ,总时间复杂度最坏为 ,无法接受。
不难发现问题在于二分的区间太大了,不妨先从小到大枚举 ,找到第一个满足 的 ,然后在 里二分,这样二分的时间复杂度就是 也就是只和当前区间的长度有关,均摊时间复杂度即为 。
参考代码
for(int i=1;i<=n;i++)
{
int rb=-1;
for(int j=0;j<=20;j++)
{
int pre=min(i+(1<<j)-1,n);
if(chk(i,pre))
{
rb=pre;
break;
}
}
if(rb==-1) break;
int lb=i,nxt=0;
while(lb<=rb)
{
int mid=lb+rb>>1;
if(chk(i,mid)) nxt=mid,rb=mid-1;
else lb=mid+1;
}
ans[++anscnt]=nxt;
i=nxt;
}
证明:
大小 的点覆盖的爆搜
每次找到还没被删掉的度数最大的点 ,若其度数 ,则图为若干环和链,可以直接处理。
否则枚举 删还是不删,若 不删则其所有邻居都要被删,故有关于点覆盖大小限制 的时间复杂度递推式 ,这个在 时大约是 。
把 的看作 , 的看作
这个技巧通常搭配二分 来使用,或者直接考虑 序列然后通过这个套路证明某些东西。
例题 1:Magic Breeding
例题 2:【2025NOI模拟赛20】排列
题解
考虑每次询问二分答案 ,对于每个初始数列 ,设 ,那么每次修改操作就相当于让 或者 ,其中 和 是按位与和按位或操作。最后若 则 合法,往上二分,否则往下二分。
观察到 很小, 只有两种取值,所以可以把 压缩成一个二进制数,用 bitset 存下 每种情况的 的取值,修改的时候简单按位操作,二分的时候直接查询即可。
时间复杂度 。
例题 2:A Serious Referee
题解
显然,对于所有的 ,设 。若每个 都能被排序,那么显然 能被排序。因为每个 都能被排序代表排序后不会有任何逆序对。
那么用搜索+剪枝即可。
曼哈顿距离和切比雪夫距离互转
曼哈顿距离:
切比雪夫距离:
把所有点 变成 后两点之间的切比雪夫距离就等于原来的曼哈顿距离,把所有点 变成 后两点的曼哈顿距离就等于原来的切比雪夫距离。
bzoj3170 松鼠聚会 AT_code_festival_2017_quala_d P3439 P2906 P5098 P4648 P7561
网格空间连通性容斥(便便容斥)
是否矩形
网格空间中的黑块形成矩形,当且仅当所有 小正方形(可超出边界)中:
- 恰好有 个小正方形包含恰好一个黑块(限制外围拐点数);
- 没有小正方形包含恰好三个黑块(限制没有洞);
相当于包含一个黑块的小正方形个数加上包含三个黑块的小正方形个数 。
连通块个数
在二维网格图中数中间没有洞的四向连通块个数可以这样容斥:

可以通过做四次扫描线来解决。
八连通也是类似的:

并且这还可以拓展到高维空间,相当于是高维面积乘上 的容斥系数然后加起来。
不会证,感性理解一下就是 是 级别的。
以后可以大胆写整除分块套整除分块。
Jerry Wen 定理
解的一些必要/充分条件的并,很有可能就是解的充要条件。
一些博弈论、图论、构造题可以尝试找必要/充分条件刻画解的充要条件。
按 分块
某些题目中 的无序点对 才有贡献,此时可以按 分块(、 等等为一块)。这样做的好处:
- 令 ,考虑固定 后有贡献的点对 的集合 ;
- 贡献可以分为块内贡献和块间( 所在块和上一块)贡献;
- 块内贡献:正着扫, 中只会加入新元素;
- 块间贡献:倒着扫, 中同样只会加入新元素;
注意要先处理块间贡献。
同余最短路
当模 同余的所有状态等价,要求 表示模 为 的状态中最小的那个时,一般建出 个点代表状态等价类,把状态间的转移映射成这些点间的边,跑最短路求 。
典型应用:
- 给一些数,求至少要拼几次才能拼出模 为 的数;
- 给一些数,求这些数完全背包后能表示的数的个数;
- 给一些数,求这些数完全背包后不能表示的最小/最大数;
例题:
贡献为函数时考虑拆开再算新增贡献
例题:
各种组合意义
排列
- 考虑建立一个 的网格,只有 有标记;
- 考虑连有向边 ,形成若干个置换环;
求 次多项式 次幂的前 项
假设要求 ,设 ,对 求导,则:
那么将 展开成幂级数即可根据 得到 的递推关系,多项式快速幂预处理 然后递推即可。
倍增并查集
用于快速维护两个区间内元素对应相等()。
类似倍增,建 层点,然后并查集。
新增相等关系的时候类似倍增拆成两对区间分别向等,最后从上到下遍历每一层下放相等关系。
具体的,若第 层时 所在集合的根为 ,那么下放相等关系 ,。
而当维护的相等关系在同一个序列上时,可以在线做,每次暴力下放。这样由于每一层只会合并 次,所以总复杂度是 的,其中 是并查集复杂度。
例题:
- P3295 [SCOI2016] 萌萌哒
- 【2025NOI模拟赛10】简单题
判断无向图 的某个边集 是否为割
即判断割掉 中的边后 是否不连通。
考虑 dfs 树,为每条返祖边赋一个随机权值,每条树边的权值则是所有跨过它的返祖边权值的 xor 和。
那么 为割当且仅当存在 的一个非空子集 满足 中边权值 xor 和为 (线性基包含 )。
证明考虑若 非空且其中边权值 xor 和为 ,则显然每个包含一条非树边的简单环都会是以下两种情况之一:
- 未被割掉边
- 被割掉至少两条边
并且所有边中至少割掉了两条边。
理解一下就容易发现满足这些条件的边集一定是 的一个割。
这个做法还可以解决二分割(割掉边后是否是二分图)的题目:
扣掉一个物品的 01 背包
劲题。
可以做到 。
具体的,考虑类似线段树一样分治,处理区间 的时候先加入 中的物品,递归右半边;再撤销掉 中的物品(通过开桶记录加入前的状态实现),递归左半边。这样递归到单点的时候就求出了答案。
时间复杂度是 的。
一个 log 求两个单调序列的第 小
其实可以拓展到求 个单调序列的第 小,不过 log 的底数会变,常数有点大。
假设要求两个单调不降序列 的第 小,那么若 则可以删去 的前 个数,并将 减去 。正确性考虑前 小肯定是 和 (),那么 和 一定有至少一个大于等于 。
某些题目中 和 是非负序列的前缀和,且非负序列带单点修。那么可以考虑对于 和 都建出线段树,然后在线段树上二分,维护 所在的区间 和 所在的区间 ,不难发现每次肯定能砍掉某个区间的一半( 较小的时候砍掉后一半,否则砍掉前一半)。
代码
#include "mitsuha.h"
#include <vector>
#include <cstdio>
#include <map>
using namespace std;
namespace Exber
{
const int S=20005;
struct data
{
Data x;
int id;
};
int cnt;
map<pair<int,int>,bool> cmp;
map<pair<int,int>,data> add;
inline data operator+(data x,data y)
{
int ix=x.id,iy=y.id;
if(ix>iy) swap(ix,iy);
auto u=make_pair(ix,iy);
if(add.find(u)!=add.end()) return add[u];
return add[u]=data{x.x+y.x,++cnt};
}
inline bool operator<(data x,data y)
{
auto u=make_pair(x.id,y.id);
if(cmp.find(u)!=cmp.end()) return cmp[u];
return cmp[u]=(x.x<y.x);
}
inline bool operator>(data x,data y){return !(x<y);}
int n;
data a[S],b[S];
data ta[S<<2],tb[S<<2];
inline void upda(data tr[],int u){tr[u]=tr[u<<1]+tr[u<<1|1];}
void build(data tr[],data a[],int u,int l,int r)
{
if(l==r) return tr[u]=a[l],void();
int mid=l+r>>1;
build(tr,a,u<<1,l,mid),build(tr,a,u<<1|1,mid+1,r);
upda(tr,u);
}
void updp(data tr[],data a[],int u,int l,int r,int p)
{
if(l==r) return tr[u]=a[l],void();
int mid=l+r>>1;
if(p<=mid) updp(tr,a,u<<1,l,mid,p);
else updp(tr,a,u<<1|1,mid+1,r,p);
upda(tr,u);
}
inline void init(int N,int Q,vector<Data> A,vector<Data> B)
{
n=N;
cnt=0;
for(int i=1;i<=n;i++) a[i]=data{A[i-1],++cnt},b[i]=data{B[i-1],++cnt};
build(ta,a,1,1,n),build(tb,b,1,1,n);
}
inline void update(int op,int x,Data y)
{
if(op==1)
{
a[x]=data{y,++cnt};
updp(ta,a,1,1,n,x);
}
else
{
b[x]=data{y,++cnt};
updp(tb,b,1,1,n,x);
}
}
Data que(int ua,int la,int ra,int ub,int lb,int rb,int k,data sma,data smb)
{
// printf("[%d %d] [%d %d] %d %d %d\n",la,ra,lb,rb,k,sma.a,smb.a);
if(la!=ra&&lb!=rb)
{
int mida=la+ra>>1;
int midb=lb+rb>>1;
int lsa=mida-la+1;
int lsb=midb-lb+1;
if(lsa>=k) return que(ua<<1,la,mida,ub,lb,rb,k,sma,smb);
if(lsb>=k) return que(ua,la,ra,ub<<1,lb,midb,k,sma,smb);
if(lsa+lsb<k)
{
data sa=sma+ta[ua<<1];
data sb=smb+tb[ub<<1];
if(sa<sb) return que(ua<<1|1,mida+1,ra,ub,lb,rb,k-lsa,sa,smb);
else return que(ua,la,ra,ub<<1|1,midb+1,rb,k-lsb,sma,sb);
}
else
{
data sa=sma+ta[ua<<1]+a[mida+1];
data sb=smb+tb[ub<<1]+b[midb+1];
if(sa>sb) return que(ua<<1,la,mida,ub,lb,rb,k,sma,smb);
else return que(ua,la,ra,ub<<1,lb,midb,k,sma,smb);
}
}
else if(la!=ra)
{
int mida=la+ra>>1;
int lsa=mida-la+1;
data sa=sma+ta[ua<<1];
data sb=smb+b[lb];
if(lsa+(sa>sb)>=k) return que(ua<<1,la,mida,ub,lb,rb,k,sma,smb);
else return que(ua<<1|1,mida+1,ra,ub,lb,rb,k-lsa,sa,smb);
}
else if(lb!=rb)
{
int midb=lb+rb>>1;
int lsb=midb-lb+1;
data sb=smb+tb[ub<<1];
data sa=sma+a[la];
if(lsb+(sb>sa)>=k) return que(ua,la,ra,ub<<1,lb,midb,k,sma,smb);
else return que(ua,la,ra,ub<<1|1,midb+1,rb,k-lsb,sma,sb);
}
else
{
data sa=sma+a[la];
data sb=smb+b[lb];
if(sa>sb) swap(sa,sb);
return k==1?sa.x:sb.x;
}
}
inline Data query(int k)
{
// puts("------");
return que(1,1,n,1,1,n,k,data{emptyData,0},data{emptyData,0});
}
}
void init(int N, int Q, vector<Data> A, vector<Data> B){Exber::init(N,Q,A,B);}
void update(int op, int x, Data y){Exber::update(op,x,y);}
Data query(int k){return Exber::query(k);}