CSP复习

一、tarjan

1.有向图缩点(强连通分量)

2.割点(根节点且有至少两个孩子 或 low[v]>=dfn[u])

if((fa==0&&son>1)||(fa!=0&&dfn[u]<=low[v]))cut[u]=true;

3.边双(两条边算一条,记一个vis标记一下有没有经过)

P2860 [USACO06JAN]冗余路径Redundant Paths

We Need More Bosses

求边双通常有两种方法:记住前驱节点,或是记住前驱边 。

注意重边问题,有些题只能记住前驱节点,有些题只能记住记住前驱边,不同的题不同对待。

记住前驱节点

for(int i=head[u];i;i=edge[i].next)
{
    int v=edge[i].too;
    if(v==fa)continue;
    if(!dfn[v]){tarjan(v,u);low[u]=min(low[u],low[v]);}
    else low[u]=min(low[u],dfn[v]);
}

记住前驱边

for(int i=head[u];i;i=edge[i].next)
if(!vis[i])
{
    vis[i]=vis[i^1]=1;
    int v=edge[i].too;
    if(!dfn[v]){tarjan(v);low[u]=min(low[u],low[v]);}
    else low[u]=min(low[u],dfn[v]);
}

4.点双(删掉任意一个节点仍是连通图)

P3225 HNOI2012]矿场搭建

P3469 [POI2008]BLO-Blockade

3331: [BeiJing2013]压力

void tarjan(int u)
{
    dfn[u]=low[u]=++deep;
    zhan[++tot]=u;
    for(int i=head[u];i;i=edge[i].next)
    {
        int v=edge[i].too;
        if(!dfn[v])
        {
            tarjan(v);
            low[u]=min(low[u],low[v]);
            if(low[v]>=dfn[u])
            {
                cnt++;
                while(zhan[tot]!=v)
                {
                    add2(cnt,zhan[tot]);
                    add2(zhan[tot],cnt);
                    tot--;
                }
                tot--;
                add2(cnt,v);
                add2(v,cnt);
                add2(cnt,u);
                add2(u,cnt);
            }
        }
        else low[u]=min(low[u],dfn[v]);
    }
}
void dfs(int u,int fa)
{
    if(u<=n)size[u]++;
    for(int i=head2[u];i;i=edge2[i].next)
    {
        int v=edge2[i].too;
        if(v==fa)continue;
        dfs(v,u);
        size[u]+=size[v];
    }
    if(u<=n)
    {
        f[u]=2*(n-1);
        for(int i=head2[u];i;i=edge2[i].next)
        {
            int v=edge2[i].too;
            if(v==fa)continue;
            f[u]+=size[v]*(n-1-size[v]);
        }
        f[u]+=(n-size[u])*(size[u]-1);
    }
}

二、2-sat

1.模板【模板】2-SAT 问题

0表示不选,1表示选

1.a=0 , a->a'

2.a=1 , a'->a

3.a=1那么b=1 , a->b b'->a'(隐藏b=0那么a=0)

4.a=0那么b=0 , a'->b' b->a(隐藏b=1那么a=1)

5.a=1那么b=0 , a->b' b->a'(隐藏b=1那么a=0)

6.a=0那么b=1 , a'->b b'->a(隐藏b=0那么a=1)

7.a=0并且b=0 a=0并且b=1 a=1并且b=0 a=1并且b=1 参考3-6,进行合并

8.a^b=0 a^b!=0 参考3-6,进行合并

如果col[a]==col[a'],那么无解

关于求一组解,由于我们知道tarjan的dfn序其实就是拓扑的反序,所以当col[a]<col[a']时,a=1。

void tarjan(int u)
{
    deep++;dfn[u]=low[u]=deep;
    vis[u]=true;zhan[++tot]=u;
    for(int i=head[u];i;i=edge[i].next)
    {
        int v=edge[i].too;
        if(!dfn[v]){tarjan(v);low[u]=min(low[u],low[v]);}
        else if(vis[v])low[u]=min(low[u],low[v]);
    }
    if(dfn[u]==low[u])
    {
        color++;vis[u]=false;col[u]=color;
        while(zhan[tot]!=u)
        {
            vis[zhan[tot]]=false;
            col[zhan[tot]]=color;
            tot--;
        }
        tot--;
    }
}
bool TWO_SAT()
{
    for(int i=1;i<=2*n;i++)
        if(!dfn[i])tarjan(i);
    for(int i=1;i<=n;i++)
        if(col[i]==col[i+n])return false;
    return true;
}
signed main()
{
    n=read();m=read();
    for(int i=1;i<=m;i++)
    {
        int a=read(),x=read(),b=read(),y=read();
        if(x==0)
        {
            if(y==0)//a=1那么b=0(b=1那么a=0) 
            {
                add(a,b+n);
                add(b,a+n);
            }
            else{//a=1那么b=1(b=0那么a=0) 
                add(a,b);
                add(b+n,a+n);
            }
        }
        else{
            if(y==0)//a=0那么b=0(b=1那么a=1) 
            {
                add(a+n,b+n);
                add(b,a);
            }
            else{//a=0那么b=1(b=0那么a=1) 
                add(a+n,b);
                add(b+n,a);
            }
        }
    }
    if(TWO_SAT())
    {
        puts("POSSIBLE");
        for(int i=1;i<=n;i++)
            printf("%d ",col[i]<col[i+n]);
    }
    else printf("IMPOSSIBLE");
    return 0;
}

2.前缀优化建图

CF1215F Radio Stations

区间,我们看成区间

如果区间那么这个物品一定不选,如果那么这个物品一定选

signed main()
{
    np=read();n=read();m=read();mp=read();
    for(int i=1;i<=np;i++)//x=0那么y=1,y=0那么x=1 
    {
        int x=read(),y=read();
        add(x+n,y);
        add(y+n,x);
    }
    for(int i=2;i<=m;i++)//x=0那么y=0,y=1那么x=1 
    {
        add(n+n+i+m,n+n+i-1+m);
        add(n+n+i-1,n+n+i);
    }
    for(int i=1;i<=n;i++)
    {
        int x=read(),y=read();
        if(x>1)
        {
            add(i,n+n+m+x-1);//x=1那么y=0,
            add(n+n+x-1,n+i);//y=1那么x=0,
        }
        add(i,n+n+y);//x=1那么y=1,
        add(n+n+m+y,n+i);//y=0那么x=0,
    }
    for(int i=1;i<=mp;i++)//x=1那么y=0,y=1那么x=0 
    {
        int x=read(),y=read();
        add(x,y+n);
        add(y,x+n);
    }
    for(int i=1;i<=n+n+m+m;i++)
        if(!dfn[i])tarjan(i);
    for(int i=1;i<=n;i++)
        if(col[i]==col[i+n]){puts("-1");return 0;}
    for(int i=1;i<=m;i++)
        if(col[n+n+i]==col[n+n+m+i]){puts("-1");return 0;}
    for(int i=1;i<=n;i++)
        if(col[i]<col[i+n])ans++;
    for(int i=1;i<=m;i++)
        if(col[n+n+i]<col[n+n+m+i])
        {
            printf("%d %d\n",ans,i);
            break;
        }
    for(int i=1;i<=n;i++)
        if(col[i]<col[i+n])printf("%d ",i);
}

三、Floyd

1.两点之间的距离

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

2.判负环

if(f[i][i]<0){puts("-1");return 0;}

3.DP

表示以前k个数做中转点,i到j的最短路。

按时间顺序排列,对于时间<=T的点都可以做中转点。

P1119 灾后重建

now=1;
for(int i=1;i<=m;i++)
{
    scanf("%d%d%d",&x,&y,&z);
    x++;y++;
    while(a[now]<=z&&now<=n)
    {
        for(int i=1;i<=n;i++)
            for(int j=1;j<=n;j++)
                g[i][j]=min(g[i][j],g[i][now]+g[j][now]); 
        now++;
    }
    if(a[x]>z||a[y]>z)cout<<-1<<endl;
    else {
        if(g[x][y]==1e8)cout<<-1<<endl;
        else cout<<g[x][y]<<endl;
    }
}

4.传递闭包

在可以走回头路的状压DP中需要用到传递闭包。

memset(f,0x3f,sizeof(f));
memset(dp,0x3f,sizeof(dp));
f[0][1]=0;
for(int i=1;i<(1<<n);i++)
{
    int gs=0;
    for(int j=0;j<n;j++)
        if(i&(1<<j))
        {
            gs++;
            id[gs]=j;    
        }
    for(int p=1;p<=gs;p++)
        for(int j=1;j<=gs;j++)
            A[j][p]=g[id[p]][id[j]];
    for(int j=1;j<=gs;j++)A[j][j]=0;
    for(int k=1;k<=gs;k++)
        for(int p=1;p<=gs;p++)
            for(int j=1;j<=gs;j++)
                A[p][j]=min(A[p][j],A[p][k]+A[k][j]);
    for(int p=1;p<=gs;p++)
        for(int j=1;j<=gs;j++)f[id[p]][i]=min(f[id[p]][i],f[id[j]][i]+A[j][p]);
    for(int j=0;j<n;j++)
        if((i&(1<<j))&&f[j][i]!=0x3f3f3f3f) 
        {
            for(int k=0;k<n;k++)
                if(((i&(1<<k))==0)&&g[j][k]!=0x3f3f3f3f)
                    f[k][i+(1<<k)]=min(f[k][i+(1<<k)],f[j][i]+g[j][k]);
        }
}

5.最小环

i和j两点加上中转点k,构成一个环

for(int i=1;i<=n;i++)
    f[i][i]=e[i][i]=0;
for(int k=1;k<=n;k++)
{
    for(int i=1;i<k;i++)
        for(int j=i+1;j<k;j++)
            ans=min(ans,f[i][j]+e[j][k]+e[k][i]);
    for(int i=1;i<=n;i++)
        for(int j=1;j<=n;j++)
            f[i][j]=min(f[i][j],f[i][k]+f[k][j]);
}

四、三分和随机(模拟退火)

三分和模拟退火都是解决极值类问题。

三分用来解决函数极值。

模拟退火还可以解决一些不是函数关系,往往要选出一些东西的题目。

#10013. 「一本通 1.2 例 3」曲线

1.三分

double l=0.0,r=n;
while(r-l>=1e-3)
{
    double mid1=l+(r-l)/3.0;
    double mid2=(r)-(r-l)/3.0;
    if(suan(mid1)<suan(mid2))r=mid2;
    else l=mid1;
}

2.模拟退火

精度有些问题,所以退火后再前后枚举一下

为了使答案更加准确,我们可以先随机若干次,这样的话跳动会更加优秀。

    for(int i=1;i<=3000;i++)
    {
        int X=rand()%n+1,Y=rand()%n+1;
        int ret=calc(X,Y);
        if(ret>ans)ans=ret,x=X,y=Y;
    }
void tui()
{
    t=1000;
    while(t>1e-12)
    {
        X=x+(rand()*2-RAND_MAX)*t*0.0001;
        if(X<0)X=0;if(X>1000)X=1000;
        now=suan(X);
        double anss=now-ans2;
        if(anss<0.0){ans1=X;ans2=now;x=X;}
        else if(exp(-anss/t)*RAND_MAX>rand())x=X;
        t=t*delta;
    }
}
void solve()
{
    ans1=0.0;ans2=suan(x);
    tui();
    double xu=ans1;
    double xjh=xu;
    double XJH=0.0;
    for(int i=1;i<=1000;i++)
    {    
        xjh+=0.0001;
        if(xjh>1000.0)break;
        XJH=suan(xjh);
        if(XJH<ans2)
        {
            ans1=xjh;
            ans2=XJH;
        }
    }
    xjh=xu;
    XJH=0.0;
    for(int i=1;i<=1000;i++)
    {    
        xjh-=0.0001;
        if(xjh<0.0)break;
        XJH=suan(xjh);
        if(XJH<ans2)
        {
            ans1=xjh;
            ans2=XJH;
        }
    }
}

3.多函数

P2571 SCOI2010]传送带

这题是双函数,三分求函数极值的话,需要三分套三分,而且细节多。

要找多个点时,模拟退火就是一个不错的选择。因为模拟退火基本上不用修改,而三分会写自闭。

三分套三分

double sanfen(double x,double y){
    double lx=cx,ly=cy,rx=dx,ry=dy;
    while(dis(lx,ly,rx,ry)>eps){
        double m1x=lx+(rx-lx)/3.0,m2x=rx-(rx-lx)/3.0,m1y=ly+(ry-ly)/3.0,m2y=ry-(ry-ly)/3.0;
        double ans1=dis(x,y,m1x,m1y)/r+dis(m1x,m1y,dx,dy)/q;
        double ans2=dis(x,y,m2x,m2y)/r+dis(m2x,m2y,dx,dy)/q;
        if(ans2-ans1>eps) rx=m2x,ry=m2y;
        else lx=m1x,ly=m1y;
    }
    return dis(x,y,lx,ly)/r+dis(lx,ly,dx,dy)/q;
}

double lx=ax,ly=ay,rx=bx,ry=by;
while(dis(lx,ly,rx,ry)>eps){
    double m1x=lx+(rx-lx)/3.0,m2x=rx-(rx-lx)/3.0,m1y=ly+(ry-ly)/3.0,m2y=ry-(ry-ly)/3.0;
    double ans1=dis(ax,ay,m1x,m1y)/p+sanfen(m1x,m1y);
    double ans2=dis(ax,ay,m2x,m2y)/p+sanfen(m2x,m2y);
    if(ans2-ans1>eps) rx=m2x,ry=m2y;
    else lx=m1x,ly=m1y;
}

模拟退火

void tui()
{
    t=2000;
    while(t>1e-14)
    {
        X=x+(rand()*2-RAND_MAX)*t;
        Y=y+(rand()*2-RAND_MAX)*t;
        if(X<0)X=0;if(X>dis(ax,bx,ay,by))X=dis(ax,bx,ay,by);
        if(Y<0)Y=0;if(Y>dis(cx,dx,cy,dy))Y=dis(cx,dx,cy,dy);
        now=suan(X,Y);
        anss=now-ans;
        if(anss<0){ans=now;x=X;y=Y;}
        else if(exp(-anss/t)*RAND_MAX>rand()){x=X;y=Y;}
        t=t*delta;
    }
}

4.和随机有点关系的题目

P2503 HAOI2006]均分数据

