ACM总结

1.暴力

1.二分

针对于函数图单调的情况。
二分答案,当所求在区间内呈单调时直接二分即可。

bool pd(){}
//整数域
ll l,r,ans;
while(l<=r){
    ll mid=l+r>>1
    if(pd(mid)) ans=mid,l=mid+1;
    else r=mid-1;
}
//实数域
long double l,r,ans;
while(r-l>eps){
    long double mid=(l+r)/2;
    if(pd(mid)) r=mid;
    else l=mid;
}

2.三分

针对于函数图出现一个拐点的情况。
三分答案,当所求在区间内呈先增后减或先减后增即可。

bool pd(){}
//整数域
ll l,r,ans;
while(l+10<r){
    ll lm=l+(r-l)/3,rm=r-(r-l)/3;
    if(pd(lm)>=pd(rm)) r=rm;
    else l=lm;
}
for(int i=l;i<=r;i++) ans=max(ans,pd(i))
//实数域
long double l,r,ans;
while(r-l>eps){
    long double lm=l+(r-l)/3,rm=r-(r-l)/3;
    if(pd(lm)>=pd(rm)) r=rm;
    else l=lm;
}
ans=l;

3.分块

带根号的复杂度,把所有数据分成根号个块,对同一块的操作可以浓缩成一次。

分块九章

void init(){
    blo=sqrt(n);
    for(int i=1;i<=n;i++) block[i]=(i-1)/blo+1;
}
void update(ll l,ll r,ll w){
    for(int i=l;i<=min(block[l]*blo,r);i++) f[i]+=w;
    if(block[l]!=block[r])
        for(int i=(block[r]-1)*blo+1;i<=r;i++)
            f[i]+=w;
    for(int i=bolck[l]+1;i<=block[r]-1;i++)
        F[i]+=w;
}

4.莫队

离线区间询问,玄幻优化后左右指针不停跳动实现 O ( n n ) O(n\sqrt{n}) O(nn )
注意加的时候是先算再加,而减的时候是先减后算。

ll n,m,block,res,k;
ll a[manx],b[manx],c[manx],L[manx],R[manx],ans[manx];
struct query{
    ll l,r,id;
    bool operator < (const query & b){
        if(l/block==b.l/block) return r/block<b.r/block;
        return l/block<b.l/block;
    }
}q[manx];
void add(ll x,ll w){
    while(x<=n) c[x]+=w, x+=(x&(-x));
}
ll query(ll x){
    ll cnt=0;
    while(x) cnt+=c[x],x-=(x&(-x));
    return cnt;
}
void add(ll x){
    ll cnt=query(R[x])-query(L[x]);
    res+=cnt;
    add(a[x], 1);
  //  cout<<"add:"<<x<<" "<<L[x]<<" "<<R[x]<<endl;
}
void del(ll x){
    add(a[x], -1);
    ll cnt=query(R[x])-query(L[x]);
    res-=cnt;
  //  cout<<"del:"<<x<<" "<<L[x]<<" "<<R[x]<<endl;
}
void mo(){
    ll l=1,r=0;
    for(int i=1;i<=m;i++){
        ll ql=q[i].l,qr=q[i].r;
        while(l<ql) del(l++);
        while(l>ql) add(--l);
        while(r<qr) add(++r);
        while(r>qr) del(r--);
        ans[q[i].id]=res;
    }
}
int main(){
    n=read(),m=read(),k=read(); block=sqrt(n);
    for(int i=1;i<=n;i++){
        a[i]=read(); b[i]=a[i];
    }
    sort(b+1,b+1+n);
    ll sz=unique(b+1,b+1+n)-b-1;
    for(int i=1;i<=n;i++){
        L[i]=lower_bound(b+1,b+1+sz,a[i]-k)-b-1;
        R[i]=upper_bound(b+1,b+1+sz,a[i]+k)-b-1;
        a[i]=lower_bound(b+1,b+1+sz,a[i])-b;
    }
    for(int i=1;i<=m;i++){
        q[i].l=read(),q[i].r=read(),q[i].id=i;
    }
    sort(q+1,q+1+m);
    mo();
    for(int i=1;i<=m;i++){
        cout<<ans[i]<<endl;
    }
    return 0;
}

5.SG函数

必胜点和必败点的性质:

  • 所有的终结点都是必败点
  • 从任何必胜点操作,至少有一种方式进入必败点
  • 无论如何操作, 从必败点都只能进入必胜点.
//f[N]:可改变当前状态的方式,N为方式的种类,f[N]要在getSG之前先预处理
//SG[]:0~n的SG函数值
//S[]:为x后继状态的集合
ll f[N],SG[MAXN],S[MAXN];
void  getSG(int n){
    int i,j;
    memset(SG,0,sizeof(SG));
    //因为SG[0]始终等于0,所以i从1开始
    for(i = 1; i <= n; i++){
        //每一次都要将上一状态 的 后继集合 重置
        memset(S,0,sizeof(S));
        for(j = 0; f[j] <= i && j <= N; j++)
            S[SG[i-f[j]]] = 1;  //将后继状态的SG函数值进行标记
        for(j = 0;; j++) if(!S[j]){   //查询当前后继状态SG值中最小的非零值
            SG[i] = j;
            break;
        }
    }
}

