用来求链表是否存在环并且找到环的起点和长度。也可以用来寻找数组的重复元素的(技巧算法)
快指针速度为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
记从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判断是否存在负圈。
该算法不能存在负圈。
下面使用最小堆实现实现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));
}
}
}
}
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
思路就是每次选取已包含的顶点和未包含的顶点之间的最短边,加入到其中。
以下是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
每次找到最小边插入其中,使用并查集实现
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
一些运算集合可以如下表示:
{i}
: 1<
{0,1,...,n-1}
: (1<
if(S>>i & 1)
S|1<
S& ~(1<
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<
对于非零整数, 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);
}
};
//一条折现由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是能够完成下述操作的数据结构:
给定一个初始值全是0的数列,a1,a2,a3,…,an
BIT保留线段树的所有左孩子节点。
计算前i项和需要从i开始,不断加入当前位置的i,并从i中减去i的二进制最低非0位对应的幂,直到i变成0为止。i的二进制最后一个i就是(i&-i)
//两个操作均为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;
}
}
};
//给定一个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<
树堆可以在logn时间内完成:
#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)匹配算法
- 滚动哈西算法(容易实现)
- 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
你可能感兴趣的:(算法)