这题数据范围一看,发现会20!,但只要求最大那就可以随机。

随机的话两种思路,random_shuffle模拟退火

怎么模拟退火,每次交换两个数,在用DP求解即可。

inline void tui()
{
    nowans=ans,t=2000;
    while (t>1e-6)
    {
        int x=0,y=0;
        x=rand()%n+1;y=rand()%n+1;
        if(x==y) continue;
        swap(a[x],a[y]);
        now=DP();
        anss=now-ans;
        if (anss<0) ans=nowans=now;
        else if (exp(-anss/t)*RAND_MAX>rand()) nowans=now;
        else swap(a[x],a[y]);
        t*=delta;
    }  
}   

P3878 [TJOI2010]分金币

可以状压,当然模拟退火一样可以。

五、容斥

容斥的一般式子:

1.简单容斥

就是加加减减,还算简单。

2.和集合有关的容斥

有题了再补

3、minmax容斥

推导详见洛谷日报或GXZlegend

公式:

应用:

常用来求“每次选一个数,使每个数被选的次数至少到达某个值的期望次数”

首先我们要知道对于一个局面(比如第一个数选1次,第二个数选2次)的期望步数就是这个局面的概率分之一

证明:设期望为,概率为,那么,化简得

为啥是这样的呢?就是有的概率一遍就行,有的概率再来一次。

普遍情况:

个数,每个数有个权重,那么每一次选中第个数的概率就为,问每个数被选的次数至少到达的期望次数。

公式:

解释:

是得到一个指定集合内元素的期望时间,有重复元素的排列下,是对于集合内每个数的概率

例题:

hdu4336]Card Collector

裸题,但也有用到一个状压技巧,和飞翔的小鸟一样。

double ans=0;
for(int i=0;i<n;i++)scanf("%lf",&a[1<<i]);
for(int i=1;i<(1<<n);i++)
{
    f[i]=f[i-lowbit(i)]+a[lowbit(i)];
    gs[i]=gs[i-lowbit(i)]+1;
}
for(int i=1;i<(1<<n);i++)
    if(gs[i]&1)ans+=1.0/f[i];
else ans-=1.0/f[i];
printf("%lf\n",ans);

E - Gachapon

题意:

个数,每个数有个权重,那么每一次选中第个数的概率就为,问每个数被选的次数至少到达的期望次数。

题解:

暴力做法就是枚举集合,然后通过上面给出的式子进行计算。

正解,考虑已经求出了一个集合的答案,现在要在这个集合里加入一个新的数

发现只要记录一下即可转移

代码:

#include<bits/stdc++.h>
using namespace std;
#define next Next
#define gc getchar
#define int long long
const int N=405;
const int mod=998244353;
int n,a[N],b[N],jc[N],inv[N],sum[N],f[N][N][N];
/*char buf[1<<21],*p1=buf,*p2=buf;
inline int gc(){return p1==p2&&(p2=(p1=buf)+fread(buf,1,1<<21,stdin),p1==p2)?EOF:*p1++;}*/
inline int read()
{
    int ret=0,f=0;char c=gc();
    while(!isdigit(c)){if(c=='-')f=1;c=gc();}
    while(isdigit(c)){ret=ret*10+c-48;c=gc();}
    if(f)return -ret;return ret;
}
int kuai(int a,int b)
{
    int res=1;
    while(b)
    {
        if(b&1)res=res*a%mod;
        a=a*a%mod;
        b=b/2;
    }
    return res;
}
signed main()
{
    n=read();
    for(int i=1;i<=n;i++)a[i]=read(),b[i]=read();
    jc[0]=jc[1]=inv[0]=inv[1]=1;
    for(int i=2;i<=400;i++)jc[i]=jc[i-1]*i%mod;
    for(int i=2;i<=400;i++)inv[i]=(mod-mod/i)*inv[mod%i]%mod;
    for(int i=2;i<=400;i++)inv[i]=inv[i]*inv[i-1]%mod;
    int s1=0,s2=0;
    f[0][0][0]=mod-1;
    for(int i=1;i<=n;i++)
    {
        for(int j=0;j<=s1;j++)
            for(int k=0;k<=s2;k++)
                f[i][j][k]=f[i-1][j][k];
        sum[0]=1;
        for(int j=1;j<b[i];j++)sum[j]=sum[j-1]*a[i]%mod;
        for(int j=0;j<b[i];j++)sum[j]=sum[j]*inv[j]%mod;
        for(int j=0;j<=s1;j++)
            for(int k=0;k<=s2;k++)
                for(int p=0;p<b[i];p++)
                    f[i][j+p][k+a[i]]=(f[i][j+p][k+a[i]]-f[i-1][j][k]*sum[p]%mod+mod)%mod;
        s1+=b[i]-1;s2+=a[i];
    }
    int ans=0;
    for(int i=0;i<=s1;i++)
        for(int j=1;j<=s2;j++)
        {
            int x=f[n][i][j]*jc[i]%mod;
            x=x*kuai(kuai(j,mod-2),i)%mod;
            ans=(ans+x*s2%mod*kuai(j,mod-2)%mod)%mod;
        }
    cout<<ans;
}

六、DP

1.树形DP

背包P2014 选课

for(int i=head[u];i;i=edge[i].next)
{
    int v=edge[i].too;
    dfs(v);
    for(int j=m;j>=1;j--)
    {
        for(int k=0;k<j;k++)
        {
            f[u][j]=max(f[u][j],f[v][k]+f[u][j-k]);
        }
    }
}

最大独立子集旅游

void dfs(int u,int fa)
{
    f[u][1]=1;
    for(int i=0;i<g[u].size();i++)
    {
        int v=g[u][i];
        if(v==fa)continue;
        dfs(v,u);
        f[u][1]+=f[v][0];
        f[u][0]+=max(f[v][1],f[v][0]);
    }
}

2.状压DP

[USACO06NOV]玉米田Corn Fields

    for(int i=2;i<=n;i++)
    {
        for(int j=1;j<=cnt;j++)
        if((xu[j]&S[i-1])==xu[j])
            for(int k=1;k<=cnt;k++)
            if(b[j][k])                
                if((xu[k]&S[i])==xu[k])dp[i][k]=(dp[i][k]+dp[i-1][j])%mod;
    }

3.数位DP

[ZJOI2010]数字计数

ll dfs(int len,bool b,int sum,bool zero,int d)//b=0表示前面都是最大的 
{
    ll ans=0;
    if(len==0)return sum;
    if(f[len][b][sum][zero]!=-1)return f[len][b][sum][zero];
    for(int i=0;i<=9;i++)
    {
        if(b==false && i>num[len]) break;
        ans=ans+dfs(len-1,(b==true)||(i<num[len]),sum+(((zero!=1)||(i!=0))&&(i==d)),zero&&(i==0),d);
    }
    f[len][b][sum][zero]=ans;
    return ans;
}

4.概率期望DP

题目:给出一个n×m的矩阵区域,一个机器人初始在第x行第y列,每一步机器人会等概率的选择停在原地,左移一步,右移一步,下移一步,如果机器人在边界则不会往区域外移动,问机器人到达最后一行的期望步数。

题解:

这题很难啊,50分的要用期望+高斯消元。

我们先设为从(i,j)到最后一行的期望步数

f[i][j]=f[i][j]/4+f[i][j-1]/4+f[i][j+1]/4+f[i+1][j]/4+1   (1<j<m)
f[i][j]=f[i][j]/3+f[i+1][j]/3+f[i][j+1]/3+1    j=1
f[i][j]=f[i][j]/3+f[i+1][j]/3+f[i][j-1]/3+1    j=m

初始化是什么?

f[n][i]=0

两维的数组不好,我们可以压掉第一维(比较好高斯消元)。

然后,我们化一下式子:

f[i]=f[i]/3+f[i+1]/3+last[i]/3+1
2/3 * f[i]=f[i+1]/3+last[i]/3+1
f[i]=f[i+1]/2+last[i]/2 + 3/2
f[i]-f[i+1]/2=last[i]/2 + 3/2         (i=1)
f[i]=f[i]/3+f[i-1]/3+last[i]/3+1
2/3 * f[i]=f[i-1]/3+last[i]/3+1
f[i]=f[i-1]/2+last[i]/2 + 3/2
f[i]-f[i-1]/2=last[i]/2 + 3/2         (i=m)
f[i]=f[i]/4+f[i-1]/4+f[i+1]/4+last[i]/4+1
3/4 * f[i]=f[i-1]/4+f[i+1]/4+last[i]/4+1
f[i]=f[i-1]/3+f[i+1]/3+last[i]/3 + 4/3
f[i]-f[i-1]/3-f[i+1]/3=last[i]/3 + 4/3         (1<i<m)

这不就是高斯消元吗,对每一行做一遍高斯消元,把答案记录在last数组里。

我们发现高斯矩阵的系数很小,不需要n^3枚举,只要手动解就好。

5.子集DP

详见我牛客题解SOS-DP(子集DP)

for(int i=0;i<n;i++)
    for(int j=0;j<(1<<n);j++)
        if(j&(1<<i))F[j]+=F[j^(1<<i)];

6.换根DP

CF990G GCD Counting

for(int i=head[u];i;i=edge[i].next)
    {
        int v=edge[i].too;
        if(v==fa)continue;
        for(int j=0;j<A[v].size();j++)
        {
            int x=b[v][j],y=A[v][j];
            x=gcd(x,a[u]);
            int z=lower_bound(b[u].begin(),b[u].end(),x)-b[u].begin();
            A[u][z]-=y;
        }
        for(int j=0;j<A[u].size();j++)
        {
            int x=b[u][j],y=A[u][j];
            x=gcd(x,a[v]);
            int z=lower_bound(b[v].begin(),b[v].end(),x)-b[v].begin();
            A[v][z]+=y;
        }
        dp(v,u);
        A[u]=B[u];    
    }

7.单调队列优化DP

跳房子

for(int i=1;i<=n;i++)
{
    while(x[j]+L<=x[i])
    {
        if(f[j]>-1e8)
        {
            while(l<=r&&f[j]>=f[q[r]])r--;
            q[++r]=j;
        }
        j++;
    }
    while(x[q[l]]+R<x[i])l++;
    if(l<=r)f[i]=f[q[l]]+s[i]; 
}

8.数据结构优化DP

题目:定义一个长度为奇数的区间的值为其所包含的的元素的中位数。

