C++ 算法笔记

一些算法笔记

Floyd判圈算法

用来求链表是否存在环并且找到环的起点和长度。也可以用来寻找数组的重复元素的(技巧算法)

快指针速度为2,慢指针速度为1。如果两个指针走着走着相交于A。则链表存在环。

求环长度:指针走一圈回到焦点,长度就是全长

求环起点:一个慢指针和交点位置慢指针分别从链表头和交点出发。第一次相交位置就是环的起点。

证明略

//给定一个包含 n + 1 个整数的数组 nums,其数字都在 1 到 n 之间(包括 1 和 n),可知至少存在一个重复的整数。假设只有一个重复的整数,找出这个重复的数。

int findDuplicate(vector& nums) {
    int t=0,h=0;
    do{
        t=nums[t];
        h=nums[nums[h]];
    }while(h!=t);
    h=0;
    do{
        t=nums[t];
        h=nums[h];
    }while(h!=t);
    return h;
}

二分查找法

二分查找的排除法

可以用来解决排序后的查找问题和一些限制n求最小m的问题。

详见:https://leetcode-cn.com/problems/search-insert-position/solution/hua-jie-suan-fa-35-sou-suo-cha-ru-wei-zhi-by-guanp/

模板:

搜索一个大于target的元素

left=0,right=size;//如果target过大,就是size
while(left>1);
	if(v[mid]

搜索一个小于target的元素

left=0,right=size-1;
while(left>1); //注意+1防止陷入死循环
	if(v[mid]>target)right=mid-1;
	else left=mid;
}

当然可以使用库函数lower_bound和upper_bound

假定是否可行

