两种降时间复杂度的方法
离线查询(做起来容易):CDQ分治
在线查询:树套树
倍增
RMQ
区间最值查询Range Minimum/Maximum Query:给定一个数组,求出给定区间[l,r]最值的下标
倍增最关键的地方是:绝对不会走都是相同的步数(比如说走两步可以到达的地方绝对不会分两个一步走)
其次可以想到:成倍的增长->2(1,2,4,8,16…)
ST(Sparse Table)算法:O(nlogn)预处理,O(1)查询
求最小值
1.预处理(动态规划)
f(i,j)表示区间[i,i+2j-1]
第i个点到第i+2j-1个点的最小值
即f(i,0)表示区间[i,i]的最小值(即这个值)、f(i,1)表示区间[i,i+1]、f(i,2)表示区间[i,i+3]、f(i,3)表示区间[i,i+7]~i+15,i+31+…依次类推
用动态规划的思想,大区间的最小值可以由小区间的最小值转移而来,由此我们可以发现区间[i,i+2j-1]的最小值可以由区间[i,i+2j-1-1]的最小值和[i+2j-1,i+2j-1]的最小值,两者的最小值得来。(取两个长度为2j-1的块取代和更新长度为2j的块)
即最小值(此时起点为i,间断点为i+2j-1(化为f的第二个参数即j-1),两个区间的终点都为j-1(因为分成两块所以终点已经不是j了))
f[i,j]=min(f[i,j-1],f[i+2j-1][j-1])
同理f[i,j]=max(f[i,j-1],f[i+2j-1][j-1])
我的一些思考:
f(1,1)是区间[1,2],f(1,1)=min(f(1,0),f(2,0))即第一个点的区间[1,1]的最小值+第二个点的区间[2,2]的最小值
f(1,2)是区间[1,4],f(1,2)=min(f(1,1),f(3,1))即第一个点的区间[1,2]的最小值+第三个点的区间[3,4]的最小值
(所以要先算出f(1,1),f(2,1),f(3,1)才能知道f(1,2)…所以j要放在外层循环)
代码模板
//应该把j放在外层,因为内层要用到的还没被计算
for (int j = 1; j < 20; j++)//假设样例n最大为50000,则2的16次方已经可以超过了
for (int i = 1; i <= n; i++)
if (i + (1 << j) - 1 <= n)//大于n的就不用计算
f[i][j] = min(f[i][j - 1], f[i + (1 << (j - 1))][j - 1]);
2.查询(ST算法)
Query(L,R)注意:L和R不一定是2的幂次
假设L+2k到不了R,同理R-2k+1也到不了L,则要找到两个覆盖[L,R]区间的最小幂区间(区间可重复,因为相交没有影响)
区间长度为R-L+1,要保证k最大,则2k<=(R-L+1)->k=log2(R-L+1)
RMQ(L,R)=min(f[L,k],f[R-2k+1,k])
代码模块
//query(a,b)
k = (int)(log((double)(b - a + 1)) / log(2.0));
mi = min(f[a][k], f[b -(1<<k) +1][k]);
板子题:
RMQ无法求区间和
LCA
最近公共祖先Lowest Common Ancestors
离线:Tarjan算法
O(n+q)
在线:倍增做法
LCA洛谷教程:https://www.luogu.org/problemnew/solution/P3379
原因:直接dfs找祖先会t,所以选择1,2,4,8,16,32…这样跳着寻找->倍增
这里要涉及到链式前向星存图,特来补充一下
建图:
struct node
{
int weight;
int end;
int next;//next[i]表示与第i条边同起点的上一条边的存储位置
}edge[maxn];
int head[maxn];//head[i]表示以i为起点的最后一条边的存储位置
加边:
int head[maxn];//初始化为-1,head[i]表示以i为起点的最后一条边的存储位置
int cnt;//cnt初始化为1
void add(int u, int v, int w)//u是起点,v是终点,w是权重
{
edge[cnt].weight = w;
edge[cnt].end = v;
edge[cnt].next = head[u];
head[u] = cnt++;
}
遍历:
for(int i=head[start];i!=0;i=edge[i].next)\\start是输入的起点
例子:
输入:
5 7
1 2 8
1 3 13
1 4 15
2 4 9
3 4 5
3 5 10
4 5 3
我对链式前向星的思考:
edge数组:所有的边都存入里面
edge[1]的权重为8,终点为2,它的next为-1,head[1]=1
edge[2]的权重为13,终点为3,它的next为1(表示第二条边指向第一条边,即把兄弟边都连在一块),head[1]=2//表示1顶点所连的边数
其他的同理可得
注:从大到小找(从小到大会把最近的跳过)
depth数组记录每个节点的深度
fa[i][j]表示节点i的2j级祖先
复杂度:
1.预处理O(nlogn)
dfs遍历每个节点,处理深度数组depth和fa数组
可以思考一下:若是森林应该如何处理?
void dfs(int now, int father)//now表示当前节点,father表示当前节点的父亲节点
{
depth[now] = depth[father] + 1;
fa[now][0] = father;
for (int i = 1; (1 << i) <= n; i++)
fa[now][i] = fa[fa[now][i - 1]][i - 1];//我的第2^i级爸爸是我的2^(i-1)级爸爸的2^(i-1)级爸爸
for (int i = head[now]; i != -1; i = edge[i].next)
{
if (edge[i].end != father)
dfs(edge[i].end, now);
}
}
2.查询O(logn)
query(x,y)
先要控制x,y深度一致,然后再倍增的往上跳
常数优化:
for (int i = 1; i <= n; i++)
{
lg[i] = lg[i - 1] + (1 << lg[i - 1] == i);
}
log(a)(MN)=log(a)(M)+log(a)(N)
为什么要算log2(i)+1=log2(2*i):因为log2(i)不好直接求得(所以后续还要-1)
查询:
long long LCA(int x,int y)
{
if (depth[x] < depth[y])
swap(x, y);
while (depth[x] > depth[y])
{
x = fa[x][lg[depth[x] - depth[y]] - 1];
}
if (x == y)return x;
for (int k = lg[depth[x]]-1;k>=0;k--)//向上跳,从大到小
{
if (fa[x][k] != fa[x][k])
{
x = fa[x][k];
y = fa[y][k];
}
}
return fa[x][0];
}
不能直接跳到LCA(两个数的LCA,因为不好判断,谁知道跳到的是祖先还是最近祖先呢),所以只能跳到两个数的某一个数的父节点是这两个数的LCA,这就是为什么一开始设的数组是存储的父亲节点。
差分
求出差分数组(后-前)B,L~R的区间修改只需要修改B[L],B[R+1]