现给出n个数的序列a,求将所有长度为奇数的区间的值排序后,第K大的值为多少。

题解:

DP转移时要求一个区间的和,可以用树状数组维护一下。

9.矩阵优化DP

[SNOI2017]礼物

node operator * (node a,node b)
{
    for(int i=1;i<=k+2;i++)
        for(int j=1;j<=k+2;j++)xu.m[i][j]=0;
    for(int i=1;i<=k+2;i++)
        for(int j=1;j<=k+2;j++)
            for(int p=1;p<=k+2;p++)
                xu.m[i][j]=(xu.m[i][j]+a.m[i][p]*b.m[p][j]%mod)%mod;
    return xu;
}

10.斜率优化DP

写出方程;
整理式子,将只和j有关的当成y,将i,j都有关的当成kx,将只和i有关的当成b
显然,如果想让F[i]最小,那就是b最小,对于图像上的若干点,选择最下面的一个就是最优的j。

P2365 任务安排

f[0]=0;
int l=1,r=1;
q[1]=0;
for(int i=1;i<=n;i++)
{
    while(l<r&&(f[q[l+1]]-f[q[l]])<=(s+sumt[i])*(sumc[q[l+1]]-sumc[q[l]]))l++;
    f[i]=f[q[l]]+sumt[i]*(sumc[i]-sumc[q[l]])+s*(sumc[n]-sumc[q[l]]);
    while(l<r&&(f[q[r]]-f[q[r-1]])*(sumc[i]-sumc[q[r]])
          >=(f[i]-f[q[r]])*(sumc[q[r]]-sumc[q[r-1]]))r--;
    q[++r]=i;
}

如果斜率k不单调递增,那么我们就不能随意删点了,查询我们也需要二分找到那个左边斜率比k小,右边比k大的点。

int find(int k)
{
    if(l==r)return q[l];
    int L=l,R=r;
    while(L<R)
    {
        int mid=(L+R)/2;
        if(f[q[mid+1]]-f[q[mid]]<=k*(sumc[q[mid+1]]-sumc[q[mid]]))L=mid+1;
        else R=mid;
    }
    return q[L];
}

如果斜率k和x不单调递增,我们就要用CDQ维护了。

其实也不是CDQ分治,其实就是简单的分治。

#2483. 「CEOI2017」Building Bridges

由于x不单调,就是要动态插入点,平衡树?不存在的。
我们排序就好了,按x排序。
但这样就会出现,原先j更新i,现在j在i的后面。
不过没事,我们可以分治,就是按原序列分治。
因为我们可以用左一半的凸壳来更新右一半。
void solve(int L,int R)
{
    if(L==R){Y[L]+=f[L];return;}
    int mid=(L+R)/2;
    solve(L,mid);
    for(int i=L;i<=R;i++)p[i]=i;
    sort(p+L,p+R+1,cmp);
    l=1,r=0;
    for(int i=L;i<=R;i++)
        if(p[i]<=mid)
        {
            while(l<r&&suan(q[r-1],q[r])>=suan(q[r],p[i]))r--;
            q[++r]=p[i];
        }
    for(int i=L;i<=R;i++)
        if(p[i]>mid)
        {
            while(l<r&&suan(q[l],q[l+1])<=2*h[p[i]])l++;
            f[p[i]]=min(f[p[i]],f[q[l]]+(h[p[i]]-h[q[l]])*(h[p[i]]-h[q[l]])+w[p[i]-1]-w[q[l]]);
        }
    solve(mid+1,R);
}

11.决策单调性优化DP

简单来说就是转移方程有函数性。往往和wps二分一起。

有两种解决方法,二分栈分治

[POI2011]Lightning Conductor

对于每个,把看成关于的函数。我们要做的就是在所有的函数中找到最值。

img

sqrt的增速是递减的,因此可能存在一个j比较小的函数,在某一时刻被j比较大的函数反超。我们大概需要维护这样的若干个函数:

img

二分栈:

int erfen(int x,int y)
{
    int l=y,r=k[x]?k[x]:n,mid,ret=r+1;
    while(l<=r)
    {
        mid=(l+r)/2;
        if(suan(mid,x)<=suan(mid,y)){ret=mid,r=mid-1;}
        else l=mid+1;
    }
    return ret;
}
void work()
{
    int l=1,r=0;
    for(int i=1;i<=n;i++)
    {
        while(l<r&&suan(k[r-1],q[r])<suan(k[r-1],i))r--;
        k[r]=erfen(q[r],i);
        q[++r]=i;
        while(l<r&&k[l]<=i)l++;
        p[i]=max(p[i],suan(i,q[l]));
    }
}

分治:

因为f[i]的最优决策点g[i]是单调递增的
solve(l,r,L,R)表示f[l]~f[r]的最优决策点在L~R
令mid=(l+r)/2
O(n)扫一遍取到k[mid]
继续分治
solve(l,mid-1,L,k[mid])
solve (mid+1,r,k[mid],R)
void solve1(int l,int r,int L,int R)
{
    if(l>r)return;
    int mid=(l+r)/2,g=0;
    double tmp=0.0;
    f1[mid]=a[mid];
    for(int i=L;i<=min(R,mid);i++)
    {
        tmp=a[i]+sqrt(double(mid-i));
        if(tmp>f1[mid])
        {
            f1[mid]=tmp;
            g=i;
        }
    }
    if(g==0)g=mid;
    f1[mid]-=a[mid];
    solve1(l,mid-1,L,g); 
    solve1(mid+1,r,g,R);
}

看另一种方程:

往往有函数性。

可以是前缀和,

所以就好像是一些斜向上的函数线(增速递增,由于对于一个固定的,每大1,函数值就大很多)

Ciel and Gondolas

inline int suan(int x,int y)
{
    int l=y+1,r=n,res=n+1;
    while(l<=r)
    {
        int mid=(l+r)/2;
        if(dp[x][k-1]+gao(x,mid)>=dp[y][k-1]+gao(y,mid))
        {
            res=mid;
            r=mid-1;
        }
        else l=mid+1;
    }
    return res;
}
for(k=1;k<=K;k++)
{
    int L=1,R=1;q[L]=0;
    for(int i=1,j;i<=n;i++)
    {
        while(L<R&&suan(q[L],q[L+1])<=i)L++;
        j=q[L];
        dp[i][k]=dp[j][k-1]+gao(j,i);
        while(L<R&&suan(q[R-1],q[R])>=suan(q[R],i))R--;
        q[++R]=i;
    }
}

12.插头DP(轮廓线DP)

P5056 【模板】插头dp

由于要维护连通性,所以不能用0/1来表示一个位置是否有插头,我们要用0(没有),1(起点插头),2(终点插头),来表示。

1.这个点不能填。

2.没有左,上插头,填成右下插头,下是起点,右是终点。

3.有上没左,向下延续或向右拐弯,插头种类不变。

4.有左没上,向右延续或向下拐弯,插头种类不变。

5.左终点上起点,直接相连。

6.左起点上起点,相连并将向右的和上匹配的终点插头改成起点插头。

7.左终点上终点,相连并将向左的和左匹配的起点插头改成终点插头。

8.左起点上终点,只有可能是最后一个格子,判一下即可。

void solve()
{
    add(0,1);
    for(int i=1;i<=n;i++)
    {
        for(int j=1;j<=m;j++)
        {
            last=now;
            now^=1;
            cnt[now]=0;
            memset(head,0,sizeof(head));
            for(int k=1;k<=cnt[last];k++)
            {
                node st=unpack(s[k].st[last]);
                int num=s[k].num[last];
                int zuo=st.s[0],shang=st.s[j];
                if(!mp[i][j])
                {
                    if((!zuo)&&(!shang))add(make(st),num);//不填 
                    goto xjh;
                }
                if((!zuo)&&(!shang))//左和上都没,那么一定填右和下 
                {
                    if(mp[i+1][j]&&mp[i][j+1])
                    {
                        st.s[0]=2;
                        st.s[j]=1;
                        add(make(st),num);
                    }
                    goto xjh;
                }
                if((!zuo)&&shang)//上有左没有 
                {
                    if(mp[i+1][j])add(make(st),num);//往下延续 
                    if(mp[i][j+1])//向右转弯 
                    {
                        st.s[0]=shang;
                        st.s[j]=0;
                        add(make(st),num);
                    }
                    goto xjh;
                }
                if(zuo&&(!shang))//左有上没有 
                {
                    if(mp[i][j+1])add(make(st),num);//往右延续 
                    if(mp[i+1][j])//向下转弯 
                    {
                        st.s[0]=0;
                        st.s[j]=zuo;
                        add(make(st),num);
                    }
                    goto xjh;
                }
                if(zuo==2&&shang==1)//左终点上起点,直接相连,相当于去掉这两个点 
                {
                    st.s[0]=st.s[j]=0;
                    add(make(st),num);
                    goto xjh;
                }
                if(zuo==1&&shang==1)//左起点上起点,找到右边的终点,让它变成起点,去掉这两个点  
                {
                    int gs=1,pos;
                    for(pos=j+1;pos<=m;pos++)
                    {
                        if(st.s[pos]==1)gs++;
                        else if(st.s[pos]==2)gs--;
                        if(!gs)break;
                    }
                    st.s[pos]=1;
                    st.s[0]=st.s[j]=0;
                    add(make(st),num);
                    goto xjh;
                }
                if(zuo==2&&shang==2)//左终点上终点,找到左边的起点,让它变成终点,去掉这两个点  
                {
                    int gs=-1,pos;
                    for(pos=j-1;pos;pos--)
                    {
                        if(st.s[pos]==1)gs++;
                        else if(st.s[pos]==2)gs--;
                        if(!gs)break;
                    }
                    st.s[pos]=2;
                    st.s[0]=st.s[j]=0;
                    add(make(st),num);
                    goto xjh;
                }
                if(zuo==1&&shang==2)//只有一种情况成立,即是最后一个要填的格子 
                {
                    st.s[0]=st.s[j]=0;
                    bool flag=0;
                    for(int pos=1;pos<=m;pos++)
                    {
                        if(st.s[pos])
                        {
                            flag=1;
                            break;
                        }
                    }
                    if(!flag&&i==edx&&j==edy)
                        ans+=num;
                }
                xjh:;
            }
        }
    }
}

13.动态DP

【模板】动态 DP

树剖好理解,每个节点都只维护轻儿子的信息,重儿子可以用树剖维护。

写出状态转移后,把转移搞成矩阵的形式(好转移),每次修改就修改每条重链顶端的父亲。

