今天首先学习了离散化,它是排序算法的一个应用。遇到数据量不大但是数据很大时可以用离散化。
具体实现为:把a数组排序并去掉重复的数值,得到有序数组b[1]~b[m],在b数组的下标i与数值b[i]之间建立映射关系。若要查询整数i(1≤i≤m)代替的数值,只需直接返回b[i];若要查询整数a[j](1<=j<=n)被哪个1~m之间的整数代替,只需在数组b中二分查找[j]的位置即可。
实现代码为:
void discrete() //离散化
{
sort(a+1,a+n+1);也可用sTL中的函数
if(i==1||a[i]!=a[i-1])
b[++m]=a[i];
}
int query(int x) //查询x映射为哪个1~m之间的整数
{
return lower_bound(b+1,b+m+1,x)-b;
}
其中codeforce670C就是一个很好地利用排序并离散化从而降低时间复杂度的问题。
动态维护序列的中位数
题型:可以求在同一坐标轴上到所有点(A[1]~A[n])的最小距离的点的坐标。只有中位数处到所有的点的距离最小。把所有n个点的标排好序后,如果n为奇数,则最小距离的点的坐标为A[(n+1)/2];如果n为偶数,则最小距离的点的坐标为A[n/2]~A[n/2+1]之间的任何位置都是最优的。
例如:CHO501的货仓选址问题
均分纸牌问题和环形均分纸牌问题
#include<iostream>
#include<algorithm>
using namespace std;
const int maxn = 1000010;
typedef long long LL;
LL a[maxn], f[maxn], sum, ans;
int main(){
ios::sync_with_stdio(false);
int n; cin>>n;
for(int i = 1; i <= n; i++){
cin>>a[i];
sum += a[i];
}
sum /= n;
for(int i = 1; i <= n; i++)a[i]-=sum;
for(int i = 1; i <= n; i++)f[i]=f[i-1]+a[i];
sort(f+1,f+n+1);
for(int i = 1; i <= n; i++)ans+=abs(f[i]-f[n+1>>1]);
cout<<ans<<'\n';
return 0;
}
对顶堆
用法:可以动态维护中位数,具体过程:建立两个二叉堆:一个大根堆、一个小根堆。
排序算法
选择排序(时间复杂度n^2)
它的基本思想是:首先在未排序的数列中找到最小(or最大)元素,然后将其存放到数列的起始位置;接着,再从剩余未排序的元素中继续寻找最小(or最大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。选择排序的时间复杂度是O(N2)。
public static void selectSort(int[] a, int n)
{
int i; // 有序区的末尾位置
int j; // 无序区的起始位置
int min; // 无序区中最小元素位置
for(i=0; i<n; i++)
{
min=i;// 找出"a[i+1] ... a[n]"之间的最小元素,并赋值给min。
for(j=i+1; j<n; j++)
{
if(a[j] < a[min])
min=j;
}
if(min != i) // 若min!=i,则交换 a[i] 和 a[min]。交换之后,保证了a[0] ... a[i] 之间的元素是有序的。
{
int tmp = a[i];
a[i] = a[min];
a[min] = tmp;
}
}
}
插入排序(时间复杂度n^2)
#include <iostream>
using namespace std;
int main()
{
int a[] = { 45, 65, 13, 25, 60, 88, 99, 78, 51, 37, 61, 50 };
int k = sizeof(a) ;
int j;
for (int i = 1; i < k; i++) // 循环从第2个元素开始
{
if (a[i] < a[i - 1])
{
int temp = a[i];
for (j = i - 1; j >= 0 && a[j] > temp; j--)
{
a[j + 1] = a[j];
}
a[j + 1] = temp; // 在此处就就a[j+1]=temp
}
}
for (int i = 0; i < k; i++)
{
cout << a[i] << " ";
}
cout << endl;
return 0;
}
冒泡排序(时间复杂度n^2)
1、算法需要对数组遍历n-1遍;
2、在每一次遍历中,比较前后相邻元素的大小,如果第一个比第二个大,则交换他们,这样第一次遍历之后数组最后一个值就是最大值;
3、依次重复上面的步骤,就可以得到升序排序的数组。
void bubbleSort1(int a[],int n){
int tmp;
for(int i=1;i<=n-1;i++){
for(int j=0;j<n-i;j++){
if(a[j]>a[j+1]){
tmp=a[j];
a[j]=a[j+1];
a[j+1]=tmp;
}
}
}
}
堆排序(时间复杂度为nlogn)
堆排序的思想:代表堆的完全二叉树的根结点的值是最大(或最小)的,因此将一个无序序列调整为一个堆,就可以找出这个序列的最大(或最小)值,然后将找出的这个最大值交换到序列的最后(或最前),这样有序序列关键字增加1个,无序序列关键字减少一个,对新的无序序列重复这样的操作,就实现了排序,这就是堆排序的思想。
堆排序中最关键的操作是将序列调整为堆,整个排序的过程就是通过不断调整,使得不符合堆定义的完全二叉树变为符合堆定义的完全二叉树。
以大顶堆为例:
1. 从无序序列所确定的完全二叉树的第一个非叶子结点开始,从右至左,从下至上,对每个结点进行调整,最终将得到一个大顶堆。(即第一个元素是”有序“的了!)
2. 对结点调整的方法:将当前结点(假设为a)的值与其孩子结点进行比较,如果存在大于a值的孩子结点,则从中选出最大的一个与a交换。当a来到下一层的时候重复上述过程,直到a的孩子结点值都小于a的值为止。
3. 将当前无序序列中的第一个关键字,反映在树中是根结点(假设为a)与无序序列中最后一个关键字交换(假设为b)。a进入有序序列,到达最终位置。无序序列中关键字减少1个,有序序列中关键字增加1个。此时只有结点b可能不满足堆的定义,对其进行调整。
4. 重复第2步,直到无序序列中的关键字剩下1个时排序结束。
归并排序(时间复杂度为nlogn)-----逆序对问题
算法思想:
分治法 归并排序将待排序列一分为二,并对每个子数组递归排序,然后再把这两个排好序的子数组合并为一个有序的数组。算法实现步骤:
1.把长度为n的输入序列分为两个长度为n/2的子序列
2.对这两个子序列分别采用归并排序
3.将两个排序好的子序列合并成一个最终的排序序列
快速排序(时间复杂度为nlogn)----查找第k大数
快速排序的基本思想如下:
- 对数组进行随机化。
- 从数列中取出一个数作为中轴数(pivot)。
- 将比这个数大的数放到它的右边,小于或等于它的数放到它的左边。
- 再对左右区间重复第三步,直到各区间只有一个数。
划分操作可以分为以下5个步骤:
- 获取中轴元素
- i从左至右扫描,如果小于基准元素,则i自增,否则记下a[i]
- j从右至左扫描,如果大于基准元素,则i自减,否则记下a[j]
- 交换a[i]和a[j]
- 重复这一步骤直至i和j交错,然后和基准元素比较,然后交换。
再说一下晚上做题的总结。B题是一道简单题,主要是看出来向左和向右的区别在于:(认真找一下规律即可)
(x3-x2)*(y2-y1)-(x2-x1)*(y3-y2)>0还是<0
D题需要注意的是一个技巧:即在遍历所有数的时候:建立一个数组,将数值作为下标,将它在这一组数中出现的顺序作为值储存,只需遍历一遍即可,防止超时。
E题是一道找规律的题,其中涉及到3^(n-1),如果for循环累加会在第七组数据超时,所以需要用到快速幂,另外需注意结果为负数时取模的问题,所以我们可以在得到的结果+m再取模。
至于C题用到的矩阵快速幂,我没有涉及到过,今天一定要搞懂。
还有,要把做过的不会题重新整理一遍题解,真正弄明白知识和坑。
对啦,还有最致命的问题,我WA了好几次,竟然是因为应该把数据范围开成long long,一定在看数据范围时一定要仔细认真。
还有,在自己写一个代码之前,就要估计一下自己写的复杂度和题目所给的数据范围会不会超时,不要等着评测的时候超时再去想法优化,浪费时间做了无用功。
总之,每天按照蓝皮书的进度一点点学,要尽量踏实深入。同时每晚做的题第二天中午之前都要彻底的明白所考查的知识点及出现的坑,并掌握这一类的问题。每天都要真正弄懂几个知识,日积月累就会发现感觉还蛮不错的。