2.数据结构

1.单调栈

单调栈用于求左(右)边第一个比自己大(小)的元素,栈内元素是单调的。
当然,也可以用这种思想去求左边第一个不是自己倍数的之类的。
算法的精华在于思想。

for(int i=1;i<=n;i++){
	l[i]=i-1;
	while(l[i]&&a[l[i]]%a[i]==0) l[i]=l[l[i]];
}

1.求左边第一个比自己大

for(int i=1;i<=n;i++){
	while(top&&a[s[top]]<=a[i]) --top;
	if(!top) lma[i]=0;
    else lma[i]=s[top];
    s[++top]=i;
}

2.求右边第一个比自己大

for(int i=n;i>=1;i--){
	while(top&&a[s[top]]<=a[i]) --top;
	if(!top) rma[i]=n+1;
    else rma[i]=s[top];
    s[++top]=i;
}

2.单调队列

维护区间最大(小)值,或者差值。
差值如何实现?
队列维护的元素是单调的,那么用两个队列维护最大值和最小值,两队头的元素差即是最大值和最小值的差值。

//窗口长度为k 求最大值
vector<ll>ans
ll q[N],a[N];
ll h=1,t=0,k; 
for(int i=1;i<=n;i++){
    while(h<=t&&q[h]<(i-k+1)) ++h;
    while(h<=t&&a[q[t]]<=a[i]) --t;  //此处改为 a[q[t]]>=a[i] 则是求窗口最小值
    q[++t]=i; 
    if(i>=k) ans.push_back(q[h]);
}

3.ST表

f [ i ] [ j ] 维 护 第 i 个 元 素 开 始 的 区 间 长 度 为 2 j 的 区 间 最 大 值 f[i][j] 维护第i个元素开始的区间长度为2^{j}的区间最大值 f[i][j]i2j
很 明 显 , f [ i ] [ 0 ] 为 当 前 元 素 很明显,f[i][0]为当前元素 f[i][0]
倍 增 思 想 , 这 个 自 己 画 图 就 可 以 理 解 了 : 倍增思想,这个自己画图就可以理解了: :
f [ i ] [ j ] = m a x ( f [ i ] [ j − 1 ] , f [ i + 2 j − 1 ] [ j − 1 ] ) f[i][j]=max(f[i][j-1],f[i+2^{j-1}][j-1]) f[i][j]=max(f[i][j1],f[i+2j1][j1])
关 于 询 问 的 处 理 , 可 以 找 到 不 大 于 区 间 长 度 的 2 的 倍 数 k 关于询问的处理,可以找到不大于区间长度的2的倍数k 2k
有 : x + 2 k − 1 = r 求 出 x 有:x + 2^{k} - 1= r 求出x x+2k1=rx
其 次 f [ l ] [ k ] 以 及 f [ r + 1 − 2 k ] [ k ] 就 可 以 覆 盖 [ l , r ] 区 间 其次f[l][k]以及f[r + 1 - 2^{k}][k]就可以覆盖[l,r]区间 f[l][k]f[r+12k][k][l,r]

//f[i][0]为自己。
//松弛区间
ll f[manx][25];
void init(){
    for(int j=1;j<=21;j++)
        for(int i=1;i+(1<<j)-1<=n;i++)
            f[i][j]=max(f[i][j-1],f[i+(1<<(j-1))][j-1]);    
}
//询问区间
ll query(ll l,ll r){
    ll k=log2(r-l+1);
    return max(f[l][k],f[r+1-(1<<k)][k]);
}        

4.并查集

连接两个节点。

ll f[manx];
void init(ll n){
    for(int i=1;i<=n;i++) f[i]=i;
}
ll find(ll x){
    return f[x]==x?x:f[x]=find(f[x]);
}

// 带权并查集
ll find(ll x)
{
	if (x != f[x])
	{
		ll t = f[x];
		f[x] = find(f[x]);
		w[x] += w[t];
	}
	return f[x];
}

5.树状数组

核心就是二进制和前缀和的思想,以维护 1 ~ i 的前缀和为主,过于复杂的操作可以改用线段树操作。

参考暴力部分的莫队专题

6.线段树

无他,唯手熟尔。

ll s[manx],laz[manx],a[manx];
inline void up(ll k){
    s[k]=s[k<<1]+s[k<<1|1];
}
inline void down(ll k,ll len){
    laz[k<<1]+=laz[k],laz[k<<1|1]+=laz[k];
    s[k<<l]+=laz[k]*(len-len>>1),s[k<<1|1]+=laz[k]*(len>>1);
    laz[k]=0;
}
//build(1,1,n);
inline void build(ll k,ll l,ll r){
    if(l==r){
        s[l]=a[l],laz[l]=0; return;
    }
    ll mid=l+r>>1;
    build(k<<1,l,mid); build(k<<1|1,mid+1,r);
    up(k);
}
//区间查询
inline ll query(ll k,ll l,ll r,ll ql,ll qr){
    if(l>=ql&&r<=qr) return s[k];
    if(laz[k]) down(k,r-l+1);
    ll mid=l+r>>1,ans=0;
    if(mid>=ql) ans+=query(k<<1,l,mid,ql,qr);
    if(mid<qr) ans+=query(k<<1|1,mid+1,r,ql,qr);
}
//区间更新
inline void update(ll k,ll l,ll r,ll ql,ll qr,ll w){
    if(l>=ql&&r<=qr){
        laz[k]+=w; s[k]+=(r-l+1)*w; return;
    }
    if(laz[k]) down(k,r-l+1);
    ll mid=l+r>>1,ans=0;
    if(mid>=ql) update(k<<1,l,mid,ql,qr,w);
    if(mid<qr) update(k<<1|1,mid+1,r,ql,qr,w);
    up(k);
}