讲一下流程,每个点记录这个点的轻儿子们的所有F[0/1] 然后树剖维护每个点的值,每个点真实的值就是每个点的
点权+所在重链上的其他点的点权 修改一个点,会对上面log个点的点权发生变化,一个个修改过去即可。 答案就
是根节点所在的重链的权值之和,相当于乘上[0,0]  
struct node{
    int g[2][2];
}c,val[N],tree[N*4];
node operator + (node a,node b)
{
    for(int i=0;i<2;i++)
        for(int j=0;j<2;j++)c.g[i][j]=0;
    for(int i=0;i<2;i++)
        for(int j=0;j<2;j++)
            for(int k=0;k<2;k++)c.g[i][j]=max(c.g[i][j],a.g[i][k]+b.g[k][j]);
    return c;
}
void dfs(int u,int fat)
{
    fa[u]=fat;size[u]=1;f[u][0]=0;f[u][1]=a[u];
    for(int i=head[u];i;i=edge[i].next)
    {
        int v=edge[i].too;
        if(v==fat)continue;
        dfs(v,u);
        size[u]+=size[v];
        if(size[v]>size[son[u]])son[u]=v;
        f[u][0]+=max(f[v][0],f[v][1]);
        f[u][1]+=f[v][0];
    }
}
void dfs2(int u,int fat)
{
    if(son[u])
    {
        rev[++seg[0]]=son[u];
        seg[son[u]]=seg[0];
        top[son[u]]=top[u];
        dfs2(son[u],u);
    }
    for(int i=head[u];i;i=edge[i].next)
    {
        int v=edge[i].too;
        if(!top[v])
        {
            rev[++seg[0]]=v;
            seg[v]=seg[0];
            top[v]=v;
            dfs2(v,u);
        }
    }
}
void build(int nod,int l,int r)
{
    if(l==r)
    {
        int u=rev[l],g0=0,g1=a[u];
        for(int i=head[u];i;i=edge[i].next)
        {
            int v=edge[i].too;
            if(v==fa[u]||v==son[u])continue;
            g0+=max(f[v][0],f[v][1]);
            g1+=f[v][0];
        }
        val[u].g[0][0]=val[u].g[0][1]=g0;
        val[u].g[1][0]=g1;
        tree[nod]=val[u];
        return;
    }
    int mid=(l+r)/2;
    build(nod*2,l,mid);
    build(nod*2+1,mid+1,r);
    tree[nod]=tree[nod*2]+tree[nod*2+1];
}
node query(int nod,int l,int r,int L,int R)
{
    if(l==L&&r==R)return tree[nod];
    int mid=(l+r)/2;
    if(R<=mid)return query(nod*2,l,mid,L,R);
    else if(L>mid)return query(nod*2+1,mid+1,r,L,R);
    else return query(nod*2,l,mid,L,mid)+query(nod*2+1,mid+1,r,mid+1,R);
}
node find(int x)
{
    return query(1,1,n,L[top[x]],R[top[x]]);
}
void change(int nod,int l,int r,int x)
{
    if(l==r)
    {
        tree[nod]=val[rev[l]];
        return;
    }
    int mid=(l+r)/2;
    if(x<=mid)change(nod*2,l,mid,x);
    else change(nod*2+1,mid+1,r,x);
    tree[nod]=tree[nod*2]+tree[nod*2+1];
}
void update(int x)
{
    while(x)
    {
        node od=find(x);
        change(1,1,n,seg[x]);
        node nw=find(x);
        int u=fa[top[x]];
        val[u].g[0][0]=val[u].g[0][0]-max(od.g[0][0],od.g[1][0])+max(nw.g[0][0],nw.g[1][0]);
        val[u].g[1][0]=val[u].g[1][0]-od.g[0][0]+nw.g[0][0];
        val[u].g[0][1]=val[u].g[0][0];
        x=u;
    }
}
signed main()
{
    n=read();m=read();
    for(int i=1;i<=n;i++)a[i]=read();
    for(int i=1;i<n;i++)
    {
        int x=read(),y=read();
        add(x,y);
        add(y,x);
    }
    dfs(1,0);
    top[1]=rev[1]=seg[0]=seg[1]=1;
    dfs2(1,0);
    for(int i=1;i<=n;i++)L[top[i]]=n;
    for(int i=1;i<=n;i++)
    {
        L[top[i]]=min(L[top[i]],seg[i]);
        R[top[i]]=max(R[top[i]],seg[i]);
    }
    build(1,1,n);
    while(m--)
    {
        int x=read(),y=read();
        val[x].g[1][0]=val[x].g[1][0]-a[x]+y;
        a[x]=y;
        update(x);
        node u=find(1);
        printf("%d\n",max(u.g[0][0],u.g[1][0]));
    }
    return 0;
}

7.数据结构

1.树状数组

树状数组二分:二进制一位一位考虑过去,不需要真的一半。

2.线段树

线段树上二分:先判一边,行就去,不行,去另一边。

权值线段树:下标为权值

动态开点线段树:权值线段树的一种,有些权值(太大)没出现就用动态开点

李超线段树:

对整个横坐标建线段树,每个节点的值存的是一条直线的编号。

要求这条直线在横坐标为mid的时候纵坐标最大。

插入:

如果是叶子节点,我们直接保留最大的。

先比较斜率,再比较横坐标为mid时的纵坐标。

如果新线段的斜率大,并且横坐标为mid时的纵坐标大,那么把原线段下放左区间,节点值改为x

如果新线段的斜率大,但是横坐标为mid时的纵坐标小,那么把新线段下放右区间,不更改节点值

如果新线段的斜率小,但是横坐标为mid时的纵坐标大,那么把原线段下放右区间,节点值改为x

如果新线段的斜率小,并且横坐标为mid时的纵坐标小,那么把新线段下放左区间,不更改节点值

double f(int w,int x)
{
    return k[w]*(x-1)+b[w];
}
void change(int nod,int l,int r,int x)
{
    if(l==r)
    { 
        if(f(x,l)>f(tree[nod],l))tree[nod]=x;
        return;
    }
    int mid=(l+r)/2;
    if(k[tree[nod]]<k[x])
    {
        if(f(x,mid)>f(tree[nod],mid))
        {
            change(nod*2,l,mid,tree[nod]);
            tree[nod]=x;
        }
        else change(nod*2+1,mid+1,r,x);
    }
    if(k[tree[nod]]>k[x])
    {
        if(f(x,mid)>f(tree[nod],mid))
        {
            change(nod*2+1,mid+1,r,tree[nod]);
            tree[nod]=x ;
        }
        else change(nod*2,l,mid,x) ;
    }
}

查询:

如果是叶子节点直接返回横坐标的值。

如果查询区间在左区间,返回当前的节点的横坐标的值 和 左区间的值 的最大值

如果查询区间在右区间,返回当前的节点的横坐标的值 和 右区间的值 的最大值

double query(int nod,int l,int r,int x)
{
    if(l==r)return f(tree[nod],x); 
    int mid=(l+r)/2;
    if(x<=mid)return max(f(tree[nod],x),query(nod*2,l,mid,x));
    else return max(f(tree[nod],x),query(nod*2+1,mid+1,r,x));
}

P4254 [JSOI2008]Blue Mary开公司(直线)

P4097 [HEOI2013]Segment(线段)

3.字典树

可持久化字典树:

4.平衡树

普通平衡树:P3369 【模板】普通平衡树

bool chk(int x)
{
    return ch[fa[x]][1]==x;
}
void Zhuan(int x)
{
    int y=fa[x],z=fa[y],k=chk(x),w=ch[x][k^1];
    ch[y][k]=w;fa[w]=y;
    ch[z][chk(y)]=x;fa[x]=z;
    ch[x][k^1]=y;fa[y]=x;
    pushup(y);pushup(x);
}
void splay(int x,int gen=0)
{
    while(fa[x]!=gen)
    {
        int y=fa[x],z=fa[y];
        if(z!=gen)
        {
            if(chk(x)==chk(y))Zhuan(y);
            else Zhuan(x);
        }
        Zhuan(x);
    }
    if(!gen)root=x;
}

文艺平衡树:就是把下标当成值,注意要多插入0和n+1。维护的值和平衡树的值没有任何关系。

https://www.luogu.org/record/17135871 用文艺平衡树代替线段树。

维护区间加,对于,我们把l-1转成根,把r+1转成根的右儿子,现在的区间就在根的右儿子的左儿子。

ETT:树,支持将一个节点换一个父亲,子树加,求一个点到根路径的和。

考虑欧拉序,换父亲就是一段括号集体换个位置,我们可以用文艺平衡树来实现。

展示没用自己的写法实现过,先贴别人的吧。

void pushup(int x)
{
    sum[x]=sum[ch[x][0]]+sum[ch[x][1]]+val[x];
    size[x]=size[ch[x][0]]+size[ch[x][1]]+mark[x];
}
void pushdown(int x){
    if(x&&fa[x])pushdown(fa[x]);
    if(!x||!lazy[x])return ;
    lazy[ch[x][0]]+=lazy[x],lazy[ch[x][1]]+=lazy[x];
    val[ch[x][0]]+=lazy[x]*mark[ch[x][0]],val[ch[x][1]]+=lazy[x]*mark[ch[x][1]];
    sum[ch[x][0]]+=(long long)size[ch[x][0]]*lazy[x],sum[ch[x][1]]+=(long long)size[ch[x][1]]*lazy[x];
    lazy[x]=0;
}
int chk(int x)
{
    return ch[fa[x]][1]==x;
}
void Zhuan(int x,int d)
{
    int y=ch[x][d^1];
    ch[x][d^1]=ch[y][d];
    if(ch[x][d^1])fa[ch[x][d^1]]=x;
    fa[y]=fa[x];
    if(fa[x])ch[fa[x]][chk(x)]=y;
    fa[x]=y;ch[y][d]=x;
    pushup(x),pushup(y);
}
void splay(int x,int gen=0)
{
    pushdown(x);
    while(fa[x]!=gen)
    {
        int y=fa[x],z=fa[y];
        if(z!=gen)
        {
            if(chk(y)==chk(x))Zhuan(z,chk(x)^1);
        }
        Zhuan(y,chk(x)^1);
    }
}
int findl(int x)
{
    splay(x);
    x=ch[x][0];
    while(ch[x][1])x=ch[x][1];
    return x;
}
int findr(int x)
{
    splay(x);
    x=ch[x][1];
    while(ch[x][0])x=ch[x][0];
    return x;
}
int build(int l,int r)
{
    if(l>r)return 0;
    int mid=(l+r)/2;
    if(l<mid)fa[ch[mid][0]=build(l,mid-1)]=mid;
    if(mid<r)fa[ch[mid][1]=build(mid+1,r)]=mid;
    val[mid]=(dfn[mid]>0)?a[dfn[mid]]:-a[-dfn[mid]];
    mark[mid]=(dfn[mid]>0)?1:-1;
    pushup(mid);
    return mid;
}
void dfs(int x)
{
    dfn[l[x]=++tot]=x;
    for(int i=head[x];i;i=e[i].next)
        dfs(e[i].to);
    dfn[r[x]=++tot]=-x;
}

5.树套树

三维偏序:

矩形修改:

6.kd-tree

个人感觉,kd-tree有两个作用,一个是和树套树一样的矩形修改(空间优秀),一个是求平面点对(感觉就是加剪枝的暴力)。

4520: [Cqoi2016]K远点对

求平面最第K远/近点对距离。

#include<bits/stdc++.h>
using namespace std;
#define int long long
#define mid (l+r)/2
const int N=1e6+5;
int n,k,cmpd,root;
struct node{
    int l,r,d[2],mx[2],mn[2];
}t[N];
struct node2{
    int dis;
}tmp;
bool operator < (node2 a,node2 b)
{
    return a.dis>b.dis;
}
priority_queue<node2>Q;
int sqr(int x)
{
    return x*x;
}
bool cmp(node a,node b)
{
    if (a.d[cmpd]!=b.d[cmpd])return a.d[cmpd]<b.d[cmpd];
    else return a.d[cmpd^1]<b.d[cmpd^1];
}
void pushup(int x)
{
    if(t[x].l)
        for(int i=0;i<2;i++)
        {
            t[x].mx[i]=max(t[x].mx[i],t[t[x].l].mx[i]);
            t[x].mn[i]=min(t[x].mn[i],t[t[x].l].mn[i]);
        }
    if(t[x].r)
        for(int i=0;i<2;i++)
        {
            t[x].mx[i]=max(t[x].mx[i],t[t[x].r].mx[i]);
            t[x].mn[i]=min(t[x].mn[i],t[t[x].r].mn[i]);
        }
}
int build(int l,int r,int D)
{
    cmpd=D;
    nth_element(t+l+1,t+mid,t+r+1,cmp);
    if(l<mid)t[mid].l=build(l,mid-1,D^1);
    if(r>mid)t[mid].r=build(mid+1,r,D^1);
    t[mid].mn[0]=t[mid].mx[0]=t[mid].d[0];
    t[mid].mn[1]=t[mid].mx[1]=t[mid].d[1];
    pushup(mid);
    return mid;
}
int dis(int x,int y)
{
    return max(sqr((t[x].d[0]-t[y].mn[0])),sqr(t[x].d[0]-t[y].mx[0]))+
           max(sqr(t[x].d[1]-t[y].mn[1]),sqr(t[x].d[1]-t[y].mx[1]));
}
int getdis(int x,int y)
{
    return sqr(t[x].d[0]-t[y].d[0])+sqr(t[x].d[1]-t[y].d[1]);
}
void query(int u)
{
    int dl=0,dr=0,dd=getdis(0,u);
    if(dd>Q.top().dis)
    {
        Q.pop();
        tmp.dis=dd;
        Q.push(tmp);
    }
    if(t[u].l)dl=dis(0,t[u].l);
    if(t[u].r)dr=dis(0,t[u].r);
    tmp=Q.top();
    if(dl>dr)
    {
        if(dl>tmp.dis)query(t[u].l);
        tmp=Q.top();
        if(dr>tmp.dis)query(t[u].r);
    }
    else{
        if(dr>tmp.dis)query(t[u].r);
        tmp=Q.top();
        if(dl>tmp.dis)query(t[u].l);
    }
}
signed main()
{
    scanf("%lld%lld",&n,&k);
    for(int i=1;i<=n;i++)scanf("%lld%lld",&t[i].d[0],&t[i].d[1]);
    root=build(1,n,0);
    tmp.dis=0;
    for(int i=1;i<=k*2;i++)Q.push(tmp);
    for(int i=1;i<=n;i++)
    {
        t[0].d[0]=t[i].d[0];
        t[0].d[1]=t[i].d[1];
        query(root);
    }
    printf("%lld",Q.top().dis);
    return 0;
}