//有N条绳子,长度Li。从中切割出K条长度相同绳子,K最长多长
int N,K;
double L[MAX_N];
bool C(double x){
    int num=0;
    for(int i=0;i=K;
}
void solve(){
    double lb=0,ub=INF;
    for(int i=0;i<100;i++){
        double mid=(lb+ub)/2;
        if(C(mid))lb=mid;
        else ub=mid;
    }
    printf("%.2f\n",floor(ub*100/100));
}

最大化最小值

//N个屋子,在Xi位置,放入N个牛,最大化每个牛距离
int N,M;
int x[MAX_N];
bool C(int d){
    int last=0;
    for(int i=1;i1){
        int mid = (lb+ub)/2;
        if(C(mid))lb=mid;
        else ub=mid;
    }
    printf("%d\n",lb);
}

##最大化平均值

//有n物品重量Wi价值Vi,选出k个使得单位重量价值最大
//sum(Vi - x*Wi) >=0时,x满足大于等于最大平均价值

int n,k;
int w[MAX_N],v[MAX_N];
double y[MAX_N]; //v=x*w
bool C(double x){
    for(int i=0;i=0;
}
void solve(){
    double lb=0,ub=INF;
    for(int i=0;i<100;i++){
        double mid = (lb+ub)/2;
        if(C(mid))lb=mid;
        else ub=mid;
    }
    printf("%.2f\n",ub);
}

尺选法

经常求连续数组元素问题。这里省略

反转问题

//N个开关关或者开,每一次可以使K个连续的开关反转。求为了让所有开关全开 所需要最少操作次数M和对应的K值

//思路:反转顺序无关紧要,而且同一个区间进行两次以上反转是没有意义的。考虑最左端开关:包含这个开关区间只有一个,我们可以通过它判断最左端区间是否需要反转。这样问题规模缩小了1。不断重复这个过程,就可以求出最小反转次数了。
int N;
int dir[MAX_N]; //开关状态,是0:开,还是1关
int f[MAX_N]; //区间[i,i+K-1]是否需要反转

int calc(int k){
    memset(f,0,sizeof(f));
    int res=0;
    int sum=0; //一个区间的f的总和
    for(int i=0;i+K<=N;i++){
        if((dir[i]+sum)%2==0){
            ++res;
            f[i]=1;
        }
        sum+=f[i];
        if(i-K+1>=0)sum-=f[i-K+1];
    }
    //检查剩下有没有关闭的情况
    for(int i=N-K+1;i=0)sum-=f[i-K+1];
    }
    return res;
}

void solve(){
    int K=1;M=N;
    for(int k=1;k<=N;k++){
        int m=calc(k);
        if(m>=0 && M>m)M=m,K=k;
    }
    printf("%d %d\n",K,M);
}
//有一个MxN的各自,格子可以反转正反面。他们一面是黑色,另一面是白色。黑色反转后就是白色,反之白色反转就是黑色。你每次反转一个格子,他的上下左右所有格子都会翻成白色。现在给定每个格子颜色,求出把所有格子反转成白色时候的每个格子的最小反转次数。如果最小步数有多个解,输出字典序最小的一组。不存在输出IMPOSSIBLE

//问题分析:1. 同一个格子反转两次就会恢复。所以多次反转是多余的。 2. 反转格子相同的话,反转的次序无关紧要。 所以一共有2^(MxN)组解。   方法就是先指定最上方一行的翻转方法,然后直接判断(2,1)是否需要翻转。类似进行(2,1)~(2,N)。反复直到所有格子指定为止

//相邻格子坐标
const int dx[5]={-1,0,0,0,1};
const int dy[5]={0,-1,0,1,0};

//input
int M,N;
int title[MAX_N][MAX_N];

int opt[MAX_N][MAX_N]; //保存最优结果
int  filp[MAX_N][MAX_N];  //保存中间结果

//查询x,y颜色
int get(int x,int y){
    int c=tile[x][y];
    for(int d=0;d<5;d++){
        int x2=x+dx[d],y2=y+dy[d];
        if(0<=x2&&x2>j &1;
        }
        int num=calc();
        if(num>=0&&(res<0 || res>num)){
            res=num;
            memcpy(opt,flip,sizeof(flip));
        }
    }
    if(res<0)printf("IMPOSSIBLE\n");
    else{
        for(int i=0;i

图论问题总结

最短路径问题

Bellman-Ford算法 O(VxE) 常用判断负圈情况

记从s出发到顶点i的最短路径是d[i]。则一下成立:

d[i] = min{d[j]+(从j到i的边的权重)|e=(j,i)}

如果图是一个DAG,就可以先拓扑排序(通过不断取出入度为0的节点实现),然后利用这个递推关系计算d。但是如果图中有圈,就无法使用特定顺序计算。这种情况,设置d[s]=0,d[i]=INF不断更新d。

Bellman-Ford算法的特点是可以检查这个图中是否存在负圈。

下面是不存在负圈的情况:

struct edge{int from,to,cost};

edge es[MAX_E];
int d[MAX_V];
int V,E; //顶点和边数

void shortest_path(int s){
    for (int i=0;id[e.from]+e.cost){
                d[e.to] = d[e.from] + e.cost;
                update = true;
            }
        }
        if(update == false)break;
    }
}

如果图中不存在从s可达的负圈,那么最短路径不会经过同一个顶点两次。也就是外层while循环最多不会循环V次。所以我们可以通过计数while循环次数是否超过V判断是否存在负圈。

Dijkstra算法

该算法不能存在负圈。

下面使用最小堆实现实现O(|E|log|V|)

struct edge {int to,cost;};
typedef pair P; //first是最短距离,second是顶点编号
int V;
vector G[MAX_V];
int d[MAX_V];

void dijkstra(int s){
    //按照first排序的小顶堆
    priority_queue,greater

>que; fill(d,d+V,INF); d[s]=0; que.push(P(0,s)); while(!que.empty()){ P p = que.top();que.pop(); int v= p.second; if(d[v] < p.first)continue; for(int i=0;id[v]+e.cost){ d[e.to]=d[v]+e.cost; que.push(P(d[e.to],e.to)); } } } }

floyd-warshall算法 任意两点最短问题 O(V^3)

int d[MAX_V][MAX_V]; //d[u][v]表示e(u,v)权重,不存在时候为INF,d[i][i]=0
int V;

void warshall_floyd(){
    for(int K = 0; k < V; k++)
        for(int i = 0; i< V;i++)
            for(int j= 0;j

最小生成树

prim 算法

思路就是每次选取已包含的顶点和未包含的顶点之间的最短边,加入到其中。

以下是O(V^2)实现,如果是使用小顶堆就是O(|E|log|V|)

int cost[MAX_V][MAX_V];
int mincost[MAX_V];
bool used[MAX_V];
int prim(){
    for(int i=0;i

kruskal 算法

每次找到最小边插入其中,使用并查集实现

struct UFTree{
    vector par;
    UFTree(int n):par(n){
        for(int i=0;i

查并集

//连续性
struct UFTree{
    vector par;
    UFTree(int n):par(n){
        for(int i=0;i par;
  UFTree()=default;
  UFTree(int n){
    par.reserve(n);
  }
  int find(int x){
    if (par.find(x)==par.end())return x;
    return par[x]=find(par[x]);
  }
  void unite(int x,int y){
    x=find(x);y=find(y);
    if(x==y)return;
    par[x]=y;
  }
};

数学窍门

辗转相除法

举例:

给定平面上面两个格点:P1(x1,y1)和P2(x2,y2)。线段上面除了P1,P2还有几个格点?

-10^9<=x1,x2,y1,y2<=10^9

这道题就是求最大公约数

int gcd(int a,int b){
    if(b==0)return a;
    return gcd(b,a % b);
}

素数

关键定理:x以内的所有合数的最小质因数都不超过x^0.5

//素数测试
bool is_prime(int n){
    for(int i=2;i*i<=n;i++){
        if(n%i==0)return false;
    }
    return n!=1;
}

//约数枚举
vector divisor(int n){
    vector res;
    for(int i=1;i*i<=n;i++){
        if(n%i==0){
            res.push_back(i);
            if(i!=n/i)res.push_back(n/i);
        }
    }
    return res;
}

//整数分解
map prime_factor(int n){
    map res;
    for(int i=2;i*i<=n;i++){
        while(n%i==0){
            ++res[i];
            n/=i;
        }
    }
    if(n!=1) res[n]=1;
    return res;
}

//筛子
int prime[MAX_N];//第i个素数
bool is_prime[MAX_N+1]; //is_prime[i]表示i是不是素数
int sieve(int n){
    int p=0;
    for(int i=0;i<=n;i++)is_prime[i]=true;
    is_prime[0]=is_prime[1]=false;
    for(int i=2;i<=n;i++){
        if(is_prime[i]){
            prime[p++]=i;
            for(int j=2*j;j<=n;j+=i)is_prime[j]=false;
        }
    }
    return p;
}

模运算

注意:

a是负数时候a%m也是负数。所以a%m + m就可以保证结果在0~m-1范围内。

假设a==c(mod m) && b==d(mod m),以下成立:

a+b == c+d(mod m)
a-b == c-d(mod m)
a*b == c*d(mod m)

快速幂

typedef long long ll;
ll mod_pow(ll x,ll n,ll mod){
    ll res=1;
    while(n>0){
        if(n&1)res=res*x % mod;
        x=x*x % mod;
        n>>=1;
    }
    return res;
}

集合的整数表示

程序中表示集合的方法有很多种。当元素比较少时候,通过二进制码表示起来比较方便。

集合{0,1,2,....,n-1}可以通过一个整数按位表示:f(S)=sum(2^i) i属于S

一些运算集合可以如下表示:

  • 空集 :0
  • 只有第i个元素的集合{i}: 1<
  • 含有所有元素的集合{0,1,...,n-1}: (1<
  • 判断第i个元素是否属于集合S : if(S>>i & 1)
  • 向集合中加入第i个元素 : S|1<
  • 从集合中去掉第i个元素 : S& ~(1<
  • 集合S,T并集 : S|T
  • 集合S,T交集 : S&T

枚举集合所有子集

for(int S=0;S < 1<

枚举集合sup的子集。其中sup是一个二进制码,本身也是某个集合的子集。
这里思路就是每次sup减一,然后和sup按位与

int sub=sup;
do{
    //对sub处理
    sub=(sub-1)⊃
}while(sub!=sup); //处理完0之后,就会有-1&sup==sup

//或者更简单
for(int k=S;;k=(k-1)&S){
	//对k处理
}

枚举{0,1,...,n-1}所包含的所有大小为k的子集方法。

int comb=(1<>1) | y;
}

解释代码:按照字典序的话,最小子集是 (1<,作为初始值。下面是求出下一个二进制码的方法:

  1. 求出最低位1开始的连续1的区间 0101110 -> 0001110
  2. 把这个区间全部变为0,并把区间左侧那个0变为1 0101110 -> 0110000
  3. 将第一步取出的区间右移,直到剩下的1个数减小了1个 0001110 -> 0000011
  4. 将第二步和第三步结果按位取或 0110000|0000011 -> 0110011

对于非零整数, comb&(-comb)就是取最低位的1,设为x。y=comb+x就是把最低位开始的连续的1都置0了。通过计算z=comb&~y得到了最低位1开始连续区间。就这样完成了转换。

折半枚举

问题:

// 给定各有n个整数的四个数列A,B,C,D。要从每个数列当中取出一个数字,使得四个数字和为0。求出这样组合的个数。一个数列中有相同数字时候会作为不同数字看待。 1<=n<=4000

//思路:如果考虑枚举,四个数列一共有n^4种情况,如果分为A,B和C,D,每一个只有n^2种情况,可以进行枚举。从A,B中取出a,b后,为了使得总和为0,需要从C,D中取出c+d=-(a+b)。因此先枚举C,D的n^2种取法,然后排序,枚举A,B,二分搜索找C,D枚举结果,复杂度为O(n^2 x logn)

int n;
int A[MAX_N],B[MAX_N],C[MAX_N],D[MAX_N];
int CD[MAX_N*MAX_N];

void solve(){
    for(int i=0;i

坐标离散化

//问题:区域个数。在w*h的格子上面画了n条水平或者竖直的线。求出这些线把格子划分成为了多少个区域。
// 1<=w,h<=1000000    1<=n<=500

//思路:简单的dfs没法在太大的区域上面进行。因此使用坐标离散化技巧。也就是将前后没有变化的行或者列删除。这样下来数组里面只需要储存有直线的列和其前后的列就行了。这样6nx6n大小就足够。

int W,H,N;
int X1[MAX_N],X2[MAX_N],Y1[MAX_N],Y2[MAX_N];
bool fld[MAX_N *6][MAX_N *6];

int dx={0,0,-1,1};
int dy={-1,1,0,0};

//对x1,x2进行坐标离散化,返回离散化之后的宽度
int compress(int *x1,int *x2,int w){
    vector xs;
    for(int i=0;i> que;
            que.push(make_pair(x,y));
            while(!que.empty()){
                int sx=que.front().first,sy=que.front().second;
                que.pop();

                for(int i=0;i<4;i++){
                    int tx=sx+dx[i],ty=sy+dy[i];
                    if(tx<0||W<=tx||ty<0||H<=ty)continue;
                    if(fld[ty][tx])continue;
                    que.push(make_pair(tx,ty));
                    fld[ty][tx]=true;
                }
            }
        }
    }
    printf("%d\n",ans);
}

线段树

线段树是一颗完美二叉树。所有叶子深度相同。下面以RMQ线段树为例。

RMQ线段树可以给定数列a0,a1,an-1情况下,在logn时间内完成

  • 求出一定范围的最小值
  • 更改数列的元素值
const int MAX_N = 1 <<17;

//储存线段树全局数组
int n,dat[2*MAX_N -1];

//RMQ的长度选取2的指数
void init(int n_){
    n=1;
    while(n0){
        k=(k-1)/2;
        dat[k]=min(dat[k*2+1],dat[k*2+2]);
    }
}
//求[a,b)的最小值。
//附加参数是为了递归方便调用。k是节点编号,l,r表示这个节点对应的是[1,r)区间
//外部调用使用query(a,b,0,0,n);
int query(int a,int b,int k,int l,int r){
    //如果[a,b)和[l,r)不相交,返回INT_MAX
    if(r<=a||b<=l)return INT_MAX;
    //如果完全包含,返回当前节点值
    if(a<=l&&r<=b) return dat[k];
    else{
        int vl=query(a,b,k*2+1,l,(l+r)/2);
        int vr=query(a,b,k*2+2,(l+r)/2,r);
        return min(vl,vr);
    }
}

区间求和模板

struct SegTr{
    int realn;
    int n;
    vector tr;
    vector data;
    static int pow2gt (int x){	
        --x;x|=x>>1;x|=x>>2;x|=x>>4;
        x|=x>>8;x|=x>>16;return x+1;
    }
    SegTr(vector nums):realn(nums.size()){
        n=pow2gt(realn);
        if(n==0)return;
        tr=vector(2*n-1,0);
        data.swap(nums);
        build(0,0,n);
    }
    SegTr(int num):realn(num){
        n=pow2gt(realn);
        if(n==0)return;
        tr=vector(2*n-1,0);
    }
    void build(int k,int l,int r){
        if(r-l==1){
            if(l0){
            k=(k-1)/2;
            tr[k]=tr[k*2+1]+tr[k*2+2];
        }
    }
    int query_(int a,int b,int k,int l,int r){
        if(r<=a||b<=l)return 0;
        if(a<=l&&r<=b)return tr[k];
        else 
            return query_(a,b,k*2+1,l,(l+r)/2) +
                query_(a,b,k*2+2,(l+r)/2,r);
    }
    int query(int a,int b){
        return query_(a,b,0,0,n);
    }
};

线段树例题1

//一条折现由N条线段首尾相接组成。第i条线段长度是Li。最开始所有线段都笔直连接,指向上方。有C条指令,指令i给出整数Si和Ai,是的Si和Si+1之间的角度变成Ai度(角度指的是线段Si逆时针旋转到Si+1所经过的角度。最开始都是180°。按照顺序执行这C条指令。每条指令之后输出折线前段(第N条线段端点的坐标)。假设折线尾部端点(第1条折线的起点)起始坐标是(0,0)。

//思路:使用线段树来解决。每个节点表示一段连续的线段集合。并且维护以下两个值。
// 1. 把对应线段集合集合的第一条线段转至垂直方向之后,从第一条线段起点指向最后一条线段终点的向量。
// 2. 如果这个节点有儿子节点,两个儿子节点对应部分链接之后,右儿子需要转动的角度。

//如果节点i表示向量是vx,vy,角度是ang。两个儿子节点是chl,chr。那么就有
// vx = vx+ (cos(ang)*vx - sin(ang)*vy)
// vy = vy+ (sin(ang)*vx - cos(ang)*vy)

const int ST_SIZE = (1<<15) -1;

//输入
int N,C;
int L[MAX_N];
int S[MAX_C],A[MAX_N];

//线段树保护的数据
double vx[ST_SIZE],vy[ST_SIZE];
double ang[ST_SIZE];

//为了查询角度保存的当前角度的数组
double prv[MAX_N];

//初始化线段树
//k是节点编号,1,r表示当前节点对应的区间是[l,r)区间
void init(int k,int l,int r){
    ang[k]=vx[k]=0.0;
    if(r-l==1){
        //叶子节点
        vy[k]=L[l];
    }else{
        int chl=k*2+1,chr=k*2+2;
        init(chl,l,(l+r)/2);
        init(chr,(l+r)/2,r);
        vy[k]=vy[chl]+vy[chr];
    }
}

//把s和s+1的角度变为a
//v是节点编号,1,l,r表示当前节点对应的是[l,r)区间
void change(int s,double a,int v,int l,int r){
    if(s<=v)return;
    if(s

树状数组 BIT

BIT是能够完成下述操作的数据结构:

给定一个初始值全是0的数列,a1,a2,a3,…,an

  • 给定i,计算a1+a2+…+ai
  • 给定i和x,执行ai+=x

BIT保留线段树的所有左孩子节点。

BIT的求和

计算前i项和需要从i开始,不断加入当前位置的i,并从i中减去i的二进制最低非0位对应的幂,直到i变成0为止。i的二进制最后一个i就是(i&-i)

BIT模板

//两个操作均为O(log n)
//[1,n]
//index from 1-n
struct Bit{
    vector tr;
    int n;
    Bit(int num):n(num),tr(num+1,0){}
    int sum(int i){
        int s=0;
        while(i>0){
            s+=tr[i];
            i-=i&-i;
        }
        return s;
    }
    //dat[i]+=x; i = [1,n]
    void add(int i,int x){
        while(i<=n){
            tr[i]+=x;
            i+=i&-i;
        }
    }
};

BIT例题

//给定一个1~n的排列a0,a1,..,an-1。求这个排列进行冒泡排序所需要的交换次数。
//思路:如果n太大不能模拟冒泡过程。所交换的次数等价与满足iaj的(i,j)数对的个数(也就是所谓的逆序数)。构建一个1~n的BIT。按照j=0,1,2...n-1顺序进行如下操作。1.把j-(BIT查询的前aj项和加到答案当中) 2.把BIT中aj位置上值+1

//简单来说就是不断加上自己前面小于自己的数字的个数!

typedef long long ll;
int n,a[MAX_N];

void solve(){
    ll ans=0;
    for(int j=0;j

分桶法和平方分割

分桶法把一排物品或者平面分成桶,每个桶维护自己的内部信息。其中平方分割是把一排n个元素每n0.5个分在一个桶里面维护的方法统称。可以减低区间操作复杂度O(n0.5)

//给定一个数列a1,a2,,,an和m个三元组表示查询操作。对于每个查询(i,j,k),输出ai,ai+1,,,aj的升序排序的第k个数

可以建立特殊的线段树,每个节点是所有子节点的排序(节点包含的元素数是子节点元素数之和。这样就可以高效的搜索结果的值。这里忽略代码过程。

荷兰国旗问题

如何高效的三划分一个数组。分为三部分a,b,c。其中a

int dat[N];
//把N分为小于pat的部分,大于pat的部分和等于pat的部分
void solve(){
    int l=0,h=N-1,i=0;
    int pat;
    cin>>pat;
    while(i<=h){
        if(dat[i]pat)swap(dat[i],dat[h--]);
        else ++i;
    }
    for(auto &i:dat)cout<

Treap

树堆可以在logn时间内完成:

  1. 插入
  2. 删除
  3. 找到第n大的元素
  4. 找到给定元素的排行位置
  5. 找到指定元素的前驱
  6. 找到指定元素的后继
#include
#include
#include
using namespace std;

struct TpNode{
  int fa;
  int ls,rs;
  int key;
  int siz;
  TpNode(){ls=rs=0;}
};

struct Treap{
  static const int SIZEN=100010,INF=0x7fffffff/2;
  static constexpr double alpha=0.75,lga=log(1.0/alpha);

  int N;
  int root,tot;
  TpNode tr[SIZEN];
  Treap(){
    root=1;tot=2;
    tr[1].key=-INF;tr[1].rs=2;tr[1].siz=2;
    tr[2].key=INF;tr[2].fa=1;tr[2].siz=1;
  }

  bool balance(int x)
  {
    if(alpha*tr[x].siz<=max(tr[tr[x].ls].siz,tr[tr[x].rs].siz)) return 0;
    return 1;
  }
  int len=0;
  int lis[SIZEN];

  void dfs(int x)
  {
    if(tr[x].ls) dfs(tr[x].ls);
    lis[++len]=x;
    if(tr[x].rs) dfs(tr[x].rs);
  }
  int built(int l,int r)
  {
    if(l>r) return 0;
    int mid=(l+r)/2,x=lis[mid];
    tr[x].ls=built(l,mid-1),tr[x].rs=built(mid+1,r);
    tr[tr[x].ls].fa=x;tr[tr[x].rs].fa=x;
    tr[x].siz=tr[tr[x].ls].siz+tr[tr[x].rs].siz+1;
    return x;
  }
  void rebuilt(int x)
  {
    len=0;
    dfs(x);
    int f=tr[x].fa;
    int l;
    if(tr[f].ls==x) l=0;
    else l=1;
    int y=built(1,len);
    if(l==0) tr[f].ls=y,tr[y].fa=f;
    else tr[f].rs=y,tr[y].fa=f;
    if(root==x) root=y;
  }
  //插入元素
  void insert(int t)
  {
    int now=root,x=++tot,dep=0;
    tr[x].siz=1,tr[x].key=t;
    while(now)
      {
	tr[now].siz++;
	if(tlog(tot+1.0)/lga)
      {
	int r=0;
	for(now=x;now;now=tr[now].fa) if(!balance(now)) r=now;
	if(r) rebuilt(r);
      }
  }
  //查找元素,返回数组中位置。找不到返回0
  int find(int t)
  {
    int now=root;
    while(now)
      {
	if(tr[now].key==t) return now;
	else if(tr[now].key=k) now=tr[now].ls;
	else k-=tr[tr[now].ls].siz+1,now=tr[now].rs;
      }
    return now;
  }
  //获取前驱
  int low(int t)
  {
    int now=root,ans=-INF;
    while(now)
      {
	if(tr[now].keyt) ans=min(ans,tr[now].key),now=tr[now].ls;
	else now=tr[now].rs;
      }
    return ans;
  }
  int get(int k){return tr[k].key;}
};

/*
全部是logn

初始化(默认包含两个元素,tp.INF  -tp.INF):
Treap tp;
插入:
tp.insert(key);
删除:
tp.erase(find(key));
获得第k大的元素(从1开始,第1是-tp.INF,第2才是最小元素):
tp.get(tp.getth(k));
获得元素的排名(其中-tp.INF是第0位,最小值是第1位,依次类推):
tp.rankof(key);
获得前驱后继:
tp.low(key);tp.high(key);
*/
#include
#include
#include
int main()
{
  vector t={4,5,3,2,6,5,4,5,1,2,0};
  Treap tp;
  for(auto &i:t)tp.insert(i);
  for(auto &i:t)cout<

匈牙利算法

求最大匹配啦

两个数据组

例题:https://leetcode-cn.com/problems/broken-board-dominoes/

//第一组数据个数N,第二组数据个数M
bool edge[N][M];

int co[M];//储存第二组与第一组的匹配

//x是第一组里面的数据,寻找与第二组的匹配
bool m_used[M];//辅助数组
bool find(int x){
    int i;
    for(i=0;i

一个数据组

//节点0~N-1
bool edge[MAXN][MAXN];
int N;
int co[N];//储存匹配结果,初始化-1

bool used[MAX];
bool find(int x){
    used[x]=true;
    for(int i=0;i

矩阵

解方程组

高斯消元法 O(n^3)

const double EPS=1E-8;
typedef vector vec;
typedef vector mat;

//求解Ax=b,A是方阵。
//当方程组无解或者无穷解时候,返回长度为0的数组
vec guass_jordan(const mat& A,const mat& b){
    int n=A.size();
    mat B(n,vec(n+1));
    for(int i=0;iabs(B[pivot][i]))pivot=j;
        }
        swap(B[i],B[pivot]);
        //无解或者无穷多解
        if(abs(B[i][i])

常用技巧

霍夫曼编码

https://leetcode-cn.com/problems/minimum-time-to-build-blocks/solution/24ms-huo-fu-man-by-yanghaomai/

单调栈

https://leetcode-cn.com/problems/number-of-valid-subarrays/solution/dai-ma-jian-dan-shi-yong-zhan-by-yanghaomai/

单调队列

https://leetcode-cn.com/problems/dui-lie-de-zui-da-zhi-lcof/solution/140s-listmo-ni-dui-lie-by-yanghaomai/
//给定一个长度n的数列a0,a1,....,an-1和整数k,求数列bi=min{ai,ai+1,,,ai+k-1}(i=0,1,,,n-k)
//思路:可以使用RMQ在nlogn求出。但是也可以使用deque在O(n)解决问题

int k,n;
int a[MAX_N];
int b[MAX_N];
int deq[MAX_N]; //双端队列

solve(){
    int s=0,t=0;//双端队列头和尾
    for(int i=0;i=a[i])--t;
        deq[t++]=i;
        if(i-k+1>=0){
            b[i-k+1]=a[deq[s]];
            if(deq[s]==i-k+1){
                s++;
            }
        }
    }
    for(int i=0;i

字符串

这里就不说kmp了,大家用find代替kmp挺好

单字符串的动态规划

/*
只考虑由'A','G','C','T'四种字符组成的DNA字符串,给定一个长度为k的字符串S,计算长度恰好为n而且不包含S的字符串个数。

解法:
假设S为"ATCG",动态规划状态就是字符的个数和最后的三个字符。一共有三种状态:"**A","*AT","ATC"。也就是把生成字符串的后缀和S的前缀长度作为状态,一共只有k个状态。
不过对于"ATCATCG"这种字符串,"ATCATC"应该不属于"***ATC"才对。所以一般化来说,状态应该是生成的字符串后缀和S的前缀匹配的长度。有多个匹配时,选取最长的作为状态。

预处理O(k^3),动态规划O(kn)
*/

const char *AGCT="AGCT";
const int MOD=10009;

//输入
int K,k;
string S;

int next[MAX_K][4];//添加某个字符后转移
int dp[MAX_N+1][MAX_K];

void solve(){
    //预处理
    for(int i=0;i

字符串O(m+n)匹配算法

  1. 滚动哈西算法(容易实现)
  2. KMP(实现较难)

下面给出滚动哈西事例:

typedef unsigned long long ull;
const ull B = 100000007;
//判断b是否包含a
bool contain(string a,string b){
	int al=a.size(),bl=b.size();
	if(al>bl)return false;
	//计算b的al次方
	ull t=1;
	for(int i=0;i

后缀数组

后缀数组指的是某个字符串所有的后缀按字典序排序后得到的数组。数组不需要保存所有后缀字符串,之后需要记录起始位置就行了。

高度数组

所谓高度数组,就是后缀数组中相邻两个后缀的最长公共前缀的长度形成的数组。后缀数组记做sa,高度数据记做lcp

代码直接包含在上面后缀数组结构体当中

struct SaFac{
  string str;
  vector sa;
  SaFac(string s):sa(s.size()+1),lcp(s.size()+1,0){
    str.swap(s);
    int n=str.size();
    vector rank(n+1);
    //初始长度是1,rank取字符编码
    for(int i=0;i tmp(n+1);
    int k;
    auto lam=[&rank,&k,&n](const int&i,const int&j)->bool{
	       if(rank[i]!=rank[j])return rank[i] lcp;
  void construct_lcp(){
    int n=str.size();
    vector rank(n+1);
    for(int i=0;i<=n;i++)rank[sa[i]]=i;
    int h=0;lcp[0]=0;
    for(int i=0;i<=n;i++){
      //计算字符串数组中从位置i开始的后缀及其在后缀数组中前一个后缀的LCP
      int j=sa[rank[i]-1];
      //将h减去首字母的1长度,在保持前缀相同情况下不断增加
      if(h>0)h--;
      for(;j+h tmp;
  for(int i=0;i<=a.size();i++)tmp.push_back(a.substr(i));
  sort(tmp.begin(),tmp.end());
  cout<

求字符串T在S中出现的位置

使用后缀数组进行模式匹配在S>>T情况下效果更好。O(|T|log|S|)

bool contain(string S,vector& sa,string T){
    if(T.empty())return true;
    int a=0,b=S.length()-1;
    while(a

应用

好他妈好用,i了

//求S,T最长公共子字符串
//思路:
//如果计算一个字符串中至少出现两次的最长子串,答案一定会在后缀数组中相邻两个后缀的公共前缀之中。高度数组的最大值也就是答案。
//对于两个字符串,可以在S和T之间插入一个不会出现的字符(例如'\0')形成新字符串S'。然后后缀数组所有相邻后缀。其中,分属于S和T的不同字符串的后缀的lcp就是最大答案。

string S,T;
void solve(){
    string C=S+'\0'+T;
    SaFac sf(C);
    int sl=S.size();
    int ans=0;
    for(int i=0;i

你可能感兴趣的:(算法)