7.树链剖分

(线段树维护树的直径)
dfs1一次遍历得到:
1. 每 个 结 点 的 父 亲 结 点 f ; 2. 深 度 d ; 3. 子 树 大 小 s z ; 4. 重 儿 子 s o n 1.每个结点的父亲结点f;2.深度d;3.子树大小sz;4.重儿子son 1.f;2.d;3.sz;4.son
dfs2一次遍历得到:
1. 每 个 结 点 所 在 链 的 顶 端 t o p ; 2. 每 个 结 点 的 d f s 序 i d ; 3. 每 个 d f s 序 代 表 的 结 点 r k ; 1.每个结点所在链的顶端top;2.每个结点的dfs序id;3.每个dfs序代表的结点rk; 1.top;2.dfsid;3.dfsrk;
注意优先进入重儿子,保证重链所在区间的连续性,跑完剩下的就是其他链
核心在于用top加速以及用dfs序把同一条链的节点转换在连续区间供线段树操作。
点权可以直接操作,边权的话因为每个节点只有一个父节点,所以借此操作把 u − > v u->v u>v的权值转换成 v v v的权值,那么 u 到 v 的路径权值和就是 u 到 v 路径上的第二个点到 v 的权值和。

vector<pair<ll,ll> >g[manx];
ll n,m,cnt;
ll d[manx],f[manx],sz[manx],son[manx],dp[manx];
ll top[manx];//,id[manx],rk[manx];
struct node{
    ll l,r,w;
}a[manx<<2];
inline void dfs1(ll u,ll pre){
    d[u]=d[pre]+1,f[u]=pre,sz[u]=1;
    for(auto pi: g[u]){
        ll v=pi.fi,w=pi.se;
        if(v==pre) continue;
        dp[v]=dp[u]+w;
        dfs1(v,u);
        sz[u]+=sz[v];
        if(sz[son[u]]<sz[v]) son[u]=v;
    }
}
inline void dfs2(ll u,ll tp){
    top[u]=tp;//,id[u]=++cnt,rk[cnt]=u;
    if(!son[u]) return;
    dfs2(son[u],tp);
    for(auto pi: g[u]){
        ll v=pi.fi;
        if(v!=son[u]&&v!=f[u]) dfs2(v,v);
    }
}
inline ll lca(ll x,ll y){
    while(top[x]!=top[y]){
        if(d[top[x]]<d[top[y]]) swap(x,y);
        x=f[top[x]];
    }
    return d[x]<d[y]?x:y;
}
inline ll dis(ll x,ll y){
    return dp[x]+dp[y]-2*dp[lca(x,y)];
}
inline node compare(node a,node b){
    return dis(a.l,a.r)>dis(b.l,b.r)?a:b;
}
inline node up(node a,node b){
    node ans1=compare((node){a.l,b.l,0},(node){a.r,b.l,0});
    node ans2=compare((node){a.l,b.r,0},(node){a.r,b.r,0});
    node ans =compare(ans1,ans2);
    ans.w=dis(ans.l,ans.r);
    if(a.w>ans.w) ans=a;
    if(b.w>ans.w) ans=b;
    return ans;
}
inline void build(ll k,ll l,ll r){
    if(l==r){
        a[k].l=a[k].r=l; a[k].w=0; return;
    }
    ll mid=l+r>>1;
    build(k<<1,l,mid); build(k<<1|1,mid+1,r);
    a[k]=up(a[k<<1],a[k<<1|1]);
}
inline node query(ll k,ll l,ll r,ll L,ll R){
    if(l>=L&&r<=R) return a[k];
    ll mid=l+r>>1;
    ll ql=0,qr=0; node ans1,ans2;
    if(mid>=L) ans1=query(k<<1,l,mid,L,R),++ql;
    if(R>mid) ans2=query(k<<1|1,mid+1,r,L,R),++qr;
    return (ql&qr)?up(ans1,ans2):(ql?ans1:ans2);
}

8.块状链表

其算法复杂度n*(n^0.5),可以在很短的时间内实现快速的插入、删除和查找字符串。

#inlcude< ext / rope >
using namespace __gnu_cxx;
rope test;

test.push_back(x);//在末尾添加x

test.insert(pos,x);//在pos插入x  

test.erase(pos,x);//从pos开始删除x个

test.copy(pos,len,x);//从pos开始到pos+len为止用x代替

test.replace(pos,x);//从pos开始换成x

test.substr(pos,x);//提取pos开始x个

