一、枚举:通过从可能的集合中枚举答案,判断题目的条件是否成立。
1.确定枚举的范围,减少枚举的空间。
2.选择合适的枚举顺序。
二、构造:从形式上来看,问题的答案往往具有某种规律性,使得在问题规模迅速增大的时候,仍然能较容易地得到答案。通常要求较高的数学能力与直觉。
eg:[Luogu P3599] Koishi Loves Construction
三、模拟:即编程模拟题目中要求的操作。题目通常具有码量大、操作多、思路繁复的特点。
四、前缀和&差分:一种重要的预处理方式,能将对区间的操作转化为对两个端点的操作,从而大大降低查询的时间复杂度。
1.前缀和:即数列的前n项的和,对于二维数组a[n][n]的前缀和S[n][n]有:
S[1][1]=a[1][1],S[i][j]=S[i-1][j]+S[i][j-1]-S[i-1][j-1]+A[i][j]。
2.差分:一种和前缀和相对的策略,可以当做是求和的逆运算,对于一个序列a[n],他的差分序列b[n]有:
b[1]=a[1],b[i]=a[i]-a[i-1](i>=2)。
五、递归:指在函数的定义中使用函数自身的方法。
!明白一个函数的作用并相信它能完成这个任务,千万不要跳进这个函数里面企图探究更多细节。
eg:[Luogu P1760] 汉诺塔 [Luogu P1242] 汉诺塔加强版
六、分治:把一个复杂的问题分成两个或更多的相同或相似的子问题,直到最后子问题可以简单的直接求解,将子问题的解合并即可得到原问题的解。//通常通过递归实现
eg:[POJ P3889] Fractal Streets [Luogu P1429] 平面最近点对
七、二分:基础用法是用于在单调序列中较快的找到目标元素(二分查找)。
0.二分的写法:
1)左支终点r=mid,右支起点l=mid+1时:mid取(l+r)/2。否则若mid取(l+r+1)/2,则当r-l=1时程序左支陷入死循环。
2)左支终点r=mid-1,右支起点l=mid时:mid取(l+r+1)/2。否则若mid取(l+r)/2,则当r-l=1时程序右支陷入死循环。
3)其他的二分写法:采用l= mid+1,r=mid-1或l=mid,r=mid来避免产生两种形式,但也相应地造成了丢失在mid点上的答案、二分结束时可行区间未缩小到确切答案等问题,需要特判处理。
1.二分答案:当问题的答案具有单调性时,就可以通过二分将求解转化为判定。即数据在答案的一侧合法,而在另一侧不合法时,我们可以把求最优解(最小值最大、最大值最小)的问题,转化为给定一个值mid,判定是否存在一个可行方案使解为mid的问题。
eg:[POJ P2018] Best Cow Fences [Luogu P1083] 借教室
2.三分法:用于求解单峰函数极值及相关问题。f(l)<f(r)->l一定在极值左(右)侧;f(l)<f(r)->r一定在极值右(左)侧。
八、排序:将一组特定的数据按某种顺序进行排列。//详细排序相关内容可参考:这篇博客
0.选择排序、冒泡排序、插入排序较为基础且时间复杂度高,在此不予介绍。
1.希尔排序(缩小增量法):基本思想为选定一个gap,把未排序序列中下标距离为gap的的两个元素进行比较排序,然后将gap逐渐减小至0重复上述分组和排序的工作。希尔排序是对插入排序的优化,gap>1时都是预排序,当gap=1时,序列接近有序,达到优化的效果。希尔排序中gap的取值方法多导致时间复杂度难以计算。实际使用情况不多,可自行了解,在此不予更多介绍。
2.计数排序:一种使用空间换时间的排序算法。需要计算每个数出现了几次,求出每个数出现次数的前缀和,利用出现次数的前缀和,将每个数赋进新数组中。
!序列中同时存在正数和负数时不能使用计数排序。
//对n个自然数进行升序排序
const int N=1e5+5;
int n,a[N],b[N],t[N];
void count(){
memset(t,0,sizeof(t));
for (int i=1;i<=n;++i) t[a[i]]++;
for (int i=1;i<=N;++i) t[i]+=t[i - 1];
for (int i=n;i>=1;--i) b[t[a[i]]--]=a[i];
}
3.桶排序:基本思路为将序列的值域划分为若干区间,对应若干桶,遍历序列时将某一元素放入所在区间对应的桶中并对桶中元素进行排序。实际使用情况不多,可自行了解,在此不予更多介绍。
4.基数排序:基本思路为根据权重,从低位到高位,对每一数位进行比较排序。实际使用情况不多,可自行了解,在此不予更多介绍。
5.快速排序:基本思想为分治,将序列分为前后两个部分后令前一个子数列中的数的优先级都高于后一个子数列中的数。快排的时间复杂度在最优与平均情况为O(nlogn),最坏为O(n^2)。快排实现方法多,常用hoare版本,其他还有挖坑法、前后指针法,在C++中可以直接使用sort(优化快排)。可用于解决第k大数问题。
const int N=1e5+5;
int n,a[N];
void quick(int l,int r){
int i=l,j=r;
int mid=(l+r)>>1;
do{
while(a[i]<a[mid])i++;
while(a[j]>a[mid])j--;
if(i<=j){
swap(a[i],a[j]);
i++;j--;
}
}while(i<=j);
if(l<j) quick(l,j);
if(i<r) quick(i,r);
return;
}
6.归并排序:基于分治思想将数组分段排序后合并,时间复杂度在最优、最坏与平均情况下均为O(nlogn)。可用于统计逆序对问题。
const int N=1e5+5;
int n,a[N],b[N];
void merge(int l,int r){
if(l==r)return ;
int mid=(l+r)>>1;
merge(l,mid); merge(mid+1,r);
int i=l,j=mid+1,tmp=l;;
while(i<=mid && j<=r){
if(a[i]<a[j]) b[tmp++]=a[i++];
else b[tmp++]=a[j++];
}
while(i<=mid) b[tmp++]=a[i++];
while(j<=r) b[tmp++]=a[j++];
for(int k=l;k<=r;k++) a[k]=b[k];
}
eg:[Luogu P1908] 逆序对 [POJ P2299] Ultra-Quicksort [POJ P2893] M*N Puzzle
7.堆排序:本质是建立在堆上的选择排序。最优时间复杂度、平均时间复杂度、最坏时间复杂度均为O(nlogn)。//关于堆的一些知识可以参考:这篇博客(博主的数据结构归纳将在后续给出)
const int N=1e5+5;
int n,a[N];
void adjust_down(int a[],int n,int root){//向下调整维护大根堆
int p=root;
int kid=p*2;
while(kid<n){
if(kid+1<n && a[kid+1]>a[kid]) kid++;
if(a[kid]>a[p]){
swap(a[kid],a[p]);
p=kid; kid=p*2;
}
else break;
}
}
void heap_sort(int a[],int n){
for (int i=n/2;i>0;i--) adjust_down(a,n,i);
for (int i=n;i>0;i--){
swap(a[i],a[1]);
adjust_down(a,i,1);
}
}
!算法竞赛中使用较多的是快速排序、归并排序(归并思想)、堆排序(数据结构)。
九、离散化:把无限空间中有限的个体映射到有限的空间中去,以此提高算法的时空效率。基本思路为先排序,再删除重复元素,最后就是索引元素离散化后对应的值。
const int N=1e5+5;
int n,tot,a[N],b[N];
int find(int x){//在存储数组中查找目标值对应的下标
int l=1,r=tot;
while(l<r){
int mid=(l+r)>>1;
if(x<b[mid]) r=mid;
else l=mid+1;
}
if(b[l]==x) return l;
return l+1;
}
void store(){
sort(a+1,a+n+1);
for(int i=1;i<=n;i++){
if(find(a[i])>tot) b[++tot]=a[i];
}
}
十、倍增:我们在进行递推时,如果状态空间很大,通常的递推无法满足时间与空间复杂度的要求,那么我们可以通过成倍增长的方式,只递推状态空间中在2的整数次幕位置上的值作为代表。当需要其他位置上的值时,因为任意整数可以表示成着干个2的次幕项的和,所以可以使用之前求出的代表值拼成所需位置的值。倍增能够使线性的处理转化为对数级的处理,大大地优化时间复杂度,常用于区间最值问题(ST表)和求LCA。
//快速幂算法——倍增思路
ll quick_power(ll a,ll b,ll c){
ll ans=1;
while(b>0){
if(b&1) ans=ans*a%c;
a=a*a%c;
b>>=1;
}
return ans%c;
}
//ST表:RMQ(区间最值)问题
//nlogn预处理后在线回答固定序列一定区间中的最值
const int N=1e5+5;
int n,t;
int a[N],f[100][100];//f[i][j]表示序列a中下标在[i,i+2^j-1]中的最值
void ST_prework(){
for(int i=1;i<=n;i++) f[i][0]=a[i];
int t=log(n)/log(2)+1;//计算n的二进制位数
for(int j=1;j<t;j++){
for(int i=1;i<=n-(1<<j)+1;i++){
f[i][j]=max(f[i][j-1],f[i+(1<<(j-1))][j-1]);
}
}
}
int ST_query(int l,int r){
int k=log(r-l+1)/log(2);
return max(f[l][k],f[r-(1<<k)+1][k]);
}
eg:[Luogu P1226] 快速幂 [KSN2021] Self Permutation
十一、贪心:贪心算法在有最优子结构的问题中尤为有效,即问题的局部最优解能递推到全局最优解。常用反证法、归纳法证明贪心算法的正确性,其他还有:
1.邻项交换:通过相邻两项后得到的不同公式判断最优情况(常用于以“排序”为贪心策略的证明)。
eg:[NOIP2012 提高组] 国王游戏 [NOIP2015 普及组] 推销员 [Luogu P2123] 皇后游戏
2.反悔贪心:思路是无论当前的选项是否最优都接受,然后进行比较,如果选择之后不是最优了,则反悔,舍弃掉这个选项。
eg:[Luogu P2949] Work Scheduling
3.范围缩放:任何对局部最优策略作用范围的扩展都不会造成整体结果变差。
eg:[POJ P3614] Sunscreen [POJ P3190] Stall Reservation
4.决策包容性:任意局面下作出的局部最优决策提供的可能性包含其他所有策略的可能性。
eg:[POJ P1328] Radar Installation
//最近不更了,麻了,好多ddl,还有四级