基础算法
基础算法
基础杂项
排序
快排
快排板子
核心思想: 分治
const int N = 1e5+5;
int g[N];
int quick_sort(int l, int r)
{
int p=l, q=r, x=g[l+r >> 1];
while(p<=q)
{
while(g[p] < x) p++;
while(g[q] > x) q--;
if(p<=q) swap(g[p], g[q]), p++, q--;
}
if(q<r) quick_sort(q, r);
if(p>l) quick_sort(l, p);
}
复杂度为\(O(nlgn)\)
第k大数
const int N = 1e5+5;
int g[N];
int kth_num(int l, int r, int k)
{
int p=l, q=r, x=g[l+r >>1];
while(p<=q)
{
while(g[p] < x) p++;
while(g[q] < x) q--;
if(p<=q) swap(g[p], g[q]), p++, q--;
}
if(p<=k) return kth_num(p, r); //停在了第p个位置,比第k个要小,结果在右边
else if(q>=k) return kth_num(l, q); //不在右边结果就在左边
else return g[l]; //前二者都不是,在中间.
}
复杂度为\(O(lgn)\)
对顶堆
对于多次询问第k大数,我们可以选择调用刚才的第k大数板子,复杂度为\(O(nlgn)\)
但是也可以用两个堆维护,一个大顶堆一个小顶堆,被称作对顶堆
//查询第k大时,此时保证元素一定足够
//声明
priority_queue<int> bh;//大顶堆
priority_queue<int, vector<int>, greater<int>> sh;//小顶堆
//插入操作
if(!bh.size() || x > bh.top()) sh.push(x);
else bh.push(x);
//查询操作,当前要查询第k大
while(bh.size() > k)
{
int p = bh.top(); bh.pop();
sh.push(p);
}
while(bh.size() < k)
{
int p = sh.top(); sh.pop();
bh.push(p);
}
对顶堆的复杂度也可近似看作\(O(nlgn)\)
对顶堆在没有删除、每次要查询的第K大数递增时时间常数要优一些.
归并
归并板子
const int N = 1e5+5;
int g[N], b[N]; //b用于备份
void MergeSort(int l, int r)
{
if(l==r) return; //边界 只有一个元素 无需排序
int mid = l+r >> 1;
MergeSort(l, mid); MergeSort(mid+1, r);
int p=l, q=mid+1, cur=p;
while(p<=mid && q<=r)
{
if(g[p] <= g[q]) b[cur++] = g[p++];
else b[cur++] = g[q++];
}
while(p<=mid) b[cur++] = g[p++];
while(q<=r) b[cur++] = g[q++];
for(int i=l; i<=r; i++) g[i] = b[i];
}
逆序对的个数
首先证明逆序对的个数就是归并排序中交换的次数
将[1, n]的问题分解为[1, mid]与[mid+1, n]的问题
如果我们知道两个子问题的解,剩下的部分解就是在左边并且比右边大的元素对数了,这用归并排序很好实现.
const int N = 1e5+5;
int g[N], b[N];
void MergeSort(int l, int r)
{
int ans = 0;
if(l==r) return;
int mid = l+r >> 1;
MergeSort(l, mid); MergeSort(mid+1, r);
int p=l, q=mid+1, cur=l;
//记录右区间的方式
while(p<=mid && q<=r)
{
if(g[p] <= g[q]) b[cur++] = g[p++]; //此处是小于等于
//是因为小于等于的边界条件,如果是小于的话当两个元素相等坐标
//q向前移动,此时答案更新,但p这个点也被算了进去.
else b[cur++] = g[q++], ans += mid - p + 1;
}
while(p<=mid) b[cur++] = g[p++];
while(q<=r) b[cur++] = g[q++];
//记录左区间的方式
while(p<=mid && q<=r)
{
if(g[p] <= g[q]) b[cur++] = g[p++], ans += q - (mid + 1);
//小于等于同理.
//如果是小于那么会加入q. 下次更新时答案会多出来
else b[cur++] = g[q++];
}
while(p<=mid) b[cur++] = g[p++], ans += q - (mid + 1); //即 r - mid
while(q<=r) b[cur++] = g[q++];
for(int i=l; i<=r; i++) g[i] = b[i];
/*有两种计数方式,更优秀的一种是记录当右区间加入时的
这样的计数方式只需记录一次,在后面的循环中无需记录
而记录左区间加入时的计数方式需要记录两次.
*/
}
最大子序列和
四种做法
-
暴力
-
优化的暴力
-
分治
-
dp
暴力和优化的暴力见视频 子序列最值
这里分析一下这个题目为什么能用分治
问题[1, n]可以分解为[1,mid]与[mid+1]. 随后的横跨问题也很好解决,所以是天然的分治模板
二分
整数二分
大于等于模板
const int N = 1e5+5;
int g[N];
int l=1, r=n;
int x; //query
while(l<r)
{
int mid = l + r >> 1;
if(g[mid] >= x) r = mid;
else l = mid + 1;
}
小于等于模板
const int N = 1e5+5;
int g[N];
int l=1, r=n;
int x; //query
while(l<r)
{
int mid = l+r+1 >> 1;
if(g[mid] <= x) l=mid;
else r = mid-1;
}
注意小于等于时候要加1防止死循环
浮点数二分
例:
给定一个浮点数n,求它的三次方根。数的三次方根
\[−10000≤n≤10000
\]
double l=-100, r=100;
double n; cin>>n;
while(fabs(l-r) <= 1e-7)
{
double mid = (l+r)/2;
if(mid*mid*mid<=n) l=mid;
else r=mid;
}
浮点数不需要考虑小于等于板子加一的问题,因为有fabs做保证。
高精度
高精度加法
// C = A + B, A >= 0, B >= 0
vector<int> add(vector<int> &A, vector<int> &B)
{
if (A.size() < B.size()) return 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;
}
高精度减法
// C = A - B, 满足A >= B, A >= 0, B >= 0
vector<int> sub(vector<int> &A, vector<int> &B)
{
vector<int> C;
for (int i = 0, t = 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;
}
高精度乘法
//高精度乘以低精度
// C = A * b, A >= 0, b > 0
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;
}
//高精度乘以高精度O(n2),必要时用FFT优化
vector<int> mul(vector<int> A, vector<int> B)
{
vector<int> C(A.size() + B.size());
for (int i = 0; i < A.size(); i ++ )
for (int j = 0; j < B.size(); j ++ )
C[i + j] += A[i] * B[j];
for (int i = 0, t = 0; i < C.size() || t; i ++ )
{
t += C[i];
if (i >= C.size()) C.push_back(t % 10);
else C[i] = t % 10;
t /= 10;
}
while (C.size() > 1 && !C.back()) C.pop_back();
return C;
}
高精度除法
// A / b = C ... r, A >= 0, b > 0
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;
}
高精度比大小
vector<int> max_vec(vector<int> a, vector<int> b)
{
if(a.size() > b.size()) return a;
if(a.size() < b.size()) return b;
if(vector<int>(a.rbegin(), a.rend()) >
vector<int>(b.rbegin(), b.rend())) return a;
return b;
}
高精度输出
void output(vector<int> a)
{
for(int i=a.size()-1; i>=0; i--) cout<<a[i];
}
前缀差分
一维
一维前缀和
一维差分
二维
二维前缀和
求前缀和:
\(g[i][j] = g[i][j] + g[i-1][j] + g[i][j-1] - g[i-1][j-1];\)
复原:
x1, y1 到 x2, y2的和:
\(g[x2][y2]-g[x1-1][y2]-g[x2][y1-1]+g[x1-1][y1-1]\)
二维差分
插入操作:
void insert (int x1, int y1, int x2, int y2, int c)
{
g[x1][y1] += c;
g[x1][y2+1] -= c;
g[x2+1][y1] -= c;
g[x2+1][y2+1] += c;
}
复原即求一边前缀和
双指针
双指针与其说是一种算法,不如说是一种思维,将问题无用的部份遗弃掉。
这里给出双指针的几个简单例子,更多的例子请见双指针专题。
最长连续不重复子序列
给定一个长度为n的整数序列,请找出最长的不包含重复的数的连续区间,输出它的长度。
位运算
XOR
lowbit
return x & -x;
二进制1的个数
while(p) p&=(p-1), cnt++;
区间问题
离散化
for(int i = 1; i <= n; i++)
cin >> g[i], alls.push_back(g[i]);
sort(alls.begin(), alls.end());
alls.erase(unique(alls.begin(), alls.end()), alls.end());
区间合并
void merge(vector<PII> &g)
{
vector<PII> res;
sort(g.begin(), g.end());
int st = -2e9, ed = st;
for(auto s: g)
{
if(s.first > ed)
{
if(st != -2e9) res.push_back({st, ed});
st = s.first, ed = s.second;
}
else ed = max(ed, s.second);
}
if(st != -2e9) res.push_back({st, ed});
g = res;
}

浙公网安备 33010602011771号