test.at(x)/[x];//访问第x个元素

3.数论

无他,唯手熟尔。

1.欧几里得

int gcd(int a,int b){
	return b==0?a:gcd(b,a%b);
}
int lcm(int a,int b){
	return a/gcd(a,b)*b;
}

2.扩展欧几里得

贝祖定理

即如果a、b是整数,那么一定存在整数x、y使得ax+by=gcd(a,b)。

特别注意的是:x = y1, y = x1 - a / b *y1

而求逆元仅限于求解线性同余 a * x mod b = 1 可转化成 ax + by = 1

ll exgcd(ll a,ll b,ll &x,ll &y){
    if(b==0){ x=1,y=0; return a;}
    ll gcd=exgcd(b,a%b,y,x);
    y-=a/b*x;
    return gcd;
}
ll inv(ll a,ll b,ll &x,ll &y){
    ll gcd=exgcd(a,b,x,y);
    if(gcd!=1) return -1;
    else return (x+b)%b;
}

3.特殊的数

1.卡特兰数

f [ n ] = C 2 n n n + 1 = C 2 n n − C 2 n n − 1 f[n]=\frac{C^{n}_{2n}}{n+1}={C^{n}_{2n}}-{C^{n-1}_{2n}} f[n]=n+1C2nn=C2nnC2nn1
前几项:1, 1, 2, 5, 14, 42, 132…
案例有:

  1. n n n边形的三角剖分方案数
  2. 栈进出序列数量 / 左右括号的合法方案数 / 01 01 01数列的填充
  3. n n n个节点的二叉树结构数量
  4. ( 1 , 1 ) 到 ( n , n ) (1,1)到(n,n) (1,1)(n,n),每次只能往右或往上走,求不超过 y = x y=x y=x这条直线的方案数.
  5. n n n个数的连乘积的乘法顺序
dp[0]=0,dp[1]=1;
for(int i=2;i<=n;i++)
    dp[i]=dp[i-1]*(4*i-2)/(i+1)

2.超级卡特兰数 / 大施罗德数

( n + 1 ) f n + 1 = ( 6 n − 3 ) f n − ( n − 2 ) f n − 1 (n+1)f_{n+1}=(6n-3)f_n-(n-2)f_{n-1} (n+1)fn+1=(6n3)fn(n2)fn1
大施罗德数的前几项为 1, 2, 6, 22, 90, 394, 1806, 8558, 41586, 206098…
超级卡特兰数数列前几项为1, 1, 3, 11, 45, 197, 903, 4279, 20793, 103049…
可以发现大施罗德数除了第一项其他项都是超级卡特兰数前一项的两倍。

案例有:

  1. n n n边形的任意剖分方案数.
  2. 左右括号+数字的合法方案数
  3. ( 1 , 1 ) 到 ( n , n ) (1,1)到(n,n) (1,1)(n,n),每次能往右,往上,往右上走,求不超过 y = x y=x y=x这条直线的方案数.
inv[0]=inv[1]=f[0]=f[1] = 1;
for(int i=2;i<=n;++i) inv[i]=1ll*(mod-mod/i)*inv[mod%i]%mod;
for(int i=2;i<=n-2;++i)
	f[i]=((6ll*i-3)*f[i-1]-(i-2ll)*f[i-2])%mod*inv[i+1]%mod

4.矩阵乘法

struct node{
    ll x[N][N];
    node(){ memset(x,0,sizeof(x));}
    inline void init(){ for(int i=0;i<=n;i++) x[i][i]=1; }
};
node operator *(const node &a, const node &b){
    node res;
    for(int k=0;k<=n;k++)
        for(int i=0;i<=n;i++)
            for(int j=0;j<=n;j++)
                res.x[i][j]=(res.x[i][j]+a.x[i][k]*b.x[k][j])%mod;
    return res;
}
node fast(node a, ll b){
    node ans; ans.init();
    while(b){
        if(b&1) ans=ans*a;
        a=a*a;
        b>>=1;
    }
    return ans;
}

5.卢卡斯定理

L u c a s ( n , m , p ) = C m % p n % p × L u c a s ( n / p , m / p , p ) Lucas(n,m,p)=C^{n\%p}_{m\%p}×Lucas(n/p,m/p,p)%p Lucas(n,m,p)=Cm%pn%p×Lucas(n/p,m/p,p)

L u c a s ( n , 0 , p ) = 1 Lucas(n,0,p)=1 Lucas(n,0,p)=1

处理组合数取模的问题。

ll Inv(ll x,ll p){return qp(x,p-2,p);}
ll Cal(ll n,ll m,ll p){
    if(m>n) return 0;
    ll ans=1;
    for(int i=1;i<=m;++i)ans=ans*Inv(i,p)%p*(n-i+1)%p;
    return ans%p;
}
ll Lucas(ll n,ll m,ll p){
    if(!m) return 1;
    return Cal(n%p,m%p,p)*Lucas(n/p,m/p,p)%p;
}

6.乘法逆元

