在要求不能带板子和使用STL的情况下,只能手撕代码。
而且手写的话说明对知识点了解更深,比赛的时候速度更快。(虽然我退役了)
但是在笔试面试的时候很可能不能让你使用STL,这个时候就考验你对基本算法的理解(记忆)
一般来说,出现在算法竞赛进阶指南的代码是必须会手撕的
基本算法
二分
- 在单调递增序列(a)中查找$geq x $的数中最小的一个
while(l<r){
int mid = (l+r)>>1;/*右移运算 相当于除2并且向下取整*/
if(a[mid]>=x) r=mid;
else l=mid+1;
}
return a[l];
- 在单调递增序列(a)中查找(leq x)的数中最大的一个
while(l<r){
int mid = (l+r+1)>>1;
if(a[mid]<=x) l=mid;
else r=mid-1;
}
return a[l];
若是选满足答案最小的,初始化(l=L,r=R+1),如果答案是(R+1)那么就是没找到
若是选满足答案最大的,初始化(l=L-1,r=R),如果答案是(L-1)那么就是没找到
像upper_bound()
和lower_bound()
中,如果不存在的话确实是返回最后的下标+1,vector则是返回end()
排序
冒泡排序
相邻比较,大的右移
void bubble_sort() {
for (int i = n - 1; i > 0; --i) {
bool flag = false;
for (int j = 0; j + 1 <= i; ++j)
if (a[j] > a[j + 1]) {
std::swap(a[j], a[j + 1]);
flag = true;
}
if (!flag) return ;
}
return ;
}
选择排序
选择一个key,最大的右移
void selection_sort() {
for (int i = 0; i < n; ++i) {
for (int j = i + 1; j < n; ++j) {
if(a[i] > a[j])
std::swap(a[i], a[j]);
}
}
}
插入排序
在前面有序的情况下,选key插入到前面应该的位置
int i,j,key;
for(i = 2;i <= n;++i){
key = a[i];
j = i - 1;
while(j > 0 && a[j] > key){
a[j+1] = a[j];
j--;
}
a[j] = key;
}
快速排序
选中间的数,左边一定比它小,右边一定比它大,然后递归分治
void quick_sort(int l, int r) {
if (l >=r ) return ;
int i = l - 1, j = r + 1, x = a[(l + r) >> 1];
while (i < j) {
do j--; while (a[j] > x);
do i++; while (a[i] < x);
if (i < j) std::swap(a[i], a[j]);
else quick_sort(l, j), quick_sort(j + 1, r);
}
}
归并排序
int merge_sort(int l, int r) {
if (l >= r) return 0;
int mid = (l + r) >> 1, res = 0;
res += merge_sort(l, mid);
res += merge_sort(mid + 1, r);
int i = l, j = mid + 1, cnt = 0;
while (i <= mid && j <= r)
if (a[i] <= a[j])
b[cnt++] = a[i++];
else {
res += mid - i + 1;
b[cnt++] = a[j++];
}
while (i <= mid) b[cnt++] = a[i++];
while (j <= r) b[cnt++] = a[j++];
for (int i = l, j = 0; j < cnt; ++i, ++j) a[i] = b[j];
return res;
}
堆排序
大根堆的实现:x结点必须大于子结点
struct heap{
int *A;
int length;
heap(int n){
length = n;
A = new int[n+5];
for(int i=1;i<=n;++i){
cin>>A[i];
}
}
void Max_Heapify(int pos){
int l = pos*2;
int r = pos*2+1;
int largest;
if(l <= length && A[l] > A[pos]){
largest = l;
}else{
largest = pos;
}
if(r <= length && A[r] > A[largest]){
largest = r;
}
if(largest != pos){
swap(A[pos],A[largest]);
Max_Heapify(largest);
}
}
void build_max_heap(){
for(int i=length/2;i>=1;--i){
Max_Heapify(i);
}
}
void heapSort(){
build_max_heap();
int n = length;
for(int i=length;i>=2;--i){
swap(A[1],A[i]);
length--;
Max_Heapify(1);
}
for(int i=1;i<=n;++i){
cout<<A[i]<<" ";
}
}
};
哈希
只要记住一个公式:
(H(x)):字符串(x)的哈希值
(p):进制数
(p^x)表示在(p)进制下左移(x)位的值
代码中只要预处理 (f(x)):前缀哈希值 (p(x)):(p^x)
大数类
大数面试官说要面向对象?可读性强?
用三个栈维护大数加法(面试官说的可读性强的方法)
#include<bits/stdc++.h>
using namespace std;
class bigNum{
private:
stack<int>num1,num2,sum;
public:
char *Num;
bigNum(char *s){
Num = s;
}
bigNum* add(bigNum* &right){
for(int i=0;Num[i];++i){
num1.push(Num[i] - '0');
}
for(int i=0;right->Num[i];++i){
num2.push(right->Num[i] - '0');
}
int flag = 0,tmp,x,y;
while(!num1.empty() || !num2.empty()){
x = num1.empty() == 1 ? 0:num1.top();
y = num2.empty() == 1 ? 0:num2.top();
tmp = x + y + flag;
flag = tmp/10;
sum.push(tmp%10);
if(!num1.empty()) num1.pop();
if(!num2.empty()) num2.pop();
}
if(flag){
sum.push(flag);
}
char *ans = new char[sum.size()+1];
int pos = 0;
while(!sum.empty()){
ans[pos++] = sum.top() + '0';
sum.pop();
}
return new bigNum(ans);
}
void printNum(){
cout<<Num<<endl;
}
};
char a[150],b[150];
int main(){
cin>>a;
cin>>b;
bigNum *left = new bigNum(a);
bigNum *right = new bigNum(b);
bigNum *sum = left ->add(right);
sum ->printNum();
return 0;
}
字符串
字典树
void ins(char *str){
int len=strlen(str);
int p=0;
for(int i=0; i<len; i++){
int ch=str[i]-'a';
if(!tire[p][ch])
tire[p][ch]=++ant;
p=tire[p][ch];
}
cnt[p]++;
}
int srh(char *str){
int ans=0;
int len=strlen(str);
int p=0;
for(int i=0; i<len; i++){
int ch=str[i]-'a';
p=tire[p][ch];
if(!p) return ans;
ans+=cnt[p];
}
return ans;
}
其实也有指针写法,可能面试官只能看懂指针
const int maxn = 26;
struct tireNode{
tireNode* next[maxn];
bool endpos;
tire(){
endpos = 0;
}
};
struct tire{
tireNode *root;
tire(){
root = new tireNode();
}
void Insert(char *str){
tireNode *now = root;
while(*str != ' '){
int pos = *str - 'a';
if(now->next[pos] == nullptr){
now -> next[pos] = new tireNode();
}
now = now -> next[pos];
str++;
}
now -> endpos = 1;
}
bool Find(char *str){
tireNode *now = root;
while(*str != ' '){
int pos = *str - 'a';
if(now -> next[pos] == nullptr){
return 0;
}
now = now -> next[pos];
str++;
}
return now -> endpos;
}
};
KMP
(Next[i]):表示字符串([0...i])的前缀和后缀的最大匹配位置,前后缀都不能包括本身
所以(Next[0]=-1),因为第一个字符肯定没有这个最大匹配位置
故(i)从1开始,(j)从-1开始
根据循环不变式,在循环的过程中维持,$Next[i] (最后一定等于)j(,即)[0...j](一定是最长的以)i$为结尾的后缀与模式串前缀匹配的字符串。
void get_Next(char *p){
Next[0] = -1;
int i = 1,j = -1;
while(p[i] != ' '){
while(j != -1 && p[i] != p[j+1]) j = Next[j];
if(p[i] == p[j+1])++j;
Next[i++] = j;
}
}
int kmp(char *s,char *p){
int i = 0,j = -1;
int ans = 0;
while(s[i] != ' '){
while(j != -1 && ( p[j+1] == ' ' || s[i] != p[j+1]))j = Next[j];
if(s[i] == p[j+1])++j;
f[i++] = j;
if(p[j+1] == ' '){
//视情况
}
}
return ans;
}
AC自动机
关键点就是从fafail序列里找到第一个(tr[u][i])不为空的,为了节省时间采用了路径压缩。
namespace AC {
int tr[N][26], tot;
int e[N], fail[N];
void insert(char *s) {
int u = 0;
for (int i = 1; s[i]; ++i) {
if (!tr[u][s[i] - 'a']) tr[u][s[i] - 'a'] = ++tot;
u = tr[u][s[i] - 'a'];
}
e[u]++;
}
queue<int> q;
void build() {
for (int i = 0; i < 26; ++i) {
if (tr[0][i]) {
q.push(tr[0][i]);
}
}
while (!q.empty()) {
int u = q.front();
q.pop();
for (int i = 0; i < 26; ++i) {
if (tr[u][i]) {
fail[tr[u][i]] = tr[fail[u]][i];
q.push(tr[u][i]);
} else {
tr[u][i] = tr[fail[u]][i];
}
}
}
}
int query(char *t) {
int u = 0, res = 0;
for (int i = 1; t[i]; i++) {
u = tr[u][t[i] - 'a'];
for (int j = u; j && e[j] != -1; j = fail[j]) {
res += e[j], e[j] = -1;
}
}
return res;
}
}
数学
线性筛法
采用性质:每个非素数都有一个最小的素因子
void get_prime(){
int pos = 0;
for(int i=2;i<N;++i){
if(!check[i]) {
Prime[pos++]=i;
}
for(j=0;j < pos && i*Prime[j] < N;++j){
check[i * Prime[j]] = true;
if(i % Prime[j] == 0) {
break;
}
}
}
}
约数
倍数法得到正约数集合
for(int i = 1;i <= n;++i){
for(int j = 1;j <= n/i;++j){
factor[i*j].push_back(i);
}
}
单个数的约数:试除法
for(int i = 1;i * i <= n;++i){
if(n % i == 0){
factor[++m] = i;
if(i != n/i) factor[++m] = n/i;
}
}
数据结构
平衡树
翻转至少要会,如果不是根节点,可以Rotate
inline void Rotate(int x) {
int y = s[x].fa, z = s[y].fa, chk = get(x);
//y与x的子节点相连
s[y].ch[chk] = s[x].ch[chk ^ 1];
s[s[x].ch[chk ^ 1]].fa = y;
//x与y父子相连
s[x].ch[chk ^ 1] = y;
s[y].fa = x;
// x与y的原来的父亲z相连
s[x].fa = z;
if(z) s[z].ch[y == s[z].ch[1]] = x;
//只有x和y的sz变化了
maintain(y);
maintain(x);
}
树状数组
int ask(int x){
int ans = 0;
for(;x;x -= x & -x) ans += c[x];
return ans;
}
void add(int x,int y){
for(;x <= N;x += x & -x) c[x] += y;
}
线段树
struct node{
int l,r;
int sum,add;//和,延迟标记
}t[maxn << 2];
void build(int p,int l,int r){//后序遍历
t[p].l = l,t[p].r;
if(l == r) {t[p].sum = s[l];return;}
int mid = (l+r) >> 1;
build(p << 1,l,mid);
build(p << 1 |1,mid + 1,r);
t[p].sum = t[p << 1].sum + t[p << 1 | 1].sum;//更新来自子结点的答案
}
void spread(int p){
if(add(p)){//如果有标记
t[p << 1].sum = t[p].add * (t[p << 1].r - t[p << 1].l + 1);
t[p << 1 | 1].sum = t[p].add * (t[p << 1 | 1].r - t[p << 1 | 1].l + 1);
t[p << 1].add += t[p].add;
t[p << 1 | 1].add += t[p].add;
t[p].add;
}
}
void change(int p,int l,int r,int d){
/*if(t[p].l == t[p].r){
t[p].sum += d;
}如果不用延迟标记
*/
if(l <= t[p].l && r >= t[p].r){
t[p].sum += d * (t[p].r - t[p].l + 1);
t[p].add += d;
return;
}
spread(p);
int mid = t[p].l + t[p].r >> 1;
if(l <= mid) change(p << 1,l,r,d);
if(r > mid) change(p << 1 | 1,l,r,d);
t[p].sum = t[p << 1].sum + t[p << 1 | 1].sum;//更新来自子结点的答案
}
long long ask(int p,int l,int r){
if(l <= t[p].l && r >= t[p].r){
return t[p].sum;
}
long long ans = 0;
int mid = (t[p].l + t[p].r) >> 1;
if(l <= mid) ans += ask(p << 1,l,r);
if(r > mid) ans += ask(p << 1|1,l,r);
return ans;
}
图论
最短路dij
- 初始化d[1]=0,其余结点为正无穷大
- 找出一个没有被标记,(d[x])最小的结点(x),标记结点(x)
- 扫描结点(x)的所有出边((x,y,z)),如果(d[y]>d[x]+z),则使用(d[x]+z)更新(y)
memset(d,inf,sizeof(d));
d[1] = 0;
q.push(make_pair(0,1));
while(!q.empty()){
int x = q.top().second;
q.pop();
if(v[x]) continue;
v[x] = 1;
for(int i=head[x];i;i=e[i].next){
int y = e[i].to;
int z = e[i].val;
if(d[y] > d[x] + z){
d[y] = d[x] + z;
q.push(make_pair(-d[y],y));
}
}
}
最小生成树
- 建立并查集,每个点各自构成一个集合
- 把所有的边按权值从小到大排序,依次扫描每条边((x,y,z))
- 把(x,y)属于同一集合(连通),则忽略这条边
- 否则,合并(x,y)所在的集合,并把(z)累加到答案中
int get(int x){
if (x == fa[x]) return x;
return fa[x] = get(fa[x]);
}
void solve(){
for(int i=1;i<=n;++i) fa[i] = i;//1
sort(e + 1,e + 1 + m,cmp);//2
int ans = 0;
for(int i = 1;i <= m;++i){
int x = get(e[i].x);
int y = get(e[i].y);
if(x != y){
fa[x] = y;
ans += e[i].z;
}
}
}
匈牙利算法
bool dfs(int x){
for(int i=head[x];i;i=e[i].next) {
if(!vis[y = e[i].to]){
vis[y] = 1;
if(!match[y] || dfs(match[y])) {
match[y] = x;
return true;
}
}
}
}
for(int i = 1;i <= n; ++i){
memset(vis,0,sizeof(vis));
if (dfs(i)) {ans++;}
}