今天是林永迪老师的讲授~
继续昨日的贪心内容。
我们继续看例题:
分析样例的过河方法:
首先1和2先过河,总时间为2;
然后1回来,总时间为3;
然后5和10过河,总时间为13;
然后2回来,总时间为15;
然后1和2过河,总时间为17;
一个很强烈的贪心思路:最慢和次慢的两个人一定要一起过河;
证明:
假设我们有四个人 A B C D,他们过河时间是依次增大的。
假如我们有两种过河的方案:
1.让过河最快的人A来回将三个人送过河:AC + A + AD + A + AB;
2.先让最快的两人A和B过河,对面最快A的回来,C和D一起过河,然后最快的B回来,然后A和B一起过河(样例的方法):AB + A + CD + B + AB;
那么第一种方案所需的时间是: C + A + D + A + B = 2 * A + B + C + D;
那么第二种方案所需的时间是: B + A + D + B + B = A + 3 * B + D;
经比较显然大多数情况下第二种方案更优,所以我们要让 C和D 一起过河。
那么我们就有了一个贪心思路:先让最快和次快的人先过河,然后最快的回来,最慢和次慢的过河,次快的再回来,这时候我们发现只有最慢和次慢的过河了,其他人还没动,原来的n个人变成了n-2个人,这不就是原问题的子问题嘛?我们仍然按照上面的贪心思路走,那么最后一定会只剩下3人或4人,对于四个人的情况,还是按照上面的贪心思路,对于3个人的情况,我们另外讨论一下:
我们还是有 A B C 三人,他们过河的时间依次增大,无非也就下面两种过河情况:
1. A 和 B 过河,A 回来,A 和 C 过河:AB + A + AC;
2. B 和 C 过河,B 回来,A 和 B 过河:BC + B + AB;
第一种方案所需的时间是:A + B + C;
第二种方案所需的时间是:2 * B + C;
显然第一种情况更优哦~
那么这个题不就做完了?
贪心骗分导论:
我们有一种搜索叫作 k搜索,我们搜索的时候就是在遍历搜索树的过程,因为贪心是找最优解的过程,那么我们在遍历搜索树的时候走最优的儿子;但这样的话往往是错的,所以我们可以尝试搜一搜次优解,再可以搜一搜次次优解,简单地说就是有限制步数的深度搜索(迭代加深搜索);
分治算法
分治分治,分而治之,分治算法就是讲一个大问题划分成几个规模更小的问题并加以解决,通过解决子问题最终解决总问题。
分治算法在OI中的运用主要在两个方面:
1.基于二分查找,三分查找的运用;
2.将题目划分成更细小的子问题运用;
二分的本质求的是边界!在一个有序区间内确定一个边界,在边界的一边的元素满足某种性质,而另外一边不满足;
故二分经常用于解决如下类型的问题:
1.简单的二分查找;
2.二分答案(即求一个单调函数在满足某个性质下的最值);
3.最值的最值(最小化最大值,最大化最小值),这也是最常见的二分题类型;
它面对的一定是有序的,这个有序可以指大小或某种性质。
首先你可能需要个好的模板:
例一:
一道经典的线段树题,对于每个订单我们都查询一下区间最小值,看看是否和dj 相同。
我们可以画一个时间轴,上面的每个数表示当天需要借出多少个教室,如果有个订单要从sj ~ tj 借 dj 个教室,那么我们将区间 [ sj , tj ] 每个数加上 dj ,那么我们可以用差分啊QwQ~就可以维护前缀和来求出当天需要多少教室;由于如果出现第一份订单不满足的话你们后面的订单也不会满足的,而前面都是满足的,这就和二分前提很类似啊,所以我们可以二分订单数量,,时间复杂度为严格的O(nlog n);
例二:
最大自然数最小,就意味着我们要二分答案:
我们二分答案 v,尝试将 1~v 内的自然数分配给 a 和 b,我们可以先把被 x 整除的数但不被 y 整除的数给 b,把被 y 整除但不被 x 整除的数给 a。然后剔除所有被 x*y 整除的数,剩下的数与 x 和 y 都互素,判一下能不能好好的分配就可以了。
inline bool check(long long num) //检查我们枚举的num是否能给够 { long long u=num/x; //能被x整除的数的个数 long long v=num/y; //能被y整除的数的个数 long long w=num/(x*y); //能被(x*y)整除的数的个数 long long c1=cnt1,c2=cnt2; //当前我们还差c1个数就给够A,还差c2个数就给够B c1-=v-w;c2-=u-w; //将是y的倍数却不是x的倍数的给A,将是x的倍数却不是y的倍数的给B if(c1<0) c1=0; //如果给多了不能是负数啊,得弄成0 if(c2<0) c2=0; if(c1+c2>num-u-w+w) return 0; //如果不够的话,增大枚举的num return 1; //够了,往小里枚举试试 }
安利一下几个做题网站:
CodeForce
CodeChef
TopCoder
例三:
由于太难而被老师拖到晚自习讲QwQ~
三分
三分的难度要低于二分(因为变换出来的花样少)
三分的用处在于求一个单峰函数的最值;
单峰函数,例如:
我们每次取这个函数的 1/3 和 2/3 处,比较两个分界,舍去次优的那个点以下的部分;
例如我们要求下面这个函数的最大值,我们先三分找到两个分界点P1,P2:
我们发现P1的值比P2的值要大,那么我们就舍弃P2以下的那一部分:
我们这样一直三分下去就好了,直到找到最优解,时间复杂度是O(log3 n);
三分代码:
实数比较:
double eps = 1 e -8(精度)
先将 a 和 b 做个差,如果这个差值小于 eps ,那么说明 a 和 b是相同的;如果差值小于0,说明 a<b;
如果不是单峰函数怎么办?
把这个函数割成若干个单峰函数:
分块
分块算法相当于是一个对于线段树和树状数组算法的下为替代品,由于其算法简单粗暴十分好写故广泛地运用于骗分领域QwQ~
分块就是好几个小数组以块的形式连起来,每个块的大小是由你事先设好的。
看一个问题:
第一问:
我们可以用分块做,设置每个块的大小为√n,有如下三种情况:
1. l 和 r 在同一个块里,那么我们直接暴力跑就好了,时间复杂度不超过√n;
2. l 和 r 在相邻的块里,这时候我们还是暴力枚举,时间复杂度不超过2 * √n;
3,.l 和 r 隔得很远,这时候我们先暴力出 l 和 r 当前块里的数加上某个数,然后在中间的所有块里打上标记,表示这个块里每个数都加上Tag;
第二问:
还是用分块,我们将每个块里面的数排个序:
1. l 和 r 在同一个块里,那么我们直接暴力跑就好了,时间复杂度不超过√n;
2. l 和 r 在相邻的块里,这时候我们还是暴力枚举,时间复杂度不超过2 * √n;
3. l 和 r 隔得很远,
第三问:
因为 264 最多开方 6 次就变成 1 了,因为√1 =1 , √0 = 0,所以无论开多少次都是不变的,如果我们发现一个块里的数不是1就是0,那么我们就可以给这个块打个标记说明这个块已经成熟了,不用·再管它了QwQ~
时间复杂度不会超过O(6n);
例一:
搜索
搜索算法不是灵丹妙药,也不是万金油;
搜索,启发式搜索,动态规划,最短路算法之间的关系;
搜索树的概念以及四种基本搜索算法的适用情况;
1.深度优先搜索;
2.广度优先搜索;
3.双向广度优先搜索;
4.迭代加深搜索;
深度优先搜索
外号:深搜,电风扇,大法师,回溯;
是欺骗自己的好东西(滑稽);
较多用于走迷宫和树的遍历中(以及大名鼎鼎的网络爬虫);
虽然要利用栈进行递归但是不可思议的空间依旧比广搜小,深搜的空间是O(深度)级别的,广搜的空间是O(个数)级别的;
一般结合强力的剪枝服用时有奇效;
由于不需要搜索完所有的节点也能出一些结果,故在骗分界有广泛好评;
在寻找最优解的问题上基本被广搜吊打;
广度优先搜索
杀人放火,居家必备,解决最优性问题有两把刷子;
擅长答案在搜索树比较浅的位置时的情况;
虽然名字叫广度优先搜索,但其实并不喜欢很宽的搜索树;
状态的表示并没有深度来的舒服;
并没有很好的剪枝策略,垃圾结点一大堆;
空间上完全被锤爆了,可以说是一点都没有考虑过内存的想法;
双向广搜
当你知道起点和终点时就可以使用的操作,能大大减少广搜扩展到垃圾结点,不过就算这样内存依旧表示很不友好;
总的一句话,比广搜只好不差(当然代码量除外);
其实是有双向深搜和双向 A*,然鹅并木有什么卵用;
迭代加深搜索
算是广搜向内存妥协后的奇葩产物,骚气地结合了深搜和广搜的特点(大雾;
迭代加深搜索在面对庞大的搜索树时往往表现出了值得信任的优异表现;
其实也是有迭代加宽搜索的,不过价值和它给人的第一印象一样不靠谱就是了;
搜索剪枝
没有剪枝的搜索就像是没有跳刀的斧王
常用的剪枝主要有一下几种:
1.可行性剪枝;
2.最优性剪枝;
3.记忆化搜索;
4.搜索顺序以及代码细节;
5.启发式剪枝;
其实做题时想不想得到全靠脑洞,有没有效果全看缘分(大雾;
例一:
首先不加任何剪枝的dfs应该长这个样子:
可行性剪枝:
如果我们求的蛋糕体积以及大于题目给出的体积,那么就返回;
最优解剪枝:
求表面积的过程中,如果求出的S不如答案优,那就返回;
数论
qbxt三次都讲到了,尤为可见其重要性!
高精度运算
高精度除法——高精除单精
void div(Bignum a,int b) //高精度除单精度,Bignum是个结构体 { int t=0; for(int i=a.size-1;i>=0;i--) { t=t*10+a.a[i]; //向高位借位当10用 a.a[i]=t/b; //作除法 t%=b; //剩下的数 } while(a[a.size-1]==0&&a.size>1) a.size--; //除去前导零 }
高精度除法——高精除高精
丧心病狂的 lyd 莫名其妙搞出来的高精度开方:
这是什么意思呢?
我们现在原数中找到小数点,然后分别往左往右找,每两个数就用一个逗号隔开,位数不足的用0补上:
先看第一组数 76,64<76<81,说明开方后整数部分是8,所以我们在76上面写个8,并用 76 减去 82 得到的数与第二组数结合起来:
我们就得到了一个新数 1254,我们继续求这个新数的开方数:
显然我们能列出这个式子来求开方:(10c+x)2 <=y(c是我们已经求出来的数,y是我们要开方的数),求最小的 x,将左边完全平方公式展开就是:100c2 + 20cx+ x2 <= y,我们将含 x 的那一项合并就是:100c2 + (20c + x)* x <=y,然后我们发现 100c2 就是我们两位两位拉下来的过程(超级雾,我们只用管后面的那一坨就好了。
将我们已经求出来的 8 乘上 20 变成 160 ,我们算(160 + x)* x <= 1254 的最大 x,由计算可知:(160 + 7)* 7 = 1169,那么第二位就是 7,我们在第二组数的上面写上 7,并用 1254 - 1169 得到的数与第三组数结合起来:
我们又得到了一个新数 8532,我们继续求这个新数的开方数:我们将我们已经求出来的 87 乘上 20 变成 1740,我们算(1740 + x)* x <= 8532 ,由计算可知:1744 * 4 = 6976,那么第二位就是 4,我们在第三组数的上面写上 4,并用 8532 - 6976 得到的数与第四组数结合起来:
快速幂
原理很简单,假设我们要算 35,考虑到 5 我们可以用二进制表示成:101,那么 5 = 22 + 20,代入就是:3(2^2 + 2^0)= 32^2 * 32^0;所以我们就可以先把 b 换成二进制,从后往前一位一位地看,这个当前这一位 i 是 1,ans 就乘上 a2^i:
矩阵快速幂
n×m 的矩阵乘上 m×r 的矩阵就会得到一个 n×r 的矩阵,其运算公式是这样的:
矩阵快速幂常用于求解线性递推方程组,例如斐波那契数列:
F(n)= F(n-1)+ F(n-2)
如果考虑递推的话时间复杂度是O(n),n如果到了1e9 以上的数据就直接死了;
那么我们就可以考虑用矩阵快速幂做!
我们先开一个 1 * 2 的矩阵:
考虑怎么转移到下一个状态:
利用递推方程 F(n)= F(n-1)+ F(n-2),我们发现可以将其乘上这个 2 × 2 的矩阵(下面称之为“转化矩阵”)就可以转化到了:
那么我们再乘一次会发生什么?看下面喽:
也就是说,我们每乘一下转化矩阵,我们斐波那契数列就求出了下一项。
所以问题就变得很简单了,我们只需要将初始矩阵乘上转化矩阵的 n-1 次幂就能求出斐波那契数列的第 n 项了,这就是矩阵快速幂的应用:
矩阵快速幂基于以下的原理,即可以找到一个矩阵 M,使得 [F(n-1) F(n)]T* M = [F(n) F(n+1)]T
以斐波拉期数列为例:M = ((1 1) (1 0));
以此类推:
[F(0) F(1)]T* Mn = [F(n) F(n+1)]T;
我们成功将一个递推式转化成了一个求矩阵幂的问题;
利用快速幂算法可以将时间缩短为 O(d3 log n);
利用 FFT + 矩阵特征多项式的黑科技可以把时间进一步缩短到 O(d logd logn);
我们来试着写一下下面的矩阵:
F(n) = 7F(n-1) + 6F(n-2) + 5n + 4 * 3n
从高斯消元到线性基
高斯消元是求线性方程组的方法,不懂的看这里哦 传送门;
欧拉筛和积性函数
常见的埃拉托斯特尼筛,时间复杂度为O(nlog n),优化后为O(nlog log n);
考虑到筛 i 的倍数的时候可以直接从 i * i 开始筛,因为更小的倍数早已被筛过了,这样优化后时间复杂度就是O(n log log n);
欧拉筛
我们发现还是有一些数被多次筛过,例如 6 既被 2 筛过也被 3 筛过;
积性函数
怎么欧拉筛的时候求积性函数?
当我们筛 i 的时候,会选取一个素数p,将 p * i 筛掉,此时会检查 i 的最小质因子是不是 p,所以有两种可能:
1. i 和 p 互素; phi [ i * p ] = phi [ i ] * phi [ p ] (符合积性函数);
2. i 的最小质因子刚好是 p; phi [ i * p ] = phi [ i ] * p;
说下第二个怎么来的吧:
我们可以将
一个有意思的问题:
n/1 + n/2 + n/3 + n/4 +……+ n/n ?
A star(A*)
启发式搜索相较于其他的搜索的优势在于引入了一个启发式函数 f(x)= g(x)+ h(x)
g:我从起点出发走了几步了;
h:当前点到终点理想最优情况需要走几步;
f:g + h (雾
广搜将点压进队列里,A*将点压进优先队列里;
A*是贪心先搜最优情况,最后搜最坏情况;
例一:八数码问题
我们先求出每个数字的曼哈顿距离相加,作为理想答案 h,但实际情况 h* 一定大于等于 h ;
我们在搜索的时候,要记录每个局面是否已经搜到过,所以我们要开 99 的数组?炸!
考虑用状压:我们将当前局面排成一行数字,然后我们可以用康托展开求出这个排列是 rank 几(这样就不会重了),开的数组最大是 9!= 362880 ,显然不会爆。
最后我们启发式搜索,按照 f 数组从小到大去搜索,如果搜到一个状态发现 h = 0(理想状态下到达目标状态还有0步),说明我们已经搜到了目标状态了,那么我们直接输出 g 就好了;
IDA*
g:从根结点下来几步
h:
g + h > D return ;
逆元
听说不少没好好学数论的都死得挺难看的,比如下面这道题:
逆元有什么用?
我们可以用它来代替除法!
尝试榨取扩展欧几里得算法的剩余价值:
扩展欧几里得算法有什么用?——计算逆元!