假 设 有 p = k i + r : 取 模 意 义 下 : k i + r m o d p = = 0 乘 上 i − 1 以 及 r − 1 : i − 1 = − k ∗ r − 1 代 换 : i − 1 = ( − ( p / i ) ∗ ( p % i ) − 1 ) % p 有 : i − 1 = ( p − ( p / i ) ∗ ( p % i ) − 1 ) % p 其 中 i n v [ 1 ] = 1 假设有 p=ki+r:\\ 取模意义下: ki + r mod p == 0\\ 乘上i^{-1} 以及r^{-1} :i^{-1}= - k*r^{-1}\\ 代换: i^{-1} = ( - (p/i) * (p\%i)^{-1} ) \%p\\ 有: i^{-1} = (p-(p/i) *(p\%i)^{-1} )\%p\\ 其中inv[1]=1\\ p=ki+r:ki+rmodp==0i1r1i1=kr1i1=((p/i)(p%i)1)%pi1=(p(p/i)(p%i)1)%pinv[1]=1

inv[1]=1;
for(int i=2;i<=n;i++)
	inv[i]=((p-p/i)*inv[p%i])%p;

7.整除分块

void gets(ll n){
    ll ans=0;
    for(ll l=1,r;l<=n;l=r+1){
        r=n/(n/l);
        ans+=(r-l+1)*(n/l);
    }
    return ans;
}

8.欧拉函数

//直接求某个数的欧拉函数
ll euler(ll n){
    ll ans=n;
    for(ll i=2;i*i<=n;i++){
        if(n%i==0){
            ans-=ans/i;
            while(n%i==0) n/=i;
        }
    }
    if(n>1) ans-=ans/n;
    return ans;
}
//埃氏筛
ll phi[manx];
void euler(ll n){
    for(int i=1;i<=n;i++) phi[i]=i;
    for(int i=2;i<=n;i++){
        if(phi[i]==i){
            for(int j=i;j<=n;j+=i)
                phi[j]-=phi[j]/i;
        }
    }
}
//欧拉筛
ll vis[manx],phi[manx],prime[manx];
ll cnt;
void euler(ll n){
    phi[1]=1;
    for(int i=2;i<=n;i++){
        if(!vis[i]) prime[++cnt]=i,phi[i]=i-1;
        for(int j=1;j<=cnt&&i*prime[j]<=n;j++){
            vis[i*prime[j]]=1;
            if(i%prime[j]==0){
                phi[i*prime[j]]=phi[i]*prime[j];  //
                break; 
            }
            else phi[i*prime[j]]=phi[i]*phi[prime[j]];
        }
    }
}

9.线性筛

//欧拉筛
ll vis[manx],prime[manx];
ll cnt;
void euler(ll n){
    for(int i=2;i<=n;i++){
        if(!vis[i]) prime[++cnt]=i;
        for(int j=1;j<=cnt&&i*prime[j]<=n;j++){
            vis[i*prime[j]]=1;
            if(i%prime[j]==0) break; 
        }
    }
}

4.字符串

1.Hash

  1. 字符串长度,hash:记从1~i的hash值,pw:记录base^i
  2. query: 字符串[l,r]hash值,type为第几个hash
  3. same:判断字符串u的[lu,ru]内的字符串和字符串v的[lv,rv]内的字符串是否相同
struct Hash{
    int base[4],mod[4];
    int tot,hash[4][manx],pw[4][manx]; 
    Hash() {
        tot=0;
        for(int i=1;i<=3;i++) pw[i][0]=1;
        base[1]=233;base[2]=19260817;base[3]=20030714;
        mod[1]=1e9+7;mod[2]=1e9+9;mod[3]=998244353;
    }
    void init() { tot=0;}
    void insert(int c) {
        tot++;
        for(int i=1;i<=3;i++) hash[i][tot]=(1LL*hash[i][tot-1]*base[i]+c)%mod[i];
        for(int i=1;i<=3;i++) pw[i][tot]=(1LL*pw[i][tot-1]*base[i])%mod[i];
    }
    int query(int l,int r,int type) { 
        return (hash[type][r]-(1LL*hash[type][l-1]*pw[type][r-l+1]%mod[type])+mod[type])%mod[type];
    }
    friend bool same(Hash &u,int lu,int ru,Hash &v,int lv,int rv) { 
        if(ru-lu!=rv-lv) return false;
        for(int i=1;i<=3;i++) if(u.query(lu,ru,i)!=v.query(lv,rv,i)) return false;
        return true;
    }
}hash;

2.KMP

1.next数组

  1. next[0]= -1 / 0
  2. next[i]表示模式串的前i个字符的最长公共前后缀的长度
  3. 出现失配的时候,因为前 j 个字符有 next[j] 个最长公共前后缀,就说明现在这里失配了但是前面的有 next[j] 个字符是不用再次匹配的,所以模式串向右移动 j-next[j] 个单位,即 j=next[j] 。
  4. 值得一提的是:设len为n-next[n]
  5. 如果next[n]不为0且n是len的倍数,那么该串的最小循环节是n-next[n],且循环次数为n/len;
  6. 否则该串需添加 len - n%len 个字符才能形成循环串。(n%len在这里为串中后缀属于循环串的一部分)
