基础算法学习笔记
贪心
2020.9.12将一本通上的贪心学的差不多了,现在就整理一下。
1.选择不相交区间问题
分析:板子题,没什么好说的,总之就是每次贪心选择活动时间尽量少并且与之前的活动不相交的,不多评述。
2.区间选点问题
例题:P1250 种树
分析:给定许多区间,叫你取尽量少的点达成每个区间都至少有一点的目标。解决方案就是每一次选择区间末尾的那个点,使得这个点能同时存在于尽量多的区间内。
3.区间覆盖问题
分析:给你几个区间,让你选几个区间覆盖一个大的。解决方案就是先把区间按左端点大小进行排序,每次再贪心选择右端点最大的,直到将区间覆盖为止。
4.流水作业调度问题
例题:P1248 加工生产调度
分析:这种类型的题属比较难的,但我做了这么久的题也没见过几回,就这一道。有\(M_1\)和\(M_2\)两台机器,要现在\(M_1\)上加工,再到\(M_2\)上加工。思路是要使用\(Johnson\)算法:让\(M_1\)没有空闲,让\(M_2\)空闲时间尽量短。
可以证明得到一个结论:设\(N_1\)为\(a<b\)的作业集合,\(N_2\)为\(a\ge b\)的作业集合,将\(N_1\)的作业按\(a\)非减序排序,\(N_2\)中的作业按\(b\)非增序排序,就可达成最优顺序。
因为感觉实在不会来考,所以不准备证明,想看代码实现和证明看书就行,不在这里多扯。
5.带限期和罚款的单位时间任务调度
例题:P1230 智力大冲浪
分析:将所有事件根据罚款大小排序,从罚款多的开始处理,再一个个看能不能安排在所需的时间段内,不能就放弃处理。
6.习题
简单的习题就不写了,这里只给出两道比较难的。
思路就不想再讲了,时间没那么多,有时间再自己去看题目。
总结
实际上,考场上纯考贪心的题实在不多,所以此处讲的这些价值大不大?着实是个问题。但是要知道的是,考场上与贪心结合起来考的题非常多,所以贪心的思想一定要掌握好,要把握住贪心的精髓。
二分
2020.10.12日时学习,刚好与贪心隔了一个月(笑)
二分查找就不说了,傻子都会。
重点:二分答案
二分答案怎么说呢,入门题应当是这一道P1182 数列分段 Section II,其实当时理解还很浅薄,虽然将题解看了几遍,但还是处于云里雾里的状态,懵懵懂懂的就过了,后面在看一本通时终于有一次豁然贯通,才理解了二分答案。
二分答案,顾名思义,我们可以根据题意确定答案的上下界,然后不断二分,确定一个答案,接着通过一个判断函数来判断这个答案是否符合要求,接着就是缩小范围,直到找到那个最优解。
适用题目类型:最大值最小或最小值最大,如果出现这两种字眼,那不用怀疑,妥妥的是二分答案了。
板子题挺多的,就不一一列举,但再在整型二分之外还有另一种二分类型,更难的实数二分,对于这种类型的题目,精度一定要格外注意,稍不留神便会被精度坑死。
想了想,还是放一个板子
while(r-l>=1e-6)
{
double mid=(l+r)/2;
if(judge(mid))l=mid;
else r=mid;
}
嗯,精度要开大一点是比较保险的,不过建议也不要太大,1e-12就有点过分,一般1e-8是完全够用的。
冷门的三分法
先放一个模板传送门:P3382 【模板】三分法
如果说二分是专门针对具有单调性的问题的话,那么三分就一定是针对具有单峰性的问题。与三分有关的问题,其答案范围构成的函数一般都具有一个单峰或低谷,分别对应答案的最大值,最小值,模板如下。
while(r-l>=1e-6)
{
double m1=l+(r-l)/3.0,m2=r-(r-l)/3.0;
if(f(m1)<f(m2))l=m1;
else r=m2;
}
f函数就要根据题意设定,意为答案取此时的函数值。
三分习题
简单说一下,其实也没什么好说,利用初中数学知识再套三分模板就可过,分类讨论很淦。
没有做好心理准备不建议乱上。函数带两个变量就很淦,一定要先确定一个变量,需使用三分套三分的方法。另,这里的double多的让我快打吐。
总结
二分思想一样很重要,虽然其实在考试里出现的似乎不多,不过基础算法毕竟是基础,学好是很必要的。至于三分?有兴趣的可以学一学,没兴趣的让它爬就行,CCF要是出三分题就是脑子进水了。实数二分不用太担心,因为精度问题实在是太少出现在考场上。总之还是四个字:领悟思想。
真正的重点:搜索
FIRST 深搜(dfs)
先放一个网上找来的模板
void dfs(答案,搜索层数,其他参数){
if(层数==maxdeep){
更新答案;
return;
}
(剪枝)
for(枚举下一层可能的状态){
更新全局变量表示状态的变量;
dfs(答案+新状态增加的价值,层数+1,其他参数);
还原全局变量表示状态的变量;
}
}
搜索,尤其是深搜,对于非常多题都适用,跟枚举的纯暴力相差无几,容易打,不易出错,很多题用来对拍的暴力都是这种。它的时间复杂度是指数级别的,但是有时题目的正解,可能就是深搜这种暴力,正常跑是肯定跑不过去,但若是能优化,结果或许就会大不一样。
说了一堆废话,回到正题。这里的主角是深搜,但是却是有剪枝的深搜,剪掉深搜的搜索树上的那些无用枝条,使得深搜能在极短的时间内找出答案,这便是我们的正解。
但却有三个加剪枝一定注意:正确性,准确性,高效性。做到这三点,那才能算真正优化到了。
优化技巧有很多,我现将书上的分类搬下来,以便观看。
1.优化搜索顺序 2.排除等效冗余 3.可行性剪枝 4.最优性剪枝 5.记忆化
先来看一道剪枝的入门题P1025 数的划分
这里使用搜索可以说是很明显了,要打出来也很简单,但是分析一下复杂度就知道,TLE是稳稳的。所以,这里一定要加剪枝。如何剪枝呢?其实也很简单,只要分析一下上下界即可,因为不考虑顺序,所以可以设\(a[i-1]\le a[i]\),现在下界已经有了,也可以轻易推出上界是\(\frac{m}{k-i+1}\),只要加了这两个简单的剪枝,就可以愉快的AC了。
再来看一道剪枝的经典题目P1120 小木棍 [数据加强版]
这道题的剪枝异常之多,大类里可以分出最优性剪枝与可行性剪枝两类,根据题目具体细节就可以写出很多剪枝,是一道很好的剪枝练习题。
经典题目还是放一下代码
代码
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
int a[105],n,len,m,minn,sum,bj,tot,b[105],next[105];
bool ex[105];
bool cmp(const int &x,const int &y)
{
return x>y;
}
void read()
{
cin>>n;
int d;
for(int i=1;i<=n;i++)
{
cin>>d;
if(d>50)continue;
a[++tot]=d;
sum+=d;
}
sort(a+1,a+tot+1,cmp);
next[tot]=tot;
for(int i=tot-1;i>0;i--)
{
if(a[i]==a[i+1]) next[i]=next[i+1];
else next[i]=i;
}
}
void dfs(int k,int last,int rest)
{
int i,j;
if(rest==0)
{
if(k==m){bj=1;return;}
for(i=1;i<=tot;i++)
if(!ex[i]){ex[i]=1;break;}
dfs(k+1,i,len-a[i]);
ex[i]=0;
if(bj)return;
}
int l=last+1,r=tot,mid;
while(l<r)
{
mid=l+r>>1;
if(a[mid]<=rest)r=mid;
else l=mid+1;
}
for(i=l;i<=tot;i++)
{
if(!ex[i])
{
ex[i]=1;
dfs(k,i,rest-a[i]);
ex[i]=0;
if(bj)return;
if(rest==a[i]||rest==len)return;
i=next[i];
if(i==tot)return;
}
}
}
void solve()
{
int i,j;
for(int i=a[1];i<=sum/2;i++)
{
if(sum%i==0)
{
len=i;
ex[1]=1;
bj=0;
m=sum/i;
dfs(1,1,len-a[1]);
if(bj){cout<<len<<endl;return;}
}
}
cout<<sum;
}
int main()
{
read();
solve();
return 0;
}
习题
-
P1074 靶形数独(比较好的剪枝练习题
虽然可以被生草做法过掉) -
P1283 平板涂色(写了题解,似乎比较水?)
-
UVA12558 埃及分数(迭代加深搜索,这里好像没讲?有时间补上)
SECOND 广搜(bfs)
除了深搜之外,搜索的另一利器是广搜。打个形象的比方,如果说深搜是一条路走到黑,不撞南墙不回头的话,广搜就是对每种枝条都稍微一探,如果在这一层没有找到结果,再逐级深入。
同时因为广搜的特殊性,广搜搜到的第一个符合条件的答案往往就是最优解。
因为广搜是利用队列进行工作的,所以判重,也就是判断这个元素有没有在队列里,是一个很重要的工作。但是有些题的判重并没有那么简单,例如此题:P2730 [USACO3.2]魔板 Magic Squares。这里的元素是一个序列,我们需要对其进行状态压缩,给它每一种不同的序列都标一个号,在判重时使用这个序号来判重。
此题判重还需要学会康托展开,有兴趣的可以点击模板传送门自行学习,这里不会讲述。
魔板代码也放这
代码
#include<bits/stdc++.h>
using namespace std;
int jc[10]={1,1,2,6,24,120,720,5040};
int g,st,prt[50005],b[1000000]={0},step[50005];
char a[50005];
struct node{
int a[2][4];
}start,goal,q[90000];
int turn(node x)
{
int i,j,res=0,t[8],s;
for(i=0;i<4;i++)t[i]=x.a[0][i];
for(i=3;i>=0;i--)t[7-i]=x.a[1][i];
for(int i=0;i<8;i++)
{
s=0;
for(j=i+1;j<=7;j++)if(t[j]<t[i])s++;
res+=jc[7-i]*s;
}
return res;
}
node change(int way,int num)
{
node tep;
if(way==1)
{
for(int i=0;i<4;i++)tep.a[0][i]=q[num].a[1][i];
for(int i=0;i<4;i++)tep.a[1][i]=q[num].a[0][i];
return tep;
}
if(way==2)
{
tep.a[0][0]=q[num].a[0][3];
tep.a[1][0]=q[num].a[1][3];
for(int i=1;i<4;i++)tep.a[0][i]=q[num].a[0][i-1];
for(int i=1;i<4;i++)tep.a[1][i]=q[num].a[1][i-1];
return tep;
}
if(way==3)
{
tep.a[0][0]=q[num].a[0][0];tep.a[1][0]=q[num].a[1][0];
tep.a[0][1]=q[num].a[1][1];tep.a[1][2]=q[num].a[0][2];
tep.a[0][3]=q[num].a[0][3];tep.a[1][3]=q[num].a[1][3];
tep.a[0][2]=q[num].a[0][1];tep.a[1][1]=q[num].a[1][2];
return tep;
}
}
void print(int num)
{
if(num==1)return;
print(prt[num]);
cout<<a[num];
}
void bfs()
{
int op=1,cl=1,i,t;node tep;
q[1]=start;step[1]=0;prt[1]=1;
while(op<=cl)
{
for(int i=1;i<=3;i++)
{
tep=change(i,op);
t=turn(tep);
if(!b[t])
{
q[++cl]=tep;
step[cl]=step[op]+1;
b[t]=1;
prt[cl]=op;
a[cl]=char('A'+i-1);
if(t==g)
{
cout<<step[cl]<<endl;
print(cl);return;
}
}
}
op++;
}
}
int main()
{
for(int i=0;i<4;i++)start.a[0][i]=i+1;
for(int i=3;i>=0;i--)start.a[1][i]=8-i;
st=turn(start);b[st]=1;
for(int i=0;i<4;i++)cin>>goal.a[0][i];
for(int i=3;i>=0;i--)cin>>goal.a[1][i];
g=turn(goal);
if(g==st)
{
cout<<0;return 0;
}
bfs();
return 0;
}