填一填之前没学或是没学好的
基本内容的话
1.倍增,ST算法
2.单调栈,单调队列
3.二进制状态压缩
倍增
倍增,就是“成倍增长”。指我们在进行递推时,如果状态空间很大,通常的线性递推无法满足时间与孔家复杂度的要求,那么我们可以通过成倍增长的方式,只递推状态空间中在2的整数次幂
位置上的值作为代表。当需要其他位置上的值时,我们通过“任意整数可以表示成若干个2的次幂项的和”这一性质,使用之前求出的代表值拼成所需的值。
因此使用倍增算法需要状态空间关于2的次幂具有可划分性
“倍增”和“二进制划分”两个思想互相结合,可以降低求解很多问题的时间与空间复杂度
例题:
给定一个长度为N的数列A,然后进行若干次询问,每次给定一个整数T,求出最大的k,满足1~k的a[i]的累加和不大于T。算法必须是在线的(即必须即时回答每一次询问,不能等待收到
所有询问后再统一处理)
最朴素的做法显然是从前向后枚举K,每次询问花费的时间与答案的大小有关,最坏情况下为O(n)
我们O(n)预处理出前缀和显然也可以O(n)求解
但是当我预处理出前缀和后,就可以二分出k的位置,预处理复杂度O(n),二分复杂度O(logn),这个算法在平均情况下表现是很好的,然而缺点是如果每次询问给出的整数T都非常小,那么
必然造成答案k也非常小,有时还不如直接扫前缀和来得快,甚至不如从前向后枚举优
所以我们想怎么优化一下,当然,我们仍旧要前缀和,但是可以丢弃二分,设计一种倍增算法
1.另k = 0, p = 1, sum = 0
2.比较A数组中k之后的p个数的和与T的关系,若sum + s[k+p] - s[k] <= T,说明这k个数可以累加到答案上,sum += s[k+p] - s[k], k+=p, p*=2.即累加上这p个数的和,然后把p
的跨度增长1倍,当sum + s[k+p] - s[k] > T时,说明我们不能累加上这k个数,令p /= 2
3.重复上述操作,知道p == 0,此时的k就是答案,(当 p == 1时,表示这是要累加一个数,如果这个数都不能累加,说明此时k个数的和达到最大了,因为是求前缀的,所以肯定k也是最大的)
该算法始终在答案大小的范围内实施“倍增”与“二进制划分”思想,通过若干长度为2的次幂的区间拼出最后的k,时间复杂度并不比二分差,又因为该算法进行倍增始终在答案大小范围内
所以我个人觉得比二分优,尽管一群学长依旧力荐二分,=-=。不过二分还是超棒的啊,O(logn)不知道比O(n)优秀到哪里去了。
例题2 Genius ACM hihocoder#1384
给定一个整数M,对于任意一个整数集合S,定义“校验值”如下:
从集合S中取出M对数(即2*M个数,不能重复使用集合中的数,如果S中的整数不够M对,则取到不能取为止),使得“每对数的差的平方”之和最大,这个最大值就称为集合S的“校验值”
给定一个长度为N的数列A以及一个整数T。我们要把A分成若干段,使得每一段的校验值都不超过T。求最少需要分成几段
利用贪心的思想,先对问题进行分析
要求每对数的差的平方和最大,显然应该取集合S中最大的M个数和最小的M个数,我们想把集合的最大值和最小值构成一对,次大值和次小值构成一堆....这样求出的“校验值”肯定是最大的。
而为了让A划分的段数尽量少,我们要让每一段中尽可能塞下更多的数,让每一段都尽可能长,到达结尾时分成的段数就是答案
于是需要解决的问题就是:当确定一个左端点L,右端点R在满足A[L]~A[R]的校验值在不超过T的前提下,最大能取到多少,这样我们同时也就确定了R是多少
求长度为N的一段的校验值需要排序配对,复杂度为O(NlogN)。当校验值上限T比较小时,如果在整个L~N的区间上二分右端点R,二分第一步就要先检验(N-L)/2最终右端点R却可能只扩展了一
点,和上一题童同样的锅,浪费了很多时间。所以我们采用倍增
用与上一题类似的倍增过程
1.p = 1, R=L
2.求出[L, R+p]的校验值,若校验值<=T,则R+=p,p*=2,否则p/=2
3.重复上一步,直到p值变为0,此时R即为我们要求的右端点。
然后我们可以不断进行上述操作直到扩展完整个A
上面的过程至多循环O(logN)次,每次循环对长为O(R-L)的一段进行排序,完成整个题目的求解累计扩展长度为N,所以总体复杂度为O(Nlog^2N)。
虽然与二分相比,总复杂度仍是O(Nlog^2N)没有改变,但是在一些特殊情况下,远比二分跑得快。
ST表
在RMQ问题(区间最值问题)中,著名的ST算法就是倍增的产物。给定一个长度为N的数列,ST算法能在O(nlong)时间的预处理后,以O(1)的时间复杂度在线回答“数列A中下标在l~r之间
的数的最大值是多少”这样的区间最值问题(当然,线段树也能qwq(逃)
一个序列的子区间个数为O(N^2)个。根据倍增思想,现在规模为O(N^2)的区间中选择一些2的整次幂的位置代表值
设f[i,j]表示数列A中下标在子区间[i,i+2^j-1]里的最大值,也就是从i开始的2^j个数的最大值,。递推的边界为f[i,0] = A[i],即数列在子区间f[i,i]里的最大值中较大的一个
递推时,我们将子区间长度成倍增长,f[i,j] = max(f[i,j - 1], f[i + 2^j, j - 1]),即长度为2^j的区间的最大值是左右两半长度为2^(j-1)的子区间的最大值中较大的一个
1 void ST_prework() {
2 for(int i = 1; i <= n; ++i) f[i][0] = a[i];
3 int t = log(n) / log(2) + 1;
4 for(int j = 1; j < t; ++j)
5 for(int i = 1; i <= n - (1 << j) + 1; ++i)
6 f[i][j] = max(f[i][j - 1], f[i + (1 << (j - 1))][j - 1]);
7 }
8 /*
9 我们之所以要先枚举j,再枚举i,我们想我们要完成全部状态的更新,如果我们单独把每一个i更新完成,那么对于更新这个i时所需要的其余区间的信息并未得到更新,
10 会导致情况的遗漏(大概和floyed的锅有点像?我也不是很确定。错了别打我
11 */
当询问任意区间[l,r]的最值时,我们先计算出一个k,满足2^k<r-l+1<=2^(k+1),也就是使2的k次幂小于区间长度的前提下最大的k,那么“从l开始的2^k个数”和“以r结尾的2^k个数”,这两
段一定覆盖了整个区间[l,r],这两段的值分别是f[l,k]和f[r-2^k+1,k]的最值,二者中较大的就是整个区间的最值。
1 int ST_query(int l, int r) {
2 int k = log(r - l + 1) / log(2);
3 return max(f[l][k], f[r - (1 << k) + 1][k]);
4 }
最后说说ST表与线段树,线段树在建树时的复杂度为O(nlogn),查询为O(logn),在查询时复杂度高于ST表,毕竟ST表示O(1)为所欲为,但是线段树优秀就优秀在支持在线修改,复杂度O(logn)
而ST表,在线修改?反正各有优劣吧
单调队列
给定一个长度为N的整数序列(可能有负数),从中找出一段长度不超过M的连续子序列,使得子序列中所有数的和最大。N,M <= 3*10^5
计算“区间和”问题,一般转化为“两个前缀和相减”的形式进行求解。我们先算出s[i]表示序列里前i项的和,则连续子序列[L,R]中的和就等于s[R]-s[L-1]。所以子问题转化为:
找出两个位值x,y,使s[y] - s[x]最大并且y - x <= M。
首先我们枚举右端点i,当i固定时,问题就变为:找到一个左端点j,其中j∈[i - m, i - 1]并且s[j]最小
比较一下任意两个位置j和k,如果k < j < i并且s[k] >= s[j],那么对于所有大于i的右端点,k永远不会是最优选择,因为不但s[k]不小于s[j],并且j离i更近,长度更容易不超过M
也就是说j的生存能力比k更强。所以当j出现后,k就是一个无用的位置了
这告诉我们:可能成为最优选择的策略集合的一定是一个“下标位置递增,对应的前缀和s的值也递增”的序列,我们可以用一个队列保存这个序列。随着右端点的变化从前向后扫描,我们对
每个i执行以下三个步骤:
1.判断队头决策与i的距离是否超过M的范围,若超出则出队
2.此时队头就是右端点为i时,左端点为j时的最佳选择
3.不断删除队尾决策,直到队尾的S值小于s[i]。然后把i作为一个新的决策入队
1 int l = 1, r = 1;
2 q[1] = 0;
3 for(int i = 1; i <= n; ++i) {
4 while(l <= r && q[l] && q[l] < i - m) l++;
5 ans = max(ans, s[i] - s[q[l]]);
6 while(l <= r && sum[q[r]] >= s[i]) r--;
7 q[++r] = i;
8 }