//此模板对应字符串整体向右移动一位 
//即next[i]表示模式串的前i个字符的最长公共前后缀的长度
void getnexts(string s){
    ll i=0,j=-1,m=s.size();
    nexts[i]=j;
    while(i<m){
        if(j==-1||s[i]==s[j]) nexts[++i]=++j;
        else j=nexts[j];
    }
}

2.kmp匹配过程

ll kmp(string s,string p){
    getsnexts();
    ll i=0,j=0,n=s.size(),m=p.size();
    while(i<n&&j<m){
        if(j==-1||s[i]==p[j]) ++i,++j;
        else j=nexts[j];
    }
    if(j==m) return i-j;
    return -1;
}

3.Manacher

mx是回文串到达的最右端,id是mx对应的回文串中点,maxpoint是最长回文串的中点,maxlen是最长回文串的长度,p数组维护 i 处的回文串长度。

特别需要注意的是:当p[i]==i时说明回文串的起点就是串的起点,而当i+p[i]==len(字符串的长度)时说明回文串的终点就是串的终点。

string Manacher(string s1){
    string s="$#";
    for(int i=0;i<s1.size();i++)
        s+=s1[i],s+="#";
    vector<int>p(s.size(),0); 
    int id=0,mx=0,maxpoint=0,maxlen=0; 
    for(int i=1;i<s.size();i++){
        p[i]=mx>i+p[i]?min(mx-i,p[2*id-i]):1;  
        while(s[i+p[i]]==s[i-p[i]]) ++p[i];  
        if(i+p[i]>mx) id=i,mx=i+p[i]; 
        if(p[i]>maxlen) maxlen=p[i],maxpoint=i; 
    }
    return s1.substr( (maxpoint-maxlen)/2 , maxlen-1 );  
}

4.序列自动机

void gets(){
    int n=s.size();
    for(int i=0;i<26;i++) dp[n][i]=-1;
    for(int foi=n-1;i>=0;i--){
        for(int j=0;j<26;j++) dp[i][j]=dp[i+1][j];
        dp[i][s[i]-'a']=i;
    }
}
bool pd(string t){
    int pos=-1;
    for(int i=0;i<t.size();i++){
        pos=dp[pos+1][t[i]-'a'];
        if(pos==-1) return false;
    }
    return true;
}

5.回文自动机

/*
nexts存儿子节点,fail是失配指针
cnt记录以第i个字符结尾本质不同的串的不完全个数(count之后才是完全的)
num记录以第i个字符结尾的回文子串个数
len记录以第i个字符结尾的回文子串最大长度
s是加入的字符串 n是字符串大小 p是节点个数
*/
struct PAM{
    ll nexts[manx][26],fail[manx],cnt[manx],num[manx],len[manx],s[manx];
    ll last,n,p;
    int newnode(ll x){ //新建节点
        for(int i=1;i<26;i++) nexts[p][i]=0;
        cnt[p]=0,num[p]=0,len[p]=x;
        return p++;
    }
    void init(){ //初始化
        p=0; newnode(0); newnode(-1);
        last=n=0; fail[0]=1; s[n]=-1;//开头放一个字符集中没有的字符,减少特判
    }
    int get_fail(ll x){
        while(s[n-len[x]-1]!=s[n]) x=fail[x];
        return  x;
    }
    void add(ll c){
        c-='a'; s[++n]=c;
        int cur=get_fail(last);
        if(!nexts[cur][c]){
            ll now=newnode(len[cur]+2);
            fail[now]=nexts[get_fail(fail[cur])][c];
            nexts[cur][c]=now;
            num[now]=num[fail[now]]+1;
        }
        last=nexts[cur][c];
        cnt[last]++;
    }
    void count(){ for(int i=p-1;i>=0;i--) cnt[fail[i]]+=cnt[i]; }
    //父亲累加儿子的cnt,因为如果fail[v]=u,则u一定是v的子回文串!
}pam;

6.AC自动机

AC自动机常用于解决多字符串匹配 / 包含问题。
查询的方式可以有三种变化:

  1. 用一个 vis 数组打标记,暴力跳 fail ,但是时间复杂度可高达 O ( ∣ 文 本 串 ∣ ∗ ∣ 模 式 串 ∣ ) O(|文本串|*|模式串|) O
  2. 以 fail 数组连边,topo 后进行 dp 延迟累加。
  3. 以 fail 数组建 fail 树,树上差分。
