知识点总结
基础算法篇
搜索
- IDA*:适用于答案较小的问题,枚举答案变成判定性问题。函数中一般有估价函数,计算最好情况所需要的步数。枚举所重复的复杂度可忽略不计。[UVA1343]
- 双向广搜:其实并不一定快……[POJ1915]
- 记忆化搜索:能存下的状态结合hash大胆存……[BZOJ3139]
贪心
重点当然是证明……做USACO某题的时候有一种对序列上的贪心比较通用的证明方法:假设贪心序列为P,且存在更优序列Q,则排序后第一个不同的区间P[r]和Q[r]blabla……[BZOJ1828]
规划篇
动态规划
DP类型:
- 状压DP:最近看到了一些神题,状压可以表示各种状态……不只是简单的选和不选。一般看数据范围就要想想状压,15的大约是 O(3n) ,20是 O(2n) 。[BZOJ3591]
- 数位DP:基本思想是如果要统计小于x的满足要求的数,那么这些数可以按照第一个小于x的位的位置分类计算。
- 树形DP:把问题搬到树上,由子树信息获得当前信息;技巧:左儿子右兄弟。当然,还有一些题不能用传统思路解决……要跳出思维局限。[BZOJ4033]
优化技巧:
- 斜率优化:将转移式中与i相关和无关的项分开,再得到形如
slope(j,k)=y(k)−y(j)x(k)−x(j)<d[i]
的式子,意义为两个转移
j
和
k 一个比另一个优,再由三个转移 x ,y , z 的slope(x,y) 和 slope(y,z) 的关系将不可能的转移 y 排除,剩下的斜率形成凸包,每次取最优值。[BZOJ1096] - 决策单调性:1D1D的通用优化。考虑每个已经计算出的点可能成为哪些点的决策点,如果决策具有单调性,则会形成一些单调的连续段;至于维护,考虑用一个栈储存,每次弹出所有不够优的决策,如果有部分较优,用二分找到分界点。标准
O(nlogn) 。[BZOJ2216] - 四边形不等式:
其实我不知道这是啥[CODEVS3002]
线性规划
单纯形是什么我不会><@喵
只做过网络流跑的假冒伪劣版……技巧:差分使每个变量出现两次,由等式得到流量平衡关系,自己脑补建图。[BZOJ1061]
01分数规划
对于式子
r=∑ci∗xi∑di∗xi,xi∈{0,1}
,要求出r的最值,可以二分r的值,然后将分母乘到左边后移项,得到
t=∑xi∗(ci−di)
,根据t与0的大小关系调整二分的值。当然这个t是有严格的定义和证明的,然而每次做题我都感性分析……[BZOJ1486]
还有一种Dinkelbach算法,每次在已确定范围内随一个值,根据结果计算每个
xi
。//反正就是玄学
图论篇
树
- 树的直径:DP或者DFS。
- 树的重心与点分治:重心的定义为去掉该点后剩余最大子树最小的点。简单DP求出。这样,得到的重心u就有一个好:任意子树v满足
size[v]≤size[u]/2
。那么任意节点所在的分治结构不超过
logn
个。然后就可以在这个结构上做一些愉快的事情了。[BZOJ4021]
有时候要用到启发式合并,通常满足某个变量与子树大小有关。[BZOJ3697] - 虚树:适用的题目一般都有固定格式:给出一些询问,每次有一些关键点,且关键点的个数和不超过blabla。构造虚树的目的是减小问题规模,可以做到大小为 O(m) 。用栈维护根到当前点的路径。[BZOJ3572]
- 树链剖分:树分治的一种。结合数据结构维护信息。主要目的是维护链的信息。
- dfs序:对子树进行操作时的常用手段。一般在进栈位置加,出栈位置减。
- 动态树:只会LCT。维护森林的连通性和链(子树)的信息。
void flip(int u)
{
a[u].f^=1;
swap(a[u].ch[0],a[u].ch[1]);
}
void pushdown(int u) {}
void maintain(int u) {}
void rotate(int u)
{
int x=a[u].pa,y=a[x].pa,d=(a[x].ch[1]==u);
if(!y) pa[u]=pa[x],pa[x]=0;
else a[y].ch[a[y].ch[1]==x]=u;
a[x].ch[d]=a[u].ch[d^1];a[a[u].ch[d^1]].pa=x;
a[u].ch[d^1]=x;a[x].pa=u;a[u].pa=y;
maintain(x);maintain(u);
}
void splay(int u)
{
int v=u;
while(v) st[++top]=v,v=a[v].pa;
while(top) pushdown(st[top]),top--;
while(a[u].pa)
{
int x=a[u].pa,y=a[x].pa;
if(!y) {rotate(u);return;}
if(a[x].ch[0]==u^a[y].ch[0]==x) rotate(u);
else rotate(x);
rotate(u);
}
}
void access(int u)
{
int v,x;
splay(u);
if(v=a[u].ch[1])
pa[v]=u,a[v].pa=a[u].ch[1]=0,maintain(u);
while(v=pa[u])
{
splay(v);
if(x=a[v].ch[1])
pa[x]=v,a[x].pa=0;
a[v].ch[1]=u;a[u].pa=v;pa[u]=0;
maintain(v);splay(u);
}
}
void sroot(int u)
{
access(u);flip(u);
}
void get(int u,int v)
{
sroot(u);access(v);
}
bool query(int u,int v)
{
get(u,v);
while(a[v].ch[0])
v=a[v].ch[0];
return u==v;
}
void join(int u,int v)
{
access(u);sroot(v);
a[u].ch[1]=v;a[v].pa=u;
maintain(u);
}
void cut(int u,int v)
{
get(u,v);
a[u].pa=a[v].ch[0]=0;
maintain(v);
}
最短路
- 省选题考最短路都是在基本算法上的变形…所以要领悟各种算法的本质。
网络流
- 最大流:构图一般比较明显,可以求最大值,也可以和二分结合判定一些东西是不是满流。[BZOJ3504]
- 最小割:
常见类型:有一些条件可以获利,一些要损耗,求最大收益。[BZOJ 2039]
最大权闭合子图:原图的边连 inf , s 到正权点连w[i] ,负权点到 t 连−w[i] ,初始利润为 ∑w[i](w[i]>0) ,减去最小割;
建图时优化点数:[BZOJ 3511] 把本来要拆的点通过讨论两个点属于哪个割变成边上的流量。遇到/2的不要真的用double……
平面图最小割与最短路的相互转化。[BZOJ 1001] [BZOJ 2007]
数据结构优化建图:如果有很多点连边方式相同(inf),可以先建好线段树,内部连好inf的边,然后覆盖区间就可以了。注意线段树的点数可能比想象的要多……极限数据要看一下最大值。 - 费用流:学得不好,总是想不到费用流……费用流可以提供双重限制:流量和费用最小。有一些看上去是图论的题又不知道往哪个方向想的时候可以尝试一下。
二分图
- 最大匹配:匈牙利算法。 O(n2)
bool find(int x)
{
if(vst[x]) return 0;
vst[x]=1;
for(int i=tail[x];i;i=e[i].next)
if(!match[e[i].v] || find(match[e[i].v]))
{
match[e[i].v]=x;
return 1;
}
return 0;
}
- 最大完备匹配:KM算法。 O(n3)
bool find(int u)
{
px[u]=1;
for(int i=1;i<=n;i++)
if(!py[i])
{
int t=x[u]+y[i]-d[u][i];
if(!t)
{
py[i]=1;
if(!match[i] || find(match[i])) {match[i]=u;return 1;}
}
else slack[i]=min(slack[i],t);
}
return 0;
}
void km() //已知最大匹配为n
{
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
x[i]=max(x[i],d[i][j]);
for(int i=1;i<=n;i++)
{
memset(slack,127,sizeof(slack));
while(true)
{
memset(px,0,sizeof(px));
memset(py,0,sizeof(py));
if(find(i)) break;
int d=INT_MAX;
for(int i=1;i<=n;i++)
if(!py[i]) d=min(d,slack[i]);
for(int i=1;i<=n;i++)
{
if(px[i]) x[i]-=d;
if(py[i]) y[i]+=d;
else slack[i]-=d;
}
}
}
}
Hall定理 :[BZOJ 1135]
二分图形成完备匹配的条件:X中的任意k个点至少与Y中的k个点相邻,即X的任意子集的临集大小>=该子集大小。
二分图染色:dfs一遍,判奇环。
- 经典模型:矩阵的行与列、最小路径覆盖……
- 性质:二分图跑dinic的复杂度是 O(nn−−√) 。
2-SAT
- 限制(a,b):a和b不能同时选->选了a’必须选b && 选了b’必须选a;连边、缩点。
判断是否有解:看a与a’是否在同一scc;
输出方案:按拓扑序从底向下选择。没有选的点a加入,删除a’;
字典序最小解:从小到大把没有选的点加入,dfs染色。
连通分量
强联通分量
- tarjan/kosaraju
割顶与桥
void dfs(int u,int pa)
{
int son=0;
d[u]=l[u]=++timer;
for(int i=tail[u];i;i=e[i].next)
if(!d[e[i].v])
{
son++;
dfs(e[i].v,u);
l[u]=min(l[u],l[e[i].v]);
if(l[e[i].v]>=d[u]) cut[u]=1;
if(l[e[i].v]>d[u]) bri[i]=1;
}
else if(e[i].v!=pa) l[u]=min(l[u],d[e[i].v]);
if(!pa && son<2) cut[u]=0;
}
点双连通分量
- 任意两条边在同一简单环内;无割顶。
边双连通分量
- 每条边在至少一个环内;没有桥边。
数学篇
数论
- 扩展欧几里得:弥天大坑……打了无数遍还不记得最小正整数解怎么搞。这里再写一遍:由于取余的结果符号与被除数相同,因此 x=((x0%mod)+|mod|)%mod
int exgcd(int a,int b,int &x,int &y)
{
if(!b) {x=1,y=0;return a;}
int res=exgcd(b,a%b,x,y);
int t=x;x=y;y=t-a/b*y;
return res;
}
- 欧拉函数: φ(x)=x∑d is a primed|nd−1d
int phi(int x)
{
int t=x,res=x;
for(int i=2;i<=t;i++)
if(t%i==0)
{
while(t%i==0) t/=i;
res=res/i*(i-1);
}
if(t>1) res=res/t*(t-1);
return res;
}
- 欧拉定理: aφ(n)≡1(mod n)
- 指数循环节: ab≡ab%φ(p)+φ(p)(mod p)
- 莫比乌斯反演: ∑d|nμ(d)=[n==1]
- Lucas定理:用来计算
Cmn%P
,P是比较小的质数。如果P是合数,用中国剩余定理合并。
lucas(n,m)=lucas(n/p,m/p)∗C(n%p,m%p) 中国剩余定理:
已知 ⎧⎩⎨⎪⎪⎪⎪⎪⎪x≡b1(mod m1)x≡b2(mod m2)……x≡bk(mod mk) ,其中 mi 互质。令 M=∏mi ,则 x 的最小整数解为
∑ki=1biinv(Mmi)(mod mi) 。裴蜀定理
两个数之间的就是exgcd的基础?若a,b是整数,且(a,b)=d,那么对于任意的整数x,y,ax+by都一定是d的倍数;特别地,一定存在整数x,y,使ax+by=d成立。
对于n元的情况:方程 ∑xiai=d 有整数解,当且仅当 gcd(ai)|d 。
矩阵乘法
- 经常用来算s到t的路径方案数……[BZOJ 1297]
高斯消元
- 不知道要说啥……感觉有时候可以打打暴力
线性基
- 可以用高斯消元实现,也可以用更漂亮的打法
for(int i=1;i<=n;i++)
for(int j=63;j>=0;j--)
if((a[i]>>j)&1)
{
if(!lb[j]) {lb[j]=a[i];break;}
else a[i]^=lb[j];
}
置换
- Polya定理:等价类个数为所有置换
f
的
km(f) 的平均数,其中 k 为颜色个数,m(f) 为置换f的循环节。
矩阵树定理
- 最基本的Matrix-Tree定理:矩阵G=度数矩阵-邻接矩阵,那么这个矩阵的n-1阶主子式的行列式为生成树的方案数。
然而在[BZOJ 3534]发现了这个:
真正的Matrix-Tree定理,其实是令 G(i,i)=∑j≠iG(i,j) ,最终行列式的绝对值 |det(G)|=∑∏(i,j)∈EG(i,j) 其中E为生成树的边集。
其实觉得他写得有点迷……就是把邻接矩阵的1换成权值 w[i][j] , G[i][i]=∑G(i,j) ,得到的行列式的值就变成了所有生成树的权值和。
- 顺便回顾一下矩阵的初等变换……长得就很适合高斯消元……
- 交换两行变号;
- 一行乘以常数加到另一行值不变;
- 一行乘以常数,结果乘以常数;
博弈论
- 裸题都是各种sg异或一下。然而非裸题不是打表就是奇怪的贪心/大胆猜测,一点都不好玩→_→
FFT
typedef complex<double> C;
void fft(C*a,int f)
{
for(int i=0;i<n;i++)
if(i<rev[i]) swap(a[i],a[rev[i]]);
for(int l=2;l<=n;l<<=1)
{
int h=l>>1;
C ur(cos(pi/h),f*sin(pi/h));
for(int i=0;i<n;i+=l)
{
C w(1,0);
for(int k=i;k<i+h;k++,w*=ur)
{
C x=a[k],y=a[k+h]*w;
a[k]=x+y;a[k+h]=x-y;
}
}
}
if(f==-1)
for(int i=0;i<n;i++)
a[i]/=n;
}
void init()
{
for(int i=0,j=0;i<n;i++)
{
rev[i]=j;
for(int x=n>>1;(j^=x)<x;x>>=1);
}
}
BSGS
- 求最小的x使得
ax≡b(mod p)
其中
p
为质数。当
a 与 p 互质时,x<=p (因为 a,2a,…,(p−1)a 对 p 的余数是1 到 p−1 的排列),所以令 m=⌈√p⌉ ,则x可表示为 i∗m+j ;将原式变为 aj≡b∗a−m∗i=b∗(a−m)i ,枚举 i 和j ,一边存进hash,另一边查找。个人理解是meet in the middle的一种应用。
计算几何篇
点积
dot(a,b)=a.x∗b.x+a.y∗b.y叉积
cross(a,b)=a.x∗b.y−a.y∗b.x- 皮克定理
对于顶点坐标均是整点的简单多边形,设面积为 S ,内部格点数目为n ,边上格点数目为 s ,则
S=n+s2−1 - 凸包
struct node
{
double x,y;
bool operator< (const node &a) const{
if(x!=a.x) return x<a.x;
return y<a.y;
}
double operator* (const node &a) const{
return x*a.y-y*a.x;
}
node operator- (const node *a) const{
return (node){x-a.x,y-a.y};
}
};
void convex_hull()
{
sort(a+1,a+1+n,cmp);
q[++s]=a[1],q[++s]=a[2];
for(int i=3;i<=n;i++)
{
while(s>1 && (q[s]-q[s-1])*(a[i]-q[s])<0) s--;
q[++s]=a[i];
}
p[++t]=a[n];p[++t]=a[n-1];
for(int i=n-2;i>=1;i--)
{
while(t>1 && (p[t]-p[t-1])*(a[i]-p[t])<0) t--;
p[++t]=a[i];
}
}
- 半平面交
数据结构篇
平衡树
- treap:第一个学习的平衡树,多年不打已经不太记得了……好像rotate一行搞定而且常数没那么大……如果可以的话应该尽量用treap。
删除操作只处理子树<=1的情况,否则旋转、递归。
void rotate(int &x,int d)
{
int u=a[x].ch[d];
a[x].ch[d]=a[u].ch[d^1];a[u].ch[d^1]=x;x=u;
}
void insert(int &u,int x)
{
if(!u) {u=++cnt;a[u]=(){};return;}
int d=(x>a[u].w);
insert(a[u].ch[d]);
if(a[a[u].ch[d]].pri>a[u].pri)
rotate(u,d);
}
void del(int &u,int x)
{
if(a[u].w==x)
{
if(!a[u].ch[0]) u=a[u].ch[1];
else if(!a[u].ch[1]) u=a[u].ch[0];
else
{
int d=(a[a[u].ch[1]].pri>a[a[u].ch[0]].pri);
rotate(u,d);
del(a[u].ch[d^1],x);
}
}
int d=(x>a[u].w);
del(a[u].ch[d]);
}
- splay:万能的splay⁄(⁄ ⁄•⁄ω⁄•⁄ ⁄)⁄特色功能是可以提取一段区间,注意要前后加两个点防止RE。以及常数大。
void rotate(int &u)
{
int x=a[u].pa,y=a[x].pa,d=(a[x].ch[1]==u);
if(!y) root=u;
else a[y].ch[a[y].ch[1]==x]=u;
a[x].ch[d]=a[u].ch[d^1];a[a[u].ch[d^1]].pa=x;
a[u].ch[d^1]=x;a[x].pa=u;a[u].pa=y;
maintain(x);maintain(u);
}
void splay(int u)
{
int v=u;
while(v) st.push(v),v=a[v].pa;
while(!st.empty())
pushdown(st.top()),st.pop();
while(a[u].pa)
{
int x=a[u].pa,y=a[x].pa;
if(!y) {rotate(u);return;}
if(a[x].ch[0]==u^a[y].ch[0]==x) rotate(u);
else rotate(x);
rotate(u);
}
}
单调栈/单调队列
- 优化DP,没什么别的用。
优先队列
- 只是想安利好题> < [BZOJ 2006]
补:其实这种思想比较常见。对于存不下的状态,如果可以进行归类并且比较方便拆开,就可以打包处理,用一个x元组代表某一类状态,前k大值就只要不断的取堆顶、将堆顶元素拆分……
线段树
- 可以刷水,可以j形……常常维护的量:最值、和、前后缀、连续子序列……
树状数组
- 年轻的我分不清是+还是-lowbit(x)……大概是那时候没理解。其实并不一定呢,比如[BZOJ 3594]我好像打的就是反的……
左偏堆
- 使得合并两个堆的时候一直往右走可以找到最近的位置。 dis[x]=dis[r[x]]+1 。所有操作都可以用merge完成。
void merge(int &u,int v) //小根堆
{
if(a[u].w>a[v].w) swap(u,v);
merge(a[u].r,v);
if(a[a[u].l].d<a[a[u].r].d)
swap(a[u].l,a[u].r);
a[u].d=a[a[u].r].d+1;
}
主席树
- 多个版本的权值线段树表示不同位置的信息,加加减减可以得到一段区间。可以放到树上。
void insert(int &k,int last,int l,int r,int x)
{
if(!k) k=++cnt;
a[k]=a[last];a[k].s++;
if(l==r) return;
int mid=(l+r)>>1;
if(x<=mid) insert(a[k].l,a[last].l,l,mid,x);
else insert(a[k].r,a[last].r,mid+1,r,x);
}
可持久化线段树
- 跟主席树分开写是因为[BZOJ 2653]……这道题算是比较正宗的可持久化:有多个版本的信息。所以对可持久化的理解应该在于版本之间便于修改,而不是死板地理解为第k大之类的。
树套树
- 线段树套线段树:码农题T^T 注意不能标记下传,要标记永久化。
- 树状数组套主席树:如果一个主席树不能满足要求就再来一个BIT……因为需要快速修改。
- 其实大部分的数据结构嵌套都是一个维护位置,一个维护权值。
字符串篇
哈希
- 经常能用的骗分/暴力手段,和二分一起。
KMP
- 用机智的办法避免重复比较。
void kmp() //在b串中找a串
{
int j=0;
for(int i=2;i<=n;i++)
{
while(j && a[j+1]!=b[i]) j=next[j];
if(a[j+1]==b[i]) j++;
next[i]=j;
}
}
Manacher
- 用机智的办法避免重复比较。
void manacher()
{
int mx=0,p=0;
for(int i=1;i<=n;i++)
{
if(i<=mx) len[i]=min(len[2*p-i],mx-i+1);
else len[i]=1;
while(a[i+len[i]]==a[i-len[i]]) len[i]++;
if(i+len[i]-1>mx)
mx=i+len[i]-1,p=i;
}
}
最小表示法
- 用机智的办法避免重复比较。
int i=1,j=2;
while(i<n && j<n)
{
int k=0;
while(s[i+k]==s[j+k]) k++;
if(s[i+k]<s[j+k]) j=j+k+1;
else if(s[i+k]>s[j+k]) i=max(i+k+1,j),j=i+1;
}
//最小的开头位置为i
AC自动机
- KMP的扩展版本。不难理解,只是在trie上加一些指针。
void build()
{
for(int i=0;i<26;i++)
if(ch[0][i]) q.push(ch[0][i]);
while(!q.empty())
{
int u=q.front();q.pop();
for(int i=0;i<26;i++)
if(!ch[u][i]) ch[u][i]=ch[pa[u]][i];
else
{
int v=ch[u][i],k=pa[u];
while(k && !ch[k][i]) k=pa[k];
pa[v]=ch[k][i];
q.push(v);
last[v]=ed[pa[v]]?pa[v]:last[pa[v]]; //ed[i]:i是否为串结尾
}
}
}
后缀数组
- 注意数组名。
- 常用技巧:二分;单调栈;翻转;连接多个串。
int k=1,p=0,q=1;
void trans(int*s1,int*s2,int*r1.int*r2)
{
for(int i=1;i<=n;i++)
v[r1[s1[i]]]=i;
for(int i=n;i>=1;i--)
if(s1[i]>k)
s2[v[r1[s1[i]-k]]--]=s1[i]-k;
for(int i=n-k+1;i<=n;i++)
s2[v[r1[i]]--]=i;
for(int i=1;i<=n;i++)
r2[s2[i]]=r2[s2[i-1]]+(r1[s2[i]]!=r1[s2[i-1]] || r1[s2[i]+k]!=r1[s2[i-1]+k]);
}
void SA()
{
while(k<n)
{
trans(sa[p],sa[q],rank[p],rank[q]);
p^=1,q^=1;k<<=1;
}
}
void get_height()
{
for(int i=1;i<=n;i++)
{
int j=sa[p][rank[p][i]-1];
h[i]=max(h[i-1]-1,0);
while(a[i+h[i]]==a[j+h[i]]) h[i]++;
}
}
后缀自动机
- 容易忘记把所有子节点改成nq。
- 补:SAM相关实在太容易忘记了……陈老师的原作思路还是很清楚。
right(s):状态s代表的串的右端点出现位置集合。同一个状态可以有多个串。
从a到b有c的边:从状态a接受字符c后走到状态b。
统计子串个数,不计重复:由于从初始状态到达每个节点都是一个合法子串,所以将所有 f[i]=1 ,然后DP统计;计重复:对所有的np, f[i]=|right(np)| , 意义为从该节点开始往后转移可以到达的子串数,right是出现次数,所以算了很多次?
int root=1;
void add(int c)
{
int np=++cnt,p=last;last=cnt;
l[np]=l[p]+1;
while(p && !ch[p][c])
ch[p][c]=np,p=pa[p];
if(!p) {pa[np]=root;return;}
int q=ch[p][c];
if(l[q]==l[p]+1) pa[np]=q;
else
{
int nq=++cnt;
l[nq]=l[p]+1;pa[nq]=pa[q];
memcpy(ch[nq],ch[q],sizeof(ch[nq]));
pa[nq]=pa[p]=nq;
while(p && ch[p][c]==q)
ch[p][c]=nq,p=pa[p];
}
}
可持久化trie
- 无意中用莫队+trie做过一道……好像是可以自己脑补出来的,只要记录每个节点的前缀和看某段时间内内是否存在这个点。
技巧篇
二分
- 基本上做题都要想一想有没有二分性质,有一些题有明显的特征。
三分
- 单峰函数用三分找最值。注意要事先判断一下端点。
int ternary_search(int l,int r) //求最大值
{
int res=max(f(l),f(r));
while(r-l>1)
{
int sl=(l+r)>>1,sr=(sl+r)>>1;
if(f(sl)>f(sr)) r=sr;
else l=sl;
}
return max(res,max(f(l),f(r)));
}
前缀和
- 听上去是傻逼东西,然而[BZOJ 1271]并没有注意到奇偶性的特殊性……
记忆化
- 有一些DP如果不会用到所有元素,用记忆化会比较快。不然还是写递推。
差分
- 可以在数据结构里把区间修改变成单点修改。经常代替树剖><并且好打。[BZOJ 4326]
分块
- 一开始觉得好高级……怎么越学越觉得暴力呢。
莫队
- 离线,独立,便于转移的询问,把询问排序,用分块的方式保证 O(n√n) 的复杂度。
- 注意某些题目要先插入再删除。
cdq分治
- 离线,独立的询问与修改。按时间分治。有三维偏序可以消掉一维,结合BIT可以再消一维。
void solve(int l,int r)
{
if(l==r) return;
int mid=(l+r)>>1,x=l,y=mid+1;
for(int i=l;i<=r;i++)
if(a[i].time<=mid && a[i].o==1) //修改
revise(xxx);
else if(a[i].time>mid && a[i].o==2) //询问
query(xxx);
for(int i=l;i<=r;i++)
if(a[i].time<=mid) t[x++]=a[i];
else t[y++]=a[i];
for(int i=l;i<=r;i++)
a[i]=t[i];
solve(l,mid); solve(mid+1,r);
}
整体二分
- 离线,独立,可二分的询问。还是二分,只不过是很多询问同时进行。
- 整个过程保证按时间有序。不需要特别维护,依次修改和询问即可。
void solve(int l,int r,int s,int t) //权值:[l,r] 操作:[s,t]
{
if(s>t) return;
if(l==r)
{
for(int i=s;i<=t;i++)
ans[a[i].id]=l;
return;
}
int mid=(l+r)>>1,x=s-1,y=t+1;
for(int i=l;i<=mid;i++)
revise(i); //对权值为i的部分进行修改
for(int i=s;i<=t;i++)
{
int cur=query(a[i]);
if(xxx) tmp[++x]=a[i];
else tmp[y--]=a[i];
}
for(int i=s;i<=t;i++)
a[i]=tmp[i];
solve(l,mid,s,x);
solve(mid+1,r,x+1,t);
}
最小乘积
- 一类问题用同样的思路解决:把两维看做一个点的坐标,那么答案在所有点的下凸壳上。用分治不断求出凸壳上的点,具体怎么求就推一下向量,然后直接套一维问题的算法……
思考方式
这是个大坑……随手更。
数据结构
- 看到特殊的题目条件一定要抓住,因为这是题目的突破口。[BZOJ 3926] [BZOJ 4012]
- 如果需要枚举[1,n]的子区间,看看[i,j]能不能与[i,j+1]快速转移。如果可以,一种常用方法是移动左端点,然后把[i,n]的答案(用线段树)全部修改。
- 点对的询问:可以把询问放在一个点上。[BZOJ 3772]
- 线段树维护连通性。[BZOJ 3995]
- 如果想到了平衡树,考虑用权值线段树/树状数组代替,因为真的比较容易打错……[BZOJ 1112]
- 有些问题对点的修改(插入)只出现一次,可以抓住这一点把插入时间作为关键字,通常可以去掉一维状态。[CF 226E] [BZOJ 4448]
算法
- 看到 ax+b 直接想斜率。[BZOJ 3533]
- 三维偏序的问题:经常用BIT优化DP。[BZOJ 3594]
- 字典序:正着贪心和逆向贪心都要考虑,一般没有别的办法。[BZOJ 1562]
逆向的贪心有时不好证明,可以考虑添加一些为了满足字典序而隐含的条件。 - 运用到容斥的DP:强行超过某个限制,剩下的随便考虑。[BZOJ 1042]
- DP的方案有重复时,把贡献拆开算,不要按定义算。[CF 261B]
- 概率DP(或者其他DP):有时候会让某些人出局,然而这并不重要;不要看到这种题就想暴力状压,要观察决定答案的因素到底是什么,反正不是这些人是谁。[BZOJ 3191] [BZOJ 4008]
图论
- 子树的问题变成dfs序/偶尔树剖。
- 双重限制:想想费用流。
- 费用流的平方项拆边变为 2x−1 。
- 网络流建模简化:如果几个结点的流量的来源/去向完全相同,则可以把它们合并成一个;有一条容量为∞的边,并且点v除了点u以外没有别的流量来源,则可以把这两个结点合并成一个。
- [入度=出度=1]==[一堆环]
数学
- 经常用的:容斥&补集转化。
- gcd(a,b)=gcd(a+b,b),欧几里得反过来用的情况。[BZOJ 2186]
- ∑ni=1σ0(i)=∑ni=1⌊ni⌋
- ∑ni=1∑mj=1σ0(ij)=∑i∑j[gcd(i,j)==1]⌊ni⌋⌊mj⌋
- mod变成下取整。
- 阶乘统计因子个数、组合数对 pk(p is a prime) 取模,这两个问题比较相似,都是提取特殊项后递归,不要记结论,要学方法。
- 组合数学的统计方案数:有时候从A中选B不好统计,反过来考虑。[BZOJ 4013]
- 二项式定理: (1+x)k=∑ki=0Cikxi ,考虑逆用可以去掉一个循环。[BZOJ 4487]
技巧
- 长得有特色的数据范围:
很大很大:矩乘/二进制。很小很小:状压/复杂度较高的DP。 - 区间
[l,r]
由长度为
d
的循环节组成
⇔[l,r−d]==[l+d,r] 。 - 大数不想hash可以取对数。
结论
- 同一个图的不同MST所用的相同权值的边个数相同。
- 平面图中
m<=3n−6
;
d<=2n−4
。
欧拉公式: V−E+F=2 - Dilworth定理:最小链覆盖数 = 最长反链长度。
- 曼哈顿距离: |x1−x2|+|y1−y2|=max(|(x1+y1)−(x2+y2)|,|(x1−y1)−(x2−y2)|)
杂项
- 用set偷懒:lower_bound(x):如果存在x,返回x;否则返回第一个比x大的元素(的迭代器);upper_bound(x):返回第一个比x大的元素。
- 在算修改距离之类的最小值时,一般都有的性质:一定是某个已经存在的值。
易错点
- 某些DP(单调队列)要先把一部分值求出来再更新信息。
- 置换有时要手动加上原位置。[BZOJ 1004]
- AC自动机经常给一样的模板串……最好判一下。
- 无向图做网络流要建双向边。