基于分治思想的不稳定排序(特殊情况:若将数组中的每个值变成值与下标的二元组,就能保证数组中所有值都不相同,此时的快排是稳定的)
平均时间复杂度 O ( n l o g 2 n ) O(nlog_2n) O(nlog2n),最坏是 O ( n 2 ) O(n^2) O(n2)
步骤:
x
:q[l] q[(l+r)/2] q[r] 随机
x
的在左半边,大于x
的在右半边暴力做法
a[]
和b[]
q[l ~ r]
中的每个数
q[i] <= x
,则x -> a[]
q[i] > x
,则x -> b[]
a[] -> q[]
,b[] -> q[]
优雅做法(双指针)
两个指针分别指向数组的两端
左侧指针指向的值小于x时,右移,大于等于x时,停止;右侧指针指向的值大于x时,左移,小于等于x时,停止
如果左侧指针在右侧指针的左侧,则交换指向的值
#include
using namespace std;
const int N = 100005;
int n, a[N];
void quick_sort(int a[], int l, int r)
{
if(l >= r) return ;
int x = a[l + r >> 1], i = l - 1, j = r + 1;
while(i < j)
{
do i++; while(a[i] < x);
do j--; while(a[j] > x);
if( i < j) swap(a[i], a[j]);
}
//处理边界
quick_sort(a, l, j);
quick_sort(a, j+1, r);
}
int main()
{
scanf("%d",&n);
for(int i=0;i<n;i++) scanf("%d", &a[i]);
quick_sort(a, 0, n-1);
for(int i=0;i<n;i++) printf("%d ", a[i]);
return 0;
}
基于分治思想的稳定排序
时间复杂度 O ( n l o g 2 n ) O(nlog_2n) O(nlog2n)
基本步骤:
mid = (l + r) / 2
双指针算法
#include
using namespace std;
const int N = 100005;
int n, a[N], tmp[N];
void merge_sort(int a[], int l, int r)
{
if (l >= r) return ;
int mid = l + r >> 1;
merge_sort(a, l, mid);
merge_sort(a, mid + 1, r);
int k = 0, i = l, j = mid + 1;
while(i <= mid && j <= r)
{
if (a[i] <= a[j]) tmp[k ++] = a[i ++];
else tmp[k ++] = a[j ++];
}
while(i <= mid) tmp[k ++] = a[i ++];
while(j <= r) tmp[k ++] = a[j ++];
for(i = l, j = 0; i <= r; i ++, j ++) a[i] = tmp[j];
}
int main()
{
scanf("%d", &n);
for(int i = 0; i < n; i ++) scanf("%d", &a[i]);
merge_sort(a, 0, n-1);
for(int i = 0; i < n; i ++) printf("%d ", a[i]);
return 0;
}
假定在***待排序***的记录序列中,存在***多个***具有***相同***的关键字的记录,若经过排序,这些记录的***相对次序保持不变***,即在原序列中,r[i] = r[j]
,且r[i]
在r[j]
之前,而在排序后的序列中,r[i]
仍在r[j]
之前,则称这种排序算法是***稳定***的;否则称为***不稳定***的。
二分的本质并不是单调性,有单调性一定可以二分,但二分不一定需要单调性。
本质:边界问题
在区间内,右边部分满足某种性质,左边部分不满足该种性质,有两个二分模板分别寻找左右两个点。
找绿色点【找大于等于给定数的第一个位置(满足某个条件的第一个数)】:取mid = l+r >> 1
,在check()函数中判断mid是否符合绿色部分性质。若符合,则表示绿色点在mid
的左边部分,此时更新r = mid
;若不符合,则表示绿色点在mid
的右边部分,此时更新l = mid+1
。
int bsearch_1(int l, int r)
{
while (l < r)
{
int mid = l + r >> 1;
if (check(mid)) r = mid;
else l = mid + 1;
}
return l;
}
找红色点【找小于等于给定数的最后一个数(满足某个条件的最后一个数)】:取mid = l+r+1 >> 1
,在check()函数中判断mid是否符合红色部分性质。若符合,则表示红色点在mid
的右边部分,此时更新l = mid
;若不符合,则表示红色点在mid
的左边部分,此时更新r = mid-1
。
mid = l+r+1 >> 1
原因:如果mid = l+r >> 1
的话,当l = r-1
,那么mid = l
,如果check(mid)
为true
的话,l = mid
,就陷入死循环。
int bsearch_2(int l, int r)
{
while (l < r)
{
int mid = l + r + 1 >> 1;
if (check(mid)) l = mid;
else r = mid - 1;
}
return l;
}
总结:
简单总结一下就是在[0,0,0,...,0]
(共k
个数) 里面搜索0
。
使用第一个会返回位置0
(对应最大值最小是…的问题)
使用第二个会返回k - 1
(对应最小值最大是…的问题)
也可以看做寻找 第一个<= target的元素 和 最后一个<= target的元素
注:从小到大的数组中,lower_bound(begin, end, num)
在[begin, end)
找第一个大于等于num
的元素,upper_bound(begin, end, num)
在[begin, end)
找第一个大于num
的元素;从大到小的数组中,lower_bound(begin, end, num)
在[begin, end)
找第一个小于等于num
的元素,upper_bound(begin, end, num)
在[begin, end)
找第一个小于num
的元素。
循环条件r - l > eps
eps
取值:4位小数1e-6
;5位小数1e-7
;6位小数1e-8
double bsearch_3(double l, double r)
{
const double eps = 1e-6; // eps 表示精度,取决于题目对精度的要求
while (r - l > eps)
{
double mid = (l + r) / 2;
if (check(mid)) r = mid;
else l = mid;
}
return l;
}
输入:将大整数以字符串形式输入,此时高位在前,低位在后;从字符串尾部按位转换成int
型插入vector
数组中,此时,低位在前,高位在后,这样便于对最高位进1
的操作。
输出:新数组的存放顺序仍为低位在前,高位在后,从数组尾部向前遍历输出即可。
思路:从低位开始,将两个大整数相同位相加,得数为t,需考虑进位,两个个位数相加,如有进位一定是1
,个位放入新数组,十位为进位与后一位相加,重复操作直到较长的数组遍历结束,此时若t = 1
,即最高位产生进位1
,需要将这个进位放入数组中。
#include
#include
#include
using namespace std;
vector<int> add(vector<int> a, vector<int> b)
{
if(a.size() < b.size()) add(b, a);
vector<int> c;
int t = 0;
for(int i=0;i<a.size();i++)
{
t += a[i];
if(i < b.size()) t += b[i];
c.push_back(t % 10);
t /= 10;
}
if(t) c.push_back(t);
return c;
}
int main()
{
string A, B;
vector<int> a, b;
cin >> A >> B;
for(int i=A.size()-1;i>=0;i--) a.push_back(A[i] - '0');
for(int i=B.size()-1;i>=0;i--) b.push_back(B[i] - '0');
vector<int> c = add(a, b);
for(int i=c.size()-1;i>=0;i--) printf("%d", c[i]);
return 0;
}
思路:首先要判断两个大整数谁大谁小,从而保证相减运算时一定为大减小。判断大小时,首先看长度,若长度相同,看各个位置,若均相同,直接返回true
,结果非负就不用加符号。在进行相减运算时,要考虑借位,借位最多借1,从低位开始按位减,首先给t
赋值为a[i] - t
,相减结果为t
,如果t >= 0
,则无需借位,如果t < 0
,则需要借位,t + 10
,将t
或者t + 10
放入新数组,合并一下操作,将(t + 10) % 10
放入数组。如果t < 0
,在下一位运算时,需要减去1
,给t
赋值1
。
#include
#include
#include
using namespace std;
bool cmp(vector<int> a, vector<int> b)
{
if(a.size() != b.size()) return a.size() > b.size();
for(int i=a.size()-1;i>=0;i--)
if(a[i] != b[i]) return a[i] > b[i];
return true;
}
vector<int> sub(vector<int> a, vector<int> b)
{
vector<int> c;
int t = 0;
for(int i=0;i<a.size();i++)
{
t = a[i] - t;
if (i < b.size()) t -= b[i];
c.push_back((t + 10) % 10);
if (t < 0) t = 1;
else t = 0;
}
while (c.size() > 1 && c.back() == 0) c.pop_back();
return c;
}
int main()
{
string A, B;
vector<int> a, b;
cin >> A >> B;
for(int i=A.size()-1;i>=0;i--) a.push_back(A[i] - '0');
for(int i=B.size()-1;i>=0;i--) b.push_back(B[i] - '0');
vector<int> c;
if(cmp(a, b)) c = sub(a, b);
else
{
cout << '-';
c = sub(b, a);
}
for(int i=c.size()-1;i>=0;i--) printf("%d", c[i]);
return 0;
}
思路:从个位开始,高精度整数的每一位乘以整数b,相乘的结果个位存入运算结果中,保留其他位数进入下一位的运算。
#include
#include
using namespace std;
vector<int> mul(vector<int> a, int b)
{
vector<int> c;
int t = 0;
for(int i=0;i<a.size() || t;i++)
{
if(i < a.size()) t += a[i] * b;
c.push_back(t % 10);
t /= 10;
}
while(c.size() > 1 && c.back() == 0) c.pop_back();
return c;
}
int main()
{
string A;
int b;
vector<int> a;
cin >> A >> b;
for(int i=A.size()-1;i>=0;i--) a.push_back(A[i] - '0');
vector<int> c = mul(a, b);
for(int i=c.size()-1;i>=0;i--) printf("%d",c[i]);
return 0;
}
思路:从最高位开始运算,模拟竖式相除。
#include
#include
#include
using namespace std;
vector<int> div(vector<int> &a, int b, int &r)
{
vector<int> c;
r = 0;
for(int i=a.size()-1;i>=0;i--)
{
r = r * 10 + a[i];
c.push_back(r / b);
r %= b;
}
reverse(c.begin(), c.end());
while (c.size() > 1 && c.back() == 0) c.pop_back();
return c;
}
int main()
{
string A;
vector<int> a;
int b;
cin >> A >> b;
for(int i=A.size()-1;i>=0;i--) a.push_back(A[i] - '0');
int r;
vector<int> c = div(a, b, r);
for(int i=c.size()-1;i>=0;i--) cout << c[i];
cout << endl << r;
return 0;
}
前缀和可以在 O ( n ) O(n) O(n)时间统计和修改,在 O ( 1 ) O(1) O(1)时间内查询统计任意区间之和;差分可看作前缀和的逆运算,可在 O ( 1 ) O(1) O(1)时间操作任意区间。
一维
O ( n ) O(n) O(n) 预处理(加和)s[i] = a[1] + ··· + a[i];
O ( 1 ) O(1) O(1) 查询区间[l, r]
内数的和 s[r] - s[l - 1];
二维
O ( n m ) O(nm) O(nm) 预处理
s[i][j]
= ∑ i = 1.. i j = 1.. j a [ i ] [ j ] \sum_{i=1..i}^{j=1..j}a[i][j] i=1..i∑j=1..ja[i][j]
O ( 1 ) O(1) O(1) 查询
如图,已知(x1, y1)
和(x2, y2)
两点,求这两个点所构成的矩阵中各数之和,根据容斥原理s=sum-s1-s2-Δs
,可知求小矩阵内各数之和需要用大矩阵内所有数之和减去其余空间的各数之和。
初始化 O ( n m ) O(nm) O(nm):
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
{
cin>>a[i][j];
a[i][j]+=a[i-1][j]+a[i][j-1]-a[i-1][j-1];
}
查询 O ( 1 ) O(1) O(1):
while(q--)
{
int x1,y1,x2,y2;
cin>>x1>>y1>>x2>>y2;
cout<<(a[x2][y2]-a[x2][y1-1]-a[x1-1][y2]+a[x1-1][y1-1])<<endl;
}
一维
根据原数组,构造一个差分数组,首位相同,其余各位上的数值表示与前一位的差,通过前缀和运算可得原数组。
定义b[i]=a[i]-a[i-1]
,可得a[i]= ∑ j = 1 i b [ j ] \sum_{j=1}^i b[j] j=1∑ib[j],那么就称b是a的差分数组。
差分数组可以将对a数组任意区间的同一操作优化到 O ( 1 ) O(1) O(1)
对a
数组区间[l,r]
同时加上c
的操作可转化为:
while(m--)
{
int l,r,c;
scanf("%d%d%d",&l,&r,&c);
b[l]+=c;
b[r+1]-=c;
}
对b数组求前缀和即可得到原数组a:
for(int i=1;i<=n;i++)
b[i]+=b[i-1];
b[x1][y1] += c;
b[x2+1][y1] -= c;
b[x1][y2+1] -= c;
b[x2+1][y2+1] += c;
与一维差分一样二维差分可以把对矩阵的同一操作优化到O(1)
红色矩形区域同时加上一个数,由图可以得到插入函数:
void insert(int x1,int y1,int x2,int y2,int c)
{
a[x1][y1]+=c;
a[x1][y2+1]-=c;
a[x2+1][y1]-=c;
a[x2+1][y2+1]+=c;
}
初始化可以视为在(i, j)
和(i, j)
的小矩形内插入a[i][j]
:
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
{
scanf("%d",&x);
insert(i,j,i,j,x);
}
对二维差分数组求二维前缀和可以得到原数组:
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++)
{
a[i][j]+=a[i-1][j]+a[i][j-1]-a[i-1][j-1];
cout<<a[i][j]<<" ";
}
cout<<endl;
}
树状数组插入和查询都可以优化到 O ( l o g n ) O(logn) O(logn)。差分和前缀和适合用在查询或修改次数十分巨大的时候,当修改和查询在同一复杂度时适合用树状数组。
n >> k & 1
:求n
的第k
位lowbit(n) = n & (-n)
-n = ~n + 1
把无限空间里的有限数据映射到有限空间,主要表示大小关系,不反映具体数值
将所有区间按左端点从小到大排序