vector<ll>g[manx];
ll sz[manx];
void dfs(ll u){
    for(auto v: g[u]){
        dfs(v);
        sz[u]+=sz[v];
    }
}
ll *query(string s){
	ll u=0,n=s.size(),ans=0;
	for(auto x: s){
		u=ac.trie[u][x-'a'];
		sz[u]++;
	}
	for(int i=0;i<=ac.cnt;i++)
        if(i!=ac.fail[i]) g[ac.fail[i]].pb(i);
    dfs(0);
    return sz;
}
struct AC{
    ll trie[manx][26],val[manx],fail[manx];
    ll cnt=0;
    void init(){
        memset(trie[0],0,sizeof(trie[0])); cnt=1;
    }
    void add(string s, ll w){
        ll u=0; ll n=s.size();
        for(int i=0;i<n;i++){
            ll v=s[i]-'a';
            if(!trie[u][v]){
                memset(trie[cnt],0,sizeof(trie[cnt]));
                trie[u][v]=cnt++;
                val[cnt-1]=0;
            }
            u=trie[u][v];
        }
        val[u]+=w;
    }
    ll query(string s){
        ll u=0,n=s.size(),ans=0;
        for(int i=0;i<n;i++){
            ll v=s[i]-'a',w=trie[u][v];
            // 跳fail累加 
            u=trie[u][v];
        }
        return ans;
    }
    void build(){
        queue<ll>q;
        for(int i=0;i<26;i++)
            if(trie[0][i]) q.push(trie[0][i]),fail[trie[0][i]]=0;
        while(q.size()){
            ll u=q.front(); q.pop();
            //失配累加和:val[u]+=val[fail[u]];
            for(int i=0;i<26;i++)
                if(trie[u][i]) fail[trie[u][i]]=trie[fail[u]][i],q.push(trie[u][i]); 
                // ,val[trie[u][i]]|=val[fail[trie[u][i]]];
                else trie[u][i]=trie[fail[u]][i];
        }
    }
}ac;

5.动态规划

区间dp

for(int len=1;len<=n;len++)
    for(int l=1;l+len-1<=n;l++){
        int r=l+len-1;
        for(int k=l;k<r;k++)
            dp[l][r]=max(dp[l][r],dp[l][k]+dp[k+1][r]+anything)
    }

6.图论

前置技能

前向星

ll head[N],d[N];
bool vis[N];
struct node{
    ll next,v,w;
}a[N];
ll n,m,s,e,k;
void add(ll u,ll v,ll w){
    a[++k].v=v; a[k].next=head[u]; a[k].w=w; head[u]=k;
}

1.最短路

1.spfa

可跑负权最短路,但不稳定,vis 数组是维护当前节点是否在队列中。

void spfa(){
	queue<ll>q;
	for(int i=1;i<=n;i++) d[i]=inf,vis[i]=0;
	q.push(s); vis[s]=1; d[s]=0;
	while(q.size()){
		ll u=q.front(); q.pop();
		vis[u]=0;
		for(int i=head[u];i;i=a[i].next){
			ll v=a[i].v,w=a[i].w;
			if(d[v]>d[u]+w){
				d[v]=d[u]+w;
				if(!vis[v]) q.push(v),vis[v]=1;
			}
		}
	}
}

2.dijkstra

较稳定, vis 数组是维护当前节点是否入队列过。

priority_queue<pair<ll,ll> >q;
void dij(){
    for(int i=1;i<=n;i++) d[i]=inf,vis[i]=0;
   	d[s]=0; q.push(mp(0,s));
    while(q.size()){
        ll u=q.top().se; q.pop();
        if(vis[u]) continue;
        vis[u]=1;
        for(int i=head[u];i;i=a[i].next){
            ll v=a[i].v,w=a[i].w;
            if(d[v]>d[u]+w){
                d[v]=d[u]+w;
                q.push(mp(-d[v],v));
            }
        }
    }
}

3.folyd

无法解决负权问题。

for(int k=1;k<=n;k++)
	for(int i=1;i<=n;i++)
        for(int j=1;j<=n;j++)
            d[i][j]=min(d[i][j],d[i][k]+d[k][j]);

2.最小生成树

ll f[manx];
struct node{
    ll u,v,w;
}a[manx];
ll find(ll x){
    return f[x]==x?x:f[x]=find(f[x]);
}
bool cmp(node x,node y){
    return x.w<y.w;
}
inline void kruskal(){
    for(int i=1;i<=m;i++){
        u=find(a[i].u),v=find(a[i].v);
        if(u==v) continue; 
        ans+=a[i].val;
        a[u]=v;
        total++;
        if(total==n-1) break;
    }
}

3.LCA

以下为倍增求法,树链剖分求法可参考本文数据结构7.树链剖分的lca函数。
lg数组为2^i,与其他人的数组代表含义可能略有不同。

vector<ll>g[manx];
ll lg[manx],d[manx],f[manx][22];
ll n,m,rt;
inline void init(){
    lg[0]=1;
    for(int i=1;i<=20;i++) lg[i]=lg[i-1]<<1;
}
void dfs(ll u,ll pre){
    d[u]=d[pre]+1,f[u][0]=pre;
    for(int i=1;lg[i]<=d[u];i++) f[u][i]=f[f[u][i-1]][i-1];
    for(auto v:g[u]){
        if(v==pre) continue;
        dfs(v,u);
    }
}
ll lca(ll x,ll y){
    if(d[x]<d[y]) swap(x,y);
    for(int i=20;i>=0;i--)
        if(d[x]-d[y]>=lg[i]) x=f[x][i];
    if(x==y) return x;
    for(int i=20;i>=0;i--)
        if(f[x][i]!=f[y][i]) x=f[x][i],y=f[y][i];
    return f[x][0];
}

4.树上启发式合并