P3710 方方方的数据结构

这是矩形修改(我还不会,抄了发孔爷的代码)

bool cmp(int a,int b)
{
    return val[a][K]<val[b][K];
}
void pushup(int u)
{
    add[u]=0;mul[u]=1;
    for(int i=0;i<=1;i++)
    {
        L[u][i]=min(val[u][i],min(L[ls][i],L[rs][i]));
        R[u][i]=max(val[u][i],max(R[ls][i],R[rs][i]));
    }
}
void build(int &u,int l,int r,int k)
{
    if(l>r)return;
    int mid=(l+r)/2;
    K=k;
    sort(t+l,t+r+1,cmp);
    u=t[mid];
    build(ls,l,mid-1,k^1);
    build(rs,mid+1,r,k^1);
    pushup(u);
}
void pushdown(int u)
{
    if(mul[u]!=1)
    {
        (sum[ls]*=mul[u])%=mod;
        (sum[rs]*=mul[u])%=mod;
        (mul[ls]*=mul[u])%=mod;
        (mul[rs]*=mul[u])%=mod;
        (add[ls]*=mul[u])%=mod;
        (add[rs]*=mul[u])%=mod;
        mul[u]=1;
    }
    if(add[u]!=0)
    {
        (sum[ls]+=add[u])%=mod;
        (sum[rs]+=add[u])%=mod;
        (add[ls]+=add[u])%=mod;
        (add[rs]+=add[u])%=mod;
        add[u]=0;
    }
}
bool xu,jia,hao;
void check(int u)
{
    xu=jia=hao=true;
    for(int i=0;i<=1;i++)
        if(x[i]>L[u][i]||R[u][i]>y[i])
        {
            xu=false; 
            if(x[i]>val[u][i]||val[u][i]>y[i])
            {
                jia=false;
                if(x[i]>R[u][i]||y[i]<L[u][i])hao=false;
            }
        }
}
void Add(int u)
{
    if(!u)return;
    check(u);
    if(!hao)return;
    if(jia)
    {
        if(opt==1)(sum[u]+=tmp)%=mod;
        else(sum[u]*=tmp)%=mod;
    }
    if(xu)
    {
        if(opt==1)(add[u]+=tmp)%=mod;
        else{
            (add[u]*=tmp)%=mod;
            (mul[u]*=tmp)%=mod;
        }
        return ;
    }
    pushdown(u);
    Add(ls);
    Add(rs);
}
void find(int u,int v)
{
    if(!u)return;
    if(val[v][0]<L[u][0]||val[v][0]>R[u][0]||val[v][1]<L[u][1]||val[v][1]>R[u][1])return;//不在这个区间 
    if(u==v)
    {
        printf("%lld\n",sum[u]);
        return ;
    }
    pushdown(u);
    find(ls,v);
    find(rs,v);
}

8.线性做法

1.单调队列

2.尺取法

3.单调栈

求最大全0/1矩阵面积

玉蟾宫

我们维护每个点向左,向右,向上最多能扩展到哪。

求面积的时候,我们只有在上面顶住的时候才算面积(很好理解的)

for(int i=1;i<=n;i++)
    for(int j=2;j<=m;j++)
        if(a[i][j]==1&&a[i][j-1]==1)l[i][j]=l[i][j-1];
for(int i=1;i<=n;i++)
    for(int j=m-1;j>0;j--)
        if(a[i][j]==1&&a[i][j+1]==1)r[i][j]=r[i][j+1];
for(int i=2;i<=n;i++)
    for(int j=1;j<=m;j++)
        if(a[i][j]==1&&a[i-1][j]==1)
        {
            h[i][j]=h[i-1][j]+1;
            l[i][j]=max(l[i][j],l[i-1][j]);
            r[i][j]=min(r[i][j],r[i-1][j]);
        }
for(int i=1;i<=n;i++)
    for(int j=1;j<=m;j++)
        if(a[i][j]==1)
            ans=max(ans,h[i][j]*(r[i][j]-l[i][j]+1));

P1578 奶牛浴场

根据障碍格来求的最大矩阵

维护每个点向右的矩阵,当然向左也要。

然后还有一种特殊情况就是,按y排序后相邻两个的矩形。

for(int i=1;i<=n;i++)a[i]=(node){read(),read()};
a[++n]=(node){0,0};
a[++n]=(node){0,w};
a[++n]=(node){l,0};
a[++n]=(node){l,w};
sort(a+1,a+n+1,cmp1);
for(int i=1;i<=n;i++)
{
    int U=w,D=0,L=a[i].x;
    for(int j=i+1;j<=n;j++)
    {
        ans=max(ans,(a[j].x-L)*(U-D));
        if(a[j].y>=a[i].y)U=min(U,a[j].y);
        else D=max(D,a[j].y);
    }
}
for(int i=n;i;i--)
{
    int U=w,D=0,R=a[i].x;
    for(int j=i-1;j;j--)
    {
        ans=max(ans,(R-a[j].x)*(U-D));
        if(a[j].y>=a[i].y)U=min(U,a[j].y);
        else D=max(D,a[j].y);
    }
}
sort(a+1,a+n+1,cmp2);
for(int i=1;i<n;i++)
    ans=max(ans,l*(a[i+1].y-a[i].y));

4.长链剖分

用于维护长度的题目。比如:求一个最大的连通块大小,联通块直径不超过

题目:

一颗个点的树,要给每个点染一个颜色,要求任意两个距离小于等于的点颜色不同,问最少的颜色数。

众所周知 树上距离小于等于k的点连成的图是一张弦图(所以可以直接用完美消除序列(bfs序)+点分树)

众所周知 弦图最小染色数等于最大团

于是问题变成,求一个连通块,直径不超过

表示的子树内,强制选,深度为的最大值

根据套路,考虑长链剖分。

我们把取前缀max,维护深度不超过的最大值。

因为取过前缀max,所以在可行的情况下,另一边长度越长越好,

也就是最长的位置要么就是卡到长度为,要么就是卡到那条链的底端,要么就是不能超过当前更新的长度

的最长深度为,三种情况分类:

1.可能会小于

2.,那么转移肯定从处转

3.,那么转移肯定从处转

发现中间部分相当于一个区间加,只需要打一个全局的即可

#include<bits/stdc++.h>
using namespace std;
#define next Next
#define gc getchar
#define int long long
const int N=1e6+5;
int n,m,ans,son[N],dis[N],tag[N];
int dp[N],*id=dp,*f[N];
vector<int>g[N];
//char buf[1<<21],*p1=buf,*p2=buf;
//inline int gc(){return p1==p2&&(p2=(p1=buf)+fread(buf,1,1<<21,stdin),p1==p2)?EOF:*p1++;}
inline int read()
{
    int ret=0,f=0;char c=gc();
    while(!isdigit(c)){if(c=='-')f=1;c=gc();}
    while(isdigit(c)){ret=ret*10+c-48;c=gc();}
    if(f)return-ret;return ret;
}
void dfs(int u,int fa)
{
    for(int i=0;i<g[u].size();i++)
    {
        int v=g[u][i];
        if(v==fa)continue;
        dfs(v,u);
        if(dis[v]+1>dis[u])
        {
            dis[u]=dis[v]+1;
            son[u]=v;
        }
    }
}
void solve(int u,int fa)
{
    f[u]=id++;
    if(son[u])
    {
        solve(son[u],u);
        tag[u]=tag[son[u]];
    }
    f[u][0]=-tag[u];
    tag[u]++;//多了点u 
    int len=min(dis[u],m);
    for(int j=0;j<g[u].size();j++)
    {
        int v=g[u][j];
        if(v==fa||v==son[u])continue;
        solve(v,u);
        int d=min(dis[v],m);
        for(int i=0;i<=d;i++)
            f[v][i]+=tag[v];
        tag[v]=0;
        for(int i=d;i>=0;i--)//注意k<=i 
        {
            int k=min(i,m-i);
            int x=f[u][i]+(k>0?f[v][k-1]:0);
            int y=f[u][k]+(i>0?f[v][i-1]:0);
            f[u][i]=max(x,y);
        }
        //x<=d,x可能会小于y+1
        int lim=max(d,m-d-1);
        tag[u]+=f[v][d];
        //x>d且x+d<=m,那么转移肯定从d处转 
        for(int i=0;i<=d;i++)f[u][i]-=f[v][d];
        for(int i=lim+1;i<=len;i++)f[u][i]-=f[v][d];
        for(int i=lim+1;i<=len;i++)
            if(m-i-1>=0)f[u][i]+=f[v][m-i-1];
        //x>d且x+d>m,那么转移肯定从m-x-1处转 
        for(int i=1;i<=d;i++)f[u][i]=max(f[u][i-1],f[u][i]);
        for(int i=lim+1;i<=len;i++)f[u][i]=max(f[u][i-1],f[u][i]);
        //为啥[d+1,m-d-1]不用取max呢
        //因为都是加了f[v][d],还是递增的 
    }
    ans=max(ans,f[u][len]+tag[u]);
}
signed main()
{
    freopen("rs.in","r",stdin);
    freopen("rs.out","w",stdout);
    n=read();m=read();
    for(int i=1;i<n;i++)
    {
        int x=read(),y=read();
        g[x].push_back(y);
        g[y].push_back(x);
    }
    dfs(1,0);
    solve(1,0);
    cout<<ans;
    return 0;
}

5.单调性

6.并查集

9.字符串

1.hash

2.kmp

求最长的前缀等于后缀。

void pre()
{
    int j=0;
    p[1]=0;
    for(int i=1;i<m;i++)
    {
        while(j>0&&b[j+1]!=b[i+1]) j=p[j];
        if(b[j+1]==b[i+1]) j++;
        p[i+1]=j;
    }
}
void kmp()
{
    int j=0;
    for(int i=0;i<n;i++)
    {
        while(j>0&&b[j+1]!=a[i+1]) j=p[j];
        if(b[j+1]==a[i+1]) j++;
        if(j==m)
        {
            printf("%d\n",i+1-m+1);
            j=p[j];
        }
    }
}

3.ac自动机

P3796 【模板】AC自动机(加强版)

多模式串匹配

void make(string s,int k)
{
    int u=1,len=s.size();
    for(int i=0;i<len;i++)
    {
        int c=s[i]-&#39;a&#39;;
        if(!ch[u][c])
        {
            ch[u][c]=++cnt;
            memset(ch[cnt],0,sizeof(ch[cnt])); 
        }
        u=ch[u][c];
    }
    bo[u]=k;
}
void bfs()
{
    int l=1,r=1;
    q[1]=1;nxt[1]=0;
    while(l<=r)
    {
        int u=q[l++];
        for(int i=0;i<26;i++)
        {
            if(!ch[u][i])ch[u][i]=ch[nxt[u]][i];
            else{
                q[++r]=ch[u][i];
                nxt[ch[u][i]]=ch[nxt[u]][i];
            }
        }
    }
}
void find(string s)
{
    int u=1,len=s.size(),c,k;
    for(int i=0;i<len;i++)
    {
        c=s[i]-&#39;a&#39;;
        k=ch[u][c];
        while(k>1)
        {
            ans[bo[k]]++;
            k=nxt[k];
        }
        u=ch[u][c];
    }
}

P5357 【模板】AC自动机(二次加强版)

有重复模式串的多重匹配 。

每次跳nxt,然后把结尾的所有串加一,妥妥的T。

转化一下建出一颗nxt树,链加,子树求和,直接树上差分。

void dfs(int u)
{
    for(int i=0;i<g[u].size();i++)
    {
        int v=g[u][i];
        dfs(v);
        size[u]+=size[v];
    }
}
signed main()
{
    n=read();
    for(int i=1;i<=n;i++)
    {
        scanf("%s",s);
        int m=strlen(s);
        int u=1;
        for(int j=0;j<m;j++)
        {
            int c=s[j]-&#39;a&#39;;
            if(!ch[u][c])ch[u][c]=++cnt;
            u=ch[u][c];
        }
        match[i]=u;
    }
    for(int i=0;i<26;i++)ch[0][i]=1;
    queue<int>q;
    q.push(1);
    while(!q.empty())
    {
        int u=q.front();q.pop();
        for(int i=0;i<26;i++)
        {
            if(ch[u][i])
            {
                nxt[ch[u][i]]=ch[nxt[u]][i];
                q.push(ch[u][i]);
            }
            else ch[u][i]=ch[nxt[u]][i];
        }
    }
    scanf("%s",s);
    int m=strlen(s);
    int u=1;
    for(int i=0;i<m;i++)
    {
        u=ch[u][s[i]-&#39;a&#39;];
        size[u]++;
    }
    for(int i=2;i<=cnt;i++)g[nxt[i]].push_back(i);
    dfs(1);
    for(int i=1;i<=n;i++)printf("%d\n",size[match[i]]);
    return 0;
}

4.sam(sa)

个人感觉sam虽然裸,但不好学,不是有深入了解的话建议别写。

sa就比较好懂了(扩展),往往要和线段树或主席树一起用。

void SA(int n,int m)
{
    int p=0;
    for(int i=1;i<=m;i++)c[i]=0;
    for(int i=1;i<=n;i++)c[x[i]=a[i]]++;
    for(int i=1;i<=m;i++)c[i]+=c[i-1];
    for(int i=n;i;i--)sa[c[x[i]]--]=i;
    for(int i=1;i;i=i*2)
    {
        p=0;
        for(int j=n-i+1;j<=n;j++)y[++p]=j;
        for(int j=1;j<=n;j++)
            if(sa[j]>i)y[++p]=sa[j]-i;
        for(int j=1;j<=m;j++)c[j]=0;
        for(int j=1;j<=n;j++)c[x[y[j]]]++;
        for(int j=1;j<=m;j++)c[j]+=c[j-1];
        for(int j=n;j;j--)sa[c[x[y[j]]]--]=y[j];
        swap(x,y);
        x[sa[1]]=1;
        p=2;
        for(int j=2;j<=n;j++)
            if(y[sa[j]]==y[sa[j-1]]&&y[sa[j]+i]==y[sa[j-1]+i])x[sa[j]]=p-1;
            else x[sa[j]]=p++;
        if(p>n)return;
        m=p;
    }
}
void Height()
{
    int k=0;
    for(int i=1;i<=n;i++)rk[sa[i]]=i;
    for(int i=1;i<=n;i++)
    {
        if(rk[i]==1)continue;
        if(k)k--;
        int j=sa[rk[i]-1];
        while(i+k<=n&&j+k<=n&&a[i+k]==a[j+k])k++;
        height[rk[i]]=k;
    }
}
void ST()
{
    memset(st,0x3f,sizeof(st));
    for(int i=1;i<=n;i++)st[i][0]=height[i];
    for(int i=1;i<=18;i++)
        for(int j=1;j<=n;j++)
            st[j][i]=min(st[j][i-1],st[j+(1<<(i-1))][i-1]);
}
int query(int l,int r)
{
    int L=l,R=r;
    l=min(rk[L],rk[R])+1;
    r=max(rk[L],rk[R]);
    int k=Log[r-l+1];
    return min(st[l][k],st[r-(1<<k)+1][k]);
}
signed main()
{
    scanf("%s",a+1);
    n=strlen(a+1);
    Log[0]=-1;
    for(int i=1;i<=n;i++)Log[i]=Log[i/2]+1;
    SA(n,233);
    Height();
    ST();
    m=read();
    while(m--)
    {
        int l=read(),r=read();
        printf("%d\n",query(l,r));
    }
    return 0;
}

10.数学

1.质数

2.gcd

P4549 【模板】裴蜀定理

关于的不定方程有整数解的充要条件是

scanf("%d",&n);
int ans;
scanf("%d",&ans);
for(int i=2;i<=n;i++)
{
    scanf("%d",&x);
    ans=__gcd(ans,x);
}
cout<<abs(ans);

3.exgcd

P1082 同余方程

可以推出

由于都可以除,所以最小的就是,最小同理

void exgcd(int a,int b,int &d,int &x,int &y)
{
    if(b==0){d=a;x=1;y=0;}
    else{exgcd(b,a%b,d,y,x);y-=x*(a/b);}
}

4.bsgs

把x换一下,

移项得:

int kuai(int a,int b)
{
    int res=1;
    while(b)
    {
        if(b%2==1)res=res*a%mod;
        a=a*a%mod;
        b=b/2;
    }
    return res;
}
int bsgs(int a,int b,int p)
{
    a%=p;b%=p;
    if(!a&&!b)return 1;
    if(!a||!b)return 0;
    int m=ceil(sqrt(p)),tmp=1;
    mp[b]=0;
    for(int i=1;i<=m;i++)
    {
        tmp=tmp*a%p;
        if(!mp[tmp*b%p])mp[tmp*b%p]=i;
    }
    int t=1;
    for(int i=1;i<=m;i++)
    {
        t=t*tmp%p;
        if(mp[t])
        {
            int ans=i*m-mp[t];
            return(ans%p+p)%p;
        }
    }
}

5.crt

【模板】扩展中国剩余定理(EXCRT)

参考题解:中国剩余定理 && 扩展中国剩余定理

注意:快速乘

假设已经求出前k-1个方程组成的同余方程组的一个解为x。

且有

则前k-1个方程的通解为

那么加入第k个方程后,我们就是要求一个正整数t,使得

转化一下式子

对于这个式子我们就可以通过扩欧求解t

若该同余式无解,则整个方程组无解

若有,则前k个同余式组成的方程组的一个解为

int kuai(int a,int b,int mod)
{
    if(b==0)return 0;
    if(b==1)return a;
    int x=kuai(a,b/2,mod);
    if(b%2==0)return(x+x)%mod;
    else return(x+x+a)%mod;
}
void exgcd(int a,int b,int &d,int &x,int &y)
{
    if(b==0){d=a;x=1;y=0;}
    else{exgcd(b,a%b,d,y,x);y-=x*(a/b);}
}
int excrt()
{
    int M=m[1],ans=a[1];
    for(int i=2;i<=n;i++)
    {
        int A=M,B=m[i],C=a[i]-ans;
        exgcd(A,B,d,X,Y);
        ans+=M*kuai(X,(C%B+B)%B/d,B/d);
        M=M/d*m[i];
        ans=(ans%M+M)%M;
    }
    return ans;
}

6.lucas

【模板】卢卡斯定理

注意:

int kuai(int a,int b)
{
    int res=1;
    while(b)
    {
        if(b%2==1)res=res*a%mod;
        a=a*a%mod;
        b=b/2;
    }
    return res;
}
int C(int n,int m)
{
    if(m>n)return 0;
    return jc[n]*inv[m]%mod*inv[n-m]%mod;
}
int lucas(int n,int m)
{
    if(m==0)return 1;
    return C(n%mod,m%mod)*lucas(n/mod,m/mod)%mod; 
}

7.二次剩余

其中p是一个奇质数

如果是p=2的话就把0,1带入求解即可。

有二次剩余的条件

题目:方程的解

while(T--)
{
    a=read(),b=read(),p=read();
    if(p<=2)
    {
        if(b%p==0||(a+b+1)%p==0)puts("Yes");
        else puts("No");
        continue;
    }
    if(a&1)a+=p;
    n=((a*a/4%p-b)%p+p)%p;
    if(n==0||kuai(n,(p-1)/2)==1)puts("Yes");
    else puts("No");
}

11.分治

1.普通分治(求方案数)

void solve(int l,int r)
{
    if(l==r)return;
    int mid=(l+r)/2;
    solve(l,mid);//左区间自己
    solve(mid+1,r);//右区间自己
    for(int i=l;i<=mid;i++)//枚举左边
        //往往是双指针或数据结构,时间复杂度1/2只log
}

区间异或和异或区间最大值异或区间最小值 (分治+双指针(线段树上二分)+可持久化trie)

void solve(int l,int r)
{
    if(l>r)return;
    if(l==r)
    {
        ans=max(a[l],ans);
        return;
    }
    int mid=(l+r)>>1;
    int suml=0,maxl=-INF,minl=INF,maxr=-INF,minr=INF,sumr=0;
    int pin1,pin2;
    for(int i=mid;i>=l;i--)
    {
        suml^=a[i];
        maxl=max(maxl,a[i]);
        minl=min(minl,a[i]);
        sumarray[i]=suml;
        minarray[i]=minl;
        maxarray[i]=maxl;
    }
    for(int i=mid+1;i<=r;i++)
    {
        sumr^=a[i];
        maxr=max(maxr,a[i]);
        minr=min(minr,a[i]);
        sumarray[i]=sumr;
        minarray[i]=minr;
        maxarray[i]=maxr;
    }
    //1.max left min left
    trie.clear();
    root[mid]=0;
    for(int i=mid+1;i<=r;i++)
        trie.insert(root[i],root[i-1],sumarray[i],30);
    pin1=mid+1;
    for(int i=mid;i>=l;i--)
    {
        while(pin1<=r&&minarray[pin1]>=minarray[i]&&maxarray[pin1]<=maxarray[i])pin1++;
        //¸æ·Ç£¬Ã»Ïëµ½£¬Ð´ÁËÏß¶ÎÊ÷É϶þ·Ö£¬×Ô±Õ
        int temp=minarray[i]^maxarray[i]^sumarray[i];
        ans=max(ans,temp);
        if(pin1>mid+1)ans=max(ans,trie.que(root[mid],root[pin1-1],temp));
    }
    //2.max right min right
    trie.clear();
    root[mid+1]=0;
    for(int i=mid;i>=l;i--)
        trie.insert(root[i],root[i+1],sumarray[i],30);
    pin1=mid;
    for(int i=mid+1;i<=r;i++)
    {
        while(pin1>=l&&minarray[pin1]>=minarray[i]&&maxarray[pin1]<=maxarray[i])pin1--;
        int temp=minarray[i]^maxarray[i]^sumarray[i];
        ans=max(ans,temp);
        if(pin1<mid)ans=max(ans,trie.que(root[mid+1],root[pin1+1],temp));
    }
    //3.max left min right
    trie.clear();
    root[mid]=0;
    for(int i=mid+1;i<=r;i++)
        trie.insert(root[i],root[i-1],sumarray[i]^minarray[i],30);
    pin1=pin2=mid+1;
    for(int i=mid;i>=l;i--)
    {
        while(pin1<=r&&minarray[i]<minarray[pin1])++pin1;
        while(pin2<=r&&maxarray[i]>=maxarray[pin2])++pin2;
        if(pin1<pin2)
        {
            int temp=sumarray[i]^maxarray[i];
            ans=max(ans,trie.que(root[pin1-1],root[pin2-1],temp));
        }
    }
    //4.max right min left
    trie.clear();
    root[mid+1]=0;
    for(int i=mid;i>=l;i--)
        trie.insert(root[i],root[i+1],sumarray[i]^minarray[i],30);
    pin1=pin2=mid;
    for(int i=mid+1;i<=r;i++)
    {
        while(pin1>=l&&minarray[i]<minarray[pin1])--pin1;
        while(pin2>=l&&maxarray[i]>=maxarray[pin2])--pin2;
        if(pin1>pin2)
        {
            int temp=sumarray[i]^maxarray[i];
            ans=max(ans,trie.que(root[pin1+1],root[pin2+1],temp));
        }
    }
    solve(l,mid);
    solve(mid+1,r);
    return;
}

2.CDQ分治

三维偏序问题。【模板】三维偏序(陌上花开) 数据结构板子题

void solve(int l,int r)//CDQ分治的思想就是算左边对右边的贡献
{
    if(l==r)return;
      int mid=(l+r)/2;
    solve(l,mid);solve(mid+1,r);
    int L=l,R=mid+1,tot=l;
    while(L<=mid&&R<=r)
    {
        if(p[L].b<=p[R].b)//将第二维小于的都放到树状数组中
        {
            add(p[L].c,1);
            Q[tot++]=p[L++];
        }
        else{
            Ans[p[R].id]=find(p[R].c);
            Q[tot++]=p[R++];
        }
    }
    while(L<=mid)
    {
        add(p[L].c,1);
        Q[tot++]=p[L++];        
    }
    while(R<=r)
    {
        Ans[p[R].id]=find(p[R].c);
        Q[tot++]=p[R++];      
    }
    for(int i=l;i<=mid;i++)add(p[i].c,-1);
    for(int i=l;i<=r;i++)p[i]=Q[i];//把第二维也排好序
}
sort(p+1,p+n+1);//排第一维
solve(1,n);//CDQ分治

3.分治FFT

【模板】分治 FFT

void CDQ_FFT(int *F,int *G,int l,int r)
{
    if(l==r)return;
    int mid=(l+r)/2;
    CDQ_FFT(F,G,l,mid);
    for(int i=l;i<=mid;i++)A[i-l]=F[i];
    for(int i=0;i<=r-l;i++)B[i]=G[i];
    int len=1,bit=0;
    while(len<=r-l+1)
    {
        len=len*2;
        bit++;
    }
    bit--;
    work(len,bit);
    for(int i=mid-l+1;i<=len;i++)A[i]=0;
    for(int i=r-l+1;i<=len;i++)B[i]=0;
    ntt(A,len,1);
    ntt(B,len,1);
    for(int i=0;i<len;i++)A[i]=1ll*A[i]*B[i]%mod;
    ntt(A,len,-1);
    for(int i=mid+1;i<=r;i++)F[i]=(F[i]+A[i-l])%mod;
    CDQ_FFT(F,G,mid+1,r);
}

[CTSC2018]青蕈领主

式子推导就不写了(我也不会)

这部分是每次在的时候加上就好。

对于后面的自己卷自己,我们需要特别处理。

板子,我们是考虑的贡献。

贡献就是

如今自己卷自己,发现如果当时,原先的卷法完全可以(因为

但当时,由于还没算出,无法卷。

解决方案:

当做已经算出,先卷进去。然后对于每一个的区间,由于如今是都算出了,我们在把之前没算的贡献加回去。

之前没算的贡献就是中各任选一个的和(仔细想想为啥是呢?,注意:递归),的总贡献是,两项加起来是 。把这两段做一个卷积即可。

注意:DP转移式子中是从开始的,前面写的一些,其实是

void CDQ(int l,int r)
{
    if(l==r)
    {
        f[l]=add(f[l],mul(f[l-1],l-1));
        return;
    }
    int mid=(l+r)/2; 
    CDQ(l,mid);
    if(l>2)
    {
        int lp=2,rp=min(l-1,r-l),len=1;
        while(len<=rp+mid-l)len=len*2;
        fill(A,A+len,0);
        fill(B,B+len,0);
        for(int i=lp;i<=rp;i++)A[i]=f[i];
        for(int i=l;i<=mid;i++)B[i-l]=f[i];
        NTT(A,len,1);NTT(B,len,1);
        for(int i=0;i<len;i++)C[i]=mul(A[i],B[i]);
        NTT(C,len,-1);
        for(int i=mid+1;i<=r;i++)
            f[i]=add(f[i],mul(C[i-l],i-2));
    }
    int len=1;
    while(len<=mid-l+mid-l)len=len*2;
    fill(A,A+len,0);
    fill(B,B+len,0);
    for(int i=l;i<=mid;i++)A[i-l]=f[i];
    for(int i=l;i<=mid;i++)B[i-l]=mul(i-1,f[i]);
    NTT(A,len,1);NTT(B,len,1);
    for(int i=0;i<len;i++)C[i]=mul(A[i],B[i]);
    NTT(C,len,-1);
    for(int i=mid+1;i<=r;i++)
        f[i]=add(f[i],C[i-l-l]);
    CDQ(mid+1,r);
}

4.整体二分

整体二分可以和很多东西配,比如并查集。

AT1998 Stamp Rally

int find(int x)
{
    if(fa[x]==x)return x;
    return find(fa[x]);
}
void merge(int x,int y)
{
    x=find(x),y=find(y);
    if(x==y)return;
    if(size[x]>size[y])swap(x,y);
    zhan[++tot]=x;
    fa[x]=y;
    size[y]+=size[x];
}
void Delete()
{
    while(tot)
    {
        int x=zhan[tot];
        size[fa[x]]-=size[x];
        fa[x]=x;
        tot--;
    }
}
void solve(int l,int r,int L,int R)
{
//    cout<<l<<" "<<r<<" "<<L<<" "<<R<<endl;
    if(L==R)
    {
        for(int i=l;i<=r;i++)Ans[Q[i].id]=L;
        merge(e[L].x,e[L].y);
        return;
    }
    int mid=(L+R)/2;
    int LL=l-1,RR=r+1;
    tot=0;
    for(int i=L;i<=mid;i++)
        merge(e[i].x,e[i].y);
    for(int i=l;i<=r;i++)
    {
        int x=find(Q[i].x),y=find(Q[i].y);
        if(x==y)
        {
            if(size[x]>=Q[i].z)xu[++LL]=Q[i];
            else xu[--RR]=Q[i];
        }
        else{
            if(size[x]+size[y]>=Q[i].z)xu[++LL]=Q[i];
            else xu[--RR]=Q[i];
        }
    }
    for(int i=l;i<=r;i++)Q[i]=xu[i];
    Delete();
    solve(l,LL,L,mid);
    solve(RR,r,mid+1,R);
}

12.矩阵

1.矩阵乘法

2.矩阵乘法优化DP

3.循环矩阵


初始矩阵就是原先矩阵的第一列

P3746 [六省联考2017]组合数问题

node operator * (node a,node b)
{
    for(int i=0;i<k;i++)c.m[i]=0;
    for(int i=0;i<k;i++)
        for(int j=0;j<k;j++)
            c.m[(i+j)%k]=(c.m[(i+j)%k]+a.m[i]*b.m[j])%mod;
    return c;
}
node kuai(node a,int b)
{
    node res;
    for(int i=0;i<k;i++)res.m[i]=0;
    res.m[0]=1;
    while(b)
    {
        if(b&1)res=res*a;
        b=b/2;
        a=a*a;
    }
    return res;
}

题目:

有一个长度为 的环,执行 次操作,在一次操作中,

对于每一个数,它变为它左边的数乘上 以及它本身以及它右边的数乘上 的和。

求最后每一个位置上的数是多少。(计算时左边和右边的数都是上一次的数)

13.点分治

1.统计边

2.点分树

点分树有一个性质:点分树上的两点的LCA,在原树两点的路径上。

所以我们把x-y的路径分成x-点分树上的LCA和点分树上的LCA-y的两条路径。

3.动态点分治

P3241 [HNOI2015]开店

点分树上树高log,由于是二叉树,那么只要和点u在不同儿子那么就能计算答案。

每个节点都维护向下能到哪些点和距离,然后就可以二分了。

void dfs1(int u,int fa)//求出每个点的size 
{
    size[u]=1;
    for(int i=head[u];i;i=edge[i].next)
    {
        int v=edge[i].too;
        if(v==fa)continue;
        if(vis[v])continue;
        dfs1(v,u);
        size[u]+=size[v];
    }
}
void dfs2(int u,int fa)//找到重心 
{
    int mx=sum-size[u];
    for(int i=head[u];i;i=edge[i].next)
    {
        int v=edge[i].too;
        if(v==fa)continue;
        if(vis[v])continue;
        dfs2(v,u);
        mx=max(mx,size[v]);
    }
    if(mx<ma){ma=mx,rt=u;}
}
void dfs3(int u,int fat,int g,int t)//维护fa和ans 
{
    fa[u].push_back((node){g,dep[u],t});
    ans[g][t].push_back((Node){a[u],1,dep[u]});
    for(int i=head[u];i;i=edge[i].next)
    {
        int v=edge[i].too;
        if(v==fat)continue;
        if(vis[v])continue;
        dep[v]=dep[u]+edge[i].zh;
        dfs3(v,u,g,t);
    }
}
void solve(int u)
{
    dfs1(u,0);
    if(size[u]==1)
    {
        vis[u]=1;
        fa[u].push_back((node){u,0,-1});
        return;
    }
    sum=size[u];
    ma=0x3f3f3f3f;
    dfs2(u,0);
    vis[rt]=1;
    fa[rt].push_back((node){rt,0,-1});
    for(int i=head[rt],t=0;i;i=edge[i].next)
    {
        int v=edge[i].too;
        if(vis[v])continue;
        dep[v]=edge[i].zh;
        dfs3(v,0,rt,t);
        ans[rt][t].push_back((Node){0x3f3f3f3f,0,0});
        sort(ans[rt][t].begin(),ans[rt][t].end());
        for(int j=ans[rt][t].size()-2;j>=0;j--)
        {
            ans[rt][t][j].gs+=ans[rt][t][j+1].gs;
            ans[rt][t][j].dis+=ans[rt][t][j+1].dis;
        }
        t++;
    }
    for(int i=head[rt];i;i=edge[i].next)
    {
        int v=edge[i].too;
        if(vis[v])continue;
        solve(v);
    }
}
void query(int l,int r,int u)
{
    res=0;
    vector<Node>::iterator L,R;
    for(int i=0;i<fa[u].size();i++)
    {
        int f=fa[u][i].f;
        for(int j=0;j<=2;j++)//由于每个点的度<=3 
        {
            if(j==fa[u][i].id||ans[f][j].empty())continue;
            L=lower_bound(ans[f][j].begin(),ans[f][j].end(),(Node){l,0,0});
            R=upper_bound(ans[f][j].begin(),ans[f][j].end(),(Node){r,0,0});
            res+=fa[u][i].dis*(L->gs-R->gs)+L->dis-R->dis;
        }
        if(l<=a[f]&&a[f]<=r)res+=fa[u][i].dis;
    }
}

P3345 [ZJOI2015]幻想乡战略游戏

动态修改点权,求带权重心。

void Addedge(int u,int v,int w)
{
    G[++tot].u=u;G[tot].v=v;G[tot].w=w;G[tot].next=head[u];head[u]=tot;
}
struct node{
    int head[N],cnt,tot;
    int st[N<<2][21],dis[N],id[N<<1];
    struct edge{int u,v,next,w;}E[2*N];
    inline void addedge(int u,int v,int w)
    {
        E[++tot].u=u;E[tot].v=v;E[tot].w=w;E[tot].next=head[u];head[u]=tot;
        E[++tot].u=v;E[tot].v=u;E[tot].w=w;E[tot].next=head[v];head[v]=tot;
    }
    int getdis(int u,int v)
    {
        if(id[u]>id[v])swap(u,v);
        int k=Log[id[v]-id[u]+1];
        return dis[u]+dis[v]-2*min(st[id[u]][k],st[id[v]-(1ll<<k)+1][k]);
    }
    void dfs(int u,int fa)
    {
        st[++cnt][0]=dis[u];
        id[u]=cnt;
        for(int i=head[u];i;i=E[i].next)
        {
            int v=E[i].v;
            if(v==fa)continue;
            dis[v]=dis[u]+E[i].w;
            dfs(v,u);
            st[++cnt][0]=dis[u];
        }
    }
    void initrmq()
    {
        memset(id,0,sizeof(id));
        cnt=0;tot=0;dis[1]=0;
        dfs(1,0);
        for(int j=1;(1ll<<j)<=cnt;j++)
            for(int i=1;i+(1ll<<j)-1<=cnt&&i<=cnt;i++)
                st[i][j]=min(st[i][j-1],st[i+(1ll<<(j-1))][j-1]);
    }
}T;
void getroot(int u,int fa)
{
    size[u]=1;
    f[u]=0;
    for(int i=T.head[u];i;i=T.E[i].next)
    {
        int v=T.E[i].v;
        if(vis[v]||v==fa)continue;
        getroot(v,u);
        size[u]+=size[v];
        f[u]=max(f[u],size[v]);
    }
    f[u]=max(f[u],sum-size[u]);
    if(f[u]<f[rt])rt=u;
}
void work(int u,int fa)
{
    vis[u]=1;par[u]=fa;
    for(int i=T.head[u];i;i=T.E[i].next)
    {
        int v=T.E[i].v;
        if(vis[v])continue;
        sum=size[v];
        f[0]=size[v];
        rt=0;
        getroot(v,0);
        Addedge(u,rt,v);
        work(rt,u);
    }
}
void change(int u,int val)
{
    sumv[u]+=val;
    for(int i=u;par[i];i=par[i])
    {
        int dist=T.getdis(par[i],u);
        dis1[par[i]]+=dist*val;
        dis2[i]+=dist*val;
        sumv[par[i]]+=val;
    }
}
int calc(int u)
{
    int ans=dis1[u];
    for(int i=u;par[i];i=par[i])
    {
        int dist=T.getdis(par[i],u);
        ans+=dis1[par[i]]-dis2[i];
        ans+=dist*(sumv[par[i]]-sumv[i]);
    }
    return ans;
}
int query(int u)
{
    int ans=calc(u);
    for(int i=head[u];i;i=G[i].next)
    {
        int tmp=calc(G[i].w);
        if(tmp<ans)return query(G[i].v);
    }
    return ans;
}  

P6329 【模板】点分树 | 震波

点分树最简单的写法就是维护两个数据结构(一般是vector)

一个记录点分树上以点u为根的子树与u的信息(到u的距离)

一个记录点分树上以点u为根的子树与fa[u]的信息(到法fa[u]的距离)

然后就可以通过容斥(减一减),求出在fa[u]的子树中且不在u的子树中的信息(到某某点的距离)

除了这种写法,就要重构树(使度数<=3)了

这题还有一个难点就是这两个数据结构是树状数组

那么就要动态开点(vector),有点麻烦

距离为0的点(本身),树状数组不好处理,我们就先不加入,到要用的时候直接加

#include<bits/stdc++.h>
using namespace std;
#define next Next
//#define gc getchar
//#define int long long
const int N=2e6+5;
int n,m,top,t,sum,rt,lastans,sz[N],fa[N],vis[N],ru[N],id[N],dep[N],a[N];
int head[N],ma[N],Log[N],size[N],st[N][21];
struct node{
    int too,next;
}edge[N*2];
char buf[1<<21],*p1=buf,*p2=buf;
inline int gc(){return p1==p2&&(p2=(p1=buf)+fread(buf,1,1<<21,stdin),p1==p2)?EOF:*p1++;}
inline int read()
{
    int ret=0,f=0;char c=gc();
    while(!isdigit(c)){if(c=='-')f=1;c=gc();}
    while(isdigit(c)){ret=ret*10+c-48;c=gc();}
    if(f)return -ret;return ret;
}
void write(int x)
{
    if(x<10){putchar(x+'0');return;}
    write(x/10);
    putchar(x%10+'0');
}
int lowbit(int x)
{
    return x&-x;
}
struct Tree{
    vector<int>c;
    void init(int n)
    {
        for(int i=0;i<=n;i++)
            c.push_back(0);
    }
    void change(int x,int y)
    {
        int n=c.size()-1;
        while(x<=n)
        {
            c[x]+=y;
            x+=lowbit(x);
        }
    }
    int find(int x)
    {
        int res=0;
        if(x>c.size()-1)x=c.size()-1;//注意 
        while(x>0)
        {
            res+=c[x];
            x-=lowbit(x);
        }
        return res;
    }
}T1[N],T2[N];
void add(int a,int b)
{
    edge[++top].too=b;edge[top].next=head[a];head[a]=top;
}
void dfs(int u,int fa,int d)
{
    ru[u]=++t;
    id[t]=u;
    dep[t]=d;
    for(int i=head[u];i;i=edge[i].next)
    {
        int v=edge[i].too;
        if(v==fa)continue;
        dfs(v,u,d+1);
        id[++t]=u;
        dep[t]=d;
    }
}
int LCA(int x,int y)
{
    int l=ru[x],r=ru[y];
    if(l>r)swap(l,r);
    int k=Log[r-l+1];
    x=st[l][k];y=st[r-(1<<k)+1][k];
    if(dep[x]<=dep[y])return id[x]; 
    return id[y];
    //注意LCA是要转化为id的 
}
int dis(int x,int y)
{
    int lca=LCA(x,y);
    return dep[ru[x]]+dep[ru[y]]-2*dep[ru[lca]];
    //注意这里的dep[x]是编号为x的点的深度,不是点x的深度 
}
void getrt(int u,int fa)
{
    ma[u]=0;size[u]=1;
    for(int i=head[u];i;i=edge[i].next)
    {
        int v=edge[i].too;
        if(v==fa||vis[v])continue;
        getrt(v,u);
        size[u]+=size[v];
        ma[u]=max(ma[u],size[v]);
    }
    ma[u]=max(ma[u],sum-size[u]);
    if(ma[u]<ma[rt])rt=u;
}
void solve(int u)
{
    vis[u]=1;
    for(int i=head[u];i;i=edge[i].next)
    {
        int v=edge[i].too;
        if(vis[v])continue;
        ma[rt=0]=sum=size[v];
        getrt(v,0);
        fa[rt]=u;
        sz[rt]=size[v];//注意rt的大小是size[v] 
        T1[rt].init(size[v]);//注意不能全开n,否则变n^2 
        T2[rt].init(size[v]); 
        solve(rt);
    }
}
void work(int x,int y)
{
    for(int i=fa[x];i;i=fa[i])
    {
        int len=dis(x,i);
        if(len<=sz[i])T1[i].change(len,y);
    }
    for(int i=x;i;i=fa[i])
    {
        if(fa[i]==0)break;
        int len=dis(x,fa[i]);
        if(len<=sz[i])T2[i].change(len,y);
    }
}
signed main()
{
    Log[0]=-1;
    for(int i=1;i<=200000;i++)Log[i]=Log[i/2]+1;
    n=read();m=read();
    for(int i=1;i<=n;i++)a[i]=read();
    for(int i=1;i<n;i++)
    {
        int x=read(),y=read();
        add(x,y);
        add(y,x);
    }
    dfs(1,0,0);
    for(int i=1;i<=t;i++)st[i][0]=i;
    for(int i=1;i<=Log[t];i++)
        for(int j=1;j<=t-(1<<i)+1;j++)
        {
            int x=st[j][i-1],y=st[j+(1<<(i-1))][i-1];
            if(dep[x]<=dep[y])st[j][i]=x;
            else st[j][i]=y;
        }
    ma[rt=0]=sum=n;
    getrt(1,0);
    sz[rt]=n;
    T1[rt].init(n);
    T2[rt].init(n);
    solve(rt);
    for(int i=1;i<=n;i++)
        work(i,a[i]);
    while(m--)
    {
        int opt=read(),x=read()^lastans,y=read()^lastans;
        if(opt==0)
        {
            int res=T1[x].find(y)+a[x];
            //注意为了方便树状数组,没有把距离为0的点放进去 
            for(int i=x;i;i=fa[i])
            {
                if(fa[i]==0)break;
                int len=dis(x,fa[i]);
                if(y-len>=0)
                {
                    res+=T1[fa[i]].find(y-len)+a[fa[i]];
                    res-=T2[i].find(y-len);
                }
            }
            write(lastans=res);
            putchar('\n'); 
        }
        else{
            work(x,y-a[x]);
            a[x]=y;
        }
    }
    return 0;
}
/*
5 1
1 3 5 7 9
1 2
2 3
3 4
3 5
0 2 1
*/
/*
点分树最简单的写法就是维护两个数据结构(一般是vector)
一个记录点分树上以点u为根的子树与u的信息(到u的距离) 
一个记录点分树上以点u为根的子树与fa[u]的信息(到法fa[u]的距离)
然后就可以通过容斥(减一减),求出在fa[u]的子树中且不在u的子树中的信息(到某某点的距离)
除了这种写法,就要重构树(使度数<=3)了
这题还有一个难点就是这两个数据结构是树状数组
那么就要动态开点(vector),有点麻烦 
*/

14.图论

1.最短路

2.最小生成树

3.拓扑排序

4.差分约束

条件形如:,我们可以看成连了一条长度为的有向边,若存在正环则无解

P3275 [SCOI2011]糖果

有源点,就从源点spfa。

void spfa()
{
    while(l<=r)
    {
        int u=q[l++];
        vis[u]=false;
        cnt[u]++;
        if(cnt[u]==n){cout<<-1;exit(0);}
        for(int i=head[u];i;i=edge[i].next)
        {
            int v=edge[i].too,w=edge[i].zh;
            if(dis[v]<dis[u]+w)
            {
                dis[v]=dis[u]+w;
                if(!vis[v])
                {
                    q[++r]=v;
                    vis[v]=true;
                }
            }
        }
    }
}
for(int i=1;i<=m;i++)
{
    scanf("%d%d%d",&x,&a,&b);
    if(x==1){add(a,b,0);add(b,a,0);}
    else if(x==2)add(a,b,1);
    else if(x==3)add(b,a,0);
    else if(x==4)add(b,a,1);
    else if(x==5)add(a,b,0);
    if(x%2==0&&a==b)
    {
        cout<<-1;
        return 0;
    }
}
l=1;
for(int i=1;i<=n;i++)
{
    dis[i]=1;
    vis[i]=true;
    q[++r]=i;
}
spfa();

P2294 [HNOI2005]狡猾的商人

这题没源点,那就每个联通块spfa一次

for(int i=1;i<=m;i++)
{
    int x=read(),y=read(),z=read();
    add(x-1,y,z);
    add(y,x-1,-z);
}
for(int i=0;i<=n;i++)
    if(gs[i]==0)
    {
        if(!spfa(i))
        {
            puts("false");
            goto xjh;
        }
    }
puts("true");
xjh:;

5.欧拉回路

只要在dfs时加一个走过的边不走的优化,时间复杂度就是

P1341 无序字母对

void dfs(int x)
{
    for(int &i=head[x];i;i=edge[i].next)
    {
        int v=edge[i].too;
        if(vis[x][v])continue;
        vis[x][v]=vis[v][x]=1;
        dfs(v);
    }
    c[t--]=x;
}
int find(int x)
{
    if(fa[x]==x)return x;
    fa[x]=find(fa[x]);
    return fa[x];
}
int main()
{
    scanf("%d",&n);
    for(int i=0;i<=500;i++)fa[i]=i;
    for(int i=1;i<=n;i++)
    {
        cin>>a>>b;
        g[a][b]=g[b][a]=1;
        int xx=find(a),yy=find(b);
        fa[xx]=yy;
        du[a]++;
        du[b]++;
    }
    for(int i=0;i<=500;i++)
        for(int j=500;j>=0;j--)
            if(g[i][j])add(i,j);
    for(int i=0;i<=500;i++)
        if(fa[i]==i&&du[i])k++;
    if(k!=1){puts("No Solution");return 0;}
    k=0;
    for(int i=0;i<=500;i++)
        if(du[i]%2==1)
        {
            k++;
            if(!S)S=i;
        }
    if(S==0)
        for(int i=0;i<500;i++)
            if(du[i]){S=i;break;}
    if(k!=0&&k!=2){puts("No Solution");return 0;}
    t=n;
    dfs(S);
    printf("%c",S);
    for(int i=1;i<=n;i++)printf("%c",c[i]);
    return 0;
}

6.二分图

P1894 [USACO4.2]完美的牛栏The Perfect Stall

bool find(int x)
{
    for(int i=head[x];i;i=edge[i].next)
    {
        int v=edge[i].too;
        if(!b[v])
        {
            b[v]=true;
            if(!a[v]||find(a[v]))
            {
                a[v]=x;
                return true;
            }
        }
    }
    return false;
}
int main()
{
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++)
    {
        scanf("%d",&k);
        for(int j=1;j<=k;j++)
        {
            scanf("%d",&x);
            add(i,x+n);
        }
    }
    for(int i=1;i<=n;i++)
    {
        memset(b,false,sizeof(b));
        if(find(i))ans++;
    }
    cout<<ans;
    return 0;
}

7.网络流

xuxuxuxuxu 文章被收录于专栏

信息学竞赛

全部评论

相关推荐

不愿透露姓名的神秘牛友
07-09 12:02
ssob上原来真有BOSS啊
硫蛋蛋:这种也是打工的,只不是是给写字楼房东打工
点赞 评论 收藏
分享
07-11 11:15
中南大学 Java
好可爱的hr姐姐哈哈哈哈
黑皮白袜臭脚体育生:兄弟们貂蝉在一起,吕布开了
点赞 评论 收藏
分享
下个早班:秒挂就是不缺人
点赞 评论 收藏
分享
机械打工仔:有说的你怀疑一下就行了,直接问也太实诚了
点赞 评论 收藏
分享
不愿透露姓名的神秘牛友
07-10 14:10
啊啊啊啊好幸福,妈妈是我找工作发疯前的一束光
黑皮白袜臭脚体育生:看了这篇帖子之后已经第一百次质问老妈,仍然没有得到我的老妈是老板的回答
点赞 评论 收藏
分享
评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务