/*
递归轻儿子
递归重儿子
update ans
del轻儿子贡献
保留重儿子贡献
时间复杂度O(nlogn)
*/
void cal(ll u,ll pre,ll f){
    if(f==1) calnow();
    else if(f==-1) del();
    for(auto v:g[u]){
        if(v==pre) continue;
        cal(v,u,f);
    }
}
void dfs2(ll u,ll pre,ll f){
    for(auto v:g[u]){
        if(v==pre||v==son[u]) continue;
        dfs2(v,u,1);
    }
    if(son[u]) dfs2(son[u],u,0);
    for(auto v:g[u]){
        if(v==pre||v==son[u]) continue;
        cal(v,u,1);
    }
    calnow();
    if(f) del();
}

5.Tarjan

1.强连通分量

ll head[manx],dfn[manx],low[manx],ans[manx];
ll out[manx],col[manx],s[manx],in[manx];
bool vis[manx];
ll k=0,ti=0,top=0,cnt=0,n,m;
struct node {
    ll v,next;
}e[mamx*2];
void add(ll u,ll v){
    e[++k].v=v;
    e[k].next=head[u];
    head[u]=k;
}
void tarjan(ll x){
    dfn[x]=low[x]=++ti,vis[x]=1,s[++top]=x;
    for(int i=head[x];i;i=e[i].next){
        ll u=e[i].v;
        if(!dfn[u]) tarjan(u),low[x]=min(low[x],low[u]);
        else if(vis[u]) low[x]=min(low[x],dfn[u]);
    }
    if(low[x]==dfn[x]){
        ++cnt;
        ll y;
        do{
            y=s[top--],vis[y]=0;
            col[y]=cnt;
            ans[cnt]++;
        }while(x!=y);
    }
}
void dosmall(){
    for(int i=1;i<=n;i++)
        if(!dfn[i])
            tarjan(i);
    for(int i=1;i<=n;i++)
        for(int j=head[i];j;j=e[j].next)
            if(col[e[j].v]!=col[i])
                ++out[col[i]],++in[col[e[j].v]];
}

2.割点

割点的条件有:
对于根节点:有2棵即以上的子树
对于非根节点:low[v]>=dfn[u]

ll head[manx],dfn[manx],low[manx];
bool vis[manx];
ll n,m,id,cnt,tot,k;
struct node {
    ll v,next;
}e[mamx*2];
void init(){
    memset(vis,0,sizeof(vis));
    memset(head,0,sizeof(head));
    memset(dfn,0,sizeof(dfn));
    memset(low,0,sizeof(low));
    id=cnt=tot=k=0;
}
void add(ll u,ll v){
    e[++k].v=v;
    e[k].next=head[u];
    head[u]=k;
}
void tarjan(ll u,ll f){
    dfn[u]=low[u]=++id;
    int child=0;
    for(int i=head[u];i;i=e[i].next){
        ll v=e[i].v;
        if(!dfn[v]){
            tarjan(v,u);
            low[u]=min(low[u],low[v]);
            if(low[v]>=dfn[u]&&u!=f) vis[u]=1;
            if(u==f) child++;
        }
        low[u]=min(low[u],dfn[v]);
    }
    if(child>=2&&u==f) vis[u]=1; //如果是根节点,并且有一个以上的子节点
}
int main()
{
    int n,u,v;
    char c;
    while(scanf("%d",&n),n){
        init();
        while(scanf("%d",&u),u){
            while(scanf("%d%c",&v,&c)){
                add(u,v),add(v,u);
                if(c=='\n') break;
            }
        }
        for(int i=1;i<=n;i++)
            if(!dfn[i])
                tarjan(i,i);
        for(int i=1;i<=n;i++)
            if(vis[i])
                tot++;
        cout<<tot<<endl;
    }
    return 0;
}

3.桥

ll low[manx],dfn[manx],head[manx];
ll k,cnt,top,id,bridge;
struct node{
    ll v,next,w;
}a[mamx];
void init(){
    memset(low,0,sizeof(low));
    memset(dfn,0,sizeof(dfn));
    memset(head,-1,sizeof(head));
    top=cnt=k=id=0;
    bridge=inf;
}
void add(ll u,ll v,ll w){
    a[k].next=head[u];
    a[k].v=v;
    a[k].w=w;
    head[u]=k++;
}
void tarjan(ll u,ll pre){
    low[u]=dfn[u]=++id;
    for(int i=head[u];i!=-1;i=a[i].next){
        ll v=a[i].v;
        if(i==(pre^1)) continue;
        if(!dfn[v]){
            tarjan(v,i),low[u]=min(low[u],low[v]);
            if(dfn[u]<low[v]) bridge=min(bridge,a[i].w);
        }
        else low[u]=min(low[u],dfn[v]);
    }
}
int main()
{
    ll n,m,u,v,w;
    while(scanf("%lld%lld",&n,&m),n+m){
        init();
        for(int i=1;i<=m;i++){
            u=read(),v=read(),w=read();
            add(u,v,w),add(v,u,w);
        }
        ll tarjans=0;
        for(int i=1;i<=n;i++)
            if(!dfn[i]) tarjan(i,-1),tarjans++;
        if(tarjans>1){
            printf("0\n");
            continue;
        }
        if(bridge==inf) bridge=-1;
        else if(bridge==0) bridge=1;
        printf("%lld\n",bridge);
    }
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值